From 5cb4b5b56cb44e4fea4526197b5a54e061dc8b55 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 7 Aug 2025 11:21:56 -0500 Subject: [PATCH] interface to any --- internal/database/database.go | 59 +++-- internal/packets/handler.go | 44 ++-- internal/world/README.md | 4 +- internal/world/achievement_events.go | 110 ++++---- internal/world/achievement_manager.go | 100 ++++---- internal/world/client_list.go | 144 +++++------ internal/world/item_manager.go | 164 ++++++------ internal/world/npc_manager.go | 274 ++++++++++---------- internal/world/packet_handlers.go | 352 +++++++++++++------------- internal/world/title_manager.go | 56 ++-- internal/world/world.go | 310 +++++++++++------------ 11 files changed, 808 insertions(+), 809 deletions(-) diff --git a/internal/database/database.go b/internal/database/database.go index 78a7039..3e7e611 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -4,7 +4,7 @@ import ( "database/sql" "fmt" "sync" - + _ "github.com/go-sql-driver/mysql" _ "modernc.org/sqlite" "zombiezen.com/go/sqlite/sqlitex" @@ -27,22 +27,22 @@ type Config struct { // Database wraps the SQL database connection type Database struct { - db *sql.DB - pool *sqlitex.Pool // For achievements system compatibility (SQLite only) - config Config - mutex sync.RWMutex + db *sql.DB + pool *sqlitex.Pool // For achievements system compatibility (SQLite only) + config Config + mutex sync.RWMutex } // New creates a new database connection with the provided configuration func New(config Config) (*Database, error) { var driverName string var pool *sqlitex.Pool - + // Set default pool size if config.PoolSize == 0 { config.PoolSize = 25 } - + switch config.Type { case SQLite: driverName = "sqlite" @@ -59,27 +59,27 @@ func New(config Config) (*Database, error) { default: return nil, fmt.Errorf("unsupported database type: %d", config.Type) } - + db, err := sql.Open(driverName, config.DSN) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } - + // Test connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } - + // Set connection pool settings db.SetMaxOpenConns(config.PoolSize) db.SetMaxIdleConns(config.PoolSize / 5) - + d := &Database{ db: db, pool: pool, config: config, } - + return d, nil } @@ -101,19 +101,18 @@ func (d *Database) GetPool() *sqlitex.Pool { return d.pool } - // Query executes a query that returns rows -func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) { +func (d *Database) Query(query string, args ...any) (*sql.Rows, error) { return d.db.Query(query, args...) } // QueryRow executes a query that returns a single row -func (d *Database) QueryRow(query string, args ...interface{}) *sql.Row { +func (d *Database) QueryRow(query string, args ...any) *sql.Row { return d.db.QueryRow(query, args...) } // Exec executes a query that doesn't return rows -func (d *Database) Exec(query string, args ...interface{}) (sql.Result, error) { +func (d *Database) Exec(query string, args ...any) (sql.Result, error) { return d.db.Exec(query, args...) } @@ -129,21 +128,21 @@ func (d *Database) LoadRules() (map[string]map[string]string, error) { return nil, err } defer rows.Close() - + rules := make(map[string]map[string]string) - + for rows.Next() { var category, name, value string if err := rows.Scan(&category, &name, &value); err != nil { return nil, err } - + if rules[category] == nil { rules[category] = make(map[string]string) } rules[category][name] = value } - + return rules, rows.Err() } @@ -174,7 +173,7 @@ func NewMySQL(dsn string) (*Database, error) { } // GetZones retrieves all zones from the database -func (d *Database) GetZones() ([]map[string]interface{}, error) { +func (d *Database) GetZones() ([]map[string]any, error) { rows, err := d.Query(` SELECT id, name, file, description, motd, min_level, max_level, min_version, xp_modifier, city_zone, weather_allowed, @@ -186,16 +185,16 @@ func (d *Database) GetZones() ([]map[string]interface{}, error) { return nil, err } defer rows.Close() - - var zones []map[string]interface{} - + + var zones []map[string]any + for rows.Next() { - zone := make(map[string]interface{}) + zone := make(map[string]any) var id, minLevel, maxLevel, minVersion int var name, file, description, motd string var xpModifier, safeX, safeY, safeZ, safeHeading float64 var cityZone, weatherAllowed bool - + err := rows.Scan(&id, &name, &file, &description, &motd, &minLevel, &maxLevel, &minVersion, &xpModifier, &cityZone, &weatherAllowed, @@ -203,7 +202,7 @@ func (d *Database) GetZones() ([]map[string]interface{}, error) { if err != nil { return nil, err } - + zone["id"] = id zone["name"] = name zone["file"] = file @@ -219,9 +218,9 @@ func (d *Database) GetZones() ([]map[string]interface{}, error) { zone["safe_y"] = safeY zone["safe_z"] = safeZ zone["safe_heading"] = safeHeading - + zones = append(zones, zone) } - + return zones, rows.Err() -} \ No newline at end of file +} diff --git a/internal/packets/handler.go b/internal/packets/handler.go index e5561e8..7eefa21 100644 --- a/internal/packets/handler.go +++ b/internal/packets/handler.go @@ -17,7 +17,7 @@ type PacketData struct { // PacketContext provides context for packet handlers type PacketContext struct { Client ClientInterface // Client connection interface - World WorldInterface // World server interface + World WorldInterface // World server interface Database DatabaseInterface // Database interface } @@ -43,8 +43,8 @@ type WorldInterface interface { // DatabaseInterface defines database operations needed by packet handlers type DatabaseInterface interface { - GetCharacter(characterID int32) (map[string]interface{}, error) - SaveCharacter(characterID int32, data map[string]interface{}) error + GetCharacter(characterID int32) (map[string]any, error) + SaveCharacter(characterID int32, data map[string]any) error // Add more database methods as needed } @@ -68,7 +68,7 @@ func NewPacketHandlerRegistry() *PacketHandlerRegistry { func (phr *PacketHandlerRegistry) RegisterHandler(opcode InternalOpcode, handler PacketHandlerFunc) { phr.mutex.Lock() defer phr.mutex.Unlock() - + phr.handlers[opcode] = handler fmt.Printf("Registered handler for opcode %s (%d)\n", GetInternalOpcodeName(opcode), int(opcode)) } @@ -77,7 +77,7 @@ func (phr *PacketHandlerRegistry) RegisterHandler(opcode InternalOpcode, handler func (phr *PacketHandlerRegistry) UnregisterHandler(opcode InternalOpcode) { phr.mutex.Lock() defer phr.mutex.Unlock() - + delete(phr.handlers, opcode) } @@ -85,7 +85,7 @@ func (phr *PacketHandlerRegistry) UnregisterHandler(opcode InternalOpcode) { func (phr *PacketHandlerRegistry) HasHandler(opcode InternalOpcode) bool { phr.mutex.RLock() defer phr.mutex.RUnlock() - + _, exists := phr.handlers[opcode] return exists } @@ -95,16 +95,16 @@ func (phr *PacketHandlerRegistry) HandlePacket(ctx *PacketContext, packet *Packe phr.mutex.RLock() handler, exists := phr.handlers[packet.Opcode] phr.mutex.RUnlock() - + if !exists { // No handler registered - this is not necessarily an error - fmt.Printf("No handler registered for opcode %s (%d) from client %s\n", - GetInternalOpcodeName(packet.Opcode), + fmt.Printf("No handler registered for opcode %s (%d) from client %s\n", + GetInternalOpcodeName(packet.Opcode), int(packet.Opcode), ctx.Client.GetCharacterName()) return nil } - + // Call the handler return handler(ctx, packet) } @@ -113,7 +113,7 @@ func (phr *PacketHandlerRegistry) HandlePacket(ctx *PacketContext, packet *Packe func (phr *PacketHandlerRegistry) GetHandlerCount() int { phr.mutex.RLock() defer phr.mutex.RUnlock() - + return len(phr.handlers) } @@ -121,18 +121,18 @@ func (phr *PacketHandlerRegistry) GetHandlerCount() int { func (phr *PacketHandlerRegistry) GetRegisteredOpcodes() []InternalOpcode { phr.mutex.RLock() defer phr.mutex.RUnlock() - + opcodes := make([]InternalOpcode, 0, len(phr.handlers)) for opcode := range phr.handlers { opcodes = append(opcodes, opcode) } - + return opcodes } // PacketProcessor combines opcode management and handler dispatch type PacketProcessor struct { - opcodeManager *OpcodeManager + opcodeManager *OpcodeManager handlerRegistry *PacketHandlerRegistry } @@ -149,17 +149,17 @@ func (pp *PacketProcessor) ProcessRawPacket(ctx *PacketContext, rawData []byte, if len(rawData) < 2 { return fmt.Errorf("packet too short: %d bytes", len(rawData)) } - + // Convert client opcode to internal opcode clientVersion := ctx.Client.GetClientVersion() internalOpcode := pp.opcodeManager.ClientOpcodeToInternal(clientVersion, clientOpcode) - + if internalOpcode == OP_Unknown { - fmt.Printf("Unknown opcode 0x%04X from client version %d (client: %s)\n", + fmt.Printf("Unknown opcode 0x%04X from client version %d (client: %s)\n", clientOpcode, clientVersion, ctx.Client.GetCharacterName()) return nil // Don't treat unknown opcodes as errors } - + // Create packet data structure packet := &PacketData{ Opcode: internalOpcode, @@ -168,7 +168,7 @@ func (pp *PacketProcessor) ProcessRawPacket(ctx *PacketContext, rawData []byte, Data: rawData, Size: int32(len(rawData)), } - + // Dispatch to handler return pp.handlerRegistry.HandlePacket(ctx, packet) } @@ -188,11 +188,11 @@ func (pp *PacketProcessor) GetStats() (int, int, []int32) { handlerCount := pp.handlerRegistry.GetHandlerCount() supportedVersions := pp.opcodeManager.GetSupportedVersions() totalOpcodes := 0 - + for _, version := range supportedVersions { totalOpcodes += pp.opcodeManager.GetOpcodeCount(version) } - + return handlerCount, totalOpcodes, supportedVersions } @@ -219,4 +219,4 @@ func ProcessGlobalPacket(ctx *PacketContext, rawData []byte, clientOpcode uint16 // LoadGlobalOpcodeMappings loads opcode mappings with the global processor func LoadGlobalOpcodeMappings(clientVersion int32, opcodeMap map[string]uint16) error { return globalPacketProcessor.LoadOpcodeMap(clientVersion, opcodeMap) -} \ No newline at end of file +} diff --git a/internal/world/README.md b/internal/world/README.md index 320c92d..7a4eca1 100644 --- a/internal/world/README.md +++ b/internal/world/README.md @@ -212,7 +212,7 @@ All server data is persisted to SQLite: func (d *Database) LoadRules() (map[string]map[string]string, error) // Zone management -func (d *Database) GetZones() ([]map[string]interface{}, error) +func (d *Database) GetZones() ([]map[string]any, error) // Character persistence (planned) func (d *Database) SaveCharacter(character *Character) error @@ -354,4 +354,4 @@ Built-in monitoring capabilities: ## Conclusion -The EQ2Go World Server provides a solid foundation for the EverQuest II server emulator. It maintains compatibility with the original protocol while leveraging modern Go patterns for improved reliability, performance, and maintainability. The modular design allows for easy extension and integration with additional game systems as they are implemented. \ No newline at end of file +The EQ2Go World Server provides a solid foundation for the EverQuest II server emulator. It maintains compatibility with the original protocol while leveraging modern Go patterns for improved reliability, performance, and maintainability. The modular design allows for easy extension and integration with additional game systems as they are implemented. diff --git a/internal/world/achievement_events.go b/internal/world/achievement_events.go index 13b9332..182122b 100644 --- a/internal/world/achievement_events.go +++ b/internal/world/achievement_events.go @@ -15,19 +15,19 @@ const ( EventDeathByPlayer EventDamageDealt EventHealingDone - - // Quest events + + // Quest events EventQuestCompleted EventQuestStep EventQuestStarted EventQuestAbandoned - + // Skill events EventSkillIncrease EventSkillMastery EventSpellLearned EventSpellCast - + // Item events EventItemDiscovered EventItemCrafted @@ -35,12 +35,12 @@ const ( EventItemEquipped EventItemSold EventItemBought - + // Exploration events EventZoneDiscovered EventLocationDiscovered EventPOIDiscovered - + // Social events EventGuildJoin EventGuildLeave @@ -48,27 +48,27 @@ const ( EventGroupLeave EventFriendAdded EventPlayerTell - + // Harvesting/Crafting events EventHarvest EventRareHarvest EventCraftingSuccess EventCraftingFailure EventRecipeDiscovered - + // Level/Experience events EventLevelGain EventAAPoint EventExperienceGain EventStatusGain - + // PvP events EventPvPKill EventPvPDeath EventPvPAssist EventArenaWin EventArenaLoss - + // Special events EventHeroicOpportunity EventRaidBoss @@ -80,7 +80,7 @@ const ( type AchievementEvent struct { Type AchievementEventType CharacterID int32 - Data map[string]interface{} + Data map[string]any Timestamp int64 } @@ -101,13 +101,13 @@ func (aeh *AchievementEventHandler) ProcessEvent(event *AchievementEvent) error if event == nil { return fmt.Errorf("event cannot be nil") } - + // Get player's achievement manager achievementMgr := aeh.world.GetAchievementManager() if achievementMgr == nil { return fmt.Errorf("achievement manager not available") } - + // Process different event types switch event.Type { case EventNPCKill: @@ -138,28 +138,28 @@ func (aeh *AchievementEventHandler) handleNPCKill(event *AchievementEvent, achie if !ok { return fmt.Errorf("npc_id not found in event data") } - + level, ok := event.Data["level"].(int32) if !ok { level = 1 // Default level } - + // Update generic kill count achievements err := achievementMgr.UpdateProgress(event.CharacterID, 1, 1) // Achievement ID 1: "First Blood" if err != nil { fmt.Printf("Error updating kill achievement: %v\n", err) } - + // Update level-specific kill achievements if level >= 10 { achievementMgr.UpdateProgress(event.CharacterID, 2, 1) // Achievement ID 2: "Veteran Hunter" } - + // Update specific NPC kill achievements (example) if npcID == 100 { // Boss NPC achievementMgr.UpdateProgress(event.CharacterID, 10, 1) // Achievement ID 10: "Boss Slayer" } - + return nil } @@ -169,18 +169,18 @@ func (aeh *AchievementEventHandler) handleQuestCompleted(event *AchievementEvent if !ok { return fmt.Errorf("quest_id not found in event data") } - + // Update quest completion achievements err := achievementMgr.UpdateProgress(event.CharacterID, 20, 1) // Achievement ID 20: "Quest Master" if err != nil { fmt.Printf("Error updating quest achievement: %v\n", err) } - + // Update specific quest achievements if questID == 1000 { // Main story quest achievementMgr.UpdateProgress(event.CharacterID, 21, 1) // Achievement ID 21: "Hero's Journey" } - + return nil } @@ -190,7 +190,7 @@ func (aeh *AchievementEventHandler) handleLevelGain(event *AchievementEvent, ach if !ok { return fmt.Errorf("level not found in event data") } - + // Update level-based achievements switch newLevel { case 10: @@ -202,10 +202,10 @@ func (aeh *AchievementEventHandler) handleLevelGain(event *AchievementEvent, ach case 90: achievementMgr.UpdateProgress(event.CharacterID, 33, 1) // Achievement ID 33: "Master Adventurer" } - + // Update max level achievement achievementMgr.UpdateProgress(event.CharacterID, 34, uint32(newLevel)) // Achievement ID 34: "Level Up!" - + return nil } @@ -215,12 +215,12 @@ func (aeh *AchievementEventHandler) handleSkillIncrease(event *AchievementEvent, if !ok { return fmt.Errorf("skill_id not found in event data") } - + skillLevel, ok := event.Data["skill_level"].(int32) if !ok { return fmt.Errorf("skill_level not found in event data") } - + // Update skill mastery achievements based on skill type switch skillID { case 1: // Melee skill @@ -236,7 +236,7 @@ func (aeh *AchievementEventHandler) handleSkillIncrease(event *AchievementEvent, achievementMgr.UpdateProgress(event.CharacterID, 42, 1) // Achievement ID 42: "Master Craftsman" } } - + return nil } @@ -246,15 +246,15 @@ func (aeh *AchievementEventHandler) handleItemDiscovered(event *AchievementEvent if !ok { return fmt.Errorf("item_id not found in event data") } - + rarity, ok := event.Data["rarity"].(string) if !ok { rarity = "common" } - + // Update item discovery achievements achievementMgr.UpdateProgress(event.CharacterID, 50, 1) // Achievement ID 50: "Treasure Hunter" - + // Update rarity-specific achievements switch rarity { case "rare": @@ -264,12 +264,12 @@ func (aeh *AchievementEventHandler) handleItemDiscovered(event *AchievementEvent case "mythical": achievementMgr.UpdateProgress(event.CharacterID, 53, 1) // Achievement ID 53: "Myth Walker" } - + // Specific item achievements if itemID == 12345 { // Special artifact achievementMgr.UpdateProgress(event.CharacterID, 54, 1) // Achievement ID 54: "Ancient Artifact" } - + return nil } @@ -279,10 +279,10 @@ func (aeh *AchievementEventHandler) handleZoneDiscovered(event *AchievementEvent if !ok { return fmt.Errorf("zone_id not found in event data") } - + // Update exploration achievements achievementMgr.UpdateProgress(event.CharacterID, 60, 1) // Achievement ID 60: "Explorer" - + // Update specific zone achievements switch zoneID { case 1: // Starting zone @@ -290,7 +290,7 @@ func (aeh *AchievementEventHandler) handleZoneDiscovered(event *AchievementEvent case 100: // End game zone achievementMgr.UpdateProgress(event.CharacterID, 62, 1) // Achievement ID 62: "Into the Unknown" } - + return nil } @@ -300,12 +300,12 @@ func (aeh *AchievementEventHandler) handleHarvest(event *AchievementEvent, achie if !ok { return fmt.Errorf("resource_type not found in event data") } - + isRare, _ := event.Data["is_rare"].(bool) - + // Update harvesting achievements achievementMgr.UpdateProgress(event.CharacterID, 70, 1) // Achievement ID 70: "Gatherer" - + // Update resource-specific achievements switch resourceType { case "ore": @@ -315,12 +315,12 @@ func (aeh *AchievementEventHandler) handleHarvest(event *AchievementEvent, achie case "fish": achievementMgr.UpdateProgress(event.CharacterID, 73, 1) // Achievement ID 73: "Angler" } - + // Update rare harvest achievement if isRare { achievementMgr.UpdateProgress(event.CharacterID, 74, 1) // Achievement ID 74: "Lucky Find" } - + return nil } @@ -330,15 +330,15 @@ func (aeh *AchievementEventHandler) handlePvPKill(event *AchievementEvent, achie if !ok { targetLevel = 1 } - + // Update PvP achievements achievementMgr.UpdateProgress(event.CharacterID, 80, 1) // Achievement ID 80: "First Blood PvP" - + // Update level-based PvP achievements if targetLevel >= 50 { achievementMgr.UpdateProgress(event.CharacterID, 81, 1) // Achievement ID 81: "Veteran Slayer" } - + return nil } @@ -346,27 +346,27 @@ func (aeh *AchievementEventHandler) handlePvPKill(event *AchievementEvent, achie func (aeh *AchievementEventHandler) handleGenericEvent(event *AchievementEvent, achievementMgr *AchievementManager) error { // For events without specific handlers, attempt generic progress updates // This allows for easy extension without requiring handler updates - + // Log unhandled event types for debugging - fmt.Printf("Unhandled achievement event type: %d for character %d\n", + fmt.Printf("Unhandled achievement event type: %d for character %d\n", int(event.Type), event.CharacterID) - + return nil } // TriggerEvent is a convenience method for triggering achievement events -func (w *World) TriggerAchievementEvent(eventType AchievementEventType, characterID int32, data map[string]interface{}) { +func (w *World) TriggerAchievementEvent(eventType AchievementEventType, characterID int32, data map[string]any) { if w.achievementMgr == nil { return // Achievement system not initialized } - + event := &AchievementEvent{ Type: eventType, CharacterID: characterID, Data: data, Timestamp: int64(w.worldTime.Year), // Use game time as timestamp } - + handler := NewAchievementEventHandler(w) go func() { if err := handler.ProcessEvent(event); err != nil { @@ -379,7 +379,7 @@ func (w *World) TriggerAchievementEvent(eventType AchievementEventType, characte // OnNPCKill triggers an NPC kill achievement event func (w *World) OnNPCKill(characterID int32, npcID int32, npcLevel int32) { - w.TriggerAchievementEvent(EventNPCKill, characterID, map[string]interface{}{ + w.TriggerAchievementEvent(EventNPCKill, characterID, map[string]any{ "npc_id": npcID, "level": npcLevel, }) @@ -387,21 +387,21 @@ func (w *World) OnNPCKill(characterID int32, npcID int32, npcLevel int32) { // OnQuestComplete triggers a quest completion achievement event func (w *World) OnQuestComplete(characterID int32, questID int32) { - w.TriggerAchievementEvent(EventQuestCompleted, characterID, map[string]interface{}{ + w.TriggerAchievementEvent(EventQuestCompleted, characterID, map[string]any{ "quest_id": questID, }) } // OnLevelGain triggers a level gain achievement event func (w *World) OnLevelGain(characterID int32, newLevel int32) { - w.TriggerAchievementEvent(EventLevelGain, characterID, map[string]interface{}{ + w.TriggerAchievementEvent(EventLevelGain, characterID, map[string]any{ "level": newLevel, }) } // OnItemDiscovered triggers an item discovery achievement event func (w *World) OnItemDiscovered(characterID int32, itemID int32, rarity string) { - w.TriggerAchievementEvent(EventItemDiscovered, characterID, map[string]interface{}{ + w.TriggerAchievementEvent(EventItemDiscovered, characterID, map[string]any{ "item_id": itemID, "rarity": rarity, }) @@ -409,7 +409,7 @@ func (w *World) OnItemDiscovered(characterID int32, itemID int32, rarity string) // OnZoneDiscovered triggers a zone discovery achievement event func (w *World) OnZoneDiscovered(characterID int32, zoneID int32) { - w.TriggerAchievementEvent(EventZoneDiscovered, characterID, map[string]interface{}{ + w.TriggerAchievementEvent(EventZoneDiscovered, characterID, map[string]any{ "zone_id": zoneID, }) -} \ No newline at end of file +} diff --git a/internal/world/achievement_manager.go b/internal/world/achievement_manager.go index 28c6f66..51b7182 100644 --- a/internal/world/achievement_manager.go +++ b/internal/world/achievement_manager.go @@ -3,19 +3,19 @@ package world import ( "fmt" "sync" - + "eq2emu/internal/achievements" "eq2emu/internal/database" ) // AchievementManager manages achievements for the world server type AchievementManager struct { - masterList *achievements.MasterList - playerManagers map[int32]*achievements.PlayerManager // CharacterID -> PlayerManager - database *database.Database - world *World // Reference to world server for notifications - - mutex sync.RWMutex + masterList *achievements.MasterList + playerManagers map[int32]*achievements.PlayerManager // CharacterID -> PlayerManager + database *database.Database + world *World // Reference to world server for notifications + + mutex sync.RWMutex } // NewAchievementManager creates a new achievement manager @@ -36,17 +36,17 @@ func (am *AchievementManager) SetWorld(world *World) { // LoadAchievements loads all achievements from database func (am *AchievementManager) LoadAchievements() error { fmt.Println("Loading master achievement list...") - + pool := am.database.GetPool() if pool == nil { return fmt.Errorf("database pool is nil") } - + err := achievements.LoadAllAchievements(pool, am.masterList) if err != nil { return fmt.Errorf("failed to load achievements: %w", err) } - + fmt.Printf("Loaded %d achievements\n", am.masterList.Size()) return nil } @@ -56,26 +56,26 @@ func (am *AchievementManager) GetPlayerManager(characterID int32) *achievements. am.mutex.RLock() playerMgr, exists := am.playerManagers[characterID] am.mutex.RUnlock() - + if exists { return playerMgr } - + // Create new player manager and load data am.mutex.Lock() defer am.mutex.Unlock() - + // Double-check after acquiring write lock if playerMgr, exists := am.playerManagers[characterID]; exists { return playerMgr } - + playerMgr = achievements.NewPlayerManager() am.playerManagers[characterID] = playerMgr - + // Load player achievement data from database go am.loadPlayerAchievements(characterID, playerMgr) - + return playerMgr } @@ -86,13 +86,13 @@ func (am *AchievementManager) loadPlayerAchievements(characterID int32, playerMg fmt.Printf("Error: database pool is nil for character %d\n", characterID) return } - + // Load player achievements err := achievements.LoadPlayerAchievements(pool, uint32(characterID), playerMgr.Achievements) if err != nil { fmt.Printf("Error loading achievements for character %d: %v\n", characterID, err) } - + // Load player progress err = achievements.LoadPlayerAchievementUpdates(pool, uint32(characterID), playerMgr.Updates) if err != nil { @@ -106,10 +106,10 @@ func (am *AchievementManager) UpdateProgress(characterID int32, achievementID ui if playerMgr == nil { return fmt.Errorf("failed to get player manager for character %d", characterID) } - + // Update progress playerMgr.Updates.UpdateProgress(achievementID, progress) - + // Check if achievement is completed achievement := am.masterList.GetAchievement(achievementID) if achievement != nil { @@ -117,24 +117,24 @@ func (am *AchievementManager) UpdateProgress(characterID int32, achievementID ui if err != nil { return fmt.Errorf("failed to check requirements: %w", err) } - + if completed && !playerMgr.Updates.IsCompleted(achievementID) { // Complete the achievement playerMgr.Updates.CompleteAchievement(achievementID) - + // Save progress to database go am.savePlayerProgress(characterID, achievementID, playerMgr) - + // Trigger achievement completion event go am.onAchievementCompleted(characterID, achievement) - + fmt.Printf("Character %d completed achievement: %s\n", characterID, achievement.Title) } else if progress > 0 { // Save progress update to database go am.savePlayerProgress(characterID, achievementID, playerMgr) } } - + return nil } @@ -144,16 +144,16 @@ func (am *AchievementManager) savePlayerProgress(characterID int32, achievementI if update == nil { return } - + pool := am.database.GetPool() if pool == nil { fmt.Printf("Error: database pool is nil for character %d\n", characterID) return } - + err := achievements.SavePlayerAchievementUpdate(pool, uint32(characterID), update) if err != nil { - fmt.Printf("Error saving achievement progress for character %d, achievement %d: %v\n", + fmt.Printf("Error saving achievement progress for character %d, achievement %d: %v\n", characterID, achievementID, err) } } @@ -165,12 +165,12 @@ func (am *AchievementManager) onAchievementCompleted(characterID int32, achievem // Increment player's achievement points fmt.Printf("Character %d earned %d achievement points\n", characterID, achievement.PointValue) } - + // Process rewards for _, reward := range achievement.Rewards { am.processReward(characterID, reward) } - + // Notify other systems about achievement completion am.notifyAchievementCompleted(characterID, achievement.ID) } @@ -231,7 +231,7 @@ func (am *AchievementManager) GetPlayerProgress(characterID int32, achievementID if playerMgr == nil { return 0 } - + return playerMgr.Updates.GetProgress(achievementID) } @@ -241,7 +241,7 @@ func (am *AchievementManager) IsPlayerCompleted(characterID int32, achievementID if playerMgr == nil { return false } - + return playerMgr.Updates.IsCompleted(achievementID) } @@ -251,7 +251,7 @@ func (am *AchievementManager) GetPlayerCompletedAchievements(characterID int32) if playerMgr == nil { return nil } - + return playerMgr.Updates.GetCompletedAchievements() } @@ -261,7 +261,7 @@ func (am *AchievementManager) GetPlayerInProgressAchievements(characterID int32) if playerMgr == nil { return nil } - + return playerMgr.Updates.GetInProgressAchievements() } @@ -271,12 +271,12 @@ func (am *AchievementManager) GetCompletionPercentage(characterID int32, achieve if playerMgr == nil { return 0.0 } - + achievement := am.masterList.GetAchievement(achievementID) if achievement == nil { return 0.0 } - + return playerMgr.GetCompletionStatus(achievement) } @@ -284,41 +284,41 @@ func (am *AchievementManager) GetCompletionPercentage(characterID int32, achieve func (am *AchievementManager) RemovePlayerManager(characterID int32) { am.mutex.Lock() defer am.mutex.Unlock() - + delete(am.playerManagers, characterID) } // GetStatistics returns achievement system statistics -func (am *AchievementManager) GetStatistics() map[string]interface{} { +func (am *AchievementManager) GetStatistics() map[string]any { am.mutex.RLock() defer am.mutex.RUnlock() - - stats := map[string]interface{}{ - "total_achievements": am.masterList.Size(), - "online_players": len(am.playerManagers), - "categories": am.masterList.GetCategories(), - "expansions": am.masterList.GetExpansions(), + + stats := map[string]any{ + "total_achievements": am.masterList.Size(), + "online_players": len(am.playerManagers), + "categories": am.masterList.GetCategories(), + "expansions": am.masterList.GetExpansions(), } - + return stats } // Shutdown gracefully shuts down the achievement manager func (am *AchievementManager) Shutdown() { fmt.Println("Shutting down achievement manager...") - + am.mutex.Lock() defer am.mutex.Unlock() - + // Save all player progress before shutdown for characterID, playerMgr := range am.playerManagers { for _, achievementID := range playerMgr.Updates.GetInProgressAchievements() { am.savePlayerProgress(characterID, achievementID, playerMgr) } } - + // Clear player managers am.playerManagers = make(map[int32]*achievements.PlayerManager) - + fmt.Println("Achievement manager shutdown complete") -} \ No newline at end of file +} diff --git a/internal/world/client_list.go b/internal/world/client_list.go index 39b17b9..29fcd99 100644 --- a/internal/world/client_list.go +++ b/internal/world/client_list.go @@ -15,64 +15,64 @@ import ( // Client represents a connected player client type Client struct { // Account information - AccountID int32 - AccountName string - AdminLevel int - + AccountID int32 + AccountName string + AdminLevel int + // Character information - CharacterID int32 - CharacterName string - Player *entity.Entity - + CharacterID int32 + CharacterName string + Player *entity.Entity + // Connection information - Connection interface{} // TODO: Will be *udp.Connection - IPAddress string - ConnectedTime time.Time - LastActivity time.Time - ClientVersion int32 // EQ2 client version - + Connection any // TODO: Will be *udp.Connection + IPAddress string + ConnectedTime time.Time + LastActivity time.Time + ClientVersion int32 // EQ2 client version + // Zone information - CurrentZone *ZoneServer - ZoneID int32 - + CurrentZone *ZoneServer + ZoneID int32 + // State flags - IsConnected bool - IsLinkdead bool - IsAFK bool - IsAnonymous bool - IsLFG bool - + IsConnected bool + IsLinkdead bool + IsAFK bool + IsAnonymous bool + IsLFG bool + // Chat state - LastTellFrom string - IgnoreList map[string]bool - + LastTellFrom string + IgnoreList map[string]bool + // Group/Guild - GroupID int32 - GuildID int32 - + GroupID int32 + GuildID int32 + // Pending operations - PendingZone *ZoneChangeDetails - - mutex sync.RWMutex + PendingZone *ZoneChangeDetails + + mutex sync.RWMutex } // ZoneChangeDetails holds information about a zone change type ZoneChangeDetails struct { - ZoneID int32 - InstanceID int32 - X float32 - Y float32 - Z float32 - Heading float32 + ZoneID int32 + InstanceID int32 + X float32 + Y float32 + Z float32 + Heading float32 } // ClientList manages all connected clients type ClientList struct { - clients map[int32]*Client // CharacterID -> Client - clientsByName map[string]*Client // Lowercase name -> Client - clientsByAcct map[int32][]*Client // AccountID -> Clients - - mutex sync.RWMutex + clients map[int32]*Client // CharacterID -> Client + clientsByName map[string]*Client // Lowercase name -> Client + clientsByAcct map[int32][]*Client // AccountID -> Clients + + mutex sync.RWMutex } // NewClientList creates a new client list @@ -88,22 +88,22 @@ func NewClientList() *ClientList { func (cl *ClientList) Add(client *Client) error { cl.mutex.Lock() defer cl.mutex.Unlock() - + if _, exists := cl.clients[client.CharacterID]; exists { return fmt.Errorf("client with character ID %d already exists", client.CharacterID) } - + // Add to maps cl.clients[client.CharacterID] = client cl.clientsByName[strings.ToLower(client.CharacterName)] = client - + // Add to account map cl.clientsByAcct[client.AccountID] = append(cl.clientsByAcct[client.AccountID], client) - + client.ConnectedTime = time.Now() client.LastActivity = time.Now() client.IsConnected = true - + return nil } @@ -111,16 +111,16 @@ func (cl *ClientList) Add(client *Client) error { func (cl *ClientList) Remove(characterID int32) { cl.mutex.Lock() defer cl.mutex.Unlock() - + client, exists := cl.clients[characterID] if !exists { return } - + // Remove from maps delete(cl.clients, characterID) delete(cl.clientsByName, strings.ToLower(client.CharacterName)) - + // Remove from account map if clients, ok := cl.clientsByAcct[client.AccountID]; ok { newClients := make([]*Client, 0, len(clients)-1) @@ -141,7 +141,7 @@ func (cl *ClientList) Remove(characterID int32) { func (cl *ClientList) GetByCharacterID(characterID int32) *Client { cl.mutex.RLock() defer cl.mutex.RUnlock() - + return cl.clients[characterID] } @@ -149,7 +149,7 @@ func (cl *ClientList) GetByCharacterID(characterID int32) *Client { func (cl *ClientList) GetByCharacterName(name string) *Client { cl.mutex.RLock() defer cl.mutex.RUnlock() - + return cl.clientsByName[strings.ToLower(name)] } @@ -157,7 +157,7 @@ func (cl *ClientList) GetByCharacterName(name string) *Client { func (cl *ClientList) GetByAccountID(accountID int32) []*Client { cl.mutex.RLock() defer cl.mutex.RUnlock() - + clients := cl.clientsByAcct[accountID] result := make([]*Client, len(clients)) copy(result, clients) @@ -168,7 +168,7 @@ func (cl *ClientList) GetByAccountID(accountID int32) []*Client { func (cl *ClientList) Count() int32 { cl.mutex.RLock() defer cl.mutex.RUnlock() - + return int32(len(cl.clients)) } @@ -176,7 +176,7 @@ func (cl *ClientList) Count() int32 { func (cl *ClientList) GetAll() []*Client { cl.mutex.RLock() defer cl.mutex.RUnlock() - + result := make([]*Client, 0, len(cl.clients)) for _, client := range cl.clients { result = append(result, client) @@ -187,7 +187,7 @@ func (cl *ClientList) GetAll() []*Client { // ProcessAll processes all clients func (cl *ClientList) ProcessAll() { clients := cl.GetAll() - + now := time.Now() for _, client := range clients { client.Process(now) @@ -197,7 +197,7 @@ func (cl *ClientList) ProcessAll() { // DisconnectAll disconnects all clients func (cl *ClientList) DisconnectAll(reason string) { clients := cl.GetAll() - + for _, client := range clients { client.DisconnectWithReason(reason) } @@ -206,7 +206,7 @@ func (cl *ClientList) DisconnectAll(reason string) { // BroadcastMessage sends a message to all clients func (cl *ClientList) BroadcastMessage(message string) { clients := cl.GetAll() - + for _, client := range clients { client.SendSimpleMessage(message) } @@ -216,24 +216,24 @@ func (cl *ClientList) BroadcastMessage(message string) { func (c *Client) Process(now time.Time) { c.mutex.Lock() defer c.mutex.Unlock() - + if !c.IsConnected { return } - + // Check for linkdead timeout if now.Sub(c.LastActivity) > 5*time.Minute { if !c.IsLinkdead { c.IsLinkdead = true fmt.Printf("Client %s has gone linkdead\n", c.CharacterName) } - + // Disconnect after 10 minutes if now.Sub(c.LastActivity) > 10*time.Minute { c.DisconnectWithReason("Linkdead timeout") } } - + // Process pending zone change if c.PendingZone != nil { // TODO: Implement zone change @@ -245,22 +245,22 @@ func (c *Client) Process(now time.Time) { func (c *Client) DisconnectWithReason(reason string) { c.mutex.Lock() defer c.mutex.Unlock() - + if !c.IsConnected { return } - + fmt.Printf("Disconnecting client %s: %s\n", c.CharacterName, reason) - + // Remove from current zone if c.CurrentZone != nil { c.CurrentZone.RemoveClient(c.CharacterID) c.CurrentZone = nil } - + // TODO: Save character data // TODO: Close connection - + c.IsConnected = false } @@ -274,7 +274,7 @@ func (c *Client) SendSimpleMessage(message string) { func (c *Client) Zone(details *ZoneChangeDetails) { c.mutex.Lock() defer c.mutex.Unlock() - + c.PendingZone = details } @@ -282,7 +282,7 @@ func (c *Client) Zone(details *ZoneChangeDetails) { func (c *Client) UpdateActivity() { c.mutex.Lock() defer c.mutex.Unlock() - + c.LastActivity = time.Now() if c.IsLinkdead { c.IsLinkdead = false @@ -411,7 +411,7 @@ func (c *Client) SetClientVersion(version int32) { func (c *Client) ProcessPacket(world *World, rawData []byte, clientOpcode uint16) error { // Create packet context ctx := world.CreatePacketContext(c) - + // Process the packet through the global packet processor return packets.ProcessGlobalPacket(ctx, rawData, clientOpcode) -} \ No newline at end of file +} diff --git a/internal/world/item_manager.go b/internal/world/item_manager.go index f480aef..8797c4c 100644 --- a/internal/world/item_manager.go +++ b/internal/world/item_manager.go @@ -10,39 +10,39 @@ import ( // ItemManager manages items for the world server type ItemManager struct { - masterItemList *items.MasterItemList + masterItemList *items.MasterItemList itemSystemAdapter *items.ItemSystemAdapter - database *database.Database - world *World // Reference to world server - + database *database.Database + world *World // Reference to world server + // World-specific item tracking - playerInventories map[uint32]*items.PlayerItemList // Player ID -> Inventory - playerEquipment map[uint32]*items.EquipmentItemList // Player ID -> Equipment - worldDrops map[int32][]*items.Item // Zone ID -> Ground items - - mutex sync.RWMutex + playerInventories map[uint32]*items.PlayerItemList // Player ID -> Inventory + playerEquipment map[uint32]*items.EquipmentItemList // Player ID -> Equipment + worldDrops map[int32][]*items.Item // Zone ID -> Ground items + + mutex sync.RWMutex } // NewItemManager creates a new item manager for the world server func NewItemManager(db *database.Database) *ItemManager { // Create master item list masterList := items.NewMasterItemList() - + // Create adapters for the item system dbAdapter := &WorldItemDatabaseAdapter{db: db} playerAdapter := &WorldItemPlayerAdapter{} packetAdapter := &WorldItemPacketAdapter{} ruleAdapter := &WorldItemRuleAdapter{} - + // Create mock adapters for optional dependencies questAdapter := &MockItemQuestAdapter{} brokerAdapter := &MockItemBrokerAdapter{} craftingAdapter := &MockItemCraftingAdapter{} housingAdapter := &MockItemHousingAdapter{} - + // Create loot manager adapter lootAdapter := &WorldItemLootAdapter{} - + // Create item system adapter systemAdapter := items.NewItemSystemAdapter( masterList, @@ -57,11 +57,11 @@ func NewItemManager(db *database.Database) *ItemManager { housingAdapter, lootAdapter, ) - + return &ItemManager{ masterItemList: masterList, itemSystemAdapter: systemAdapter, - database: db, + database: db, playerInventories: make(map[uint32]*items.PlayerItemList), playerEquipment: make(map[uint32]*items.EquipmentItemList), worldDrops: make(map[int32][]*items.Item), @@ -76,13 +76,13 @@ func (im *ItemManager) SetWorld(world *World) { // LoadItems loads all item templates and data from database func (im *ItemManager) LoadItems() error { fmt.Println("Loading item data...") - + // Initialize the item system adapter err := im.itemSystemAdapter.Initialize() if err != nil { return fmt.Errorf("failed to initialize item system: %w", err) } - + stats := im.masterItemList.GetStats() fmt.Printf("Loaded %d item templates\n", stats.TotalItems) return nil @@ -92,7 +92,7 @@ func (im *ItemManager) LoadItems() error { func (im *ItemManager) GetPlayerInventory(playerID uint32) (*items.PlayerItemList, error) { im.mutex.RLock() defer im.mutex.RUnlock() - + return im.itemSystemAdapter.GetPlayerInventory(playerID) } @@ -100,7 +100,7 @@ func (im *ItemManager) GetPlayerInventory(playerID uint32) (*items.PlayerItemLis func (im *ItemManager) GetPlayerEquipment(playerID uint32, appearanceType int8) (*items.EquipmentItemList, error) { im.mutex.RLock() defer im.mutex.RUnlock() - + return im.itemSystemAdapter.GetPlayerEquipment(playerID, appearanceType) } @@ -133,26 +133,26 @@ func (im *ItemManager) MoveItem(playerID uint32, fromBagID int32, fromSlot int16 func (im *ItemManager) CreateWorldDrop(itemID int32, quantity int16, x, y, z float32, zoneID int32) error { im.mutex.Lock() defer im.mutex.Unlock() - + // Get item template itemTemplate := im.masterItemList.GetItem(itemID) if itemTemplate == nil { return fmt.Errorf("item template not found: %d", itemID) } - + // Create item instance item := items.NewItemFromTemplate(itemTemplate) item.Details.Count = quantity - + // Set position data (would normally be in a WorldItem wrapper) // TODO: Create WorldItem wrapper with position data - + // Add to world drops tracking im.worldDrops[zoneID] = append(im.worldDrops[zoneID], item) - + fmt.Printf("Created world drop: %s x%d at (%.2f, %.2f, %.2f) in zone %d\n", item.Name, quantity, x, y, z, zoneID) - + return nil } @@ -160,14 +160,14 @@ func (im *ItemManager) CreateWorldDrop(itemID int32, quantity int16, x, y, z flo func (im *ItemManager) GetWorldDrops(zoneID int32) []*items.Item { im.mutex.RLock() defer im.mutex.RUnlock() - + if drops, exists := im.worldDrops[zoneID]; exists { // Return a copy to avoid concurrent modification result := make([]*items.Item, len(drops)) copy(result, drops) return result } - + return nil } @@ -175,16 +175,16 @@ func (im *ItemManager) GetWorldDrops(zoneID int32) []*items.Item { func (im *ItemManager) PickupWorldDrop(playerID uint32, itemUniqueID int32, zoneID int32) error { im.mutex.Lock() defer im.mutex.Unlock() - + // Find the item in world drops drops, exists := im.worldDrops[zoneID] if !exists { return fmt.Errorf("no drops in zone %d", zoneID) } - + var foundItem *items.Item var itemIndex int = -1 - + for i, item := range drops { if int32(item.Details.UniqueID) == itemUniqueID { foundItem = item @@ -192,20 +192,20 @@ func (im *ItemManager) PickupWorldDrop(playerID uint32, itemUniqueID int32, zone break } } - + if foundItem == nil { return fmt.Errorf("item not found: %d", itemUniqueID) } - + // Try to give item to player err := im.GiveItemToPlayer(playerID, foundItem.Details.ItemID, foundItem.Details.Count) if err != nil { return fmt.Errorf("failed to give item to player: %w", err) } - + // Remove from world drops im.worldDrops[zoneID] = append(drops[:itemIndex], drops[itemIndex+1:]...) - + fmt.Printf("Player %d picked up item: %s x%d\n", playerID, foundItem.Name, foundItem.Details.Count) return nil } @@ -214,12 +214,12 @@ func (im *ItemManager) PickupWorldDrop(playerID uint32, itemUniqueID int32, zone func (im *ItemManager) GenerateLootForNPC(npcID int32, killerLevel int16) ([]*items.Item, error) { // TODO: Implement proper loot table lookup and generation // For now, create some basic test loot - + testLoot := []*items.Item{} - + // Example: Generate some coins based on NPC level // This would normally come from loot tables in the database - + fmt.Printf("Generated loot for NPC %d (placeholder implementation)\n", npcID) return testLoot, nil } @@ -227,18 +227,18 @@ func (im *ItemManager) GenerateLootForNPC(npcID int32, killerLevel int16) ([]*it // OnPlayerLogin handles player login - loads their item data func (im *ItemManager) OnPlayerLogin(playerID uint32) error { fmt.Printf("Loading item data for player %d...\n", playerID) - + // Pre-load player inventory and equipment _, err := im.GetPlayerInventory(playerID) if err != nil { return fmt.Errorf("failed to load player inventory: %w", err) } - + _, err = im.GetPlayerEquipment(playerID, items.BaseEquipment) if err != nil { return fmt.Errorf("failed to load player equipment: %w", err) } - + fmt.Printf("Item data loaded for player %d\n", playerID) return nil } @@ -246,16 +246,16 @@ func (im *ItemManager) OnPlayerLogin(playerID uint32) error { // OnPlayerLogout handles player logout - saves and clears their item data func (im *ItemManager) OnPlayerLogout(playerID uint32) error { fmt.Printf("Saving item data for player %d...\n", playerID) - + // Save player data err := im.itemSystemAdapter.SavePlayerData(playerID) if err != nil { return fmt.Errorf("failed to save player data: %w", err) } - + // Clear cached data im.itemSystemAdapter.ClearPlayerData(playerID) - + fmt.Printf("Item data saved and cleared for player %d\n", playerID) return nil } @@ -272,26 +272,26 @@ func (im *ItemManager) SearchItems(name string, maxResults int32) []*items.Item } // GetStatistics returns item system statistics -func (im *ItemManager) GetStatistics() map[string]interface{} { +func (im *ItemManager) GetStatistics() map[string]any { im.mutex.RLock() defer im.mutex.RUnlock() - + systemStats := im.itemSystemAdapter.GetSystemStats() - + // Add world-specific statistics totalWorldDrops := 0 for _, drops := range im.worldDrops { totalWorldDrops += len(drops) } - - result := make(map[string]interface{}) + + result := make(map[string]any) for k, v := range systemStats { result[k] = v } - + result["world_drops"] = totalWorldDrops result["zones_with_drops"] = len(im.worldDrops) - + return result } @@ -303,15 +303,15 @@ func (im *ItemManager) ValidatePlayerItems(playerID uint32) *items.ItemValidatio // Shutdown gracefully shuts down the item manager func (im *ItemManager) Shutdown() { fmt.Println("Shutting down item manager...") - + im.mutex.Lock() defer im.mutex.Unlock() - + // Clear all tracking im.playerInventories = make(map[uint32]*items.PlayerItemList) im.playerEquipment = make(map[uint32]*items.EquipmentItemList) im.worldDrops = make(map[int32][]*items.Item) - + fmt.Println("Item manager shutdown complete") } @@ -323,7 +323,7 @@ type WorldItemDatabaseAdapter struct { // LoadItems implements items.DatabaseService interface func (wdb *WorldItemDatabaseAdapter) LoadItems(masterList *items.MasterItemList) error { fmt.Println("Loading items from database...") - + // Load item templates from database rows, err := wdb.db.Query(` SELECT id, name, item_type, icon, description, tier, level, classes, races, @@ -336,17 +336,17 @@ func (wdb *WorldItemDatabaseAdapter) LoadItems(masterList *items.MasterItemList) return fmt.Errorf("failed to query items: %w", err) } defer rows.Close() - + itemCount := 0 for rows.Next() { // Create new item from database data item := items.NewItem() - + var id, itemType, icon, tier, level, stackCount int32 var weight, genericInfo, bagInfo, foodInfo, weaponInfo, rangedInfo int32 var details, appearance int32 var name, description, classes, races, adventureClasses, tradeskillClasses string - + err := rows.Scan(&id, &name, &itemType, &icon, &description, &tier, &level, &classes, &races, &adventureClasses, &tradeskillClasses, &stackCount, &weight, &genericInfo, &bagInfo, &foodInfo, &weaponInfo, &rangedInfo, @@ -355,7 +355,7 @@ func (wdb *WorldItemDatabaseAdapter) LoadItems(masterList *items.MasterItemList) fmt.Printf("Error scanning item row: %v\n", err) continue } - + // Set item properties item.Details.ItemID = id item.Name = name @@ -366,25 +366,25 @@ func (wdb *WorldItemDatabaseAdapter) LoadItems(masterList *items.MasterItemList) item.StackCount = int16(stackCount) // TODO: Set weight when field is available // item.Weight = weight - + // TODO: Parse class/race strings and set appropriate fields // item.SetClasses(parseClassString(classes)) // item.SetRaces(parseRaceString(races)) - + // Add to master list masterList.AddItem(item) itemCount++ } - + if err := rows.Err(); err != nil { return fmt.Errorf("error iterating item rows: %w", err) } - + fmt.Printf("Successfully loaded %d item templates from database\n", itemCount) return nil } -// SaveItem implements items.DatabaseService interface +// SaveItem implements items.DatabaseService interface func (wdb *WorldItemDatabaseAdapter) SaveItem(item *items.Item) error { // TODO: Implement item template saving fmt.Printf("Saving item template: %s (ID: %d) - not yet implemented\n", item.Name, item.Details.ItemID) @@ -432,27 +432,27 @@ func (wdb *WorldItemDatabaseAdapter) SavePlayerEquipment(playerID uint32, equipm func (wdb *WorldItemDatabaseAdapter) LoadItemStats() (map[string]int32, map[int32]string, error) { // TODO: Implement item stat mapping loading fmt.Println("Loading item stats - using placeholder data") - + statsStrings := map[string]int32{ - "health": 1, - "power": 2, - "strength": 3, - "stamina": 4, - "agility": 5, - "wisdom": 6, + "health": 1, + "power": 2, + "strength": 3, + "stamina": 4, + "agility": 5, + "wisdom": 6, "intelligence": 7, } - + statsIDs := map[int32]string{ 1: "health", - 2: "power", + 2: "power", 3: "strength", 4: "stamina", 5: "agility", 6: "wisdom", 7: "intelligence", } - + return statsStrings, statsIDs, nil } @@ -601,14 +601,14 @@ type MockPlayer struct { alignment int8 } -func (mp *MockPlayer) GetID() uint32 { return mp.id } -func (mp *MockPlayer) GetName() string { return mp.name } -func (mp *MockPlayer) GetLevel() int16 { return mp.level } -func (mp *MockPlayer) GetAdventureClass() int8 { return mp.adventureClass } -func (mp *MockPlayer) GetTradeskillClass() int8 { return mp.tradeskillClass } -func (mp *MockPlayer) GetRace() int8 { return mp.race } -func (mp *MockPlayer) GetGender() int8 { return mp.gender } -func (mp *MockPlayer) GetAlignment() int8 { return mp.alignment } +func (mp *MockPlayer) GetID() uint32 { return mp.id } +func (mp *MockPlayer) GetName() string { return mp.name } +func (mp *MockPlayer) GetLevel() int16 { return mp.level } +func (mp *MockPlayer) GetAdventureClass() int8 { return mp.adventureClass } +func (mp *MockPlayer) GetTradeskillClass() int8 { return mp.tradeskillClass } +func (mp *MockPlayer) GetRace() int8 { return mp.race } +func (mp *MockPlayer) GetGender() int8 { return mp.gender } +func (mp *MockPlayer) GetAlignment() int8 { return mp.alignment } // MockItemQuestAdapter provides mock quest functionality type MockItemQuestAdapter struct{} @@ -676,4 +676,4 @@ func (mha *MockItemHousingAdapter) RemoveItem(playerID uint32, houseID int32, it func (mha *MockItemHousingAdapter) GetHouseItems(houseID int32) ([]*items.Item, error) { return []*items.Item{}, nil -} \ No newline at end of file +} diff --git a/internal/world/npc_manager.go b/internal/world/npc_manager.go index dc3b91c..c3d9a4f 100644 --- a/internal/world/npc_manager.go +++ b/internal/world/npc_manager.go @@ -3,24 +3,24 @@ package world import ( "fmt" "sync" - + + "eq2emu/internal/database" "eq2emu/internal/npc" "eq2emu/internal/npc/ai" - "eq2emu/internal/database" ) // NPCManager manages NPCs for the world server type NPCManager struct { - npcManager *npc.Manager - aiManager *ai.AIManager - database *database.Database - world *World // Reference to world server - + npcManager *npc.Manager + aiManager *ai.AIManager + database *database.Database + world *World // Reference to world server + // World-specific NPC tracking - npcsByZone map[int32][]*npc.NPC // Zone ID -> NPCs - activeCombat map[int32]bool // NPC ID -> in combat - - mutex sync.RWMutex + npcsByZone map[int32][]*npc.NPC // Zone ID -> NPCs + activeCombat map[int32]bool // NPC ID -> in combat + + mutex sync.RWMutex } // NewNPCManager creates a new NPC manager for the world server @@ -28,13 +28,13 @@ func NewNPCManager(db *database.Database) *NPCManager { // Create adapters for the NPC system dbAdapter := &WorldNPCDatabaseAdapter{db: db} logAdapter := &WorldNPCLoggerAdapter{} - + // Create core NPC manager npcMgr := npc.NewManager(dbAdapter, logAdapter) - + // Create AI manager with logger aiMgr := ai.NewAIManager(logAdapter, nil) // No Lua interface for now - + return &NPCManager{ npcManager: npcMgr, aiManager: aiMgr, @@ -52,13 +52,13 @@ func (nm *NPCManager) SetWorld(world *World) { // LoadNPCs loads all NPCs from database func (nm *NPCManager) LoadNPCs() error { fmt.Println("Loading NPC data...") - + // Initialize the NPC manager err := nm.npcManager.Initialize() if err != nil { return fmt.Errorf("failed to initialize NPC manager: %w", err) } - + // Load NPCs from database dbAdapter := &WorldNPCDatabaseAdapter{db: nm.database} dbNPCs, err := dbAdapter.LoadAllNPCs() @@ -72,7 +72,7 @@ func (nm *NPCManager) LoadNPCs() error { // Add to zone tracking (using zone ID from database or default) zoneID := int32(1) // TODO: Get actual zone ID from NPC nm.AddNPCToZone(zoneID, npc) - + // Create AI brain for this NPC (skip for now due to interface incompatibility) // TODO: Create proper NPC-to-AI adapter or implement missing methods // err := nm.aiManager.CreateBrainForNPC(npc, ai.BrainTypeDefault) @@ -83,13 +83,13 @@ func (nm *NPCManager) LoadNPCs() error { } fmt.Printf("Loaded %d NPCs from database\n", len(dbNPCs)) } - + // Setup default/test NPCs for development err = nm.createTestNPCs() if err != nil { fmt.Printf("Warning: Failed to create test NPCs: %v\n", err) } - + stats := nm.npcManager.GetStatistics() fmt.Printf("Total NPCs loaded: %d\n", stats.TotalNPCs) return nil @@ -98,7 +98,7 @@ func (nm *NPCManager) LoadNPCs() error { // createTestNPCs creates some test NPCs for development func (nm *NPCManager) createTestNPCs() error { fmt.Println("Creating test NPCs for development...") - + // Create a few test NPCs using npc.NewNPC() testNPCs := []struct { id int32 @@ -111,47 +111,47 @@ func (nm *NPCManager) createTestNPCs() error { {1003, "Test Forest Bear", 15, 2}, {1004, "Test Fire Elemental", 20, 3}, } - + for _, testData := range testNPCs { // Create new NPC instance newNPC := npc.NewNPC() if newNPC == nil { return fmt.Errorf("failed to create NPC %s", testData.name) } - + // Set basic NPC properties newNPC.SetID(testData.id) newNPC.SetName(testData.name) newNPC.SetLevel(int16(testData.level)) - + // Set some default stats based on level baseHP := int32(testData.level) * 50 newNPC.SetTotalHP(baseHP) newNPC.SetHP(baseHP) - + // Set position (placeholder coordinates) x := float32(100 + testData.id) - y := float32(100 + testData.id) + y := float32(100 + testData.id) z := float32(50) newNPC.SetX(x) - newNPC.SetY(y, false) // SetY takes bool parameter + newNPC.SetY(y, false) // SetY takes bool parameter newNPC.SetZ(z) newNPC.SetHeadingFromFloat(0.0) - + // Add to zone tracking nm.AddNPCToZone(testData.zoneID, newNPC) - + // Create an AI brain for this NPC (skip for now due to interface incompatibility) // TODO: Create proper NPC-to-AI adapter or implement missing methods // err := nm.aiManager.CreateBrainForNPC(newNPC, ai.BrainTypeDefault) // if err != nil { // fmt.Printf("Warning: Failed to create AI brain for NPC %s: %v\n", testData.name, err) // } - - fmt.Printf("Created test NPC: %s (ID: %d, Level: %d, Zone: %d)\n", + + fmt.Printf("Created test NPC: %s (ID: %d, Level: %d, Zone: %d)\n", testData.name, testData.id, testData.level, testData.zoneID) } - + fmt.Printf("Successfully created %d test NPCs\n", len(testNPCs)) return nil } @@ -160,10 +160,10 @@ func (nm *NPCManager) createTestNPCs() error { func (nm *NPCManager) ProcessNPCs() { nm.mutex.RLock() defer nm.mutex.RUnlock() - + // Process AI for all NPCs nm.aiManager.ProcessAllBrains() - + // Process NPC-specific logic stats := nm.npcManager.GetStatistics() if stats.NPCsInCombat > 0 { @@ -176,13 +176,13 @@ func (nm *NPCManager) ProcessNPCs() { func (nm *NPCManager) processCombatNPCs() { nm.mutex.RLock() defer nm.mutex.RUnlock() - + // Process each NPC that's in combat for npcID, inCombat := range nm.activeCombat { if !inCombat { continue } - + // Find the NPC in our zone tracking var combatNPC *npc.NPC for _, npcs := range nm.npcsByZone { @@ -196,19 +196,19 @@ func (nm *NPCManager) processCombatNPCs() { break } } - + if combatNPC == nil { // NPC not found, remove from combat tracking delete(nm.activeCombat, npcID) continue } - + // Skip if NPC is dead or incapacitated if !combatNPC.IsAlive() { delete(nm.activeCombat, npcID) continue } - + // Process combat AI nm.processCombatNPC(combatNPC) } @@ -219,21 +219,21 @@ func (nm *NPCManager) processCombatNPC(npc *npc.NPC) { if npc == nil { return } - + npcID := npc.GetID() // TODO: Implement proper target system - GetTarget() returns int32 not Entity // For now, simulate combat without target validation - + // Check if NPC should still be in combat (placeholder logic) if !npc.IsAlive() { npc.InCombat(false) delete(nm.activeCombat, npcID) return } - + // Simplified combat processing (placeholder until target system is implemented) fmt.Printf("NPC %s (%d) processing combat AI (simplified)\n", npc.GetName(), npcID) - + // For now, just simulate combat for a few seconds then exit combat // TODO: Implement proper combat mechanics with target system // This is a placeholder to test the basic NPC system @@ -247,7 +247,7 @@ func (nm *NPCManager) processCombatNPC(npc *npc.NPC) { // // Implementation deferred until interface issues are resolved // } -// processCombatMelee handles NPC melee combat +// processCombatMelee handles NPC melee combat // func (nm *NPCManager) processCombatMelee(npc *npc.NPC, target entity.Entity, distance float32) { // // Implementation deferred until interface issues are resolved // } @@ -262,7 +262,7 @@ func (nm *NPCManager) OnNPCKilled(npcID int32, killerCharacterID int32) { nm.mutex.Lock() delete(nm.activeCombat, npcID) nm.mutex.Unlock() - + // Trigger achievement events if nm.world != nil && nm.world.achievementMgr != nil { // Get NPC info @@ -272,7 +272,7 @@ func (nm *NPCManager) OnNPCKilled(npcID int32, killerCharacterID int32) { nm.world.OnNPCKill(killerCharacterID, npcID, int32(npcInfo.Level)) } } - + // Generate and distribute loot if nm.world != nil && nm.world.itemMgr != nil { npcInfo := nm.GetNPCInfo(npcID) @@ -289,19 +289,19 @@ func (nm *NPCManager) OnNPCKilled(npcID int32, killerCharacterID int32) { if zoneID == 0 { zoneID = 1 // Default zone } - + for _, item := range loot { err := nm.world.itemMgr.CreateWorldDrop(item.Details.ItemID, item.Details.Count, x, y, z, zoneID) if err != nil { fmt.Printf("Failed to create loot drop for item %d: %v\n", item.Details.ItemID, err) } } - + fmt.Printf("Generated %d loot items for NPC %d kill\n", len(loot), npcID) } } } - + fmt.Printf("NPC %d killed by character %d\n", npcID, killerCharacterID) } @@ -310,7 +310,7 @@ func (nm *NPCManager) OnNPCEnteredCombat(npcID int32, targetID int32) { nm.mutex.Lock() nm.activeCombat[npcID] = true nm.mutex.Unlock() - + fmt.Printf("NPC %d entered combat with target %d\n", npcID, targetID) } @@ -319,7 +319,7 @@ func (nm *NPCManager) OnNPCLeftCombat(npcID int32) { nm.mutex.Lock() delete(nm.activeCombat, npcID) nm.mutex.Unlock() - + fmt.Printf("NPC %d left combat\n", npcID) } @@ -327,7 +327,7 @@ func (nm *NPCManager) OnNPCLeftCombat(npcID int32) { func (nm *NPCManager) GetNPCInfo(npcID int32) *NPCInfo { nm.mutex.RLock() defer nm.mutex.RUnlock() - + // First, check if we have the NPC in memory for _, npcs := range nm.npcsByZone { for _, npc := range npcs { @@ -341,18 +341,18 @@ func (nm *NPCManager) GetNPCInfo(npcID int32) *NPCInfo { } } } - + // If not in memory, try to load from database row := nm.database.QueryRow(` SELECT id, name, level, zone_id FROM npcs WHERE id = ? `, npcID) - + var id, zoneID int32 var level int8 var name string - + err := row.Scan(&id, &name, &level, &zoneID) if err != nil { // NPC not found in database either, return default info @@ -363,7 +363,7 @@ func (nm *NPCManager) GetNPCInfo(npcID int32) *NPCInfo { ZoneID: 0, } } - + return &NPCInfo{ ID: id, Name: name, @@ -376,14 +376,14 @@ func (nm *NPCManager) GetNPCInfo(npcID int32) *NPCInfo { func (nm *NPCManager) GetNPCsByZone(zoneID int32) []*npc.NPC { nm.mutex.RLock() defer nm.mutex.RUnlock() - + if npcs, exists := nm.npcsByZone[zoneID]; exists { // Return a copy to avoid concurrent modification result := make([]*npc.NPC, len(npcs)) copy(result, npcs) return result } - + return nil } @@ -391,7 +391,7 @@ func (nm *NPCManager) GetNPCsByZone(zoneID int32) []*npc.NPC { func (nm *NPCManager) AddNPCToZone(zoneID int32, npc *npc.NPC) { nm.mutex.Lock() defer nm.mutex.Unlock() - + nm.npcsByZone[zoneID] = append(nm.npcsByZone[zoneID], npc) } @@ -399,7 +399,7 @@ func (nm *NPCManager) AddNPCToZone(zoneID int32, npc *npc.NPC) { func (nm *NPCManager) RemoveNPCFromZone(zoneID int32, npcID int32) { nm.mutex.Lock() defer nm.mutex.Unlock() - + if npcs, exists := nm.npcsByZone[zoneID]; exists { for i, npc := range npcs { if npc != nil && npc.GetID() == npcID { @@ -412,56 +412,56 @@ func (nm *NPCManager) RemoveNPCFromZone(zoneID int32, npcID int32) { } // GetStatistics returns NPC system statistics -func (nm *NPCManager) GetStatistics() map[string]interface{} { +func (nm *NPCManager) GetStatistics() map[string]any { nm.mutex.RLock() defer nm.mutex.RUnlock() - + // Get base statistics from NPC manager stats := nm.npcManager.GetStatistics() - + // Add world-specific statistics totalZones := len(nm.npcsByZone) totalInCombat := len(nm.activeCombat) - - result := map[string]interface{}{ - "total_npcs": stats.TotalNPCs, - "npcs_in_combat": stats.NPCsInCombat, - "npcs_with_spells": stats.NPCsWithSpells, - "npcs_with_skills": stats.NPCsWithSkills, - "spell_cast_count": stats.SpellCastCount, - "skill_usage_count": stats.SkillUsageCount, - "runback_count": stats.RunbackCount, + + result := map[string]any{ + "total_npcs": stats.TotalNPCs, + "npcs_in_combat": stats.NPCsInCombat, + "npcs_with_spells": stats.NPCsWithSpells, + "npcs_with_skills": stats.NPCsWithSkills, + "spell_cast_count": stats.SpellCastCount, + "skill_usage_count": stats.SkillUsageCount, + "runback_count": stats.RunbackCount, "average_aggro_radius": stats.AverageAggroRadius, - "ai_strategy_counts": stats.AIStrategyCounts, - "zones_with_npcs": totalZones, + "ai_strategy_counts": stats.AIStrategyCounts, + "zones_with_npcs": totalZones, "world_npcs_in_combat": totalInCombat, } - + return result } // Shutdown gracefully shuts down the NPC manager func (nm *NPCManager) Shutdown() { fmt.Println("Shutting down NPC manager...") - + nm.mutex.Lock() defer nm.mutex.Unlock() - + // Clear all tracking nm.npcsByZone = make(map[int32][]*npc.NPC) nm.activeCombat = make(map[int32]bool) - + // TODO: Shutdown AI manager when shutdown method is available // nm.aiManager.Shutdown() - + fmt.Println("NPC manager shutdown complete") } // NPCInfo represents basic information about an NPC type NPCInfo struct { - ID int32 - Name string - Level int8 + ID int32 + Name string + Level int8 ZoneID int32 } @@ -473,7 +473,7 @@ type WorldNPCDatabaseAdapter struct { // LoadAllNPCs implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) LoadAllNPCs() ([]*npc.NPC, error) { fmt.Println("Loading NPCs from database...") - + rows, err := wdb.db.Query(` SELECT id, name, level, max_level, race, model_type, size, hp, power, x, y, z, heading, respawn_time, zone_id, aggro_radius, ai_strategy, @@ -486,22 +486,22 @@ func (wdb *WorldNPCDatabaseAdapter) LoadAllNPCs() ([]*npc.NPC, error) { return nil, fmt.Errorf("failed to query NPCs: %w", err) } defer rows.Close() - + var npcs []*npc.NPC - + for rows.Next() { newNPC := npc.NewNPC() if newNPC == nil { continue // Skip if we can't create NPC } - + var id, maxLevel, race, modelType, size, hp, power int32 var x, y, z, heading, aggroRadius float32 var respawnTime, zoneID, aiStrategy, lootTableID, merchantType int32 var randomizeAppearance, showName, showLevel, targetable, showCommandIcon, displayHandIcon, factionID int32 var name string var level int8 - + err := rows.Scan(&id, &name, &level, &maxLevel, &race, &modelType, &size, &hp, &power, &x, &y, &z, &heading, &respawnTime, &zoneID, &aggroRadius, &aiStrategy, &lootTableID, &merchantType, @@ -511,7 +511,7 @@ func (wdb *WorldNPCDatabaseAdapter) LoadAllNPCs() ([]*npc.NPC, error) { fmt.Printf("Error scanning NPC row: %v\n", err) continue } - + // Set NPC properties newNPC.SetID(id) newNPC.SetName(name) @@ -522,21 +522,21 @@ func (wdb *WorldNPCDatabaseAdapter) LoadAllNPCs() ([]*npc.NPC, error) { newNPC.SetY(y, false) newNPC.SetZ(z) newNPC.SetHeadingFromFloat(heading) - + // Set additional properties if the NPC supports them // TODO: Set additional properties like race, model type, etc. // This would require checking what methods are available on the NPC type - + npcs = append(npcs, newNPC) - - fmt.Printf("Loaded NPC: %s (ID: %d, Level: %d, Zone: %d)\n", + + fmt.Printf("Loaded NPC: %s (ID: %d, Level: %d, Zone: %d)\n", name, id, level, zoneID) } - + if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating NPC rows: %w", err) } - + fmt.Printf("Successfully loaded %d NPCs from database\n", len(npcs)) return npcs, nil } @@ -546,7 +546,7 @@ func (wdb *WorldNPCDatabaseAdapter) SaveNPC(npcEntity *npc.NPC) error { if npcEntity == nil { return fmt.Errorf("cannot save nil NPC") } - + // Extract NPC properties for saving id := npcEntity.GetID() name := npcEntity.GetName() @@ -556,24 +556,24 @@ func (wdb *WorldNPCDatabaseAdapter) SaveNPC(npcEntity *npc.NPC) error { y := npcEntity.GetY() z := npcEntity.GetZ() heading := npcEntity.GetHeading() - + // Insert or update NPC in database _, err := wdb.db.Exec(` - INSERT OR REPLACE INTO npcs + INSERT OR REPLACE INTO npcs (id, name, level, max_level, race, model_type, size, hp, power, x, y, z, heading, respawn_time, zone_id, aggro_radius, ai_strategy, loot_table_id, merchant_type, randomize_appearance, show_name, show_level, targetable, show_command_icon, display_hand_icon, faction_id, created_date, last_modified) - VALUES + VALUES (?, ?, ?, ?, 0, 0, 32, ?, ?, ?, ?, ?, ?, 300, 0, 10.0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, strftime('%s', 'now'), strftime('%s', 'now')) `, id, name, level, level, hp, hp, x, y, z, heading) - + if err != nil { return fmt.Errorf("failed to save NPC %d: %w", id, err) } - + fmt.Printf("Saved NPC: %s (ID: %d) to database\n", name, id) return nil } @@ -586,44 +586,44 @@ func (wdb *WorldNPCDatabaseAdapter) DeleteNPC(npcID int32) error { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() - + // Delete related data first (due to foreign key constraints) _, err = tx.Exec("DELETE FROM npc_spells WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC spells: %w", err) } - + _, err = tx.Exec("DELETE FROM npc_skills WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC skills: %w", err) } - + _, err = tx.Exec("DELETE FROM npc_loot WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC loot: %w", err) } - + // Delete the NPC itself result, err := tx.Exec("DELETE FROM npcs WHERE id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC: %w", err) } - + // Check if NPC was actually deleted rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to check rows affected: %w", err) } - + if rowsAffected == 0 { return fmt.Errorf("NPC %d not found", npcID) } - + // Commit transaction if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } - + fmt.Printf("Deleted NPC %d from database\n", npcID) return nil } @@ -640,19 +640,19 @@ func (wdb *WorldNPCDatabaseAdapter) LoadNPCSpells(npcID int32) ([]*npc.NPCSpell, return nil, fmt.Errorf("failed to query NPC spells: %w", err) } defer rows.Close() - + var spells []*npc.NPCSpell - + for rows.Next() { spell := npc.NewNPCSpell() var npcIDDB, spellID, tier, hpPercentage, priority, castType, recastDelay int32 - + err := rows.Scan(&npcIDDB, &spellID, &tier, &hpPercentage, &priority, &castType, &recastDelay) if err != nil { fmt.Printf("Error scanning NPC spell row: %v\n", err) continue } - + // Set spell properties (using available methods from NPCSpell) // TODO: Set spell properties based on what methods are available // spell.SetSpellID(spellID) @@ -661,14 +661,14 @@ func (wdb *WorldNPCDatabaseAdapter) LoadNPCSpells(npcID int32) ([]*npc.NPCSpell, // spell.SetPriority(priority) // spell.SetCastType(castType) // spell.SetRecastDelay(recastDelay) - + spells = append(spells, spell) } - + if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating NPC spell rows: %w", err) } - + return spells, nil } @@ -680,37 +680,37 @@ func (wdb *WorldNPCDatabaseAdapter) SaveNPCSpells(npcID int32, spells []*npc.NPC return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() - + // Delete existing spells for this NPC _, err = tx.Exec("DELETE FROM npc_spells WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete existing NPC spells: %w", err) } - + // Insert new spells for _, spell := range spells { if spell == nil { continue } - + // TODO: Get spell properties from NPCSpell object // For now, use placeholder values _, err = tx.Exec(` - INSERT INTO npc_spells + INSERT INTO npc_spells (npc_id, spell_id, tier, hp_percentage, priority, cast_type, recast_delay) VALUES (?, ?, ?, ?, ?, ?, ?) `, npcID, 1, 1, 100, 1, 0, 5) // Placeholder values - + if err != nil { return fmt.Errorf("failed to save NPC spell: %w", err) } } - + // Commit transaction if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } - + fmt.Printf("Saved %d spells for NPC %d\n", len(spells), npcID) return nil } @@ -727,29 +727,29 @@ func (wdb *WorldNPCDatabaseAdapter) LoadNPCSkills(npcID int32) (map[string]*npc. return nil, fmt.Errorf("failed to query NPC skills: %w", err) } defer rows.Close() - + skills := make(map[string]*npc.Skill) - + for rows.Next() { var npcIDDB, skillValue, maxValue int32 var skillName string - + err := rows.Scan(&npcIDDB, &skillName, &skillValue, &maxValue) if err != nil { fmt.Printf("Error scanning NPC skill row: %v\n", err) continue } - + // Create skill object skill := npc.NewSkill(0, skillName, int16(skillValue), int16(maxValue)) - + skills[skillName] = skill } - + if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating NPC skill rows: %w", err) } - + return skills, nil } @@ -761,40 +761,40 @@ func (wdb *WorldNPCDatabaseAdapter) SaveNPCSkills(npcID int32, skills map[string return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() - + // Delete existing skills for this NPC _, err = tx.Exec("DELETE FROM npc_skills WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete existing NPC skills: %w", err) } - + // Insert new skills for skillName, skill := range skills { if skill == nil { continue } - + // Get skill values (need to access directly since GetMaxVal doesn't exist) currentVal := skill.GetCurrentVal() // TODO: Add GetMaxVal method to Skill struct or access MaxVal field maxVal := int16(100) // Placeholder - should be skill.MaxVal when accessible - + _, err = tx.Exec(` - INSERT INTO npc_skills + INSERT INTO npc_skills (npc_id, skill_name, skill_value, max_value) VALUES (?, ?, ?, ?) `, npcID, skillName, currentVal, maxVal) - + if err != nil { return fmt.Errorf("failed to save NPC skill %s: %w", skillName, err) } } - + // Commit transaction if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } - + fmt.Printf("Saved %d skills for NPC %d\n", len(skills), npcID) return nil } @@ -820,4 +820,4 @@ func (wl *WorldNPCLoggerAdapter) LogDebug(message string, args ...any) { // LogWarning implements npc.Logger interface func (wl *WorldNPCLoggerAdapter) LogWarning(message string, args ...any) { fmt.Printf("[NPC] WARNING: "+message+"\n", args...) -} \ No newline at end of file +} diff --git a/internal/world/packet_handlers.go b/internal/world/packet_handlers.go index a5488f9..a64b449 100644 --- a/internal/world/packet_handlers.go +++ b/internal/world/packet_handlers.go @@ -8,47 +8,47 @@ import ( // RegisterPacketHandlers registers all world server packet handlers func (w *World) RegisterPacketHandlers() { fmt.Println("Registering world server packet handlers...") - + // Basic connection and loading handlers packets.RegisterGlobalHandler(packets.OP_DoneLoadingZoneResourcesMsg, w.HandleDoneLoadingZoneResources) packets.RegisterGlobalHandler(packets.OP_DoneSendingInitialEntitiesMsg, w.HandleDoneSendingInitialEntities) packets.RegisterGlobalHandler(packets.OP_DoneLoadingEntityResourcesMsg, w.HandleDoneLoadingEntityResources) packets.RegisterGlobalHandler(packets.OP_DoneLoadingUIResourcesMsg, w.HandleDoneLoadingUIResources) - + // Zone readiness packets.RegisterGlobalHandler(packets.OP_ReadyToZoneMsg, w.HandleReadyToZone) - + // Command handling packets.RegisterGlobalHandler(packets.OP_ClientCmdMsg, w.HandleClientCommand) packets.RegisterGlobalHandler(packets.OP_DispatchClientCmdMsg, w.HandleDispatchClientCommand) - + // Position updates packets.RegisterGlobalHandler(packets.OP_UpdatePositionMsg, w.HandlePositionUpdate) - + // Chat system packets.RegisterGlobalHandler(packets.OP_ChatTellChannelMsg, w.HandleChatTellChannel) packets.RegisterGlobalHandler(packets.OP_ChatTellUserMsg, w.HandleChatTellUser) - + // Zone transitions packets.RegisterGlobalHandler(packets.OP_ChangeZoneMsg, w.HandleChangeZone) packets.RegisterGlobalHandler(packets.OP_ClientTeleportRequestMsg, w.HandleClientTeleportRequest) - + // Achievement system packets.RegisterGlobalHandler(packets.OP_AchievementUpdateMsg, w.HandleAchievementUpdate) packets.RegisterGlobalHandler(packets.OP_CharacterAchievements, w.HandleCharacterAchievements) - + // Title system packets.RegisterGlobalHandler(packets.OP_TitleUpdateMsg, w.HandleTitleUpdate) packets.RegisterGlobalHandler(packets.OP_CharacterTitles, w.HandleCharacterTitles) packets.RegisterGlobalHandler(packets.OP_SetActiveTitleMsg, w.HandleSetActiveTitle) - + // NPC system packets.RegisterGlobalHandler(packets.OP_NPCAttackMsg, w.HandleNPCAttack) packets.RegisterGlobalHandler(packets.OP_NPCTargetMsg, w.HandleNPCTarget) packets.RegisterGlobalHandler(packets.OP_NPCInfoMsg, w.HandleNPCInfo) packets.RegisterGlobalHandler(packets.OP_NPCSpellCastMsg, w.HandleNPCSpellCast) packets.RegisterGlobalHandler(packets.OP_NPCMovementMsg, w.HandleNPCMovement) - + // Item system packets.RegisterGlobalHandler(packets.OP_ItemMoveMsg, w.HandleItemMove) packets.RegisterGlobalHandler(packets.OP_ItemEquipMsg, w.HandleItemEquip) @@ -57,77 +57,77 @@ func (w *World) RegisterPacketHandlers() { packets.RegisterGlobalHandler(packets.OP_ItemDropMsg, w.HandleItemDrop) packets.RegisterGlobalHandler(packets.OP_ItemExamineMsg, w.HandleItemExamine) packets.RegisterGlobalHandler(packets.OP_ItemUpdateMsg, w.HandleItemUpdate) - + fmt.Printf("Registered %d packet handlers\n", 28) } // HandleDoneLoadingZoneResources handles when client finishes loading zone resources func (w *World) HandleDoneLoadingZoneResources(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s finished loading zone resources\n", ctx.Client.GetCharacterName()) - + // Update client state client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() // TODO: Send initial zone data, spawns, etc. } - + return nil } // HandleDoneSendingInitialEntities handles when client finishes receiving initial entities func (w *World) HandleDoneSendingInitialEntities(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s finished receiving initial entities\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() // TODO: Mark client as fully loaded } - + return nil } // HandleDoneLoadingEntityResources handles when client finishes loading entity resources func (w *World) HandleDoneLoadingEntityResources(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s finished loading entity resources\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() } - + return nil } // HandleDoneLoadingUIResources handles when client finishes loading UI resources func (w *World) HandleDoneLoadingUIResources(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s finished loading UI resources\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() // TODO: Send initial UI packets (character sheet, spellbook, etc.) } - + return nil } // HandleReadyToZone handles when client is ready to enter the zone func (w *World) HandleReadyToZone(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s is ready to enter zone\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Complete zone entry process // - Send world time // - Send MOTD // - Send initial game state // - Add player to zone } - + return nil } @@ -136,28 +136,28 @@ func (w *World) HandleClientCommand(ctx *packets.PacketContext, packet *packets. // TODO: Parse command from packet data // For now, just log the attempt fmt.Printf("Client %s sent command (raw packet)\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Extract command text and dispatch to command manager // This will require parsing the packet structure } - + return nil } // HandleDispatchClientCommand handles dispatched client commands func (w *World) HandleDispatchClientCommand(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent dispatched command\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() // TODO: Handle dispatched commands } - + return nil } @@ -167,106 +167,106 @@ func (w *World) HandlePositionUpdate(ctx *packets.PacketContext, packet *packets client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse position data from packet // TODO: Update player position in zone // TODO: Send position update to other players in range } - + return nil } // HandleChatTellChannel handles channel chat messages func (w *World) HandleChatTellChannel(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent channel chat message\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse chat message from packet // TODO: Validate channel permissions // TODO: Broadcast message to appropriate recipients } - + return nil } // HandleChatTellUser handles direct tell messages func (w *World) HandleChatTellUser(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent tell message\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse tell message and target from packet // TODO: Find target player // TODO: Send message to target } - + return nil } // HandleChangeZone handles zone change requests func (w *World) HandleChangeZone(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested zone change\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse zone change request from packet // TODO: Validate zone change is allowed // TODO: Begin zone transfer process } - + return nil } // HandleClientTeleportRequest handles client teleport requests func (w *World) HandleClientTeleportRequest(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested teleport\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse teleport request from packet // TODO: Validate teleport permissions // TODO: Execute teleport } - + return nil } // HandleAchievementUpdate handles achievement update requests from client func (w *World) HandleAchievementUpdate(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested achievement update\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // Send current achievement data to client w.SendAchievementData(client) } - + return nil } -// HandleCharacterAchievements handles character achievements request from client +// HandleCharacterAchievements handles character achievements request from client func (w *World) HandleCharacterAchievements(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested character achievements\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // Send complete achievement list to client w.SendCharacterAchievements(client) } - + return nil } @@ -275,22 +275,22 @@ func (w *World) SendAchievementData(client *Client) { if w.achievementMgr == nil { return } - + characterID := client.CharacterID - + // Get player's completed achievements completedAchievements := w.achievementMgr.GetPlayerCompletedAchievements(characterID) inProgressAchievements := w.achievementMgr.GetPlayerInProgressAchievements(characterID) - - fmt.Printf("Sending achievement data to %s: %d completed, %d in progress\n", + + fmt.Printf("Sending achievement data to %s: %d completed, %d in progress\n", client.CharacterName, len(completedAchievements), len(inProgressAchievements)) - + // Create achievement update packet // This would normally build a proper packet structure totalPoints := w.calculateAchievementPoints(characterID) - + // Send packet to client (placeholder - would use actual packet building) - client.SendSimpleMessage(fmt.Sprintf("Achievement Update: %d completed, %d in progress, %d points", + client.SendSimpleMessage(fmt.Sprintf("Achievement Update: %d completed, %d in progress, %d points", len(completedAchievements), len(inProgressAchievements), totalPoints)) } @@ -299,19 +299,19 @@ func (w *World) SendCharacterAchievements(client *Client) { if w.achievementMgr == nil { return } - + characterID := client.CharacterID - + // Get all achievements with player progress allAchievements := w.achievementMgr.masterList.GetAllAchievements() - characterData := make(map[string]interface{}) - + characterData := make(map[string]any) + for achievementID, achievement := range allAchievements { progress := w.achievementMgr.GetPlayerProgress(characterID, achievementID) completed := w.achievementMgr.IsPlayerCompleted(characterID, achievementID) percentage := w.achievementMgr.GetCompletionPercentage(characterID, achievementID) - - characterData[fmt.Sprintf("achievement_%d", achievementID)] = map[string]interface{}{ + + characterData[fmt.Sprintf("achievement_%d", achievementID)] = map[string]any{ "id": achievementID, "title": achievement.Title, "description": achievement.UncompletedText, @@ -324,10 +324,10 @@ func (w *World) SendCharacterAchievements(client *Client) { "expansion": achievement.Expansion, } } - - fmt.Printf("Sending complete achievement list to %s: %d achievements\n", + + fmt.Printf("Sending complete achievement list to %s: %d achievements\n", client.CharacterName, len(allAchievements)) - + // Send packet to client (placeholder - would use actual packet building) client.SendSimpleMessage(fmt.Sprintf("Character Achievements: %d total achievements", len(allAchievements))) } @@ -337,67 +337,67 @@ func (w *World) calculateAchievementPoints(characterID int32) uint32 { if w.achievementMgr == nil { return 0 } - + completedAchievements := w.achievementMgr.GetPlayerCompletedAchievements(characterID) totalPoints := uint32(0) - + for _, achievementID := range completedAchievements { achievement := w.achievementMgr.GetAchievement(achievementID) if achievement != nil { totalPoints += achievement.PointValue } } - + return totalPoints } // HandleTitleUpdate handles title update requests from client func (w *World) HandleTitleUpdate(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested title update\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // Send current title data to client w.SendTitleData(client) } - + return nil } // HandleCharacterTitles handles character titles request from client func (w *World) HandleCharacterTitles(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested character titles\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // Send complete title list to client w.SendCharacterTitles(client) } - + return nil } // HandleSetActiveTitle handles setting active title requests from client func (w *World) HandleSetActiveTitle(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested to set active title\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse title ID and position from packet data // TODO: Validate player has the title // TODO: Set active title // TODO: Send confirmation to client - + // For now, just log the request fmt.Printf("Set active title request for %s processed\n", client.CharacterName) } - + return nil } @@ -406,16 +406,16 @@ func (w *World) SendTitleData(client *Client) { if w.titleMgr == nil { return } - + characterID := client.CharacterID - + // Get player's titles playerTitles := w.titleMgr.GetPlayerTitles(characterID) titleCount := playerTitles.GetTitleCount() - - fmt.Printf("Sending title data to %s: %d titles\n", + + fmt.Printf("Sending title data to %s: %d titles\n", client.CharacterName, titleCount) - + // Create title update packet (placeholder) client.SendSimpleMessage(fmt.Sprintf("Title Update: %d titles available", titleCount)) } @@ -425,24 +425,24 @@ func (w *World) SendCharacterTitles(client *Client) { if w.titleMgr == nil { return } - + characterID := client.CharacterID - + // Get player's titles and master list playerTitles := w.titleMgr.GetPlayerTitles(characterID) masterList := w.titleMgr.titleManager.GetMasterList() - + titleCount := playerTitles.GetTitleCount() totalTitles := masterList.GetTitleCount() - - fmt.Printf("Sending complete title list to %s: %d owned out of %d total\n", + + fmt.Printf("Sending complete title list to %s: %d owned out of %d total\n", client.CharacterName, titleCount, totalTitles) - + // Get player's formatted name with titles formattedName := w.titleMgr.GetPlayerFormattedName(characterID, client.CharacterName) - + // Send packet to client (placeholder - would use actual packet building) - client.SendSimpleMessage(fmt.Sprintf("Character Titles: %d owned, %d total. Display name: %s", + client.SendSimpleMessage(fmt.Sprintf("Character Titles: %d owned, %d total. Display name: %s", titleCount, totalTitles, formattedName)) } @@ -451,77 +451,77 @@ func (w *World) SendCharacterTitles(client *Client) { // HandleNPCAttack handles NPC attack packets from clients func (w *World) HandleNPCAttack(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent NPC attack packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse NPC ID and attack type from packet data // TODO: Validate player can attack NPC // TODO: Process attack through combat system // TODO: Send attack result to client and nearby players - + // For now, just trigger a test NPC kill event for achievement testing if w.npcMgr != nil { testNPCID := int32(1001) w.npcMgr.OnNPCKilled(testNPCID, ctx.Client.GetCharacterID()) } } - + return nil } // HandleNPCTarget handles NPC targeting packets from clients func (w *World) HandleNPCTarget(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent NPC target packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse NPC ID from packet data // TODO: Validate NPC exists and is targetable // TODO: Set player's target // TODO: Send targeting confirmation to client - + // For testing, send NPC info for any targeting w.SendNPCInfo(client, 1001) // Test NPC } - + return nil } // HandleNPCInfo handles NPC info requests from clients func (w *World) HandleNPCInfo(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s requested NPC info\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse NPC ID from packet data // TODO: Send NPC information to client - + // For testing, send test NPC info w.SendNPCInfo(client, 1001) } - + return nil } // HandleNPCSpellCast handles NPC spell cast notifications func (w *World) HandleNPCSpellCast(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s received NPC spell cast notification\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse spell cast data from packet // TODO: Process spell effects // TODO: Update client state based on spell effects } - + return nil } @@ -531,12 +531,12 @@ func (w *World) HandleNPCMovement(ctx *packets.PacketContext, packet *packets.Pa client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse NPC movement data from packet // TODO: Update NPC position in world // TODO: Send movement update to other clients in range } - + return nil } @@ -545,31 +545,31 @@ func (w *World) SendNPCInfo(client *Client, npcID int32) { if w.npcMgr == nil { return } - + // Get NPC information npcInfo := w.npcMgr.GetNPCInfo(npcID) if npcInfo == nil { return } - - fmt.Printf("Sending NPC info to %s: NPC %d (%s) Level %d\n", + + fmt.Printf("Sending NPC info to %s: NPC %d (%s) Level %d\n", client.CharacterName, npcInfo.ID, npcInfo.Name, npcInfo.Level) - + // Get NPC statistics for additional info stats := w.npcMgr.GetStatistics() - + // Create NPC info packet (placeholder) - client.SendSimpleMessage(fmt.Sprintf("NPC Info: %s (ID: %d, Level: %d) - %v total NPCs active", + client.SendSimpleMessage(fmt.Sprintf("NPC Info: %s (ID: %d, Level: %d) - %v total NPCs active", npcInfo.Name, npcInfo.ID, npcInfo.Level, stats["total_npcs"])) } // SendNPCUpdate sends NPC update to clients in range -func (w *World) SendNPCUpdate(npcID int32, updateType string, data map[string]interface{}) { +func (w *World) SendNPCUpdate(npcID int32, updateType string, data map[string]any) { // TODO: Implement NPC update broadcasting // This would send updates to all clients in range of the NPC - + fmt.Printf("NPC Update: NPC %d - %s: %v\n", npcID, updateType, data) - + // Get all clients and send update (placeholder) clients := w.clients.GetAll() for _, client := range clients { @@ -583,16 +583,16 @@ func (w *World) SendNPCUpdate(npcID int32, updateType string, data map[string]in // SendNPCCombatUpdate sends combat-related NPC updates to clients func (w *World) SendNPCCombatUpdate(npcID int32, targetID int32, combatType string, damage int32) { // TODO: Implement NPC combat update broadcasting - - fmt.Printf("NPC Combat Update: NPC %d -> Target %d, %s for %d damage\n", + + fmt.Printf("NPC Combat Update: NPC %d -> Target %d, %s for %d damage\n", npcID, targetID, combatType, damage) - + // Send to relevant clients (placeholder) clients := w.clients.GetAll() for _, client := range clients { - if client.CurrentZone != nil && (client.CharacterID == targetID || + if client.CurrentZone != nil && (client.CharacterID == targetID || client.CharacterID == npcID) { // TODO: Proper range check - client.SendSimpleMessage(fmt.Sprintf("Combat: NPC %d %s target %d for %d damage", + client.SendSimpleMessage(fmt.Sprintf("Combat: NPC %d %s target %d for %d damage", npcID, combatType, targetID, damage)) } } @@ -604,13 +604,13 @@ type WorldDatabaseAdapter struct { } // GetCharacter implements packets.DatabaseInterface -func (wda *WorldDatabaseAdapter) GetCharacter(characterID int32) (map[string]interface{}, error) { +func (wda *WorldDatabaseAdapter) GetCharacter(characterID int32) (map[string]any, error) { // TODO: Implement character loading from database return nil, fmt.Errorf("character loading not yet implemented") } // SaveCharacter implements packets.DatabaseInterface -func (wda *WorldDatabaseAdapter) SaveCharacter(characterID int32, data map[string]interface{}) error { +func (wda *WorldDatabaseAdapter) SaveCharacter(characterID int32, data map[string]any) error { // TODO: Implement character saving to database return fmt.Errorf("character saving not yet implemented") } @@ -654,8 +654,8 @@ func (wca *WorldClientAdapter) IsInZone() bool { // SendPacket implements packets.ClientInterface func (wca *WorldClientAdapter) SendPacket(opcode packets.InternalOpcode, data []byte) error { // TODO: Implement packet sending via UDP connection - fmt.Printf("Sending packet %s to client %s\n", - packets.GetInternalOpcodeName(opcode), + fmt.Printf("Sending packet %s to client %s\n", + packets.GetInternalOpcodeName(opcode), wca.client.CharacterName) return nil } @@ -684,11 +684,11 @@ func (wsa *WorldServerAdapter) GetClientByID(characterID int32) packets.ClientIn func (wsa *WorldServerAdapter) GetAllClients() []packets.ClientInterface { clients := wsa.world.clients.GetAll() result := make([]packets.ClientInterface, len(clients)) - + for i, client := range clients { result[i] = &WorldClientAdapter{client: client, world: wsa.world} } - + return result } @@ -718,20 +718,20 @@ func (w *World) CreatePacketContext(client *Client) *packets.PacketContext { // HandleItemMove handles item movement within player inventory func (w *World) HandleItemMove(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent item move packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse move data from packet (fromBagID, fromSlot, toBagID, toSlot) // For now, use placeholder values for testing fromBagID := int32(0) fromSlot := int16(0) toBagID := int32(0) toSlot := int16(1) - + if w.itemMgr != nil { - err := w.itemMgr.MoveItem(uint32(ctx.Client.GetCharacterID()), + err := w.itemMgr.MoveItem(uint32(ctx.Client.GetCharacterID()), fromBagID, fromSlot, toBagID, toSlot) if err != nil { client.SendSimpleMessage(fmt.Sprintf("Item move failed: %v", err)) @@ -740,23 +740,23 @@ func (w *World) HandleItemMove(ctx *packets.PacketContext, packet *packets.Packe } } } - + return nil } // HandleItemEquip handles item equipping func (w *World) HandleItemEquip(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent item equip packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse equip data from packet (uniqueID, slot) // For now, use placeholder values for testing uniqueID := int32(1001) slot := int8(0) // Primary hand - + if w.itemMgr != nil { err := w.itemMgr.EquipItem(uint32(ctx.Client.GetCharacterID()), uniqueID, slot) if err != nil { @@ -766,22 +766,22 @@ func (w *World) HandleItemEquip(ctx *packets.PacketContext, packet *packets.Pack } } } - + return nil } // HandleItemUnequip handles item unequipping func (w *World) HandleItemUnequip(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent item unequip packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse unequip data from packet (slot) // For now, use placeholder values for testing slot := int8(0) // Primary hand - + if w.itemMgr != nil { err := w.itemMgr.UnequipItem(uint32(ctx.Client.GetCharacterID()), slot) if err != nil { @@ -791,23 +791,23 @@ func (w *World) HandleItemUnequip(ctx *packets.PacketContext, packet *packets.Pa } } } - + return nil } // HandleItemPickup handles picking up world drops func (w *World) HandleItemPickup(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent item pickup packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse pickup data from packet (itemUniqueID) // For now, use placeholder values for testing itemUniqueID := int32(5001) zoneID := int32(1) // Current zone - + if w.itemMgr != nil { err := w.itemMgr.PickupWorldDrop(uint32(ctx.Client.GetCharacterID()), itemUniqueID, zoneID) if err != nil { @@ -818,29 +818,29 @@ func (w *World) HandleItemPickup(ctx *packets.PacketContext, packet *packets.Pac } } } - + return nil } // HandleItemDrop handles dropping items to the world func (w *World) HandleItemDrop(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent item drop packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse drop data from packet (uniqueID, quantity, x, y, z) // For now, use placeholder values for testing itemID := int32(1001) quantity := int16(1) x, y, z := float32(100), float32(100), float32(50) zoneID := int32(1) - + if w.itemMgr != nil { // First remove from player inventory (would need to look up by uniqueID) // TODO: Get uniqueID from packet and remove from player - + // Create world drop err := w.itemMgr.CreateWorldDrop(itemID, quantity, x, y, z, zoneID) if err != nil { @@ -851,22 +851,22 @@ func (w *World) HandleItemDrop(ctx *packets.PacketContext, packet *packets.Packe } } } - + return nil } // HandleItemExamine handles item examination requests func (w *World) HandleItemExamine(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent item examine packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse examine data from packet (uniqueID or itemID) // For now, use placeholder values for testing itemID := int32(1001) - + if w.itemMgr != nil { itemTemplate := w.itemMgr.GetItemTemplate(itemID) if itemTemplate == nil { @@ -877,41 +877,41 @@ func (w *World) HandleItemExamine(ctx *packets.PacketContext, packet *packets.Pa } } } - + return nil } // HandleItemUpdate handles item update notifications func (w *World) HandleItemUpdate(ctx *packets.PacketContext, packet *packets.PacketData) error { fmt.Printf("Client %s sent item update packet\n", ctx.Client.GetCharacterName()) - + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) if client != nil { client.UpdateActivity() - + // TODO: Parse update data from packet // This might be triggered when client needs updated item information - + if w.itemMgr != nil { // Send updated inventory to client w.SendPlayerInventory(client) } } - + return nil } // SendItemDetails sends detailed item information to a client -func (w *World) SendItemDetails(client *Client, item interface{}) { +func (w *World) SendItemDetails(client *Client, item any) { if w.itemMgr == nil { return } - + // TODO: Implement actual item detail packet building // This would include stats, description, level requirements, etc. - + fmt.Printf("Sending item details to %s\n", client.CharacterName) - + // Placeholder - send basic item info as chat message client.SendSimpleMessage("Item Details: [Item information would be displayed here]") } @@ -921,52 +921,52 @@ func (w *World) SendPlayerInventory(client *Client) { if w.itemMgr == nil { return } - + playerID := uint32(client.CharacterID) - + // Get player inventory and equipment inventory, err := w.itemMgr.GetPlayerInventory(playerID) if err != nil { fmt.Printf("Failed to get inventory for player %d: %v\n", playerID, err) return } - + equipment, err := w.itemMgr.GetPlayerEquipment(playerID, 0) // Base equipment if err != nil { fmt.Printf("Failed to get equipment for player %d: %v\n", playerID, err) return } - + // TODO: Build and send inventory packet - fmt.Printf("Sending inventory to %s: %d inventory items, %d equipped items\n", + fmt.Printf("Sending inventory to %s: %d inventory items, %d equipped items\n", client.CharacterName, inventory.GetNumberOfItems(), equipment.GetNumberOfItems()) - + // Placeholder - send summary as chat message client.SendSimpleMessage(fmt.Sprintf("Inventory Update: %d items in inventory, %d equipped", inventory.GetNumberOfItems(), equipment.GetNumberOfItems())) } // SendItemUpdate sends item update to client -func (w *World) SendItemUpdate(client *Client, updateType string, itemData map[string]interface{}) { +func (w *World) SendItemUpdate(client *Client, updateType string, itemData map[string]any) { if w.itemMgr == nil { return } - + // TODO: Build and send item update packet - fmt.Printf("Sending item update to %s: %s - %v\n", + fmt.Printf("Sending item update to %s: %s - %v\n", client.CharacterName, updateType, itemData) - + // Placeholder - send update as chat message client.SendSimpleMessage(fmt.Sprintf("Item Update: %s", updateType)) } // BroadcastItemUpdate broadcasts item updates to nearby players -func (w *World) BroadcastItemUpdate(sourcePlayerID uint32, updateType string, itemData map[string]interface{}) { +func (w *World) BroadcastItemUpdate(sourcePlayerID uint32, updateType string, itemData map[string]any) { // TODO: Implement item update broadcasting (for things like equipment changes visible to others) - - fmt.Printf("Broadcasting item update from player %d: %s - %v\n", + + fmt.Printf("Broadcasting item update from player %d: %s - %v\n", sourcePlayerID, updateType, itemData) - + // Send to players in range (placeholder) clients := w.clients.GetAll() for _, client := range clients { @@ -975,4 +975,4 @@ func (w *World) BroadcastItemUpdate(sourcePlayerID uint32, updateType string, it client.SendSimpleMessage(fmt.Sprintf("Player item update: %s", updateType)) } } -} \ No newline at end of file +} diff --git a/internal/world/title_manager.go b/internal/world/title_manager.go index d35a97a..183eb5a 100644 --- a/internal/world/title_manager.go +++ b/internal/world/title_manager.go @@ -3,25 +3,25 @@ package world import ( "fmt" "sync" - - "eq2emu/internal/titles" + "eq2emu/internal/database" + "eq2emu/internal/titles" ) // TitleManager manages titles for the world server type TitleManager struct { - titleManager *titles.TitleManager - integrationMgr *titles.IntegrationManager - database *database.Database - - mutex sync.RWMutex + titleManager *titles.TitleManager + integrationMgr *titles.IntegrationManager + database *database.Database + + mutex sync.RWMutex } // NewTitleManager creates a new title manager for the world server func NewTitleManager(db *database.Database) *TitleManager { titleMgr := titles.NewTitleManager() integrationMgr := titles.NewIntegrationManager(titleMgr) - + return &TitleManager{ titleManager: titleMgr, integrationMgr: integrationMgr, @@ -32,19 +32,19 @@ func NewTitleManager(db *database.Database) *TitleManager { // LoadTitles loads all titles from database func (tm *TitleManager) LoadTitles() error { fmt.Println("Loading master title list...") - + pool := tm.database.GetPool() if pool == nil { return fmt.Errorf("database pool is nil") } - + // TODO: Implement title loading from database when database functions are available // For now, create some default titles for testing err := tm.createDefaultTitles() if err != nil { return fmt.Errorf("failed to create default titles: %w", err) } - + fmt.Printf("Loaded %d titles\n", tm.titleManager.GetMasterList().GetTitleCount()) return nil } @@ -52,7 +52,7 @@ func (tm *TitleManager) LoadTitles() error { // createDefaultTitles creates some default titles for testing func (tm *TitleManager) createDefaultTitles() error { masterList := tm.titleManager.GetMasterList() - + // Achievement-based titles achievementTitles := map[string]*titles.Title{ "First Blood": { @@ -136,7 +136,7 @@ func (tm *TitleManager) createDefaultTitles() error { Description: "Granted for leveling up", }, } - + // Add titles to master list for name, title := range achievementTitles { err := masterList.AddTitle(title) @@ -144,7 +144,7 @@ func (tm *TitleManager) createDefaultTitles() error { fmt.Printf("Warning: Failed to add title '%s': %v\n", name, err) } } - + return nil } @@ -173,12 +173,12 @@ func (tm *TitleManager) SetupAchievementIntegration() { // Setup callback to handle achievement completions tm.integrationMgr.AddTitleEarnedCallback(func(playerID, titleID int32, source string) { fmt.Printf("Player %d earned title %d from %s\n", playerID, titleID, source) - + // TODO: Send title granted packet to client // TODO: Send title list update to client // TODO: Broadcast title earned message if appropriate }) - + fmt.Println("Achievement-to-title integration setup complete") } @@ -189,41 +189,41 @@ func (tm *TitleManager) ProcessAchievementCompletion(playerID int32, achievement if err != nil { return fmt.Errorf("failed to process achievement completion: %w", err) } - + // Notify integration system tm.integrationMgr.NotifyTitleEarned(playerID, 0, "achievement") // Title ID is handled internally - + fmt.Printf("Processed achievement completion %d for player %d\n", achievementID, playerID) return nil } // GetStatistics returns title system statistics -func (tm *TitleManager) GetStatistics() map[string]interface{} { +func (tm *TitleManager) GetStatistics() map[string]any { tm.mutex.RLock() defer tm.mutex.RUnlock() - + // Get statistics from the underlying title manager titleManagerStats := tm.titleManager.GetStatistics() - + // Combine with our own statistics - stats := map[string]interface{}{ - "total_titles": tm.titleManager.GetMasterList().GetTitleCount(), + stats := map[string]any{ + "total_titles": tm.titleManager.GetMasterList().GetTitleCount(), } - + // Add statistics from the title manager for key, value := range titleManagerStats { stats[key] = value } - + return stats } // Shutdown gracefully shuts down the title manager func (tm *TitleManager) Shutdown() { fmt.Println("Shutting down title manager...") - + // TODO: Save player title data to database // TODO: Cleanup any background processes - + fmt.Println("Title manager shutdown complete") -} \ No newline at end of file +} diff --git a/internal/world/world.go b/internal/world/world.go index 1af7a2a..70a810f 100644 --- a/internal/world/world.go +++ b/internal/world/world.go @@ -16,88 +16,88 @@ import ( // World represents the main world server instance type World struct { // Core components - db *database.Database - commandManager *commands.CommandManager - rulesManager *rules.RuleManager - + db *database.Database + commandManager *commands.CommandManager + rulesManager *rules.RuleManager + // Server configuration - config *WorldConfig - startTime time.Time - shutdownTime *time.Time - shutdownReason string - + config *WorldConfig + startTime time.Time + shutdownTime *time.Time + shutdownReason string + // World time management worldTime *WorldTime worldTimeTicker *time.Ticker - + // Zones management - zones *ZoneList - + zones *ZoneList + // Client management - clients *ClientList - + clients *ClientList + // Achievement system - achievementMgr *AchievementManager - + achievementMgr *AchievementManager + // Title system - titleMgr *TitleManager - + titleMgr *TitleManager + // NPC system - npcMgr *NPCManager - + npcMgr *NPCManager + // Item system - itemMgr *ItemManager - + itemMgr *ItemManager + // Master lists (singletons) - masterSpells interface{} // TODO: implement spell manager - masterQuests interface{} // TODO: implement quest manager - masterSkills interface{} // TODO: implement skill manager - masterFactions interface{} // TODO: implement faction manager - + masterSpells any // TODO: implement spell manager + masterQuests any // TODO: implement quest manager + masterSkills any // TODO: implement skill manager + masterFactions any // TODO: implement faction manager + // Server statistics - stats *ServerStatistics - + stats *ServerStatistics + // Synchronization - mutex sync.RWMutex - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup + mutex sync.RWMutex + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup } // WorldConfig holds world server configuration type WorldConfig struct { // Network settings - ListenAddr string `json:"listen_addr"` - ListenPort int `json:"listen_port"` - MaxClients int `json:"max_clients"` - + ListenAddr string `json:"listen_addr"` + ListenPort int `json:"listen_port"` + MaxClients int `json:"max_clients"` + // Web interface settings - WebAddr string `json:"web_addr"` - WebPort int `json:"web_port"` - WebCertFile string `json:"web_cert_file"` - WebKeyFile string `json:"web_key_file"` - WebKeyPassword string `json:"web_key_password"` - + WebAddr string `json:"web_addr"` + WebPort int `json:"web_port"` + WebCertFile string `json:"web_cert_file"` + WebKeyFile string `json:"web_key_file"` + WebKeyPassword string `json:"web_key_password"` + // Database settings - DatabaseType string `json:"database_type"` // "sqlite" or "mysql" - DatabasePath string `json:"database_path"` // For SQLite: file path - DatabaseHost string `json:"database_host"` // For MySQL: hostname - DatabasePort int `json:"database_port"` // For MySQL: port - DatabaseName string `json:"database_name"` // For MySQL: database name - DatabaseUser string `json:"database_user"` // For MySQL: username - DatabasePass string `json:"database_pass"` // For MySQL: password - + DatabaseType string `json:"database_type"` // "sqlite" or "mysql" + DatabasePath string `json:"database_path"` // For SQLite: file path + DatabaseHost string `json:"database_host"` // For MySQL: hostname + DatabasePort int `json:"database_port"` // For MySQL: port + DatabaseName string `json:"database_name"` // For MySQL: database name + DatabaseUser string `json:"database_user"` // For MySQL: username + DatabasePass string `json:"database_pass"` // For MySQL: password + // Server settings - ServerName string `json:"server_name"` - ServerMOTD string `json:"server_motd"` - LogLevel string `json:"log_level"` - + ServerName string `json:"server_name"` + ServerMOTD string `json:"server_motd"` + LogLevel string `json:"log_level"` + // Game settings - XPRate float32 `json:"xp_rate"` - TSXPRate float32 `json:"ts_xp_rate"` - CoinRate float32 `json:"coin_rate"` - LootRate float32 `json:"loot_rate"` - + XPRate float32 `json:"xp_rate"` + TSXPRate float32 `json:"ts_xp_rate"` + CoinRate float32 `json:"coin_rate"` + LootRate float32 `json:"loot_rate"` + // Login server settings LoginServerAddr string `json:"login_server_addr"` LoginServerPort int `json:"login_server_port"` @@ -117,29 +117,29 @@ type WorldTime struct { // ServerStatistics tracks server metrics type ServerStatistics struct { // Server info - ServerCreated time.Time - ServerStartTime time.Time - + ServerCreated time.Time + ServerStartTime time.Time + // Connection stats - TotalConnections int64 - CurrentConnections int32 - MaxConnections int32 - + TotalConnections int64 + CurrentConnections int32 + MaxConnections int32 + // Character stats - TotalAccounts int32 - TotalCharacters int32 - AverageCharLevel float32 - + TotalAccounts int32 + TotalCharacters int32 + AverageCharLevel float32 + // Zone stats - ActiveZones int32 - ActiveInstances int32 - + ActiveZones int32 + ActiveInstances int32 + // Performance stats - CPUUsage float32 - MemoryUsage int64 - PeakMemoryUsage int64 - - mutex sync.RWMutex + CPUUsage float32 + MemoryUsage int64 + PeakMemoryUsage int64 + + mutex sync.RWMutex } // NewWorld creates a new world server instance @@ -147,7 +147,7 @@ func NewWorld(config *WorldConfig) (*World, error) { // Initialize database var db *database.Database var err error - + switch strings.ToLower(config.DatabaseType) { case "mysql", "mariadb": dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci", @@ -163,35 +163,35 @@ func NewWorld(config *WorldConfig) (*World, error) { default: return nil, fmt.Errorf("unsupported database type: %s", config.DatabaseType) } - + if err != nil { return nil, fmt.Errorf("failed to initialize database: %w", err) } - + // Initialize command manager cmdManager, err := commands.InitializeCommands() if err != nil { return nil, fmt.Errorf("failed to initialize commands: %w", err) } - + // Initialize rules manager rulesManager := rules.NewRuleManager() - + // Initialize achievement manager achievementMgr := NewAchievementManager(db) - + // Initialize title manager titleMgr := NewTitleManager(db) - + // Initialize NPC manager npcMgr := NewNPCManager(db) - + // Initialize item manager itemMgr := NewItemManager(db) - + // Create context ctx, cancel := context.WithCancel(context.Background()) - + w := &World{ db: db, commandManager: cmdManager, @@ -205,24 +205,24 @@ func NewWorld(config *WorldConfig) (*World, error) { worldTime: &WorldTime{Year: 3721, Month: 1, Day: 1, Hour: 12, Minute: 0}, zones: NewZoneList(), clients: NewClientList(), - stats: &ServerStatistics{ + stats: &ServerStatistics{ ServerStartTime: time.Now(), }, - ctx: ctx, - cancel: cancel, + ctx: ctx, + cancel: cancel, } - + // Set world references for cross-system communication achievementMgr.SetWorld(w) npcMgr.SetWorld(w) itemMgr.SetWorld(w) - + // Load server data from database if err := w.loadServerData(); err != nil { cancel() return nil, fmt.Errorf("failed to load server data: %w", err) } - + return w, nil } @@ -230,33 +230,33 @@ func NewWorld(config *WorldConfig) (*World, error) { func (w *World) Start() error { w.mutex.Lock() defer w.mutex.Unlock() - + fmt.Printf("Starting EQ2Go World Server '%s'...\n", w.config.ServerName) fmt.Printf("Listen Address: %s:%d\n", w.config.ListenAddr, w.config.ListenPort) - + // Register packet handlers w.RegisterPacketHandlers() - + // Load sample opcode mappings (TODO: Load from configuration files) w.loadSampleOpcodeMappings() - + // Start world time ticker w.worldTimeTicker = time.NewTicker(3 * time.Second) // EQ2 time tick w.wg.Add(1) go w.worldTimeTick() - + // Start statistics updater w.wg.Add(1) go w.updateStatistics() - + // Start zone watchdog w.wg.Add(1) go w.zoneWatchdog() - + // Start client handler w.wg.Add(1) go w.clientHandler() - + fmt.Println("World server started successfully!") return nil } @@ -265,46 +265,46 @@ func (w *World) Start() error { func (w *World) Stop() error { w.mutex.Lock() defer w.mutex.Unlock() - + fmt.Println("Shutting down world server...") - + // Cancel context to signal shutdown w.cancel() - + // Stop world time ticker if w.worldTimeTicker != nil { w.worldTimeTicker.Stop() } - + // Disconnect all clients w.clients.DisconnectAll("Server shutting down") - + // Shutdown all zones w.zones.ShutdownAll() - + // Wait for all goroutines to finish w.wg.Wait() - + // Shutdown achievement manager if w.achievementMgr != nil { w.achievementMgr.Shutdown() } - + // Shutdown title manager if w.titleMgr != nil { w.titleMgr.Shutdown() } - + // Shutdown NPC manager if w.npcMgr != nil { w.npcMgr.Shutdown() } - + // Close database if w.db != nil { w.db.Close() } - + fmt.Println("World server shutdown complete.") return nil } @@ -313,7 +313,7 @@ func (w *World) Stop() error { func (w *World) Process() { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() - + for { select { case <-w.ctx.Done(): @@ -328,16 +328,16 @@ func (w *World) Process() { func (w *World) processFrame() { // Process zones w.zones.ProcessAll() - + // Process clients w.clients.ProcessAll() - + // Process NPCs w.npcMgr.ProcessNPCs() - + // Check for scheduled shutdown w.checkShutdown() - + // Update vitality w.updateVitality() } @@ -345,14 +345,14 @@ func (w *World) processFrame() { // worldTimeTick advances the in-game time func (w *World) worldTimeTick() { defer w.wg.Done() - + for { select { case <-w.ctx.Done(): return case <-w.worldTimeTicker.C: w.worldTime.mutex.Lock() - + // Advance time (3 seconds = 1 game minute) w.worldTime.Minute++ if w.worldTime.Minute >= 60 { @@ -371,9 +371,9 @@ func (w *World) worldTimeTick() { } } } - + w.worldTime.mutex.Unlock() - + // Send time update to all zones w.zones.SendTimeUpdate(w.worldTime) } @@ -383,24 +383,24 @@ func (w *World) worldTimeTick() { // updateStatistics updates server statistics periodically func (w *World) updateStatistics() { defer w.wg.Done() - + ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() - + for { select { case <-w.ctx.Done(): return case <-ticker.C: w.stats.mutex.Lock() - + // Update current stats w.stats.CurrentConnections = w.clients.Count() w.stats.ActiveZones = w.zones.Count() w.stats.ActiveInstances = w.zones.CountInstances() - + // TODO: Update other statistics - + w.stats.mutex.Unlock() } } @@ -409,10 +409,10 @@ func (w *World) updateStatistics() { // zoneWatchdog monitors zone health func (w *World) zoneWatchdog() { defer w.wg.Done() - + ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() - + for { select { case <-w.ctx.Done(): @@ -420,7 +420,7 @@ func (w *World) zoneWatchdog() { case <-ticker.C: // Check zone health w.zones.CheckHealth() - + // Clean up dead zones w.zones.CleanupDead() } @@ -430,18 +430,18 @@ func (w *World) zoneWatchdog() { // clientHandler handles incoming client connections func (w *World) clientHandler() { defer w.wg.Done() - + // TODO: Implement UDP listener and client connection handling // This will create a UDP server that listens for incoming connections // and creates Client instances for each connection - + fmt.Printf("Client handler ready - waiting for UDP server integration\n") fmt.Printf("When UDP integration is complete, this will:\n") fmt.Printf(" - Listen on %s:%d for client connections\n", w.config.ListenAddr, w.config.ListenPort) fmt.Printf(" - Create Client instances for new connections\n") fmt.Printf(" - Process incoming packets through the opcode system\n") fmt.Printf(" - Handle client authentication and zone entry\n") - + // For now, just wait for shutdown <-w.ctx.Done() } @@ -449,39 +449,39 @@ func (w *World) clientHandler() { // loadServerData loads initial data from database func (w *World) loadServerData() error { fmt.Println("Loading server data from database...") - + // Load achievements if err := w.achievementMgr.LoadAchievements(); err != nil { fmt.Printf("Warning: Failed to load achievements: %v\n", err) // Don't fail startup if achievements don't load - server can still run } - + // Load titles if err := w.titleMgr.LoadTitles(); err != nil { fmt.Printf("Warning: Failed to load titles: %v\n", err) // Don't fail startup if titles don't load - server can still run } - + // Load NPCs if err := w.npcMgr.LoadNPCs(); err != nil { fmt.Printf("Warning: Failed to load NPCs: %v\n", err) // Don't fail startup if NPCs don't load - server can still run } - + // Load items if err := w.itemMgr.LoadItems(); err != nil { fmt.Printf("Warning: Failed to load items: %v\n", err) // Don't fail startup if items don't load - server can still run } - + // Setup title and achievement integration w.setupTitleAchievementIntegration() - + // Load rules (TODO: implement when rules database integration is ready) // if err := w.rulesManager.LoadRules(); err != nil { // return fmt.Errorf("failed to load rules: %w", err) // } - + // TODO: Load other server data // - Master spell list // - Master item list @@ -491,7 +491,7 @@ func (w *World) loadServerData() error { // - Starting skills/spells // - Merchant data // - Transport data - + fmt.Println("Server data loaded successfully.") return nil } @@ -501,7 +501,7 @@ func (w *World) checkShutdown() { w.mutex.RLock() shutdownTime := w.shutdownTime w.mutex.RUnlock() - + if shutdownTime != nil && time.Now().After(*shutdownTime) { fmt.Printf("Scheduled shutdown: %s\n", w.shutdownReason) go w.Stop() @@ -517,11 +517,11 @@ func (w *World) updateVitality() { func (w *World) ScheduleShutdown(minutes int, reason string) { w.mutex.Lock() defer w.mutex.Unlock() - + shutdownTime := time.Now().Add(time.Duration(minutes) * time.Minute) w.shutdownTime = &shutdownTime w.shutdownReason = reason - + // Announce to all clients message := fmt.Sprintf("Server shutdown scheduled in %d minutes: %s", minutes, reason) w.clients.BroadcastMessage(message) @@ -531,11 +531,11 @@ func (w *World) ScheduleShutdown(minutes int, reason string) { func (w *World) CancelShutdown() { w.mutex.Lock() defer w.mutex.Unlock() - + if w.shutdownTime != nil { w.shutdownTime = nil w.shutdownReason = "" - + // Announce cancellation w.clients.BroadcastMessage("Scheduled shutdown has been cancelled.") } @@ -545,7 +545,7 @@ func (w *World) CancelShutdown() { func (w *World) GetWorldTime() WorldTime { w.worldTime.mutex.RLock() defer w.worldTime.mutex.RUnlock() - + return WorldTime{ Year: w.worldTime.Year, Month: w.worldTime.Month, @@ -593,7 +593,7 @@ func (w *World) GetNPCManager() *NPCManager { // loadSampleOpcodeMappings loads sample opcode mappings for testing func (w *World) loadSampleOpcodeMappings() { fmt.Println("Loading sample opcode mappings...") - + // Sample opcode mappings for a common client version (60013) // These should eventually be loaded from configuration files sampleOpcodes := map[string]uint16{ @@ -659,7 +659,7 @@ func (w *World) loadSampleOpcodeMappings() { "OP_EqSetControlGhostCmd": 0x1006, "OP_EqSetPOVGhostCmd": 0x1007, } - + // Load opcodes for client version 60013 err := packets.LoadGlobalOpcodeMappings(60013, sampleOpcodes) if err != nil { @@ -667,7 +667,7 @@ func (w *World) loadSampleOpcodeMappings() { } else { fmt.Printf("Loaded %d opcode mappings for client version 60013\n", len(sampleOpcodes)) } - + // TODO: Load additional client versions and their opcode mappings // This would typically be done from external configuration files } @@ -675,14 +675,14 @@ func (w *World) loadSampleOpcodeMappings() { // setupTitleAchievementIntegration sets up integration between titles and achievements func (w *World) setupTitleAchievementIntegration() { fmt.Println("Setting up title and achievement integration...") - + if w.titleMgr == nil || w.achievementMgr == nil { fmt.Println("Warning: Cannot setup integration - title or achievement manager is nil") return } - + // Setup title manager's achievement integration w.titleMgr.SetupAchievementIntegration() - + fmt.Println("Title and achievement integration setup complete") -} \ No newline at end of file +}