package items import ( "database/sql" "fmt" "log" "strconv" "strings" "sync/atomic" "time" ) // ItemDatabase handles all database operations for items type ItemDatabase struct { db *sql.DB queries map[string]*sql.Stmt loadedItems map[int32]bool } // NewItemDatabase creates a new item database manager func NewItemDatabase(db *sql.DB) *ItemDatabase { idb := &ItemDatabase{ db: db, queries: make(map[string]*sql.Stmt), loadedItems: make(map[int32]bool), } // Prepare commonly used queries idb.prepareQueries() return idb } // prepareQueries prepares all commonly used SQL queries func (idb *ItemDatabase) prepareQueries() { queries := map[string]string{ "load_items": ` 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 `, "load_item_stats": ` SELECT item_id, stat_type, stat_subtype, value, stat_name, level FROM item_mod_stats WHERE item_id = ? `, "load_item_effects": ` SELECT item_id, effect, percentage, subbulletflag FROM item_effects WHERE item_id = ? `, "load_item_appearances": ` SELECT item_id, type, red, green, blue, highlight_red, highlight_green, highlight_blue FROM item_appearances WHERE item_id = ? `, "load_item_level_overrides": ` SELECT item_id, adventure_class, tradeskill_class, level FROM item_levels_override WHERE item_id = ? `, "load_item_mod_strings": ` SELECT item_id, stat_string FROM item_mod_strings WHERE item_id = ? `, "load_character_items": ` 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 = ? `, "save_character_item": ` 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, "delete_character_item": ` DELETE FROM character_items WHERE char_id = ? AND unique_id = ? `, "delete_character_items": ` DELETE FROM character_items WHERE char_id = ? `, } for name, query := range queries { if stmt, err := idb.db.Prepare(query); err != nil { log.Printf("Failed to prepare query %s: %v", name, err) } else { idb.queries[name] = stmt } } } // LoadItems loads all items from the database into the master item list func (idb *ItemDatabase) LoadItems(masterList *MasterItemList) error { log.Printf("Loading items from database...") stmt := idb.queries["load_items"] if stmt == nil { return fmt.Errorf("load_items query not prepared") } rows, err := stmt.Query() if err != nil { return fmt.Errorf("failed to query items: %v", err) } defer rows.Close() itemCount := 0 for rows.Next() { item, err := idb.scanItemFromRow(rows) if err != nil { log.Printf("Error scanning item from row: %v", err) continue } // Load additional item data if err := idb.loadItemDetails(item); err != nil { log.Printf("Error loading details for item %d: %v", item.Details.ItemID, err) continue } masterList.AddItem(item) idb.loadedItems[item.Details.ItemID] = true itemCount++ } if err = rows.Err(); err != nil { return fmt.Errorf("error iterating item rows: %v", err) } log.Printf("Loaded %d items from database", itemCount) return nil } // scanItemFromRow scans a database row into an Item struct func (idb *ItemDatabase) scanItemFromRow(rows *sql.Rows) (*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) var createdStr string var scriptName, luaScript sql.NullString err := rows.Scan( &item.Details.ItemID, &item.Details.SOEId, &item.Name, &item.Description, &item.Details.Icon, &item.Details.ClassicIcon, &item.GenericInfo.AppearanceID, // icon_heroic_op &item.GenericInfo.AppearanceID, // icon_heroic_op2 (duplicate) &item.GenericInfo.AppearanceID, // icon_id &item.GenericInfo.AppearanceID, // icon_backdrop &item.GenericInfo.AppearanceID, // icon_border &item.GenericInfo.AppearanceRed, // icon_tint_red &item.GenericInfo.AppearanceGreen, // icon_tint_green &item.GenericInfo.AppearanceBlue, // icon_tint_blue &item.Details.Tier, &item.Details.RecommendedLevel, &item.SellPrice, // success_sellback &item.StackCount, &item.GenericInfo.ShowName, &item.GenericInfo.ItemFlags, &item.GenericInfo.ItemFlags2, &item.GenericInfo.CreatorFlag, &item.GenericInfo.Condition, &item.GenericInfo.Weight, &item.GenericInfo.SkillReq1, &item.GenericInfo.SkillReq2, &item.GenericInfo.SkillMin, &item.GenericInfo.ItemType, &item.GenericInfo.AppearanceID, &item.GenericInfo.AppearanceRed, &item.GenericInfo.AppearanceGreen, &item.GenericInfo.AppearanceBlue, &item.GenericInfo.AppearanceHighlightRed, &item.GenericInfo.AppearanceHighlightGreen, &item.GenericInfo.AppearanceHighlightBlue, &item.GenericInfo.Collectable, &item.GenericInfo.OffersQuestID, &item.GenericInfo.PartOfQuestID, &item.GenericInfo.MaxCharges, &item.GenericInfo.AdventureClasses, &item.GenericInfo.TradeskillClasses, &item.GenericInfo.AdventureDefaultLevel, &item.GenericInfo.TradeskillDefaultLevel, &item.GenericInfo.Usable, &item.GenericInfo.Harvest, &item.GenericInfo.BodyDrop, &item.GenericInfo.PvPDescription, &item.GenericInfo.MercOnly, &item.GenericInfo.MountOnly, &item.GenericInfo.SetID, &item.GenericInfo.CollectableUnk, &item.GenericInfo.TransmutedMaterial, &item.BrokerPrice, &item.SellPrice, &item.MaxSellValue, &createdStr, &scriptName, &luaScript, ) if err != nil { return nil, fmt.Errorf("failed to scan item row: %v", err) } // Set lowercase name for searching item.LowerName = strings.ToLower(item.Name) // Parse created timestamp if createdStr != "" { if created, err := time.Parse("2006-01-02 15:04:05", createdStr); err == nil { item.Created = created } } // Set script names if scriptName.Valid { item.ItemScript = scriptName.String } if luaScript.Valid { item.ItemScript = luaScript.String // Lua script takes precedence } // Generate unique ID item.Details.UniqueID = NextUniqueItemID() return item, nil } // loadItemDetails loads all additional details for an item func (idb *ItemDatabase) loadItemDetails(item *Item) error { // Load item stats if err := idb.loadItemStats(item); err != nil { return fmt.Errorf("failed to load stats: %v", err) } // Load item effects if err := idb.loadItemEffects(item); err != nil { return fmt.Errorf("failed to load effects: %v", err) } // Load item appearances if err := idb.loadItemAppearances(item); err != nil { return fmt.Errorf("failed to load appearances: %v", err) } // Load level overrides if err := idb.loadItemLevelOverrides(item); err != nil { return fmt.Errorf("failed to load level overrides: %v", err) } // Load modifier strings if err := idb.loadItemModStrings(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(item *Item) error { stmt := idb.queries["load_item_stats"] if stmt == nil { return fmt.Errorf("load_item_stats query not prepared") } rows, err := stmt.Query(item.Details.ItemID) if err != nil { return err } defer rows.Close() for rows.Next() { var stat ItemStat var itemID int32 var statName sql.NullString err := rows.Scan(&itemID, &stat.StatType, &stat.StatSubtype, &stat.Value, &statName, &stat.Level) if err != nil { return err } if statName.Valid { stat.StatName = statName.String } item.ItemStats = append(item.ItemStats, &stat) } return rows.Err() } // loadItemEffects loads item effects and descriptions func (idb *ItemDatabase) loadItemEffects(item *Item) error { stmt := idb.queries["load_item_effects"] if stmt == nil { return fmt.Errorf("load_item_effects query not prepared") } rows, err := stmt.Query(item.Details.ItemID) if err != nil { return err } defer rows.Close() for rows.Next() { var effect ItemEffect var itemID int32 err := rows.Scan(&itemID, &effect.Effect, &effect.Percentage, &effect.SubBulletFlag) if err != nil { return err } item.ItemEffects = append(item.ItemEffects, &effect) } return rows.Err() } // loadItemAppearances loads item appearance data func (idb *ItemDatabase) loadItemAppearances(item *Item) error { stmt := idb.queries["load_item_appearances"] if stmt == nil { return fmt.Errorf("load_item_appearances query not prepared") } rows, err := stmt.Query(item.Details.ItemID) if err != nil { return err } defer rows.Close() // Only process the first appearance if rows.Next() { var appearance ItemAppearance var itemID int32 err := rows.Scan(&itemID, &appearance.Type, &appearance.Red, &appearance.Green, &appearance.Blue, &appearance.HighlightRed, &appearance.HighlightGreen, &appearance.HighlightBlue) if err != nil { return err } // Set the appearance data on the item item.GenericInfo.AppearanceID = appearance.Type item.GenericInfo.AppearanceRed = appearance.Red item.GenericInfo.AppearanceGreen = appearance.Green item.GenericInfo.AppearanceBlue = appearance.Blue item.GenericInfo.AppearanceHighlightRed = appearance.HighlightRed item.GenericInfo.AppearanceHighlightGreen = appearance.HighlightGreen item.GenericInfo.AppearanceHighlightBlue = appearance.HighlightBlue } return rows.Err() } // loadItemLevelOverrides loads item level overrides for different classes func (idb *ItemDatabase) loadItemLevelOverrides(item *Item) error { stmt := idb.queries["load_item_level_overrides"] if stmt == nil { return fmt.Errorf("load_item_level_overrides query not prepared") } rows, err := stmt.Query(item.Details.ItemID) if err != nil { return err } defer rows.Close() for rows.Next() { var override ItemLevelOverride var itemID int32 err := rows.Scan(&itemID, &override.AdventureClass, &override.TradeskillClass, &override.Level) if err != nil { return err } item.ItemLevelOverrides = append(item.ItemLevelOverrides, &override) } return rows.Err() } // loadItemModStrings loads item modifier strings func (idb *ItemDatabase) loadItemModStrings(item *Item) error { stmt := idb.queries["load_item_mod_strings"] if stmt == nil { return fmt.Errorf("load_item_mod_strings query not prepared") } rows, err := stmt.Query(item.Details.ItemID) if err != nil { return err } defer rows.Close() for rows.Next() { var statString ItemStatString var itemID int32 err := rows.Scan(&itemID, &statString.StatString) if err != nil { return err } item.ItemStringStats = append(item.ItemStringStats, &statString) } return rows.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 all prepared statements and the database connection func (idb *ItemDatabase) Close() error { for name, stmt := range idb.queries { if err := stmt.Close(); err != nil { log.Printf("Error closing statement %s: %v", name, err) } } return nil }