Compare commits
2 Commits
1987d48a77
...
180f0ec3fa
Author | SHA1 | Date | |
---|---|---|---|
180f0ec3fa | |||
789729a07e |
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,3 +17,6 @@
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# LuaJIT wrapper source
|
||||
luawrapper
|
133
CLAUDE.md
133
CLAUDE.md
@ -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
1
cmd/login_server/TODO.md
Normal file
@ -0,0 +1 @@
|
||||
need to implement
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
1
cmd/world_server/TODO.md
Normal file
@ -0,0 +1 @@
|
||||
Need to implement
|
@ -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")
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
182
internal/events/README.md
Normal 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
193
internal/events/context.go
Normal 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...))
|
||||
}
|
531
internal/events/functions/attributes.go
Normal file
531
internal/events/functions/attributes.go
Normal 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
|
||||
}
|
611
internal/events/functions/combat.go
Normal file
611
internal/events/functions/combat.go
Normal 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
|
||||
}
|
254
internal/events/functions/functions_test.go
Normal file
254
internal/events/functions/functions_test.go
Normal 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")
|
||||
}
|
||||
}
|
365
internal/events/functions/health.go
Normal file
365
internal/events/functions/health.go
Normal 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
|
||||
}
|
492
internal/events/functions/misc.go
Normal file
492
internal/events/functions/misc.go
Normal 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
|
||||
}
|
534
internal/events/functions/movement.go
Normal file
534
internal/events/functions/movement.go
Normal 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
|
||||
}
|
255
internal/events/functions/registry.go
Normal file
255
internal/events/functions/registry.go
Normal 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",
|
||||
},
|
||||
}
|
||||
}
|
82
internal/events/handler.go
Normal file
82
internal/events/handler.go
Normal 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
82
internal/events/types.go
Normal 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{})
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user