begin work on world server, integrations

This commit is contained in:
Sky Johnson 2025-08-06 23:11:08 -05:00
parent abec68872f
commit 75a7f8b49e
12 changed files with 4125 additions and 1 deletions

7
.gitignore vendored
View File

@ -16,4 +16,9 @@
*.out
# Go workspace file
go.work
go.work
# Test builds
world_config.json
world_server
world.db

View File

@ -0,0 +1,360 @@
package database
import (
"database/sql"
"fmt"
"sync"
_ "modernc.org/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// Database wraps the SQL database connection
type Database struct {
db *sql.DB
pool *sqlitex.Pool // For achievements system compatibility
dbPath string // Store path for pool creation
mutex sync.RWMutex
}
// New creates a new database connection
func New(path string) (*Database, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Test connection
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
// Set connection pool settings
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
// Create sqlitex pool for achievements system
pool, err := sqlitex.NewPool(path, sqlitex.PoolOptions{
PoolSize: 5,
})
if err != nil {
return nil, fmt.Errorf("failed to create sqlite pool: %w", err)
}
d := &Database{
db: db,
pool: pool,
dbPath: path,
}
// Initialize schema
if err := d.initSchema(); err != nil {
return nil, fmt.Errorf("failed to initialize schema: %w", err)
}
return d, nil
}
// Close closes the database connection
func (d *Database) Close() error {
if d.pool != nil {
d.pool.Close()
}
return d.db.Close()
}
// GetPool returns the sqlitex pool for achievements system compatibility
func (d *Database) GetPool() *sqlitex.Pool {
return d.pool
}
// initSchema creates the database schema if it doesn't exist
func (d *Database) initSchema() error {
schemas := []string{
// Rules table
`CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT,
UNIQUE(category, name)
)`,
// Accounts table
`CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT,
admin_level INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
)`,
// Characters table
`CREATE TABLE IF NOT EXISTS characters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
name TEXT UNIQUE NOT NULL,
race_id INTEGER NOT NULL,
class_id INTEGER NOT NULL,
level INTEGER DEFAULT 1,
x REAL DEFAULT 0,
y REAL DEFAULT 0,
z REAL DEFAULT 0,
heading REAL DEFAULT 0,
zone_id INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_played DATETIME,
FOREIGN KEY(account_id) REFERENCES accounts(id)
)`,
// Zones table
`CREATE TABLE IF NOT EXISTS zones (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
file TEXT NOT NULL,
description TEXT,
motd TEXT,
min_level INTEGER DEFAULT 0,
max_level INTEGER DEFAULT 100,
min_version INTEGER DEFAULT 0,
xp_modifier REAL DEFAULT 1.0,
city_zone INTEGER DEFAULT 0,
weather_allowed INTEGER DEFAULT 1,
safe_x REAL DEFAULT 0,
safe_y REAL DEFAULT 0,
safe_z REAL DEFAULT 0,
safe_heading REAL DEFAULT 0
)`,
// Server statistics table
`CREATE TABLE IF NOT EXISTS server_stats (
stat_id INTEGER PRIMARY KEY,
stat_value INTEGER,
stat_date INTEGER,
save_needed INTEGER DEFAULT 0
)`,
// Merchant tables
`CREATE TABLE IF NOT EXISTS merchants (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
merchant_type INTEGER DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS merchant_items (
merchant_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER DEFAULT -1,
price_coins INTEGER DEFAULT 0,
price_status INTEGER DEFAULT 0,
PRIMARY KEY(merchant_id, item_id),
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
)`,
// Achievement tables
`CREATE TABLE IF NOT EXISTS achievements (
achievement_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
uncompleted_text TEXT,
completed_text TEXT,
category TEXT,
expansion TEXT,
icon INTEGER DEFAULT 0,
point_value INTEGER DEFAULT 0,
qty_req INTEGER DEFAULT 1,
hide_achievement INTEGER DEFAULT 0,
unknown3a INTEGER DEFAULT 0,
unknown3b INTEGER DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS achievements_requirements (
achievement_id INTEGER NOT NULL,
name TEXT NOT NULL,
qty_req INTEGER DEFAULT 1,
PRIMARY KEY(achievement_id, name),
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS achievements_rewards (
achievement_id INTEGER NOT NULL,
reward TEXT NOT NULL,
PRIMARY KEY(achievement_id, reward),
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS character_achievements (
char_id INTEGER NOT NULL,
achievement_id INTEGER NOT NULL,
completed_date INTEGER,
PRIMARY KEY(char_id, achievement_id),
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS character_achievements_items (
char_id INTEGER NOT NULL,
achievement_id INTEGER NOT NULL,
items INTEGER DEFAULT 0,
PRIMARY KEY(char_id, achievement_id),
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
)`,
// Title tables
`CREATE TABLE IF NOT EXISTS titles (
title_id INTEGER PRIMARY KEY,
text TEXT NOT NULL,
category INTEGER DEFAULT 0,
rarity INTEGER DEFAULT 0,
position INTEGER DEFAULT 0,
description TEXT,
is_unique INTEGER DEFAULT 0,
is_hidden INTEGER DEFAULT 0,
color_code TEXT,
requirements TEXT,
source_type INTEGER DEFAULT 0,
source_id INTEGER DEFAULT 0,
created_date INTEGER,
expire_date INTEGER
)`,
`CREATE TABLE IF NOT EXISTS character_titles (
char_id INTEGER NOT NULL,
title_id INTEGER NOT NULL,
source_achievement_id INTEGER DEFAULT 0,
source_quest_id INTEGER DEFAULT 0,
granted_date INTEGER,
expire_date INTEGER,
PRIMARY KEY(char_id, title_id),
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
FOREIGN KEY(title_id) REFERENCES titles(title_id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS character_active_titles (
char_id INTEGER PRIMARY KEY,
prefix_title_id INTEGER DEFAULT 0,
suffix_title_id INTEGER DEFAULT 0,
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
FOREIGN KEY(prefix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL,
FOREIGN KEY(suffix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL
)`,
}
for _, schema := range schemas {
if _, err := d.db.Exec(schema); err != nil {
return fmt.Errorf("failed to create schema: %w", err)
}
}
return nil
}
// Query executes a query that returns rows
func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) {
return d.db.Query(query, args...)
}
// QueryRow executes a query that returns a single row
func (d *Database) QueryRow(query string, args ...interface{}) *sql.Row {
return d.db.QueryRow(query, args...)
}
// Exec executes a query that doesn't return rows
func (d *Database) Exec(query string, args ...interface{}) (sql.Result, error) {
return d.db.Exec(query, args...)
}
// Begin starts a transaction
func (d *Database) Begin() (*sql.Tx, error) {
return d.db.Begin()
}
// LoadRules loads all rules from the database
func (d *Database) LoadRules() (map[string]map[string]string, error) {
rows, err := d.Query("SELECT category, name, value FROM rules")
if err != nil {
return nil, err
}
defer rows.Close()
rules := make(map[string]map[string]string)
for rows.Next() {
var category, name, value string
if err := rows.Scan(&category, &name, &value); err != nil {
return nil, err
}
if rules[category] == nil {
rules[category] = make(map[string]string)
}
rules[category][name] = value
}
return rules, rows.Err()
}
// SaveRule saves a rule to the database
func (d *Database) SaveRule(category, name, value, description string) error {
_, err := d.Exec(`
INSERT OR REPLACE INTO rules (category, name, value, description)
VALUES (?, ?, ?, ?)
`, category, name, value, description)
return err
}
// GetZones retrieves all zones from the database
func (d *Database) GetZones() ([]map[string]interface{}, error) {
rows, err := d.Query(`
SELECT id, name, file, description, motd, min_level, max_level,
min_version, xp_modifier, city_zone, weather_allowed,
safe_x, safe_y, safe_z, safe_heading
FROM zones
ORDER BY name
`)
if err != nil {
return nil, err
}
defer rows.Close()
var zones []map[string]interface{}
for rows.Next() {
zone := make(map[string]interface{})
var id, minLevel, maxLevel, minVersion int
var name, file, description, motd string
var xpModifier, safeX, safeY, safeZ, safeHeading float64
var cityZone, weatherAllowed bool
err := rows.Scan(&id, &name, &file, &description, &motd,
&minLevel, &maxLevel, &minVersion, &xpModifier,
&cityZone, &weatherAllowed,
&safeX, &safeY, &safeZ, &safeHeading)
if err != nil {
return nil, err
}
zone["id"] = id
zone["name"] = name
zone["file"] = file
zone["description"] = description
zone["motd"] = motd
zone["min_level"] = minLevel
zone["max_level"] = maxLevel
zone["min_version"] = minVersion
zone["xp_modifier"] = xpModifier
zone["city_zone"] = cityZone
zone["weather_allowed"] = weatherAllowed
zone["safe_x"] = safeX
zone["safe_y"] = safeY
zone["safe_z"] = safeZ
zone["safe_heading"] = safeHeading
zones = append(zones, zone)
}
return zones, rows.Err()
}

222
internal/packets/handler.go Normal file
View File

@ -0,0 +1,222 @@
package packets
import (
"fmt"
"sync"
)
// PacketData represents processed packet information
type PacketData struct {
Opcode InternalOpcode
ClientOpcode uint16
ClientVersion int32
Data []byte
Size int32
}
// PacketContext provides context for packet handlers
type PacketContext struct {
Client ClientInterface // Client connection interface
World WorldInterface // World server interface
Database DatabaseInterface // Database interface
}
// ClientInterface defines the interface that client connections must implement
type ClientInterface interface {
GetCharacterID() int32
GetAccountID() int32
GetCharacterName() string
GetClientVersion() int32
GetAdminLevel() int
IsInZone() bool
SendPacket(opcode InternalOpcode, data []byte) error
Disconnect() error
}
// WorldInterface defines the interface for world server operations
type WorldInterface interface {
GetClientByID(characterID int32) ClientInterface
GetAllClients() []ClientInterface
BroadcastPacket(opcode InternalOpcode, data []byte)
BroadcastToZone(zoneID int32, opcode InternalOpcode, data []byte)
}
// DatabaseInterface defines database operations needed by packet handlers
type DatabaseInterface interface {
GetCharacter(characterID int32) (map[string]interface{}, error)
SaveCharacter(characterID int32, data map[string]interface{}) error
// Add more database methods as needed
}
// PacketHandlerFunc defines the signature for packet handler functions
type PacketHandlerFunc func(ctx *PacketContext, packet *PacketData) error
// PacketHandlerRegistry manages registration and dispatch of packet handlers
type PacketHandlerRegistry struct {
handlers map[InternalOpcode]PacketHandlerFunc
mutex sync.RWMutex
}
// NewPacketHandlerRegistry creates a new packet handler registry
func NewPacketHandlerRegistry() *PacketHandlerRegistry {
return &PacketHandlerRegistry{
handlers: make(map[InternalOpcode]PacketHandlerFunc),
}
}
// RegisterHandler registers a handler for a specific opcode
func (phr *PacketHandlerRegistry) RegisterHandler(opcode InternalOpcode, handler PacketHandlerFunc) {
phr.mutex.Lock()
defer phr.mutex.Unlock()
phr.handlers[opcode] = handler
fmt.Printf("Registered handler for opcode %s (%d)\n", GetInternalOpcodeName(opcode), int(opcode))
}
// UnregisterHandler removes a handler for a specific opcode
func (phr *PacketHandlerRegistry) UnregisterHandler(opcode InternalOpcode) {
phr.mutex.Lock()
defer phr.mutex.Unlock()
delete(phr.handlers, opcode)
}
// HasHandler checks if a handler is registered for an opcode
func (phr *PacketHandlerRegistry) HasHandler(opcode InternalOpcode) bool {
phr.mutex.RLock()
defer phr.mutex.RUnlock()
_, exists := phr.handlers[opcode]
return exists
}
// HandlePacket dispatches a packet to its registered handler
func (phr *PacketHandlerRegistry) HandlePacket(ctx *PacketContext, packet *PacketData) error {
phr.mutex.RLock()
handler, exists := phr.handlers[packet.Opcode]
phr.mutex.RUnlock()
if !exists {
// No handler registered - this is not necessarily an error
fmt.Printf("No handler registered for opcode %s (%d) from client %s\n",
GetInternalOpcodeName(packet.Opcode),
int(packet.Opcode),
ctx.Client.GetCharacterName())
return nil
}
// Call the handler
return handler(ctx, packet)
}
// GetHandlerCount returns the number of registered handlers
func (phr *PacketHandlerRegistry) GetHandlerCount() int {
phr.mutex.RLock()
defer phr.mutex.RUnlock()
return len(phr.handlers)
}
// GetRegisteredOpcodes returns all opcodes with registered handlers
func (phr *PacketHandlerRegistry) GetRegisteredOpcodes() []InternalOpcode {
phr.mutex.RLock()
defer phr.mutex.RUnlock()
opcodes := make([]InternalOpcode, 0, len(phr.handlers))
for opcode := range phr.handlers {
opcodes = append(opcodes, opcode)
}
return opcodes
}
// PacketProcessor combines opcode management and handler dispatch
type PacketProcessor struct {
opcodeManager *OpcodeManager
handlerRegistry *PacketHandlerRegistry
}
// NewPacketProcessor creates a new packet processor
func NewPacketProcessor() *PacketProcessor {
return &PacketProcessor{
opcodeManager: GetOpcodeManager(),
handlerRegistry: NewPacketHandlerRegistry(),
}
}
// ProcessRawPacket processes a raw packet from the UDP layer
func (pp *PacketProcessor) ProcessRawPacket(ctx *PacketContext, rawData []byte, clientOpcode uint16) error {
if len(rawData) < 2 {
return fmt.Errorf("packet too short: %d bytes", len(rawData))
}
// Convert client opcode to internal opcode
clientVersion := ctx.Client.GetClientVersion()
internalOpcode := pp.opcodeManager.ClientOpcodeToInternal(clientVersion, clientOpcode)
if internalOpcode == OP_Unknown {
fmt.Printf("Unknown opcode 0x%04X from client version %d (client: %s)\n",
clientOpcode, clientVersion, ctx.Client.GetCharacterName())
return nil // Don't treat unknown opcodes as errors
}
// Create packet data structure
packet := &PacketData{
Opcode: internalOpcode,
ClientOpcode: clientOpcode,
ClientVersion: clientVersion,
Data: rawData,
Size: int32(len(rawData)),
}
// Dispatch to handler
return pp.handlerRegistry.HandlePacket(ctx, packet)
}
// RegisterHandler registers a packet handler
func (pp *PacketProcessor) RegisterHandler(opcode InternalOpcode, handler PacketHandlerFunc) {
pp.handlerRegistry.RegisterHandler(opcode, handler)
}
// LoadOpcodeMap loads opcode mappings for a client version
func (pp *PacketProcessor) LoadOpcodeMap(clientVersion int32, opcodeMap map[string]uint16) error {
return pp.opcodeManager.LoadOpcodeMap(clientVersion, opcodeMap)
}
// GetStats returns processor statistics
func (pp *PacketProcessor) GetStats() (int, int, []int32) {
handlerCount := pp.handlerRegistry.GetHandlerCount()
supportedVersions := pp.opcodeManager.GetSupportedVersions()
totalOpcodes := 0
for _, version := range supportedVersions {
totalOpcodes += pp.opcodeManager.GetOpcodeCount(version)
}
return handlerCount, totalOpcodes, supportedVersions
}
// Global packet processor instance
var globalPacketProcessor = NewPacketProcessor()
// GetPacketProcessor returns the global packet processor
func GetPacketProcessor() *PacketProcessor {
return globalPacketProcessor
}
// Convenience functions for global access
// RegisterGlobalHandler registers a handler with the global processor
func RegisterGlobalHandler(opcode InternalOpcode, handler PacketHandlerFunc) {
globalPacketProcessor.RegisterHandler(opcode, handler)
}
// ProcessGlobalPacket processes a packet with the global processor
func ProcessGlobalPacket(ctx *PacketContext, rawData []byte, clientOpcode uint16) error {
return globalPacketProcessor.ProcessRawPacket(ctx, rawData, clientOpcode)
}
// LoadGlobalOpcodeMappings loads opcode mappings with the global processor
func LoadGlobalOpcodeMappings(clientVersion int32, opcodeMap map[string]uint16) error {
return globalPacketProcessor.LoadOpcodeMap(clientVersion, opcodeMap)
}

292
internal/packets/opcodes.go Normal file
View File

@ -0,0 +1,292 @@
package packets
import (
"fmt"
"sync"
)
// InternalOpcode represents the internal opcode enumeration
type InternalOpcode int32
// Internal opcode constants - these map to the C++ EmuOpcode enum
const (
OP_Unknown InternalOpcode = iota
// Login and authentication operations
OP_LoginReplyMsg
OP_LoginByNumRequestMsg
OP_WSLoginRequestMsg
// Server initialization and zone management
OP_ESInitMsg
OP_ESReadyForClientsMsg
OP_CreateZoneInstanceMsg
OP_ZoneInstanceCreateReplyMsg
OP_ZoneInstanceDestroyedMsg
OP_ExpectClientAsCharacterRequest
OP_ExpectClientAsCharacterReplyMs
OP_ZoneInfoMsg
// Character creation and loading
OP_CreateCharacterRequestMsg
OP_DoneLoadingZoneResourcesMsg
OP_DoneSendingInitialEntitiesMsg
OP_DoneLoadingEntityResourcesMsg
OP_DoneLoadingUIResourcesMsg
// Game state updates
OP_PredictionUpdateMsg
OP_RemoteCmdMsg
OP_SetRemoteCmdsMsg
OP_GameWorldTimeMsg
OP_MOTDMsg
OP_ZoneMOTDMsg
// Command dispatching
OP_ClientCmdMsg
OP_DispatchClientCmdMsg
OP_DispatchESMsg
// Character sheet and inventory updates
OP_UpdateCharacterSheetMsg
OP_UpdateSpellBookMsg
OP_UpdateInventoryMsg
// Zone transitions
OP_ChangeZoneMsg
OP_ClientTeleportRequestMsg
OP_TeleportWithinZoneMsg
OP_ReadyToZoneMsg
// Chat system
OP_ChatTellChannelMsg
OP_ChatTellUserMsg
// Position updates
OP_UpdatePositionMsg
// Achievement system
OP_AchievementUpdateMsg
OP_CharacterAchievements
// Title system
OP_TitleUpdateMsg
OP_CharacterTitles
OP_SetActiveTitleMsg
// EverQuest specific commands - Core
OP_EqHearChatCmd
OP_EqDisplayTextCmd
OP_EqCreateGhostCmd
OP_EqCreateWidgetCmd
OP_EqDestroyGhostCmd
OP_EqUpdateGhostCmd
OP_EqSetControlGhostCmd
OP_EqSetPOVGhostCmd
// Add more opcodes as needed...
_maxInternalOpcode // Sentinel value
)
// OpcodeNames maps internal opcodes to their string names for debugging
var OpcodeNames = map[InternalOpcode]string{
OP_Unknown: "OP_Unknown",
OP_LoginReplyMsg: "OP_LoginReplyMsg",
OP_LoginByNumRequestMsg: "OP_LoginByNumRequestMsg",
OP_WSLoginRequestMsg: "OP_WSLoginRequestMsg",
OP_ESInitMsg: "OP_ESInitMsg",
OP_ESReadyForClientsMsg: "OP_ESReadyForClientsMsg",
OP_CreateZoneInstanceMsg: "OP_CreateZoneInstanceMsg",
OP_ZoneInstanceCreateReplyMsg: "OP_ZoneInstanceCreateReplyMsg",
OP_ZoneInstanceDestroyedMsg: "OP_ZoneInstanceDestroyedMsg",
OP_ExpectClientAsCharacterRequest: "OP_ExpectClientAsCharacterRequest",
OP_ExpectClientAsCharacterReplyMs: "OP_ExpectClientAsCharacterReplyMs",
OP_ZoneInfoMsg: "OP_ZoneInfoMsg",
OP_CreateCharacterRequestMsg: "OP_CreateCharacterRequestMsg",
OP_DoneLoadingZoneResourcesMsg: "OP_DoneLoadingZoneResourcesMsg",
OP_DoneSendingInitialEntitiesMsg: "OP_DoneSendingInitialEntitiesMsg",
OP_DoneLoadingEntityResourcesMsg: "OP_DoneLoadingEntityResourcesMsg",
OP_DoneLoadingUIResourcesMsg: "OP_DoneLoadingUIResourcesMsg",
OP_PredictionUpdateMsg: "OP_PredictionUpdateMsg",
OP_RemoteCmdMsg: "OP_RemoteCmdMsg",
OP_SetRemoteCmdsMsg: "OP_SetRemoteCmdsMsg",
OP_GameWorldTimeMsg: "OP_GameWorldTimeMsg",
OP_MOTDMsg: "OP_MOTDMsg",
OP_ZoneMOTDMsg: "OP_ZoneMOTDMsg",
OP_ClientCmdMsg: "OP_ClientCmdMsg",
OP_DispatchClientCmdMsg: "OP_DispatchClientCmdMsg",
OP_DispatchESMsg: "OP_DispatchESMsg",
OP_UpdateCharacterSheetMsg: "OP_UpdateCharacterSheetMsg",
OP_UpdateSpellBookMsg: "OP_UpdateSpellBookMsg",
OP_UpdateInventoryMsg: "OP_UpdateInventoryMsg",
OP_ChangeZoneMsg: "OP_ChangeZoneMsg",
OP_ClientTeleportRequestMsg: "OP_ClientTeleportRequestMsg",
OP_TeleportWithinZoneMsg: "OP_TeleportWithinZoneMsg",
OP_ReadyToZoneMsg: "OP_ReadyToZoneMsg",
OP_ChatTellChannelMsg: "OP_ChatTellChannelMsg",
OP_ChatTellUserMsg: "OP_ChatTellUserMsg",
OP_UpdatePositionMsg: "OP_UpdatePositionMsg",
OP_AchievementUpdateMsg: "OP_AchievementUpdateMsg",
OP_CharacterAchievements: "OP_CharacterAchievements",
OP_TitleUpdateMsg: "OP_TitleUpdateMsg",
OP_CharacterTitles: "OP_CharacterTitles",
OP_SetActiveTitleMsg: "OP_SetActiveTitleMsg",
OP_EqHearChatCmd: "OP_EqHearChatCmd",
OP_EqDisplayTextCmd: "OP_EqDisplayTextCmd",
OP_EqCreateGhostCmd: "OP_EqCreateGhostCmd",
OP_EqCreateWidgetCmd: "OP_EqCreateWidgetCmd",
OP_EqDestroyGhostCmd: "OP_EqDestroyGhostCmd",
OP_EqUpdateGhostCmd: "OP_EqUpdateGhostCmd",
OP_EqSetControlGhostCmd: "OP_EqSetControlGhostCmd",
OP_EqSetPOVGhostCmd: "OP_EqSetPOVGhostCmd",
}
// OpcodeManager handles the mapping between client-specific opcodes and internal opcodes
type OpcodeManager struct {
// Maps client version -> (client opcode -> internal opcode)
clientToInternal map[int32]map[uint16]InternalOpcode
// Maps internal opcode -> client version -> client opcode
internalToClient map[InternalOpcode]map[int32]uint16
mutex sync.RWMutex
}
// NewOpcodeManager creates a new opcode manager
func NewOpcodeManager() *OpcodeManager {
return &OpcodeManager{
clientToInternal: make(map[int32]map[uint16]InternalOpcode),
internalToClient: make(map[InternalOpcode]map[int32]uint16),
}
}
// LoadOpcodeMap loads opcode mappings for a specific client version
func (om *OpcodeManager) LoadOpcodeMap(clientVersion int32, opcodeMap map[string]uint16) error {
om.mutex.Lock()
defer om.mutex.Unlock()
// Initialize maps for this client version
if om.clientToInternal[clientVersion] == nil {
om.clientToInternal[clientVersion] = make(map[uint16]InternalOpcode)
}
// Process each opcode mapping
for opcodeName, clientOpcode := range opcodeMap {
// Find the internal opcode for this name
internalOpcode := OP_Unknown
for intOp, name := range OpcodeNames {
if name == opcodeName {
internalOpcode = intOp
break
}
}
if internalOpcode == OP_Unknown && opcodeName != "OP_Unknown" {
// Log warning for unknown opcode but don't fail
fmt.Printf("Warning: Unknown internal opcode name: %s\n", opcodeName)
continue
}
// Set client -> internal mapping
om.clientToInternal[clientVersion][clientOpcode] = internalOpcode
// Set internal -> client mapping
if om.internalToClient[internalOpcode] == nil {
om.internalToClient[internalOpcode] = make(map[int32]uint16)
}
om.internalToClient[internalOpcode][clientVersion] = clientOpcode
}
fmt.Printf("Loaded %d opcode mappings for client version %d\n", len(opcodeMap), clientVersion)
return nil
}
// ClientOpcodeToInternal converts a client opcode to internal opcode
func (om *OpcodeManager) ClientOpcodeToInternal(clientVersion int32, clientOpcode uint16) InternalOpcode {
om.mutex.RLock()
defer om.mutex.RUnlock()
if versionMap, exists := om.clientToInternal[clientVersion]; exists {
if internalOp, found := versionMap[clientOpcode]; found {
return internalOp
}
}
return OP_Unknown
}
// InternalOpcodeToClient converts an internal opcode to client opcode
func (om *OpcodeManager) InternalOpcodeToClient(internalOpcode InternalOpcode, clientVersion int32) uint16 {
om.mutex.RLock()
defer om.mutex.RUnlock()
if versionMap, exists := om.internalToClient[internalOpcode]; exists {
if clientOp, found := versionMap[clientVersion]; found {
return clientOp
}
}
return 0 // Invalid client opcode
}
// GetOpcodeName returns the human-readable name for an internal opcode
func (om *OpcodeManager) GetOpcodeName(internalOpcode InternalOpcode) string {
if name, exists := OpcodeNames[internalOpcode]; exists {
return name
}
return "OP_Unknown"
}
// GetSupportedVersions returns all client versions with loaded opcodes
func (om *OpcodeManager) GetSupportedVersions() []int32 {
om.mutex.RLock()
defer om.mutex.RUnlock()
versions := make([]int32, 0, len(om.clientToInternal))
for version := range om.clientToInternal {
versions = append(versions, version)
}
return versions
}
// GetOpcodeCount returns the number of opcodes loaded for a client version
func (om *OpcodeManager) GetOpcodeCount(clientVersion int32) int {
om.mutex.RLock()
defer om.mutex.RUnlock()
if versionMap, exists := om.clientToInternal[clientVersion]; exists {
return len(versionMap)
}
return 0
}
// Global opcode manager instance
var globalOpcodeManager = NewOpcodeManager()
// GetOpcodeManager returns the global opcode manager
func GetOpcodeManager() *OpcodeManager {
return globalOpcodeManager
}
// Convenience functions for global access
// LoadGlobalOpcodeMap loads opcodes into the global manager
func LoadGlobalOpcodeMap(clientVersion int32, opcodeMap map[string]uint16) error {
return globalOpcodeManager.LoadOpcodeMap(clientVersion, opcodeMap)
}
// ClientToInternal converts using the global manager
func ClientToInternal(clientVersion int32, clientOpcode uint16) InternalOpcode {
return globalOpcodeManager.ClientOpcodeToInternal(clientVersion, clientOpcode)
}
// InternalToClient converts using the global manager
func InternalToClient(internalOpcode InternalOpcode, clientVersion int32) uint16 {
return globalOpcodeManager.InternalOpcodeToClient(internalOpcode, clientVersion)
}
// GetInternalOpcodeName returns name using the global manager
func GetInternalOpcodeName(internalOpcode InternalOpcode) string {
return globalOpcodeManager.GetOpcodeName(internalOpcode)
}

331
internal/world/README.md Normal file
View File

@ -0,0 +1,331 @@
# 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_path": "world.db",
"server_name": "EQ2Go World Server",
"xp_rate": 1.0,
"ts_xp_rate": 1.0
}
```
## 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]interface{}, 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.

View File

@ -0,0 +1,415 @@
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]interface{}
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]interface{}) {
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]interface{}{
"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]interface{}{
"quest_id": questID,
})
}
// OnLevelGain triggers a level gain achievement event
func (w *World) OnLevelGain(characterID int32, newLevel int32) {
w.TriggerAchievementEvent(EventLevelGain, characterID, map[string]interface{}{
"level": newLevel,
})
}
// OnItemDiscovered triggers an item discovery achievement event
func (w *World) OnItemDiscovered(characterID int32, itemID int32, rarity string) {
w.TriggerAchievementEvent(EventItemDiscovered, characterID, map[string]interface{}{
"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]interface{}{
"zone_id": zoneID,
})
}

View File

@ -0,0 +1,324 @@
package world
import (
"fmt"
"sync"
"eq2emu/internal/achievements"
"eq2emu/internal/database"
)
// AchievementManager manages achievements for the world server
type AchievementManager struct {
masterList *achievements.MasterList
playerManagers map[int32]*achievements.PlayerManager // CharacterID -> PlayerManager
database *database.Database
world *World // Reference to world server for notifications
mutex sync.RWMutex
}
// 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...")
pool := am.database.GetPool()
if pool == nil {
return fmt.Errorf("database pool is nil")
}
err := achievements.LoadAllAchievements(pool, am.masterList)
if err != nil {
return fmt.Errorf("failed to load achievements: %w", err)
}
fmt.Printf("Loaded %d achievements\n", am.masterList.Size())
return nil
}
// 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) {
pool := am.database.GetPool()
if pool == nil {
fmt.Printf("Error: database pool is nil for character %d\n", characterID)
return
}
// Load player achievements
err := achievements.LoadPlayerAchievements(pool, uint32(characterID), playerMgr.Achievements)
if err != nil {
fmt.Printf("Error loading achievements for character %d: %v\n", characterID, err)
}
// Load player progress
err = achievements.LoadPlayerAchievementUpdates(pool, uint32(characterID), playerMgr.Updates)
if err != nil {
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)
fmt.Printf("Character %d completed achievement: %s\n", characterID, achievement.Title)
} else if progress > 0 {
// Save progress update to database
go am.savePlayerProgress(characterID, achievementID, playerMgr)
}
}
return nil
}
// 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
}
pool := am.database.GetPool()
if pool == nil {
fmt.Printf("Error: database pool is nil for character %d\n", characterID)
return
}
err := achievements.SavePlayerAchievementUpdate(pool, uint32(characterID), update)
if err != nil {
fmt.Printf("Error saving achievement progress for character %d, achievement %d: %v\n",
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)
}
// GetStatistics returns achievement system statistics
func (am *AchievementManager) GetStatistics() map[string]interface{} {
am.mutex.RLock()
defer am.mutex.RUnlock()
stats := map[string]interface{}{
"total_achievements": am.masterList.Size(),
"online_players": len(am.playerManagers),
"categories": am.masterList.GetCategories(),
"expansions": am.masterList.GetExpansions(),
}
return stats
}
// Shutdown gracefully shuts down the achievement manager
func (am *AchievementManager) Shutdown() {
fmt.Println("Shutting down achievement manager...")
am.mutex.Lock()
defer am.mutex.Unlock()
// Save all player progress before shutdown
for characterID, playerMgr := range am.playerManagers {
for _, achievementID := range playerMgr.Updates.GetInProgressAchievements() {
am.savePlayerProgress(characterID, achievementID, playerMgr)
}
}
// Clear player managers
am.playerManagers = make(map[int32]*achievements.PlayerManager)
fmt.Println("Achievement manager shutdown complete")
}

View File

@ -0,0 +1,417 @@
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 interface{} // 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)
}

View File

@ -0,0 +1,546 @@
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)
fmt.Printf("Registered %d packet handlers\n", 16)
}
// 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]interface{})
for achievementID, achievement := range allAchievements {
progress := w.achievementMgr.GetPlayerProgress(characterID, achievementID)
completed := w.achievementMgr.IsPlayerCompleted(characterID, achievementID)
percentage := w.achievementMgr.GetCompletionPercentage(characterID, achievementID)
characterData[fmt.Sprintf("achievement_%d", achievementID)] = map[string]interface{}{
"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))
}
// 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]interface{}, error) {
// TODO: Implement character loading from database
return nil, fmt.Errorf("character loading not yet implemented")
}
// SaveCharacter implements packets.DatabaseInterface
func (wda *WorldDatabaseAdapter) SaveCharacter(characterID int32, data map[string]interface{}) error {
// 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},
}
}

View File

@ -0,0 +1,229 @@
package world
import (
"fmt"
"sync"
"eq2emu/internal/titles"
"eq2emu/internal/database"
)
// 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...")
pool := tm.database.GetPool()
if pool == nil {
return fmt.Errorf("database pool is nil")
}
// TODO: Implement title loading from database when database functions are available
// For now, create some default titles for testing
err := tm.createDefaultTitles()
if err != nil {
return fmt.Errorf("failed to create default titles: %w", err)
}
fmt.Printf("Loaded %d titles\n", tm.titleManager.GetMasterList().GetTitleCount())
return nil
}
// 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]interface{} {
tm.mutex.RLock()
defer tm.mutex.RUnlock()
// Get statistics from the underlying title manager
titleManagerStats := tm.titleManager.GetStatistics()
// Combine with our own statistics
stats := map[string]interface{}{
"total_titles": tm.titleManager.GetMasterList().GetTitleCount(),
}
// 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")
}

611
internal/world/world.go Normal file
View File

@ -0,0 +1,611 @@
package world
import (
"context"
"fmt"
"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
// Master lists (singletons)
masterSpells interface{} // TODO: implement spell manager
masterItems interface{} // TODO: implement item manager
masterQuests interface{} // TODO: implement quest manager
masterSkills interface{} // TODO: implement skill manager
masterFactions interface{} // 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
DatabasePath string `json:"database_path"`
// 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
db, err := database.New(config.DatabasePath)
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)
// Create context
ctx, cancel := context.WithCancel(context.Background())
w := &World{
db: db,
commandManager: cmdManager,
rulesManager: rulesManager,
achievementMgr: achievementMgr,
titleMgr: titleMgr,
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)
// 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()
}
// 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()
// 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
}
// 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
}
// 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_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")
}

372
internal/world/zone_list.go Normal file
View File

@ -0,0 +1,372 @@
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]
}