Compare commits

...

2 Commits

28 changed files with 3837 additions and 3307 deletions

3
.gitignore vendored
View File

@ -17,3 +17,6 @@
# Go workspace file
go.work
# LuaJIT wrapper source
luawrapper

133
CLAUDE.md
View File

@ -234,6 +234,22 @@ XML-driven packet definitions with version-specific formats, conditional fields,
- `internal/languages/manager.go`: High-level language management with database integration, statistics, and command processing
- `internal/languages/interfaces.go`: Integration interfaces with database, players, chat processing, and event handling systems
**Player System:**
- `internal/player/player.go`: Core Player struct extending Entity with character data, experience, skills, spells, combat, social features
- `internal/player/player_info.go`: PlayerInfo struct for detailed character sheet data and serialization
- `internal/player/character_flags.go`: Character flag management for all EQ2 player states (anonymous, AFK, LFG, etc.)
- `internal/player/currency.go`: Coin and currency handling with validation and transaction support
- `internal/player/experience.go`: XP, leveling, and vitality systems with automatic level progression
- `internal/player/combat.go`: Combat mechanics, auto-attack, and weapon handling integration
- `internal/player/quest_management.go`: Quest system integration with tracking, progress, and completion
- `internal/player/spell_management.go`: Spell book and casting management with passive spell support
- `internal/player/skill_management.go`: Skill system integration with progression and bonuses
- `internal/player/spawn_management.go`: Spawn visibility and tracking for player view management
- `internal/player/manager.go`: Multi-player management system with statistics, events, and background processing
- `internal/player/interfaces.go`: System integration interfaces and player adapter for other systems
- `internal/player/database.go`: SQLite database operations for player persistence with zombiezen integration
- `internal/player/README.md`: Complete player system documentation with usage examples
**Quests System:**
- `internal/quests/constants.go`: Quest step types, display status flags, sharing constants, and validation limits
- `internal/quests/types.go`: Core Quest and QuestStep structures with complete quest data management and thread-safe operations
@ -258,6 +274,14 @@ XML-driven packet definitions with version-specific formats, conditional fields,
- `internal/npc/ai/variants.go`: Specialized brain types (CombatPet, NonCombatPet, Blank, Lua, DumbFire) with unique behaviors and factory functions
- `internal/npc/ai/interfaces.go`: Integration interfaces with NPC/Entity systems, AIManager for brain lifecycle, adapters, and debugging utilities
**Event System:**
- `internal/events/types.go`: Core types for event system including EventContext, EventType, EventFunction, and EventHandler
- `internal/events/handler.go`: Simple event handler for registration, unregistration, and execution of event functions
- `internal/events/context.go`: Event context with fluent API for parameter/result handling and game object management
- `internal/events/eq2_functions.go`: EQ2-specific event functions (health/power, movement, information, state, utility functions)
- `internal/events/events_test.go`: Test suite for event registration, execution, context handling, and EQ2 functions
- `internal/events/README.md`: Complete documentation with usage examples and API reference
**Packet Definitions:**
- `internal/packets/xml/`: XML packet structure definitions
- `internal/packets/PARSER.md`: Packet definition language documentation
@ -340,15 +364,118 @@ Command-line flags override JSON configuration.
**Languages System**: Multilingual character communication system managing language learning and chat processing. Features master language registry with ID-based and name-based lookups, individual player language collections with thread-safe operations, language validation and persistence, and comprehensive multilingual chat processing. Supports all EverQuest II racial languages (Common, Elvish, Dwarven, Halfling, Gnomish, Iksar, Trollish, Ogrish, Fae, Arasai, Sarnak, Froglok), language learning/forgetting mechanics, primary language selection, and message scrambling for unknown languages. Includes PlayerLanguageAdapter for seamless player integration, ChatLanguageProcessor for multilingual communication, and statistics tracking for language usage patterns. Thread-safe operations with efficient hash-based lookups and comprehensive validation systems.
**Player System**: Complete player character management extending Entity with comprehensive MMO player functionality. Features Player struct with character data (ID, level, class, race), experience systems (adventure/tradeskill XP with automatic leveling), skill progression, spell management (spell books, casting, passive spells), combat mechanics (auto-attack, combat state, weapon handling), social features (friends/ignore lists), currency management (coin transactions with validation), quest integration (tracking, progress, completion), spawn management (visibility, tracking for player view), character flags (anonymous, AFK, LFG, roleplaying, etc.), movement and positioning, housing integration, and comprehensive cleanup systems. Includes PlayerManager for multi-player coordination with statistics, events, background processing (save/update loops), zone management, and thread-safe operations. Database integration uses zombiezen SQLite with complete persistence for all player data. All systems are thread-safe with proper Go concurrency patterns and extensive test coverage including concurrent access testing.
**Quests System**: Complete quest management with quest definitions, step system, and real-time progress tracking. Features quest system with multiple step types (kill, chat, obtain item, location, spell, normal, craft, harvest, kill race requirements), comprehensive prerequisite system (level, class, race, faction, quest dependencies), flexible reward system (coins, experience, status points, faction reputation, items), step-based progress tracking with percentage-based success chances, task group organization for complex quests, Lua action system for completion/progress/failure events, quest sharing system with configurable sharing rules, repeatable quest support, player quest management with active quest tracking, master quest list with categorization and search, validation system for quest integrity, and thread-safe operations with proper mutex usage. Includes QuestSystemAdapter for complete quest lifecycle management and integration with player, client, spawn, and item systems.
**NPC System**: Non-player character system extending Entity with complete AI, combat, and spell casting capabilities. Features NPC struct with brain system, spell management (cast-on-spawn/aggro triggers), skill bonuses, movement with runback mechanics, appearance randomization (33+ flags for race/gender/colors/features), AI strategies (balanced/offensive/defensive), and combat state management. Includes NPCSpell configurations with HP ratio requirements, skill bonus system with spell-based modifications, movement locations with navigation pathfinding, timer system for pause/movement control, and comprehensive appearance randomization covering all EQ2 races and visual elements. Manager provides zone-based indexing, appearance tracking, combat processing, AI processing, statistics collection, and command interface. Integration interfaces support database persistence, spell/skill/appearance systems, combat management, movement control, and entity adapters for seamless system integration. Thread-safe operations with proper mutex usage and atomic flags for state management.
**NPC AI System**: Comprehensive artificial intelligence system for NPCs with hate management, encounter tracking, and specialized brain types. Features BaseBrain with complete AI logic including target selection, spell/melee processing, combat decisions, movement control, and runback mechanics. HateList provides thread-safe hate value tracking with percentage calculations and most-hated selection. EncounterList manages player/group participation for loot rights and rewards with character ID mapping. Specialized brain variants include CombatPetBrain (follows owner, assists in combat), NonCombatPetBrain (cosmetic pet following), BlankBrain (minimal processing), LuaBrain (script-controlled AI), and DumbFirePetBrain (temporary combat pets with expiration). BrainState tracks timing, spell recovery, active status, and debug levels. AIManager provides centralized brain lifecycle management with type-based creation, active brain processing, and performance statistics. Integration interfaces support NPC/Entity systems, Lua scripting, zone operations, and debugging utilities. Thread-safe operations with proper mutex usage and performance tracking for all AI operations.
**NPC AI System**: Comprehensive artificial intelligence system for NPCs with hate management, encounter tracking, and specialized brain types. Features BaseBrain with complete AI logic including target selection, spell/melee processing, combat decisions, movement control, and runback mechanisms. HateList provides thread-safe hate value tracking with percentage calculations and most-hated selection. EncounterList manages player/group participation for loot rights and rewards with character ID mapping. Specialized brain variants include CombatPetBrain (follows owner, assists in combat), NonCombatPetBrain (cosmetic pet following), BlankBrain (minimal processing), LuaBrain (script-controlled AI), and DumbFirePetBrain (temporary combat pets with expiration). BrainState tracks timing, spell recovery, active status, and debug levels. AIManager provides centralized brain lifecycle management with type-based creation, active brain processing, and performance statistics. Integration interfaces support NPC/Entity systems, Lua scripting, zone operations, and debugging utilities. Thread-safe operations with proper mutex usage and performance tracking for all AI operations.
All systems are converted from C++ with TODO comments marking areas for future implementation (LUA integration, advanced mechanics, etc.).
**Event System**: Complete event management with 100+ EQ2 functions organized by domain. Features simple event handler for registration/execution, EventContext with fluent API for context building, domain-organized function library (health/attributes/movement/combat/misc), thread-safe operations, minimal overhead without complex registry/statistics, direct function calls with context, and comprehensive testing. Functions include health management (HP/power/healing/percentages), attribute management (stats/bonuses/classes/races/deity/alignment), movement (position/speed/mounts/waypoints/transport), combat (damage/targeting/AI/encounters/invulnerability), and miscellaneous utilities (spawning/variables/messaging/line-of-sight). Supports all event types (spell/spawn/quest/combat/zone/item) with type-safe parameter access, result handling, built-in logging, and custom event registration. Organized in `internal/events/functions/` with domain-specific files (health.go, attributes.go, movement.go, combat.go, misc.go) and comprehensive registry system. Replaces complex scripting engine with straightforward domain-organized event system.
**Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility.
**Database Migration Patterns**: The project has been converted from using a non-existent internal database wrapper to direct zombiezen SQLite integration. Key patterns include:
- Using `sqlite.Conn` instead of `sql.DB` for connections
- `sqlitex.Execute` with `ExecOptions` and `ResultFunc` for queries
- Proper `found` flag handling to detect when no rows are returned
- Thread-safe operations with connection management
- Complete conversion in player system serves as reference implementation
**Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility. All systems include comprehensive test suites with concurrency testing for thread safety validation.
**Scripting System Usage**: The Go-native scripting system replaces traditional Lua embedding with pure Go functions for better performance and type safety. Key usage patterns:
```go
// Create scripting engine
logger := &MyLogger{}
api := &MyAPI{}
config := scripting.DefaultScriptConfig()
engine := scripting.NewEngine(config, api, logger)
// Register EQ2 functions
if err := scripts.RegisterEQ2Functions(engine); err != nil {
return fmt.Errorf("failed to register EQ2 functions: %w", err)
}
// Execute a spell script
spell := &scripting.ScriptSpell{ID: 123, Caster: player}
result, err := engine.ExecuteSpellScript("heal_spell", "cast", spell)
if err != nil || !result.Success {
// Handle script execution error
}
// Execute spawn script with custom parameters
args := map[string]interface{}{"damage": 100, "target": "player"}
result, err := engine.ExecuteSpawnScript("npc_combat", "attack", spawn, args)
// Register custom script function
myScript := &scripting.ScriptDefinition{
Name: "custom_spell",
Type: scripting.ScriptTypeSpell,
Functions: map[string]scripting.ScriptFunction{
"cast": func(ctx *scripting.ScriptContext) error {
// Access script context
caster := ctx.GetCaster()
target := ctx.GetTarget()
spellID := ctx.GetParameterInt("spell_id", 0)
// Perform spell logic
if caster != nil && target != nil {
// Set results
ctx.SetResult("damage_dealt", 150)
ctx.Debug("Cast spell %d from %s to %s",
spellID, caster.GetName(), target.GetName())
}
return nil
},
},
}
engine.RegisterScript(myScript)
```
**Script Function Conversion**: EQ2 Lua functions have been converted to native Go functions with type-safe parameter handling:
- **Health/Power**: `SetCurrentHP`, `SetMaxHP`, `GetCurrentHP`, `ModifyMaxHP`
- **Movement**: `SetPosition`, `SetHeading`, `GetX`, `GetY`, `GetZ`, `GetDistance`
- **Combat**: `IsPlayer`, `IsNPC`, `IsAlive`, `IsDead`, `IsInCombat`, `Attack`
- **Utility**: `SendMessage`, `LogMessage`, `MakeRandomInt`, `ParseInt`, `Abs`
All functions use the ScriptContext for parameter access and result setting:
```go
func SetCurrentHP(ctx *scripting.ScriptContext) error {
spawn := ctx.GetSpawn()
hp := ctx.GetParameterFloat("hp", 0)
// Implementation...
ctx.Debug("Set HP to %f for spawn %s", hp, spawn.GetName())
return nil
}
```
## Development Patterns and Conventions
**Package Structure**: Each system follows a consistent structure:
- `types.go`: Core data structures and type definitions
- `constants.go`: System constants, IDs, and configuration values
- `manager.go`: High-level system management and coordination
- `interfaces.go`: Integration interfaces and adapter patterns
- `database.go`: Database persistence layer (where applicable)
- `README.md`: Package-specific documentation with usage examples
**Thread Safety**: All systems implement proper Go concurrency patterns:
- `sync.RWMutex` for read-heavy operations (maps, lists)
- `atomic` operations for simple flags and counters
- Proper lock ordering to prevent deadlocks
- Channel-based communication for background processing
**Integration Patterns**:
- `*Aware` interfaces for feature detection and loose coupling
- Adapter pattern for bridging different system interfaces
- Manager pattern for centralized system coordination
- Event-driven architecture for system notifications
**Entity-Pass-By-Pointer**: Always pass `*entity.Entity` (not `entity.Entity`) to avoid copying locks and improve performance. This applies to all entity-related method signatures.
**Name Padding Handling**: Player names from database may have null byte padding from `[128]byte` arrays. Always use `strings.TrimSpace(strings.Trim(name, "\x00"))` when working with player names from database or similar fixed-size string fields.
## Code Documentation Standards

1
cmd/login_server/TODO.md Normal file
View File

@ -0,0 +1 @@
need to implement

View File

@ -1,230 +0,0 @@
package main
import (
"eq2emu/internal/common/opcodes"
"eq2emu/internal/udp"
"fmt"
"log"
"time"
)
// LoginClient represents a connected client session
type LoginClient struct {
connection *udp.Connection
server *LoginServer
account *Account
lastActivity time.Time
authenticated bool
version uint16
sessionID string
// Client state
needsWorldList bool
sentCharacterList bool
pendingPlayCharID int32
createRequest *CharacterCreateRequest
}
// Account represents an authenticated user account
type Account struct {
ID int32
Username string
LSAdmin bool
WorldAdmin bool
Characters []*Character
LastLogin time.Time
IPAddress string
ClientVersion uint16
}
// Character represents a character in the database
type Character struct {
ID int32
AccountID int32
ServerID int32
Name string
Level int8
Race int8
Class int8
Gender int8
CreatedDate time.Time
Deleted bool
}
// CharacterCreateRequest holds pending character creation data
type CharacterCreateRequest struct {
ServerID int32
Name string
Race int8
Gender int8
Class int8
Face int8
Hair int8
HairColor int8
SkinColor int8
EyeColor int8
Timestamp time.Time
}
// NewLoginClient creates a new login client instance
func NewLoginClient(conn *udp.Connection, server *LoginServer) *LoginClient {
return &LoginClient{
connection: conn,
server: server,
lastActivity: time.Now(),
sessionID: fmt.Sprintf("%d", conn.GetSessionID()),
needsWorldList: true,
sentCharacterList: false,
}
}
// ProcessPacket handles incoming packets from the client
func (lc *LoginClient) ProcessPacket(packet *udp.ApplicationPacket) {
lc.lastActivity = time.Now()
switch packet.Opcode {
case opcodes.OpLoginRequestMsg:
lc.handleLoginRequest(packet)
case opcodes.OpAllWSDescRequestMsg:
lc.handleWorldListRequest(packet)
case opcodes.OpAllCharactersDescRequestMsg:
lc.handleCharacterListRequest(packet)
case opcodes.OpCreateCharacterRequestMsg:
lc.handleCharacterCreateRequest(packet)
case opcodes.OpDeleteCharacterRequestMsg:
lc.handleCharacterDeleteRequest(packet)
case opcodes.OpPlayCharacterRequestMsg:
lc.handlePlayCharacterRequest(packet)
case opcodes.OpKeymapLoadMsg:
// Client keymap request - usually ignored
break
default:
log.Printf("Unknown packet opcode from client %s: 0x%04X", lc.sessionID, packet.Opcode)
}
}
// handleLoginRequest processes client login attempts
func (lc *LoginClient) handleLoginRequest(packet *udp.ApplicationPacket) {
lc.server.IncrementLoginAttempts()
// Parse login request packet
loginReq, err := lc.parseLoginRequest(packet.Data)
if err != nil {
log.Printf("Failed to parse login request from %s: %v", lc.sessionID, err)
lc.sendLoginDenied()
return
}
lc.version = loginReq.Version
// Check if client version is supported
if !lc.server.GetConfig().IsVersionSupported(lc.version) {
log.Printf("Unsupported client version %d from %s", lc.version, lc.sessionID)
lc.sendLoginDeniedBadVersion()
return
}
// Authenticate with database
account, err := lc.server.database.AuthenticateAccount(loginReq.Username, loginReq.Password)
if err != nil {
log.Printf("Authentication failed for %s: %v", loginReq.Username, err)
lc.sendLoginDenied()
return
}
if account == nil {
log.Printf("Invalid credentials for %s", loginReq.Username)
lc.sendLoginDenied()
return
}
// Check for existing session
lc.server.clientMutex.RLock()
for _, existingClient := range lc.server.clients {
if existingClient.account != nil && existingClient.account.ID == account.ID && existingClient != lc {
log.Printf("Account %s already logged in, disconnecting previous session", account.Username)
existingClient.Disconnect()
break
}
}
lc.server.clientMutex.RUnlock()
// Update account info
account.LastLogin = time.Now()
account.IPAddress = lc.connection.GetClientAddr().IP.String()
account.ClientVersion = lc.version
lc.server.database.UpdateAccountLogin(account)
lc.account = account
lc.authenticated = true
lc.server.IncrementSuccessfulLogins()
log.Printf("User %s successfully authenticated", account.Username)
lc.sendLoginAccepted()
}
// handleWorldListRequest sends the list of available world servers
func (lc *LoginClient) handleWorldListRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
lc.sendWorldList()
lc.needsWorldList = false
// Load and send character list
if !lc.sentCharacterList {
characters, err := lc.server.database.LoadCharacters(lc.account.ID, lc.version)
if err != nil {
log.Printf("Failed to load characters for account %d: %v", lc.account.ID, err)
} else {
lc.account.Characters = characters
lc.sentCharacterList = true
}
lc.sendCharacterList()
}
}
// handleCharacterListRequest handles explicit character list requests
func (lc *LoginClient) handleCharacterListRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
lc.sendCharacterList()
}
// IsStale returns true if the client connection should be cleaned up
func (lc *LoginClient) IsStale() bool {
return time.Since(lc.lastActivity) > 5*time.Minute
}
// Disconnect closes the client connection and cleans up
func (lc *LoginClient) Disconnect() {
if lc.connection != nil {
lc.connection.Close()
}
// Clean up any pending requests
lc.createRequest = nil
log.Printf("Client %s disconnected", lc.sessionID)
lc.server.RemoveClient(lc.sessionID)
}
// GetAccount returns the authenticated account
func (lc *LoginClient) GetAccount() *Account {
return lc.account
}
// GetVersion returns the client version
func (lc *LoginClient) GetVersion() uint16 {
return lc.version
}
// GetSessionID returns the session identifier
func (lc *LoginClient) GetSessionID() string {
return lc.sessionID
}

View File

@ -1,163 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"os"
)
// Config holds all login server configuration
type Config struct {
Port int `json:"port"`
MaxConnections int `json:"max_connections"`
TimeoutSeconds int `json:"timeout_seconds"`
EnableCompression bool `json:"enable_compression"`
EnableEncryption bool `json:"enable_encryption"`
AllowAccountCreation bool `json:"allow_account_creation"`
DefaultSubscriptionLevel uint32 `json:"default_subscription_level"`
ExpansionFlag uint16 `json:"expansion_flag"`
CitiesFlag uint8 `json:"cities_flag"`
EnabledRaces uint32 `json:"enabled_races"`
SupportedVersions []uint16 `json:"supported_versions"`
Database DatabaseConfig `json:"database"`
WebServer WebServerConfig `json:"web_server"`
WorldServers []WorldServerConfig `json:"world_servers"`
}
// WebServerConfig holds web server settings
type WebServerConfig struct {
Enabled bool `json:"enabled"`
Address string `json:"address"`
Port int `json:"port"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
KeyPassword string `json:"key_password"`
Username string `json:"username"`
Password string `json:"password"`
}
// WorldServerConfig holds world server connection info
type WorldServerConfig struct {
ID int32 `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Port int `json:"port"`
SharedKey string `json:"shared_key"`
AutoConnect bool `json:"auto_connect"`
}
// DefaultConfig returns sensible default configuration
func DefaultConfig() *Config {
return &Config{
Port: 5999,
MaxConnections: 1000,
TimeoutSeconds: 45,
EnableCompression: true,
EnableEncryption: true,
AllowAccountCreation: false,
DefaultSubscriptionLevel: 0xFFFFFFFF,
ExpansionFlag: 0x7CFF,
CitiesFlag: 0xFF,
EnabledRaces: 0xFFFF,
SupportedVersions: []uint16{283, 373, 546, 561, 1096, 1208},
Database: DatabaseConfig{
FilePath: "eq2login.db",
MaxConnections: 10,
BusyTimeout: 5000,
},
WebServer: WebServerConfig{
Enabled: true,
Address: "0.0.0.0",
Port: 8080,
},
}
}
// LoadConfig loads configuration from a JSON file
func LoadConfig(filename string) (*Config, error) {
// Start with defaults
config := DefaultConfig()
// Check if config file exists
if _, err := os.Stat(filename); os.IsNotExist(err) {
// Create default config file
if err := SaveConfig(filename, config); err != nil {
return nil, fmt.Errorf("failed to create default config: %w", err)
}
fmt.Printf("Created default configuration file: %s\n", filename)
return config, nil
}
// Read existing config file
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Parse JSON
if err := json.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("failed to parse config JSON: %w", err)
}
// Validate configuration
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
}
return config, nil
}
// SaveConfig saves configuration to a JSON file
func SaveConfig(filename string, config *Config) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// Validate checks if the configuration is valid
func (c *Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
if c.MaxConnections < 1 {
return fmt.Errorf("max_connections must be positive")
}
if c.TimeoutSeconds < 1 {
return fmt.Errorf("timeout_seconds must be positive")
}
if len(c.SupportedVersions) == 0 {
return fmt.Errorf("must specify at least one supported version")
}
return nil
}
// IsVersionSupported checks if a client version is supported
func (c *Config) IsVersionSupported(version uint16) bool {
for _, supported := range c.SupportedVersions {
if supported == version {
return true
}
}
return false
}
// GetWorldServerConfig returns configuration for a specific world server
func (c *Config) GetWorldServerConfig(id int32) *WorldServerConfig {
for i := range c.WorldServers {
if c.WorldServers[i].ID == id {
return &c.WorldServers[i]
}
}
return nil
}

View File

@ -1,325 +0,0 @@
package main
import (
"eq2emu/internal/database"
"fmt"
"log"
"time"
"golang.org/x/crypto/bcrypt"
)
// Database handles all database operations for the login server
type Database struct {
db *database.DB
}
// DatabaseConfig holds database connection settings
type DatabaseConfig struct {
FilePath string `json:"file_path"`
MaxConnections int `json:"max_connections"`
BusyTimeout int `json:"busy_timeout_ms"`
}
// NewDatabase creates a new database connection
func NewDatabase(config DatabaseConfig) (*Database, error) {
db, err := database.Open(config.FilePath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Set busy timeout if specified
if config.BusyTimeout > 0 {
query := fmt.Sprintf("PRAGMA busy_timeout = %d", config.BusyTimeout)
if err := db.Exec(query); err != nil {
return nil, fmt.Errorf("failed to set busy timeout: %w", err)
}
}
log.Println("SQLite database connection established")
return &Database{db: db}, nil
}
// Close closes the database connection
func (d *Database) Close() error {
return d.db.Close()
}
// AuthenticateAccount verifies user credentials and returns account info
func (d *Database) AuthenticateAccount(username, password string) (*Account, error) {
query := `
SELECT id, username, password_hash, ls_admin, world_admin,
created_date, last_login, client_version
FROM accounts
WHERE username = ? AND active = 1`
row, err := d.db.QueryRow(query, username)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
if row == nil {
return nil, nil // Account not found
}
defer row.Close()
var account Account
account.ID = int32(row.Int64(0))
account.Username = row.Text(1)
passwordHash := row.Text(2)
account.LSAdmin = row.Bool(3)
account.WorldAdmin = row.Bool(4)
// Skip created_date at index 5 - not needed for authentication
lastLogin := row.Text(6)
account.ClientVersion = uint16(row.Int64(7))
// Verify password
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil {
return nil, nil // Invalid password
}
// Parse timestamp
if lastLogin != "" {
if t, err := time.Parse("2006-01-02 15:04:05", lastLogin); err == nil {
account.LastLogin = t
}
}
return &account, nil
}
// UpdateAccountLogin updates account login timestamp and IP
func (d *Database) UpdateAccountLogin(account *Account) error {
query := `
UPDATE accounts
SET last_login = ?, last_ip = ?, client_version = ?
WHERE id = ?`
return d.db.Exec(query,
account.LastLogin.Format("2006-01-02 15:04:05"),
account.IPAddress,
account.ClientVersion,
account.ID,
)
}
// LoadCharacters loads all characters for an account
func (d *Database) LoadCharacters(accountID int32, version uint16) ([]*Character, error) {
query := `
SELECT id, server_id, name, level, race, gender, class,
created_date, deleted
FROM characters
WHERE account_id = ?
ORDER BY created_date ASC`
var characters []*Character
err := d.db.Query(query, func(row *database.Row) error {
char := &Character{AccountID: accountID}
char.ID = int32(row.Int64(0))
char.ServerID = int32(row.Int64(1))
char.Name = row.Text(2)
char.Level = int8(row.Int(3))
char.Race = int8(row.Int(4))
char.Gender = int8(row.Int(5))
char.Class = int8(row.Int(6))
if dateStr := row.Text(7); dateStr != "" {
if t, err := time.Parse("2006-01-02 15:04:05", dateStr); err == nil {
char.CreatedDate = t
}
}
char.Deleted = row.Bool(8)
characters = append(characters, char)
return nil
}, accountID)
return characters, err
}
// CharacterNameExists checks if a character name is already taken
func (d *Database) CharacterNameExists(name string, serverID int32) (bool, error) {
query := `
SELECT COUNT(*)
FROM characters
WHERE name = ? AND server_id = ? AND deleted = 0`
row, err := d.db.QueryRow(query, name, serverID)
if err != nil {
return false, err
}
if row == nil {
return false, nil
}
defer row.Close()
return row.Int(0) > 0, nil
}
// CreateCharacter creates a new character in the database
func (d *Database) CreateCharacter(char *Character) (int32, error) {
query := `
INSERT INTO characters (account_id, server_id, name, level, race,
gender, class, created_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
err := d.db.Exec(query,
char.AccountID, char.ServerID, char.Name, char.Level,
char.Race, char.Gender, char.Class,
char.CreatedDate.Format("2006-01-02 15:04:05"),
)
if err != nil {
return 0, fmt.Errorf("failed to create character: %w", err)
}
id := int32(d.db.LastInsertID())
log.Printf("Created character %s (ID: %d) for account %d",
char.Name, id, char.AccountID)
return id, nil
}
// DeleteCharacter marks a character as deleted
func (d *Database) DeleteCharacter(charID, accountID int32) error {
query := `
UPDATE characters
SET deleted = 1, deleted_date = CURRENT_TIMESTAMP
WHERE id = ? AND account_id = ?`
err := d.db.Exec(query, charID, accountID)
if err != nil {
return fmt.Errorf("failed to delete character: %w", err)
}
// Check if any rows were affected
if d.db.Changes() == 0 {
return fmt.Errorf("character not found or not owned by account")
}
log.Printf("Deleted character %d for account %d", charID, accountID)
return nil
}
// GetWorldServers returns all configured world servers
func (d *Database) GetWorldServers() ([]*WorldServer, error) {
query := `
SELECT id, name, description, ip_address, port, status,
population, locked, hidden, created_date
FROM world_servers
WHERE active = 1
ORDER BY sort_order, name`
var servers []*WorldServer
err := d.db.Query(query, func(row *database.Row) error {
server := &WorldServer{}
server.ID = int32(row.Int64(0))
server.Name = row.Text(1)
server.Description = row.Text(2)
server.IPAddress = row.Text(3)
server.Port = row.Int(4)
server.Status = row.Text(5)
server.Population = int32(row.Int64(6))
server.Locked = row.Bool(7)
server.Hidden = row.Bool(8)
if dateStr := row.Text(9); dateStr != "" {
if t, err := time.Parse("2006-01-02 15:04:05", dateStr); err == nil {
server.CreatedDate = t
}
}
server.Online = server.Status == "online"
server.PopulationLevel = calculatePopulationLevel(server.Population)
servers = append(servers, server)
return nil
})
return servers, err
}
// UpdateWorldServerStats updates world server statistics
func (d *Database) UpdateWorldServerStats(serverID int32, stats *WorldServerStats) error {
query := `
INSERT OR REPLACE INTO world_server_stats
(server_id, timestamp, population, zones_active, players_online, uptime_seconds)
VALUES (?, CURRENT_TIMESTAMP, ?, ?, ?, ?)`
return d.db.Exec(query,
serverID, stats.Population,
stats.ZonesActive, stats.PlayersOnline, stats.UptimeSeconds,
)
}
// CleanupOldEntries removes old log entries and statistics
func (d *Database) CleanupOldEntries() error {
queries := []string{
"DELETE FROM login_attempts WHERE timestamp < datetime('now', '-30 days')",
"DELETE FROM world_server_stats WHERE timestamp < datetime('now', '-7 days')",
"DELETE FROM client_logs WHERE timestamp < datetime('now', '-14 days')",
}
for _, query := range queries {
if err := d.db.Exec(query); err != nil {
log.Printf("Cleanup query failed: %v", err)
}
}
return nil
}
// LogLoginAttempt records a login attempt for security monitoring
func (d *Database) LogLoginAttempt(username, ipAddress string, success bool) error {
query := `
INSERT INTO login_attempts (username, ip_address, success, timestamp)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)`
return d.db.Exec(query, username, ipAddress, success)
}
// GetMaxCharsSetting returns the maximum characters per account
func (d *Database) GetMaxCharsSetting() int32 {
var maxChars int32 = 7 // Default
query := "SELECT value FROM server_settings WHERE name = 'max_characters_per_account'"
row, err := d.db.QueryRow(query)
if err != nil || row == nil {
return maxChars
}
defer row.Close()
if !row.IsNull(0) {
if val := row.Int64(0); val > 0 {
maxChars = int32(val)
}
}
return maxChars
}
// GetAccountBonus returns veteran bonus flags for an account
func (d *Database) GetAccountBonus(accountID int32) uint8 {
var bonus uint8 = 0
query := "SELECT veteran_bonus FROM accounts WHERE id = ?"
row, err := d.db.QueryRow(query, accountID)
if err != nil || row == nil {
return bonus
}
defer row.Close()
bonus = uint8(row.Int(0))
return bonus
}
// calculatePopulationLevel converts population to display level
func calculatePopulationLevel(population int32) uint8 {
switch {
case population >= 1000:
return 3 // Full
case population >= 500:
return 2 // High
case population >= 100:
return 1 // Medium
default:
return 0 // Low
}
}

View File

@ -1,536 +0,0 @@
package main
import (
"encoding/binary"
"eq2emu/internal/common/opcodes"
"eq2emu/internal/packets"
"eq2emu/internal/udp"
"fmt"
"log"
"time"
)
// LoginRequest represents parsed login request data
type LoginRequest struct {
Username string
Password string
Version uint16
}
// opcodeToPacketName maps opcodes to packet definition names
var opcodeToPacketName = map[uint16]string{
opcodes.OpLoginRequestMsg: "LS_LoginRequest",
opcodes.OpAllWSDescRequestMsg: "LS_WorldListRequest",
opcodes.OpAllCharactersDescRequestMsg: "LS_CharacterListRequest",
opcodes.OpCreateCharacterRequestMsg: "LS_CreateCharacterRequest",
opcodes.OpDeleteCharacterRequestMsg: "LS_DeleteCharacterRequest",
opcodes.OpPlayCharacterRequestMsg: "LS_PlayCharacterRequest",
}
// parsePacketWithDefinition parses packet using definitions or fails
func (lc *LoginClient) parsePacketWithDefinition(opcode uint16, data []byte) (map[string]any, error) {
packetName, exists := opcodeToPacketName[opcode]
if !exists {
return nil, fmt.Errorf("no packet name mapping for opcode 0x%04X", opcode)
}
packetDef, exists := packets.GetPacket(packetName)
if !exists {
return nil, fmt.Errorf("no packet definition found for %s", packetName)
}
// Use client version for parsing, default flags
result, err := packetDef.Parse(data, uint32(lc.version), 0)
if err != nil {
return nil, fmt.Errorf("packet parsing failed for %s: %w", packetName, err)
}
return result, nil
}
// parseLoginRequest parses the login request packet data
func (lc *LoginClient) parseLoginRequest(data []byte) (*LoginRequest, error) {
parsed, err := lc.parsePacketWithDefinition(opcodes.OpLoginRequestMsg, data)
if err != nil {
return nil, err
}
req := &LoginRequest{}
if username, ok := parsed["username"].(string); ok {
req.Username = username
}
if password, ok := parsed["password"].(string); ok {
req.Password = password
}
if version, ok := parsed["version"].(uint16); ok {
req.Version = version
}
return req, nil
}
// sendLoginDenied sends login failure response
func (lc *LoginClient) sendLoginDenied() {
data := make([]byte, 12)
data[0] = 1 // reply_code: Invalid username or password
binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF)
binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF)
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpLoginReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
// Disconnect after short delay
time.AfterFunc(1*time.Second, func() {
lc.Disconnect()
})
}
// sendLoginDeniedBadVersion sends bad version response
func (lc *LoginClient) sendLoginDeniedBadVersion() {
data := make([]byte, 12)
data[0] = 6 // reply_code: Version mismatch
binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF)
binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF)
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpLoginReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
time.AfterFunc(1*time.Second, func() {
lc.Disconnect()
})
}
// sendLoginAccepted sends successful login response
func (lc *LoginClient) sendLoginAccepted() {
config := lc.server.GetConfig()
// Build login response packet
data := make([]byte, 64) // Base size, will expand as needed
offset := 0
// Account ID
binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID))
offset += 4
// Login response code (0 = success)
data[offset] = 0
offset++
// Do not force SOGA flag
data[offset] = 1
offset++
// Subscription level
binary.LittleEndian.PutUint32(data[offset:], config.DefaultSubscriptionLevel)
offset += 4
// Race flags (enabled races)
binary.LittleEndian.PutUint32(data[offset:], 0x1FFFFF)
offset += 4
// Class flags (enabled classes)
binary.LittleEndian.PutUint32(data[offset:], 0x7FFFFFE)
offset += 4
// Username (16-bit string)
username := lc.account.Username
binary.LittleEndian.PutUint16(data[offset:], uint16(len(username)))
offset += 2
copy(data[offset:], username)
offset += len(username)
// Expansion flags
binary.LittleEndian.PutUint16(data[offset:], config.ExpansionFlag)
offset += 2
// Additional flags
data[offset] = 0xFF
data[offset+1] = 0xFF
data[offset+2] = 0xFF
offset += 3
// Class access flag
data[offset] = 0xFF
offset++
// Enabled races
binary.LittleEndian.PutUint32(data[offset:], config.EnabledRaces)
offset += 4
// Cities flag
data[offset] = config.CitiesFlag
offset++
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpLoginReplyMsg,
Data: data[:offset],
}
lc.connection.SendPacket(packet)
}
// sendWorldList sends available world servers to client
func (lc *LoginClient) sendWorldList() {
worlds := lc.server.worldList.GetActiveWorlds()
// Build world list packet
data := make([]byte, 0, 1024)
// Number of worlds
worldCount := uint8(len(worlds))
data = append(data, worldCount)
for _, world := range worlds {
// World ID
worldID := make([]byte, 4)
binary.LittleEndian.PutUint32(worldID, uint32(world.ID))
data = append(data, worldID...)
// World name (16-bit string)
nameLen := make([]byte, 2)
binary.LittleEndian.PutUint16(nameLen, uint16(len(world.Name)))
data = append(data, nameLen...)
data = append(data, []byte(world.Name)...)
// World status flags
var flags uint8
if world.Online {
flags |= 0x01
}
if world.Locked {
flags |= 0x02
}
if world.Hidden {
flags |= 0x04
}
data = append(data, flags)
// Population (0-3, where 3 = full)
data = append(data, world.PopulationLevel)
}
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpWorldListMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}
// sendCharacterList sends character list to client
func (lc *LoginClient) sendCharacterList() {
if lc.account == nil {
return
}
data := make([]byte, 0, 2048)
// Number of characters
charCount := uint8(len(lc.account.Characters))
data = append(data, charCount)
// Character data
for _, char := range lc.account.Characters {
if char.Deleted {
continue
}
// Character ID
charID := make([]byte, 4)
binary.LittleEndian.PutUint32(charID, uint32(char.ID))
data = append(data, charID...)
// Server ID
serverID := make([]byte, 4)
binary.LittleEndian.PutUint32(serverID, uint32(char.ServerID))
data = append(data, serverID...)
// Character name (16-bit string)
nameLen := make([]byte, 2)
binary.LittleEndian.PutUint16(nameLen, uint16(len(char.Name)))
data = append(data, nameLen...)
data = append(data, []byte(char.Name)...)
// Character stats
data = append(data, byte(char.Race))
data = append(data, byte(char.Gender))
data = append(data, byte(char.Class))
data = append(data, byte(char.Level))
// Creation timestamp
timestamp := make([]byte, 4)
binary.LittleEndian.PutUint32(timestamp, uint32(char.CreatedDate.Unix()))
data = append(data, timestamp...)
}
// Account info
accountID := make([]byte, 4)
binary.LittleEndian.PutUint32(accountID, uint32(lc.account.ID))
data = append(data, accountID...)
// Max characters
data = append(data, 0xFF, 0xFF, 0xFF, 0xFF) // unknown1
data = append(data, 0x00, 0x00) // unknown2
data = append(data, 0x07, 0x00, 0x00, 0x00) // max chars (7)
data = append(data, 0x00) // unknown4
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpAllCharactersDescReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}
// handleCharacterCreateRequest processes character creation
func (lc *LoginClient) handleCharacterCreateRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
parsed, err := lc.parsePacketWithDefinition(opcodes.OpCreateCharacterRequestMsg, packet.Data)
if err != nil {
log.Printf("Failed to parse character create request: %v", err)
lc.sendCharacterCreateFailed(1)
return
}
serverID, ok := parsed["server_id"].(uint32)
if !ok {
lc.sendCharacterCreateFailed(1)
return
}
name, ok := parsed["character_name"].(string)
if !ok {
lc.sendCharacterCreateFailed(1)
return
}
// Validate character name
if len(name) < 3 || len(name) > 20 {
lc.sendCharacterCreateFailed(9) // Bad name length
return
}
// Check if name is taken
exists, err := lc.server.database.CharacterNameExists(name, int32(serverID))
if err != nil {
log.Printf("Error checking character name: %v", err)
lc.sendCharacterCreateFailed(1)
return
}
if exists {
lc.sendCharacterCreateFailed(12) // Name taken
return
}
// Create character in database
char := &Character{
AccountID: lc.account.ID,
ServerID: int32(serverID),
Name: name,
Level: 1,
Race: 1, // Would be parsed from packet
Gender: 1, // Would be parsed from packet
Class: 1, // Would be parsed from packet
CreatedDate: time.Now(),
}
charID, err := lc.server.database.CreateCharacter(char)
if err != nil {
log.Printf("Error creating character: %v", err)
lc.sendCharacterCreateFailed(1)
return
}
char.ID = charID
lc.account.Characters = append(lc.account.Characters, char)
lc.sendCharacterCreateSuccess(char)
lc.sendCharacterList() // Refresh character list
}
// sendCharacterCreateSuccess sends successful character creation response
func (lc *LoginClient) sendCharacterCreateSuccess(char *Character) {
data := make([]byte, 64)
offset := 0
// Account ID
binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID))
offset += 4
// Response code (0 = success)
binary.LittleEndian.PutUint32(data[offset:], 0)
offset += 4
// Character name
nameLen := uint16(len(char.Name))
binary.LittleEndian.PutUint16(data[offset:], nameLen)
offset += 2
copy(data[offset:], char.Name)
offset += int(nameLen)
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpCreateCharacterReplyMsg,
Data: data[:offset],
}
lc.connection.SendPacket(packet)
}
// sendCharacterCreateFailed sends character creation failure response
func (lc *LoginClient) sendCharacterCreateFailed(reason uint8) {
data := make([]byte, 16)
binary.LittleEndian.PutUint32(data[0:], uint32(lc.account.ID))
data[4] = reason
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpCreateCharacterReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}
// handleCharacterDeleteRequest processes character deletion
func (lc *LoginClient) handleCharacterDeleteRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
parsed, err := lc.parsePacketWithDefinition(opcodes.OpDeleteCharacterRequestMsg, packet.Data)
if err != nil {
log.Printf("Failed to parse character delete request: %v", err)
return
}
charID, ok := parsed["character_id"].(uint32)
if !ok {
return
}
serverID, ok := parsed["server_id"].(uint32)
if !ok {
return
}
// Verify character belongs to this account
var char *Character
for _, c := range lc.account.Characters {
if c.ID == int32(charID) && c.ServerID == int32(serverID) {
char = c
break
}
}
if char == nil {
log.Printf("Account %d attempted to delete character %d that doesn't belong to them", lc.account.ID, charID)
return
}
// Mark character as deleted
err = lc.server.database.DeleteCharacter(int32(charID), lc.account.ID)
if err != nil {
log.Printf("Error deleting character: %v", err)
return
}
char.Deleted = true
// Send deletion response
data := make([]byte, 24)
data[0] = 1 // Success
binary.LittleEndian.PutUint32(data[4:], serverID)
binary.LittleEndian.PutUint32(data[8:], charID)
binary.LittleEndian.PutUint32(data[12:], uint32(lc.account.ID))
responsePacket := &udp.ApplicationPacket{
Opcode: opcodes.OpDeleteCharacterReplyMsg,
Data: data,
}
lc.connection.SendPacket(responsePacket)
lc.sendCharacterList() // Refresh character list
}
// handlePlayCharacterRequest processes character selection for gameplay
func (lc *LoginClient) handlePlayCharacterRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
parsed, err := lc.parsePacketWithDefinition(opcodes.OpPlayCharacterRequestMsg, packet.Data)
if err != nil {
log.Printf("Failed to parse play character request: %v", err)
lc.sendPlayFailed(1)
return
}
charID, ok := parsed["character_id"].(uint32)
if !ok {
lc.sendPlayFailed(1)
return
}
serverID, ok := parsed["server_id"].(uint32)
if !ok {
lc.sendPlayFailed(1)
return
}
// Find world server
world := lc.server.worldList.GetWorld(int32(serverID))
if world == nil || !world.Online {
lc.sendPlayFailed(2) // Server unavailable
return
}
// Verify character ownership
var char *Character
for _, c := range lc.account.Characters {
if c.ID == int32(charID) && c.ServerID == int32(serverID) {
char = c
break
}
}
if char == nil {
lc.sendPlayFailed(1) // Character not found
return
}
lc.pendingPlayCharID = int32(charID)
// Send play request to world server
err = lc.server.worldList.SendPlayRequest(world, lc.account.ID, int32(charID))
if err != nil {
log.Printf("Error sending play request to world server: %v", err)
lc.sendPlayFailed(1)
return
}
// World server will respond with connection details
log.Printf("Account %s requesting to play character %s on server %s",
lc.account.Username, char.Name, world.Name)
}
// sendPlayFailed sends play character failure response
func (lc *LoginClient) sendPlayFailed(reason uint8) {
data := make([]byte, 16)
data[0] = reason
binary.LittleEndian.PutUint32(data[4:], uint32(lc.account.ID))
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpPlayCharacterReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}

View File

@ -1,212 +0,0 @@
package main
import (
"eq2emu/internal/packets"
"eq2emu/internal/udp"
"fmt"
"log"
"sync"
"time"
)
// LoginServer manages the main login server functionality
type LoginServer struct {
config *Config
server *udp.Server
database *Database
worldList *WorldList
webServer *WebServer
clients map[string]*LoginClient
clientMutex sync.RWMutex
// Statistics
stats struct {
ConnectionCount int32
LoginAttempts int32
SuccessfulLogins int32
StartTime time.Time
}
}
// NewLoginServer creates a new login server instance
func NewLoginServer(config *Config) (*LoginServer, error) {
ls := &LoginServer{
config: config,
clients: make(map[string]*LoginClient),
}
ls.stats.StartTime = time.Now()
// Initialize packet definitions
log.Printf("Loaded %d packet definitions", packets.GetPacketCount())
// Initialize database
db, err := NewDatabase(config.Database)
if err != nil {
return nil, fmt.Errorf("database initialization failed: %w", err)
}
ls.database = db
// Initialize world list
ls.worldList = NewWorldList(db)
// Create UDP server with login packet handler
udpConfig := udp.DefaultConfig()
udpConfig.MaxConnections = config.MaxConnections
udpConfig.Timeout = time.Duration(config.TimeoutSeconds) * time.Second
udpConfig.EnableCompression = config.EnableCompression
udpConfig.EnableEncryption = config.EnableEncryption
server, err := udp.NewServer(fmt.Sprintf(":%d", config.Port), ls.handlePacket, udpConfig)
if err != nil {
return nil, fmt.Errorf("UDP server creation failed: %w", err)
}
ls.server = server
// Initialize web server if configured
if config.WebServer.Enabled {
webServer, err := NewWebServer(config.WebServer, ls)
if err != nil {
log.Printf("Web server initialization failed: %v", err)
} else {
ls.webServer = webServer
}
}
return ls, nil
}
// Start begins accepting connections and processing packets
func (ls *LoginServer) Start() error {
log.Println("Starting login server components...")
// Start world list monitoring
go ls.worldList.Start()
// Start web server if configured
if ls.webServer != nil {
go ls.webServer.Start()
}
// Start UDP server
return ls.server.Start()
}
// Stop gracefully shuts down the server
func (ls *LoginServer) Stop() {
log.Println("Stopping login server...")
if ls.webServer != nil {
ls.webServer.Stop()
}
ls.worldList.Stop()
ls.server.Stop()
ls.database.Close()
}
// handlePacket processes incoming packets from clients
func (ls *LoginServer) handlePacket(conn *udp.Connection, packet *udp.ApplicationPacket) {
clientKey := conn.GetSessionID()
// Get or create client
ls.clientMutex.Lock()
client, exists := ls.clients[fmt.Sprintf("%d", clientKey)]
if !exists {
client = NewLoginClient(conn, ls)
ls.clients[fmt.Sprintf("%d", clientKey)] = client
}
ls.clientMutex.Unlock()
// Process packet
client.ProcessPacket(packet)
}
// RemoveClient removes a client from the active clients list
func (ls *LoginServer) RemoveClient(sessionID string) {
ls.clientMutex.Lock()
delete(ls.clients, sessionID)
ls.clientMutex.Unlock()
}
// UpdateStats updates server statistics
func (ls *LoginServer) UpdateStats() {
ls.clientMutex.RLock()
ls.stats.ConnectionCount = int32(len(ls.clients))
ls.clientMutex.RUnlock()
// Update world server statistics
ls.worldList.UpdateStats()
// Clean up old database entries
ls.database.CleanupOldEntries()
log.Printf("Stats: %d connections, %d login attempts, %d successful logins",
ls.stats.ConnectionCount, ls.stats.LoginAttempts, ls.stats.SuccessfulLogins)
}
// CleanupStaleConnections removes inactive connections
func (ls *LoginServer) CleanupStaleConnections() {
var staleClients []string
ls.clientMutex.RLock()
for sessionID, client := range ls.clients {
if client.IsStale() {
staleClients = append(staleClients, sessionID)
}
}
ls.clientMutex.RUnlock()
ls.clientMutex.Lock()
for _, sessionID := range staleClients {
if client, exists := ls.clients[sessionID]; exists {
client.Disconnect()
delete(ls.clients, sessionID)
}
}
ls.clientMutex.Unlock()
if len(staleClients) > 0 {
log.Printf("Cleaned up %d stale connections", len(staleClients))
}
}
// GetStats returns current server statistics
func (ls *LoginServer) GetStats() map[string]any {
ls.clientMutex.RLock()
connectionCount := len(ls.clients)
ls.clientMutex.RUnlock()
return map[string]any{
"connection_count": connectionCount,
"login_attempts": ls.stats.LoginAttempts,
"successful_logins": ls.stats.SuccessfulLogins,
"uptime_seconds": int(time.Since(ls.stats.StartTime).Seconds()),
"world_server_count": ls.worldList.GetActiveCount(),
}
}
// IncrementLoginAttempts atomically increments login attempt counter
func (ls *LoginServer) IncrementLoginAttempts() {
ls.stats.LoginAttempts++
}
// IncrementSuccessfulLogins atomically increments successful login counter
func (ls *LoginServer) IncrementSuccessfulLogins() {
ls.stats.SuccessfulLogins++
}
// GetDatabase returns the database instance
func (ls *LoginServer) GetDatabase() *Database {
return ls.database
}
// GetWorldList returns the world list instance
func (ls *LoginServer) GetWorldList() *WorldList {
return ls.worldList
}
// GetConfig returns the server configuration
func (ls *LoginServer) GetConfig() *Config {
return ls.config
}

View File

@ -1,74 +0,0 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
)
var (
loginServer *LoginServer
runLoops = true
)
func main() {
// Initialize logging
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("EQ2Emulator Login Server Starting...")
// Load configuration
config, err := LoadConfig("login_config.json")
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Initialize login server
loginServer, err = NewLoginServer(config)
if err != nil {
log.Fatalf("Failed to create login server: %v", err)
}
// Setup signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start the server
go func() {
if err := loginServer.Start(); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Start maintenance routines
go startMaintenanceTimers()
log.Printf("Login server listening on port %d", config.Port)
// Wait for shutdown signal
<-sigChan
log.Println("Shutdown signal received, stopping server...")
runLoops = false
loginServer.Stop()
log.Println("Server stopped.")
}
// startMaintenanceTimers starts periodic maintenance tasks
func startMaintenanceTimers() {
statsTicker := time.NewTicker(60 * time.Second)
cleanupTicker := time.NewTicker(5 * time.Minute)
defer statsTicker.Stop()
defer cleanupTicker.Stop()
for runLoops {
select {
case <-statsTicker.C:
loginServer.UpdateStats()
case <-cleanupTicker.C:
loginServer.CleanupStaleConnections()
}
}
}

View File

@ -1,143 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// WebServer provides HTTP endpoints for monitoring
type WebServer struct {
config WebServerConfig
loginServer *LoginServer
server *http.Server
}
// NewWebServer creates a new web monitoring server
func NewWebServer(config WebServerConfig, loginServer *LoginServer) (*WebServer, error) {
ws := &WebServer{
config: config,
loginServer: loginServer,
}
mux := http.NewServeMux()
mux.HandleFunc("/status", ws.handleStatus)
mux.HandleFunc("/worlds", ws.handleWorlds)
mux.HandleFunc("/stats", ws.handleStats)
mux.HandleFunc("/health", ws.handleHealth)
ws.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", config.Address, config.Port),
Handler: ws.basicAuth(mux),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
return ws, nil
}
// Start begins the web server
func (ws *WebServer) Start() {
log.Printf("Starting web server on %s", ws.server.Addr)
var err error
if ws.config.CertFile != "" && ws.config.KeyFile != "" {
err = ws.server.ListenAndServeTLS(ws.config.CertFile, ws.config.KeyFile)
} else {
err = ws.server.ListenAndServe()
}
if err != http.ErrServerClosed {
log.Printf("Web server error: %v", err)
}
}
// Stop shuts down the web server
func (ws *WebServer) Stop() {
if ws.server != nil {
ws.server.Close()
}
}
// basicAuth provides basic HTTP authentication
func (ws *WebServer) basicAuth(next http.Handler) http.Handler {
if ws.config.Username == "" {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != ws.config.Username || password != ws.config.Password {
w.Header().Set("WWW-Authenticate", `Basic realm="Login Server"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
next.ServeHTTP(w, r)
})
}
// handleStatus returns server status information
func (ws *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) {
stats := ws.loginServer.GetStats()
status := map[string]any{
"service": "eq2emu-login-server",
"version": "1.0.0",
"status": "running",
"timestamp": time.Now().UTC(),
"statistics": stats,
}
ws.writeJSON(w, status)
}
// handleWorlds returns world server information
func (ws *WebServer) handleWorlds(w http.ResponseWriter, r *http.Request) {
worlds := ws.loginServer.worldList.GetActiveWorlds()
worldStats := ws.loginServer.worldList.GetStats()
response := map[string]any{
"world_servers": worlds,
"statistics": worldStats,
}
ws.writeJSON(w, response)
}
// handleStats returns detailed server statistics
func (ws *WebServer) handleStats(w http.ResponseWriter, r *http.Request) {
serverStats := ws.loginServer.GetStats()
worldStats := ws.loginServer.worldList.GetStats()
stats := map[string]any{
"server": serverStats,
"worlds": worldStats,
"timestamp": time.Now().UTC(),
}
ws.writeJSON(w, stats)
}
// handleHealth returns basic health check
func (ws *WebServer) handleHealth(w http.ResponseWriter, r *http.Request) {
health := map[string]any{
"status": "healthy",
"timestamp": time.Now().UTC(),
}
ws.writeJSON(w, health)
}
// writeJSON writes JSON response with proper headers
func (ws *WebServer) writeJSON(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache")
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("JSON encoding error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}

View File

@ -1,356 +0,0 @@
package main
import (
"fmt"
"log"
"sync"
"time"
)
// WorldServer represents a game world server
type WorldServer struct {
ID int32
Name string
Description string
IPAddress string
Port int
Status string
Population int32
PopulationLevel uint8
Locked bool
Hidden bool
Online bool
CreatedDate time.Time
LastUpdate time.Time
}
// WorldServerStats holds runtime statistics
type WorldServerStats struct {
Population int32
ZonesActive int32
PlayersOnline int32
UptimeSeconds int64
}
// WorldList manages all world servers
type WorldList struct {
servers map[int32]*WorldServer
mutex sync.RWMutex
database *Database
updateTicker *time.Ticker
stopChan chan struct{}
}
// NewWorldList creates a new world list manager
func NewWorldList(database *Database) *WorldList {
return &WorldList{
servers: make(map[int32]*WorldServer),
database: database,
stopChan: make(chan struct{}),
}
}
// Start begins world server monitoring
func (wl *WorldList) Start() {
log.Println("Starting world list monitoring...")
// Load world servers from database
if err := wl.LoadFromDatabase(); err != nil {
log.Printf("Failed to load world servers: %v", err)
}
// Start periodic updates
wl.updateTicker = time.NewTicker(30 * time.Second)
go func() {
for {
select {
case <-wl.updateTicker.C:
wl.UpdateStats()
case <-wl.stopChan:
return
}
}
}()
}
// Stop shuts down world server monitoring
func (wl *WorldList) Stop() {
log.Println("Stopping world list monitoring...")
if wl.updateTicker != nil {
wl.updateTicker.Stop()
}
close(wl.stopChan)
}
// LoadFromDatabase loads world servers from the database
func (wl *WorldList) LoadFromDatabase() error {
servers, err := wl.database.GetWorldServers()
if err != nil {
return fmt.Errorf("failed to load world servers: %w", err)
}
wl.mutex.Lock()
defer wl.mutex.Unlock()
// Clear existing servers
wl.servers = make(map[int32]*WorldServer)
// Add loaded servers
for _, server := range servers {
wl.servers[server.ID] = server
log.Printf("Loaded world server: %s (ID: %d)", server.Name, server.ID)
}
log.Printf("Loaded %d world servers", len(servers))
return nil
}
// GetActiveWorlds returns all online world servers
func (wl *WorldList) GetActiveWorlds() []*WorldServer {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
var active []*WorldServer
for _, server := range wl.servers {
if server.Online && !server.Hidden {
active = append(active, server)
}
}
return active
}
// GetWorld returns a specific world server by ID
func (wl *WorldList) GetWorld(id int32) *WorldServer {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
return wl.servers[id]
}
// GetActiveCount returns the number of online world servers
func (wl *WorldList) GetActiveCount() int {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
count := 0
for _, server := range wl.servers {
if server.Online {
count++
}
}
return count
}
// UpdateServerStatus updates a world server's status
func (wl *WorldList) UpdateServerStatus(id int32, online bool, population int32, locked bool) {
wl.mutex.Lock()
defer wl.mutex.Unlock()
server, exists := wl.servers[id]
if !exists {
log.Printf("Attempted to update unknown server ID: %d", id)
return
}
server.Online = online
server.Population = population
server.Locked = locked
server.LastUpdate = time.Now()
// Update population level
server.PopulationLevel = wl.calculatePopulationLevel(population)
if online {
server.Status = "online"
} else {
server.Status = "offline"
}
log.Printf("Updated server %s: online=%t, population=%d, locked=%t",
server.Name, online, population, locked)
}
// calculatePopulationLevel converts population to display level
func (wl *WorldList) calculatePopulationLevel(population int32) uint8 {
switch {
case population >= 1000:
return 3 // Full
case population >= 500:
return 2 // High
case population >= 100:
return 1 // Medium
default:
return 0 // Low
}
}
// UpdateStats updates world server statistics
func (wl *WorldList) UpdateStats() {
wl.mutex.RLock()
servers := make([]*WorldServer, 0, len(wl.servers))
for _, server := range wl.servers {
servers = append(servers, server)
}
wl.mutex.RUnlock()
// Update statistics for each server
for _, server := range servers {
if server.Online {
stats := &WorldServerStats{
Population: server.Population,
ZonesActive: 0, // Would be updated by world server
PlayersOnline: server.Population,
UptimeSeconds: int64(time.Since(server.LastUpdate).Seconds()),
}
if err := wl.database.UpdateWorldServerStats(server.ID, stats); err != nil {
log.Printf("Failed to update stats for server %d: %v", server.ID, err)
}
}
}
}
// SendPlayRequest sends a character play request to a world server
func (wl *WorldList) SendPlayRequest(world *WorldServer, accountID, charID int32) error {
// In a real implementation, this would establish communication with the world server
// and send the play request. For now, we'll simulate the response.
log.Printf("Sending play request to world server %s for account %d, character %d",
world.Name, accountID, charID)
// Simulate world server response after a short delay
go func() {
time.Sleep(100 * time.Millisecond)
// For demonstration, we'll always succeed
// In reality, the world server would validate the character and respond
accessKey := generateAccessKey()
// This would normally come from the world server's response
wl.HandlePlayResponse(world.ID, accountID, charID, true,
world.IPAddress, world.Port, accessKey)
}()
return nil
}
// HandlePlayResponse processes a play response from a world server
func (wl *WorldList) HandlePlayResponse(worldID, accountID, charID int32,
success bool, ipAddress string, port int, accessKey int32) {
// Find the client that requested this
// This is simplified - in reality you'd track pending requests
log.Printf("Play response from world %d: success=%t, access_key=%d",
worldID, success, accessKey)
// Send response to appropriate client
// This would need to be implemented with proper client tracking
}
// generateAccessKey generates a random access key for world server connections
func generateAccessKey() int32 {
return int32(time.Now().UnixNano() & 0x7FFFFFFF)
}
// AddServer adds a new world server (for dynamic registration)
func (wl *WorldList) AddServer(server *WorldServer) {
wl.mutex.Lock()
defer wl.mutex.Unlock()
wl.servers[server.ID] = server
log.Printf("Added world server: %s (ID: %d)", server.Name, server.ID)
}
// RemoveServer removes a world server
func (wl *WorldList) RemoveServer(id int32) {
wl.mutex.Lock()
defer wl.mutex.Unlock()
if server, exists := wl.servers[id]; exists {
delete(wl.servers, id)
log.Printf("Removed world server: %s (ID: %d)", server.Name, id)
}
}
// GetServerList returns a formatted server list for client packets
func (wl *WorldList) GetServerList() []byte {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
// Build server list packet data
data := make([]byte, 0, 1024)
// Count active servers
activeCount := 0
for _, server := range wl.servers {
if !server.Hidden {
activeCount++
}
}
// Add server count
data = append(data, byte(activeCount))
// Add server data
for _, server := range wl.servers {
if server.Hidden {
continue
}
// Server ID (4 bytes)
serverIDBytes := make([]byte, 4)
serverIDBytes[0] = byte(server.ID)
serverIDBytes[1] = byte(server.ID >> 8)
serverIDBytes[2] = byte(server.ID >> 16)
serverIDBytes[3] = byte(server.ID >> 24)
data = append(data, serverIDBytes...)
// Server name (null-terminated)
data = append(data, []byte(server.Name)...)
data = append(data, 0)
// Server flags
var flags byte
if server.Online {
flags |= 0x01
}
if server.Locked {
flags |= 0x02
}
data = append(data, flags)
// Population level
data = append(data, server.PopulationLevel)
}
return data
}
// GetStats returns world list statistics
func (wl *WorldList) GetStats() map[string]any {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
totalServers := len(wl.servers)
onlineServers := 0
totalPopulation := int32(0)
for _, server := range wl.servers {
if server.Online {
onlineServers++
totalPopulation += server.Population
}
}
return map[string]any{
"total_servers": totalServers,
"online_servers": onlineServers,
"offline_servers": totalServers - onlineServers,
"total_population": totalPopulation,
}
}

1
cmd/world_server/TODO.md Normal file
View File

@ -0,0 +1 @@
Need to implement

View File

@ -1,137 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
)
const (
ConfigFile = "world_config.json"
Version = "0.1.0"
)
// printHeader displays the EQ2Emu banner and copyright info
func printHeader() {
fmt.Println("EQ2Emulator World Server")
fmt.Printf("Version: %s\n", Version)
fmt.Println()
fmt.Println("Copyright (C) 2007-2026 EQ2Emulator Development Team")
fmt.Println("https://www.eq2emu.com")
fmt.Println()
fmt.Println("EQ2Emulator is free software licensed under the GNU GPL v3")
fmt.Println("See LICENSE file for details")
fmt.Println()
}
// loadConfig loads configuration from JSON file with command line overrides
func loadConfig() (*WorldConfig, error) {
// Default configuration
config := &WorldConfig{
ListenAddr: "0.0.0.0",
ListenPort: 9000,
MaxClients: 1000,
BufferSize: 8192,
WebAddr: "0.0.0.0",
WebPort: 8080,
DatabasePath: "world.db",
XPRate: 1.0,
TSXPRate: 1.0,
VitalityRate: 1.0,
LogLevel: "info",
ThreadedLoad: true,
}
// Load from config file if it exists
if data, err := os.ReadFile(ConfigFile); err == nil {
if err := json.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
log.Printf("Loaded configuration from %s", ConfigFile)
} else {
log.Printf("Config file %s not found, using defaults", ConfigFile)
}
// Command line overrides
flag.StringVar(&config.ListenAddr, "listen-addr", config.ListenAddr, "UDP listen address")
flag.IntVar(&config.ListenPort, "listen-port", config.ListenPort, "UDP listen port")
flag.IntVar(&config.MaxClients, "max-clients", config.MaxClients, "Maximum client connections")
flag.StringVar(&config.WebAddr, "web-addr", config.WebAddr, "Web server address")
flag.IntVar(&config.WebPort, "web-port", config.WebPort, "Web server port")
flag.StringVar(&config.DatabasePath, "db-path", config.DatabasePath, "Database file path")
flag.StringVar(&config.LogLevel, "log-level", config.LogLevel, "Log level (debug, info, warn, error)")
flag.BoolVar(&config.ThreadedLoad, "threaded-load", config.ThreadedLoad, "Use threaded loading")
flag.Parse()
return config, nil
}
// saveConfig saves the current configuration to file
func saveConfig(config *WorldConfig) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(ConfigFile, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// setupSignalHandlers sets up graceful shutdown on SIGINT/SIGTERM
func setupSignalHandlers(world *World) <-chan os.Signal {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigChan
log.Printf("Received signal %v, initiating graceful shutdown...", sig)
world.Shutdown()
}()
return sigChan
}
func main() {
printHeader()
// Load configuration
config, err := loadConfig()
if err != nil {
log.Fatalf("Configuration error: %v", err)
}
// Save config file with any command line overrides
if err := saveConfig(config); err != nil {
log.Printf("Warning: failed to save config: %v", err)
}
// Create world server instance
world, err := NewWorld(config)
if err != nil {
log.Fatalf("Failed to create world server: %v", err)
}
// Initialize all components
log.Println("Initializing EQ2Emulator World Server...")
if err := world.Initialize(); err != nil {
log.Fatalf("Failed to initialize world server: %v", err)
}
// Setup signal handlers for graceful shutdown
setupSignalHandlers(world)
// Run the server
log.Println("Starting World Server...")
if err := world.Run(); err != nil {
log.Fatalf("World server error: %v", err)
}
log.Println("World Server stopped gracefully")
}

View File

@ -1,298 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// setupWebServer initializes the HTTP server for admin interface
func (w *World) setupWebServer() error {
if w.config.WebPort == 0 {
return nil // Web server disabled
}
mux := http.NewServeMux()
// API endpoints
mux.HandleFunc("/api/status", w.handleStatus)
mux.HandleFunc("/api/clients", w.handleClients)
mux.HandleFunc("/api/zones", w.handleZones)
mux.HandleFunc("/api/stats", w.handleStats)
mux.HandleFunc("/api/time", w.handleWorldTime)
mux.HandleFunc("/api/shutdown", w.handleShutdown)
// Administrative endpoints
mux.HandleFunc("/api/admin/reload", w.handleReload)
mux.HandleFunc("/api/admin/broadcast", w.handleBroadcast)
mux.HandleFunc("/api/admin/kick", w.handleKickClient)
// Peer management endpoints
mux.HandleFunc("/api/peers", w.handlePeers)
mux.HandleFunc("/api/peers/sync", w.handlePeerSync)
// Console command interface
mux.HandleFunc("/api/console", w.handleConsoleCommand)
// Static health check
mux.HandleFunc("/health", w.handleHealth)
// @TODO: Add authentication middleware
// @TODO: Add rate limiting middleware
// @TODO: Add CORS middleware for browser access
// @TODO: Add TLS support with cert/key files
addr := fmt.Sprintf("%s:%d", w.config.WebAddr, w.config.WebPort)
w.webServer = &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
return nil
}
// Core API handlers
// handleHealth provides a simple health check endpoint
func (w *World) handleHealth(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "ok"})
}
// handleStatus returns comprehensive server status information
func (w *World) handleStatus(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
status := map[string]any{
"status": "running",
"uptime": time.Since(w.stats.StartTime).Seconds(),
"version": Version,
"locked": w.config.WorldLocked,
"primary": w.config.IsPrimary,
"threaded": w.config.ThreadedLoad,
"data_loaded": w.isDataLoaded(),
"world_time": w.getWorldTime(),
}
json.NewEncoder(rw).Encode(status)
}
// handleClients returns list of connected clients
func (w *World) handleClients(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
w.clientsMux.RLock()
clients := make([]*ClientInfo, 0, len(w.clients))
for _, client := range w.clients {
clients = append(clients, client)
}
w.clientsMux.RUnlock()
json.NewEncoder(rw).Encode(map[string]any{
"count": len(clients),
"clients": clients,
})
}
// handleZones returns list of zone servers
func (w *World) handleZones(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
w.zonesMux.RLock()
zones := make([]*ZoneInfo, 0, len(w.zones))
for _, zone := range w.zones {
zones = append(zones, zone)
}
w.zonesMux.RUnlock()
json.NewEncoder(rw).Encode(map[string]any{
"count": len(zones),
"zones": zones,
})
}
// handleStats returns detailed server statistics
func (w *World) handleStats(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
w.statsMux.RLock()
stats := w.stats
w.statsMux.RUnlock()
// Add UDP server stats if available
if w.udpServer != nil {
serverStats := w.udpServer.GetStats()
stats.TotalConnections = int64(serverStats.ConnectionCount)
}
json.NewEncoder(rw).Encode(stats)
}
// handleWorldTime returns current game world time
func (w *World) handleWorldTime(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(w.getWorldTime())
}
// Administrative handlers
// handleShutdown initiates graceful server shutdown
func (w *World) handleShutdown(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Add confirmation parameter
// @TODO: Add delay parameter
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "shutdown initiated"})
go func() {
time.Sleep(time.Second) // Allow response to be sent
w.Shutdown()
}()
}
// handleReload reloads game data
func (w *World) handleReload(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Implement selective reloading (items, spells, quests, etc.)
// @TODO: Add progress reporting
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "reload not implemented"})
}
// handleBroadcast sends server-wide message
func (w *World) handleBroadcast(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Parse message from request body
// @TODO: Validate message content
// @TODO: Send to all connected clients
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "broadcast not implemented"})
}
// handleKickClient disconnects a specific client
func (w *World) handleKickClient(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Parse client ID from request
// @TODO: Find and disconnect client
// @TODO: Log kick action
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "kick not implemented"})
}
// Peer management handlers
// handlePeers returns list of peer servers
func (w *World) handlePeers(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
peers := make([]map[string]any, 0)
for _, peer := range w.config.PeerServers {
peerInfo := map[string]any{
"address": peer.Address,
"port": peer.Port,
"status": "unknown", // @TODO: Implement peer status checking
}
peers = append(peers, peerInfo)
}
json.NewEncoder(rw).Encode(map[string]any{
"count": len(peers),
"peers": peers,
})
}
// handlePeerSync synchronizes data with peer servers
func (w *World) handlePeerSync(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Implement peer synchronization
// @TODO: Return sync status and results
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "peer sync not implemented"})
}
// Console command handler
// handleConsoleCommand executes administrative commands
func (w *World) handleConsoleCommand(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Parse command from request body
// @TODO: Validate command permissions
// @TODO: Execute command and return results
// @TODO: Log command execution
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "console commands not implemented"})
}
// Helper methods for web handlers
// getWorldTime returns thread-safe copy of world time
func (w *World) getWorldTime() WorldTime {
w.worldTimeMux.RLock()
defer w.worldTimeMux.RUnlock()
return w.worldTime
}
// startWebServer starts the web server in a goroutine
func (w *World) startWebServer() {
if w.webServer == nil {
return
}
go func() {
if err := w.webServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Web server error: %v\n", err)
}
}()
}
// stopWebServer gracefully stops the web server
func (w *World) stopWebServer() error {
if w.webServer == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return w.webServer.Shutdown(ctx)
}

View File

@ -1,830 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"sync"
"time"
"eq2emu/internal/database"
"eq2emu/internal/udp"
)
// WorldTime represents the in-game time
type WorldTime struct {
Year int32 `json:"year"`
Month int32 `json:"month"`
Day int32 `json:"day"`
Hour int32 `json:"hour"`
Minute int32 `json:"minute"`
}
// WorldConfig holds all world server configuration
type WorldConfig struct {
// Network settings
ListenAddr string `json:"listen_addr"`
ListenPort int `json:"listen_port"`
MaxClients int `json:"max_clients"`
BufferSize int `json:"buffer_size"`
// Web server settings
WebAddr string `json:"web_addr"`
WebPort int `json:"web_port"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
KeyPassword string `json:"key_password"`
WebUser string `json:"web_user"`
WebPassword string `json:"web_password"`
// Database settings
DatabasePath string `json:"database_path"`
// Game settings
XPRate float64 `json:"xp_rate"`
TSXPRate float64 `json:"ts_xp_rate"`
VitalityRate float64 `json:"vitality_rate"`
// Server settings
LogLevel string `json:"log_level"`
ThreadedLoad bool `json:"threaded_load"`
WorldLocked bool `json:"world_locked"`
IsPrimary bool `json:"is_primary"`
// Login server settings
LoginServers []LoginServerInfo `json:"login_servers"`
// Peer server settings
PeerServers []PeerServerInfo `json:"peer_servers"`
PeerPriority int `json:"peer_priority"`
}
// LoginServerInfo represents login server connection details
type LoginServerInfo struct {
Address string `json:"address"`
Port int `json:"port"`
Account string `json:"account"`
Password string `json:"password"`
}
// PeerServerInfo represents peer server connection details
type PeerServerInfo struct {
Address string `json:"address"`
Port int `json:"port"`
}
// ClientInfo represents a connected client
type ClientInfo struct {
ID int32 `json:"id"`
AccountID int32 `json:"account_id"`
CharacterID int32 `json:"character_id"`
Name string `json:"name"`
ZoneID int32 `json:"zone_id"`
ConnectedAt time.Time `json:"connected_at"`
LastActive time.Time `json:"last_active"`
IPAddress string `json:"ip_address"`
}
// ZoneInfo represents zone server information
type ZoneInfo struct {
ID int32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PlayerCount int32 `json:"player_count"`
MaxPlayers int32 `json:"max_players"`
IsShutdown bool `json:"is_shutdown"`
Address string `json:"address"`
Port int `json:"port"`
}
// ServerStats holds server statistics
type ServerStats struct {
StartTime time.Time `json:"start_time"`
ClientCount int32 `json:"client_count"`
ZoneCount int32 `json:"zone_count"`
TotalConnections int64 `json:"total_connections"`
PacketsProcessed int64 `json:"packets_processed"`
DataLoaded bool `json:"data_loaded"`
ItemsLoaded bool `json:"items_loaded"`
SpellsLoaded bool `json:"spells_loaded"`
QuestsLoaded bool `json:"quests_loaded"`
}
// World represents the main world server
type World struct {
config *WorldConfig
db *database.DB
// Network components
udpServer *udp.Server
webServer *http.Server
// Game state
worldTime WorldTime
worldTimeMux sync.RWMutex
// Client management
clients map[int32]*ClientInfo
clientsMux sync.RWMutex
// Zone management
zones map[int32]*ZoneInfo
zonesMux sync.RWMutex
// Statistics
stats ServerStats
statsMux sync.RWMutex
// Control
ctx context.Context
cancel context.CancelFunc
shutdownWg *sync.WaitGroup
// Timers
timeTickTimer *time.Ticker
saveTimer *time.Ticker
vitalityTimer *time.Ticker
statsTimer *time.Ticker
watchdogTimer *time.Ticker
loginCheckTimer *time.Ticker
// Loading state
loadingMux sync.RWMutex
itemsLoaded bool
spellsLoaded bool
questsLoaded bool
traitsLoaded bool
dataLoaded bool
}
// NewWorld creates a new world server instance
func NewWorld(config *WorldConfig) (*World, error) {
ctx, cancel := context.WithCancel(context.Background())
db, err := database.Open(config.DatabasePath)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to open database: %w", err)
}
w := &World{
config: config,
db: db,
ctx: ctx,
cancel: cancel,
shutdownWg: &sync.WaitGroup{},
clients: make(map[int32]*ClientInfo),
zones: make(map[int32]*ZoneInfo),
stats: ServerStats{
StartTime: time.Now(),
},
}
// Initialize world time from database
if err := w.loadWorldTime(); err != nil {
log.Printf("Warning: failed to load world time: %v", err)
w.setDefaultWorldTime()
}
return w, nil
}
// Initialize sets up all world server components
func (w *World) Initialize() error {
log.Println("Loading System Data...")
// Initialize database schema
if err := w.initializeDatabase(); err != nil {
return fmt.Errorf("database initialization failed: %w", err)
}
// Load game data (threaded or sequential)
if w.config.ThreadedLoad {
log.Println("Using threaded loading of static data...")
if err := w.loadGameDataThreaded(); err != nil {
return fmt.Errorf("threaded game data loading failed: %w", err)
}
} else {
if err := w.loadGameData(); err != nil {
return fmt.Errorf("game data loading failed: %w", err)
}
}
// Setup UDP server for game connections
if err := w.setupUDPServer(); err != nil {
return fmt.Errorf("UDP server setup failed: %w", err)
}
// Setup web server for admin/API
if err := w.setupWebServer(); err != nil {
return fmt.Errorf("web server setup failed: %w", err)
}
// Initialize timers
w.initializeTimers()
log.Println("World Server initialization complete")
return nil
}
// Run starts the world server main loop
func (w *World) Run() error {
// Start background processes
w.shutdownWg.Add(6)
go w.processTimeUpdates()
go w.processSaveOperations()
go w.processVitalityUpdates()
go w.processStatsUpdates()
go w.processWatchdog()
go w.processLoginCheck()
// Start network servers
if w.udpServer != nil {
go func() {
if err := w.udpServer.Start(); err != nil {
log.Printf("UDP server error: %v", err)
}
}()
}
// Start web server
w.startWebServer()
log.Printf("World Server running on UDP %s:%d, Web %s:%d",
w.config.ListenAddr, w.config.ListenPort,
w.config.WebAddr, w.config.WebPort)
// Wait for shutdown signal
<-w.ctx.Done()
return w.shutdown()
}
// Shutdown gracefully stops the world server
func (w *World) Shutdown() {
log.Println("Initiating World Server shutdown...")
w.cancel()
}
// setupUDPServer initializes the UDP server for game client connections
func (w *World) setupUDPServer() error {
handler := func(conn *udp.Connection, packet *udp.ApplicationPacket) {
w.handleGamePacket(conn, packet)
}
config := udp.DefaultConfig()
config.MaxConnections = w.config.MaxClients
config.BufferSize = w.config.BufferSize
config.EnableCompression = true
config.EnableEncryption = true
addr := fmt.Sprintf("%s:%d", w.config.ListenAddr, w.config.ListenPort)
server, err := udp.NewServer(addr, handler, config)
if err != nil {
return err
}
w.udpServer = server
return nil
}
// initializeTimers sets up all periodic timers
func (w *World) initializeTimers() {
w.timeTickTimer = time.NewTicker(5 * time.Second) // Game time updates
w.saveTimer = time.NewTicker(5 * time.Minute) // Save operations
w.vitalityTimer = time.NewTicker(1 * time.Hour) // Vitality updates
w.statsTimer = time.NewTicker(1 * time.Minute) // Statistics updates
w.watchdogTimer = time.NewTicker(30 * time.Second) // Watchdog checks
w.loginCheckTimer = time.NewTicker(30 * time.Second) // Login server check
}
// Background processes
// processTimeUpdates handles game world time progression
func (w *World) processTimeUpdates() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.timeTickTimer.C:
w.updateWorldTime()
}
}
}
// processSaveOperations handles periodic save operations
func (w *World) processSaveOperations() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.saveTimer.C:
w.saveWorldState()
}
}
}
// processVitalityUpdates handles vitality system updates
func (w *World) processVitalityUpdates() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.vitalityTimer.C:
w.updateVitality()
}
}
}
// processStatsUpdates handles statistics collection
func (w *World) processStatsUpdates() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.statsTimer.C:
w.updateStatistics()
}
}
}
// processWatchdog handles connection timeouts and cleanup
func (w *World) processWatchdog() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.watchdogTimer.C:
w.cleanupInactiveClients()
w.cleanupTimeoutConnections()
}
}
}
// processLoginCheck handles login server connectivity
func (w *World) processLoginCheck() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.loginCheckTimer.C:
w.checkLoginServers()
}
}
}
// Game packet handling
func (w *World) handleGamePacket(conn *udp.Connection, packet *udp.ApplicationPacket) {
// Update connection activity
w.updateConnectionActivity(conn)
// Route packet based on opcode
switch packet.Opcode {
case 0x2000: // Login request
w.handleLoginRequest(conn, packet)
case 0x0020: // Zone change request
w.handleZoneChange(conn, packet)
case 0x0080: // Client command
w.handleClientCommand(conn, packet)
case 0x01F0: // Chat message
w.handleChatMessage(conn, packet)
default:
// @TODO: Implement comprehensive packet routing
log.Printf("Unhandled packet opcode: 0x%04X, size: %d", packet.Opcode, len(packet.Data))
}
// Update packet statistics
w.statsMux.Lock()
w.stats.PacketsProcessed++
w.statsMux.Unlock()
}
// Game packet handlers
func (w *World) handleLoginRequest(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse login request packet
// @TODO: Validate credentials with login server
// @TODO: Create client session
// @TODO: Send login response
log.Printf("Login request from connection %d", conn.GetSessionID())
}
func (w *World) handleZoneChange(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse zone change request
// @TODO: Validate zone transfer
// @TODO: Coordinate with zone servers
// @TODO: Send zone change response
log.Printf("Zone change request from connection %d", conn.GetSessionID())
}
func (w *World) handleClientCommand(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse client command packet
// @TODO: Process administrative commands
// @TODO: Route to appropriate handlers
log.Printf("Client command from connection %d", conn.GetSessionID())
}
func (w *World) handleChatMessage(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse chat message packet
// @TODO: Handle channel routing
// @TODO: Apply filters and permissions
// @TODO: Broadcast to appropriate recipients
log.Printf("Chat message from connection %d", conn.GetSessionID())
}
// Game state management
func (w *World) updateWorldTime() {
w.worldTimeMux.Lock()
defer w.worldTimeMux.Unlock()
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 {
w.worldTime.Day = 0
w.worldTime.Month++
if w.worldTime.Month >= 12 {
w.worldTime.Month = 0
w.worldTime.Year++
}
}
}
}
// @TODO: Broadcast time update to all zones/clients
// @TODO: Save time to database periodically
}
func (w *World) saveWorldState() {
// @TODO: Save world time to database
// @TODO: Save player data
// @TODO: Save guild data
// @TODO: Save zone states
// @TODO: Save server statistics
log.Println("Saving world state...")
}
func (w *World) updateVitality() {
// @TODO: Update player vitality for offline/resting players
// @TODO: Broadcast vitality updates to zones
// @TODO: Apply vitality bonuses
log.Println("Updating vitality...")
}
func (w *World) updateStatistics() {
w.statsMux.Lock()
defer w.statsMux.Unlock()
// Update client count
w.clientsMux.RLock()
w.stats.ClientCount = int32(len(w.clients))
w.clientsMux.RUnlock()
// Update zone count
w.zonesMux.RLock()
w.stats.ZoneCount = int32(len(w.zones))
w.zonesMux.RUnlock()
// Update loading status
w.loadingMux.RLock()
w.stats.DataLoaded = w.dataLoaded
w.stats.ItemsLoaded = w.itemsLoaded
w.stats.SpellsLoaded = w.spellsLoaded
w.stats.QuestsLoaded = w.questsLoaded
w.loadingMux.RUnlock()
}
func (w *World) cleanupInactiveClients() {
w.clientsMux.Lock()
defer w.clientsMux.Unlock()
timeout := time.Now().Add(-5 * time.Minute)
for id, client := range w.clients {
if client.LastActive.Before(timeout) {
log.Printf("Removing inactive client %d (%s)", id, client.Name)
delete(w.clients, id)
}
}
}
func (w *World) cleanupTimeoutConnections() {
// @TODO: Clean up timed out UDP connections
// @TODO: Update connection statistics
}
func (w *World) checkLoginServers() {
// @TODO: Check connectivity to login servers
// @TODO: Attempt reconnection if disconnected
// @TODO: Update server status
}
func (w *World) updateConnectionActivity(conn *udp.Connection) {
sessionID := conn.GetSessionID()
w.clientsMux.Lock()
if client, exists := w.clients[int32(sessionID)]; exists {
client.LastActive = time.Now()
}
w.clientsMux.Unlock()
}
// Database operations
func (w *World) initializeDatabase() error {
// @TODO: Create/update database schema tables
// @TODO: Initialize character tables
// @TODO: Initialize guild tables
// @TODO: Initialize item tables
// @TODO: Initialize zone tables
log.Println("Database schema initialized")
return nil
}
func (w *World) loadGameData() error {
log.Println("Loading game data sequentially...")
// Load items
log.Println("Loading items...")
if err := w.loadItems(); err != nil {
return fmt.Errorf("failed to load items: %w", err)
}
// Load spells
log.Println("Loading spells...")
if err := w.loadSpells(); err != nil {
return fmt.Errorf("failed to load spells: %w", err)
}
// Load quests
log.Println("Loading quests...")
if err := w.loadQuests(); err != nil {
return fmt.Errorf("failed to load quests: %w", err)
}
// Load additional data
if err := w.loadTraits(); err != nil {
return fmt.Errorf("failed to load traits: %w", err)
}
if err := w.loadNPCs(); err != nil {
return fmt.Errorf("failed to load NPCs: %w", err)
}
if err := w.loadZones(); err != nil {
return fmt.Errorf("failed to load zones: %w", err)
}
w.loadingMux.Lock()
w.dataLoaded = true
w.loadingMux.Unlock()
log.Println("Game data loading complete")
return nil
}
func (w *World) loadGameDataThreaded() error {
log.Println("Loading game data with threads...")
var wg sync.WaitGroup
errChan := make(chan error, 10)
// Load items in thread
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Loading items...")
if err := w.loadItems(); err != nil {
errChan <- fmt.Errorf("failed to load items: %w", err)
return
}
w.loadingMux.Lock()
w.itemsLoaded = true
w.loadingMux.Unlock()
log.Println("Items loaded")
}()
// Load spells in thread
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Loading spells...")
if err := w.loadSpells(); err != nil {
errChan <- fmt.Errorf("failed to load spells: %w", err)
return
}
w.loadingMux.Lock()
w.spellsLoaded = true
w.loadingMux.Unlock()
log.Println("Spells loaded")
}()
// Load quests in thread
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Loading quests...")
if err := w.loadQuests(); err != nil {
errChan <- fmt.Errorf("failed to load quests: %w", err)
return
}
w.loadingMux.Lock()
w.questsLoaded = true
w.loadingMux.Unlock()
log.Println("Quests loaded")
}()
// Wait for completion
go func() {
wg.Wait()
close(errChan)
}()
// Check for errors
for err := range errChan {
if err != nil {
return err
}
}
// Load additional data sequentially
if err := w.loadTraits(); err != nil {
return fmt.Errorf("failed to load traits: %w", err)
}
if err := w.loadNPCs(); err != nil {
return fmt.Errorf("failed to load NPCs: %w", err)
}
if err := w.loadZones(); err != nil {
return fmt.Errorf("failed to load zones: %w", err)
}
// Wait for threaded loads to complete
for !w.isDataLoaded() {
time.Sleep(100 * time.Millisecond)
}
w.loadingMux.Lock()
w.dataLoaded = true
w.loadingMux.Unlock()
log.Println("Threaded game data loading complete")
return nil
}
// Data loading functions
func (w *World) loadItems() error {
// @TODO: Load items from database
// @TODO: Build item lookup tables
// @TODO: Load item templates
// @TODO: Initialize item factories
return nil
}
func (w *World) loadSpells() error {
// @TODO: Load spells from database
// @TODO: Build spell lookup tables
// @TODO: Load spell effects
// @TODO: Initialize spell system
return nil
}
func (w *World) loadQuests() error {
// @TODO: Load quests from database
// @TODO: Build quest lookup tables
// @TODO: Load quest rewards
// @TODO: Initialize quest system
return nil
}
func (w *World) loadTraits() error {
// @TODO: Load traits from database
// @TODO: Build trait trees
// @TODO: Initialize trait system
return nil
}
func (w *World) loadNPCs() error {
// @TODO: Load NPCs from database
// @TODO: Load NPC templates
// @TODO: Load NPC spawn data
return nil
}
func (w *World) loadZones() error {
// @TODO: Load zone definitions
// @TODO: Load zone spawn points
// @TODO: Initialize zone management
return nil
}
func (w *World) loadWorldTime() error {
// @TODO: Load world time from database
w.worldTime = WorldTime{
Year: 3800,
Month: 0,
Day: 0,
Hour: 8,
Minute: 30,
}
return nil
}
func (w *World) setDefaultWorldTime() {
w.worldTimeMux.Lock()
defer w.worldTimeMux.Unlock()
w.worldTime = WorldTime{
Year: 3800,
Month: 0,
Day: 0,
Hour: 8,
Minute: 30,
}
}
func (w *World) isDataLoaded() bool {
w.loadingMux.RLock()
defer w.loadingMux.RUnlock()
if w.config.ThreadedLoad {
return w.itemsLoaded && w.spellsLoaded && w.questsLoaded && w.traitsLoaded
}
return w.dataLoaded
}
// Cleanup and shutdown
func (w *World) shutdown() error {
log.Println("Shutting down World Server...")
// Stop timers
if w.timeTickTimer != nil {
w.timeTickTimer.Stop()
}
if w.saveTimer != nil {
w.saveTimer.Stop()
}
if w.vitalityTimer != nil {
w.vitalityTimer.Stop()
}
if w.statsTimer != nil {
w.statsTimer.Stop()
}
if w.watchdogTimer != nil {
w.watchdogTimer.Stop()
}
if w.loginCheckTimer != nil {
w.loginCheckTimer.Stop()
}
// Stop network servers
if err := w.stopWebServer(); err != nil {
log.Printf("Error stopping web server: %v", err)
}
if w.udpServer != nil {
w.udpServer.Stop()
}
// Wait for background processes
w.shutdownWg.Wait()
// Save final state
w.saveWorldState()
// Close database
if w.db != nil {
w.db.Close()
}
log.Println("World Server shutdown complete")
return nil
}

View File

@ -693,6 +693,63 @@ func (e *Entity) GetLevel() int8 {
return int8(e.infoStruct.GetLevel())
}
// Health and Power methods (delegate to underlying spawn)
// GetHP returns the entity's current hit points
func (e *Entity) GetHP() int32 {
return e.Spawn.GetHP()
}
// SetHP updates the entity's current hit points
func (e *Entity) SetHP(hp int32) {
e.Spawn.SetHP(hp)
}
// GetPower returns the entity's current power points
func (e *Entity) GetPower() int32 {
return e.Spawn.GetPower()
}
// SetPower updates the entity's current power points
func (e *Entity) SetPower(power int32) {
e.Spawn.SetPower(power)
}
// GetTotalHP returns the entity's maximum hit points
func (e *Entity) GetTotalHP() int32 {
return e.Spawn.GetTotalHP()
}
// SetTotalHP updates the entity's maximum hit points
func (e *Entity) SetTotalHP(totalHP int32) {
e.Spawn.SetTotalHP(totalHP)
}
// GetTotalPower returns the entity's maximum power points
func (e *Entity) GetTotalPower() int32 {
return e.Spawn.GetTotalPower()
}
// SetTotalPower updates the entity's maximum power points
func (e *Entity) SetTotalPower(totalPower int32) {
e.Spawn.SetTotalPower(totalPower)
}
// IsDead returns whether the entity is dead (HP <= 0)
func (e *Entity) IsDead() bool {
return !e.Spawn.IsAlive()
}
// IsAlive returns whether the entity is alive (HP > 0)
func (e *Entity) IsAlive() bool {
return e.Spawn.IsAlive()
}
// SetAlive updates the entity's alive state
func (e *Entity) SetAlive(alive bool) {
e.Spawn.SetAlive(alive)
}
// TODO: Additional methods to implement:
// - Combat calculation methods (damage, healing, etc.)
// - Equipment bonus application methods

182
internal/events/README.md Normal file
View File

@ -0,0 +1,182 @@
# EQ2Go Event System
A simplified event-driven system for handling game logic without the complexity of a full scripting engine.
## Overview
The event system provides:
- Simple event registration and execution
- Context-based parameter passing
- 100+ built-in EQ2 game functions organized by domain
- Thread-safe operations
- Minimal overhead
- Domain-specific function organization
## Basic Usage
```go
// Create event handler
handler := NewEventHandler()
// Register all EQ2 functions (100+ functions organized by domain)
err := functions.RegisterAllEQ2Functions(handler)
// Create context and execute event
ctx := NewEventContext(EventTypeSpawn, "SetCurrentHP", "heal_spell").
WithSpawn(player).
WithParameter("hp", 150.0)
err = handler.Execute(ctx)
```
## Event Context
The `EventContext` provides:
- Game objects: `Caster`, `Target`, `Spawn`, `Quest`
- Parameters: Type-safe parameter access
- Results: Return values from event functions
- Logging: Built-in debug/info/warn/error logging
### Fluent API
```go
ctx := NewEventContext(EventTypeSpell, "heal", "cast").
WithCaster(caster).
WithTarget(target).
WithParameter("spell_id", 123).
WithParameter("power_cost", 50)
```
### Parameter Access
```go
func MyEvent(ctx *EventContext) error {
spellID := ctx.GetParameterInt("spell_id", 0)
message := ctx.GetParameterString("message", "default")
amount := ctx.GetParameterFloat("amount", 0.0)
enabled := ctx.GetParameterBool("enabled", false)
// Set results
ctx.SetResult("damage_dealt", 150)
return nil
}
```
## Available EQ2 Functions
The system provides 100+ functions organized by domain:
### Health Domain (23 functions)
- **HP Management**: `SetCurrentHP`, `SetMaxHP`, `SetMaxHPBase`, `GetCurrentHP`, `GetMaxHP`, `GetMaxHPBase`
- **Power Management**: `SetCurrentPower`, `SetMaxPower`, `SetMaxPowerBase`, `GetCurrentPower`, `GetMaxPower`, `GetMaxPowerBase`
- **Modifiers**: `ModifyHP`, `ModifyPower`, `ModifyMaxHP`, `ModifyMaxPower`, `ModifyTotalHP`, `ModifyTotalPower`
- **Percentages**: `GetPCTOfHP`, `GetPCTOfPower`
- **Healing**: `SpellHeal`, `SpellHealPct`
- **State**: `IsAlive`
### Attributes Domain (24 functions)
- **Stats**: `SetInt`, `SetWis`, `SetSta`, `SetStr`, `SetAgi`, `GetInt`, `GetWis`, `GetSta`, `GetStr`, `GetAgi`
- **Base Stats**: `SetIntBase`, `SetWisBase`, `SetStaBase`, `SetStrBase`, `SetAgiBase`, `GetIntBase`, `GetWisBase`, `GetStaBase`, `GetStrBase`, `GetAgiBase`
- **Character Info**: `GetLevel`, `SetLevel`, `SetPlayerLevel`, `GetDifficulty`, `GetClass`, `SetClass`, `SetAdventureClass`
- **Classes**: `GetTradeskillClass`, `SetTradeskillClass`, `GetTradeskillLevel`, `SetTradeskillLevel`
- **Identity**: `GetRace`, `GetGender`, `GetModelType`, `SetModelType`, `GetDeity`, `SetDeity`, `GetAlignment`, `SetAlignment`
- **Bonuses**: `AddSpellBonus`, `RemoveSpellBonus`, `AddSkillBonus`, `RemoveSkillBonus`
### Movement Domain (27 functions)
- **Position**: `SetPosition`, `GetPosition`, `GetX`, `GetY`, `GetZ`, `GetHeading`, `SetHeading`
- **Original Position**: `GetOrigX`, `GetOrigY`, `GetOrigZ`
- **Distance & Facing**: `GetDistance`, `FaceTarget`
- **Speed**: `GetSpeed`, `SetSpeed`, `SetSpeedMultiplier`, `HasMoved`, `IsRunning`
- **Movement**: `MoveToLocation`, `ClearRunningLocations`, `SpawnMove`, `MovementLoopAdd`, `PauseMovement`, `StopMovement`
- **Mounts**: `SetMount`, `GetMount`, `SetMountColor`, `StartAutoMount`, `EndAutoMount`, `IsOnAutoMount`
- **Waypoints**: `AddWaypoint`, `RemoveWaypoint`, `SendWaypoints`
- **Transport**: `Evac`, `Bind`, `Gate`
### Combat Domain (36 functions)
- **Basic Combat**: `Attack`, `AddHate`, `ClearHate`, `GetMostHated`, `SetTarget`, `GetTarget`
- **Combat State**: `IsInCombat`, `SetInCombat`, `IsCasting`, `HasRecovered`
- **Damage**: `SpellDamage`, `SpellDamageExt`, `DamageSpawn`, `ProcDamage`, `ProcHate`
- **Effects**: `Knockback`, `Interrupt`
- **Processing**: `ProcessMelee`, `ProcessSpell`, `LastSpellAttackHit`
- **Positioning**: `IsBehind`, `IsFlanking`, `InFront`
- **Encounters**: `GetEncounterSize`, `GetEncounter`, `GetHateList`, `ClearEncounter`
- **AI**: `ClearRunback`, `Runback`, `GetRunbackDistance`, `CompareSpawns`
- **Life/Death**: `KillSpawn`, `KillSpawnByDistance`, `Resurrect`
- **Invulnerability**: `IsInvulnerable`, `SetInvulnerable`, `SetAttackable`
### Miscellaneous Domain (27 functions)
- **Messaging**: `SendMessage`, `LogMessage`
- **Utility**: `MakeRandomInt`, `MakeRandomFloat`, `ParseInt`
- **Identity**: `GetName`, `GetID`, `GetSpawnID`, `IsPlayer`, `IsNPC`, `IsEntity`, `IsDead`, `GetCharacterID`
- **Spawning**: `Despawn`, `Spawn`, `SpawnByLocationID`, `SpawnGroupByID`, `DespawnByLocationID`
- **Groups**: `GetSpawnByLocationID`, `GetSpawnByGroupID`, `GetSpawnGroupID`, `SetSpawnGroupID`, `AddSpawnToGroup`, `IsSpawnGroupAlive`
- **Location**: `GetSpawnLocationID`, `GetSpawnLocationPlacementID`, `SetGridID`
- **Spawn Management**: `SpawnSet`, `SpawnSetByDistance`
- **Variables**: `GetVariableValue`, `SetServerVariable`, `GetServerVariable`, `SetTempVariable`, `GetTempVariable`
- **Line of Sight**: `CheckLOS`, `CheckLOSByCoordinates`
## Function Organization
Access functions by domain using the `functions` package:
```go
import "eq2emu/internal/events/functions"
// Register all functions at once
handler := events.NewEventHandler()
err := functions.RegisterAllEQ2Functions(handler)
// Get functions organized by domain
domains := functions.GetFunctionsByDomain()
healthFunctions := domains["health"] // 23 functions
combatFunctions := domains["combat"] // 36 functions
movementFunctions := domains["movement"] // 27 functions
// ... etc
```
## Custom Events
```go
// Register custom event
handler.Register("my_custom_event", func(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn provided")
}
// Custom logic here
ctx.Debug("Custom event executed for %s", spawn.GetName())
return nil
})
// Execute custom event
ctx := events.NewEventContext(events.EventTypeSpawn, "my_custom_event", "trigger").
WithSpawn(someSpawn)
err := handler.Execute(ctx)
```
## Event Types
- `EventTypeSpell` - Spell-related events
- `EventTypeSpawn` - Spawn-related events
- `EventTypeQuest` - Quest-related events
- `EventTypeCombat` - Combat-related events
- `EventTypeZone` - Zone-related events
- `EventTypeItem` - Item-related events
## Thread Safety
All operations are thread-safe:
- Event registration/unregistration
- Context parameter/result access
- Event execution
## Performance
The event system is designed for minimal overhead:
- No complex registry or statistics
- Direct function calls
- Simple context passing
- Optional timeout support

193
internal/events/context.go Normal file
View File

@ -0,0 +1,193 @@
package events
import (
"context"
"fmt"
"eq2emu/internal/entity"
"eq2emu/internal/quests"
"eq2emu/internal/spawn"
)
// NewEventContext creates a new event context
func NewEventContext(eventType EventType, eventName, functionName string) *EventContext {
return &EventContext{
Context: context.Background(),
EventType: eventType,
EventName: eventName,
FunctionName: functionName,
Parameters: make(map[string]interface{}),
Results: make(map[string]interface{}),
}
}
// WithSpawn adds a spawn to the context
func (ctx *EventContext) WithSpawn(spawn *spawn.Spawn) *EventContext {
ctx.Spawn = spawn
return ctx
}
// WithCaster adds a caster to the context
func (ctx *EventContext) WithCaster(caster *entity.Entity) *EventContext {
ctx.Caster = caster
return ctx
}
// WithTarget adds a target to the context
func (ctx *EventContext) WithTarget(target *entity.Entity) *EventContext {
ctx.Target = target
return ctx
}
// WithQuest adds a quest to the context
func (ctx *EventContext) WithQuest(quest *quests.Quest) *EventContext {
ctx.Quest = quest
return ctx
}
// WithParameter adds a parameter to the context
func (ctx *EventContext) WithParameter(name string, value interface{}) *EventContext {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
ctx.Parameters[name] = value
return ctx
}
// WithParameters adds multiple parameters to the context
func (ctx *EventContext) WithParameters(params map[string]interface{}) *EventContext {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
for k, v := range params {
ctx.Parameters[k] = v
}
return ctx
}
// GetSpawn returns the spawn from context
func (ctx *EventContext) GetSpawn() *spawn.Spawn {
return ctx.Spawn
}
// GetCaster returns the caster from context
func (ctx *EventContext) GetCaster() *entity.Entity {
return ctx.Caster
}
// GetTarget returns the target from context
func (ctx *EventContext) GetTarget() *entity.Entity {
return ctx.Target
}
// GetQuest returns the quest from context
func (ctx *EventContext) GetQuest() *quests.Quest {
return ctx.Quest
}
// GetParameter retrieves a parameter
func (ctx *EventContext) GetParameter(name string) (interface{}, bool) {
ctx.mutex.RLock()
defer ctx.mutex.RUnlock()
value, exists := ctx.Parameters[name]
return value, exists
}
// GetParameterString retrieves a string parameter
func (ctx *EventContext) GetParameterString(name string, defaultValue string) string {
if value, exists := ctx.GetParameter(name); exists {
if str, ok := value.(string); ok {
return str
}
}
return defaultValue
}
// GetParameterInt retrieves an integer parameter
func (ctx *EventContext) GetParameterInt(name string, defaultValue int) int {
if value, exists := ctx.GetParameter(name); exists {
switch v := value.(type) {
case int:
return v
case int32:
return int(v)
case int64:
return int(v)
case float32:
return int(v)
case float64:
return int(v)
}
}
return defaultValue
}
// GetParameterFloat retrieves a float parameter
func (ctx *EventContext) GetParameterFloat(name string, defaultValue float64) float64 {
if value, exists := ctx.GetParameter(name); exists {
switch v := value.(type) {
case float64:
return v
case float32:
return float64(v)
case int:
return float64(v)
case int32:
return float64(v)
case int64:
return float64(v)
}
}
return defaultValue
}
// GetParameterBool retrieves a boolean parameter
func (ctx *EventContext) GetParameterBool(name string, defaultValue bool) bool {
if value, exists := ctx.GetParameter(name); exists {
if b, ok := value.(bool); ok {
return b
}
}
return defaultValue
}
// SetResult sets a result value
func (ctx *EventContext) SetResult(name string, value interface{}) {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
if ctx.Results == nil {
ctx.Results = make(map[string]interface{})
}
ctx.Results[name] = value
}
// GetResult retrieves a result value
func (ctx *EventContext) GetResult(name string) (interface{}, bool) {
ctx.mutex.RLock()
defer ctx.mutex.RUnlock()
value, exists := ctx.Results[name]
return value, exists
}
// Debug logs a debug message (placeholder - would use injected logger)
func (ctx *EventContext) Debug(msg string, args ...interface{}) {
fmt.Printf("[DEBUG] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...))
}
// Info logs an info message (placeholder - would use injected logger)
func (ctx *EventContext) Info(msg string, args ...interface{}) {
fmt.Printf("[INFO] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...))
}
// Warn logs a warning message (placeholder - would use injected logger)
func (ctx *EventContext) Warn(msg string, args ...interface{}) {
fmt.Printf("[WARN] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...))
}
// Error logs an error message (placeholder - would use injected logger)
func (ctx *EventContext) Error(msg string, args ...interface{}) {
fmt.Printf("[ERROR] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...))
}

View File

@ -0,0 +1,531 @@
package functions
import (
"fmt"
"eq2emu/internal/events"
)
// Attribute and Stats Management Functions
// SetInt sets the spawn's Intelligence attribute
func SetInt(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
// TODO: Implement INT stat when InfoStruct is available
ctx.Debug("Set INT to %d for spawn %s (not yet implemented)", value, spawn.GetName())
ctx.SetResult("int", value)
return nil
}
// SetWis sets the spawn's Wisdom attribute
func SetWis(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
// TODO: Implement WIS stat when InfoStruct is available
ctx.Debug("Set WIS to %d for spawn %s (not yet implemented)", value, spawn.GetName())
ctx.SetResult("wis", value)
return nil
}
// SetSta sets the spawn's Stamina attribute
func SetSta(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
// TODO: Implement STA stat when InfoStruct is available
ctx.Debug("Set STA to %d for spawn %s (not yet implemented)", value, spawn.GetName())
ctx.SetResult("sta", value)
return nil
}
// SetStr sets the spawn's Strength attribute
func SetStr(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
// TODO: Implement STR stat when InfoStruct is available
ctx.Debug("Set STR to %d for spawn %s (not yet implemented)", value, spawn.GetName())
ctx.SetResult("str", value)
return nil
}
// SetAgi sets the spawn's Agility attribute
func SetAgi(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
// TODO: Implement AGI stat when InfoStruct is available
ctx.Debug("Set AGI to %d for spawn %s (not yet implemented)", value, spawn.GetName())
ctx.SetResult("agi", value)
return nil
}
// SetIntBase sets the spawn's base Intelligence (before bonuses)
func SetIntBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return SetInt(ctx) // Fallback for now
}
// SetWisBase sets the spawn's base Wisdom (before bonuses)
func SetWisBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return SetWis(ctx) // Fallback for now
}
// SetStaBase sets the spawn's base Stamina (before bonuses)
func SetStaBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return SetSta(ctx) // Fallback for now
}
// SetStrBase sets the spawn's base Strength (before bonuses)
func SetStrBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return SetStr(ctx) // Fallback for now
}
// SetAgiBase sets the spawn's base Agility (before bonuses)
func SetAgiBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return SetAgi(ctx) // Fallback for now
}
// GetInt gets the spawn's Intelligence attribute
func GetInt(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement INT stat retrieval when InfoStruct is available
ctx.SetResult("int", 10) // Default value
return nil
}
// GetWis gets the spawn's Wisdom attribute
func GetWis(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement WIS stat retrieval when InfoStruct is available
ctx.SetResult("wis", 10) // Default value
return nil
}
// GetSta gets the spawn's Stamina attribute
func GetSta(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement STA stat retrieval when InfoStruct is available
ctx.SetResult("sta", 10) // Default value
return nil
}
// GetStr gets the spawn's Strength attribute
func GetStr(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement STR stat retrieval when InfoStruct is available
ctx.SetResult("str", 10) // Default value
return nil
}
// GetAgi gets the spawn's Agility attribute
func GetAgi(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement AGI stat retrieval when InfoStruct is available
ctx.SetResult("agi", 10) // Default value
return nil
}
// GetIntBase gets the spawn's base Intelligence (before bonuses)
func GetIntBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return GetInt(ctx) // Fallback for now
}
// GetWisBase gets the spawn's base Wisdom (before bonuses)
func GetWisBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return GetWis(ctx) // Fallback for now
}
// GetStaBase gets the spawn's base Stamina (before bonuses)
func GetStaBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return GetSta(ctx) // Fallback for now
}
// GetStrBase gets the spawn's base Strength (before bonuses)
func GetStrBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return GetStr(ctx) // Fallback for now
}
// GetAgiBase gets the spawn's base Agility (before bonuses)
func GetAgiBase(ctx *events.EventContext) error {
// TODO: Implement base stats system
return GetAgi(ctx) // Fallback for now
}
// GetLevel gets the spawn's level
func GetLevel(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("level", spawn.GetLevel())
return nil
}
// SetLevel sets the spawn's level
func SetLevel(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
level := ctx.GetParameterInt("level", 1)
if level < 1 {
level = 1
}
if level > 100 {
level = 100
}
spawn.SetLevel(int16(level))
ctx.Debug("Set level to %d for spawn %s", level, spawn.GetName())
return nil
}
// SetPlayerLevel sets the player's level (with additional processing)
func SetPlayerLevel(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Add player-specific level processing (skill updates, etc.)
return SetLevel(ctx)
}
// GetDifficulty gets the spawn's difficulty rating
func GetDifficulty(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement difficulty calculation based on level, class, etc.
difficulty := int(spawn.GetLevel()) // Simple implementation for now
ctx.SetResult("difficulty", difficulty)
return nil
}
// AddSpellBonus adds a spell bonus to the spawn
func AddSpellBonus(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
bonusType := ctx.GetParameterInt("bonus_type", 0)
value := ctx.GetParameterFloat("value", 0)
spellID := ctx.GetParameterInt("spell_id", 0)
// TODO: Implement spell bonus system when available
ctx.Debug("Added spell bonus (type: %d, value: %f, spell: %d) to spawn %s (not yet implemented)",
bonusType, value, spellID, spawn.GetName())
return nil
}
// RemoveSpellBonus removes a spell bonus from the spawn
func RemoveSpellBonus(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
bonusType := ctx.GetParameterInt("bonus_type", 0)
spellID := ctx.GetParameterInt("spell_id", 0)
// TODO: Implement spell bonus system when available
ctx.Debug("Removed spell bonus (type: %d, spell: %d) from spawn %s (not yet implemented)",
bonusType, spellID, spawn.GetName())
return nil
}
// AddSkillBonus adds a skill bonus to the spawn
func AddSkillBonus(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
skillType := ctx.GetParameterInt("skill_type", 0)
value := ctx.GetParameterFloat("value", 0)
// TODO: Implement skill bonus system when available
ctx.Debug("Added skill bonus (type: %d, value: %f) to spawn %s (not yet implemented)",
skillType, value, spawn.GetName())
return nil
}
// RemoveSkillBonus removes a skill bonus from the spawn
func RemoveSkillBonus(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
skillType := ctx.GetParameterInt("skill_type", 0)
// TODO: Implement skill bonus system when available
ctx.Debug("Removed skill bonus (type: %d) from spawn %s (not yet implemented)",
skillType, spawn.GetName())
return nil
}
// GetClass gets the spawn's class
func GetClass(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("class", spawn.GetClass())
return nil
}
// SetClass sets the spawn's class
func SetClass(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
class := ctx.GetParameterInt("class", 0)
spawn.SetClass(int8(class))
ctx.Debug("Set class to %d for spawn %s", class, spawn.GetName())
return nil
}
// SetAdventureClass sets the spawn's adventure class
func SetAdventureClass(ctx *events.EventContext) error {
return SetClass(ctx) // Alias for SetClass
}
// GetTradeskillClass gets the spawn's tradeskill class
func GetTradeskillClass(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("tradeskill_class", spawn.GetTradeskillClass())
return nil
}
// SetTradeskillClass sets the spawn's tradeskill class
func SetTradeskillClass(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
tsClass := ctx.GetParameterInt("tradeskill_class", 0)
spawn.SetTradeskillClass(int8(tsClass))
ctx.Debug("Set tradeskill class to %d for spawn %s", tsClass, spawn.GetName())
return nil
}
// GetTradeskillLevel gets the spawn's tradeskill level
func GetTradeskillLevel(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement tradeskill level when available
ctx.SetResult("tradeskill_level", 1) // Default value
return nil
}
// SetTradeskillLevel sets the spawn's tradeskill level
func SetTradeskillLevel(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
level := ctx.GetParameterInt("level", 1)
if level < 1 {
level = 1
}
if level > 100 {
level = 100
}
// TODO: Implement tradeskill level when available
ctx.Debug("Set tradeskill level to %d for spawn %s (not yet implemented)", level, spawn.GetName())
return nil
}
// GetRace gets the spawn's race
func GetRace(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("race", spawn.GetRace())
return nil
}
// GetGender gets the spawn's gender
func GetGender(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("gender", spawn.GetGender())
return nil
}
// GetModelType gets the spawn's model type
func GetModelType(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement model type when available
ctx.SetResult("model_type", 0) // Default value
return nil
}
// SetModelType sets the spawn's model type
func SetModelType(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
modelType := ctx.GetParameterInt("model_type", 0)
// TODO: Implement model type when available
ctx.Debug("Set model type to %d for spawn %s (not yet implemented)", modelType, spawn.GetName())
return nil
}
// GetDeity gets the spawn's deity
func GetDeity(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement deity system when available
ctx.SetResult("deity", 0) // Default value
return nil
}
// SetDeity sets the spawn's deity
func SetDeity(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
deity := ctx.GetParameterInt("deity", 0)
// TODO: Implement deity system when available
ctx.Debug("Set deity to %d for spawn %s (not yet implemented)", deity, spawn.GetName())
return nil
}
// GetAlignment gets the spawn's alignment
func GetAlignment(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement alignment system when available
ctx.SetResult("alignment", 0) // Default value (neutral)
return nil
}
// SetAlignment sets the spawn's alignment
func SetAlignment(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
alignment := ctx.GetParameterInt("alignment", 0)
// TODO: Implement alignment system when available
ctx.Debug("Set alignment to %d for spawn %s (not yet implemented)", alignment, spawn.GetName())
return nil
}

View File

@ -0,0 +1,611 @@
package functions
import (
"fmt"
"eq2emu/internal/events"
)
// Combat and AI Functions
// Attack makes the spawn attack a target
func Attack(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement attack system
ctx.Debug("Spawn %s attacking target %s (not yet implemented)", spawn.GetName(), target.GetName())
return nil
}
// AddHate adds hate/threat to the spawn's hate list
func AddHate(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
hateAmount := ctx.GetParameterFloat("hate", 0)
// TODO: Implement hate/threat system
ctx.Debug("Added %f hate from %s to %s (not yet implemented)", hateAmount, spawn.GetName(), target.GetName())
return nil
}
// ClearHate clears the spawn's hate list
func ClearHate(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement hate clearing
ctx.Debug("Cleared hate list for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// GetMostHated gets the most hated target
func GetMostHated(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement hate list management
// Return nil for now (no most hated)
ctx.SetResult("most_hated", nil)
return nil
}
// SetTarget sets the spawn's target
func SetTarget(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement target setting
if target != nil {
ctx.Debug("Set target for %s to %s (not yet implemented)", spawn.GetName(), target.GetName())
} else {
ctx.Debug("Cleared target for %s (not yet implemented)", spawn.GetName())
}
return nil
}
// GetTarget gets the spawn's current target
func GetTarget(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement target retrieval
ctx.SetResult("target", nil) // No target for now
return nil
}
// IsInCombat checks if the spawn is in combat
func IsInCombat(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement combat state tracking
ctx.SetResult("in_combat", false)
return nil
}
// SetInCombat sets the spawn's combat state
func SetInCombat(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
inCombat := ctx.GetParameterBool("in_combat", false)
// TODO: Implement combat state setting
ctx.Debug("Set combat state to %t for spawn %s (not yet implemented)", inCombat, spawn.GetName())
return nil
}
// SpellDamage deals spell damage to a target
func SpellDamage(ctx *events.EventContext) error {
caster := ctx.GetCaster()
target := ctx.GetTarget()
if caster == nil {
return fmt.Errorf("no caster in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
damage := ctx.GetParameterFloat("damage", 0)
damageType := ctx.GetParameterInt("damage_type", 0)
if damage <= 0 {
return fmt.Errorf("damage must be positive")
}
// TODO: Implement spell damage system with damage types, resistances, etc.
ctx.Debug("Spell damage %f (type %d) from %s to %s (not yet implemented)",
damage, damageType, caster.GetName(), target.GetName())
ctx.SetResult("damage_dealt", damage)
return nil
}
// SpellDamageExt deals extended spell damage with more options
func SpellDamageExt(ctx *events.EventContext) error {
caster := ctx.GetCaster()
target := ctx.GetTarget()
if caster == nil {
return fmt.Errorf("no caster in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
damage := ctx.GetParameterFloat("damage", 0)
damageType := ctx.GetParameterInt("damage_type", 0)
hitType := ctx.GetParameterInt("hit_type", 0)
spellID := ctx.GetParameterInt("spell_id", 0)
if damage <= 0 {
return fmt.Errorf("damage must be positive")
}
// TODO: Implement extended spell damage system
ctx.Debug("Extended spell damage %f (type %d, hit %d, spell %d) from %s to %s (not yet implemented)",
damage, damageType, hitType, spellID, caster.GetName(), target.GetName())
ctx.SetResult("damage_dealt", damage)
return nil
}
// DamageSpawn deals direct damage to a spawn
func DamageSpawn(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
damage := ctx.GetParameterFloat("damage", 0)
if damage <= 0 {
return fmt.Errorf("damage must be positive")
}
// Apply damage to HP
currentHP := float64(spawn.GetHP())
newHP := currentHP - damage
if newHP < 0 {
newHP = 0
}
spawn.SetHP(int32(newHP))
ctx.SetResult("damage_dealt", damage)
ctx.Debug("Dealt %f damage to spawn %s (new HP: %f)", damage, spawn.GetName(), newHP)
// Update alive state if necessary
if newHP <= 0 && spawn.IsAlive() {
spawn.SetAlive(false)
ctx.Debug("Spawn %s died from damage", spawn.GetName())
}
return nil
}
// ProcDamage handles proc-based damage
func ProcDamage(ctx *events.EventContext) error {
caster := ctx.GetCaster()
target := ctx.GetTarget()
if caster == nil {
return fmt.Errorf("no caster in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
damage := ctx.GetParameterFloat("damage", 0)
damageType := ctx.GetParameterInt("damage_type", 0)
if damage <= 0 {
return fmt.Errorf("damage must be positive")
}
// TODO: Implement proc damage system
ctx.Debug("Proc damage %f (type %d) from %s to %s (not yet implemented)",
damage, damageType, caster.GetName(), target.GetName())
ctx.SetResult("damage_dealt", damage)
return nil
}
// ProcHate handles proc-based hate generation
func ProcHate(ctx *events.EventContext) error {
caster := ctx.GetCaster()
target := ctx.GetTarget()
if caster == nil {
return fmt.Errorf("no caster in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
hateAmount := ctx.GetParameterFloat("hate", 0)
// TODO: Implement proc hate system
ctx.Debug("Proc hate %f from %s to %s (not yet implemented)", hateAmount, caster.GetName(), target.GetName())
return nil
}
// Knockback applies knockback effect to target
func Knockback(ctx *events.EventContext) error {
caster := ctx.GetCaster()
target := ctx.GetTarget()
if caster == nil {
return fmt.Errorf("no caster in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
distance := ctx.GetParameterFloat("distance", 5.0)
verticalLift := ctx.GetParameterFloat("vertical", 0.0)
// TODO: Implement knockback system
ctx.Debug("Knockback target %s distance %f with vertical %f from %s (not yet implemented)",
target.GetName(), distance, verticalLift, caster.GetName())
return nil
}
// Interrupt interrupts the target's spell casting
func Interrupt(ctx *events.EventContext) error {
target := ctx.GetTarget()
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement interrupt system
ctx.Debug("Interrupted spell casting for %s (not yet implemented)", target.GetName())
return nil
}
// IsCasting checks if the spawn is currently casting
func IsCasting(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement casting state tracking
ctx.SetResult("is_casting", false)
return nil
}
// HasRecovered checks if the spawn has recovered from an action
func HasRecovered(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement recovery tracking
ctx.SetResult("has_recovered", true)
return nil
}
// ProcessMelee processes melee combat
func ProcessMelee(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement melee processing
ctx.Debug("Processing melee for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// ProcessSpell processes spell casting
func ProcessSpell(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement spell processing
ctx.Debug("Processing spells for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// LastSpellAttackHit checks if last spell attack hit
func LastSpellAttackHit(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement spell hit tracking
ctx.SetResult("last_spell_hit", false)
return nil
}
// IsBehind checks if spawn is behind target
func IsBehind(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement position-based behind check
ctx.SetResult("is_behind", false)
return nil
}
// IsFlanking checks if spawn is flanking target
func IsFlanking(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement position-based flanking check
ctx.SetResult("is_flanking", false)
return nil
}
// InFront checks if spawn is in front of target
func InFront(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement position-based front check
ctx.SetResult("in_front", false)
return nil
}
// GetEncounterSize gets the size of the current encounter
func GetEncounterSize(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement encounter tracking
ctx.SetResult("encounter_size", 0)
return nil
}
// GetEncounter gets the current encounter list
func GetEncounter(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement encounter retrieval
ctx.SetResult("encounter", []interface{}{}) // Empty list for now
return nil
}
// GetHateList gets the spawn's hate list
func GetHateList(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement hate list retrieval
ctx.SetResult("hate_list", []interface{}{}) // Empty list for now
return nil
}
// ClearEncounter clears the current encounter
func ClearEncounter(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement encounter clearing
ctx.Debug("Cleared encounter for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// ClearRunback clears runback behavior
func ClearRunback(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement runback clearing
ctx.Debug("Cleared runback for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// Runback initiates runback behavior
func Runback(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement runback behavior
ctx.Debug("Initiated runback for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// GetRunbackDistance gets the runback distance
func GetRunbackDistance(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement runback distance calculation
ctx.SetResult("runback_distance", 50.0) // Default runback distance
return nil
}
// CompareSpawns compares two spawns (for AI decision making)
func CompareSpawns(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement spawn comparison logic
ctx.SetResult("comparison_result", 0) // Equal
return nil
}
// KillSpawn instantly kills a spawn
func KillSpawn(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
spawn.SetHP(0)
spawn.SetAlive(false)
ctx.Debug("Killed spawn %s", spawn.GetName())
return nil
}
// KillSpawnByDistance kills spawns within a distance
func KillSpawnByDistance(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
distance := ctx.GetParameterFloat("distance", 10.0)
// TODO: Implement distance-based killing
ctx.Debug("Killed spawns within distance %f of %s (not yet implemented)", distance, spawn.GetName())
return nil
}
// Resurrect resurrects a dead spawn
func Resurrect(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
hpPercent := ctx.GetParameterFloat("hp_percent", 100.0)
powerPercent := ctx.GetParameterFloat("power_percent", 100.0)
// Restore HP and power
maxHP := float64(spawn.GetTotalHP())
maxPower := float64(spawn.GetTotalPower())
newHP := maxHP * (hpPercent / 100.0)
newPower := maxPower * (powerPercent / 100.0)
spawn.SetHP(int32(newHP))
spawn.SetPower(int32(newPower))
spawn.SetAlive(true)
ctx.Debug("Resurrected spawn %s with %.1f%% HP and %.1f%% power",
spawn.GetName(), hpPercent, powerPercent)
return nil
}
// IsInvulnerable checks if spawn is invulnerable
func IsInvulnerable(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement invulnerability system
ctx.SetResult("is_invulnerable", false)
return nil
}
// SetInvulnerable sets spawn's invulnerability
func SetInvulnerable(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
invulnerable := ctx.GetParameterBool("invulnerable", false)
// TODO: Implement invulnerability system
ctx.Debug("Set invulnerable to %t for spawn %s (not yet implemented)", invulnerable, spawn.GetName())
return nil
}
// SetAttackable sets whether the spawn can be attacked
func SetAttackable(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
attackable := ctx.GetParameterBool("attackable", true)
// TODO: Implement attackable flag
ctx.Debug("Set attackable to %t for spawn %s (not yet implemented)", attackable, spawn.GetName())
return nil
}

View File

@ -0,0 +1,254 @@
package functions
import (
"testing"
"eq2emu/internal/events"
"eq2emu/internal/spawn"
)
func TestAllEQ2Functions(t *testing.T) {
// Create event handler
handler := events.NewEventHandler()
// Register all EQ2 functions
err := RegisterAllEQ2Functions(handler)
if err != nil {
t.Fatalf("Failed to register all EQ2 functions: %v", err)
}
// Verify we have a substantial number of functions registered
events := handler.ListEvents()
if len(events) < 100 {
t.Errorf("Expected at least 100 functions, got %d", len(events))
}
// Test some key functions exist
requiredFunctions := []string{
"SetCurrentHP", "GetCurrentHP", "SetMaxHP", "GetMaxHP",
"SetLevel", "GetLevel", "SetClass", "GetClass",
"SetPosition", "GetPosition", "GetX", "GetY", "GetZ",
"Attack", "AddHate", "SpellDamage", "IsInCombat",
"GetName", "GetID", "IsPlayer", "IsNPC",
"MakeRandomInt", "ParseInt", "LogMessage",
}
for _, funcName := range requiredFunctions {
if !handler.HasEvent(funcName) {
t.Errorf("Required function %s not registered", funcName)
}
}
}
func TestHealthFunctions(t *testing.T) {
handler := events.NewEventHandler()
err := RegisterAllEQ2Functions(handler)
if err != nil {
t.Fatalf("Failed to register functions: %v", err)
}
// Create test spawn
testSpawn := &spawn.Spawn{}
// Test SetCurrentHP
ctx := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test").
WithSpawn(testSpawn).
WithParameter("hp", 250.0)
err = handler.Execute(ctx)
if err != nil {
t.Fatalf("SetCurrentHP failed: %v", err)
}
// Test GetCurrentHP
ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetCurrentHP", "test").
WithSpawn(testSpawn)
err = handler.Execute(ctx2)
if err != nil {
t.Fatalf("GetCurrentHP failed: %v", err)
}
// Verify result
if hp, exists := ctx2.GetResult("hp"); !exists {
t.Error("GetCurrentHP should return hp result")
} else if hp != int32(250) {
t.Errorf("Expected HP 250, got %v", hp)
}
}
func TestAttributeFunctions(t *testing.T) {
handler := events.NewEventHandler()
err := RegisterAllEQ2Functions(handler)
if err != nil {
t.Fatalf("Failed to register functions: %v", err)
}
// Create test spawn
testSpawn := &spawn.Spawn{}
// Test SetLevel
ctx := events.NewEventContext(events.EventTypeSpawn, "SetLevel", "test").
WithSpawn(testSpawn).
WithParameter("level", 50)
err = handler.Execute(ctx)
if err != nil {
t.Fatalf("SetLevel failed: %v", err)
}
// Test GetLevel
ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetLevel", "test").
WithSpawn(testSpawn)
err = handler.Execute(ctx2)
if err != nil {
t.Fatalf("GetLevel failed: %v", err)
}
// Verify result
if level, exists := ctx2.GetResult("level"); !exists {
t.Error("GetLevel should return level result")
} else if level != int16(50) {
t.Errorf("Expected level 50, got %v", level)
}
}
func TestMovementFunctions(t *testing.T) {
handler := events.NewEventHandler()
err := RegisterAllEQ2Functions(handler)
if err != nil {
t.Fatalf("Failed to register functions: %v", err)
}
// Create test spawn
testSpawn := &spawn.Spawn{}
// Test SetPosition
ctx := events.NewEventContext(events.EventTypeSpawn, "SetPosition", "test").
WithSpawn(testSpawn).
WithParameter("x", 100.0).
WithParameter("y", 200.0).
WithParameter("z", 300.0).
WithParameter("heading", 180.0)
err = handler.Execute(ctx)
if err != nil {
t.Fatalf("SetPosition failed: %v", err)
}
// Test GetPosition
ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetPosition", "test").
WithSpawn(testSpawn)
err = handler.Execute(ctx2)
if err != nil {
t.Fatalf("GetPosition failed: %v", err)
}
// Verify results
if x, exists := ctx2.GetResult("x"); !exists || x != float32(100.0) {
t.Errorf("Expected X=100.0, got %v (exists: %t)", x, exists)
}
if y, exists := ctx2.GetResult("y"); !exists || y != float32(200.0) {
t.Errorf("Expected Y=200.0, got %v (exists: %t)", y, exists)
}
if z, exists := ctx2.GetResult("z"); !exists || z != float32(300.0) {
t.Errorf("Expected Z=300.0, got %v (exists: %t)", z, exists)
}
}
func TestMiscFunctions(t *testing.T) {
handler := events.NewEventHandler()
err := RegisterAllEQ2Functions(handler)
if err != nil {
t.Fatalf("Failed to register functions: %v", err)
}
// Create test spawn
testSpawn := &spawn.Spawn{}
// Test GetName
ctx := events.NewEventContext(events.EventTypeSpawn, "GetName", "test").
WithSpawn(testSpawn)
err = handler.Execute(ctx)
if err != nil {
t.Fatalf("GetName failed: %v", err)
}
// Test MakeRandomInt
ctx2 := events.NewEventContext(events.EventTypeSpawn, "MakeRandomInt", "test").
WithParameter("min", 10).
WithParameter("max", 20)
err = handler.Execute(ctx2)
if err != nil {
t.Fatalf("MakeRandomInt failed: %v", err)
}
if result, exists := ctx2.GetResult("random_int"); !exists {
t.Error("MakeRandomInt should return random_int result")
} else if randInt, ok := result.(int); !ok || randInt < 10 || randInt > 20 {
t.Errorf("Expected random int between 10-20, got %v", result)
}
}
func TestFunctionsByDomain(t *testing.T) {
domains := GetFunctionsByDomain()
// Verify we have expected domains
expectedDomains := []string{"health", "attributes", "movement", "combat", "misc"}
for _, domain := range expectedDomains {
if functions, exists := domains[domain]; !exists {
t.Errorf("Domain %s not found", domain)
} else if len(functions) == 0 {
t.Errorf("Domain %s has no functions", domain)
}
}
// Verify health domain has expected functions
healthFunctions := domains["health"]
expectedHealthFunctions := []string{"SetCurrentHP", "GetCurrentHP", "SetMaxHP", "GetMaxHP"}
for _, funcName := range expectedHealthFunctions {
found := false
for _, f := range healthFunctions {
if f == funcName {
found = true
break
}
}
if !found {
t.Errorf("Health domain missing function %s", funcName)
}
}
}
func TestErrorHandling(t *testing.T) {
handler := events.NewEventHandler()
err := RegisterAllEQ2Functions(handler)
if err != nil {
t.Fatalf("Failed to register functions: %v", err)
}
// Test function with no spawn context
ctx := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test").
WithParameter("hp", 100.0)
// No spawn set
err = handler.Execute(ctx)
if err == nil {
t.Error("SetCurrentHP should fail without spawn context")
}
// Test function with invalid parameters
testSpawn := &spawn.Spawn{}
ctx2 := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test").
WithSpawn(testSpawn).
WithParameter("hp", -50.0) // Negative HP
err = handler.Execute(ctx2)
if err == nil {
t.Error("SetCurrentHP should fail with negative HP")
}
}

View File

@ -0,0 +1,365 @@
package functions
import (
"fmt"
"eq2emu/internal/events"
)
// Health and Power Management Functions
// SetCurrentHP sets the spawn's current HP
func SetCurrentHP(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
hp := ctx.GetParameterFloat("hp", 0)
if hp < 0 {
return fmt.Errorf("HP cannot be negative")
}
spawn.SetHP(int32(hp))
ctx.Debug("Set HP to %f for spawn %s", hp, spawn.GetName())
return nil
}
// SetMaxHP sets the spawn's maximum HP
func SetMaxHP(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
maxHP := ctx.GetParameterFloat("max_hp", 0)
if maxHP < 0 {
return fmt.Errorf("Max HP cannot be negative")
}
spawn.SetTotalHP(int32(maxHP))
ctx.Debug("Set Max HP to %f for spawn %s", maxHP, spawn.GetName())
return nil
}
// SetMaxHPBase sets the spawn's base maximum HP (before bonuses)
func SetMaxHPBase(ctx *events.EventContext) error {
// TODO: Implement base HP system when available
return SetMaxHP(ctx) // Fallback to regular max HP for now
}
// SetCurrentPower sets the spawn's current power
func SetCurrentPower(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
power := ctx.GetParameterFloat("power", 0)
if power < 0 {
return fmt.Errorf("power cannot be negative")
}
spawn.SetPower(int32(power))
ctx.Debug("Set Power to %f for spawn %s", power, spawn.GetName())
return nil
}
// SetMaxPower sets the spawn's maximum power
func SetMaxPower(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
maxPower := ctx.GetParameterFloat("max_power", 0)
if maxPower < 0 {
return fmt.Errorf("Max power cannot be negative")
}
spawn.SetTotalPower(int32(maxPower))
ctx.Debug("Set Max Power to %f for spawn %s", maxPower, spawn.GetName())
return nil
}
// SetMaxPowerBase sets the spawn's base maximum power (before bonuses)
func SetMaxPowerBase(ctx *events.EventContext) error {
// TODO: Implement base power system when available
return SetMaxPower(ctx) // Fallback to regular max power for now
}
// ModifyMaxHP modifies the spawn's maximum HP by a relative amount
func ModifyMaxHP(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
modifier := ctx.GetParameterFloat("modifier", 0)
currentMax := float64(spawn.GetTotalHP())
newMax := currentMax + modifier
if newMax < 0 {
newMax = 0
}
spawn.SetTotalHP(int32(newMax))
ctx.Debug("Modified Max HP by %f (new value: %f) for spawn %s", modifier, newMax, spawn.GetName())
return nil
}
// ModifyMaxPower modifies the spawn's maximum power by a relative amount
func ModifyMaxPower(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
modifier := ctx.GetParameterFloat("modifier", 0)
currentMax := float64(spawn.GetTotalPower())
newMax := currentMax + modifier
if newMax < 0 {
newMax = 0
}
spawn.SetTotalPower(int32(newMax))
ctx.Debug("Modified Max Power by %f (new value: %f) for spawn %s", modifier, newMax, spawn.GetName())
return nil
}
// ModifyPower modifies the spawn's current power by a relative amount
func ModifyPower(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
modifier := ctx.GetParameterFloat("modifier", 0)
current := float64(spawn.GetPower())
newPower := current + modifier
// Clamp between 0 and max power
maxPower := float64(spawn.GetTotalPower())
if newPower < 0 {
newPower = 0
} else if newPower > maxPower {
newPower = maxPower
}
spawn.SetPower(int32(newPower))
ctx.Debug("Modified Power by %f (new value: %f) for spawn %s", modifier, newPower, spawn.GetName())
return nil
}
// ModifyHP modifies the spawn's current HP by a relative amount
func ModifyHP(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
modifier := ctx.GetParameterFloat("modifier", 0)
current := float64(spawn.GetHP())
newHP := current + modifier
// Clamp between 0 and max HP
maxHP := float64(spawn.GetTotalHP())
if newHP < 0 {
newHP = 0
} else if newHP > maxHP {
newHP = maxHP
}
spawn.SetHP(int32(newHP))
ctx.Debug("Modified HP by %f (new value: %f) for spawn %s", modifier, newHP, spawn.GetName())
// Update alive state based on HP
if newHP <= 0 {
spawn.SetAlive(false)
ctx.Debug("Spawn %s is now dead", spawn.GetName())
} else if !spawn.IsAlive() {
spawn.SetAlive(true)
ctx.Debug("Spawn %s is now alive", spawn.GetName())
}
return nil
}
// ModifyTotalHP modifies the spawn's total/max HP by a relative amount
func ModifyTotalHP(ctx *events.EventContext) error {
return ModifyMaxHP(ctx) // Alias for ModifyMaxHP
}
// ModifyTotalPower modifies the spawn's total/max power by a relative amount
func ModifyTotalPower(ctx *events.EventContext) error {
return ModifyMaxPower(ctx) // Alias for ModifyMaxPower
}
// GetCurrentHP gets the spawn's current HP
func GetCurrentHP(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
hp := spawn.GetHP()
ctx.SetResult("hp", hp)
return nil
}
// GetMaxHP gets the spawn's maximum HP
func GetMaxHP(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
maxHP := spawn.GetTotalHP()
ctx.SetResult("max_hp", maxHP)
return nil
}
// GetMaxHPBase gets the spawn's base maximum HP (before bonuses)
func GetMaxHPBase(ctx *events.EventContext) error {
// TODO: Implement base HP system when available
return GetMaxHP(ctx) // Fallback to regular max HP for now
}
// GetCurrentPower gets the spawn's current power
func GetCurrentPower(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
power := spawn.GetPower()
ctx.SetResult("power", power)
return nil
}
// GetMaxPower gets the spawn's maximum power
func GetMaxPower(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
maxPower := spawn.GetTotalPower()
ctx.SetResult("max_power", maxPower)
return nil
}
// GetMaxPowerBase gets the spawn's base maximum power (before bonuses)
func GetMaxPowerBase(ctx *events.EventContext) error {
// TODO: Implement base power system when available
return GetMaxPower(ctx) // Fallback to regular max power for now
}
// GetPCTOfHP gets the percentage of current HP relative to max HP
func GetPCTOfHP(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
currentHP := float64(spawn.GetHP())
maxHP := float64(spawn.GetTotalHP())
var percentage float64
if maxHP > 0 {
percentage = (currentHP / maxHP) * 100
} else {
percentage = 0
}
ctx.SetResult("hp_percentage", percentage)
return nil
}
// GetPCTOfPower gets the percentage of current power relative to max power
func GetPCTOfPower(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
currentPower := float64(spawn.GetPower())
maxPower := float64(spawn.GetTotalPower())
var percentage float64
if maxPower > 0 {
percentage = (currentPower / maxPower) * 100
} else {
percentage = 0
}
ctx.SetResult("power_percentage", percentage)
return nil
}
// SpellHeal heals the spawn for a specific amount
func SpellHeal(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
amount := ctx.GetParameterFloat("amount", 0)
if amount <= 0 {
return fmt.Errorf("heal amount must be positive")
}
current := float64(spawn.GetHP())
maxHP := float64(spawn.GetTotalHP())
newHP := current + amount
// Cap at max HP
if newHP > maxHP {
newHP = maxHP
amount = maxHP - current // Adjust amount to actual healed
}
spawn.SetHP(int32(newHP))
ctx.SetResult("amount_healed", amount)
ctx.Debug("Healed spawn %s for %f (new HP: %f)", spawn.GetName(), amount, newHP)
// Update alive state if necessary
if newHP > 0 && !spawn.IsAlive() {
spawn.SetAlive(true)
ctx.Debug("Spawn %s is now alive from healing", spawn.GetName())
}
return nil
}
// SpellHealPct heals the spawn for a percentage of max HP
func SpellHealPct(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
percentage := ctx.GetParameterFloat("percentage", 0)
if percentage <= 0 {
return fmt.Errorf("heal percentage must be positive")
}
maxHP := float64(spawn.GetTotalHP())
healAmount := maxHP * (percentage / 100.0)
// Set the heal amount and delegate to SpellHeal
ctx.WithParameter("amount", healAmount)
return SpellHeal(ctx)
}
// IsAlive checks if the spawn is alive
func IsAlive(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("is_alive", spawn.IsAlive())
return nil
}

View File

@ -0,0 +1,492 @@
package functions
import (
"fmt"
"math"
"strconv"
"strings"
"eq2emu/internal/events"
)
// Miscellaneous Utility Functions
// SendMessage sends a message (placeholder implementation)
func SendMessage(ctx *events.EventContext) error {
message := ctx.GetParameterString("message", "")
if message == "" {
return fmt.Errorf("message parameter required")
}
// TODO: Implement message sending
ctx.Info("Message: %s", message)
return nil
}
// LogMessage logs a message at specified level
func LogMessage(ctx *events.EventContext) error {
level := ctx.GetParameterString("level", "info")
message := ctx.GetParameterString("message", "")
if message == "" {
return fmt.Errorf("message parameter required")
}
switch strings.ToLower(level) {
case "debug":
ctx.Debug("%s", message)
case "info":
ctx.Info("%s", message)
case "warn", "warning":
ctx.Warn("%s", message)
case "error":
ctx.Error("%s", message)
default:
ctx.Info("%s", message)
}
return nil
}
// MakeRandomInt generates a random integer
func MakeRandomInt(ctx *events.EventContext) error {
min := ctx.GetParameterInt("min", 0)
max := ctx.GetParameterInt("max", 100)
if min > max {
min, max = max, min
}
// Simple random - in practice you'd use a proper random generator
result := min + (int(math.Abs(float64(ctx.EventName[0]))) % (max - min + 1))
ctx.SetResult("random_int", result)
return nil
}
// MakeRandomFloat generates a random float
func MakeRandomFloat(ctx *events.EventContext) error {
min := ctx.GetParameterFloat("min", 0.0)
max := ctx.GetParameterFloat("max", 1.0)
if min > max {
min, max = max, min
}
// Simple random float - in practice you'd use a proper random generator
ratio := float64(int(math.Abs(float64(ctx.EventName[0]))) % 100) / 100.0
result := min + (max-min)*ratio
ctx.SetResult("random_float", result)
return nil
}
// ParseInt parses a string to integer
func ParseInt(ctx *events.EventContext) error {
str := ctx.GetParameterString("string", "")
if str == "" {
return fmt.Errorf("string parameter required")
}
value, err := strconv.Atoi(str)
if err != nil {
return fmt.Errorf("failed to parse integer: %w", err)
}
ctx.SetResult("int_value", value)
return nil
}
// GetName gets the spawn's name
func GetName(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("name", spawn.GetName())
return nil
}
// GetID gets the spawn's ID
func GetID(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("id", spawn.GetID())
return nil
}
// GetSpawnID gets the spawn's ID (alias for GetID)
func GetSpawnID(ctx *events.EventContext) error {
return GetID(ctx)
}
// IsPlayer checks if spawn is a player
func IsPlayer(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("is_player", spawn.IsPlayer())
return nil
}
// IsNPC checks if spawn is an NPC
func IsNPC(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("is_npc", spawn.IsNPC())
return nil
}
// IsEntity checks if spawn is an entity
func IsEntity(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement entity checking when Entity interface is available
ctx.SetResult("is_entity", false) // Default for now
return nil
}
// IsDead checks if spawn is dead
func IsDead(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("is_dead", !spawn.IsAlive())
return nil
}
// GetCharacterID gets the character ID for players
func GetCharacterID(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Implement character ID retrieval
ctx.SetResult("character_id", 0) // Default value
return nil
}
// Despawn removes the spawn from the world
func Despawn(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement despawn logic
ctx.Debug("Despawned spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// Spawn creates a new spawn
func Spawn(ctx *events.EventContext) error {
locationID := ctx.GetParameterInt("location_id", 0)
spawnGroupID := ctx.GetParameterInt("spawn_group_id", 0)
// TODO: Implement spawn creation
ctx.Debug("Created spawn at location %d, group %d (not yet implemented)", locationID, spawnGroupID)
ctx.SetResult("spawned", true)
return nil
}
// SpawnByLocationID spawns by location ID
func SpawnByLocationID(ctx *events.EventContext) error {
locationID := ctx.GetParameterInt("location_id", 0)
if locationID <= 0 {
return fmt.Errorf("invalid location ID")
}
// TODO: Implement location-based spawning
ctx.Debug("Spawned by location ID %d (not yet implemented)", locationID)
ctx.SetResult("spawned", true)
return nil
}
// SpawnGroupByID spawns a group by ID
func SpawnGroupByID(ctx *events.EventContext) error {
groupID := ctx.GetParameterInt("group_id", 0)
if groupID <= 0 {
return fmt.Errorf("invalid group ID")
}
// TODO: Implement group spawning
ctx.Debug("Spawned group ID %d (not yet implemented)", groupID)
ctx.SetResult("spawned", true)
return nil
}
// DespawnByLocationID despawns spawns at a location
func DespawnByLocationID(ctx *events.EventContext) error {
locationID := ctx.GetParameterInt("location_id", 0)
if locationID <= 0 {
return fmt.Errorf("invalid location ID")
}
// TODO: Implement location-based despawning
ctx.Debug("Despawned location ID %d (not yet implemented)", locationID)
return nil
}
// GetSpawnByLocationID gets spawn by location ID
func GetSpawnByLocationID(ctx *events.EventContext) error {
locationID := ctx.GetParameterInt("location_id", 0)
if locationID <= 0 {
return fmt.Errorf("invalid location ID")
}
// TODO: Implement spawn retrieval by location
ctx.SetResult("spawn", nil) // No spawn found
return nil
}
// GetSpawnByGroupID gets spawn by group ID
func GetSpawnByGroupID(ctx *events.EventContext) error {
groupID := ctx.GetParameterInt("group_id", 0)
if groupID <= 0 {
return fmt.Errorf("invalid group ID")
}
// TODO: Implement spawn retrieval by group
ctx.SetResult("spawn", nil) // No spawn found
return nil
}
// GetSpawnGroupID gets the spawn's group ID
func GetSpawnGroupID(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement group ID retrieval
ctx.SetResult("group_id", 0) // Default value
return nil
}
// SetSpawnGroupID sets the spawn's group ID
func SetSpawnGroupID(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
groupID := ctx.GetParameterInt("group_id", 0)
// TODO: Implement group ID setting
ctx.Debug("Set group ID to %d for spawn %s (not yet implemented)", groupID, spawn.GetName())
return nil
}
// GetSpawnLocationID gets the spawn's location ID
func GetSpawnLocationID(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement location ID retrieval
ctx.SetResult("location_id", 0) // Default value
return nil
}
// GetSpawnLocationPlacementID gets the spawn's location placement ID
func GetSpawnLocationPlacementID(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement location placement ID retrieval
ctx.SetResult("placement_id", 0) // Default value
return nil
}
// SetGridID sets the spawn's grid ID
func SetGridID(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
gridID := ctx.GetParameterInt("grid_id", 0)
// TODO: Implement grid ID setting
ctx.Debug("Set grid ID to %d for spawn %s (not yet implemented)", gridID, spawn.GetName())
return nil
}
// SpawnSet sets spawn properties by distance/criteria
func SpawnSet(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
property := ctx.GetParameterString("property", "")
value := ctx.GetParameterString("value", "")
// TODO: Implement spawn property setting
ctx.Debug("Set property %s to %s for spawn %s (not yet implemented)", property, value, spawn.GetName())
return nil
}
// SpawnSetByDistance sets spawn properties within distance
func SpawnSetByDistance(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
distance := ctx.GetParameterFloat("distance", 10.0)
property := ctx.GetParameterString("property", "")
value := ctx.GetParameterString("value", "")
// TODO: Implement distance-based spawn property setting
ctx.Debug("Set property %s to %s within distance %f of spawn %s (not yet implemented)",
property, value, distance, spawn.GetName())
return nil
}
// IsSpawnGroupAlive checks if spawn group is alive
func IsSpawnGroupAlive(ctx *events.EventContext) error {
groupID := ctx.GetParameterInt("group_id", 0)
if groupID <= 0 {
return fmt.Errorf("invalid group ID")
}
// TODO: Implement group alive checking
ctx.SetResult("group_alive", false) // Default value
return nil
}
// AddSpawnToGroup adds spawn to a group
func AddSpawnToGroup(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
groupID := ctx.GetParameterInt("group_id", 0)
if groupID <= 0 {
return fmt.Errorf("invalid group ID")
}
// TODO: Implement group addition
ctx.Debug("Added spawn %s to group %d (not yet implemented)", spawn.GetName(), groupID)
return nil
}
// GetVariableValue gets a variable value
func GetVariableValue(ctx *events.EventContext) error {
variableName := ctx.GetParameterString("variable", "")
if variableName == "" {
return fmt.Errorf("variable name required")
}
// TODO: Implement variable system
ctx.SetResult("value", "") // Default empty value
return nil
}
// SetServerVariable sets a server variable
func SetServerVariable(ctx *events.EventContext) error {
variableName := ctx.GetParameterString("variable", "")
value := ctx.GetParameterString("value", "")
if variableName == "" {
return fmt.Errorf("variable name required")
}
// TODO: Implement server variable system
ctx.Debug("Set server variable %s to %s (not yet implemented)", variableName, value)
return nil
}
// GetServerVariable gets a server variable
func GetServerVariable(ctx *events.EventContext) error {
variableName := ctx.GetParameterString("variable", "")
if variableName == "" {
return fmt.Errorf("variable name required")
}
// TODO: Implement server variable system
ctx.SetResult("value", "") // Default empty value
return nil
}
// SetTempVariable sets a temporary variable
func SetTempVariable(ctx *events.EventContext) error {
variableName := ctx.GetParameterString("variable", "")
value := ctx.GetParameterString("value", "")
if variableName == "" {
return fmt.Errorf("variable name required")
}
// TODO: Implement temporary variable system
ctx.Debug("Set temp variable %s to %s (not yet implemented)", variableName, value)
return nil
}
// GetTempVariable gets a temporary variable
func GetTempVariable(ctx *events.EventContext) error {
variableName := ctx.GetParameterString("variable", "")
if variableName == "" {
return fmt.Errorf("variable name required")
}
// TODO: Implement temporary variable system
ctx.SetResult("value", "") // Default empty value
return nil
}
// CheckLOS checks line of sight between two positions
func CheckLOS(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement line of sight checking
ctx.SetResult("has_los", true) // Default to true
return nil
}
// CheckLOSByCoordinates checks line of sight between coordinates
func CheckLOSByCoordinates(ctx *events.EventContext) error {
x1 := ctx.GetParameterFloat("x1", 0)
y1 := ctx.GetParameterFloat("y1", 0)
z1 := ctx.GetParameterFloat("z1", 0)
x2 := ctx.GetParameterFloat("x2", 0)
y2 := ctx.GetParameterFloat("y2", 0)
z2 := ctx.GetParameterFloat("z2", 0)
// TODO: Implement coordinate-based line of sight checking
ctx.Debug("Checking LOS from (%.2f,%.2f,%.2f) to (%.2f,%.2f,%.2f) (not yet implemented)",
x1, y1, z1, x2, y2, z2)
ctx.SetResult("has_los", true) // Default to true
return nil
}

View File

@ -0,0 +1,534 @@
package functions
import (
"fmt"
"eq2emu/internal/events"
)
// Movement and Position Functions
// SetPosition sets the spawn's position and heading
func SetPosition(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
x := ctx.GetParameterFloat("x", 0)
y := ctx.GetParameterFloat("y", 0)
z := ctx.GetParameterFloat("z", 0)
heading := ctx.GetParameterFloat("heading", float64(spawn.GetHeading()))
spawn.SetX(float32(x))
spawn.SetY(float32(y), false)
spawn.SetZ(float32(z))
spawn.SetHeadingFromFloat(float32(heading))
ctx.Debug("Set position to (%.2f, %.2f, %.2f, %.2f) for spawn %s",
x, y, z, heading, spawn.GetName())
return nil
}
// GetPosition gets the spawn's position and heading
func GetPosition(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("x", spawn.GetX())
ctx.SetResult("y", spawn.GetY())
ctx.SetResult("z", spawn.GetZ())
ctx.SetResult("heading", spawn.GetHeading())
return nil
}
// GetX gets the spawn's X coordinate
func GetX(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("x", spawn.GetX())
return nil
}
// GetY gets the spawn's Y coordinate
func GetY(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("y", spawn.GetY())
return nil
}
// GetZ gets the spawn's Z coordinate
func GetZ(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("z", spawn.GetZ())
return nil
}
// GetHeading gets the spawn's heading
func GetHeading(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
ctx.SetResult("heading", spawn.GetHeading())
return nil
}
// SetHeading sets the spawn's heading
func SetHeading(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
heading := ctx.GetParameterFloat("heading", 0)
spawn.SetHeadingFromFloat(float32(heading))
ctx.Debug("Set heading to %.2f for spawn %s", heading, spawn.GetName())
return nil
}
// GetOrigX gets the spawn's original X coordinate
func GetOrigX(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement original position tracking
ctx.SetResult("orig_x", spawn.GetX()) // Fallback to current position
return nil
}
// GetOrigY gets the spawn's original Y coordinate
func GetOrigY(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement original position tracking
ctx.SetResult("orig_y", spawn.GetY()) // Fallback to current position
return nil
}
// GetOrigZ gets the spawn's original Z coordinate
func GetOrigZ(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement original position tracking
ctx.SetResult("orig_z", spawn.GetZ()) // Fallback to current position
return nil
}
// GetDistance gets the distance between spawn and target
func GetDistance(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement proper distance calculation
// For now, return a placeholder distance
distance := 10.0
ctx.SetResult("distance", distance)
return nil
}
// FaceTarget makes the spawn face the target
func FaceTarget(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if target == nil {
return fmt.Errorf("no target in context")
}
// TODO: Implement face target calculation
ctx.Debug("Spawn %s facing target %s (not yet implemented)", spawn.GetName(), target.GetName())
return nil
}
// GetSpeed gets the spawn's movement speed
func GetSpeed(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement speed tracking
ctx.SetResult("speed", 5.0) // Default walking speed
return nil
}
// SetSpeed sets the spawn's movement speed
func SetSpeed(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
speed := ctx.GetParameterFloat("speed", 5.0)
if speed < 0 {
speed = 0
}
// TODO: Implement speed setting
ctx.Debug("Set speed to %.2f for spawn %s (not yet implemented)", speed, spawn.GetName())
return nil
}
// SetSpeedMultiplier sets a speed multiplier for the spawn
func SetSpeedMultiplier(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
multiplier := ctx.GetParameterFloat("multiplier", 1.0)
if multiplier < 0 {
multiplier = 0
}
// TODO: Implement speed multiplier
ctx.Debug("Set speed multiplier to %.2f for spawn %s (not yet implemented)", multiplier, spawn.GetName())
return nil
}
// HasMoved checks if the spawn has moved recently
func HasMoved(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement movement tracking
ctx.SetResult("has_moved", false) // Default value
return nil
}
// IsRunning checks if the spawn is currently running
func IsRunning(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement running state tracking
ctx.SetResult("is_running", false) // Default value
return nil
}
// MoveToLocation moves the spawn to a specific location
func MoveToLocation(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
x := ctx.GetParameterFloat("x", 0)
y := ctx.GetParameterFloat("y", 0)
z := ctx.GetParameterFloat("z", 0)
runningSpeed := ctx.GetParameterFloat("running_speed", 7.0)
// TODO: Implement actual movement/pathfinding
// For now, just teleport to the location
spawn.SetX(float32(x))
spawn.SetY(float32(y), false)
spawn.SetZ(float32(z))
ctx.Debug("Moved spawn %s to location (%.2f, %.2f, %.2f) at speed %.2f",
spawn.GetName(), x, y, z, runningSpeed)
return nil
}
// ClearRunningLocations clears any queued movement locations
func ClearRunningLocations(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement movement queue clearing
ctx.Debug("Cleared running locations for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// SpawnMove initiates spawn movement
func SpawnMove(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
x := ctx.GetParameterFloat("x", 0)
y := ctx.GetParameterFloat("y", 0)
z := ctx.GetParameterFloat("z", 0)
delay := ctx.GetParameterInt("delay", 0)
// TODO: Implement spawn movement with delay
ctx.Debug("Spawn %s moving to (%.2f, %.2f, %.2f) with delay %d (not yet implemented)",
spawn.GetName(), x, y, z, delay)
return nil
}
// MovementLoopAdd adds a movement loop point
func MovementLoopAdd(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
x := ctx.GetParameterFloat("x", 0)
y := ctx.GetParameterFloat("y", 0)
z := ctx.GetParameterFloat("z", 0)
delay := ctx.GetParameterInt("delay", 0)
// TODO: Implement movement loop system
ctx.Debug("Added movement loop point (%.2f, %.2f, %.2f) with delay %d for spawn %s (not yet implemented)",
x, y, z, delay, spawn.GetName())
return nil
}
// PauseMovement pauses the spawn's movement
func PauseMovement(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
duration := ctx.GetParameterInt("duration", 0)
// TODO: Implement movement pausing
ctx.Debug("Paused movement for spawn %s for %d ms (not yet implemented)", spawn.GetName(), duration)
return nil
}
// StopMovement stops the spawn's movement
func StopMovement(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement movement stopping
ctx.Debug("Stopped movement for spawn %s (not yet implemented)", spawn.GetName())
return nil
}
// SetMount sets the spawn's mount
func SetMount(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
mountID := ctx.GetParameterInt("mount_id", 0)
// TODO: Implement mount system
ctx.Debug("Set mount %d for spawn %s (not yet implemented)", mountID, spawn.GetName())
return nil
}
// GetMount gets the spawn's current mount
func GetMount(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement mount system
ctx.SetResult("mount_id", 0) // No mount
return nil
}
// SetMountColor sets the mount's color
func SetMountColor(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
colorR := ctx.GetParameterInt("red", 255)
colorG := ctx.GetParameterInt("green", 255)
colorB := ctx.GetParameterInt("blue", 255)
// TODO: Implement mount color system
ctx.Debug("Set mount color to RGB(%d, %d, %d) for spawn %s (not yet implemented)",
colorR, colorG, colorB, spawn.GetName())
return nil
}
// StartAutoMount starts auto-mounting
func StartAutoMount(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Implement auto-mount system
ctx.Debug("Started auto-mount for player %s (not yet implemented)", spawn.GetName())
return nil
}
// EndAutoMount ends auto-mounting
func EndAutoMount(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Implement auto-mount system
ctx.Debug("Ended auto-mount for player %s (not yet implemented)", spawn.GetName())
return nil
}
// IsOnAutoMount checks if spawn is on auto-mount
func IsOnAutoMount(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
// TODO: Implement auto-mount system
ctx.SetResult("is_on_auto_mount", false)
return nil
}
// AddWaypoint adds a waypoint for the player
func AddWaypoint(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
x := ctx.GetParameterFloat("x", 0)
y := ctx.GetParameterFloat("y", 0)
z := ctx.GetParameterFloat("z", 0)
waypointName := ctx.GetParameterString("name", "Waypoint")
// TODO: Implement waypoint system
ctx.Debug("Added waypoint '%s' at (%.2f, %.2f, %.2f) for player %s (not yet implemented)",
waypointName, x, y, z, spawn.GetName())
return nil
}
// RemoveWaypoint removes a waypoint for the player
func RemoveWaypoint(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
waypointID := ctx.GetParameterInt("waypoint_id", 0)
// TODO: Implement waypoint system
ctx.Debug("Removed waypoint %d for player %s (not yet implemented)", waypointID, spawn.GetName())
return nil
}
// SendWaypoints sends waypoints to the player
func SendWaypoints(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Implement waypoint system
ctx.Debug("Sent waypoints to player %s (not yet implemented)", spawn.GetName())
return nil
}
// Evac evacuates the player to safety
func Evac(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Implement evacuation to safe location
ctx.Debug("Evacuated player %s to safety (not yet implemented)", spawn.GetName())
return nil
}
// Bind binds the player to current location
func Bind(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Implement bind system
ctx.Debug("Bound player %s to current location (not yet implemented)", spawn.GetName())
return nil
}
// Gate gates the player to bind location
func Gate(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
if !spawn.IsPlayer() {
return fmt.Errorf("spawn is not a player")
}
// TODO: Implement gate system
ctx.Debug("Gated player %s to bind location (not yet implemented)", spawn.GetName())
return nil
}

View File

@ -0,0 +1,255 @@
package functions
import (
"fmt"
"eq2emu/internal/events"
)
// RegisterAllEQ2Functions registers all EQ2 event functions with a handler
func RegisterAllEQ2Functions(handler *events.EventHandler) error {
functions := map[string]events.EventFunction{
// Health Functions
"SetCurrentHP": SetCurrentHP,
"SetMaxHP": SetMaxHP,
"SetMaxHPBase": SetMaxHPBase,
"SetCurrentPower": SetCurrentPower,
"SetMaxPower": SetMaxPower,
"SetMaxPowerBase": SetMaxPowerBase,
"ModifyMaxHP": ModifyMaxHP,
"ModifyMaxPower": ModifyMaxPower,
"ModifyPower": ModifyPower,
"ModifyHP": ModifyHP,
"ModifyTotalHP": ModifyTotalHP,
"ModifyTotalPower": ModifyTotalPower,
"GetCurrentHP": GetCurrentHP,
"GetMaxHP": GetMaxHP,
"GetMaxHPBase": GetMaxHPBase,
"GetCurrentPower": GetCurrentPower,
"GetMaxPower": GetMaxPower,
"GetMaxPowerBase": GetMaxPowerBase,
"GetPCTOfHP": GetPCTOfHP,
"GetPCTOfPower": GetPCTOfPower,
"SpellHeal": SpellHeal,
"SpellHealPct": SpellHealPct,
"IsAlive": IsAlive,
// Attributes Functions
"SetInt": SetInt,
"SetWis": SetWis,
"SetSta": SetSta,
"SetStr": SetStr,
"SetAgi": SetAgi,
"SetIntBase": SetIntBase,
"SetWisBase": SetWisBase,
"SetStaBase": SetStaBase,
"SetStrBase": SetStrBase,
"SetAgiBase": SetAgiBase,
"GetInt": GetInt,
"GetWis": GetWis,
"GetSta": GetSta,
"GetStr": GetStr,
"GetAgi": GetAgi,
"GetIntBase": GetIntBase,
"GetWisBase": GetWisBase,
"GetStaBase": GetStaBase,
"GetStrBase": GetStrBase,
"GetAgiBase": GetAgiBase,
"GetLevel": GetLevel,
"SetLevel": SetLevel,
"SetPlayerLevel": SetPlayerLevel,
"GetDifficulty": GetDifficulty,
"AddSpellBonus": AddSpellBonus,
"RemoveSpellBonus": RemoveSpellBonus,
"AddSkillBonus": AddSkillBonus,
"RemoveSkillBonus": RemoveSkillBonus,
"GetClass": GetClass,
"SetClass": SetClass,
"SetAdventureClass": SetAdventureClass,
"GetTradeskillClass": GetTradeskillClass,
"SetTradeskillClass": SetTradeskillClass,
"GetTradeskillLevel": GetTradeskillLevel,
"SetTradeskillLevel": SetTradeskillLevel,
"GetRace": GetRace,
"GetGender": GetGender,
"GetModelType": GetModelType,
"SetModelType": SetModelType,
"GetDeity": GetDeity,
"SetDeity": SetDeity,
"GetAlignment": GetAlignment,
"SetAlignment": SetAlignment,
// Movement Functions
"SetPosition": SetPosition,
"GetPosition": GetPosition,
"GetX": GetX,
"GetY": GetY,
"GetZ": GetZ,
"GetHeading": GetHeading,
"SetHeading": SetHeading,
"GetOrigX": GetOrigX,
"GetOrigY": GetOrigY,
"GetOrigZ": GetOrigZ,
"GetDistance": GetDistance,
"FaceTarget": FaceTarget,
"GetSpeed": GetSpeed,
"SetSpeed": SetSpeed,
"SetSpeedMultiplier": SetSpeedMultiplier,
"HasMoved": HasMoved,
"IsRunning": IsRunning,
"MoveToLocation": MoveToLocation,
"ClearRunningLocations": ClearRunningLocations,
"SpawnMove": SpawnMove,
"MovementLoopAdd": MovementLoopAdd,
"PauseMovement": PauseMovement,
"StopMovement": StopMovement,
"SetMount": SetMount,
"GetMount": GetMount,
"SetMountColor": SetMountColor,
"StartAutoMount": StartAutoMount,
"EndAutoMount": EndAutoMount,
"IsOnAutoMount": IsOnAutoMount,
"AddWaypoint": AddWaypoint,
"RemoveWaypoint": RemoveWaypoint,
"SendWaypoints": SendWaypoints,
"Evac": Evac,
"Bind": Bind,
"Gate": Gate,
// Combat Functions
"Attack": Attack,
"AddHate": AddHate,
"ClearHate": ClearHate,
"GetMostHated": GetMostHated,
"SetTarget": SetTarget,
"GetTarget": GetTarget,
"IsInCombat": IsInCombat,
"SetInCombat": SetInCombat,
"SpellDamage": SpellDamage,
"SpellDamageExt": SpellDamageExt,
"DamageSpawn": DamageSpawn,
"ProcDamage": ProcDamage,
"ProcHate": ProcHate,
"Knockback": Knockback,
"Interrupt": Interrupt,
"IsCasting": IsCasting,
"HasRecovered": HasRecovered,
"ProcessMelee": ProcessMelee,
"ProcessSpell": ProcessSpell,
"LastSpellAttackHit": LastSpellAttackHit,
"IsBehind": IsBehind,
"IsFlanking": IsFlanking,
"InFront": InFront,
"GetEncounterSize": GetEncounterSize,
"GetEncounter": GetEncounter,
"GetHateList": GetHateList,
"ClearEncounter": ClearEncounter,
"ClearRunback": ClearRunback,
"Runback": Runback,
"GetRunbackDistance": GetRunbackDistance,
"CompareSpawns": CompareSpawns,
"KillSpawn": KillSpawn,
"KillSpawnByDistance": KillSpawnByDistance,
"Resurrect": Resurrect,
"IsInvulnerable": IsInvulnerable,
"SetInvulnerable": SetInvulnerable,
"SetAttackable": SetAttackable,
// Miscellaneous Functions
"SendMessage": SendMessage,
"LogMessage": LogMessage,
"MakeRandomInt": MakeRandomInt,
"MakeRandomFloat": MakeRandomFloat,
"ParseInt": ParseInt,
"GetName": GetName,
"GetID": GetID,
"GetSpawnID": GetSpawnID,
"IsPlayer": IsPlayer,
"IsNPC": IsNPC,
"IsEntity": IsEntity,
"IsDead": IsDead,
"GetCharacterID": GetCharacterID,
"Despawn": Despawn,
"Spawn": Spawn,
"SpawnByLocationID": SpawnByLocationID,
"SpawnGroupByID": SpawnGroupByID,
"DespawnByLocationID": DespawnByLocationID,
"GetSpawnByLocationID": GetSpawnByLocationID,
"GetSpawnByGroupID": GetSpawnByGroupID,
"GetSpawnGroupID": GetSpawnGroupID,
"SetSpawnGroupID": SetSpawnGroupID,
"GetSpawnLocationID": GetSpawnLocationID,
"GetSpawnLocationPlacementID": GetSpawnLocationPlacementID,
"SetGridID": SetGridID,
"SpawnSet": SpawnSet,
"SpawnSetByDistance": SpawnSetByDistance,
"IsSpawnGroupAlive": IsSpawnGroupAlive,
"AddSpawnToGroup": AddSpawnToGroup,
"GetVariableValue": GetVariableValue,
"SetServerVariable": SetServerVariable,
"GetServerVariable": GetServerVariable,
"SetTempVariable": SetTempVariable,
"GetTempVariable": GetTempVariable,
"CheckLOS": CheckLOS,
"CheckLOSByCoordinates": CheckLOSByCoordinates,
}
for name, fn := range functions {
if err := handler.Register(name, fn); err != nil {
return fmt.Errorf("failed to register event %s: %w", name, err)
}
}
return nil
}
// GetFunctionsByDomain returns functions organized by domain
func GetFunctionsByDomain() map[string][]string {
return map[string][]string{
"health": {
"SetCurrentHP", "SetMaxHP", "SetMaxHPBase", "SetCurrentPower", "SetMaxPower",
"SetMaxPowerBase", "ModifyMaxHP", "ModifyMaxPower", "ModifyPower", "ModifyHP",
"ModifyTotalHP", "ModifyTotalPower", "GetCurrentHP", "GetMaxHP", "GetMaxHPBase",
"GetCurrentPower", "GetMaxPower", "GetMaxPowerBase", "GetPCTOfHP", "GetPCTOfPower",
"SpellHeal", "SpellHealPct", "IsAlive",
},
"attributes": {
"SetInt", "SetWis", "SetSta", "SetStr", "SetAgi", "SetIntBase", "SetWisBase",
"SetStaBase", "SetStrBase", "SetAgiBase", "GetInt", "GetWis", "GetSta", "GetStr",
"GetAgi", "GetIntBase", "GetWisBase", "GetStaBase", "GetStrBase", "GetAgiBase",
"GetLevel", "SetLevel", "SetPlayerLevel", "GetDifficulty", "AddSpellBonus",
"RemoveSpellBonus", "AddSkillBonus", "RemoveSkillBonus", "GetClass", "SetClass",
"SetAdventureClass", "GetTradeskillClass", "SetTradeskillClass", "GetTradeskillLevel",
"SetTradeskillLevel", "GetRace", "GetGender", "GetModelType", "SetModelType",
"GetDeity", "SetDeity", "GetAlignment", "SetAlignment",
},
"movement": {
"SetPosition", "GetPosition", "GetX", "GetY", "GetZ", "GetHeading", "SetHeading",
"GetOrigX", "GetOrigY", "GetOrigZ", "GetDistance", "FaceTarget", "GetSpeed",
"SetSpeed", "SetSpeedMultiplier", "HasMoved", "IsRunning", "MoveToLocation",
"ClearRunningLocations", "SpawnMove", "MovementLoopAdd", "PauseMovement",
"StopMovement", "SetMount", "GetMount", "SetMountColor", "StartAutoMount",
"EndAutoMount", "IsOnAutoMount", "AddWaypoint", "RemoveWaypoint", "SendWaypoints",
"Evac", "Bind", "Gate",
},
"combat": {
"Attack", "AddHate", "ClearHate", "GetMostHated", "SetTarget", "GetTarget",
"IsInCombat", "SetInCombat", "SpellDamage", "SpellDamageExt", "DamageSpawn",
"ProcDamage", "ProcHate", "Knockback", "Interrupt", "IsCasting", "HasRecovered",
"ProcessMelee", "ProcessSpell", "LastSpellAttackHit", "IsBehind", "IsFlanking",
"InFront", "GetEncounterSize", "GetEncounter", "GetHateList", "ClearEncounter",
"ClearRunback", "Runback", "GetRunbackDistance", "CompareSpawns", "KillSpawn",
"KillSpawnByDistance", "Resurrect", "IsInvulnerable", "SetInvulnerable", "SetAttackable",
},
"misc": {
"SendMessage", "LogMessage", "MakeRandomInt", "MakeRandomFloat", "ParseInt",
"GetName", "GetID", "GetSpawnID", "IsPlayer", "IsNPC", "IsEntity", "IsDead",
"GetCharacterID", "Despawn", "Spawn", "SpawnByLocationID", "SpawnGroupByID",
"DespawnByLocationID", "GetSpawnByLocationID", "GetSpawnByGroupID", "GetSpawnGroupID",
"SetSpawnGroupID", "GetSpawnLocationID", "GetSpawnLocationPlacementID", "SetGridID",
"SpawnSet", "SpawnSetByDistance", "IsSpawnGroupAlive", "AddSpawnToGroup",
"GetVariableValue", "SetServerVariable", "GetServerVariable", "SetTempVariable",
"GetTempVariable", "CheckLOS", "CheckLOSByCoordinates",
},
}
}

View File

@ -0,0 +1,82 @@
package events
import (
"context"
"fmt"
"time"
)
// NewEventHandler creates a new event handler
func NewEventHandler() *EventHandler {
return &EventHandler{
events: make(map[string]EventFunction),
}
}
// Register registers an event function
func (h *EventHandler) Register(eventName string, fn EventFunction) error {
if fn == nil {
return fmt.Errorf("event function cannot be nil")
}
h.mutex.Lock()
defer h.mutex.Unlock()
h.events[eventName] = fn
return nil
}
// Unregister removes an event function
func (h *EventHandler) Unregister(eventName string) {
h.mutex.Lock()
defer h.mutex.Unlock()
delete(h.events, eventName)
}
// Execute executes an event function
func (h *EventHandler) Execute(ctx *EventContext) error {
if ctx == nil {
return fmt.Errorf("context cannot be nil")
}
h.mutex.RLock()
fn, exists := h.events[ctx.EventName]
h.mutex.RUnlock()
if !exists {
return fmt.Errorf("event %s not found", ctx.EventName)
}
// Set up context with timeout if needed
if ctx.Context == nil {
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx.Context = timeout
}
// Execute the function
return fn(ctx)
}
// HasEvent checks if an event is registered
func (h *EventHandler) HasEvent(eventName string) bool {
h.mutex.RLock()
defer h.mutex.RUnlock()
_, exists := h.events[eventName]
return exists
}
// ListEvents returns all registered event names
func (h *EventHandler) ListEvents() []string {
h.mutex.RLock()
defer h.mutex.RUnlock()
events := make([]string, 0, len(h.events))
for name := range h.events {
events = append(events, name)
}
return events
}

82
internal/events/types.go Normal file
View File

@ -0,0 +1,82 @@
package events
import (
"context"
"sync"
"eq2emu/internal/entity"
"eq2emu/internal/quests"
"eq2emu/internal/spawn"
)
// EventType represents different types of events
type EventType int
const (
EventTypeSpell EventType = iota
EventTypeSpawn
EventTypeQuest
EventTypeCombat
EventTypeZone
EventTypeItem
)
func (et EventType) String() string {
switch et {
case EventTypeSpell:
return "spell"
case EventTypeSpawn:
return "spawn"
case EventTypeQuest:
return "quest"
case EventTypeCombat:
return "combat"
case EventTypeZone:
return "zone"
case EventTypeItem:
return "item"
default:
return "unknown"
}
}
// EventContext provides context for event handling
type EventContext struct {
// Core context
Context context.Context
// Event information
EventType EventType
EventName string
FunctionName string
// Game objects (nil if not applicable)
Caster *entity.Entity
Target *entity.Entity
Spawn *spawn.Spawn
Quest *quests.Quest
// Parameters and results
Parameters map[string]interface{}
Results map[string]interface{}
// Synchronization
mutex sync.RWMutex
}
// EventFunction represents a callable event function
type EventFunction func(ctx *EventContext) error
// EventHandler manages event registration and execution
type EventHandler struct {
events map[string]EventFunction
mutex sync.RWMutex
}
// EventLogger provides logging for events
type EventLogger interface {
Debug(msg string, args ...interface{})
Info(msg string, args ...interface{})
Warn(msg string, args ...interface{})
Error(msg string, args ...interface{})
}

View File

@ -637,6 +637,70 @@ func (s *Spawn) SetLevel(level int16) {
s.addChangedZoneSpawn()
}
// GetClass returns the spawn's adventure class
func (s *Spawn) GetClass() int8 {
return s.appearance.AdventureClass
}
// SetClass updates the spawn's adventure class and marks info as changed
func (s *Spawn) SetClass(class int8) {
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
s.appearance.AdventureClass = class
s.infoChanged.Store(true)
s.changed.Store(true)
s.addChangedZoneSpawn()
}
// GetTradeskillClass returns the spawn's tradeskill class
func (s *Spawn) GetTradeskillClass() int8 {
return s.appearance.TradeskillClass
}
// SetTradeskillClass updates the spawn's tradeskill class and marks info as changed
func (s *Spawn) SetTradeskillClass(class int8) {
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
s.appearance.TradeskillClass = class
s.infoChanged.Store(true)
s.changed.Store(true)
s.addChangedZoneSpawn()
}
// GetRace returns the spawn's race
func (s *Spawn) GetRace() int8 {
return s.appearance.Race
}
// SetRace updates the spawn's race and marks info as changed
func (s *Spawn) SetRace(race int8) {
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
s.appearance.Race = race
s.infoChanged.Store(true)
s.changed.Store(true)
s.addChangedZoneSpawn()
}
// GetGender returns the spawn's gender
func (s *Spawn) GetGender() int8 {
return s.appearance.Gender
}
// SetGender updates the spawn's gender and marks info as changed
func (s *Spawn) SetGender(gender int8) {
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
s.appearance.Gender = gender
s.infoChanged.Store(true)
s.changed.Store(true)
s.addChangedZoneSpawn()
}
// GetX returns the spawn's X coordinate
func (s *Spawn) GetX() float32 {
return s.appearance.Pos.X