550 lines
14 KiB
Go
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)
|
|
} |