472 lines
13 KiB
Go
472 lines
13 KiB
Go
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
|
|
} |