package loot import ( "context" "fmt" "log" "sync" "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // LootDatabase handles all database operations for the loot system type LootDatabase struct { pool *sqlitex.Pool lootTables map[int32]*LootTable spawnLoot map[int32][]int32 // spawn_id -> []loot_table_id globalLoot []*GlobalLoot mutex sync.RWMutex } // NewLootDatabase creates a new loot database manager func NewLootDatabase(pool *sqlitex.Pool) *LootDatabase { ldb := &LootDatabase{ pool: pool, lootTables: make(map[int32]*LootTable), spawnLoot: make(map[int32][]int32), globalLoot: make([]*GlobalLoot, 0), } return ldb } // LoadAllLootData loads all loot data from the database func (ldb *LootDatabase) LoadAllLootData() error { log.Printf("%s Loading loot data from database...", LogPrefixDatabase) // Load loot tables first if err := ldb.loadLootTables(); err != nil { return fmt.Errorf("failed to load loot tables: %v", err) } // Load loot drops for each table if err := ldb.loadLootDrops(); err != nil { return fmt.Errorf("failed to load loot drops: %v", err) } // Load spawn loot assignments if err := ldb.loadSpawnLoot(); err != nil { return fmt.Errorf("failed to load spawn loot: %v", err) } // Load global loot configuration if err := ldb.loadGlobalLoot(); err != nil { return fmt.Errorf("failed to load global loot: %v", err) } ldb.mutex.RLock() tableCount := len(ldb.lootTables) spawnCount := len(ldb.spawnLoot) globalCount := len(ldb.globalLoot) ldb.mutex.RUnlock() log.Printf("%s Loaded %d loot tables, %d spawn assignments, %d global loot entries", LogPrefixDatabase, tableCount, spawnCount, globalCount) return nil } // loadLootTables loads all loot tables from the database func (ldb *LootDatabase) loadLootTables() error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) query := ` SELECT id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability FROM loottable ORDER BY id ` ldb.mutex.Lock() defer ldb.mutex.Unlock() // Clear existing tables ldb.lootTables = make(map[int32]*LootTable) err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { table := &LootTable{ Drops: make([]*LootDrop, 0), } table.ID = int32(stmt.ColumnInt64(0)) table.Name = stmt.ColumnText(1) table.MinCoin = int32(stmt.ColumnInt64(2)) table.MaxCoin = int32(stmt.ColumnInt64(3)) table.MaxLootItems = int16(stmt.ColumnInt64(4)) table.LootDropProbability = float32(stmt.ColumnFloat(5)) table.CoinProbability = float32(stmt.ColumnFloat(6)) ldb.lootTables[table.ID] = table return nil }, }) return err } // loadLootDrops loads all loot drops for the loaded loot tables func (ldb *LootDatabase) loadLootDrops() error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) query := ` SELECT loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id FROM lootdrop WHERE loot_table_id = ? ORDER BY probability DESC ` ldb.mutex.Lock() defer ldb.mutex.Unlock() for tableID, table := range ldb.lootTables { err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{tableID}, ResultFunc: func(stmt *sqlite.Stmt) error { drop := &LootDrop{} drop.LootTableID = int32(stmt.ColumnInt64(0)) drop.ItemID = int32(stmt.ColumnInt64(1)) drop.ItemCharges = int16(stmt.ColumnInt64(2)) equipItem := int8(stmt.ColumnInt64(3)) drop.Probability = float32(stmt.ColumnFloat(4)) drop.NoDropQuestCompletedID = int32(stmt.ColumnInt64(5)) drop.EquipItem = equipItem == 1 table.Drops = append(table.Drops, drop) return nil }, }) if err != nil { log.Printf("%s Failed to query loot drops for table %d: %v", LogPrefixDatabase, tableID, err) } } return nil } // loadSpawnLoot loads spawn to loot table assignments func (ldb *LootDatabase) loadSpawnLoot() error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) query := ` SELECT spawn_id, loottable_id FROM spawn_loot ORDER BY spawn_id ` ldb.mutex.Lock() defer ldb.mutex.Unlock() // Clear existing spawn loot ldb.spawnLoot = make(map[int32][]int32) err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { spawnID := int32(stmt.ColumnInt64(0)) lootTableID := int32(stmt.ColumnInt64(1)) ldb.spawnLoot[spawnID] = append(ldb.spawnLoot[spawnID], lootTableID) return nil }, }) return err } // loadGlobalLoot loads global loot configuration func (ldb *LootDatabase) loadGlobalLoot() error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) query := ` SELECT type, loot_table, value1, value2, value3, value4 FROM loot_global ORDER BY type, value1 ` ldb.mutex.Lock() defer ldb.mutex.Unlock() // Clear existing global loot ldb.globalLoot = make([]*GlobalLoot, 0) err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { lootType := stmt.ColumnText(0) tableID := int32(stmt.ColumnInt64(1)) value1 := int32(stmt.ColumnInt64(2)) value2 := int32(stmt.ColumnInt64(3)) value3 := int32(stmt.ColumnInt64(4)) // value4 := int32(stmt.ColumnInt64(5)) // unused global := &GlobalLoot{ TableID: tableID, } // Parse loot type and values switch lootType { case "level": global.Type = GlobalLootTypeLevel global.MinLevel = int8(value1) global.MaxLevel = int8(value2) global.LootTier = value3 case "race": global.Type = GlobalLootTypeRace global.Race = int16(value1) global.LootTier = value2 case "zone": global.Type = GlobalLootTypeZone global.ZoneID = value1 global.LootTier = value2 default: log.Printf("%s Unknown global loot type: %s", LogPrefixDatabase, lootType) return nil // Continue processing } ldb.globalLoot = append(ldb.globalLoot, global) return nil }, }) return err } // GetLootTable returns a loot table by ID (thread-safe) func (ldb *LootDatabase) GetLootTable(tableID int32) *LootTable { ldb.mutex.RLock() defer ldb.mutex.RUnlock() return ldb.lootTables[tableID] } // GetSpawnLootTables returns all loot table IDs for a spawn (thread-safe) func (ldb *LootDatabase) GetSpawnLootTables(spawnID int32) []int32 { ldb.mutex.RLock() defer ldb.mutex.RUnlock() tables := ldb.spawnLoot[spawnID] if tables == nil { return nil } // Return a copy to prevent external modification result := make([]int32, len(tables)) copy(result, tables) return result } // GetGlobalLootTables returns applicable global loot tables for given parameters func (ldb *LootDatabase) GetGlobalLootTables(level int16, race int16, zoneID int32) []*GlobalLoot { ldb.mutex.RLock() defer ldb.mutex.RUnlock() var result []*GlobalLoot for _, global := range ldb.globalLoot { switch global.Type { case GlobalLootTypeLevel: if level >= int16(global.MinLevel) && level <= int16(global.MaxLevel) { result = append(result, global) } case GlobalLootTypeRace: if race == global.Race { result = append(result, global) } case GlobalLootTypeZone: if zoneID == global.ZoneID { result = append(result, global) } } } return result } // AddLootTable adds a new loot table to the database func (ldb *LootDatabase) AddLootTable(table *LootTable) error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) // Use a savepoint for transaction support defer sqlitex.Save(conn)(&err) query := `INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability) VALUES (?, ?, ?, ?, ?, ?, ?)` err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ table.ID, table.Name, table.MinCoin, table.MaxCoin, table.MaxLootItems, table.LootDropProbability, table.CoinProbability, }, }) if err != nil { return fmt.Errorf("failed to insert loot table: %v", err) } // Add drops if any for _, drop := range table.Drops { if err := ldb.addLootDropWithConn(conn, drop); err != nil { log.Printf("%s Failed to add loot drop for table %d: %v", LogPrefixDatabase, table.ID, err) } } // Update in-memory cache ldb.mutex.Lock() ldb.lootTables[table.ID] = table ldb.mutex.Unlock() log.Printf("%s Added loot table %d (%s) with %d drops", LogPrefixDatabase, table.ID, table.Name, len(table.Drops)) return nil } // AddLootDrop adds a new loot drop to the database func (ldb *LootDatabase) AddLootDrop(drop *LootDrop) error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) return ldb.addLootDropWithConn(conn, drop) } // addLootDropWithConn adds a loot drop using an existing connection func (ldb *LootDatabase) addLootDropWithConn(conn *sqlite.Conn, drop *LootDrop) error { equipItem := int8(0) if drop.EquipItem { equipItem = 1 } query := `INSERT INTO lootdrop (loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id) VALUES (?, ?, ?, ?, ?, ?)` err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ drop.LootTableID, drop.ItemID, drop.ItemCharges, equipItem, drop.Probability, drop.NoDropQuestCompletedID, }, }) return err } // UpdateLootTable updates an existing loot table func (ldb *LootDatabase) UpdateLootTable(table *LootTable) error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) // Use a savepoint for transaction support defer sqlitex.Save(conn)(&err) updateQuery := `UPDATE loottable SET name = ?, mincoin = ?, maxcoin = ?, maxlootitems = ?, lootdrop_probability = ?, coin_probability = ? WHERE id = ?` err = sqlitex.Execute(conn, updateQuery, &sqlitex.ExecOptions{ Args: []any{ table.Name, table.MinCoin, table.MaxCoin, table.MaxLootItems, table.LootDropProbability, table.CoinProbability, table.ID, }, }) if err != nil { return fmt.Errorf("failed to update loot table: %v", err) } // Update drops - delete old ones and insert new ones if err := ldb.deleteLootDropsWithConn(conn, table.ID); err != nil { log.Printf("%s Failed to delete old loot drops for table %d: %v", LogPrefixDatabase, table.ID, err) } for _, drop := range table.Drops { if err := ldb.addLootDropWithConn(conn, drop); err != nil { log.Printf("%s Failed to add updated loot drop for table %d: %v", LogPrefixDatabase, table.ID, err) } } // Update in-memory cache ldb.mutex.Lock() ldb.lootTables[table.ID] = table ldb.mutex.Unlock() return nil } // DeleteLootTable removes a loot table and all its drops func (ldb *LootDatabase) DeleteLootTable(tableID int32) error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) // Use a savepoint for transaction support defer sqlitex.Save(conn)(&err) // Delete drops first if err := ldb.deleteLootDropsWithConn(conn, tableID); err != nil { return fmt.Errorf("failed to delete loot drops: %v", err) } // Delete table query := `DELETE FROM loottable WHERE id = ?` err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{tableID}, }) if err != nil { return fmt.Errorf("failed to delete loot table: %v", err) } // Remove from in-memory cache ldb.mutex.Lock() delete(ldb.lootTables, tableID) ldb.mutex.Unlock() return nil } // DeleteLootDrops removes all drops for a loot table func (ldb *LootDatabase) DeleteLootDrops(tableID int32) error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) return ldb.deleteLootDropsWithConn(conn, tableID) } // deleteLootDropsWithConn removes all drops for a loot table using an existing connection func (ldb *LootDatabase) deleteLootDropsWithConn(conn *sqlite.Conn, tableID int32) error { query := `DELETE FROM lootdrop WHERE loot_table_id = ?` err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{tableID}, }) return err } // AddSpawnLoot assigns a loot table to a spawn func (ldb *LootDatabase) AddSpawnLoot(spawnID, tableID int32) error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) query := `INSERT OR REPLACE INTO spawn_loot (spawn_id, loottable_id) VALUES (?, ?)` err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{spawnID, tableID}, }) if err != nil { return fmt.Errorf("failed to insert spawn loot: %v", err) } // Update in-memory cache ldb.mutex.Lock() ldb.spawnLoot[spawnID] = append(ldb.spawnLoot[spawnID], tableID) ldb.mutex.Unlock() return nil } // DeleteSpawnLoot removes all loot table assignments for a spawn func (ldb *LootDatabase) DeleteSpawnLoot(spawnID int32) error { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) query := `DELETE FROM spawn_loot WHERE spawn_id = ?` err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{spawnID}, }) if err != nil { return fmt.Errorf("failed to delete spawn loot: %v", err) } // Remove from in-memory cache ldb.mutex.Lock() delete(ldb.spawnLoot, spawnID) ldb.mutex.Unlock() return nil } // GetLootStatistics returns database statistics func (ldb *LootDatabase) GetLootStatistics() (map[string]any, error) { ctx := context.Background() conn, err := ldb.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer ldb.pool.Put(conn) stats := make(map[string]any) // Count loot tables var count int err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM loottable", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { count = int(stmt.ColumnInt64(0)) return nil }, }) if err == nil { stats["loot_tables"] = count } // Count loot drops err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM lootdrop", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { count = int(stmt.ColumnInt64(0)) return nil }, }) if err == nil { stats["loot_drops"] = count } // Count spawn loot assignments err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM spawn_loot", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { count = int(stmt.ColumnInt64(0)) return nil }, }) if err == nil { stats["spawn_loot_assignments"] = count } // In-memory statistics ldb.mutex.RLock() stats["cached_loot_tables"] = len(ldb.lootTables) stats["cached_spawn_assignments"] = len(ldb.spawnLoot) stats["cached_global_loot"] = len(ldb.globalLoot) ldb.mutex.RUnlock() stats["loaded_at"] = time.Now().Format(time.RFC3339) return stats, nil } // ReloadLootData reloads all loot data from the database func (ldb *LootDatabase) ReloadLootData() error { log.Printf("%s Reloading loot data from database...", LogPrefixDatabase) return ldb.LoadAllLootData() } // Close closes the database pool func (ldb *LootDatabase) Close() error { if ldb.pool != nil { return ldb.pool.Close() } return nil }