eq2go/internal/items/loot/integration.go

417 lines
12 KiB
Go

package loot
import (
"database/sql"
"fmt"
"log"
"eq2emu/internal/items"
)
// LootSystem represents the complete loot system integration
type LootSystem struct {
Database *LootDatabase
Manager *LootManager
ChestService *ChestService
PacketService *LootPacketService
}
// LootSystemConfig holds configuration for the loot system
type LootSystemConfig struct {
DatabaseConnection *sql.DB
ItemMasterList items.MasterItemListService
PlayerService PlayerService
ZoneService ZoneService
ClientService ClientService
ItemPacketBuilder ItemPacketBuilder
StartCleanupTimer bool
}
// NewLootSystem creates a complete loot system with all components
func NewLootSystem(config *LootSystemConfig) (*LootSystem, error) {
if config.DatabaseConnection == nil {
return nil, fmt.Errorf("database connection is required")
}
if config.ItemMasterList == nil {
return nil, fmt.Errorf("item master list is required")
}
// Create database layer
database := NewLootDatabase(config.DatabaseConnection)
// Load loot data
if err := database.LoadAllLootData(); err != nil {
return nil, fmt.Errorf("failed to load loot data: %v", err)
}
// Create loot manager
manager := NewLootManager(database, config.ItemMasterList)
// Create chest service (optional - requires player and zone services)
var chestService *ChestService
if config.PlayerService != nil && config.ZoneService != nil {
chestService = NewChestService(manager, config.PlayerService, config.ZoneService)
}
// Create packet service (optional - requires client and item packet builder)
var packetService *LootPacketService
if config.ClientService != nil && config.ItemPacketBuilder != nil {
packetBuilder := NewLootPacketBuilder(config.ItemPacketBuilder)
packetService = NewLootPacketService(packetBuilder, config.ClientService)
}
// Start cleanup timer if requested
if config.StartCleanupTimer {
manager.StartCleanupTimer()
}
system := &LootSystem{
Database: database,
Manager: manager,
ChestService: chestService,
PacketService: packetService,
}
log.Printf("%s Loot system initialized successfully", LogPrefixLoot)
return system, nil
}
// GenerateAndCreateChest generates loot for a spawn and creates a treasure chest
func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, z, heading float32,
context *LootContext) (*TreasureChest, error) {
if ls.ChestService == nil {
return nil, fmt.Errorf("chest service not available")
}
// Generate loot
lootResult, err := ls.Manager.GenerateLoot(spawnID, context)
if err != nil {
return nil, fmt.Errorf("failed to generate loot: %v", err)
}
// Don't create chest if no loot
if lootResult.IsEmpty() {
log.Printf("%s No loot generated for spawn %d, not creating chest", LogPrefixLoot, spawnID)
return nil, nil
}
// Create treasure chest
chest, err := ls.ChestService.CreateTreasureChestFromLoot(spawnID, zoneID, x, y, z, heading,
lootResult, context.GroupMembers)
if err != nil {
return nil, fmt.Errorf("failed to create treasure chest: %v", err)
}
return chest, nil
}
// HandlePlayerLootInteraction handles a player's interaction with a chest and sends appropriate packets
func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32,
interaction ChestInteraction, itemUniqueID int64) error {
if ls.ChestService == nil {
return fmt.Errorf("chest service not available")
}
// Handle the interaction
result := ls.ChestService.HandleChestInteraction(chestID, playerID, interaction, itemUniqueID)
// Send response packet if packet service is available
if ls.PacketService != nil {
if err := ls.PacketService.SendLootResponse(result, playerID); err != nil {
log.Printf("%s Failed to send loot response packet: %v", LogPrefixLoot, err)
}
// Send updated loot window if chest is still open and has items
if result.Success && !result.ChestClosed {
chest := ls.Manager.GetTreasureChest(chestID)
if chest != nil && !chest.LootResult.IsEmpty() {
if err := ls.PacketService.SendLootUpdate(chest, playerID); err != nil {
log.Printf("%s Failed to send loot update packet: %v", LogPrefixLoot, err)
}
} else if chest != nil && chest.LootResult.IsEmpty() {
// Send stopped looting packet for empty chest
if err := ls.PacketService.SendStoppedLooting(chestID, playerID); err != nil {
log.Printf("%s Failed to send stopped looting packet: %v", LogPrefixLoot, err)
}
}
}
}
// Log the interaction
log.Printf("%s Player %d %s chest %d: %s",
LogPrefixLoot, playerID, interaction.String(), chestID, result.Message)
return nil
}
// ShowChestToPlayer sends the loot window to a player
func (ls *LootSystem) ShowChestToPlayer(chestID int32, playerID uint32) error {
if ls.PacketService == nil {
return fmt.Errorf("packet service not available")
}
chest := ls.Manager.GetTreasureChest(chestID)
if chest == nil {
return fmt.Errorf("chest %d not found", chestID)
}
// Check loot rights
if !chest.HasLootRights(playerID) {
return fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID)
}
// Send loot update packet
return ls.PacketService.SendLootUpdate(chest, playerID)
}
// GetSystemStatistics returns comprehensive statistics about the loot system
func (ls *LootSystem) GetSystemStatistics() (map[string]any, error) {
stats := make(map[string]any)
// Database statistics
if dbStats, err := ls.Database.GetLootStatistics(); err == nil {
stats["database"] = dbStats
}
// Manager statistics
stats["generation"] = ls.Manager.GetStatistics()
// Active chests count
chestCount := 0
ls.Manager.mutex.RLock()
chestCount = len(ls.Manager.treasureChests)
ls.Manager.mutex.RUnlock()
stats["active_chests"] = chestCount
return stats, nil
}
// ReloadAllData reloads all loot data from the database
func (ls *LootSystem) ReloadAllData() error {
log.Printf("%s Reloading all loot system data", LogPrefixLoot)
return ls.Database.LoadAllLootData()
}
// Shutdown gracefully shuts down the loot system
func (ls *LootSystem) Shutdown() error {
log.Printf("%s Shutting down loot system", LogPrefixLoot)
// Close database connections
if err := ls.Database.Close(); err != nil {
log.Printf("%s Error closing database: %v", LogPrefixLoot, err)
return err
}
// Clear active chests
ls.Manager.mutex.Lock()
ls.Manager.treasureChests = make(map[int32]*TreasureChest)
ls.Manager.mutex.Unlock()
log.Printf("%s Loot system shutdown complete", LogPrefixLoot)
return nil
}
// AddLootTableWithDrops adds a complete loot table with drops in a single transaction
func (ls *LootSystem) AddLootTableWithDrops(table *LootTable) error {
return ls.Database.AddLootTable(table)
}
// CreateQuickLootTable creates a simple loot table with basic parameters
func (ls *LootSystem) CreateQuickLootTable(tableID int32, name string, items []QuickLootItem,
minCoin, maxCoin int32, maxItems int16) error {
table := &LootTable{
ID: tableID,
Name: name,
MinCoin: minCoin,
MaxCoin: maxCoin,
MaxLootItems: maxItems,
LootDropProbability: DefaultLootDropProbability,
CoinProbability: DefaultCoinProbability,
Drops: make([]*LootDrop, len(items)),
}
for i, item := range items {
table.Drops[i] = &LootDrop{
LootTableID: tableID,
ItemID: item.ItemID,
ItemCharges: item.Charges,
EquipItem: item.AutoEquip,
Probability: item.Probability,
}
}
return ls.AddLootTableWithDrops(table)
}
// QuickLootItem represents a simple loot item for quick table creation
type QuickLootItem struct {
ItemID int32
Charges int16
Probability float32
AutoEquip bool
}
// AssignLootToSpawns assigns a loot table to multiple spawns
func (ls *LootSystem) AssignLootToSpawns(tableID int32, spawnIDs []int32) error {
for _, spawnID := range spawnIDs {
if err := ls.Database.AddSpawnLoot(spawnID, tableID); err != nil {
return fmt.Errorf("failed to assign loot table %d to spawn %d: %v", tableID, spawnID, err)
}
}
log.Printf("%s Assigned loot table %d to %d spawns", LogPrefixLoot, tableID, len(spawnIDs))
return nil
}
// CreateGlobalLevelLoot creates global loot for a level range
func (ls *LootSystem) CreateGlobalLevelLoot(minLevel, maxLevel int8, tableID int32, tier int32) error {
global := &GlobalLoot{
Type: GlobalLootTypeLevel,
MinLevel: minLevel,
MaxLevel: maxLevel,
TableID: tableID,
LootTier: tier,
}
// Insert into database
query := `INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4) VALUES (?, ?, ?, ?, ?, ?)`
_, err := ls.Database.db.Exec(query, "level", tableID, minLevel, maxLevel, tier, 0)
if err != nil {
return fmt.Errorf("failed to insert global level loot: %v", err)
}
// Add to in-memory cache
ls.Database.mutex.Lock()
ls.Database.globalLoot = append(ls.Database.globalLoot, global)
ls.Database.mutex.Unlock()
log.Printf("%s Created global level loot for levels %d-%d using table %d",
LogPrefixLoot, minLevel, maxLevel, tableID)
return nil
}
// GetActiveChestsInZone returns all active chests in a specific zone
func (ls *LootSystem) GetActiveChestsInZone(zoneID int32) []*TreasureChest {
return ls.Manager.GetZoneChests(zoneID)
}
// CleanupZoneChests removes all chests from a specific zone
func (ls *LootSystem) CleanupZoneChests(zoneID int32) {
chests := ls.Manager.GetZoneChests(zoneID)
for _, chest := range chests {
ls.Manager.RemoveTreasureChest(chest.ID)
// Remove from zone if chest service is available
if ls.ChestService != nil {
ls.ChestService.zoneService.RemoveObjectFromZone(zoneID, chest.ID)
}
}
log.Printf("%s Cleaned up %d chests from zone %d", LogPrefixLoot, len(chests), zoneID)
}
// ValidateItemsInLootTables checks that all items in loot tables exist in the item master list
func (ls *LootSystem) ValidateItemsInLootTables() []ValidationError {
var errors []ValidationError
ls.Database.mutex.RLock()
defer ls.Database.mutex.RUnlock()
for tableID, table := range ls.Database.lootTables {
for _, drop := range table.Drops {
item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
if item == nil {
errors = append(errors, ValidationError{
Type: "missing_item",
TableID: tableID,
ItemID: drop.ItemID,
Description: fmt.Sprintf("Item %d in loot table %d (%s) does not exist", drop.ItemID, tableID, table.Name),
})
}
}
}
if len(errors) > 0 {
log.Printf("%s Found %d validation errors in loot tables", LogPrefixLoot, len(errors))
}
return errors
}
// ValidationError represents a loot system validation error
type ValidationError struct {
Type string `json:"type"`
TableID int32 `json:"table_id"`
ItemID int32 `json:"item_id,omitempty"`
Description string `json:"description"`
}
// GetLootPreview generates a preview of potential loot without actually creating it
func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*LootPreview, error) {
tableIDs := ls.Database.GetSpawnLootTables(spawnID)
globalLoot := ls.Database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID)
for _, global := range globalLoot {
tableIDs = append(tableIDs, global.TableID)
}
preview := &LootPreview{
SpawnID: spawnID,
TableIDs: tableIDs,
PossibleItems: make([]*LootPreviewItem, 0),
MinCoins: 0,
MaxCoins: 0,
}
for _, tableID := range tableIDs {
table := ls.Database.GetLootTable(tableID)
if table == nil {
continue
}
preview.MinCoins += table.MinCoin
preview.MaxCoins += table.MaxCoin
for _, drop := range table.Drops {
item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
if item == nil {
continue
}
previewItem := &LootPreviewItem{
ItemID: drop.ItemID,
ItemName: item.Name,
Probability: drop.Probability,
Tier: item.Details.Tier,
}
preview.PossibleItems = append(preview.PossibleItems, previewItem)
}
}
return preview, nil
}
// LootPreview represents a preview of potential loot
type LootPreview struct {
SpawnID int32 `json:"spawn_id"`
TableIDs []int32 `json:"table_ids"`
PossibleItems []*LootPreviewItem `json:"possible_items"`
MinCoins int32 `json:"min_coins"`
MaxCoins int32 `json:"max_coins"`
}
// LootPreviewItem represents a potential loot item in a preview
type LootPreviewItem struct {
ItemID int32 `json:"item_id"`
ItemName string `json:"item_name"`
Probability float32 `json:"probability"`
Tier int8 `json:"tier"`
}