eq2go/internal/player/unified.go
2025-08-30 06:54:05 -05:00

550 lines
14 KiB
Go

package player
import (
"fmt"
"sync"
"time"
"eq2emu/internal/database"
)
// Logger interface for player system
type Logger interface {
Debug(string, ...interface{})
Info(string, ...interface{})
Warn(string, ...interface{})
Error(string, ...interface{})
}
// Config interface for player system
type Config interface {
GetString(string) string
GetInt(string) int
GetBool(string) bool
}
// PlayerManager manages all player operations in a unified system
type PlayerManager struct {
db *database.Database
logger Logger
config Config
playersLock sync.RWMutex
players map[int32]*Player
experienceCalc ExperienceCalculator
combatSystem CombatSystem
questSystem QuestSystem
dbPersister DatabasePersister
eventManager EventManager
}
// ExperienceCalculator handles all experience calculations
type ExperienceCalculator struct {
baseLevelXP []int64
tsLevelXP []int64
vitaeLevels []float32
aaMutex sync.RWMutex
}
// CombatSystem handles all combat-related functionality
type CombatSystem struct {
activeCombats map[int32]*CombatSession
combatMutex sync.RWMutex
damageTypes map[int32]string
}
// QuestSystem manages all quest-related functionality
type QuestSystem struct {
questCache map[int32]*Quest
questMutex sync.RWMutex
completionCache map[int32]bool
}
// DatabasePersister handles all database operations
type DatabasePersister struct {
db *database.Database
saveMutex sync.Mutex
saveQueue chan *Player
stopChan chan bool
}
// EventManager handles all player events
type EventManager struct {
eventQueue chan PlayerEvent
subscribers map[EventType][]EventHandler
eventMutex sync.RWMutex
}
// PlayerEvent represents a player event
type PlayerEvent struct {
Type EventType
PlayerID int32
Data interface{}
}
// EventType represents the type of player event
type EventType int
const (
EventPlayerLogin EventType = iota
EventPlayerLogout
EventPlayerLevelUp
EventPlayerDeath
EventPlayerCombat
EventPlayerQuest
)
// EventHandler handles player events
type EventHandler func(PlayerEvent)
// CombatSession represents an active combat session
type CombatSession struct {
PlayerID int32
TargetID int32
StartTime time.Time
DamageDealt int32
DamageTaken int32
}
// Quest represents a quest (placeholder for full implementation)
type Quest struct {
ID int32
Name string
Description string
Status int32
}
// NewPlayerManager creates a new unified player manager
func NewPlayerManager(db *database.Database, logger Logger, config Config) *PlayerManager {
pm := &PlayerManager{
db: db,
logger: logger,
config: config,
players: make(map[int32]*Player),
experienceCalc: ExperienceCalculator{
baseLevelXP: make([]int64, 101),
tsLevelXP: make([]int64, 101),
vitaeLevels: make([]float32, 101),
},
combatSystem: CombatSystem{
activeCombats: make(map[int32]*CombatSession),
damageTypes: make(map[int32]string),
},
questSystem: QuestSystem{
questCache: make(map[int32]*Quest),
completionCache: make(map[int32]bool),
},
dbPersister: DatabasePersister{
db: db,
saveQueue: make(chan *Player, 100),
stopChan: make(chan bool),
},
eventManager: EventManager{
eventQueue: make(chan PlayerEvent, 1000),
subscribers: make(map[EventType][]EventHandler),
},
}
// Initialize experience tables
pm.initializeExperienceTables()
// Start background processors
pm.startBackgroundProcessors()
return pm
}
// AddPlayer adds a player to the manager
func (pm *PlayerManager) AddPlayer(player *Player) {
pm.playersLock.Lock()
defer pm.playersLock.Unlock()
pm.players[player.charID] = player
pm.logger.Info("Player added: %d", player.charID)
// Send login event
pm.eventManager.eventQueue <- PlayerEvent{
Type: EventPlayerLogin,
PlayerID: player.charID,
Data: player,
}
}
// RemovePlayer removes a player from the manager
func (pm *PlayerManager) RemovePlayer(charID int32) {
pm.playersLock.Lock()
defer pm.playersLock.Unlock()
if player, exists := pm.players[charID]; exists {
// Send logout event
pm.eventManager.eventQueue <- PlayerEvent{
Type: EventPlayerLogout,
PlayerID: charID,
Data: player,
}
delete(pm.players, charID)
pm.logger.Info("Player removed: %d", charID)
}
}
// GetPlayer retrieves a player by character ID
func (pm *PlayerManager) GetPlayer(charID int32) *Player {
pm.playersLock.RLock()
defer pm.playersLock.RUnlock()
return pm.players[charID]
}
// GetAllPlayers returns all active players
func (pm *PlayerManager) GetAllPlayers() []*Player {
pm.playersLock.RLock()
defer pm.playersLock.RUnlock()
players := make([]*Player, 0, len(pm.players))
for _, player := range pm.players {
players = append(players, player)
}
return players
}
// UpdatePlayerLevel handles player level progression
func (pm *PlayerManager) UpdatePlayerLevel(charID int32, newLevel int16) error {
player := pm.GetPlayer(charID)
if player == nil {
return fmt.Errorf("player not found: %d", charID)
}
oldLevel := player.GetLevel()
player.SetLevel(newLevel)
// Send level up event
pm.eventManager.eventQueue <- PlayerEvent{
Type: EventPlayerLevelUp,
PlayerID: charID,
Data: map[string]interface{}{"oldLevel": oldLevel, "newLevel": newLevel},
}
// Send level up packet
pm.sendLevelUpPacket(player)
pm.logger.Info("Player %d leveled up from %d to %d", charID, oldLevel, newLevel)
return nil
}
// AddExperience adds experience to a player
func (pm *PlayerManager) AddExperience(charID int32, xpAmount int64, xpType string) error {
player := pm.GetPlayer(charID)
if player == nil {
return fmt.Errorf("player not found: %d", charID)
}
// Calculate vitae modifier
vitaeModifier := pm.experienceCalc.getVitaeModifier(int16(player.GetLevel()))
adjustedXP := int64(float32(xpAmount) * vitaeModifier)
// Validate XP type
switch xpType {
case "adventure", "tradeskill":
// Valid types - continue
default:
return fmt.Errorf("unknown XP type: %s", xpType)
}
// For now, just log the XP addition - actual XP modification would require
// accessor methods to be added to InfoStruct
pm.logger.Debug("Would add %d %s XP to player %d (vitae: %.2f)", adjustedXP, xpType, charID, vitaeModifier)
// Check for level up (simplified version)
pm.checkLevelUp(player, xpType)
// Send XP update packet
pm.sendXPUpdatePacket(player, adjustedXP, xpType)
return nil
}
// ProcessCombat handles combat between entities
func (pm *PlayerManager) ProcessCombat(attackerID, defenderID int32, damageAmount int32) error {
attacker := pm.GetPlayer(attackerID)
if attacker == nil {
return fmt.Errorf("attacker not found: %d", attackerID)
}
// Create or update combat session
pm.combatSystem.combatMutex.Lock()
session, exists := pm.combatSystem.activeCombats[attackerID]
if !exists {
session = &CombatSession{
PlayerID: attackerID,
TargetID: defenderID,
StartTime: time.Now(),
}
pm.combatSystem.activeCombats[attackerID] = session
}
session.DamageDealt += damageAmount
pm.combatSystem.combatMutex.Unlock()
// Send combat event
pm.eventManager.eventQueue <- PlayerEvent{
Type: EventPlayerCombat,
PlayerID: attackerID,
Data: session,
}
// Send combat packet
pm.sendCombatPacket(attacker, defenderID, damageAmount)
pm.logger.Debug("Combat: Player %d dealt %d damage to %d", attackerID, damageAmount, defenderID)
return nil
}
// UpdateCurrency updates a player's currency
func (pm *PlayerManager) UpdateCurrency(charID int32, currencyType string, amount int64) error {
player := pm.GetPlayer(charID)
if player == nil {
return fmt.Errorf("player not found: %d", charID)
}
// Get the player's info struct
info := player.GetInfoStruct()
if info == nil {
return fmt.Errorf("player info not available")
}
// Update currency based on type
switch currencyType {
case "coin":
info.AddCoins(int32(amount))
case "tokens":
// Tokens would be handled separately if implemented
pm.logger.Debug("Would add %d tokens to player %d", amount, charID)
case "status":
// Status would require accessor methods
pm.logger.Debug("Would add %d status to player %d", amount, charID)
default:
return fmt.Errorf("unknown currency type: %s", currencyType)
}
// Send currency update packet
pm.sendCurrencyUpdatePacket(player, currencyType, amount)
pm.logger.Debug("Updated %s currency for player %d: %d", currencyType, charID, amount)
return nil
}
// SavePlayer saves a player to the database
func (pm *PlayerManager) SavePlayer(charID int32) error {
player := pm.GetPlayer(charID)
if player == nil {
return fmt.Errorf("player not found: %d", charID)
}
// Queue for background save
select {
case pm.dbPersister.saveQueue <- player:
pm.logger.Debug("Queued player %d for save", charID)
default:
pm.logger.Warn("Save queue full, performing synchronous save for player %d", charID)
return pm.savePlayerSync(player)
}
return nil
}
// SaveAllPlayers saves all active players
func (pm *PlayerManager) SaveAllPlayers() error {
players := pm.GetAllPlayers()
for _, player := range players {
if err := pm.SavePlayer(player.charID); err != nil {
pm.logger.Error("Failed to save player %d: %v", player.charID, err)
}
}
pm.logger.Info("Initiated save for %d players", len(players))
return nil
}
// Shutdown gracefully shuts down the player manager
func (pm *PlayerManager) Shutdown() {
pm.logger.Info("Shutting down player manager...")
// Stop background processors
close(pm.dbPersister.stopChan)
// Save all players
pm.SaveAllPlayers()
// Wait for save queue to empty
time.Sleep(time.Second * 2)
pm.logger.Info("Player manager shutdown complete")
}
// initializeExperienceTables initializes the XP lookup tables
func (pm *PlayerManager) initializeExperienceTables() {
// Initialize base level XP requirements (example values)
for i := 0; i < 101; i++ {
pm.experienceCalc.baseLevelXP[i] = int64(i * i * 100)
pm.experienceCalc.tsLevelXP[i] = int64(i * i * 80)
pm.experienceCalc.vitaeLevels[i] = 1.0 // No vitae by default
}
}
// startBackgroundProcessors starts all background processing routines
func (pm *PlayerManager) startBackgroundProcessors() {
// Start save processor
go pm.dbPersister.processSaveQueue()
// Start event processor
go pm.eventManager.processEvents()
// Start periodic tasks
go pm.periodicTasks()
}
// processSaveQueue processes the background save queue
func (dp *DatabasePersister) processSaveQueue() {
for {
select {
case player := <-dp.saveQueue:
if err := dp.savePlayerToDB(player); err != nil {
// Log error but continue processing
}
case <-dp.stopChan:
// Process remaining items in queue
for len(dp.saveQueue) > 0 {
player := <-dp.saveQueue
dp.savePlayerToDB(player)
}
return
}
}
}
// processEvents processes the background event queue
func (em *EventManager) processEvents() {
for event := range em.eventQueue {
em.eventMutex.RLock()
handlers := em.subscribers[event.Type]
em.eventMutex.RUnlock()
for _, handler := range handlers {
go handler(event)
}
}
}
// periodicTasks runs periodic maintenance tasks
func (pm *PlayerManager) periodicTasks() {
ticker := time.NewTicker(time.Minute * 5)
defer ticker.Stop()
for {
select {
case <-ticker.C:
pm.performMaintenance()
case <-pm.dbPersister.stopChan:
return
}
}
}
// performMaintenance performs periodic maintenance tasks
func (pm *PlayerManager) performMaintenance() {
// Clean up old combat sessions
pm.combatSystem.combatMutex.Lock()
cutoff := time.Now().Add(-time.Minute * 10)
for playerID, session := range pm.combatSystem.activeCombats {
if session.StartTime.Before(cutoff) {
delete(pm.combatSystem.activeCombats, playerID)
}
}
pm.combatSystem.combatMutex.Unlock()
pm.logger.Debug("Performed maintenance tasks")
}
// Helper methods for experience calculation
func (ec *ExperienceCalculator) getVitaeModifier(level int16) float32 {
if level < 0 || int(level) >= len(ec.vitaeLevels) {
return 1.0
}
return ec.vitaeLevels[level]
}
// checkLevelUp checks if a player should level up
func (pm *PlayerManager) checkLevelUp(player *Player, xpType string) {
currentLevel := player.GetLevel()
// Simplified level up check - actual implementation would check XP values
if xpType == "adventure" {
pm.logger.Debug("Would check adventure level up for player %d at level %d", player.charID, currentLevel)
} else if xpType == "tradeskill" {
pm.logger.Debug("Would check tradeskill level up for player %d", player.charID)
}
}
// Packet sending methods
func (pm *PlayerManager) sendLevelUpPacket(player *Player) {
if pm.db == nil {
return // Skip if no database connection
}
// Send packet using simplified approach - the actual packet building
// would be handled by the network layer
pm.logger.Debug("Sent level up packet for player %d to level %d", player.charID, player.GetLevel())
}
func (pm *PlayerManager) sendXPUpdatePacket(player *Player, xpAmount int64, xpType string) {
if pm.db == nil {
return
}
pm.logger.Debug("Sent %s XP update packet for player %d: %d", xpType, player.charID, xpAmount)
}
func (pm *PlayerManager) sendCombatPacket(attacker *Player, defenderID int32, damage int32) {
if pm.db == nil {
return
}
pm.logger.Debug("Sent combat packet for player %d vs %d: %d damage", attacker.charID, defenderID, damage)
}
func (pm *PlayerManager) sendCurrencyUpdatePacket(player *Player, currencyType string, amount int64) {
if pm.db == nil {
return
}
pm.logger.Debug("Sent currency update packet for player %d: %s +%d", player.charID, currencyType, amount)
}
// Database operations
func (dp *DatabasePersister) savePlayerToDB(player *Player) error {
if dp.db == nil {
return fmt.Errorf("no database connection")
}
dp.saveMutex.Lock()
defer dp.saveMutex.Unlock()
// Get player info
info := player.GetInfoStruct()
if info == nil {
return fmt.Errorf("player info not available")
}
// Save player data (implementation depends on database schema)
// This is a simplified approach - actual implementation would use proper DB operations
return fmt.Errorf("database save not yet implemented")
}
func (pm *PlayerManager) savePlayerSync(player *Player) error {
return pm.dbPersister.savePlayerToDB(player)
}
// Event subscription methods
func (pm *PlayerManager) SubscribeToEvent(eventType EventType, handler EventHandler) {
pm.eventManager.eventMutex.Lock()
defer pm.eventManager.eventMutex.Unlock()
pm.eventManager.subscribers[eventType] = append(pm.eventManager.subscribers[eventType], handler)
}