372 lines
12 KiB
Go
372 lines
12 KiB
Go
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
|
|
} |