get rid of world since it's broken
This commit is contained in:
parent
6b0f888fef
commit
f2484a403c
@ -1,357 +0,0 @@
|
||||
# EQ2Go World Server
|
||||
|
||||
The EQ2Go World Server is the main game server component that handles client connections, zone management, and game logic. This implementation is converted from the C++ EQ2EMu WorldServer while leveraging modern Go patterns.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The world server consists of several key components:
|
||||
|
||||
### Core Components
|
||||
|
||||
- **World**: Main server instance managing all subsystems
|
||||
- **ZoneList**: Zone management with instance support
|
||||
- **ClientList**: Connected player management
|
||||
- **Database**: SQLite-based data persistence
|
||||
- **CommandManager**: Integrated command system
|
||||
- **RuleManager**: Server configuration rules
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Multi-Zone Support**: Manages multiple zone instances
|
||||
- **Client Management**: Handles player connections and state
|
||||
- **Command Integration**: Full admin and player command support
|
||||
- **Database Integration**: SQLite with automatic schema creation
|
||||
- **Configuration Management**: JSON-based configuration with CLI overrides
|
||||
- **Graceful Shutdown**: Clean shutdown with proper resource cleanup
|
||||
- **Thread-Safe Operations**: All components use proper synchronization
|
||||
|
||||
## Server Components
|
||||
|
||||
### World Server (`world.go`)
|
||||
|
||||
The main World struct coordinates all server operations:
|
||||
|
||||
```go
|
||||
type World struct {
|
||||
db *database.Database
|
||||
commandManager *commands.CommandManager
|
||||
rulesManager *rules.RuleManager
|
||||
zones *ZoneList
|
||||
clients *ClientList
|
||||
config *WorldConfig
|
||||
worldTime *WorldTime
|
||||
stats *ServerStatistics
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
- `NewWorld()`: Creates a new world server instance
|
||||
- `Start()`: Starts all server subsystems
|
||||
- `Stop()`: Gracefully shuts down the server
|
||||
- `Process()`: Main server processing loop
|
||||
|
||||
### Zone Management (`zone_list.go`)
|
||||
|
||||
Manages all active zones and their instances:
|
||||
|
||||
**Features:**
|
||||
- Zone instance management
|
||||
- Population tracking
|
||||
- Health monitoring
|
||||
- Automatic cleanup
|
||||
- Load balancing
|
||||
|
||||
**Zone Properties:**
|
||||
- ID, Name, Instance ID
|
||||
- Level requirements
|
||||
- Population limits
|
||||
- Safe locations
|
||||
- Processing state
|
||||
|
||||
### Client Management (`client_list.go`)
|
||||
|
||||
Handles all connected players:
|
||||
|
||||
**Features:**
|
||||
- Connection tracking
|
||||
- Linkdead detection
|
||||
- Admin level management
|
||||
- Zone transitions
|
||||
- Command integration
|
||||
|
||||
**Client State:**
|
||||
- Account/Character information
|
||||
- Connection details
|
||||
- Zone assignment
|
||||
- AFK/Anonymous flags
|
||||
- Group/Guild membership
|
||||
|
||||
### Database Integration (`database.go`)
|
||||
|
||||
SQLite-based persistence with automatic schema creation:
|
||||
|
||||
**Tables:**
|
||||
- Rules: Server configuration rules
|
||||
- Accounts: Player accounts
|
||||
- Characters: Character data
|
||||
- Zones: Zone definitions
|
||||
- Server Stats: Performance metrics
|
||||
- Merchants: NPC merchant data
|
||||
|
||||
### Configuration System
|
||||
|
||||
JSON-based configuration with CLI overrides:
|
||||
|
||||
```json
|
||||
{
|
||||
"listen_addr": "0.0.0.0",
|
||||
"listen_port": 9000,
|
||||
"max_clients": 1000,
|
||||
"web_port": 8080,
|
||||
"database_type": "sqlite",
|
||||
"database_path": "eq2.db",
|
||||
"database_host": "localhost",
|
||||
"database_port": 3306,
|
||||
"database_name": "eq2emu",
|
||||
"database_user": "eq2",
|
||||
"database_pass": "password",
|
||||
"server_name": "EQ2Go World Server",
|
||||
"xp_rate": 1.0,
|
||||
"ts_xp_rate": 1.0
|
||||
}
|
||||
```
|
||||
|
||||
For MySQL/MariaDB configuration:
|
||||
```json
|
||||
{
|
||||
"database_type": "mysql",
|
||||
"database_host": "localhost",
|
||||
"database_port": 3306,
|
||||
"database_name": "eq2emu",
|
||||
"database_user": "eq2",
|
||||
"database_pass": "password"
|
||||
}
|
||||
```
|
||||
|
||||
For SQLite configuration (default):
|
||||
```json
|
||||
{
|
||||
"database_type": "sqlite",
|
||||
"database_path": "eq2.db"
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Startup
|
||||
|
||||
```bash
|
||||
# Use default configuration
|
||||
./world_server
|
||||
|
||||
# Override specific settings
|
||||
./world_server -listen-port 9001 -xp-rate 2.0 -name "My EQ2 Server"
|
||||
|
||||
# Use custom config file
|
||||
./world_server -config custom_config.json
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
|
||||
- `-config`: Configuration file path
|
||||
- `-listen-addr`: Override listen address
|
||||
- `-listen-port`: Override listen port
|
||||
- `-web-port`: Override web interface port
|
||||
- `-db`: Override database path
|
||||
- `-log-level`: Override log level
|
||||
- `-name`: Override server name
|
||||
- `-xp-rate`: Override XP rate multiplier
|
||||
- `-version`: Show version information
|
||||
|
||||
### Configuration File
|
||||
|
||||
On first run, a default configuration file is created automatically. The configuration includes:
|
||||
|
||||
**Network Settings:**
|
||||
- Listen address and port
|
||||
- Maximum client connections
|
||||
- Web interface settings
|
||||
|
||||
**Database Settings:**
|
||||
- Database file path
|
||||
- Connection pool settings
|
||||
|
||||
**Game Settings:**
|
||||
- XP/TS XP/Coin/Loot rate multipliers
|
||||
- Server name and MOTD
|
||||
- Login server connection details
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Command System Integration
|
||||
|
||||
The world server fully integrates with the command system:
|
||||
|
||||
```go
|
||||
// Client implements commands.ClientInterface
|
||||
func (c *Client) GetPlayer() *entity.Entity
|
||||
func (c *Client) SendMessage(channel int, color int, message string)
|
||||
func (c *Client) GetAdminLevel() int
|
||||
|
||||
// Zone implements commands.ZoneInterface through ZoneAdapter
|
||||
func (za *ZoneAdapter) GetID() int32
|
||||
func (za *ZoneAdapter) SendZoneMessage(channel int, color int, message string)
|
||||
```
|
||||
|
||||
### Database Integration
|
||||
|
||||
All server data is persisted to SQLite:
|
||||
|
||||
```go
|
||||
// Load server configuration
|
||||
func (d *Database) LoadRules() (map[string]map[string]string, error)
|
||||
|
||||
// Zone management
|
||||
func (d *Database) GetZones() ([]map[string]any, error)
|
||||
|
||||
// Character persistence (planned)
|
||||
func (d *Database) SaveCharacter(character *Character) error
|
||||
```
|
||||
|
||||
### Rules System Integration
|
||||
|
||||
Integrates with the existing rules package:
|
||||
|
||||
```go
|
||||
// Rules manager provides server configuration
|
||||
rulesManager := rules.NewRuleManager()
|
||||
|
||||
// Access server rules
|
||||
maxLevel := rulesManager.GetInt32(rules.CategoryServer, rules.TypeMaxLevel)
|
||||
xpRate := rulesManager.GetFloat32(rules.CategoryServer, rules.TypeXPMultiplier)
|
||||
```
|
||||
|
||||
## Server Statistics
|
||||
|
||||
The world server tracks comprehensive statistics:
|
||||
|
||||
- Connection counts (current, total, peak)
|
||||
- Zone statistics (active zones, instances)
|
||||
- Performance metrics (CPU, memory usage)
|
||||
- Character statistics (total accounts, average level)
|
||||
|
||||
## Time System
|
||||
|
||||
Implements EverQuest II's accelerated time system:
|
||||
|
||||
- 3 real seconds = 1 game minute
|
||||
- 72 real minutes = 1 game day
|
||||
- Broadcasts time updates to all zones
|
||||
- Supports custom starting year/date
|
||||
|
||||
## Lifecycle Management
|
||||
|
||||
### Startup Sequence
|
||||
|
||||
1. Load/create configuration file
|
||||
2. Initialize database connection
|
||||
3. Load server data from database
|
||||
4. Initialize command and rules managers
|
||||
5. Start background processing threads
|
||||
6. Begin accepting client connections
|
||||
|
||||
### Shutdown Sequence
|
||||
|
||||
1. Stop accepting new connections
|
||||
2. Disconnect all clients gracefully
|
||||
3. Shutdown all zones
|
||||
4. Save server state
|
||||
5. Close database connections
|
||||
6. Clean up resources
|
||||
|
||||
## Thread Safety
|
||||
|
||||
All components use proper Go synchronization:
|
||||
|
||||
- `sync.RWMutex` for read-heavy operations
|
||||
- `sync.Mutex` for exclusive access
|
||||
- `context.Context` for cancellation
|
||||
- `sync.WaitGroup` for graceful shutdown
|
||||
|
||||
## Future Integration Points
|
||||
|
||||
The world server is designed for easy integration with:
|
||||
|
||||
### Network Layer (UDP Package)
|
||||
```go
|
||||
// TODO: Integrate UDP connection handling
|
||||
func (w *World) handleClientConnection(conn *udp.Connection) {
|
||||
client := &Client{
|
||||
Connection: conn,
|
||||
// ...
|
||||
}
|
||||
w.clients.Add(client)
|
||||
}
|
||||
```
|
||||
|
||||
### Game Systems
|
||||
```go
|
||||
// TODO: Integrate additional game systems
|
||||
w.spellManager = spells.NewManager()
|
||||
w.questManager = quests.NewManager()
|
||||
w.itemManager = items.NewManager()
|
||||
```
|
||||
|
||||
### Zone Loading
|
||||
```go
|
||||
// TODO: Implement zone loading from database
|
||||
func (zl *ZoneList) LoadZone(zoneID int32) (*ZoneServer, error) {
|
||||
// Load zone data from database
|
||||
// Initialize NPCs, spawns, objects
|
||||
// Start zone processing
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Comprehensive error handling throughout:
|
||||
|
||||
- Database connection errors
|
||||
- Configuration validation
|
||||
- Resource cleanup on errors
|
||||
- Graceful degradation
|
||||
- Detailed error logging
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Efficient concurrent data structures
|
||||
- Connection pooling for database
|
||||
- Batched time updates
|
||||
- Lazy loading of game data
|
||||
- Memory pool reuse (planned)
|
||||
|
||||
## Testing
|
||||
|
||||
The world server can be tested in isolation:
|
||||
|
||||
```bash
|
||||
# Run server with test configuration
|
||||
./world_server -config test_config.json -db test.db
|
||||
|
||||
# Test with different rates
|
||||
./world_server -xp-rate 10.0 -name "Test Server"
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
Built-in monitoring capabilities:
|
||||
|
||||
- Server statistics tracking
|
||||
- Zone health monitoring
|
||||
- Client connection monitoring
|
||||
- Performance metrics collection
|
||||
- Automatic dead zone cleanup
|
||||
|
||||
## 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.
|
@ -1,415 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AchievementEventType represents different types of achievement events
|
||||
type AchievementEventType int
|
||||
|
||||
const (
|
||||
// Combat events
|
||||
EventNPCKill AchievementEventType = iota
|
||||
EventPlayerKill
|
||||
EventDeathByNPC
|
||||
EventDeathByPlayer
|
||||
EventDamageDealt
|
||||
EventHealingDone
|
||||
|
||||
// Quest events
|
||||
EventQuestCompleted
|
||||
EventQuestStep
|
||||
EventQuestStarted
|
||||
EventQuestAbandoned
|
||||
|
||||
// Skill events
|
||||
EventSkillIncrease
|
||||
EventSkillMastery
|
||||
EventSpellLearned
|
||||
EventSpellCast
|
||||
|
||||
// Item events
|
||||
EventItemDiscovered
|
||||
EventItemCrafted
|
||||
EventItemLooted
|
||||
EventItemEquipped
|
||||
EventItemSold
|
||||
EventItemBought
|
||||
|
||||
// Exploration events
|
||||
EventZoneDiscovered
|
||||
EventLocationDiscovered
|
||||
EventPOIDiscovered
|
||||
|
||||
// Social events
|
||||
EventGuildJoin
|
||||
EventGuildLeave
|
||||
EventGroupJoin
|
||||
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
|
||||
EventInstanceComplete
|
||||
EventCollectionComplete
|
||||
)
|
||||
|
||||
// AchievementEvent represents a single achievement-related event
|
||||
type AchievementEvent struct {
|
||||
Type AchievementEventType
|
||||
CharacterID int32
|
||||
Data map[string]any
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
// AchievementEventHandler processes achievement events
|
||||
type AchievementEventHandler struct {
|
||||
world *World
|
||||
}
|
||||
|
||||
// NewAchievementEventHandler creates a new achievement event handler
|
||||
func NewAchievementEventHandler(world *World) *AchievementEventHandler {
|
||||
return &AchievementEventHandler{
|
||||
world: world,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessEvent processes an achievement event and updates progress
|
||||
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:
|
||||
return aeh.handleNPCKill(event, achievementMgr)
|
||||
case EventQuestCompleted:
|
||||
return aeh.handleQuestCompleted(event, achievementMgr)
|
||||
case EventLevelGain:
|
||||
return aeh.handleLevelGain(event, achievementMgr)
|
||||
case EventSkillIncrease:
|
||||
return aeh.handleSkillIncrease(event, achievementMgr)
|
||||
case EventItemDiscovered:
|
||||
return aeh.handleItemDiscovered(event, achievementMgr)
|
||||
case EventZoneDiscovered:
|
||||
return aeh.handleZoneDiscovered(event, achievementMgr)
|
||||
case EventHarvest:
|
||||
return aeh.handleHarvest(event, achievementMgr)
|
||||
case EventPvPKill:
|
||||
return aeh.handlePvPKill(event, achievementMgr)
|
||||
default:
|
||||
// For unhandled events, try generic processing
|
||||
return aeh.handleGenericEvent(event, achievementMgr)
|
||||
}
|
||||
}
|
||||
|
||||
// handleNPCKill processes NPC kill events
|
||||
func (aeh *AchievementEventHandler) handleNPCKill(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
npcID, ok := event.Data["npc_id"].(int32)
|
||||
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
|
||||
}
|
||||
|
||||
// handleQuestCompleted processes quest completion events
|
||||
func (aeh *AchievementEventHandler) handleQuestCompleted(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
questID, ok := event.Data["quest_id"].(int32)
|
||||
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
|
||||
}
|
||||
|
||||
// handleLevelGain processes level gain events
|
||||
func (aeh *AchievementEventHandler) handleLevelGain(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
newLevel, ok := event.Data["level"].(int32)
|
||||
if !ok {
|
||||
return fmt.Errorf("level not found in event data")
|
||||
}
|
||||
|
||||
// Update level-based achievements
|
||||
switch newLevel {
|
||||
case 10:
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 30, 1) // Achievement ID 30: "Growing Strong"
|
||||
case 20:
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 31, 1) // Achievement ID 31: "Seasoned Adventurer"
|
||||
case 50:
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 32, 1) // Achievement ID 32: "Veteran"
|
||||
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
|
||||
}
|
||||
|
||||
// handleSkillIncrease processes skill increase events
|
||||
func (aeh *AchievementEventHandler) handleSkillIncrease(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
skillID, ok := event.Data["skill_id"].(int32)
|
||||
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
|
||||
if skillLevel >= 300 {
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 40, 1) // Achievement ID 40: "Weapon Master"
|
||||
}
|
||||
case 10: // Magic skill
|
||||
if skillLevel >= 300 {
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 41, 1) // Achievement ID 41: "Arcane Scholar"
|
||||
}
|
||||
case 20: // Crafting skill
|
||||
if skillLevel >= 300 {
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 42, 1) // Achievement ID 42: "Master Craftsman"
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleItemDiscovered processes item discovery events
|
||||
func (aeh *AchievementEventHandler) handleItemDiscovered(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
itemID, ok := event.Data["item_id"].(int32)
|
||||
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":
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 51, 1) // Achievement ID 51: "Rare Collector"
|
||||
case "legendary":
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 52, 1) // Achievement ID 52: "Legend Seeker"
|
||||
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
|
||||
}
|
||||
|
||||
// handleZoneDiscovered processes zone discovery events
|
||||
func (aeh *AchievementEventHandler) handleZoneDiscovered(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
zoneID, ok := event.Data["zone_id"].(int32)
|
||||
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
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 61, 1) // Achievement ID 61: "First Steps"
|
||||
case 100: // End game zone
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 62, 1) // Achievement ID 62: "Into the Unknown"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleHarvest processes harvesting events
|
||||
func (aeh *AchievementEventHandler) handleHarvest(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
resourceType, ok := event.Data["resource_type"].(string)
|
||||
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":
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 71, 1) // Achievement ID 71: "Miner"
|
||||
case "wood":
|
||||
achievementMgr.UpdateProgress(event.CharacterID, 72, 1) // Achievement ID 72: "Lumberjack"
|
||||
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
|
||||
}
|
||||
|
||||
// handlePvPKill processes PvP kill events
|
||||
func (aeh *AchievementEventHandler) handlePvPKill(event *AchievementEvent, achievementMgr *AchievementManager) error {
|
||||
targetLevel, ok := event.Data["target_level"].(int32)
|
||||
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
|
||||
}
|
||||
|
||||
// handleGenericEvent processes generic events
|
||||
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",
|
||||
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]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 {
|
||||
fmt.Printf("Error processing achievement event: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Convenience methods for common events
|
||||
|
||||
// OnNPCKill triggers an NPC kill achievement event
|
||||
func (w *World) OnNPCKill(characterID int32, npcID int32, npcLevel int32) {
|
||||
w.TriggerAchievementEvent(EventNPCKill, characterID, map[string]any{
|
||||
"npc_id": npcID,
|
||||
"level": npcLevel,
|
||||
})
|
||||
}
|
||||
|
||||
// OnQuestComplete triggers a quest completion achievement event
|
||||
func (w *World) OnQuestComplete(characterID int32, questID int32) {
|
||||
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]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]any{
|
||||
"item_id": itemID,
|
||||
"rarity": rarity,
|
||||
})
|
||||
}
|
||||
|
||||
// OnZoneDiscovered triggers a zone discovery achievement event
|
||||
func (w *World) OnZoneDiscovered(characterID int32, zoneID int32) {
|
||||
w.TriggerAchievementEvent(EventZoneDiscovered, characterID, map[string]any{
|
||||
"zone_id": zoneID,
|
||||
})
|
||||
}
|
@ -1,685 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/achievements"
|
||||
"eq2emu/internal/database"
|
||||
"eq2emu/internal/packets"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewAchievementManager creates a new achievement manager
|
||||
func NewAchievementManager(db *database.Database) *AchievementManager {
|
||||
return &AchievementManager{
|
||||
masterList: achievements.NewMasterList(),
|
||||
playerManagers: make(map[int32]*achievements.PlayerManager),
|
||||
database: db,
|
||||
world: nil, // Set by world server after creation
|
||||
}
|
||||
}
|
||||
|
||||
// SetWorld sets the world server reference for notifications
|
||||
func (am *AchievementManager) SetWorld(world *World) {
|
||||
am.world = world
|
||||
}
|
||||
|
||||
// LoadAchievements loads all achievements from database
|
||||
func (am *AchievementManager) LoadAchievements() error {
|
||||
fmt.Println("Loading master achievement list...")
|
||||
|
||||
err := achievements.LoadAllAchievements(am.database, 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
|
||||
}
|
||||
|
||||
// GetPlayerManager gets or creates a player achievement manager
|
||||
func (am *AchievementManager) GetPlayerManager(characterID int32) *achievements.PlayerManager {
|
||||
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
|
||||
}
|
||||
|
||||
// loadPlayerAchievements loads achievement data for a specific player
|
||||
func (am *AchievementManager) loadPlayerAchievements(characterID int32, playerMgr *achievements.PlayerManager) {
|
||||
// Load player achievements
|
||||
err := achievements.LoadPlayerAchievements(am.database, 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(am.database, uint32(characterID), playerMgr.Updates)
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading achievement progress for character %d: %v\n", characterID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateProgress updates player progress for an achievement
|
||||
func (am *AchievementManager) UpdateProgress(characterID int32, achievementID uint32, progress uint32) error {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
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 {
|
||||
completed, err := playerMgr.CheckRequirements(achievement)
|
||||
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)
|
||||
|
||||
// Send achievement update packet to client
|
||||
go am.sendAchievementUpdateToClient(characterID)
|
||||
|
||||
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)
|
||||
|
||||
// Send achievement update packet for progress update
|
||||
go am.sendAchievementUpdateToClient(characterID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// savePlayerProgress saves player achievement progress to database
|
||||
func (am *AchievementManager) savePlayerProgress(characterID int32, achievementID uint32, playerMgr *achievements.PlayerManager) {
|
||||
update := playerMgr.Updates.GetUpdate(achievementID)
|
||||
if update == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := achievements.SavePlayerAchievementUpdate(am.database, uint32(characterID), update)
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving achievement progress for character %d, achievement %d: %v\n",
|
||||
characterID, achievementID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// onAchievementCompleted handles achievement completion events
|
||||
func (am *AchievementManager) onAchievementCompleted(characterID int32, achievement *achievements.Achievement) {
|
||||
// Award points
|
||||
if achievement.PointValue > 0 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// notifyAchievementCompleted notifies other systems about achievement completion
|
||||
func (am *AchievementManager) notifyAchievementCompleted(characterID int32, achievementID uint32) {
|
||||
// Notify title system if available
|
||||
if am.world != nil && am.world.titleMgr != nil {
|
||||
integrationMgr := am.world.titleMgr.GetIntegrationManager()
|
||||
if integrationMgr != nil {
|
||||
achievementIntegration := integrationMgr.GetAchievementIntegration()
|
||||
if achievementIntegration != nil {
|
||||
err := achievementIntegration.OnAchievementCompleted(characterID, achievementID)
|
||||
if err != nil {
|
||||
fmt.Printf("Error processing achievement completion for titles: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processReward processes an achievement reward
|
||||
func (am *AchievementManager) processReward(characterID int32, reward achievements.Reward) {
|
||||
// Basic reward processing - extend based on reward types
|
||||
switch reward.Reward {
|
||||
case "title":
|
||||
// Award title
|
||||
fmt.Printf("Character %d earned a title reward\n", characterID)
|
||||
case "item":
|
||||
// Award item
|
||||
fmt.Printf("Character %d earned an item reward\n", characterID)
|
||||
case "experience":
|
||||
// Award experience
|
||||
fmt.Printf("Character %d earned experience reward\n", characterID)
|
||||
default:
|
||||
fmt.Printf("Character %d earned reward: %s\n", characterID, reward.Reward)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAchievement gets an achievement by ID from master list
|
||||
func (am *AchievementManager) GetAchievement(achievementID uint32) *achievements.Achievement {
|
||||
return am.masterList.GetAchievement(achievementID)
|
||||
}
|
||||
|
||||
// GetAchievementsByCategory gets achievements filtered by category
|
||||
func (am *AchievementManager) GetAchievementsByCategory(category string) []*achievements.Achievement {
|
||||
return am.masterList.GetAchievementsByCategory(category)
|
||||
}
|
||||
|
||||
// GetAchievementsByExpansion gets achievements filtered by expansion
|
||||
func (am *AchievementManager) GetAchievementsByExpansion(expansion string) []*achievements.Achievement {
|
||||
return am.masterList.GetAchievementsByExpansion(expansion)
|
||||
}
|
||||
|
||||
// GetPlayerProgress gets player's progress for an achievement
|
||||
func (am *AchievementManager) GetPlayerProgress(characterID int32, achievementID uint32) uint32 {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return playerMgr.Updates.GetProgress(achievementID)
|
||||
}
|
||||
|
||||
// IsPlayerCompleted checks if player has completed an achievement
|
||||
func (am *AchievementManager) IsPlayerCompleted(characterID int32, achievementID uint32) bool {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return playerMgr.Updates.IsCompleted(achievementID)
|
||||
}
|
||||
|
||||
// GetPlayerCompletedAchievements gets all completed achievement IDs for a player
|
||||
func (am *AchievementManager) GetPlayerCompletedAchievements(characterID int32) []uint32 {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return playerMgr.Updates.GetCompletedAchievements()
|
||||
}
|
||||
|
||||
// GetPlayerInProgressAchievements gets all in-progress achievement IDs for a player
|
||||
func (am *AchievementManager) GetPlayerInProgressAchievements(characterID int32) []uint32 {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return playerMgr.Updates.GetInProgressAchievements()
|
||||
}
|
||||
|
||||
// GetCompletionPercentage gets completion percentage for player's achievement
|
||||
func (am *AchievementManager) GetCompletionPercentage(characterID int32, achievementID uint32) float64 {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
achievement := am.masterList.GetAchievement(achievementID)
|
||||
if achievement == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return playerMgr.GetCompletionStatus(achievement)
|
||||
}
|
||||
|
||||
// RemovePlayerManager removes a player manager (called when player logs out)
|
||||
func (am *AchievementManager) RemovePlayerManager(characterID int32) {
|
||||
am.mutex.Lock()
|
||||
defer am.mutex.Unlock()
|
||||
|
||||
delete(am.playerManagers, characterID)
|
||||
}
|
||||
|
||||
// GetMasterList returns the master achievement list
|
||||
func (am *AchievementManager) GetMasterList() *achievements.MasterList {
|
||||
return am.masterList
|
||||
}
|
||||
|
||||
// GetStatistics returns achievement system statistics
|
||||
func (am *AchievementManager) GetStatistics() map[string]any {
|
||||
am.mutex.RLock()
|
||||
defer am.mutex.RUnlock()
|
||||
|
||||
stats := map[string]any{
|
||||
"total_achievements": am.masterList.Size(),
|
||||
"online_players": len(am.playerManagers),
|
||||
"categories": am.masterList.GetCategories(),
|
||||
"expansions": am.masterList.GetExpansions(),
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// CreateAchievementUpdatePacket creates an achievement update packet for a player
|
||||
func (am *AchievementManager) CreateAchievementUpdatePacket(characterID int32, version uint32) ([]byte, error) {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return nil, fmt.Errorf("player manager not found for character %d", characterID)
|
||||
}
|
||||
|
||||
updates := playerMgr.Updates.GetAllUpdates()
|
||||
|
||||
// Build the packet data map according to the AchievementUpdate.xml structure
|
||||
achievementArray := make([]map[string]any, 0, len(updates))
|
||||
|
||||
for achievementID, update := range updates {
|
||||
var completedDate uint32
|
||||
if !update.CompletedDate.IsZero() {
|
||||
completedDate = uint32(update.CompletedDate.Unix())
|
||||
}
|
||||
|
||||
// Build item array for this achievement
|
||||
itemArray := make([]map[string]any, 0, len(update.UpdateItems))
|
||||
for _, item := range update.UpdateItems {
|
||||
itemArray = append(itemArray, map[string]any{
|
||||
"item_update": item.ItemUpdate,
|
||||
})
|
||||
}
|
||||
|
||||
achievementData := map[string]any{
|
||||
"achievement_id": achievementID,
|
||||
"completed_date": completedDate,
|
||||
"num_items": uint8(len(update.UpdateItems)),
|
||||
"item_array": itemArray,
|
||||
}
|
||||
|
||||
achievementArray = append(achievementArray, achievementData)
|
||||
}
|
||||
|
||||
packetData := map[string]any{
|
||||
"unknown1": uint8(0),
|
||||
"num_achievements": uint16(len(updates)),
|
||||
"achievement_array": achievementArray,
|
||||
}
|
||||
|
||||
// Build the packet using the packet system
|
||||
packetBytes, err := packets.BuildPacket("AchievementUpdate", packetData, version, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build achievement update packet: %w", err)
|
||||
}
|
||||
|
||||
return packetBytes, nil
|
||||
}
|
||||
|
||||
// SendAchievementUpdateToPlayer sends achievement update packet to a player
|
||||
func (am *AchievementManager) SendAchievementUpdateToPlayer(characterID int32, clientVersion int32) error {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return fmt.Errorf("player manager not found for character %d", characterID)
|
||||
}
|
||||
|
||||
// Create the packet data
|
||||
packetData, err := am.CreateAchievementUpdatePacket(characterID, uint32(clientVersion))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create achievement update packet: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Send packet to player through world server client connection
|
||||
// This would typically use the world server's client manager
|
||||
if am.world != nil {
|
||||
// Get client opcode for this version
|
||||
clientOpcode := packets.InternalToClient(packets.OP_AchievementUpdateMsg, clientVersion)
|
||||
if clientOpcode == 0 {
|
||||
return fmt.Errorf("no client opcode mapping for achievement update in version %d", clientVersion)
|
||||
}
|
||||
|
||||
fmt.Printf("Would send achievement update packet to character %d (opcode: %d, size: %d bytes)\n",
|
||||
characterID, clientOpcode, len(packetData))
|
||||
|
||||
// In a real implementation:
|
||||
// return am.world.SendPacketToClient(characterID, clientOpcode, packetData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCharacterAchievementsPacket creates a character achievements packet (master list) for a player
|
||||
func (am *AchievementManager) CreateCharacterAchievementsPacket(characterID int32, version uint32) ([]byte, error) {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return nil, fmt.Errorf("player manager not found for character %d", characterID)
|
||||
}
|
||||
|
||||
// Get all achievements from master list
|
||||
allAchievements := am.masterList.GetAllAchievements()
|
||||
|
||||
// Build achievement array according to CharacterAchievements.xml structure
|
||||
achievementArray := make([]map[string]any, 0, len(allAchievements))
|
||||
|
||||
for _, achievement := range allAchievements {
|
||||
// Build requirements array
|
||||
itemArray := make([]map[string]any, 0, len(achievement.Requirements))
|
||||
for _, req := range achievement.Requirements {
|
||||
itemArray = append(itemArray, map[string]any{
|
||||
"item_name": req.Name,
|
||||
"item_qty_req": req.QtyRequired,
|
||||
})
|
||||
}
|
||||
|
||||
// Build rewards array
|
||||
rewardArray := make([]map[string]any, 0, len(achievement.Rewards))
|
||||
for _, reward := range achievement.Rewards {
|
||||
rewardData := map[string]any{
|
||||
"reward_item": reward.Reward,
|
||||
}
|
||||
|
||||
// Add unknown4 field for version 57032+
|
||||
if version >= 57032 {
|
||||
rewardData["unknown4"] = uint32(0)
|
||||
}
|
||||
|
||||
rewardArray = append(rewardArray, rewardData)
|
||||
}
|
||||
|
||||
// Build achievement data based on version
|
||||
achievementData := map[string]any{
|
||||
"achievement_id": achievement.AchievementID,
|
||||
"title": achievement.Title,
|
||||
"uncompleted_text": achievement.UncompletedText,
|
||||
"completed_text": achievement.CompletedText,
|
||||
"category": achievement.Category,
|
||||
"expansion": achievement.Expansion,
|
||||
"icon": achievement.Icon,
|
||||
"point_value": achievement.PointValue,
|
||||
"qty_req": achievement.QtyRequired,
|
||||
"hide_achievement": uint8(0), // Convert bool to uint8
|
||||
}
|
||||
|
||||
if achievement.Hide {
|
||||
achievementData["hide_achievement"] = uint8(1)
|
||||
}
|
||||
|
||||
// Handle version-specific fields
|
||||
switch {
|
||||
case version >= 57032:
|
||||
achievementData["unknown3"] = [2]uint32{achievement.Unknown3A, achievement.Unknown3B}
|
||||
achievementData["num_items"] = uint8(len(achievement.Requirements))
|
||||
achievementData["item_array"] = itemArray
|
||||
achievementData["num_rewards"] = uint8(len(achievement.Rewards))
|
||||
achievementData["reward_array"] = rewardArray
|
||||
achievementData["num_reward_links"] = uint8(0) // TODO: Implement reward links if needed
|
||||
achievementData["reward_link_array"] = []map[string]any{}
|
||||
|
||||
case version >= 1096:
|
||||
achievementData["unknown3"] = [2]uint32{achievement.Unknown3A, achievement.Unknown3B}
|
||||
achievementData["num_items"] = uint8(len(achievement.Requirements))
|
||||
achievementData["item_array"] = itemArray
|
||||
achievementData["num_rewards"] = uint8(len(achievement.Rewards))
|
||||
achievementData["reward_array"] = rewardArray
|
||||
|
||||
case version >= 603:
|
||||
achievementData["unknown3a"] = achievement.Unknown3A
|
||||
achievementData["unknown3b"] = achievement.Unknown3B
|
||||
achievementData["guild"] = uint8(0) // TODO: Implement guild achievements if needed
|
||||
achievementData["num_items"] = uint8(len(achievement.Requirements))
|
||||
achievementData["item_array"] = itemArray
|
||||
achievementData["num_reward_links"] = uint8(0) // TODO: Implement reward links if needed
|
||||
achievementData["reward_link_array"] = []map[string]any{}
|
||||
}
|
||||
|
||||
achievementArray = append(achievementArray, achievementData)
|
||||
}
|
||||
|
||||
packetData := map[string]any{
|
||||
"num_achievements": uint16(len(allAchievements)),
|
||||
"achievement_array": achievementArray,
|
||||
}
|
||||
|
||||
// Build the packet using the packet system
|
||||
packetBytes, err := packets.BuildPacket("CharacterAchievements", packetData, version, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build character achievements packet: %w", err)
|
||||
}
|
||||
|
||||
return packetBytes, nil
|
||||
}
|
||||
|
||||
// SendCharacterAchievementsToPlayer sends the master achievement list to a player
|
||||
func (am *AchievementManager) SendCharacterAchievementsToPlayer(characterID int32, clientVersion int32) error {
|
||||
// Create the packet data
|
||||
packetData, err := am.CreateCharacterAchievementsPacket(characterID, uint32(clientVersion))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create character achievements packet: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Send packet to player through world server client connection
|
||||
if am.world != nil {
|
||||
// Get client opcode for this version
|
||||
clientOpcode := packets.InternalToClient(packets.OP_CharacterAchievements, clientVersion)
|
||||
if clientOpcode == 0 {
|
||||
return fmt.Errorf("no client opcode mapping for character achievements in version %d", clientVersion)
|
||||
}
|
||||
|
||||
fmt.Printf("Would send character achievements packet to character %d (opcode: %d, size: %d bytes)\n",
|
||||
characterID, clientOpcode, len(packetData))
|
||||
|
||||
// In a real implementation:
|
||||
// return am.world.SendPacketToClient(characterID, clientOpcode, packetData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AwardAchievementPoints awards achievement points to a player
|
||||
func (am *AchievementManager) AwardAchievementPoints(characterID int32, points uint32) error {
|
||||
// TODO: Integrate with player character system to award achievement points
|
||||
// This would typically update the player's total achievement points
|
||||
fmt.Printf("Character %d awarded %d achievement points\n", characterID, points)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessAchievementTrigger processes an achievement trigger for a player
|
||||
func (am *AchievementManager) ProcessAchievementTrigger(characterID int32, triggerType string, value uint32) error {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return fmt.Errorf("player manager not found for character %d", characterID)
|
||||
}
|
||||
|
||||
// Get all achievements and check if any match the trigger
|
||||
allAchievements := am.masterList.GetAllAchievements()
|
||||
|
||||
for _, achievement := range allAchievements {
|
||||
// Check requirements to see if any match the trigger
|
||||
for _, requirement := range achievement.Requirements {
|
||||
if requirement.Name == triggerType {
|
||||
// Update progress for this achievement
|
||||
currentProgress := playerMgr.Updates.GetProgress(achievement.AchievementID)
|
||||
newProgress := currentProgress + value
|
||||
|
||||
// Ensure we don't exceed the requirement
|
||||
if newProgress > requirement.QtyRequired {
|
||||
newProgress = requirement.QtyRequired
|
||||
}
|
||||
|
||||
// Update the progress
|
||||
err := am.UpdateProgress(characterID, achievement.AchievementID, newProgress)
|
||||
if err != nil {
|
||||
fmt.Printf("Error updating progress for achievement %d: %v\n", achievement.AchievementID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlayerAchievementPoints returns total achievement points for a player
|
||||
func (am *AchievementManager) GetPlayerAchievementPoints(characterID int32) uint32 {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var totalPoints uint32
|
||||
completedAchievements := playerMgr.Updates.GetCompletedAchievements()
|
||||
|
||||
for _, achievementID := range completedAchievements {
|
||||
achievement := am.masterList.GetAchievement(achievementID)
|
||||
if achievement != nil {
|
||||
totalPoints += achievement.PointValue
|
||||
}
|
||||
}
|
||||
|
||||
return totalPoints
|
||||
}
|
||||
|
||||
// RefreshPlayerAchievements refreshes a player's achievement data
|
||||
func (am *AchievementManager) RefreshPlayerAchievements(characterID int32) error {
|
||||
// Remove existing player manager
|
||||
am.RemovePlayerManager(characterID)
|
||||
|
||||
// This will create a new manager and load fresh data
|
||||
am.GetPlayerManager(characterID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAchievementProgress returns detailed progress information for a player's achievement
|
||||
func (am *AchievementManager) GetAchievementProgress(characterID int32, achievementID uint32) map[string]any {
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
achievement := am.masterList.GetAchievement(achievementID)
|
||||
if achievement == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
progress := playerMgr.Updates.GetProgress(achievementID)
|
||||
completed := playerMgr.Updates.IsCompleted(achievementID)
|
||||
completionPercentage := am.GetCompletionPercentage(characterID, achievementID)
|
||||
|
||||
result := map[string]any{
|
||||
"achievement_id": achievementID,
|
||||
"title": achievement.Title,
|
||||
"category": achievement.Category,
|
||||
"current_progress": progress,
|
||||
"required_progress": achievement.QtyRequired,
|
||||
"completed": completed,
|
||||
"completion_percentage": completionPercentage,
|
||||
"point_value": achievement.PointValue,
|
||||
}
|
||||
|
||||
if completed {
|
||||
completedDate := playerMgr.Updates.GetCompletedDate(achievementID)
|
||||
if !completedDate.IsZero() {
|
||||
result["completed_date"] = completedDate.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// sendAchievementUpdateToClient sends achievement update to client with version detection
|
||||
func (am *AchievementManager) sendAchievementUpdateToClient(characterID int32) {
|
||||
// TODO: Get client version from world server client connection
|
||||
// For now, use a common version (1096 is a common EQ2 client version)
|
||||
defaultClientVersion := int32(1096)
|
||||
|
||||
err := am.SendAchievementUpdateToPlayer(characterID, defaultClientVersion)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to send achievement update to character %d: %v\n", characterID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAndSendInitialAchievements loads and sends all achievements to a newly connected player
|
||||
func (am *AchievementManager) LoadAndSendInitialAchievements(characterID int32, clientVersion int32) error {
|
||||
// Ensure player manager is loaded
|
||||
playerMgr := am.GetPlayerManager(characterID)
|
||||
if playerMgr == nil {
|
||||
return fmt.Errorf("failed to create player manager for character %d", characterID)
|
||||
}
|
||||
|
||||
// Send master achievement list first
|
||||
err := am.SendCharacterAchievementsToPlayer(characterID, clientVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send character achievements: %w", err)
|
||||
}
|
||||
|
||||
// Then send current progress
|
||||
err = am.SendAchievementUpdateToPlayer(characterID, clientVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send achievement updates: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Sent initial achievement data to character %d\n", characterID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleAchievementTriggerEvent processes achievement triggers from game events
|
||||
func (am *AchievementManager) HandleAchievementTriggerEvent(characterID int32, triggerType string, value uint32) {
|
||||
err := am.ProcessAchievementTrigger(characterID, triggerType, value)
|
||||
if err != nil {
|
||||
fmt.Printf("Error processing achievement trigger for character %d: %v\n", characterID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
@ -1,417 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/commands"
|
||||
"eq2emu/internal/entity"
|
||||
"eq2emu/internal/packets"
|
||||
"eq2emu/internal/spawn"
|
||||
)
|
||||
|
||||
// Client represents a connected player client
|
||||
type Client struct {
|
||||
// Account information
|
||||
AccountID int32
|
||||
AccountName string
|
||||
AdminLevel int
|
||||
|
||||
// Character information
|
||||
CharacterID int32
|
||||
CharacterName string
|
||||
Player *entity.Entity
|
||||
|
||||
// Connection information
|
||||
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
|
||||
|
||||
// State flags
|
||||
IsConnected bool
|
||||
IsLinkdead bool
|
||||
IsAFK bool
|
||||
IsAnonymous bool
|
||||
IsLFG bool
|
||||
|
||||
// Chat state
|
||||
LastTellFrom string
|
||||
IgnoreList map[string]bool
|
||||
|
||||
// Group/Guild
|
||||
GroupID int32
|
||||
GuildID int32
|
||||
|
||||
// Pending operations
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewClientList creates a new client list
|
||||
func NewClientList() *ClientList {
|
||||
return &ClientList{
|
||||
clients: make(map[int32]*Client),
|
||||
clientsByName: make(map[string]*Client),
|
||||
clientsByAcct: make(map[int32][]*Client),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a client to the list
|
||||
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
|
||||
}
|
||||
|
||||
// Remove removes a client from the list
|
||||
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)
|
||||
for _, c := range clients {
|
||||
if c.CharacterID != characterID {
|
||||
newClients = append(newClients, c)
|
||||
}
|
||||
}
|
||||
if len(newClients) > 0 {
|
||||
cl.clientsByAcct[client.AccountID] = newClients
|
||||
} else {
|
||||
delete(cl.clientsByAcct, client.AccountID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetByCharacterID returns a client by character ID
|
||||
func (cl *ClientList) GetByCharacterID(characterID int32) *Client {
|
||||
cl.mutex.RLock()
|
||||
defer cl.mutex.RUnlock()
|
||||
|
||||
return cl.clients[characterID]
|
||||
}
|
||||
|
||||
// GetByCharacterName returns a client by character name
|
||||
func (cl *ClientList) GetByCharacterName(name string) *Client {
|
||||
cl.mutex.RLock()
|
||||
defer cl.mutex.RUnlock()
|
||||
|
||||
return cl.clientsByName[strings.ToLower(name)]
|
||||
}
|
||||
|
||||
// GetByAccountID returns all clients for an account
|
||||
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)
|
||||
return result
|
||||
}
|
||||
|
||||
// Count returns the total number of connected clients
|
||||
func (cl *ClientList) Count() int32 {
|
||||
cl.mutex.RLock()
|
||||
defer cl.mutex.RUnlock()
|
||||
|
||||
return int32(len(cl.clients))
|
||||
}
|
||||
|
||||
// GetAll returns all connected clients
|
||||
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)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ProcessAll processes all clients
|
||||
func (cl *ClientList) ProcessAll() {
|
||||
clients := cl.GetAll()
|
||||
|
||||
now := time.Now()
|
||||
for _, client := range clients {
|
||||
client.Process(now)
|
||||
}
|
||||
}
|
||||
|
||||
// DisconnectAll disconnects all clients
|
||||
func (cl *ClientList) DisconnectAll(reason string) {
|
||||
clients := cl.GetAll()
|
||||
|
||||
for _, client := range clients {
|
||||
client.DisconnectWithReason(reason)
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastMessage sends a message to all clients
|
||||
func (cl *ClientList) BroadcastMessage(message string) {
|
||||
clients := cl.GetAll()
|
||||
|
||||
for _, client := range clients {
|
||||
client.SendSimpleMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Process handles client processing
|
||||
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
|
||||
c.PendingZone = nil
|
||||
}
|
||||
}
|
||||
|
||||
// DisconnectWithReason disconnects the client with a reason
|
||||
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
|
||||
}
|
||||
|
||||
// SendSimpleMessage sends a simple message to the client
|
||||
func (c *Client) SendSimpleMessage(message string) {
|
||||
// TODO: Implement when UDP connection is available
|
||||
fmt.Printf("[%s] %s\n", c.CharacterName, message)
|
||||
}
|
||||
|
||||
// Zone changes the client's zone
|
||||
func (c *Client) Zone(details *ZoneChangeDetails) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.PendingZone = details
|
||||
}
|
||||
|
||||
// UpdateActivity updates the client's last activity time
|
||||
func (c *Client) UpdateActivity() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.LastActivity = time.Now()
|
||||
if c.IsLinkdead {
|
||||
c.IsLinkdead = false
|
||||
fmt.Printf("Client %s is no longer linkdead\n", c.CharacterName)
|
||||
}
|
||||
}
|
||||
|
||||
// Command interface implementations for Client
|
||||
|
||||
// GetPlayer implements commands.ClientInterface
|
||||
func (c *Client) GetPlayer() *entity.Entity {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return c.Player
|
||||
}
|
||||
|
||||
// GetAccountID implements commands.ClientInterface
|
||||
func (c *Client) GetAccountID() int32 {
|
||||
return c.AccountID
|
||||
}
|
||||
|
||||
// GetCharacterID implements commands.ClientInterface
|
||||
func (c *Client) GetCharacterID() int32 {
|
||||
return c.CharacterID
|
||||
}
|
||||
|
||||
// GetAdminLevel implements commands.ClientInterface
|
||||
func (c *Client) GetAdminLevel() int {
|
||||
return c.AdminLevel
|
||||
}
|
||||
|
||||
// GetName implements commands.ClientInterface
|
||||
func (c *Client) GetName() string {
|
||||
return c.CharacterName
|
||||
}
|
||||
|
||||
// IsInZone implements commands.ClientInterface
|
||||
func (c *Client) IsInZone() bool {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return c.CurrentZone != nil
|
||||
}
|
||||
|
||||
// GetZone implements commands.ClientInterface
|
||||
func (c *Client) GetZone() commands.ZoneInterface {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
if c.CurrentZone != nil {
|
||||
return &ZoneAdapter{zone: c.CurrentZone}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMessage implements commands.ClientInterface (channel version)
|
||||
func (c *Client) SendMessage(channel int, color int, message string) {
|
||||
// TODO: Implement channel-based messaging when packets are available
|
||||
fmt.Printf("[%s][Ch:%d] %s\n", c.CharacterName, channel, message)
|
||||
}
|
||||
|
||||
// SendPopupMessage implements commands.ClientInterface
|
||||
func (c *Client) SendPopupMessage(message string) {
|
||||
// TODO: Implement popup messaging when packets are available
|
||||
c.SendMessage(0, 0, fmt.Sprintf("[POPUP] %s", message))
|
||||
}
|
||||
|
||||
// Disconnect implements commands.ClientInterface
|
||||
func (c *Client) Disconnect() {
|
||||
c.DisconnectWithReason("Disconnected by command")
|
||||
}
|
||||
|
||||
// ZoneAdapter adapts ZoneServer to commands.ZoneInterface
|
||||
type ZoneAdapter struct {
|
||||
zone *ZoneServer
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) GetID() int32 {
|
||||
return za.zone.ID
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) GetName() string {
|
||||
return za.zone.Name
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) GetDescription() string {
|
||||
return za.zone.Description
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) GetPlayers() []*entity.Entity {
|
||||
// TODO: Implement when entity package is fully integrated
|
||||
return nil
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) Shutdown() {
|
||||
za.zone.Shutdown()
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) SendZoneMessage(channel int, color int, message string) {
|
||||
// TODO: Implement zone-wide messaging
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) GetSpawnByName(name string) *spawn.Spawn {
|
||||
// TODO: Implement spawn lookup
|
||||
return nil
|
||||
}
|
||||
|
||||
func (za *ZoneAdapter) GetSpawnByID(id int32) *spawn.Spawn {
|
||||
// TODO: Implement spawn lookup
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClientVersion returns the client version
|
||||
func (c *Client) GetClientVersion() int32 {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return c.ClientVersion
|
||||
}
|
||||
|
||||
// SetClientVersion sets the client version
|
||||
func (c *Client) SetClientVersion(version int32) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.ClientVersion = version
|
||||
}
|
||||
|
||||
// ProcessPacket processes an incoming packet for this client
|
||||
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)
|
||||
}
|
@ -1,679 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
"eq2emu/internal/items"
|
||||
)
|
||||
|
||||
// ItemManager manages items for the world server
|
||||
type ItemManager struct {
|
||||
masterItemList *items.MasterItemList
|
||||
itemSystemAdapter *items.ItemSystemAdapter
|
||||
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
|
||||
}
|
||||
|
||||
// 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,
|
||||
nil, // SpellManager - will be set later when spells are integrated
|
||||
playerAdapter,
|
||||
packetAdapter,
|
||||
ruleAdapter,
|
||||
dbAdapter,
|
||||
questAdapter,
|
||||
brokerAdapter,
|
||||
craftingAdapter,
|
||||
housingAdapter,
|
||||
lootAdapter,
|
||||
)
|
||||
|
||||
return &ItemManager{
|
||||
masterItemList: masterList,
|
||||
itemSystemAdapter: systemAdapter,
|
||||
database: db,
|
||||
playerInventories: make(map[uint32]*items.PlayerItemList),
|
||||
playerEquipment: make(map[uint32]*items.EquipmentItemList),
|
||||
worldDrops: make(map[int32][]*items.Item),
|
||||
}
|
||||
}
|
||||
|
||||
// SetWorld sets the world server reference
|
||||
func (im *ItemManager) SetWorld(world *World) {
|
||||
im.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
|
||||
}
|
||||
|
||||
// GetPlayerInventory gets a player's inventory
|
||||
func (im *ItemManager) GetPlayerInventory(playerID uint32) (*items.PlayerItemList, error) {
|
||||
im.mutex.RLock()
|
||||
defer im.mutex.RUnlock()
|
||||
|
||||
return im.itemSystemAdapter.GetPlayerInventory(playerID)
|
||||
}
|
||||
|
||||
// GetPlayerEquipment gets a player's equipment
|
||||
func (im *ItemManager) GetPlayerEquipment(playerID uint32, appearanceType int8) (*items.EquipmentItemList, error) {
|
||||
im.mutex.RLock()
|
||||
defer im.mutex.RUnlock()
|
||||
|
||||
return im.itemSystemAdapter.GetPlayerEquipment(playerID, appearanceType)
|
||||
}
|
||||
|
||||
// GiveItemToPlayer gives an item to a player
|
||||
func (im *ItemManager) GiveItemToPlayer(playerID uint32, itemID int32, quantity int16) error {
|
||||
return im.itemSystemAdapter.GiveItemToPlayer(playerID, itemID, quantity, items.NotSet)
|
||||
}
|
||||
|
||||
// RemoveItemFromPlayer removes an item from a player
|
||||
func (im *ItemManager) RemoveItemFromPlayer(playerID uint32, uniqueID int32, quantity int16) error {
|
||||
return im.itemSystemAdapter.RemoveItemFromPlayer(playerID, uniqueID, quantity)
|
||||
}
|
||||
|
||||
// EquipItem equips an item for a player
|
||||
func (im *ItemManager) EquipItem(playerID uint32, uniqueID int32, slot int8) error {
|
||||
return im.itemSystemAdapter.EquipItem(playerID, uniqueID, slot, 0) // Base equipment
|
||||
}
|
||||
|
||||
// UnequipItem unequips an item for a player
|
||||
func (im *ItemManager) UnequipItem(playerID uint32, slot int8) error {
|
||||
return im.itemSystemAdapter.UnequipItem(playerID, slot, 0) // Base equipment
|
||||
}
|
||||
|
||||
// MoveItem moves an item within a player's inventory
|
||||
func (im *ItemManager) MoveItem(playerID uint32, fromBagID int32, fromSlot int16, toBagID int32, toSlot int16) error {
|
||||
return im.itemSystemAdapter.MoveItem(playerID, fromBagID, fromSlot, toBagID, toSlot, 0) // Base equipment
|
||||
}
|
||||
|
||||
// CreateWorldDrop creates an item drop in the world
|
||||
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
|
||||
}
|
||||
|
||||
// GetWorldDrops gets all ground items in a zone
|
||||
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
|
||||
}
|
||||
|
||||
// PickupWorldDrop handles picking up a ground 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
|
||||
itemIndex = i
|
||||
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
|
||||
}
|
||||
|
||||
// GenerateLootForNPC generates loot for an NPC kill
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetItemTemplate gets an item template by ID
|
||||
func (im *ItemManager) GetItemTemplate(itemID int32) *items.Item {
|
||||
return im.masterItemList.GetItem(itemID)
|
||||
}
|
||||
|
||||
// SearchItems searches for item templates by name
|
||||
func (im *ItemManager) SearchItems(name string, maxResults int32) []*items.Item {
|
||||
// TODO: Implement proper search - for now return empty slice
|
||||
return []*items.Item{}
|
||||
}
|
||||
|
||||
// GetStatistics returns item system statistics
|
||||
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]any)
|
||||
for k, v := range systemStats {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
result["world_drops"] = totalWorldDrops
|
||||
result["zones_with_drops"] = len(im.worldDrops)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ValidatePlayerItems validates a player's items
|
||||
func (im *ItemManager) ValidatePlayerItems(playerID uint32) *items.ItemValidationResult {
|
||||
return im.itemSystemAdapter.ValidatePlayerItems(playerID)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// WorldItemDatabaseAdapter adapts the world database for item use
|
||||
type WorldItemDatabaseAdapter struct {
|
||||
db *database.Database
|
||||
}
|
||||
|
||||
// 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,
|
||||
adventure_classes, tradeskill_classes, stack_count, weight, generic_info,
|
||||
bag_info, food_info, weapon_info, ranged_info, details, appearance
|
||||
FROM items
|
||||
ORDER BY id
|
||||
`)
|
||||
if err != nil {
|
||||
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,
|
||||
&details, &appearance)
|
||||
if err != nil {
|
||||
fmt.Printf("Error scanning item row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set item properties
|
||||
item.Details.ItemID = id
|
||||
item.Name = name
|
||||
item.Details.Icon = int16(icon)
|
||||
item.Description = description
|
||||
item.Details.Tier = int8(tier)
|
||||
item.Details.RecommendedLevel = int16(level)
|
||||
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
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteItem implements items.DatabaseService interface
|
||||
func (wdb *WorldItemDatabaseAdapter) DeleteItem(itemID int32) error {
|
||||
// TODO: Implement item template deletion
|
||||
fmt.Printf("Deleting item template: %d - not yet implemented\n", itemID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadPlayerItems implements items.DatabaseService interface
|
||||
func (wdb *WorldItemDatabaseAdapter) LoadPlayerItems(playerID uint32) (*items.PlayerItemList, error) {
|
||||
// TODO: Implement player inventory loading from database
|
||||
fmt.Printf("Loading player inventory for player %d - creating empty inventory\n", playerID)
|
||||
return items.NewPlayerItemList(), nil
|
||||
}
|
||||
|
||||
// SavePlayerItems implements items.DatabaseService interface
|
||||
func (wdb *WorldItemDatabaseAdapter) SavePlayerItems(playerID uint32, itemList *items.PlayerItemList) error {
|
||||
// TODO: Implement player inventory saving to database
|
||||
fmt.Printf("Saving player inventory for player %d - not yet implemented\n", playerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadPlayerEquipment implements items.DatabaseService interface
|
||||
func (wdb *WorldItemDatabaseAdapter) LoadPlayerEquipment(playerID uint32, appearanceType int8) (*items.EquipmentItemList, error) {
|
||||
// TODO: Implement player equipment loading from database
|
||||
fmt.Printf("Loading player equipment for player %d (type %d) - creating empty equipment\n", playerID, appearanceType)
|
||||
equipment := items.NewEquipmentItemList()
|
||||
equipment.SetAppearanceType(appearanceType)
|
||||
return equipment, nil
|
||||
}
|
||||
|
||||
// SavePlayerEquipment implements items.DatabaseService interface
|
||||
func (wdb *WorldItemDatabaseAdapter) SavePlayerEquipment(playerID uint32, equipment *items.EquipmentItemList) error {
|
||||
// TODO: Implement player equipment saving to database
|
||||
fmt.Printf("Saving player equipment for player %d - not yet implemented\n", playerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadItemStats implements items.DatabaseService interface
|
||||
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,
|
||||
"intelligence": 7,
|
||||
}
|
||||
|
||||
statsIDs := map[int32]string{
|
||||
1: "health",
|
||||
2: "power",
|
||||
3: "strength",
|
||||
4: "stamina",
|
||||
5: "agility",
|
||||
6: "wisdom",
|
||||
7: "intelligence",
|
||||
}
|
||||
|
||||
return statsStrings, statsIDs, nil
|
||||
}
|
||||
|
||||
// SaveItemStat implements items.DatabaseService interface
|
||||
func (wdb *WorldItemDatabaseAdapter) SaveItemStat(statID int32, statName string) error {
|
||||
// TODO: Implement item stat mapping saving
|
||||
fmt.Printf("Saving item stat mapping: %d = %s - not yet implemented\n", statID, statName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorldItemPlayerAdapter adapts world player functionality for items
|
||||
type WorldItemPlayerAdapter struct{}
|
||||
|
||||
// GetPlayer implements items.PlayerManager interface
|
||||
func (wpa *WorldItemPlayerAdapter) GetPlayer(playerID uint32) (items.Player, error) {
|
||||
// TODO: Get actual player from world server when player system is integrated
|
||||
return &MockPlayer{id: playerID, name: fmt.Sprintf("Player_%d", playerID), level: 50}, nil
|
||||
}
|
||||
|
||||
// GetPlayerLevel implements items.PlayerManager interface
|
||||
func (wpa *WorldItemPlayerAdapter) GetPlayerLevel(playerID uint32) (int16, error) {
|
||||
// TODO: Get actual player level from world server
|
||||
return 50, nil // Placeholder
|
||||
}
|
||||
|
||||
// GetPlayerClass implements items.PlayerManager interface
|
||||
func (wpa *WorldItemPlayerAdapter) GetPlayerClass(playerID uint32) (int8, error) {
|
||||
// TODO: Get actual player class from world server
|
||||
return 1, nil // Placeholder
|
||||
}
|
||||
|
||||
// GetPlayerRace implements items.PlayerManager interface
|
||||
func (wpa *WorldItemPlayerAdapter) GetPlayerRace(playerID uint32) (int8, error) {
|
||||
// TODO: Get actual player race from world server
|
||||
return 1, nil // Placeholder
|
||||
}
|
||||
|
||||
// SendMessageToPlayer implements items.PlayerManager interface
|
||||
func (wpa *WorldItemPlayerAdapter) SendMessageToPlayer(playerID uint32, channel int8, message string) error {
|
||||
// TODO: Send actual message via world server
|
||||
fmt.Printf("[CHAT] Player %d: %s\n", playerID, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlayerName implements items.PlayerManager interface
|
||||
func (wpa *WorldItemPlayerAdapter) GetPlayerName(playerID uint32) (string, error) {
|
||||
// TODO: Get actual player name from world server
|
||||
return fmt.Sprintf("Player_%d", playerID), nil
|
||||
}
|
||||
|
||||
// WorldItemPacketAdapter adapts world packet functionality for items
|
||||
type WorldItemPacketAdapter struct{}
|
||||
|
||||
// SendPacketToPlayer implements items.PacketManager interface
|
||||
func (wpa *WorldItemPacketAdapter) SendPacketToPlayer(playerID uint32, packetData []byte) error {
|
||||
// TODO: Send actual packet via world server
|
||||
fmt.Printf("Sending item packet to player %d (%d bytes)\n", playerID, len(packetData))
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueuePacketForPlayer implements items.PacketManager interface
|
||||
func (wpa *WorldItemPacketAdapter) QueuePacketForPlayer(playerID uint32, packetData []byte) error {
|
||||
// TODO: Queue packet via world server
|
||||
fmt.Printf("Queuing item packet for player %d (%d bytes)\n", playerID, len(packetData))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClientVersion implements items.PacketManager interface
|
||||
func (wpa *WorldItemPacketAdapter) GetClientVersion(playerID uint32) (int16, error) {
|
||||
// TODO: Get actual client version from world server
|
||||
return 1096, nil // Default EQ2 client version
|
||||
}
|
||||
|
||||
// SerializeItem implements items.PacketManager interface
|
||||
func (wpa *WorldItemPacketAdapter) SerializeItem(item *items.Item, clientVersion int16, player items.Player) ([]byte, error) {
|
||||
// TODO: Implement actual item serialization for network transmission
|
||||
fmt.Printf("Serializing item %s for client version %d\n", item.Name, clientVersion)
|
||||
return []byte{0x01, 0x02, 0x03}, nil // Placeholder data
|
||||
}
|
||||
|
||||
// WorldItemRuleAdapter adapts world rules functionality for items
|
||||
type WorldItemRuleAdapter struct{}
|
||||
|
||||
// GetBool implements items.RuleManager interface
|
||||
func (wra *WorldItemRuleAdapter) GetBool(category, rule string) bool {
|
||||
// TODO: Get actual rule values from world server rules system
|
||||
fmt.Printf("Getting rule %s:%s (bool) - using default\n", category, rule)
|
||||
return true // Default value
|
||||
}
|
||||
|
||||
// GetInt32 implements items.RuleManager interface
|
||||
func (wra *WorldItemRuleAdapter) GetInt32(category, rule string) int32 {
|
||||
// TODO: Get actual rule values from world server rules system
|
||||
fmt.Printf("Getting rule %s:%s (int32) - using default\n", category, rule)
|
||||
return 100 // Default value
|
||||
}
|
||||
|
||||
// GetFloat implements items.RuleManager interface
|
||||
func (wra *WorldItemRuleAdapter) GetFloat(category, rule string) float32 {
|
||||
// TODO: Get actual rule values from world server rules system
|
||||
fmt.Printf("Getting rule %s:%s (float) - using default\n", category, rule)
|
||||
return 1.0 // Default value
|
||||
}
|
||||
|
||||
// GetString implements items.RuleManager interface
|
||||
func (wra *WorldItemRuleAdapter) GetString(category, rule string) string {
|
||||
// TODO: Get actual rule values from world server rules system
|
||||
fmt.Printf("Getting rule %s:%s (string) - using default\n", category, rule)
|
||||
return "default" // Default value
|
||||
}
|
||||
|
||||
// WorldItemLootAdapter adapts world loot functionality for items
|
||||
type WorldItemLootAdapter struct{}
|
||||
|
||||
// GenerateLoot implements items.LootManager interface
|
||||
func (wla *WorldItemLootAdapter) GenerateLoot(lootTableID int32, playerLevel int16) ([]*items.Item, error) {
|
||||
// TODO: Implement actual loot generation from loot tables
|
||||
fmt.Printf("Generating loot from table %d for level %d player\n", lootTableID, playerLevel)
|
||||
return []*items.Item{}, nil
|
||||
}
|
||||
|
||||
// DistributeLoot implements items.LootManager interface
|
||||
func (wla *WorldItemLootAdapter) DistributeLoot(lootItems []*items.Item, playerIDs []uint32, lootMethod int8) error {
|
||||
// TODO: Implement actual loot distribution
|
||||
fmt.Printf("Distributing %d items to %d players using method %d\n", len(lootItems), len(playerIDs), lootMethod)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanLootItem implements items.LootManager interface
|
||||
func (wla *WorldItemLootAdapter) CanLootItem(playerID uint32, item *items.Item) bool {
|
||||
// TODO: Implement actual loot permission checking
|
||||
return true // Allow all looting for now
|
||||
}
|
||||
|
||||
// Mock implementations for optional dependencies
|
||||
|
||||
// MockPlayer implements items.Player interface for testing
|
||||
type MockPlayer struct {
|
||||
id uint32
|
||||
name string
|
||||
level int16
|
||||
adventureClass int8
|
||||
tradeskillClass int8
|
||||
race int8
|
||||
gender int8
|
||||
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 }
|
||||
|
||||
// MockItemQuestAdapter provides mock quest functionality
|
||||
type MockItemQuestAdapter struct{}
|
||||
|
||||
func (mqa *MockItemQuestAdapter) CheckQuestPrerequisites(playerID uint32, questID int32) bool {
|
||||
return true // Allow all for now
|
||||
}
|
||||
|
||||
func (mqa *MockItemQuestAdapter) GetQuestRewards(questID int32) ([]*items.QuestRewardData, error) {
|
||||
return []*items.QuestRewardData{}, nil
|
||||
}
|
||||
|
||||
func (mqa *MockItemQuestAdapter) IsQuestItem(itemID int32) bool {
|
||||
return false // No quest items for now
|
||||
}
|
||||
|
||||
// MockItemBrokerAdapter provides mock broker functionality
|
||||
type MockItemBrokerAdapter struct{}
|
||||
|
||||
func (mba *MockItemBrokerAdapter) SearchItems(criteria *items.ItemSearchCriteria) ([]*items.Item, error) {
|
||||
return []*items.Item{}, nil
|
||||
}
|
||||
|
||||
func (mba *MockItemBrokerAdapter) ListItem(playerID uint32, item *items.Item, price int64) error {
|
||||
return fmt.Errorf("broker not implemented")
|
||||
}
|
||||
|
||||
func (mba *MockItemBrokerAdapter) BuyItem(playerID uint32, itemID int32, sellerID uint32) error {
|
||||
return fmt.Errorf("broker not implemented")
|
||||
}
|
||||
|
||||
func (mba *MockItemBrokerAdapter) GetItemPrice(itemID int32) (int64, error) {
|
||||
return 0, fmt.Errorf("broker not implemented")
|
||||
}
|
||||
|
||||
// MockItemCraftingAdapter provides mock crafting functionality
|
||||
type MockItemCraftingAdapter struct{}
|
||||
|
||||
func (mca *MockItemCraftingAdapter) CanCraftItem(playerID uint32, itemID int32) bool {
|
||||
return false // No crafting for now
|
||||
}
|
||||
|
||||
func (mca *MockItemCraftingAdapter) GetCraftingRequirements(itemID int32) ([]items.CraftingRequirement, error) {
|
||||
return []items.CraftingRequirement{}, nil
|
||||
}
|
||||
|
||||
func (mca *MockItemCraftingAdapter) CraftItem(playerID uint32, itemID int32, quality int8) (*items.Item, error) {
|
||||
return nil, fmt.Errorf("crafting not implemented")
|
||||
}
|
||||
|
||||
// MockItemHousingAdapter provides mock housing functionality
|
||||
type MockItemHousingAdapter struct{}
|
||||
|
||||
func (mha *MockItemHousingAdapter) CanPlaceItem(playerID uint32, houseID int32, item *items.Item) bool {
|
||||
return false // No housing for now
|
||||
}
|
||||
|
||||
func (mha *MockItemHousingAdapter) PlaceItem(playerID uint32, houseID int32, item *items.Item, location items.HouseLocation) error {
|
||||
return fmt.Errorf("housing not implemented")
|
||||
}
|
||||
|
||||
func (mha *MockItemHousingAdapter) RemoveItem(playerID uint32, houseID int32, itemID int32) error {
|
||||
return fmt.Errorf("housing not implemented")
|
||||
}
|
||||
|
||||
func (mha *MockItemHousingAdapter) GetHouseItems(houseID int32) ([]*items.Item, error) {
|
||||
return []*items.Item{}, nil
|
||||
}
|
@ -1,823 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
"eq2emu/internal/npc"
|
||||
"eq2emu/internal/npc/ai"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// World-specific NPC tracking
|
||||
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
|
||||
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,
|
||||
database: db,
|
||||
npcsByZone: make(map[int32][]*npc.NPC),
|
||||
activeCombat: make(map[int32]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// SetWorld sets the world server reference
|
||||
func (nm *NPCManager) SetWorld(world *World) {
|
||||
nm.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()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to load NPCs from database: %v\n", err)
|
||||
// Continue with test NPCs
|
||||
} else {
|
||||
// Add loaded NPCs to zone tracking and AI management
|
||||
for _, npc := range dbNPCs {
|
||||
if npc != nil {
|
||||
// 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)
|
||||
// if err != nil {
|
||||
// fmt.Printf("Warning: Failed to create AI brain for NPC %s: %v\n", npc.GetName(), err)
|
||||
// }
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
name string
|
||||
level int8
|
||||
zoneID int32
|
||||
}{
|
||||
{1001, "Test Goblin Scout", 5, 1},
|
||||
{1002, "Test Orc Warrior", 10, 1},
|
||||
{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)
|
||||
z := float32(50)
|
||||
newNPC.SetX(x)
|
||||
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",
|
||||
testData.name, testData.id, testData.level, testData.zoneID)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully created %d test NPCs\n", len(testNPCs))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessNPCs processes all NPCs for one tick
|
||||
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 {
|
||||
// Process combat NPCs
|
||||
nm.processCombatNPCs()
|
||||
}
|
||||
}
|
||||
|
||||
// processCombatNPCs handles NPCs currently in combat
|
||||
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 {
|
||||
for _, npc := range npcs {
|
||||
if npc != nil && npc.GetID() == npcID {
|
||||
combatNPC = npc
|
||||
break
|
||||
}
|
||||
}
|
||||
if combatNPC != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// processCombatNPC processes a single NPC in combat
|
||||
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
|
||||
}
|
||||
|
||||
// TODO: Combat helper methods commented out due to interface incompatibilities
|
||||
// These will be re-implemented once proper target system and interfaces are resolved
|
||||
|
||||
// processCombatSpell handles NPC spell casting in combat
|
||||
// func (nm *NPCManager) processCombatSpell(npc *npc.NPC, target entity.Entity, spell Spell, distance float32) {
|
||||
// // Implementation deferred until interface issues are resolved
|
||||
// }
|
||||
|
||||
// processCombatMelee handles NPC melee combat
|
||||
// func (nm *NPCManager) processCombatMelee(npc *npc.NPC, target entity.Entity, distance float32) {
|
||||
// // Implementation deferred until interface issues are resolved
|
||||
// }
|
||||
|
||||
// processCombatMovement handles NPC movement in combat
|
||||
// func (nm *NPCManager) processCombatMovement(npc *npc.NPC, target entity.Entity, distance float32) {
|
||||
// // Implementation deferred until interface issues are resolved
|
||||
// }
|
||||
|
||||
// OnNPCKilled handles when an NPC is killed
|
||||
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
|
||||
npcInfo := nm.GetNPCInfo(npcID)
|
||||
if npcInfo != nil {
|
||||
// Trigger NPC kill event for achievements
|
||||
nm.world.OnNPCKill(killerCharacterID, npcID, int32(npcInfo.Level))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and distribute loot
|
||||
if nm.world != nil && nm.world.itemMgr != nil {
|
||||
npcInfo := nm.GetNPCInfo(npcID)
|
||||
if npcInfo != nil {
|
||||
// Generate loot for this NPC kill
|
||||
loot, err := nm.world.itemMgr.GenerateLootForNPC(npcID, int16(npcInfo.Level))
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to generate loot for NPC %d: %v\n", npcID, err)
|
||||
} else if len(loot) > 0 {
|
||||
// Create loot drops in the world (for now, just create basic drops)
|
||||
// TODO: Get NPC position for proper loot positioning
|
||||
x, y, z := float32(100), float32(100), float32(50)
|
||||
zoneID := npcInfo.ZoneID
|
||||
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)
|
||||
}
|
||||
|
||||
// OnNPCEnteredCombat handles when an NPC enters combat
|
||||
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)
|
||||
}
|
||||
|
||||
// OnNPCLeftCombat handles when an NPC leaves combat
|
||||
func (nm *NPCManager) OnNPCLeftCombat(npcID int32) {
|
||||
nm.mutex.Lock()
|
||||
delete(nm.activeCombat, npcID)
|
||||
nm.mutex.Unlock()
|
||||
|
||||
fmt.Printf("NPC %d left combat\n", npcID)
|
||||
}
|
||||
|
||||
// GetNPCInfo gets basic info about an NPC
|
||||
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 {
|
||||
if npc != nil && npc.GetID() == npcID {
|
||||
return &NPCInfo{
|
||||
ID: npc.GetID(),
|
||||
Name: npc.GetName(),
|
||||
Level: npc.GetLevel(),
|
||||
ZoneID: 0, // TODO: Get zone ID from NPC or tracking
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
return &NPCInfo{
|
||||
ID: npcID,
|
||||
Level: 1,
|
||||
Name: fmt.Sprintf("Unknown_NPC_%d", npcID),
|
||||
ZoneID: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return &NPCInfo{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Level: level,
|
||||
ZoneID: zoneID,
|
||||
}
|
||||
}
|
||||
|
||||
// GetNPCsByZone gets all NPCs in a zone
|
||||
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
|
||||
}
|
||||
|
||||
// AddNPCToZone adds an NPC to a zone's tracking
|
||||
func (nm *NPCManager) AddNPCToZone(zoneID int32, npc *npc.NPC) {
|
||||
nm.mutex.Lock()
|
||||
defer nm.mutex.Unlock()
|
||||
|
||||
nm.npcsByZone[zoneID] = append(nm.npcsByZone[zoneID], npc)
|
||||
}
|
||||
|
||||
// RemoveNPCFromZone removes an NPC from zone tracking
|
||||
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 {
|
||||
// Remove NPC from slice
|
||||
nm.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatistics returns NPC system statistics
|
||||
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]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,
|
||||
"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
|
||||
ZoneID int32
|
||||
}
|
||||
|
||||
// WorldNPCDatabaseAdapter adapts the world database for NPC use
|
||||
type WorldNPCDatabaseAdapter struct {
|
||||
db *database.Database
|
||||
}
|
||||
|
||||
// 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,
|
||||
loot_table_id, merchant_type, randomize_appearance, show_name,
|
||||
show_level, targetable, show_command_icon, display_hand_icon, faction_id
|
||||
FROM npcs
|
||||
ORDER BY zone_id, id
|
||||
`)
|
||||
if err != nil {
|
||||
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,
|
||||
&randomizeAppearance, &showName, &showLevel, &targetable,
|
||||
&showCommandIcon, &displayHandIcon, &factionID)
|
||||
if err != nil {
|
||||
fmt.Printf("Error scanning NPC row: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Set NPC properties
|
||||
newNPC.SetID(id)
|
||||
newNPC.SetName(name)
|
||||
newNPC.SetLevel(int16(level))
|
||||
newNPC.SetTotalHP(hp)
|
||||
newNPC.SetHP(hp)
|
||||
newNPC.SetX(x)
|
||||
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",
|
||||
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
|
||||
}
|
||||
|
||||
// SaveNPC implements npc.Database interface
|
||||
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()
|
||||
level := npcEntity.GetLevel()
|
||||
hp := npcEntity.GetTotalHP()
|
||||
x := npcEntity.GetX()
|
||||
y := npcEntity.GetY()
|
||||
z := npcEntity.GetZ()
|
||||
heading := npcEntity.GetHeading()
|
||||
|
||||
// Insert or update NPC in database
|
||||
_, err := wdb.db.Exec(`
|
||||
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
|
||||
(?, ?, ?, ?, 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
|
||||
}
|
||||
|
||||
// DeleteNPC implements npc.Database interface
|
||||
func (wdb *WorldNPCDatabaseAdapter) DeleteNPC(npcID int32) error {
|
||||
// Start transaction to delete NPC and related data
|
||||
tx, err := wdb.db.Begin()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// LoadNPCSpells implements npc.Database interface
|
||||
func (wdb *WorldNPCDatabaseAdapter) LoadNPCSpells(npcID int32) ([]*npc.NPCSpell, error) {
|
||||
rows, err := wdb.db.Query(`
|
||||
SELECT npc_id, spell_id, tier, hp_percentage, priority, cast_type, recast_delay
|
||||
FROM npc_spells
|
||||
WHERE npc_id = ?
|
||||
ORDER BY priority DESC, hp_percentage DESC
|
||||
`, npcID)
|
||||
if err != nil {
|
||||
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)
|
||||
// spell.SetTier(tier)
|
||||
// spell.SetHPPercentage(hpPercentage)
|
||||
// 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
|
||||
}
|
||||
|
||||
// SaveNPCSpells implements npc.Database interface
|
||||
func (wdb *WorldNPCDatabaseAdapter) SaveNPCSpells(npcID int32, spells []*npc.NPCSpell) error {
|
||||
// Start transaction
|
||||
tx, err := wdb.db.Begin()
|
||||
if err != nil {
|
||||
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
|
||||
(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
|
||||
}
|
||||
|
||||
// LoadNPCSkills implements npc.Database interface
|
||||
func (wdb *WorldNPCDatabaseAdapter) LoadNPCSkills(npcID int32) (map[string]*npc.Skill, error) {
|
||||
rows, err := wdb.db.Query(`
|
||||
SELECT npc_id, skill_name, skill_value, max_value
|
||||
FROM npc_skills
|
||||
WHERE npc_id = ?
|
||||
ORDER BY skill_name
|
||||
`, npcID)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// SaveNPCSkills implements npc.Database interface
|
||||
func (wdb *WorldNPCDatabaseAdapter) SaveNPCSkills(npcID int32, skills map[string]*npc.Skill) error {
|
||||
// Start transaction
|
||||
tx, err := wdb.db.Begin()
|
||||
if err != nil {
|
||||
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
|
||||
(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
|
||||
}
|
||||
|
||||
// WorldNPCLoggerAdapter adapts world logging for NPC use
|
||||
type WorldNPCLoggerAdapter struct{}
|
||||
|
||||
// LogInfo implements npc.Logger interface
|
||||
func (wl *WorldNPCLoggerAdapter) LogInfo(message string, args ...any) {
|
||||
fmt.Printf("[NPC] INFO: "+message+"\n", args...)
|
||||
}
|
||||
|
||||
// LogError implements npc.Logger interface
|
||||
func (wl *WorldNPCLoggerAdapter) LogError(message string, args ...any) {
|
||||
fmt.Printf("[NPC] ERROR: "+message+"\n", args...)
|
||||
}
|
||||
|
||||
// LogDebug implements npc.Logger interface
|
||||
func (wl *WorldNPCLoggerAdapter) LogDebug(message string, args ...any) {
|
||||
fmt.Printf("[NPC] DEBUG: "+message+"\n", args...)
|
||||
}
|
||||
|
||||
// LogWarning implements npc.Logger interface
|
||||
func (wl *WorldNPCLoggerAdapter) LogWarning(message string, args ...any) {
|
||||
fmt.Printf("[NPC] WARNING: "+message+"\n", args...)
|
||||
}
|
@ -1,978 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"eq2emu/internal/packets"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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)
|
||||
packets.RegisterGlobalHandler(packets.OP_ItemUnequipMsg, w.HandleItemUnequip)
|
||||
packets.RegisterGlobalHandler(packets.OP_ItemPickupMsg, w.HandleItemPickup)
|
||||
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
|
||||
}
|
||||
|
||||
// HandleClientCommand handles client command messages
|
||||
func (w *World) HandleClientCommand(ctx *packets.PacketContext, packet *packets.PacketData) error {
|
||||
// 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
|
||||
}
|
||||
|
||||
// HandlePositionUpdate handles player position updates
|
||||
func (w *World) HandlePositionUpdate(ctx *packets.PacketContext, packet *packets.PacketData) error {
|
||||
// Position updates are frequent, so only log occasionally
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// SendAchievementData sends achievement data to a client
|
||||
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",
|
||||
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",
|
||||
len(completedAchievements), len(inProgressAchievements), totalPoints))
|
||||
}
|
||||
|
||||
// SendCharacterAchievements sends complete character achievements to client
|
||||
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]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]any{
|
||||
"id": achievementID,
|
||||
"title": achievement.Title,
|
||||
"description": achievement.UncompletedText,
|
||||
"completed": completed,
|
||||
"progress": progress,
|
||||
"required": achievement.QtyRequired,
|
||||
"percentage": percentage,
|
||||
"points": achievement.PointValue,
|
||||
"category": achievement.Category,
|
||||
"expansion": achievement.Expansion,
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
// calculateAchievementPoints calculates total achievement points for a character
|
||||
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
|
||||
}
|
||||
|
||||
// SendTitleData sends title data to a client
|
||||
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",
|
||||
client.CharacterName, titleCount)
|
||||
|
||||
// Create title update packet (placeholder)
|
||||
client.SendSimpleMessage(fmt.Sprintf("Title Update: %d titles available", titleCount))
|
||||
}
|
||||
|
||||
// SendCharacterTitles sends complete character titles to client
|
||||
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",
|
||||
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",
|
||||
titleCount, totalTitles, formattedName))
|
||||
}
|
||||
|
||||
// NPC Packet Handlers
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// HandleNPCMovement handles NPC movement updates
|
||||
func (w *World) HandleNPCMovement(ctx *packets.PacketContext, packet *packets.PacketData) error {
|
||||
// NPC movement updates can be frequent, so only log occasionally
|
||||
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
|
||||
}
|
||||
|
||||
// SendNPCInfo sends NPC information to a client
|
||||
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",
|
||||
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",
|
||||
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]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 {
|
||||
if client.CurrentZone != nil {
|
||||
// TODO: Check if client is in range of NPC
|
||||
client.SendSimpleMessage(fmt.Sprintf("NPC Update: %s for NPC %d", updateType, npcID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
npcID, targetID, combatType, damage)
|
||||
|
||||
// Send to relevant clients (placeholder)
|
||||
clients := w.clients.GetAll()
|
||||
for _, client := range clients {
|
||||
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",
|
||||
npcID, combatType, targetID, damage))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WorldDatabaseAdapter adapts the World's database for packet handlers
|
||||
type WorldDatabaseAdapter struct {
|
||||
world *World
|
||||
}
|
||||
|
||||
// GetCharacter implements packets.DatabaseInterface
|
||||
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]any) error {
|
||||
// TODO: Implement character saving to database
|
||||
return fmt.Errorf("character saving not yet implemented")
|
||||
}
|
||||
|
||||
// WorldClientAdapter adapts World's Client to packets.ClientInterface
|
||||
type WorldClientAdapter struct {
|
||||
client *Client
|
||||
world *World
|
||||
}
|
||||
|
||||
// GetCharacterID implements packets.ClientInterface
|
||||
func (wca *WorldClientAdapter) GetCharacterID() int32 {
|
||||
return wca.client.CharacterID
|
||||
}
|
||||
|
||||
// GetAccountID implements packets.ClientInterface
|
||||
func (wca *WorldClientAdapter) GetAccountID() int32 {
|
||||
return wca.client.AccountID
|
||||
}
|
||||
|
||||
// GetCharacterName implements packets.ClientInterface
|
||||
func (wca *WorldClientAdapter) GetCharacterName() string {
|
||||
return wca.client.CharacterName
|
||||
}
|
||||
|
||||
// GetClientVersion implements packets.ClientInterface
|
||||
func (wca *WorldClientAdapter) GetClientVersion() int32 {
|
||||
return wca.client.GetClientVersion()
|
||||
}
|
||||
|
||||
// GetAdminLevel implements packets.ClientInterface
|
||||
func (wca *WorldClientAdapter) GetAdminLevel() int {
|
||||
return wca.client.AdminLevel
|
||||
}
|
||||
|
||||
// IsInZone implements packets.ClientInterface
|
||||
func (wca *WorldClientAdapter) IsInZone() bool {
|
||||
return wca.client.CurrentZone != nil
|
||||
}
|
||||
|
||||
// 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),
|
||||
wca.client.CharacterName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect implements packets.ClientInterface
|
||||
func (wca *WorldClientAdapter) Disconnect() error {
|
||||
wca.client.DisconnectWithReason("Disconnected by packet handler")
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorldServerAdapter adapts World to packets.WorldInterface
|
||||
type WorldServerAdapter struct {
|
||||
world *World
|
||||
}
|
||||
|
||||
// GetClientByID implements packets.WorldInterface
|
||||
func (wsa *WorldServerAdapter) GetClientByID(characterID int32) packets.ClientInterface {
|
||||
client := wsa.world.clients.GetByCharacterID(characterID)
|
||||
if client != nil {
|
||||
return &WorldClientAdapter{client: client, world: wsa.world}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllClients implements packets.WorldInterface
|
||||
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
|
||||
}
|
||||
|
||||
// BroadcastPacket implements packets.WorldInterface
|
||||
func (wsa *WorldServerAdapter) BroadcastPacket(opcode packets.InternalOpcode, data []byte) {
|
||||
// TODO: Implement packet broadcasting
|
||||
fmt.Printf("Broadcasting packet %s to all clients\n", packets.GetInternalOpcodeName(opcode))
|
||||
}
|
||||
|
||||
// BroadcastToZone implements packets.WorldInterface
|
||||
func (wsa *WorldServerAdapter) BroadcastToZone(zoneID int32, opcode packets.InternalOpcode, data []byte) {
|
||||
// TODO: Implement zone-specific broadcasting
|
||||
fmt.Printf("Broadcasting packet %s to zone %d\n", packets.GetInternalOpcodeName(opcode), zoneID)
|
||||
}
|
||||
|
||||
// CreatePacketContext creates a packet context for a client
|
||||
func (w *World) CreatePacketContext(client *Client) *packets.PacketContext {
|
||||
return &packets.PacketContext{
|
||||
Client: &WorldClientAdapter{client: client, world: w},
|
||||
World: &WorldServerAdapter{world: w},
|
||||
Database: &WorldDatabaseAdapter{world: w},
|
||||
}
|
||||
}
|
||||
|
||||
// Item Packet Handlers
|
||||
|
||||
// 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()),
|
||||
fromBagID, fromSlot, toBagID, toSlot)
|
||||
if err != nil {
|
||||
client.SendSimpleMessage(fmt.Sprintf("Item move failed: %v", err))
|
||||
} else {
|
||||
client.SendSimpleMessage("Item moved successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
client.SendSimpleMessage(fmt.Sprintf("Item equip failed: %v", err))
|
||||
} else {
|
||||
client.SendSimpleMessage(fmt.Sprintf("Item equipped to slot %d", slot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
client.SendSimpleMessage(fmt.Sprintf("Item unequip failed: %v", err))
|
||||
} else {
|
||||
client.SendSimpleMessage(fmt.Sprintf("Item unequipped from slot %d", slot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
client.SendSimpleMessage(fmt.Sprintf("Item pickup failed: %v", err))
|
||||
} else {
|
||||
client.SendSimpleMessage("Item picked up")
|
||||
// TODO: Remove item from world display for other players
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
client.SendSimpleMessage(fmt.Sprintf("Item drop failed: %v", err))
|
||||
} else {
|
||||
client.SendSimpleMessage("Item dropped")
|
||||
// TODO: Show item to other players in range
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
client.SendSimpleMessage("Item not found")
|
||||
} else {
|
||||
// Send item details to client
|
||||
w.SendItemDetails(client, itemTemplate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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]")
|
||||
}
|
||||
|
||||
// SendPlayerInventory sends complete inventory to a client
|
||||
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",
|
||||
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]any) {
|
||||
if w.itemMgr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Build and send item update packet
|
||||
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]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",
|
||||
sourcePlayerID, updateType, itemData)
|
||||
|
||||
// Send to players in range (placeholder)
|
||||
clients := w.clients.GetAll()
|
||||
for _, client := range clients {
|
||||
if client.CurrentZone != nil && client.CharacterID != int32(sourcePlayerID) {
|
||||
// TODO: Check if client is in range
|
||||
client.SendSimpleMessage(fmt.Sprintf("Player item update: %s", updateType))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// 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,
|
||||
database: db,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadTitles loads all titles from database
|
||||
func (tm *TitleManager) LoadTitles() error {
|
||||
fmt.Println("Loading master title list...")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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": {
|
||||
ID: 1,
|
||||
Name: "Killer",
|
||||
Category: titles.CategoryCombat,
|
||||
Rarity: titles.TitleRarityCommon,
|
||||
Position: titles.TitlePositionPrefix,
|
||||
Description: "Granted for first NPC kill",
|
||||
},
|
||||
"Veteran Hunter": {
|
||||
ID: 2,
|
||||
Name: "Veteran",
|
||||
Category: titles.CategoryCombat,
|
||||
Rarity: titles.TitleRarityUncommon,
|
||||
Position: titles.TitlePositionPrefix,
|
||||
Description: "Granted for killing high-level NPCs",
|
||||
},
|
||||
"Boss Slayer": {
|
||||
ID: 10,
|
||||
Name: "Boss Slayer",
|
||||
Category: titles.CategoryCombat,
|
||||
Rarity: titles.TitleRarityRare,
|
||||
Position: titles.TitlePositionSuffix,
|
||||
Description: "Granted for killing boss NPCs",
|
||||
},
|
||||
"Quest Master": {
|
||||
ID: 20,
|
||||
Name: "the Questor",
|
||||
Category: titles.CategoryQuest,
|
||||
Rarity: titles.TitleRarityCommon,
|
||||
Position: titles.TitlePositionSuffix,
|
||||
Description: "Granted for completing quests",
|
||||
},
|
||||
"Hero's Journey": {
|
||||
ID: 21,
|
||||
Name: "Hero",
|
||||
Category: titles.CategoryQuest,
|
||||
Rarity: titles.TitleRarityLegendary,
|
||||
Position: titles.TitlePositionPrefix,
|
||||
Description: "Granted for completing main story quest",
|
||||
},
|
||||
"Growing Strong": {
|
||||
ID: 30,
|
||||
Name: "the Promising",
|
||||
Category: "Progression",
|
||||
Rarity: titles.TitleRarityCommon,
|
||||
Position: titles.TitlePositionSuffix,
|
||||
Description: "Granted for reaching level 10",
|
||||
},
|
||||
"Seasoned Adventurer": {
|
||||
ID: 31,
|
||||
Name: "the Seasoned",
|
||||
Category: "Progression",
|
||||
Rarity: titles.TitleRarityUncommon,
|
||||
Position: titles.TitlePositionSuffix,
|
||||
Description: "Granted for reaching level 20",
|
||||
},
|
||||
"Veteran": {
|
||||
ID: 32,
|
||||
Name: "Veteran",
|
||||
Category: "Progression",
|
||||
Rarity: titles.TitleRarityRare,
|
||||
Position: titles.TitlePositionPrefix,
|
||||
Description: "Granted for reaching level 50",
|
||||
},
|
||||
"Master Adventurer": {
|
||||
ID: 33,
|
||||
Name: "Master",
|
||||
Category: "Progression",
|
||||
Rarity: titles.TitleRarityEpic,
|
||||
Position: titles.TitlePositionPrefix,
|
||||
Description: "Granted for reaching level 90",
|
||||
},
|
||||
"Level Up!": {
|
||||
ID: 34,
|
||||
Name: "the Accomplished",
|
||||
Category: "Progression",
|
||||
Rarity: titles.TitleRarityCommon,
|
||||
Position: titles.TitlePositionSuffix,
|
||||
Description: "Granted for leveling up",
|
||||
},
|
||||
}
|
||||
|
||||
// Add titles to master list
|
||||
for name, title := range achievementTitles {
|
||||
err := masterList.AddTitle(title)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to add title '%s': %v\n", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GrantTitle grants a title to a player
|
||||
func (tm *TitleManager) GrantTitle(playerID, titleID int32, sourceAchievementID, sourceQuestID uint32) error {
|
||||
return tm.titleManager.GrantTitle(playerID, titleID, sourceAchievementID, sourceQuestID)
|
||||
}
|
||||
|
||||
// GetPlayerTitles gets a player's title collection
|
||||
func (tm *TitleManager) GetPlayerTitles(playerID int32) *titles.PlayerTitlesList {
|
||||
return tm.titleManager.GetPlayerTitles(playerID)
|
||||
}
|
||||
|
||||
// GetPlayerFormattedName returns a player's name with active titles
|
||||
func (tm *TitleManager) GetPlayerFormattedName(playerID int32, playerName string) string {
|
||||
return tm.titleManager.GetPlayerFormattedName(playerID, playerName)
|
||||
}
|
||||
|
||||
// GetIntegrationManager returns the integration manager
|
||||
func (tm *TitleManager) GetIntegrationManager() *titles.IntegrationManager {
|
||||
return tm.integrationMgr
|
||||
}
|
||||
|
||||
// SetupAchievementIntegration sets up achievement-to-title integration
|
||||
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")
|
||||
}
|
||||
|
||||
// ProcessAchievementCompletion processes an achievement completion and grants associated titles
|
||||
func (tm *TitleManager) ProcessAchievementCompletion(playerID int32, achievementID uint32) error {
|
||||
// Use the title manager's built-in method
|
||||
err := tm.titleManager.ProcessAchievementCompletion(playerID, achievementID)
|
||||
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]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]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")
|
||||
}
|
@ -1,688 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/commands"
|
||||
"eq2emu/internal/database"
|
||||
"eq2emu/internal/packets"
|
||||
"eq2emu/internal/rules"
|
||||
)
|
||||
|
||||
// World represents the main world server instance
|
||||
type World struct {
|
||||
// Core components
|
||||
db *database.Database
|
||||
commandManager *commands.CommandManager
|
||||
rulesManager *rules.RuleManager
|
||||
|
||||
// Server configuration
|
||||
config *WorldConfig
|
||||
startTime time.Time
|
||||
shutdownTime *time.Time
|
||||
shutdownReason string
|
||||
|
||||
// World time management
|
||||
worldTime *WorldTime
|
||||
worldTimeTicker *time.Ticker
|
||||
|
||||
// Zones management
|
||||
zones *ZoneList
|
||||
|
||||
// Client management
|
||||
clients *ClientList
|
||||
|
||||
// Achievement system
|
||||
achievementMgr *AchievementManager
|
||||
|
||||
// Title system
|
||||
titleMgr *TitleManager
|
||||
|
||||
// NPC system
|
||||
npcMgr *NPCManager
|
||||
|
||||
// Item system
|
||||
itemMgr *ItemManager
|
||||
|
||||
// Master lists (singletons)
|
||||
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
|
||||
|
||||
// Synchronization
|
||||
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"`
|
||||
|
||||
// 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"`
|
||||
|
||||
// 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
|
||||
|
||||
// Server settings
|
||||
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"`
|
||||
|
||||
// Login server settings
|
||||
LoginServerAddr string `json:"login_server_addr"`
|
||||
LoginServerPort int `json:"login_server_port"`
|
||||
LoginServerKey string `json:"login_server_key"`
|
||||
}
|
||||
|
||||
// WorldTime represents in-game time
|
||||
type WorldTime struct {
|
||||
Year int32
|
||||
Month int32
|
||||
Day int32
|
||||
Hour int32
|
||||
Minute int32
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// ServerStatistics tracks server metrics
|
||||
type ServerStatistics struct {
|
||||
// Server info
|
||||
ServerCreated time.Time
|
||||
ServerStartTime time.Time
|
||||
|
||||
// Connection stats
|
||||
TotalConnections int64
|
||||
CurrentConnections int32
|
||||
MaxConnections int32
|
||||
|
||||
// Character stats
|
||||
TotalAccounts int32
|
||||
TotalCharacters int32
|
||||
AverageCharLevel float32
|
||||
|
||||
// Zone stats
|
||||
ActiveZones int32
|
||||
ActiveInstances int32
|
||||
|
||||
// Performance stats
|
||||
CPUUsage float32
|
||||
MemoryUsage int64
|
||||
PeakMemoryUsage int64
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewWorld creates a new world server instance
|
||||
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",
|
||||
config.DatabaseUser, config.DatabasePass, config.DatabaseHost, config.DatabasePort, config.DatabaseName)
|
||||
db, err = database.NewMySQL(dsn)
|
||||
case "sqlite", "":
|
||||
// Default to SQLite if not specified
|
||||
dbPath := config.DatabasePath
|
||||
if dbPath == "" {
|
||||
dbPath = "eq2.db"
|
||||
}
|
||||
return nil, fmt.Errorf("SQLite support has been removed, please use MySQL")
|
||||
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,
|
||||
rulesManager: rulesManager,
|
||||
achievementMgr: achievementMgr,
|
||||
titleMgr: titleMgr,
|
||||
npcMgr: npcMgr,
|
||||
itemMgr: itemMgr,
|
||||
config: config,
|
||||
startTime: time.Now(),
|
||||
worldTime: &WorldTime{Year: 3721, Month: 1, Day: 1, Hour: 12, Minute: 0},
|
||||
zones: NewZoneList(),
|
||||
clients: NewClientList(),
|
||||
stats: &ServerStatistics{
|
||||
ServerStartTime: time.Now(),
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
// Start begins the world server operation
|
||||
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
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the world server
|
||||
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
|
||||
}
|
||||
|
||||
// Process handles the main world server loop
|
||||
func (w *World) Process() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.processFrame()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processFrame handles one frame of world processing
|
||||
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()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
w.worldTime.Minute = 0
|
||||
w.worldTime.Hour++
|
||||
if w.worldTime.Hour >= 24 {
|
||||
w.worldTime.Hour = 0
|
||||
w.worldTime.Day++
|
||||
if w.worldTime.Day > 30 { // Simplified calendar
|
||||
w.worldTime.Day = 1
|
||||
w.worldTime.Month++
|
||||
if w.worldTime.Month > 12 {
|
||||
w.worldTime.Month = 1
|
||||
w.worldTime.Year++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.worldTime.mutex.Unlock()
|
||||
|
||||
// Send time update to all zones
|
||||
w.zones.SendTimeUpdate(w.worldTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Check zone health
|
||||
w.zones.CheckHealth()
|
||||
|
||||
// Clean up dead zones
|
||||
w.zones.CleanupDead()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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
|
||||
// - Master quest list
|
||||
// - Master skill list
|
||||
// - Master faction list
|
||||
// - Starting skills/spells
|
||||
// - Merchant data
|
||||
// - Transport data
|
||||
|
||||
fmt.Println("Server data loaded successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkShutdown checks if a scheduled shutdown should occur
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// updateVitality updates player vitality
|
||||
func (w *World) updateVitality() {
|
||||
// TODO: Implement vitality system
|
||||
}
|
||||
|
||||
// ScheduleShutdown schedules a server shutdown
|
||||
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)
|
||||
}
|
||||
|
||||
// CancelShutdown cancels a scheduled shutdown
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
// GetWorldTime returns the current in-game time
|
||||
func (w *World) GetWorldTime() WorldTime {
|
||||
w.worldTime.mutex.RLock()
|
||||
defer w.worldTime.mutex.RUnlock()
|
||||
|
||||
return WorldTime{
|
||||
Year: w.worldTime.Year,
|
||||
Month: w.worldTime.Month,
|
||||
Day: w.worldTime.Day,
|
||||
Hour: w.worldTime.Hour,
|
||||
Minute: w.worldTime.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig returns the world configuration
|
||||
func (w *World) GetConfig() *WorldConfig {
|
||||
return w.config
|
||||
}
|
||||
|
||||
// GetDatabase returns the database connection
|
||||
func (w *World) GetDatabase() *database.Database {
|
||||
return w.db
|
||||
}
|
||||
|
||||
// GetCommandManager returns the command manager
|
||||
func (w *World) GetCommandManager() *commands.CommandManager {
|
||||
return w.commandManager
|
||||
}
|
||||
|
||||
// GetRulesManager returns the rules manager
|
||||
func (w *World) GetRulesManager() *rules.RuleManager {
|
||||
return w.rulesManager
|
||||
}
|
||||
|
||||
// GetAchievementManager returns the achievement manager
|
||||
func (w *World) GetAchievementManager() *AchievementManager {
|
||||
return w.achievementMgr
|
||||
}
|
||||
|
||||
// GetTitleManager returns the title manager
|
||||
func (w *World) GetTitleManager() *TitleManager {
|
||||
return w.titleMgr
|
||||
}
|
||||
|
||||
// GetNPCManager returns the NPC manager
|
||||
func (w *World) GetNPCManager() *NPCManager {
|
||||
return w.npcMgr
|
||||
}
|
||||
|
||||
// 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{
|
||||
"OP_Unknown": 0x0000,
|
||||
"OP_LoginReplyMsg": 0x0001,
|
||||
"OP_LoginByNumRequestMsg": 0x0002,
|
||||
"OP_WSLoginRequestMsg": 0x0003,
|
||||
"OP_ESInitMsg": 0x0010,
|
||||
"OP_ESReadyForClientsMsg": 0x0011,
|
||||
"OP_CreateZoneInstanceMsg": 0x0012,
|
||||
"OP_ZoneInstanceCreateReplyMsg": 0x0013,
|
||||
"OP_ZoneInstanceDestroyedMsg": 0x0014,
|
||||
"OP_ExpectClientAsCharacterRequest": 0x0015,
|
||||
"OP_ExpectClientAsCharacterReplyMs": 0x0016,
|
||||
"OP_ZoneInfoMsg": 0x0017,
|
||||
"OP_CreateCharacterRequestMsg": 0x0020,
|
||||
"OP_DoneLoadingZoneResourcesMsg": 0x0021,
|
||||
"OP_DoneSendingInitialEntitiesMsg": 0x0022,
|
||||
"OP_DoneLoadingEntityResourcesMsg": 0x0023,
|
||||
"OP_DoneLoadingUIResourcesMsg": 0x0024,
|
||||
"OP_PredictionUpdateMsg": 0x0030,
|
||||
"OP_RemoteCmdMsg": 0x0031,
|
||||
"OP_SetRemoteCmdsMsg": 0x0032,
|
||||
"OP_GameWorldTimeMsg": 0x0033,
|
||||
"OP_MOTDMsg": 0x0034,
|
||||
"OP_ZoneMOTDMsg": 0x0035,
|
||||
"OP_ClientCmdMsg": 0x0040,
|
||||
"OP_DispatchClientCmdMsg": 0x0041,
|
||||
"OP_DispatchESMsg": 0x0042,
|
||||
"OP_UpdateCharacterSheetMsg": 0x0050,
|
||||
"OP_UpdateSpellBookMsg": 0x0051,
|
||||
"OP_UpdateInventoryMsg": 0x0052,
|
||||
"OP_ChangeZoneMsg": 0x0060,
|
||||
"OP_ClientTeleportRequestMsg": 0x0061,
|
||||
"OP_TeleportWithinZoneMsg": 0x0062,
|
||||
"OP_ReadyToZoneMsg": 0x0063,
|
||||
"OP_ChatTellChannelMsg": 0x0070,
|
||||
"OP_ChatTellUserMsg": 0x0071,
|
||||
"OP_UpdatePositionMsg": 0x0080,
|
||||
"OP_AchievementUpdateMsg": 0x0090,
|
||||
"OP_CharacterAchievements": 0x0091,
|
||||
"OP_TitleUpdateMsg": 0x0092,
|
||||
"OP_CharacterTitles": 0x0093,
|
||||
"OP_SetActiveTitleMsg": 0x0094,
|
||||
"OP_NPCAttackMsg": 0x0095,
|
||||
"OP_NPCTargetMsg": 0x0096,
|
||||
"OP_NPCInfoMsg": 0x0097,
|
||||
"OP_NPCSpellCastMsg": 0x0098,
|
||||
"OP_NPCMovementMsg": 0x0099,
|
||||
"OP_ItemMoveMsg": 0x00A0,
|
||||
"OP_ItemEquipMsg": 0x00A1,
|
||||
"OP_ItemUnequipMsg": 0x00A2,
|
||||
"OP_ItemPickupMsg": 0x00A3,
|
||||
"OP_ItemDropMsg": 0x00A4,
|
||||
"OP_ItemExamineMsg": 0x00A5,
|
||||
"OP_ItemUpdateMsg": 0x00A6,
|
||||
"OP_EqHearChatCmd": 0x1000,
|
||||
"OP_EqDisplayTextCmd": 0x1001,
|
||||
"OP_EqCreateGhostCmd": 0x1002,
|
||||
"OP_EqCreateWidgetCmd": 0x1003,
|
||||
"OP_EqDestroyGhostCmd": 0x1004,
|
||||
"OP_EqUpdateGhostCmd": 0x1005,
|
||||
"OP_EqSetControlGhostCmd": 0x1006,
|
||||
"OP_EqSetPOVGhostCmd": 0x1007,
|
||||
}
|
||||
|
||||
// Load opcodes for client version 60013
|
||||
err := packets.LoadGlobalOpcodeMappings(60013, sampleOpcodes)
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading opcode mappings: %v\n", err)
|
||||
} 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
@ -1,372 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ZoneServer represents a single zone instance
|
||||
type ZoneServer struct {
|
||||
ID int32
|
||||
Name string
|
||||
InstanceID int32
|
||||
ZoneFile string
|
||||
Description string
|
||||
MOTD string
|
||||
|
||||
// Zone properties
|
||||
MinLevel int16
|
||||
MaxLevel int16
|
||||
MinVersion int16
|
||||
XPModifier float32
|
||||
CityZone bool
|
||||
WeatherAllowed bool
|
||||
|
||||
// Safe location
|
||||
SafeX float32
|
||||
SafeY float32
|
||||
SafeZ float32
|
||||
SafeHeading float32
|
||||
|
||||
// Zone state
|
||||
IsRunning bool
|
||||
IsShuttingDown bool
|
||||
Population int32
|
||||
CreatedTime time.Time
|
||||
|
||||
// Clients in zone
|
||||
clients map[int32]*Client
|
||||
clientMutex sync.RWMutex
|
||||
|
||||
// Zone processing
|
||||
lastProcess time.Time
|
||||
processInterval time.Duration
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// ZoneList manages all active zones
|
||||
type ZoneList struct {
|
||||
zones map[int32]*ZoneServer
|
||||
zonesByName map[string][]*ZoneServer // Multiple instances per zone name
|
||||
instances map[int32]*ZoneServer // Instance ID to zone mapping
|
||||
|
||||
nextInstanceID int32
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewZoneList creates a new zone list
|
||||
func NewZoneList() *ZoneList {
|
||||
return &ZoneList{
|
||||
zones: make(map[int32]*ZoneServer),
|
||||
zonesByName: make(map[string][]*ZoneServer),
|
||||
instances: make(map[int32]*ZoneServer),
|
||||
nextInstanceID: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a zone to the list
|
||||
func (zl *ZoneList) Add(zone *ZoneServer) error {
|
||||
zl.mutex.Lock()
|
||||
defer zl.mutex.Unlock()
|
||||
|
||||
if _, exists := zl.zones[zone.ID]; exists {
|
||||
return fmt.Errorf("zone with ID %d already exists", zone.ID)
|
||||
}
|
||||
|
||||
// Assign instance ID if not set
|
||||
if zone.InstanceID == 0 {
|
||||
zone.InstanceID = zl.nextInstanceID
|
||||
zl.nextInstanceID++
|
||||
}
|
||||
|
||||
// Add to maps
|
||||
zl.zones[zone.ID] = zone
|
||||
zl.zonesByName[zone.Name] = append(zl.zonesByName[zone.Name], zone)
|
||||
zl.instances[zone.InstanceID] = zone
|
||||
|
||||
zone.CreatedTime = time.Now()
|
||||
zone.IsRunning = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a zone from the list
|
||||
func (zl *ZoneList) Remove(zoneID int32) {
|
||||
zl.mutex.Lock()
|
||||
defer zl.mutex.Unlock()
|
||||
|
||||
zone, exists := zl.zones[zoneID]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove from zones map
|
||||
delete(zl.zones, zoneID)
|
||||
|
||||
// Remove from instances map
|
||||
delete(zl.instances, zone.InstanceID)
|
||||
|
||||
// Remove from name map
|
||||
if zones, ok := zl.zonesByName[zone.Name]; ok {
|
||||
newZones := make([]*ZoneServer, 0, len(zones)-1)
|
||||
for _, z := range zones {
|
||||
if z.ID != zoneID {
|
||||
newZones = append(newZones, z)
|
||||
}
|
||||
}
|
||||
if len(newZones) > 0 {
|
||||
zl.zonesByName[zone.Name] = newZones
|
||||
} else {
|
||||
delete(zl.zonesByName, zone.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetByID returns a zone by its ID
|
||||
func (zl *ZoneList) GetByID(zoneID int32) *ZoneServer {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
return zl.zones[zoneID]
|
||||
}
|
||||
|
||||
// GetByName returns all zones with the given name
|
||||
func (zl *ZoneList) GetByName(name string) []*ZoneServer {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
zones := zl.zonesByName[name]
|
||||
result := make([]*ZoneServer, len(zones))
|
||||
copy(result, zones)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetByInstanceID returns a zone by its instance ID
|
||||
func (zl *ZoneList) GetByInstanceID(instanceID int32) *ZoneServer {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
return zl.instances[instanceID]
|
||||
}
|
||||
|
||||
// GetByLowestPopulation returns the zone instance with the lowest population
|
||||
func (zl *ZoneList) GetByLowestPopulation(zoneName string) *ZoneServer {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
zones := zl.zonesByName[zoneName]
|
||||
if len(zones) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lowestPop := zones[0]
|
||||
for _, zone := range zones[1:] {
|
||||
if zone.Population < lowestPop.Population && zone.IsRunning && !zone.IsShuttingDown {
|
||||
lowestPop = zone
|
||||
}
|
||||
}
|
||||
|
||||
return lowestPop
|
||||
}
|
||||
|
||||
// Count returns the total number of zones
|
||||
func (zl *ZoneList) Count() int32 {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
return int32(len(zl.zones))
|
||||
}
|
||||
|
||||
// CountInstances returns the number of instance zones
|
||||
func (zl *ZoneList) CountInstances() int32 {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
count := int32(0)
|
||||
for _, zone := range zl.zones {
|
||||
if zone.InstanceID > 0 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// GetTotalPopulation returns the total population across all zones
|
||||
func (zl *ZoneList) GetTotalPopulation() int32 {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
total := int32(0)
|
||||
for _, zone := range zl.zones {
|
||||
total += zone.Population
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// ProcessAll processes all zones
|
||||
func (zl *ZoneList) ProcessAll() {
|
||||
zl.mutex.RLock()
|
||||
zones := make([]*ZoneServer, 0, len(zl.zones))
|
||||
for _, zone := range zl.zones {
|
||||
zones = append(zones, zone)
|
||||
}
|
||||
zl.mutex.RUnlock()
|
||||
|
||||
for _, zone := range zones {
|
||||
if zone.IsRunning && !zone.IsShuttingDown {
|
||||
zone.Process()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendTimeUpdate sends time update to all zones
|
||||
func (zl *ZoneList) SendTimeUpdate(worldTime *WorldTime) {
|
||||
zl.mutex.RLock()
|
||||
defer zl.mutex.RUnlock()
|
||||
|
||||
for _, zone := range zl.zones {
|
||||
if zone.IsRunning {
|
||||
// TODO: Send time update packet to all clients in zone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckHealth checks the health of all zones
|
||||
func (zl *ZoneList) CheckHealth() {
|
||||
zl.mutex.RLock()
|
||||
zones := make([]*ZoneServer, 0, len(zl.zones))
|
||||
for _, zone := range zl.zones {
|
||||
zones = append(zones, zone)
|
||||
}
|
||||
zl.mutex.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
for _, zone := range zones {
|
||||
zone.mutex.Lock()
|
||||
|
||||
// Check if zone has been processing
|
||||
if zone.IsRunning && now.Sub(zone.lastProcess) > 30*time.Second {
|
||||
fmt.Printf("Warning: Zone %s (%d) has not processed in %v\n",
|
||||
zone.Name, zone.ID, now.Sub(zone.lastProcess))
|
||||
}
|
||||
|
||||
zone.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupDead removes zones that are no longer running
|
||||
func (zl *ZoneList) CleanupDead() {
|
||||
zl.mutex.Lock()
|
||||
defer zl.mutex.Unlock()
|
||||
|
||||
toRemove := make([]int32, 0)
|
||||
|
||||
for id, zone := range zl.zones {
|
||||
if !zone.IsRunning && zone.Population == 0 {
|
||||
toRemove = append(toRemove, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range toRemove {
|
||||
zl.Remove(id)
|
||||
fmt.Printf("Cleaned up dead zone ID %d\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
// ShutdownAll shuts down all zones
|
||||
func (zl *ZoneList) ShutdownAll() {
|
||||
zl.mutex.RLock()
|
||||
zones := make([]*ZoneServer, 0, len(zl.zones))
|
||||
for _, zone := range zl.zones {
|
||||
zones = append(zones, zone)
|
||||
}
|
||||
zl.mutex.RUnlock()
|
||||
|
||||
for _, zone := range zones {
|
||||
zone.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// Process handles zone processing
|
||||
func (z *ZoneServer) Process() {
|
||||
z.mutex.Lock()
|
||||
defer z.mutex.Unlock()
|
||||
|
||||
if !z.IsRunning || z.IsShuttingDown {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if now.Sub(z.lastProcess) < z.processInterval {
|
||||
return
|
||||
}
|
||||
|
||||
z.lastProcess = now
|
||||
|
||||
// TODO: Implement zone processing
|
||||
// - Process spawns
|
||||
// - Process spell timers
|
||||
// - Process movement
|
||||
// - Process combat
|
||||
// - Process respawns
|
||||
// - Send updates to clients
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the zone
|
||||
func (z *ZoneServer) Shutdown() {
|
||||
z.mutex.Lock()
|
||||
defer z.mutex.Unlock()
|
||||
|
||||
if z.IsShuttingDown {
|
||||
return
|
||||
}
|
||||
|
||||
z.IsShuttingDown = true
|
||||
|
||||
// Notify all clients
|
||||
z.clientMutex.RLock()
|
||||
for _, client := range z.clients {
|
||||
client.SendSimpleMessage("Zone is shutting down...")
|
||||
}
|
||||
z.clientMutex.RUnlock()
|
||||
|
||||
// TODO: Save zone state
|
||||
// TODO: Disconnect all clients
|
||||
// TODO: Clean up resources
|
||||
|
||||
z.IsRunning = false
|
||||
}
|
||||
|
||||
// AddClient adds a client to the zone
|
||||
func (z *ZoneServer) AddClient(client *Client) {
|
||||
z.clientMutex.Lock()
|
||||
defer z.clientMutex.Unlock()
|
||||
|
||||
if z.clients == nil {
|
||||
z.clients = make(map[int32]*Client)
|
||||
}
|
||||
|
||||
z.clients[client.CharacterID] = client
|
||||
z.Population++
|
||||
}
|
||||
|
||||
// RemoveClient removes a client from the zone
|
||||
func (z *ZoneServer) RemoveClient(characterID int32) {
|
||||
z.clientMutex.Lock()
|
||||
defer z.clientMutex.Unlock()
|
||||
|
||||
if _, exists := z.clients[characterID]; exists {
|
||||
delete(z.clients, characterID)
|
||||
z.Population--
|
||||
}
|
||||
}
|
||||
|
||||
// GetClient returns a client by character ID
|
||||
func (z *ZoneServer) GetClient(characterID int32) *Client {
|
||||
z.clientMutex.RLock()
|
||||
defer z.clientMutex.RUnlock()
|
||||
|
||||
return z.clients[characterID]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user