eq2go/internal/items/loot/database.go

645 lines
15 KiB
Go

package loot
import (
"database/sql"
"fmt"
"log"
"sync"
"time"
)
// LootDatabase handles all database operations for the loot system
type LootDatabase struct {
db *sql.DB
queries map[string]*sql.Stmt
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(db *sql.DB) *LootDatabase {
ldb := &LootDatabase{
db: db,
queries: make(map[string]*sql.Stmt),
lootTables: make(map[int32]*LootTable),
spawnLoot: make(map[int32][]int32),
globalLoot: make([]*GlobalLoot, 0),
}
// Prepare commonly used queries
ldb.prepareQueries()
return ldb
}
// prepareQueries prepares all commonly used SQL queries
func (ldb *LootDatabase) prepareQueries() {
queries := map[string]string{
"load_loot_tables": `
SELECT id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability
FROM loottable
ORDER BY id
`,
"load_loot_drops": `
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
`,
"load_spawn_loot": `
SELECT spawn_id, loottable_id
FROM spawn_loot
ORDER BY spawn_id
`,
"load_global_loot": `
SELECT type, loot_table, value1, value2, value3, value4
FROM loot_global
ORDER BY type, value1
`,
"insert_loot_table": `
INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability)
VALUES (?, ?, ?, ?, ?, ?, ?)
`,
"update_loot_table": `
UPDATE loottable
SET name = ?, mincoin = ?, maxcoin = ?, maxlootitems = ?, lootdrop_probability = ?, coin_probability = ?
WHERE id = ?
`,
"delete_loot_table": `
DELETE FROM loottable WHERE id = ?
`,
"insert_loot_drop": `
INSERT INTO lootdrop (loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id)
VALUES (?, ?, ?, ?, ?, ?)
`,
"delete_loot_drops": `
DELETE FROM lootdrop WHERE loot_table_id = ?
`,
"insert_spawn_loot": `
INSERT OR REPLACE INTO spawn_loot (spawn_id, loottable_id)
VALUES (?, ?)
`,
"delete_spawn_loot": `
DELETE FROM spawn_loot WHERE spawn_id = ?
`,
"insert_global_loot": `
INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4)
VALUES (?, ?, ?, ?, ?, ?)
`,
"delete_global_loot": `
DELETE FROM loot_global WHERE type = ?
`,
"get_loot_table": `
SELECT id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability
FROM loottable
WHERE id = ?
`,
"get_spawn_loot_tables": `
SELECT loottable_id
FROM spawn_loot
WHERE spawn_id = ?
`,
"count_loot_tables": `
SELECT COUNT(*) FROM loottable
`,
"count_loot_drops": `
SELECT COUNT(*) FROM lootdrop
`,
"count_spawn_loot": `
SELECT COUNT(*) FROM spawn_loot
`,
}
for name, query := range queries {
if stmt, err := ldb.db.Prepare(query); err != nil {
log.Printf("%s Failed to prepare query %s: %v", LogPrefixDatabase, name, err)
} else {
ldb.queries[name] = stmt
}
}
}
// 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 {
stmt := ldb.queries["load_loot_tables"]
if stmt == nil {
return fmt.Errorf("load_loot_tables query not prepared")
}
rows, err := stmt.Query()
if err != nil {
return fmt.Errorf("failed to query loot tables: %v", err)
}
defer rows.Close()
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
// Clear existing tables
ldb.lootTables = make(map[int32]*LootTable)
for rows.Next() {
table := &LootTable{
Drops: make([]*LootDrop, 0),
}
err := rows.Scan(
&table.ID,
&table.Name,
&table.MinCoin,
&table.MaxCoin,
&table.MaxLootItems,
&table.LootDropProbability,
&table.CoinProbability,
)
if err != nil {
log.Printf("%s Error scanning loot table row: %v", LogPrefixDatabase, err)
continue
}
ldb.lootTables[table.ID] = table
}
return rows.Err()
}
// loadLootDrops loads all loot drops for the loaded loot tables
func (ldb *LootDatabase) loadLootDrops() error {
stmt := ldb.queries["load_loot_drops"]
if stmt == nil {
return fmt.Errorf("load_loot_drops query not prepared")
}
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
for tableID, table := range ldb.lootTables {
rows, err := stmt.Query(tableID)
if err != nil {
log.Printf("%s Failed to query loot drops for table %d: %v", LogPrefixDatabase, tableID, err)
continue
}
for rows.Next() {
drop := &LootDrop{}
var equipItem int8
err := rows.Scan(
&drop.LootTableID,
&drop.ItemID,
&drop.ItemCharges,
&equipItem,
&drop.Probability,
&drop.NoDropQuestCompletedID,
)
if err != nil {
log.Printf("%s Error scanning loot drop row: %v", LogPrefixDatabase, err)
continue
}
drop.EquipItem = equipItem == 1
table.Drops = append(table.Drops, drop)
}
rows.Close()
}
return nil
}
// loadSpawnLoot loads spawn to loot table assignments
func (ldb *LootDatabase) loadSpawnLoot() error {
stmt := ldb.queries["load_spawn_loot"]
if stmt == nil {
return fmt.Errorf("load_spawn_loot query not prepared")
}
rows, err := stmt.Query()
if err != nil {
return fmt.Errorf("failed to query spawn loot: %v", err)
}
defer rows.Close()
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
// Clear existing spawn loot
ldb.spawnLoot = make(map[int32][]int32)
for rows.Next() {
var spawnID, lootTableID int32
err := rows.Scan(&spawnID, &lootTableID)
if err != nil {
log.Printf("%s Error scanning spawn loot row: %v", LogPrefixDatabase, err)
continue
}
ldb.spawnLoot[spawnID] = append(ldb.spawnLoot[spawnID], lootTableID)
}
return rows.Err()
}
// loadGlobalLoot loads global loot configuration
func (ldb *LootDatabase) loadGlobalLoot() error {
stmt := ldb.queries["load_global_loot"]
if stmt == nil {
return fmt.Errorf("load_global_loot query not prepared")
}
rows, err := stmt.Query()
if err != nil {
return fmt.Errorf("failed to query global loot: %v", err)
}
defer rows.Close()
ldb.mutex.Lock()
defer ldb.mutex.Unlock()
// Clear existing global loot
ldb.globalLoot = make([]*GlobalLoot, 0)
for rows.Next() {
var lootType string
var tableID, value1, value2, value3, value4 int32
err := rows.Scan(&lootType, &tableID, &value1, &value2, &value3, &value4)
if err != nil {
log.Printf("%s Error scanning global loot row: %v", LogPrefixDatabase, err)
continue
}
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)
continue
}
ldb.globalLoot = append(ldb.globalLoot, global)
}
return rows.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 {
stmt := ldb.queries["insert_loot_table"]
if stmt == nil {
return fmt.Errorf("insert_loot_table query not prepared")
}
_, err := stmt.Exec(
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.AddLootDrop(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 {
stmt := ldb.queries["insert_loot_drop"]
if stmt == nil {
return fmt.Errorf("insert_loot_drop query not prepared")
}
equipItem := int8(0)
if drop.EquipItem {
equipItem = 1
}
_, err := stmt.Exec(
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 {
stmt := ldb.queries["update_loot_table"]
if stmt == nil {
return fmt.Errorf("update_loot_table query not prepared")
}
_, err := stmt.Exec(
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.DeleteLootDrops(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.AddLootDrop(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 {
// Delete drops first
if err := ldb.DeleteLootDrops(tableID); err != nil {
return fmt.Errorf("failed to delete loot drops: %v", err)
}
// Delete table
stmt := ldb.queries["delete_loot_table"]
if stmt == nil {
return fmt.Errorf("delete_loot_table query not prepared")
}
_, err := stmt.Exec(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 {
stmt := ldb.queries["delete_loot_drops"]
if stmt == nil {
return fmt.Errorf("delete_loot_drops query not prepared")
}
_, err := stmt.Exec(tableID)
return err
}
// AddSpawnLoot assigns a loot table to a spawn
func (ldb *LootDatabase) AddSpawnLoot(spawnID, tableID int32) error {
stmt := ldb.queries["insert_spawn_loot"]
if stmt == nil {
return fmt.Errorf("insert_spawn_loot query not prepared")
}
_, err := stmt.Exec(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 {
stmt := ldb.queries["delete_spawn_loot"]
if stmt == nil {
return fmt.Errorf("delete_spawn_loot query not prepared")
}
_, err := stmt.Exec(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) {
stats := make(map[string]any)
// Count loot tables
if stmt := ldb.queries["count_loot_tables"]; stmt != nil {
var count int
if err := stmt.QueryRow().Scan(&count); err == nil {
stats["loot_tables"] = count
}
}
// Count loot drops
if stmt := ldb.queries["count_loot_drops"]; stmt != nil {
var count int
if err := stmt.QueryRow().Scan(&count); err == nil {
stats["loot_drops"] = count
}
}
// Count spawn loot assignments
if stmt := ldb.queries["count_spawn_loot"]; stmt != nil {
var count int
if err := stmt.QueryRow().Scan(&count); 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 all prepared statements
func (ldb *LootDatabase) Close() error {
for name, stmt := range ldb.queries {
if err := stmt.Close(); err != nil {
log.Printf("%s Error closing statement %s: %v", LogPrefixDatabase, name, err)
}
}
return nil
}