613 lines
16 KiB
Go
613 lines
16 KiB
Go
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
|
|
}
|