migrate commands package

This commit is contained in:
Sky Johnson 2025-08-06 22:13:32 -05:00
parent 8bc92f035a
commit abec68872f
23 changed files with 4271 additions and 649 deletions

557
CLAUDE.md
View File

@ -2,482 +2,7 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
EQ2Go is a Go rewrite of the EverQuest II server emulator from C++ EQ2EMu. Implements core MMO server functionality with authentication, world simulation, and game mechanics using modern Go practices.
## Development Commands
### Building the Project
```bash
# Build login server
go build -o bin/login_server ./cmd/login_server
# Build world server
go build -o bin/world_server ./cmd/world_server
# Build both servers
go build ./cmd/...
```
### Running the Servers
```bash
# Run login server (requires login_config.json)
./bin/login_server
# Run world server (creates world_config.json with defaults if missing)
./bin/world_server
# With custom configuration
./bin/world_server -listen-port 9001 -web-port 8081 -log-level debug
```
### Testing
```bash
# Run all tests
go test ./...
# Test specific packages
go test ./internal/udp
go test ./internal/packets/parser
# Run tests with verbose output
go test -v ./...
# Test with race detection
go test -race ./...
```
### Development Tools
```bash
# Format code
go fmt ./...
# Run linter (if golangci-lint is installed)
golangci-lint run
# Tidy modules
go mod tidy
# Check for vulnerabilities
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
```
## Architecture Overview
### Core Components
**Login Server** - Client authentication, character management, world server coordination
**World Server** - Game simulation engine, zones, NPCs, combat, quests, web admin interface
**Shared Libraries:**
- `common/` - EQ2-specific data types and constants
- `database/` - SQLite wrapper with simplified query interface
- `udp/` - Custom UDP protocol with reliability layer
- `packets/` - XML-driven packet definition parser
- `achievements/` - Achievement system
- `spawn/` - Base game entity system (positioning, commands, scripting)
- `entity/` - Combat-capable entities with spell effects, stats, pet management
- `spells/` - Complete spell system with casting mechanics and processing
- `titles/` - Character title system with achievement integration
- `trade/` - Player trading system with item/coin exchange and validation
- `object/` - Interactive objects extending spawn system (merchants, transporters, devices, collectors)
- `races/` - Race system with all EQ2 races, alignment classification, stat modifiers, and entity integration
- `classes/` - Class system with all EQ2 classes, progression paths, stat bonuses, and entity integration
- `widget/` - Interactive widget system for doors, lifts, and other usable objects with spawn integration
- `transmute/` - Item transmutation system for converting items into crafting materials with skill requirements
- `skills/` - Character skill system with master skill list, player skills, bonuses, and progression mechanics
- `sign/` - Interactive sign system extending spawn functionality with zone transport, entity commands, and text display
- `appearances/` - Appearance management system with client version compatibility and efficient ID-based lookups
- `factions/` - Faction reputation system with player standings, consideration levels, and inter-faction relationships
- `ground_spawn/` - Harvestable resource node system with skill-based harvesting, rare item generation, and multi-attempt mechanics
- `languages/` - Multilingual character communication system with master language registry and player-specific language knowledge
### Network Protocol
EverQuest II UDP protocol with reliability layer, RC4 encryption, CRC validation, connection management, packet combining.
### Database Layer
SQLite with simplified query interface, transactions, connection pooling, parameter binding.
### Packet System
XML-driven packet definitions with version-specific formats, conditional fields, templates, dynamic arrays.
## Key Files and Locations
**Server Entry Points:**
- `cmd/login_server/main.go`: Login server startup and configuration
- `cmd/world_server/main.go`: World server startup with command-line options
**Core Data Structures:**
- `internal/common/types.go`: EQ2-specific types (EQ2Color, EQ2Equipment, AppearanceData, etc.)
- `internal/common/visual_states.go`: Visual states, emotes, and spell visuals with version management
- `internal/common/variables.go`: Configuration variable management with type conversion utilities
**Network Implementation:**
- `internal/udp/server.go`: Multi-connection UDP server
- `internal/udp/connection.go`: Individual client connection handling
- `internal/udp/protocol.go`: Protocol packet types and constants
**Database:**
- `internal/database/wrapper.go`: Simplified SQLite interface
- Database files: `login.db`, `world.db` (created automatically)
**Spawn System:**
- `internal/spawn/spawn.go`: Base spawn entity with position, stats, commands, and scripting
- `internal/spawn/spawn_lists.go`: Spawn location and entry management
- `internal/spawn/README.md`: Comprehensive spawn system documentation
**Entity System:**
- `internal/entity/entity.go`: Combat-capable spawn extension with spell casting and pet management
- `internal/entity/info_struct.go`: Comprehensive character statistics and information management
- `internal/entity/spell_effects.go`: DEPRECATED - now imports from spells package
- `internal/entity/README.md`: Complete entity system documentation
**Spells System:**
- Core: `spell_data.go`, `spell.go`, `spell_effects.go`, `spell_manager.go`, `constants.go`
- Processing: `process_constants.go`, `spell_process.go`, `spell_targeting.go`, `spell_resources.go`
- Docs: `README.md`, `SPELL_PROCESS.md`
**Titles System:**
- `internal/titles/title.go`: Core title data structures and player title management
- `internal/titles/constants.go`: Title system constants, categories, and rarity levels
- `internal/titles/master_list.go`: Global title registry and management with categorization
- `internal/titles/player_titles.go`: Individual player title collections and active title tracking
- `internal/titles/title_manager.go`: Central title system coordinator with background cleanup
- `internal/titles/integration.go`: Integration systems for earning titles through achievements, quests, PvP, events
- `internal/titles/README.md`: Complete title system documentation
**Trade System:**
- `internal/trade/trade.go`: Core trade mechanics with two-party item/coin exchange
- `internal/trade/types.go`: Trade data structures, participants, and validation
- `internal/trade/constants.go`: Trade constants, error codes, and configuration
- `internal/trade/utils.go`: Coin calculations, validation helpers, and formatting
- `internal/trade/manager.go`: Trade service with high-level management and placeholders
**Object System:**
- `internal/object/object.go`: Core object mechanics extending spawn functionality with interaction
- `internal/object/constants.go`: Object constants, spawn types, and interaction types
- `internal/object/manager.go`: Object manager with zone-based and type-based indexing
- `internal/object/integration.go`: Spawn system integration with ObjectSpawn wrapper
- `internal/object/interfaces.go`: Integration interfaces for trade/spell systems and entity adapters
**Race System:**
- `internal/races/races.go`: Core race management with all 21 EQ2 races and alignment mapping
- `internal/races/constants.go`: Race IDs, names, and display constants
- `internal/races/utils.go`: Race utilities with stat modifiers, descriptions, and parsing
- `internal/races/integration.go`: Entity system integration with RaceAware interface
- `internal/races/manager.go`: High-level race management with statistics and command processing
**Class System:**
- `internal/classes/classes.go`: Core class management with all EQ2 adventure and tradeskill classes
- `internal/classes/constants.go`: Class IDs, names, and display constants with progression hierarchy
- `internal/classes/utils.go`: Class utilities with progression paths, stat bonuses, and parsing
- `internal/classes/integration.go`: Entity system integration with ClassAware interface
- `internal/classes/manager.go`: High-level class management with statistics and command processing
**Widget System:**
- `internal/widget/widget.go`: Interactive spawn objects like doors and lifts with movement and state management
- `internal/widget/constants.go`: Widget type constants and display name mappings
- `internal/widget/actions.go`: Widget interaction logic with open/close mechanics and client handling
- `internal/widget/interfaces.go`: Integration interfaces for client and zone systems with spawn wrapper
- `internal/widget/manager.go`: Widget management with timer handling and linked spawn resolution
**Transmute System:**
- `internal/transmute/transmute.go`: Core transmutation logic with item validation and material generation
- `internal/transmute/types.go`: Transmute data structures and interface definitions for system integration
- `internal/transmute/constants.go`: Transmutation constants including probabilities and item flags
- `internal/transmute/manager.go`: High-level transmute management with statistics and command processing
- `internal/transmute/database.go`: Database operations for transmuting tier configuration
- `internal/transmute/packet_builder.go`: Client packet construction for transmutation UI and responses
**Skills System:**
- `internal/skills/types.go`: Core skill data structures with Skill, SkillBonus, and SkillBonusValue types
- `internal/skills/constants.go`: Skill type constants, special skill IDs, and skill increase parameters
- `internal/skills/master_skill_list.go`: Master skill registry with all available skills and packet building
- `internal/skills/player_skill_list.go`: Individual player skill management with values, caps, and updates
- `internal/skills/skill_bonuses.go`: Skill bonus system for spell-based skill modifications and calculations
- `internal/skills/manager.go`: High-level skill management with statistics, validation, and command processing
- `internal/skills/integration.go`: Integration interfaces including SkillAware and EntitySkillAdapter
**Sign System:**
- `internal/sign/types.go`: Core Sign struct extending spawn functionality with widget and zone transport properties
- `internal/sign/constants.go`: Sign type constants, default values, and configuration parameters
- `internal/sign/sign.go`: Sign functionality including copy, serialization, usage handling, and validation
- `internal/sign/interfaces.go`: Integration interfaces with SignAware, SignAdapter, and system dependencies
- `internal/sign/manager.go`: Sign management with zone loading, statistics, validation, and command processing
**Appearances System:**
- `internal/appearances/types.go`: Core Appearance struct with ID, name, and client version compatibility
- `internal/appearances/constants.go`: Hash search constants and client version parameters
- `internal/appearances/appearances.go`: Appearance collection management with thread-safe operations
- `internal/appearances/manager.go`: High-level appearance management with database integration and statistics
- `internal/appearances/interfaces.go`: Integration interfaces with caching, entity adapters, and system dependencies
**Factions System:**
- `internal/factions/types.go`: Core Faction struct with reputation properties and validation methods
- `internal/factions/constants.go`: Faction value limits, consideration levels, and calculation constants
- `internal/factions/master_faction_list.go`: Master faction registry with hostile/friendly relationships
- `internal/factions/player_faction.go`: Individual player faction standings with consideration and percentage calculations
- `internal/factions/manager.go`: High-level faction management with statistics, validation, and command processing
- `internal/factions/interfaces.go`: Integration interfaces with entity adapters, player managers, and system dependencies
**Ground Spawn System:**
- `internal/ground_spawn/constants.go`: Harvest type constants, skill names, rarity flags, and configuration defaults
- `internal/ground_spawn/types.go`: Core GroundSpawn struct, harvest context data, result structures, and statistics tracking
- `internal/ground_spawn/ground_spawn.go`: Core ground spawn functionality with complex harvest processing and skill-based mechanics
- `internal/ground_spawn/interfaces.go`: Integration interfaces with database, players, items, skills, and event handling systems
- `internal/ground_spawn/manager.go`: High-level ground spawn management with respawn scheduling, statistics, and command processing
**Languages System:**
- `internal/languages/constants.go`: Language ID constants, validation limits, and system configuration values
- `internal/languages/types.go`: Core Language struct, master language list, player language list, and statistics tracking
- `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
- `internal/quests/quest.go`: Core quest functionality including step management, progress tracking, update checking, and validation
- `internal/quests/prerequisites.go`: Quest prerequisite management with class, race, level, faction, and quest requirements
- `internal/quests/rewards.go`: Quest reward system with coins, experience, status, faction rewards, and level-based calculations
- `internal/quests/actions.go`: Quest action system for Lua script execution on completion, progress, and failure events
- `internal/quests/manager.go`: MasterQuestList and QuestManager for system-wide quest management and player quest tracking
- `internal/quests/interfaces.go`: Integration interfaces with player, client, spawn, item systems and QuestSystemAdapter for complete quest lifecycle management
**NPC System:**
- `internal/npc/constants.go`: AI strategy constants, randomization flags, pet types, cast types, and system limits
- `internal/npc/types.go`: Core NPC struct extending Entity, NPCSpell configurations, skill bonuses, movement locations, brain system
- `internal/npc/npc.go`: Complete NPC functionality with AI, combat, spell casting, movement, appearance randomization, and validation
- `internal/npc/manager.go`: High-level NPC management with zone indexing, appearance tracking, combat processing, and statistics
- `internal/npc/interfaces.go`: Integration interfaces with database, spell/skill/appearance systems, combat, movement, and entity adapters
**NPC AI System:**
- `internal/npc/ai/constants.go`: AI timing constants, combat ranges, hate limits, brain types, and decision parameters
- `internal/npc/ai/types.go`: HateList and EncounterList management, BrainState tracking, performance statistics, and thread-safe operations
- `internal/npc/ai/brain.go`: BaseBrain with complete AI logic including hate management, encounter tracking, spell/melee processing, and combat decisions
- `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
- `internal/packets/parser/`: Parser implementation for XML packet definitions
## Configuration
**Login Server**: Requires `login_config.json` with database and server settings
**World Server**: Creates `world_config.json` with defaults:
```json
{
"listen_addr": "0.0.0.0",
"listen_port": 9000,
"max_clients": 1000,
"web_port": 8080,
"database_path": "world.db",
"log_level": "info"
}
```
Command-line flags override JSON configuration.
## Important Dependencies
**Core Dependencies:**
- `zombiezen.com/go/sqlite`: Modern SQLite driver
- `golang.org/x/crypto`: Cryptographic functions
**Module Information:**
- Go version: 1.24.5
- Module name: `eq2emu`
## Development Notes
**Architecture Transition**: This Go implementation is based on the extensive C++ EQ2EMu codebase documented in `EQ2EMU_Architecture_White_Paper.md`. The Go version maintains compatibility with the original protocol while modernizing the implementation.
**Packet Handling**: The XML packet definition system allows for easy addition of new packet types without code changes. See `internal/packets/PARSER.md` for syntax reference.
**Database Schema**: Currently uses SQLite for development/testing. Production deployments may require migration to PostgreSQL or MySQL for better concurrent access.
**Spawn System**: The spawn system (`internal/spawn/`) provides the foundation for all game entities. It's converted from the C++ EQ2EMu codebase with modern Go concurrency patterns.
**Entity System**: The entity system (`internal/entity/`) extends spawns with combat capabilities, implementing the complete EverQuest II character statistics system, spell effect management, and pet systems. Key features include:
- **InfoStruct**: Complete character statistics (attributes, resistances, experience, currency, equipment data)
- **Entity**: Combat-capable spawn with spell casting, pet management, and bonus calculations
- **Thread Safety**: All systems use proper Go concurrency patterns with mutexes and atomic operations
- **Stat Calculations**: Dynamic stat calculations with bonuses from equipment, spells, and other sources
- **Pet Management**: Support for multiple pet types (summon, charm, deity, cosmetic) with proper ownership tracking
**Spells System**: Complete spell management with spell definitions, effect system, and real-time casting engine. Features spell processing (50ms intervals), comprehensive targeting (self/single/group/AOE), resource management (power/health/concentration), cast/recast timers, interrupt handling, heroic opportunities, and thread-safe operations.
**Titles System**: Character title management with prefix/suffix positioning, categories, rarity levels, achievement integration, and thread-safe operations.
**Trade System**: Player-to-player trading with item/coin exchange, validation (no-trade, heirloom restrictions), slot management, acceptance workflow, bot trading support, and client version compatibility (6/12 slots).
**Object System**: Interactive world objects extending spawn system with merchants, transporters, devices, and collectors. Features clickable interaction, command handling, device IDs, size randomization, transport/teleport support, and complete spawn integration with entity/item adapters for trade/spell system compatibility.
**Race System**: Complete EverQuest II race management with all 21 races (Human through Aerakyn), alignment classification (good/evil/neutral), racial stat modifiers, starting locations, lore descriptions, and full entity system integration. Features randomization by alignment, compatibility checking, usage statistics, and RaceAware interface for seamless integration with existing systems.
**Class System**: Complete EverQuest II class management with all 58 classes (adventure and tradeskill), hierarchical progression paths (Commoner → Base → Secondary → Final), stat bonuses, starting stat calculations, and full entity system integration. Features class transition validation, progression tracking, usage statistics, and ClassAware interface for seamless integration with existing systems. Includes all 4 base classes (Fighter, Priest, Mage, Scout) with their complete specialization trees.
**Visual States System**: Manages visual animations, emotes, and spell visuals with client version support. Features version-based emote/visual selection, animation ID mapping, message formatting (targeted/untargeted), and thread-safe concurrent access. Supports both named lookups and ID-based lookups for efficient client communication.
**Variables System**: Configuration variable management for runtime settings and game parameters. Features type-safe value conversion (int, float, bool), partial name matching, variable cloning and merging, comment support for documentation, and thread-safe operations. Used for game rules, server settings, and dynamic configuration.
**Widget System**: Interactive spawn objects like doors, lifts, and other usable world elements. Features open/close state management, position-based movement with timers, sound integration, linked widget chains (action/linked spawns), house integration for player housing, multi-floor lift support, and complete spawn system integration. Supports complex interactions like transporter integration and custom scripted behaviors.
**Transmute System**: Item transmutation system allowing players to convert items into crafting materials. Features tier-based material generation, skill requirement validation, probabilistic loot rolls (15% both materials, 75%/25% split), automatic skill progression, request state management, and comprehensive validation. Supports level-based transmuting tiers with four material types (fragments, powder, infusions, mana) and integrates with item, spell, and skill systems.
**Skills System**: Complete character skill system with master skill registry and individual player skill management. Features all EQ2 skill types (weaponry, spellcasting, avoidance, armor, harvesting, artisan, etc.), skill bonuses from spells, skill progression with automatic increases, disarm skill checks for chests, and comprehensive skill value calculations. Supports skill caps, type-based operations, packet building for client updates, and full integration with entity system through SkillAware interface. Includes special handling for weapon skills, crafting skills, and language skills with version-specific client compatibility.
**Sign System**: Interactive sign system extending spawn functionality for in-world text displays and zone transport. Features two sign types (generic and zone transport), zone teleportation with coordinate validation, entity command processing, sign marking system, transporter integration, distance-based interaction, and comprehensive text display with location/heading options. Supports quest requirement checking, instance zone handling, size randomization on copy, validation system, and full spawn system integration. Includes SignAware interface and SignAdapter for seamless integration with existing entity systems.
**Appearances System**: Comprehensive appearance management system handling visual character and entity representations with client version compatibility. Features efficient hash-based ID lookups, client version compatibility checking, name-based searching, statistics tracking, and thread-safe operations. Supports caching with SimpleAppearanceCache and CachedAppearanceManager, database integration for persistence, entity appearance adapters for seamless integration, and comprehensive validation. Includes AppearanceAware interface and EntityAppearanceAdapter for entity system integration. Designed to handle large appearance collections with sparse ID ranges efficiently using hash tables as noted in C++ comments.
**Factions System**: Complete faction reputation system managing player standings and inter-faction relationships. Features master faction list with hostile/friendly relationships, individual player faction standings with consideration levels (-4 to 4), percentage calculations within consideration ranges, attack determination based on faction standing, and comprehensive faction value management (-50000 to 50000 range). Supports special faction handling (IDs <= 10), faction increase/decrease with configurable amounts, packet building for client updates, statistics tracking, and thread-safe operations. Includes EntityFactionAdapter and PlayerFactionManager for seamless integration with entity and player systems. Maintains exact C++ calculation formulas for consideration levels and percentage values.
**Ground Spawn System**: Harvestable resource node system extending spawn functionality for gathering, mining, fishing, trapping, and foresting. Features skill-based table selection, probabilistic harvest outcomes (1/3/5/10 items + rare/imbue types), multi-attempt harvesting sessions, skill progression mechanics, and automatic respawn scheduling. Implements complex C++ harvest logic with table filtering by skill/level requirements, random item selection from filtered pools, grid-based location restrictions, and comprehensive item reward processing. Supports collection vs. harvesting skill differentiation, harvest message generation, spell integration for casting animations, and statistics tracking. Includes PlayerGroundSpawnAdapter and HarvestEventAdapter for seamless integration with player and event systems. Thread-safe operations with separate mutexes for harvest processing and usage handling.
**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 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.
**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.
**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
## Code Quality Requirements
**Function Documentation**: All functions must have thorough documentation explaining their purpose, parameters, return values, and any important behavior. Do NOT follow the standard Go convention of starting comments with the function name - instead, write clear explanations of what the function does.
@ -502,4 +27,82 @@ func NewServer(addr string, handler PacketHandler, config ...Config) (*Server, e
- Include examples in documentation for complex functions
- Explain any non-obvious algorithms or business logic
- Document error conditions and edge cases
- For packet-related code, reference the relevant XML definitions or protocol specifications
- For packet-related code, reference the relevant XML definitions or protocol specifications
## Project Overview
EQ2Go is a Go rewrite of the EverQuest II server emulator from C++ EQ2EMu. Implements core MMO server functionality with authentication, world simulation, and game mechanics using modern Go practices.
## Development Commands
```bash
# Build servers
go build ./cmd/...
# Test with race detection
go test -race ./...
# Format and tidy
go fmt ./...; go mod tidy
```
## Architecture Overview
**Login Server** - Client authentication, character management, world server coordination
**World Server** - Game simulation engine, zones, NPCs, combat, quests, web admin interface
**Core Systems:**
- Network: EverQuest II UDP protocol with reliability layer, RC4 encryption, CRC validation
- Database: SQLite with simplified query interface, transactions, connection pooling
- Packets: XML-driven packet definitions with version-specific formats
- Events: 100+ EQ2 functions organized by domain (health/attributes/movement/combat/misc)
## Key Systems
**Spawn System**: Base game entity system with positioning, commands, and scripting. Foundation for all game entities with modern Go concurrency patterns.
**Entity System**: Combat-capable spawns with spell effects, stats, pet management. Includes InfoStruct for complete character statistics, spell casting, pet management, and bonus calculations.
**Spells System**: Complete spell management with 50ms processing intervals, comprehensive targeting (self/single/group/AOE), resource management (power/health/concentration), cast/recast timers, interrupt handling, and heroic opportunities.
**Player System**: Complete player character management extending Entity with experience systems, skill progression, spell management, combat mechanics, social features, currency management, quest integration, spawn management, character flags, and comprehensive cleanup systems. Database integration uses zombiezen SQLite with complete persistence.
**NPC System**: Non-player characters extending Entity with complete AI, combat, and spell casting. Features brain system, spell management, skill bonuses, movement with runback mechanics, appearance randomization, and AI strategies. Includes specialized brain types (Combat/NonCombat/Blank/Lua/DumbFire pets).
**Quest System**: Complete quest management with multiple step types (kill/chat/obtain/location/spell/normal/craft/harvest), comprehensive prerequisite system, flexible reward system, step-based progress tracking, Lua action system, quest sharing, and repeatable quest support.
**Event System**: 137 EQ2 functions organized by domain in `internal/events/functions/` - health.go (23 functions), attributes.go (24 functions), movement.go (27 functions), combat.go (36 functions), misc.go (27 functions). Features EventContext with fluent API, thread-safe operations, minimal overhead, type-safe parameter access, and comprehensive registry system.
**Trade/Object/Widget/Transmute/Skills/Titles/Races/Classes/Languages/Factions/Ground Spawn/Appearances/Sign Systems**: Complete implementations with full EQ2 compatibility, thread-safe operations, and comprehensive integration interfaces.
## Development Patterns
**Package Structure**: Each system follows consistent structure with types.go, constants.go, manager.go, interfaces.go, database.go (where applicable), and README.md.
**Thread Safety**: All systems implement proper Go concurrency patterns with sync.RWMutex for read-heavy operations, atomic operations for flags/counters, proper lock ordering, and channel-based communication.
**Integration Patterns**: *Aware interfaces for feature detection, adapter pattern for bridging systems, manager pattern for coordination, event-driven architecture for notifications.
**Entity-Pass-By-Pointer**: Always pass `*entity.Entity` (not `entity.Entity`) to avoid copying locks and improve performance.
**Name Padding Handling**: Player names from database may have null byte padding from `[128]byte` arrays. Always use `strings.TrimSpace(strings.Trim(name, "\x00"))`.
## Configuration
**World Server** creates `world_config.json` with defaults. Command-line flags override JSON configuration.
## Dependencies
- `zombiezen.com/go/sqlite`: Modern SQLite driver
- `golang.org/x/crypto`: Cryptographic functions
- Go version: 1.24.5, Module: `eq2emu`
## Important Notes
**C++ Reference Code**: The original C++ EQ2EMu source code is available in `/old` directory for functionality reference only. Use this to understand game mechanics, algorithms, and expected behavior - NOT for structure or patterns. The Go implementation uses modern Go practices and patterns.
**Database Migration**: Uses zombiezen SQLite with `sqlite.Conn`, `sqlitex.Execute` with `ExecOptions` and `ResultFunc`, proper `found` flag handling, and thread-safe connection management.
**Architecture Transition**: Converted from C++ EQ2EMu codebase maintaining protocol compatibility while modernizing implementation.
**Testing**: Focus on UDP protocol and packet parsing for client compatibility. All systems include comprehensive test suites with concurrency testing.

327
internal/commands/README.md Normal file
View File

@ -0,0 +1,327 @@
# EQ2Go Command System
The EQ2Go command system provides a comprehensive framework for handling player commands, admin commands, and console commands in the EverQuest II server emulator. This system is converted from the original C++ EQ2EMu codebase while leveraging modern Go patterns and practices.
## Architecture Overview
The command system consists of several key components:
- **CommandManager**: Central registry for all commands with thread-safe operations
- **CommandContext**: Execution context providing access to players, zones, arguments, and messaging
- **Command Types**: Different command categories (Player, Admin, Console, etc.)
- **Subcommand Support**: Hierarchical command structure for complex operations
## Core Components
### CommandManager
The `CommandManager` is the central hub that:
- Registers commands by name with case-insensitive lookup
- Manages subcommands under parent commands
- Executes commands with proper admin level checking
- Parses raw command strings into structured contexts
### CommandContext
The `CommandContext` provides:
- Type-safe argument parsing (string, int, float, bool)
- Message queuing with channel and color support
- Validation helpers for argument counts and requirements
- Access to game objects (player, zone, target)
- Result storage for complex operations
### Command Types
Commands are categorized by type:
- **Player Commands** (`CommandTypePlayer`): Basic player actions like `/say`, `/tell`, `/who`
- **Admin Commands** (`CommandTypeAdmin`): GM/Admin tools like `/kick`, `/ban`, `/summon`
- **Console Commands** (`CommandTypeConsole`): Server management from console
- **Specialized Types**: Spawn, Zone, Guild, Item, and Quest commands
## Admin Levels
The system supports four admin levels:
- **Player** (0): Basic player commands
- **Guide** (100): Helper/guide commands
- **GM** (200): Game master commands
- **Admin** (300): Full administrative access
## Usage Examples
### Basic Command Registration
```go
// Initialize the command system
cm, err := InitializeCommands()
if err != nil {
log.Fatalf("Failed to initialize commands: %v", err)
}
// Register a custom command
customCmd := &Command{
Name: "custom",
Type: CommandTypePlayer,
Description: "A custom command",
Usage: "/custom <arg>",
RequiredLevel: AdminLevelPlayer,
Handler: func(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: /custom <arg>")
return nil
}
arg := ctx.Arguments[0]
ctx.AddStatusMessage(fmt.Sprintf("You used custom command with: %s", arg))
return nil
},
}
err = cm.Register(customCmd)
if err != nil {
log.Printf("Failed to register custom command: %v", err)
}
```
### Executing Commands
```go
// Parse and execute a command from a client
err := ExecuteCommand(cm, "/say Hello everyone!", client)
if err != nil {
log.Printf("Command execution failed: %v", err)
}
```
### Subcommand Registration
```go
// Register a parent command
parentCmd := &Command{
Name: "manage",
Type: CommandTypeAdmin,
RequiredLevel: AdminLevelGM,
Handler: func(ctx *CommandContext) error {
ctx.AddErrorMessage("Usage: /manage <player|item|zone> [options]")
return nil
},
}
cm.Register(parentCmd)
// Register subcommands
playerSubCmd := &Command{
Name: "player",
Type: CommandTypeAdmin,
RequiredLevel: AdminLevelGM,
Handler: func(ctx *CommandContext) error {
// Handle player management
return nil
},
}
cm.RegisterSubCommand("manage", playerSubCmd)
```
## Command Implementation
### Player Commands
Located in `player.go`, these include:
- **Communication**: `/say`, `/tell`, `/yell`, `/shout`, `/ooc`, `/emote`
- **Group Management**: `/group`, `/groupsay`, `/gsay`
- **Guild Management**: `/guild`, `/guildsay`, `/officersay`
- **Information**: `/who`, `/time`, `/location`, `/consider`
- **Character**: `/afk`, `/anon`, `/lfg`, `/inventory`
- **Utilities**: `/help`, `/quit`, `/trade`, `/quest`
### Admin Commands
Located in `admin.go`, these include:
- **Player Management**: `/kick`, `/ban`, `/unban`, `/summon`, `/goto`
- **Communication**: `/broadcast`, `/announce`
- **Spawn Management**: `/spawn`, `/npc`
- **Item Management**: `/item`, `/giveitem`
- **Character Modification**: `/modify`, `/setlevel`, `/setclass`
- **Server Management**: `/reload`, `/shutdown`, `/cancelshutdown`
- **GM Utilities**: `/invisible`, `/invulnerable`, `/speed`, `/flymode`
- **Information**: `/info`, `/version`
### Console Commands
Located in `console.go`, these include prefixed versions of admin commands for console use:
- **Player Management**: `console_ban`, `console_kick`, `console_unban`
- **Communication**: `console_broadcast`, `console_announce`, `console_tell`
- **Information**: `console_guild`, `player`, `console_zone`, `console_who`
- **Server Management**: `console_reload`, `console_shutdown`, `exit`
- **MOTD Management**: `getmotd`, `setmotd`
- **Rules Management**: `rules`
## Message Channels and Colors
Commands can send messages through various channels:
### Channels
- `ChannelSay` (8): Local say messages
- `ChannelTell` (28): Private messages
- `ChannelBroadcast` (92): Server-wide broadcasts
- `ChannelError` (3): Error messages
- `ChannelStatus` (4): Status updates
### Colors
- `ColorWhite` (254): Standard text
- `ColorRed` (3): Errors and warnings
- `ColorYellow` (5): Status messages
- `ColorChatRelation` (4): Relationship text
## Error Handling
The command system provides comprehensive error handling:
```go
func HandleExampleCommand(ctx *CommandContext) error {
// Validate player is present
if err := ctx.RequirePlayer(); err != nil {
return err
}
// Validate argument count
if err := ctx.ValidateArgumentCount(1, 3); err != nil {
ctx.AddErrorMessage("Usage: /example <required> [optional1] [optional2]")
return nil
}
// Validate admin level
if err := ctx.RequireAdminLevel(AdminLevelGM); err != nil {
return err
}
// Command logic here...
return nil
}
```
## Testing
The command system includes comprehensive tests in `commands_test.go`:
```bash
# Run all command tests
go test ./internal/commands -v
# Run specific test
go test ./internal/commands -run TestCommandManager_Execute
```
Tests cover:
- Command registration and retrieval
- Argument parsing and validation
- Message handling
- Admin level checking
- Subcommand functionality
- Error conditions
## Integration with Server
To integrate the command system with the server:
```go
// Initialize commands during server startup
commandManager, err := commands.InitializeCommands()
if err != nil {
log.Fatalf("Failed to initialize commands: %v", err)
}
// Handle incoming command from client
func handleClientCommand(client ClientInterface, rawCommand string) {
err := commands.ExecuteCommand(commandManager, rawCommand, client)
if err != nil {
log.Printf("Command execution error: %v", err)
}
}
```
## Thread Safety
The command system is designed to be thread-safe:
- CommandManager uses read/write mutexes for concurrent access
- CommandContext uses mutexes for message and result storage
- All operations are safe for concurrent use
## Extension Points
The system is designed for easy extension:
### Custom Command Types
```go
const CommandTypeCustom CommandType = 100
func (ct CommandType) String() string {
switch ct {
case CommandTypeCustom:
return "custom"
default:
return CommandType(ct).String()
}
}
```
### Custom Handlers
```go
func MyCustomHandler(ctx *CommandContext) error {
// Custom command logic
ctx.AddStatusMessage("Custom command executed!")
return nil
}
```
## Performance Considerations
- Commands are indexed by name in a hash map for O(1) lookup
- Case-insensitive matching uses normalized lowercase keys
- Message batching reduces client communication overhead
- Argument parsing is lazy and type-safe
## Migration from C++
Key differences from the original C++ implementation:
1. **Type Safety**: Go's type system prevents many runtime errors
2. **Memory Management**: Automatic garbage collection eliminates memory leaks
3. **Concurrency**: Native goroutine support for concurrent operations
4. **Error Handling**: Explicit error returns instead of exceptions
5. **Testing**: Built-in testing framework with comprehensive coverage
## Future Enhancements
Planned improvements:
- Lua script integration for dynamic commands
- Command aliasing system
- Advanced permission system
- Command cooldowns and rate limiting
- Audit logging for admin commands
- Dynamic command loading/unloading
## Troubleshooting
### Common Issues
**Command Not Found**: Ensure command is registered and name matches exactly
**Insufficient Privileges**: Check admin level requirements
**Argument Validation**: Use proper argument count validation
**Interface Errors**: Ensure client implements ClientInterface correctly
### Debug Output
Enable debug output for command execution:
```go
ctx.AddDefaultMessage(fmt.Sprintf("Debug: Command %s executed with %d args",
ctx.CommandName, ctx.ArgumentCount()))
```
## Conclusion
The EQ2Go command system provides a robust, extensible framework for handling all types of game commands while maintaining compatibility with the original EverQuest II protocol. The system's modular design and comprehensive testing ensure reliability and ease of maintenance.

937
internal/commands/admin.go Normal file
View File

@ -0,0 +1,937 @@
package commands
import (
"fmt"
"strings"
)
// RegisterAdminCommands registers all admin-level commands
func RegisterAdminCommands(cm *CommandManager) error {
commands := []*Command{
// Player management commands
{
Name: "kick",
Type: CommandTypeAdmin,
Description: "Kicks a player from the server",
Usage: "/kick <player> [reason]",
RequiredLevel: AdminLevelGM,
Handler: HandleKick,
},
{
Name: "ban",
Type: CommandTypeAdmin,
Description: "Bans a player from the server",
Usage: "/ban <player> [duration] [reason]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleBan,
},
{
Name: "unban",
Type: CommandTypeAdmin,
Description: "Unbans a player",
Usage: "/unban <player>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleUnban,
},
{
Name: "summon",
Type: CommandTypeAdmin,
Description: "Summons a player to your location",
Usage: "/summon <player>",
RequiredLevel: AdminLevelGM,
Handler: HandleSummon,
},
{
Name: "goto",
Type: CommandTypeAdmin,
Description: "Teleports you to a player or location",
Usage: "/goto <player|x y z>",
RequiredLevel: AdminLevelGM,
Handler: HandleGoto,
},
{
Name: "zone",
Type: CommandTypeAdmin,
Description: "Zone management commands",
Usage: "/zone <player> <zone_name>",
RequiredLevel: AdminLevelGM,
Handler: HandleZonePlayer,
},
// Communication commands
{
Name: "broadcast",
Type: CommandTypeAdmin,
Description: "Broadcasts a message to all players",
Usage: "/broadcast <message>",
RequiredLevel: AdminLevelGM,
Handler: HandleBroadcast,
},
{
Name: "announce",
Type: CommandTypeAdmin,
Description: "Announces a message to all players",
Usage: "/announce <message>",
RequiredLevel: AdminLevelGM,
Handler: HandleAnnounce,
},
// Spawn management commands
{
Name: "spawn",
Type: CommandTypeAdmin,
Description: "Spawn management commands",
Usage: "/spawn <create|remove|list|set> [options]",
RequiredLevel: AdminLevelGM,
Handler: HandleSpawn,
},
{
Name: "npc",
Type: CommandTypeAdmin,
Description: "NPC management commands",
Usage: "/npc <create|remove|edit> [options]",
RequiredLevel: AdminLevelGM,
Handler: HandleNPC,
},
// Item commands
{
Name: "item",
Type: CommandTypeAdmin,
Description: "Item management commands",
Usage: "/item <give|remove|create> [options]",
RequiredLevel: AdminLevelGM,
Handler: HandleItem,
},
{
Name: "giveitem",
Type: CommandTypeAdmin,
Description: "Gives an item to a player",
Usage: "/giveitem <player> <item_id> [quantity]",
RequiredLevel: AdminLevelGM,
Handler: HandleGiveItem,
},
// Character modification commands
{
Name: "modify",
Type: CommandTypeAdmin,
Description: "Modification commands",
Usage: "/modify <character|spawn|zone|quest|item> [options]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleModify,
},
{
Name: "setlevel",
Type: CommandTypeAdmin,
Description: "Sets a player's level",
Usage: "/setlevel <player> <level>",
RequiredLevel: AdminLevelGM,
Handler: HandleSetLevel,
},
{
Name: "setclass",
Type: CommandTypeAdmin,
Description: "Sets a player's class",
Usage: "/setclass <player> <class_id>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleSetClass,
},
// Server management commands
{
Name: "reload",
Type: CommandTypeAdmin,
Description: "Reloads server data",
Usage: "/reload <spells|quests|zones|items|all>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleReload,
},
{
Name: "shutdown",
Type: CommandTypeAdmin,
Description: "Shuts down the server",
Usage: "/shutdown [time_minutes] [reason]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleShutdown,
},
{
Name: "cancelshutdown",
Type: CommandTypeAdmin,
Description: "Cancels a scheduled shutdown",
Usage: "/cancelshutdown",
RequiredLevel: AdminLevelAdmin,
Handler: HandleCancelShutdown,
},
// GM utility commands
{
Name: "invisible",
Type: CommandTypeAdmin,
Description: "Toggles GM invisibility",
Usage: "/invisible",
RequiredLevel: AdminLevelGM,
Handler: HandleInvisible,
},
{
Name: "invulnerable",
Type: CommandTypeAdmin,
Description: "Toggles invulnerability",
Usage: "/invulnerable",
RequiredLevel: AdminLevelGM,
Handler: HandleInvulnerable,
},
{
Name: "speed",
Type: CommandTypeAdmin,
Description: "Sets movement speed",
Usage: "/speed <multiplier>",
RequiredLevel: AdminLevelGM,
Handler: HandleSpeed,
},
{
Name: "flymode",
Type: CommandTypeAdmin,
Description: "Toggles fly mode",
Usage: "/flymode",
RequiredLevel: AdminLevelGM,
Handler: HandleFlyMode,
},
// Information commands
{
Name: "info",
Type: CommandTypeAdmin,
Description: "Shows detailed information about target",
Usage: "/info [target]",
RequiredLevel: AdminLevelGM,
Handler: HandleInfo,
},
{
Name: "version",
Type: CommandTypeAdmin,
Description: "Shows server version information",
Usage: "/version",
RequiredLevel: AdminLevelGM,
Handler: HandleVersion,
},
// Testing commands
{
Name: "test",
Type: CommandTypeAdmin,
Description: "Testing command for development",
Usage: "/test [parameters]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleTest,
},
}
for _, cmd := range commands {
if err := cm.Register(cmd); err != nil {
return fmt.Errorf("failed to register admin command %s: %w", cmd.Name, err)
}
}
// Register subcommands
if err := registerModifySubcommands(cm); err != nil {
return fmt.Errorf("failed to register modify subcommands: %w", err)
}
if err := registerSpawnSubcommands(cm); err != nil {
return fmt.Errorf("failed to register spawn subcommands: %w", err)
}
return nil
}
// registerModifySubcommands registers subcommands for the /modify command
func registerModifySubcommands(cm *CommandManager) error {
subcommands := []*Command{
{
Name: "character",
Type: CommandTypeAdmin,
Description: "Modifies character attributes",
Usage: "/modify character <player> <attribute> <value>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleModifyCharacter,
},
{
Name: "spawn",
Type: CommandTypeAdmin,
Description: "Modifies spawn attributes",
Usage: "/modify spawn <spawn_id> <attribute> <value>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleModifySpawn,
},
{
Name: "zone",
Type: CommandTypeAdmin,
Description: "Modifies zone attributes",
Usage: "/modify zone <zone_id> <attribute> <value>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleModifyZone,
},
}
for _, subcmd := range subcommands {
if err := cm.RegisterSubCommand("modify", subcmd); err != nil {
return err
}
}
return nil
}
// registerSpawnSubcommands registers subcommands for the /spawn command
func registerSpawnSubcommands(cm *CommandManager) error {
subcommands := []*Command{
{
Name: "create",
Type: CommandTypeAdmin,
Description: "Creates a new spawn",
Usage: "/spawn create <type> [options]",
RequiredLevel: AdminLevelGM,
Handler: HandleSpawnCreate,
},
{
Name: "remove",
Type: CommandTypeAdmin,
Description: "Removes a spawn",
Usage: "/spawn remove <spawn_id>",
RequiredLevel: AdminLevelGM,
Handler: HandleSpawnRemove,
},
{
Name: "list",
Type: CommandTypeAdmin,
Description: "Lists spawns in area",
Usage: "/spawn list [radius]",
RequiredLevel: AdminLevelGM,
Handler: HandleSpawnList,
},
}
for _, subcmd := range subcommands {
if err := cm.RegisterSubCommand("spawn", subcmd); err != nil {
return err
}
}
return nil
}
// HandleKick handles the /kick command
func HandleKick(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /kick <player> [reason]")
return nil
}
playerName := ctx.Arguments[0]
reason := "Kicked by administrator"
if ctx.ArgumentCount() > 1 {
reason = ctx.GetRemainingArguments(1)
}
// TODO: Implement actual kick functionality
ctx.AddStatusMessage(fmt.Sprintf("Kicked player '%s' with reason: %s", playerName, reason))
// TODO: Find and disconnect player
// if player := world.FindPlayerByName(playerName); player != nil {
// player.Disconnect(reason)
// broadcast to admins about kick
// } else {
// ctx.AddErrorMessage(fmt.Sprintf("Player '%s' not found", playerName))
// }
return nil
}
// HandleBan handles the /ban command
func HandleBan(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /ban <player> [duration] [reason]")
return nil
}
playerName := ctx.Arguments[0]
duration := "permanent"
reason := "Banned by administrator"
if ctx.ArgumentCount() > 1 {
duration = ctx.Arguments[1]
}
if ctx.ArgumentCount() > 2 {
reason = ctx.GetRemainingArguments(2)
}
// TODO: Implement actual ban functionality
ctx.AddStatusMessage(fmt.Sprintf("Banned player '%s' for %s with reason: %s",
playerName, duration, reason))
return nil
}
// HandleUnban handles the /unban command
func HandleUnban(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: /unban <player>")
return nil
}
playerName := ctx.Arguments[0]
// TODO: Implement actual unban functionality
ctx.AddStatusMessage(fmt.Sprintf("Unbanned player '%s'", playerName))
return nil
}
// HandleSummon handles the /summon command
func HandleSummon(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: /summon <player>")
return nil
}
targetName := ctx.Arguments[0]
// TODO: Implement actual summon functionality
ctx.AddStatusMessage(fmt.Sprintf("Summoned player '%s' to your location", targetName))
// TODO: Find target player and teleport to admin location
// if target := world.FindPlayerByName(targetName); target != nil {
// adminPos := ctx.Player.GetPosition()
// target.Teleport(adminPos.X, adminPos.Y, adminPos.Z, ctx.Zone.GetID())
// } else {
// ctx.AddErrorMessage(fmt.Sprintf("Player '%s' not found", targetName))
// }
return nil
}
// HandleGoto handles the /goto command
func HandleGoto(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, 3); err != nil {
ctx.AddErrorMessage("Usage: /goto <player> or /goto <x> <y> <z>")
return nil
}
if ctx.ArgumentCount() == 1 {
// Go to player
targetName := ctx.Arguments[0]
// TODO: Implement teleport to player
ctx.AddStatusMessage(fmt.Sprintf("Teleported to player '%s'", targetName))
} else if ctx.ArgumentCount() == 3 {
// Go to coordinates
x := ctx.GetArgumentFloat(0, 0)
y := ctx.GetArgumentFloat(1, 0)
z := ctx.GetArgumentFloat(2, 0)
// TODO: Implement teleport to coordinates
ctx.AddStatusMessage(fmt.Sprintf("Teleported to coordinates (%.2f, %.2f, %.2f)", x, y, z))
} else {
ctx.AddErrorMessage("Usage: /goto <player> or /goto <x> <y> <z>")
}
return nil
}
// HandleZonePlayer handles the /zone command
func HandleZonePlayer(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(2, 2); err != nil {
ctx.AddErrorMessage("Usage: /zone <player> <zone_name>")
return nil
}
playerName := ctx.Arguments[0]
zoneName := ctx.Arguments[1]
// TODO: Implement zone transfer
ctx.AddStatusMessage(fmt.Sprintf("Zoned player '%s' to '%s'", playerName, zoneName))
return nil
}
// HandleBroadcast handles the /broadcast command
func HandleBroadcast(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /broadcast <message>")
return nil
}
message := ctx.RawArguments
adminName := ctx.GetPlayerName()
// TODO: Implement server-wide broadcast
ctx.AddMessage(ChannelBroadcast, ColorYellow,
fmt.Sprintf("[BROADCAST] %s: %s", adminName, message))
return nil
}
// HandleAnnounce handles the /announce command
func HandleAnnounce(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /announce <message>")
return nil
}
message := ctx.RawArguments
// TODO: Implement server announcement
ctx.AddMessage(ChannelBroadcast, ColorRed, fmt.Sprintf("[ANNOUNCEMENT] %s", message))
return nil
}
// HandleSpawn handles the main /spawn command
func HandleSpawn(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: /spawn <create|remove|list|set> [options]")
ctx.AddErrorMessage("Use /spawn <subcommand> for specific help")
return nil
}
// Subcommands are handled by the command manager
return fmt.Errorf("spawn subcommand '%s' not found", ctx.Arguments[0])
}
// HandleSpawnCreate handles the /spawn create subcommand
func HandleSpawnCreate(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /spawn create <type> [name] [level]")
return nil
}
spawnType := ctx.Arguments[0]
spawnName := "New Spawn"
spawnLevel := 1
if ctx.ArgumentCount() > 1 {
spawnName = ctx.Arguments[1]
}
if ctx.ArgumentCount() > 2 {
spawnLevel = ctx.GetArgumentInt(2, 1)
}
// TODO: Implement spawn creation
ctx.AddStatusMessage(fmt.Sprintf("Created %s spawn '%s' (level %d) at your location",
spawnType, spawnName, spawnLevel))
return nil
}
// HandleSpawnRemove handles the /spawn remove subcommand
func HandleSpawnRemove(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: /spawn remove <spawn_id>")
return nil
}
spawnID := ctx.GetArgumentInt(0, 0)
if spawnID <= 0 {
ctx.AddErrorMessage("Invalid spawn ID")
return nil
}
// TODO: Implement spawn removal
ctx.AddStatusMessage(fmt.Sprintf("Removed spawn with ID %d", spawnID))
return nil
}
// HandleSpawnList handles the /spawn list subcommand
func HandleSpawnList(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
radius := 50.0 // Default radius
if ctx.ArgumentCount() > 0 {
radius = ctx.GetArgumentFloat(0, 50.0)
}
// TODO: Implement spawn listing
ctx.AddStatusMessage(fmt.Sprintf("Spawns within %.1f units:", radius))
ctx.AddStatusMessage(" ID: 1, Name: Test NPC, Level: 10, Distance: 5.2")
ctx.AddStatusMessage("Total: 1 spawn found")
return nil
}
// HandleNPC handles the /npc command
func HandleNPC(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: /npc <create|remove|edit> [options]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "create":
// TODO: Implement NPC creation
ctx.AddStatusMessage("NPC creation (not yet implemented)")
case "remove":
// TODO: Implement NPC removal
ctx.AddStatusMessage("NPC removal (not yet implemented)")
case "edit":
// TODO: Implement NPC editing
ctx.AddStatusMessage("NPC editing (not yet implemented)")
default:
ctx.AddErrorMessage("Usage: /npc <create|remove|edit> [options]")
}
return nil
}
// HandleItem handles the /item command
func HandleItem(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: /item <give|remove|create> [options]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "give":
if ctx.ArgumentCount() < 3 {
ctx.AddErrorMessage("Usage: /item give <player> <item_id> [quantity]")
return nil
}
return handleItemGive(ctx)
case "remove":
// TODO: Implement item removal
ctx.AddStatusMessage("Item removal (not yet implemented)")
case "create":
// TODO: Implement item creation
ctx.AddStatusMessage("Item creation (not yet implemented)")
default:
ctx.AddErrorMessage("Usage: /item <give|remove|create> [options]")
}
return nil
}
// handleItemGive handles giving items to players
func handleItemGive(ctx *CommandContext) error {
playerName := ctx.Arguments[1]
itemID := ctx.GetArgumentInt(2, 0)
quantity := ctx.GetArgumentInt(3, 1)
if itemID <= 0 {
ctx.AddErrorMessage("Invalid item ID")
return nil
}
// TODO: Implement item giving
ctx.AddStatusMessage(fmt.Sprintf("Gave %d x item %d to player '%s'",
quantity, itemID, playerName))
return nil
}
// HandleGiveItem handles the /giveitem command
func HandleGiveItem(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(2, 3); err != nil {
ctx.AddErrorMessage("Usage: /giveitem <player> <item_id> [quantity]")
return nil
}
playerName := ctx.Arguments[0]
itemID := ctx.GetArgumentInt(1, 0)
quantity := ctx.GetArgumentInt(2, 1)
if itemID <= 0 {
ctx.AddErrorMessage("Invalid item ID")
return nil
}
// TODO: Implement item giving
ctx.AddStatusMessage(fmt.Sprintf("Gave %d x item %d to player '%s'",
quantity, itemID, playerName))
return nil
}
// HandleModify handles the main /modify command
func HandleModify(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: /modify <character|spawn|zone|quest|item> [options]")
return nil
}
// Subcommands are handled by the command manager
return fmt.Errorf("modify subcommand '%s' not found", ctx.Arguments[0])
}
// HandleModifyCharacter handles the /modify character subcommand
func HandleModifyCharacter(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(3, 3); err != nil {
ctx.AddErrorMessage("Usage: /modify character <player> <attribute> <value>")
return nil
}
playerName := ctx.Arguments[0]
attribute := ctx.Arguments[1]
value := ctx.Arguments[2]
// TODO: Implement character modification
ctx.AddStatusMessage(fmt.Sprintf("Modified %s's %s to %s", playerName, attribute, value))
return nil
}
// HandleModifySpawn handles the /modify spawn subcommand
func HandleModifySpawn(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(3, 3); err != nil {
ctx.AddErrorMessage("Usage: /modify spawn <spawn_id> <attribute> <value>")
return nil
}
spawnID := ctx.GetArgumentInt(0, 0)
attribute := ctx.Arguments[1]
value := ctx.Arguments[2]
if spawnID <= 0 {
ctx.AddErrorMessage("Invalid spawn ID")
return nil
}
// TODO: Implement spawn modification
ctx.AddStatusMessage(fmt.Sprintf("Modified spawn %d's %s to %s", spawnID, attribute, value))
return nil
}
// HandleModifyZone handles the /modify zone subcommand
func HandleModifyZone(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(3, 3); err != nil {
ctx.AddErrorMessage("Usage: /modify zone <zone_id> <attribute> <value>")
return nil
}
zoneID := ctx.GetArgumentInt(0, 0)
attribute := ctx.Arguments[1]
value := ctx.Arguments[2]
if zoneID <= 0 {
ctx.AddErrorMessage("Invalid zone ID")
return nil
}
// TODO: Implement zone modification
ctx.AddStatusMessage(fmt.Sprintf("Modified zone %d's %s to %s", zoneID, attribute, value))
return nil
}
// HandleSetLevel handles the /setlevel command
func HandleSetLevel(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(2, 2); err != nil {
ctx.AddErrorMessage("Usage: /setlevel <player> <level>")
return nil
}
playerName := ctx.Arguments[0]
level := ctx.GetArgumentInt(1, 1)
if level < 1 || level > 100 {
ctx.AddErrorMessage("Level must be between 1 and 100")
return nil
}
// TODO: Implement level setting
ctx.AddStatusMessage(fmt.Sprintf("Set %s's level to %d", playerName, level))
return nil
}
// HandleSetClass handles the /setclass command
func HandleSetClass(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(2, 2); err != nil {
ctx.AddErrorMessage("Usage: /setclass <player> <class_id>")
return nil
}
playerName := ctx.Arguments[0]
classID := ctx.GetArgumentInt(1, 0)
if classID < 0 {
ctx.AddErrorMessage("Invalid class ID")
return nil
}
// TODO: Implement class setting
ctx.AddStatusMessage(fmt.Sprintf("Set %s's class to %d", playerName, classID))
return nil
}
// HandleReload handles the /reload command
func HandleReload(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: /reload <spells|quests|zones|items|all>")
return nil
}
reloadType := strings.ToLower(ctx.Arguments[0])
switch reloadType {
case "spells":
ctx.AddStatusMessage("Reloaded spells")
case "quests":
ctx.AddStatusMessage("Reloaded quests")
case "zones":
ctx.AddStatusMessage("Reloaded zones")
case "items":
ctx.AddStatusMessage("Reloaded items")
case "all":
ctx.AddStatusMessage("Reloaded all server data")
default:
ctx.AddErrorMessage("Usage: /reload <spells|quests|zones|items|all>")
}
return nil
}
// HandleShutdown handles the /shutdown command
func HandleShutdown(ctx *CommandContext) error {
minutes := 5 // Default 5 minutes
reason := "Server maintenance"
if ctx.ArgumentCount() > 0 {
minutes = ctx.GetArgumentInt(0, 5)
}
if ctx.ArgumentCount() > 1 {
reason = ctx.GetRemainingArguments(1)
}
// TODO: Implement server shutdown
ctx.AddStatusMessage(fmt.Sprintf("Server shutdown scheduled in %d minutes. Reason: %s",
minutes, reason))
return nil
}
// HandleCancelShutdown handles the /cancelshutdown command
func HandleCancelShutdown(ctx *CommandContext) error {
// TODO: Implement shutdown cancellation
ctx.AddStatusMessage("Server shutdown cancelled")
return nil
}
// HandleInvisible handles the /invisible command
func HandleInvisible(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Toggle GM invisibility
ctx.AddStatusMessage("GM invisibility toggled")
return nil
}
// HandleInvulnerable handles the /invulnerable command
func HandleInvulnerable(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Toggle invulnerability
ctx.AddStatusMessage("Invulnerability toggled")
return nil
}
// HandleSpeed handles the /speed command
func HandleSpeed(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: /speed <multiplier>")
return nil
}
multiplier := ctx.GetArgumentFloat(0, 1.0)
if multiplier < 0.1 || multiplier > 10.0 {
ctx.AddErrorMessage("Speed multiplier must be between 0.1 and 10.0")
return nil
}
// TODO: Set movement speed
ctx.AddStatusMessage(fmt.Sprintf("Movement speed set to %.1fx", multiplier))
return nil
}
// HandleFlyMode handles the /flymode command
func HandleFlyMode(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Toggle fly mode
ctx.AddStatusMessage("Fly mode toggled")
return nil
}
// HandleInfo handles the /info command
func HandleInfo(ctx *CommandContext) error {
// TODO: Show detailed target information
targetName := ctx.GetTargetName()
if targetName == "No Target" {
ctx.AddErrorMessage("You must target something to get info")
return nil
}
ctx.AddStatusMessage(fmt.Sprintf("Information for %s:", targetName))
ctx.AddStatusMessage(" ID: 12345")
ctx.AddStatusMessage(" Level: 25")
ctx.AddStatusMessage(" HP: 1250/1250")
ctx.AddStatusMessage(" Position: 100.5, 200.3, 150.7")
return nil
}
// HandleVersion handles the /version command
func HandleVersion(ctx *CommandContext) error {
// TODO: Get actual version information
ctx.AddStatusMessage("EQ2Go Server Version 1.0.0")
ctx.AddStatusMessage("Build: Development")
ctx.AddStatusMessage("Go Version: go1.24.5")
return nil
}
// HandleTest handles the /test command
func HandleTest(ctx *CommandContext) error {
ctx.AddStatusMessage("Test command executed successfully")
if ctx.ArgumentCount() > 0 {
ctx.AddStatusMessage(fmt.Sprintf("Arguments: %s", ctx.RawArguments))
}
// TODO: Add actual test functionality for development
return nil
}

View File

@ -0,0 +1,85 @@
package commands
import "fmt"
// InitializeCommands initializes and registers all command types
func InitializeCommands() (*CommandManager, error) {
cm := NewCommandManager()
// Register all command types
if err := RegisterPlayerCommands(cm); err != nil {
return nil, fmt.Errorf("failed to register player commands: %w", err)
}
if err := RegisterAdminCommands(cm); err != nil {
return nil, fmt.Errorf("failed to register admin commands: %w", err)
}
if err := RegisterConsoleCommands(cm); err != nil {
return nil, fmt.Errorf("failed to register console commands: %w", err)
}
return cm, nil
}
// GetCommandHelp returns help text for a specific command
func GetCommandHelp(cm *CommandManager, commandName string, adminLevel int) string {
command, exists := cm.GetCommand(commandName)
if !exists {
return fmt.Sprintf("Command '%s' not found", commandName)
}
if adminLevel < command.RequiredLevel {
return fmt.Sprintf("Command '%s' not available at your admin level", commandName)
}
help := fmt.Sprintf("Command: %s\n", command.Name)
help += fmt.Sprintf("Usage: %s\n", command.Usage)
help += fmt.Sprintf("Description: %s\n", command.Description)
help += fmt.Sprintf("Required Admin Level: %d\n", command.RequiredLevel)
help += fmt.Sprintf("Type: %s\n", command.Type.String())
if len(command.SubCommands) > 0 {
help += "\nSubcommands:\n"
for name, subCmd := range command.SubCommands {
if adminLevel >= subCmd.RequiredLevel {
help += fmt.Sprintf(" %s - %s\n", name, subCmd.Description)
}
}
}
return help
}
// GetAvailableCommands returns a list of commands available to the given admin level
func GetAvailableCommands(cm *CommandManager, adminLevel int) []string {
allCommands := cm.ListCommands()
available := make([]string, 0)
for _, cmdName := range allCommands {
if command, exists := cm.GetCommand(cmdName); exists {
if adminLevel >= command.RequiredLevel {
available = append(available, cmdName)
}
}
}
return available
}
// ExecuteCommand is a convenience function that parses and executes a command
func ExecuteCommand(cm *CommandManager, rawCommand string, client ClientInterface) error {
ctx := cm.ParseCommand(rawCommand, client)
if ctx == nil {
return fmt.Errorf("failed to parse command")
}
err := cm.Execute(ctx)
if err != nil {
ctx.AddErrorMessage(err.Error())
}
// Send all messages to client
ctx.SendMessages()
return err
}

View File

@ -0,0 +1,614 @@
package commands
import (
"testing"
"eq2emu/internal/entity"
"eq2emu/internal/spawn"
)
// Mock implementations for testing
type mockClient struct {
player *entity.Entity
accountID int32
characterID int32
adminLevel int
name string
zone *mockZone
messages []mockMessage
}
type mockMessage struct {
channel int
color int
message string
}
func (mc *mockClient) GetPlayer() *entity.Entity { return mc.player }
func (mc *mockClient) GetAccountID() int32 { return mc.accountID }
func (mc *mockClient) GetCharacterID() int32 { return mc.characterID }
func (mc *mockClient) GetAdminLevel() int { return mc.adminLevel }
func (mc *mockClient) GetName() string { return mc.name }
func (mc *mockClient) IsInZone() bool { return mc.zone != nil }
func (mc *mockClient) GetZone() ZoneInterface { return mc.zone }
func (mc *mockClient) SendMessage(channel, color int, message string) {
mc.messages = append(mc.messages, mockMessage{channel, color, message})
}
func (mc *mockClient) SendPopupMessage(message string) {}
func (mc *mockClient) Disconnect() {}
type mockZone struct {
id int32
name string
description string
players []*entity.Entity
}
func (mz *mockZone) GetID() int32 { return mz.id }
func (mz *mockZone) GetName() string { return mz.name }
func (mz *mockZone) GetDescription() string { return mz.description }
func (mz *mockZone) GetPlayers() []*entity.Entity { return mz.players }
func (mz *mockZone) Shutdown() {}
func (mz *mockZone) SendZoneMessage(channel, color int, message string) {}
func (mz *mockZone) GetSpawnByName(name string) *spawn.Spawn { return nil }
func (mz *mockZone) GetSpawnByID(id int32) *spawn.Spawn { return nil }
func TestCommandManager_Register(t *testing.T) {
cm := NewCommandManager()
// Test registering a valid command
cmd := &Command{
Name: "test",
Type: CommandTypePlayer,
Description: "Test command",
Usage: "/test",
RequiredLevel: AdminLevelPlayer,
Handler: func(ctx *CommandContext) error {
ctx.AddStatusMessage("Test executed")
return nil
},
}
err := cm.Register(cmd)
if err != nil {
t.Errorf("Failed to register valid command: %v", err)
}
// Test registering duplicate command
err = cm.Register(cmd)
if err == nil {
t.Error("Expected error when registering duplicate command")
}
// Test registering nil command
err = cm.Register(nil)
if err == nil {
t.Error("Expected error when registering nil command")
}
// Test registering command with empty name
invalidCmd := &Command{
Name: "",
Handler: func(ctx *CommandContext) error { return nil },
}
err = cm.Register(invalidCmd)
if err == nil {
t.Error("Expected error when registering command with empty name")
}
// Test registering command with nil handler
invalidCmd2 := &Command{
Name: "invalid",
Handler: nil,
}
err = cm.Register(invalidCmd2)
if err == nil {
t.Error("Expected error when registering command with nil handler")
}
}
func TestCommandManager_HasCommand(t *testing.T) {
cm := NewCommandManager()
cmd := &Command{
Name: "testcmd",
Type: CommandTypePlayer,
RequiredLevel: AdminLevelPlayer,
Handler: func(ctx *CommandContext) error { return nil },
}
// Test command doesn't exist initially
if cm.HasCommand("testcmd") {
t.Error("Command should not exist initially")
}
// Register command
err := cm.Register(cmd)
if err != nil {
t.Fatalf("Failed to register command: %v", err)
}
// Test command exists after registration
if !cm.HasCommand("testcmd") {
t.Error("Command should exist after registration")
}
// Test case insensitivity
if !cm.HasCommand("TESTCMD") {
t.Error("Command lookup should be case insensitive")
}
}
func TestCommandManager_GetCommand(t *testing.T) {
cm := NewCommandManager()
cmd := &Command{
Name: "getcmd",
Type: CommandTypeAdmin,
RequiredLevel: AdminLevelGM,
Handler: func(ctx *CommandContext) error { return nil },
}
// Test getting non-existent command
_, exists := cm.GetCommand("getcmd")
if exists {
t.Error("Command should not exist initially")
}
// Register command
err := cm.Register(cmd)
if err != nil {
t.Fatalf("Failed to register command: %v", err)
}
// Test getting existing command
retrievedCmd, exists := cm.GetCommand("getcmd")
if !exists {
t.Error("Command should exist after registration")
}
if retrievedCmd.Name != "getcmd" {
t.Errorf("Expected command name 'getcmd', got '%s'", retrievedCmd.Name)
}
if retrievedCmd.Type != CommandTypeAdmin {
t.Errorf("Expected command type Admin, got %v", retrievedCmd.Type)
}
// Test case insensitivity
_, exists = cm.GetCommand("GETCMD")
if !exists {
t.Error("Command lookup should be case insensitive")
}
}
func TestCommandManager_Execute(t *testing.T) {
cm := NewCommandManager()
executed := false
cmd := &Command{
Name: "exectest",
Type: CommandTypePlayer,
RequiredLevel: AdminLevelPlayer,
Handler: func(ctx *CommandContext) error {
executed = true
ctx.AddStatusMessage("Command executed successfully")
return nil
},
}
err := cm.Register(cmd)
if err != nil {
t.Fatalf("Failed to register command: %v", err)
}
// Test successful execution
ctx := NewCommandContext(CommandTypePlayer, "exectest", []string{})
ctx.AdminLevel = AdminLevelPlayer
err = cm.Execute(ctx)
if err != nil {
t.Errorf("Command execution failed: %v", err)
}
if !executed {
t.Error("Command handler was not executed")
}
// Test insufficient admin level
ctx2 := NewCommandContext(CommandTypePlayer, "exectest", []string{})
ctx2.AdminLevel = 0
// Register admin-only command
adminCmd := &Command{
Name: "admintest",
Type: CommandTypeAdmin,
RequiredLevel: AdminLevelAdmin,
Handler: func(ctx *CommandContext) error {
return nil
},
}
err = cm.Register(adminCmd)
if err != nil {
t.Fatalf("Failed to register admin command: %v", err)
}
ctx3 := NewCommandContext(CommandTypeAdmin, "admintest", []string{})
ctx3.AdminLevel = AdminLevelPlayer
err = cm.Execute(ctx3)
if err == nil {
t.Error("Expected error for insufficient admin level")
}
// Test unknown command
ctx4 := NewCommandContext(CommandTypePlayer, "unknown", []string{})
err = cm.Execute(ctx4)
if err == nil {
t.Error("Expected error for unknown command")
}
}
func TestCommandManager_RegisterSubCommand(t *testing.T) {
cm := NewCommandManager()
// Register parent command
parentCmd := &Command{
Name: "parent",
Type: CommandTypeAdmin,
RequiredLevel: AdminLevelGM,
Handler: func(ctx *CommandContext) error {
return nil
},
}
err := cm.Register(parentCmd)
if err != nil {
t.Fatalf("Failed to register parent command: %v", err)
}
// Register subcommand
subCmd := &Command{
Name: "sub",
Type: CommandTypeAdmin,
RequiredLevel: AdminLevelGM,
Handler: func(ctx *CommandContext) error {
ctx.AddStatusMessage("Subcommand executed")
return nil
},
}
err = cm.RegisterSubCommand("parent", subCmd)
if err != nil {
t.Errorf("Failed to register subcommand: %v", err)
}
// Test registering subcommand for non-existent parent
err = cm.RegisterSubCommand("nonexistent", subCmd)
if err == nil {
t.Error("Expected error when registering subcommand for non-existent parent")
}
// Test registering duplicate subcommand
err = cm.RegisterSubCommand("parent", subCmd)
if err == nil {
t.Error("Expected error when registering duplicate subcommand")
}
}
func TestCommandManager_ParseCommand(t *testing.T) {
cm := NewCommandManager()
client := &mockClient{
name: "TestPlayer",
adminLevel: AdminLevelPlayer,
}
// Test parsing valid command
ctx := cm.ParseCommand("/say Hello world", client)
if ctx == nil {
t.Error("Expected non-nil context for valid command")
}
if ctx.CommandName != "say" {
t.Errorf("Expected command name 'say', got '%s'", ctx.CommandName)
}
if len(ctx.Arguments) != 2 {
t.Errorf("Expected 2 arguments, got %d", len(ctx.Arguments))
}
if ctx.Arguments[0] != "Hello" || ctx.Arguments[1] != "world" {
t.Errorf("Unexpected arguments: %v", ctx.Arguments)
}
if ctx.RawArguments != "Hello world" {
t.Errorf("Expected raw arguments 'Hello world', got '%s'", ctx.RawArguments)
}
// Test parsing command without slash
ctx2 := cm.ParseCommand("tell Player Hi there", client)
if ctx2 == nil {
t.Error("Expected non-nil context for command without slash")
}
if ctx2.CommandName != "tell" {
t.Errorf("Expected command name 'tell', got '%s'", ctx2.CommandName)
}
// Test parsing empty command
ctx3 := cm.ParseCommand("", client)
if ctx3 != nil {
t.Error("Expected nil context for empty command")
}
// Test parsing whitespace-only command
ctx4 := cm.ParseCommand(" ", client)
if ctx4 != nil {
t.Error("Expected nil context for whitespace-only command")
}
}
func TestCommandContext_ValidateArgumentCount(t *testing.T) {
ctx := NewCommandContext(CommandTypePlayer, "test", []string{"arg1", "arg2", "arg3"})
// Test valid argument count (exactly 3)
err := ctx.ValidateArgumentCount(3, 3)
if err != nil {
t.Errorf("Expected no error for exact argument count, got: %v", err)
}
// Test valid argument count (range 2-4)
err = ctx.ValidateArgumentCount(2, 4)
if err != nil {
t.Errorf("Expected no error for argument count in range, got: %v", err)
}
// Test too few arguments
err = ctx.ValidateArgumentCount(5, 6)
if err == nil {
t.Error("Expected error for too few arguments")
}
// Test too many arguments
err = ctx.ValidateArgumentCount(1, 2)
if err == nil {
t.Error("Expected error for too many arguments")
}
// Test unlimited max arguments (-1)
err = ctx.ValidateArgumentCount(2, -1)
if err != nil {
t.Errorf("Expected no error for unlimited max arguments, got: %v", err)
}
}
func TestCommandContext_GetArgument(t *testing.T) {
ctx := NewCommandContext(CommandTypePlayer, "test", []string{"first", "second", "third"})
// Test valid indices
arg, exists := ctx.GetArgument(0)
if !exists || arg != "first" {
t.Errorf("Expected 'first', got '%s' (exists: %v)", arg, exists)
}
arg, exists = ctx.GetArgument(2)
if !exists || arg != "third" {
t.Errorf("Expected 'third', got '%s' (exists: %v)", arg, exists)
}
// Test invalid indices
_, exists = ctx.GetArgument(-1)
if exists {
t.Error("Expected false for negative index")
}
_, exists = ctx.GetArgument(3)
if exists {
t.Error("Expected false for out-of-bounds index")
}
}
func TestCommandContext_GetArgumentInt(t *testing.T) {
ctx := NewCommandContext(CommandTypePlayer, "test", []string{"123", "not_a_number", "-456"})
// Test valid integer
val := ctx.GetArgumentInt(0, 999)
if val != 123 {
t.Errorf("Expected 123, got %d", val)
}
// Test invalid integer (should return default)
val = ctx.GetArgumentInt(1, 999)
if val != 999 {
t.Errorf("Expected default value 999, got %d", val)
}
// Test negative integer
val = ctx.GetArgumentInt(2, 999)
if val != -456 {
t.Errorf("Expected -456, got %d", val)
}
// Test out-of-bounds index (should return default)
val = ctx.GetArgumentInt(5, 999)
if val != 999 {
t.Errorf("Expected default value 999, got %d", val)
}
}
func TestCommandContext_GetArgumentFloat(t *testing.T) {
ctx := NewCommandContext(CommandTypePlayer, "test", []string{"12.34", "not_a_number", "-56.78"})
// Test valid float
val := ctx.GetArgumentFloat(0, 999.0)
if val != 12.34 {
t.Errorf("Expected 12.34, got %f", val)
}
// Test invalid float (should return default)
val = ctx.GetArgumentFloat(1, 999.0)
if val != 999.0 {
t.Errorf("Expected default value 999.0, got %f", val)
}
// Test negative float
val = ctx.GetArgumentFloat(2, 999.0)
if val != -56.78 {
t.Errorf("Expected -56.78, got %f", val)
}
}
func TestCommandContext_GetArgumentBool(t *testing.T) {
ctx := NewCommandContext(CommandTypePlayer, "test",
[]string{"true", "false", "yes", "no", "on", "off", "1", "0", "invalid"})
tests := []struct {
index int
expected bool
}{
{0, true}, // "true"
{1, false}, // "false"
{2, true}, // "yes"
{3, false}, // "no"
{4, true}, // "on"
{5, false}, // "off"
{6, true}, // "1"
{7, false}, // "0"
}
for _, test := range tests {
val := ctx.GetArgumentBool(test.index, !test.expected) // Use opposite as default
if val != test.expected {
t.Errorf("Index %d: expected %v, got %v", test.index, test.expected, val)
}
}
// Test invalid boolean (should return default)
val := ctx.GetArgumentBool(8, true)
if val != true {
t.Errorf("Expected default value true for invalid boolean, got %v", val)
}
}
func TestCommandContext_Messages(t *testing.T) {
ctx := NewCommandContext(CommandTypePlayer, "test", []string{})
// Test adding messages
ctx.AddDefaultMessage("Default message")
ctx.AddErrorMessage("Error message")
ctx.AddStatusMessage("Status message")
ctx.AddMessage(ChannelSay, ColorWhite, "Custom message")
if len(ctx.Messages) != 4 {
t.Errorf("Expected 4 messages, got %d", len(ctx.Messages))
}
// Check message types
expectedMessages := []struct {
channel int
color int
message string
}{
{ChannelDefault, ColorWhite, "Default message"},
{ChannelError, ColorRed, "Error message"},
{ChannelStatus, ColorYellow, "Status message"},
{ChannelSay, ColorWhite, "Custom message"},
}
for i, expected := range expectedMessages {
msg := ctx.Messages[i]
if msg.Channel != expected.channel {
t.Errorf("Message %d: expected channel %d, got %d", i, expected.channel, msg.Channel)
}
if msg.Color != expected.color {
t.Errorf("Message %d: expected color %d, got %d", i, expected.color, msg.Color)
}
if msg.Message != expected.message {
t.Errorf("Message %d: expected message '%s', got '%s'", i, expected.message, msg.Message)
}
}
// Test clearing messages
ctx.ClearMessages()
if len(ctx.Messages) != 0 {
t.Errorf("Expected 0 messages after clearing, got %d", len(ctx.Messages))
}
}
func TestInitializeCommands(t *testing.T) {
cm, err := InitializeCommands()
if err != nil {
t.Fatalf("Failed to initialize commands: %v", err)
}
if cm == nil {
t.Fatal("Expected non-nil command manager")
}
// Test that some basic commands are registered
expectedCommands := []string{"say", "tell", "kick", "ban", "reload", "shutdown", "console_ban", "console_kick"}
for _, cmdName := range expectedCommands {
if !cm.HasCommand(cmdName) {
t.Errorf("Expected command '%s' to be registered", cmdName)
}
}
// Test command counts by type
playerCommands := cm.ListCommandsByType(CommandTypePlayer)
adminCommands := cm.ListCommandsByType(CommandTypeAdmin)
consoleCommands := cm.ListCommandsByType(CommandTypeConsole)
if len(playerCommands) == 0 {
t.Error("Expected player commands to be registered")
}
if len(adminCommands) == 0 {
t.Error("Expected admin commands to be registered")
}
if len(consoleCommands) == 0 {
t.Error("Expected console commands to be registered")
}
}
func TestGetAvailableCommands(t *testing.T) {
cm, err := InitializeCommands()
if err != nil {
t.Fatalf("Failed to initialize commands: %v", err)
}
// Test player level commands
playerCommands := GetAvailableCommands(cm, AdminLevelPlayer)
if len(playerCommands) == 0 {
t.Error("Expected player-level commands to be available")
}
// Test admin level commands
adminCommands := GetAvailableCommands(cm, AdminLevelAdmin)
if len(adminCommands) <= len(playerCommands) {
t.Error("Expected admin to have more commands available than player")
}
// Verify all player commands are also available to admin
playerCommandMap := make(map[string]bool)
for _, cmd := range playerCommands {
playerCommandMap[cmd] = true
}
for _, cmd := range playerCommands {
found := false
for _, adminCmd := range adminCommands {
if adminCmd == cmd {
found = true
break
}
}
if !found {
t.Errorf("Player command '%s' should be available to admin", cmd)
}
}
}

View File

@ -0,0 +1,629 @@
package commands
import (
"fmt"
"strings"
)
// RegisterConsoleCommands registers all console-level commands
func RegisterConsoleCommands(cm *CommandManager) error {
commands := []*Command{
// Player management commands
{
Name: "console_ban",
Type: CommandTypeConsole,
Description: "Bans a player account",
Usage: "console_ban <player> [duration] [reason]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleBan,
},
{
Name: "console_unban",
Type: CommandTypeConsole,
Description: "Unbans a player account",
Usage: "console_unban <player>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleUnban,
},
{
Name: "console_kick",
Type: CommandTypeConsole,
Description: "Kicks a player from the server",
Usage: "console_kick <player> [reason]",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleKick,
},
// Communication commands
{
Name: "console_announce",
Type: CommandTypeConsole,
Description: "Announces a message to all players",
Usage: "console_announce <message>",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleAnnounce,
},
{
Name: "console_broadcast",
Type: CommandTypeConsole,
Description: "Broadcasts a message to all players",
Usage: "console_broadcast <message>",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleBroadcast,
},
{
Name: "channel",
Type: CommandTypeConsole,
Description: "Sends message to specific channel",
Usage: "channel <channel_id> <message>",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleChannel,
},
{
Name: "console_tell",
Type: CommandTypeConsole,
Description: "Sends private message to a player",
Usage: "console_tell <player> <message>",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleTell,
},
// Information commands
{
Name: "console_guild",
Type: CommandTypeConsole,
Description: "Guild management commands",
Usage: "console_guild <list|info|delete> [options]",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleGuild,
},
{
Name: "player",
Type: CommandTypeConsole,
Description: "Player information and management",
Usage: "player <list|info|modify> [options]",
RequiredLevel: AdminLevelGM,
Handler: HandleConsolePlayer,
},
{
Name: "setadmin",
Type: CommandTypeConsole,
Description: "Sets admin level for a player",
Usage: "setadmin <player> <admin_level>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleSetAdmin,
},
{
Name: "world",
Type: CommandTypeConsole,
Description: "World server information",
Usage: "world",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleWorld,
},
{
Name: "console_zone",
Type: CommandTypeConsole,
Description: "Zone information and management",
Usage: "console_zone <list|info|shutdown> [options]",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleZone,
},
{
Name: "console_who",
Type: CommandTypeConsole,
Description: "Lists players online",
Usage: "console_who [pattern]",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleWho,
},
// MOTD commands
{
Name: "getmotd",
Type: CommandTypeConsole,
Description: "Gets the current MOTD",
Usage: "getmotd",
RequiredLevel: AdminLevelGM,
Handler: HandleConsoleGetMOTD,
},
{
Name: "setmotd",
Type: CommandTypeConsole,
Description: "Sets the MOTD",
Usage: "setmotd <message>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleSetMOTD,
},
// Server management commands
{
Name: "console_reload",
Type: CommandTypeConsole,
Description: "Reloads server data",
Usage: "console_reload <spells|quests|zones|items|rules|all>",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleReload,
},
{
Name: "console_shutdown",
Type: CommandTypeConsole,
Description: "Shuts down the server",
Usage: "console_shutdown [time_minutes] [reason]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleShutdown,
},
{
Name: "console_cancelshutdown",
Type: CommandTypeConsole,
Description: "Cancels a scheduled shutdown",
Usage: "console_cancelshutdown",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleCancelShutdown,
},
{
Name: "exit",
Type: CommandTypeConsole,
Description: "Exits the server immediately",
Usage: "exit",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleExit,
},
{
Name: "rules",
Type: CommandTypeConsole,
Description: "Rules management commands",
Usage: "rules <list|get|set> [options]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleRules,
},
// Testing commands
{
Name: "console_test",
Type: CommandTypeConsole,
Description: "Testing command for development",
Usage: "console_test [parameters]",
RequiredLevel: AdminLevelAdmin,
Handler: HandleConsoleTest,
},
}
for _, cmd := range commands {
if err := cm.Register(cmd); err != nil {
return fmt.Errorf("failed to register console command %s: %w", cmd.Name, err)
}
}
return nil
}
// HandleConsoleBan handles the console ban command
func HandleConsoleBan(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: ban <player> [duration] [reason]")
return nil
}
playerName := ctx.Arguments[0]
duration := "permanent"
reason := "Banned by administrator"
if ctx.ArgumentCount() > 1 {
duration = ctx.Arguments[1]
}
if ctx.ArgumentCount() > 2 {
reason = ctx.GetRemainingArguments(2)
}
// TODO: Implement actual ban functionality
ctx.AddStatusMessage(fmt.Sprintf("Banned player '%s' for %s with reason: %s",
playerName, duration, reason))
return nil
}
// HandleConsoleUnban handles the console unban command
func HandleConsoleUnban(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: unban <player>")
return nil
}
playerName := ctx.Arguments[0]
// TODO: Implement actual unban functionality
ctx.AddStatusMessage(fmt.Sprintf("Unbanned player '%s'", playerName))
return nil
}
// HandleConsoleKick handles the console kick command
func HandleConsoleKick(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: kick <player> [reason]")
return nil
}
playerName := ctx.Arguments[0]
reason := "Kicked by administrator"
if ctx.ArgumentCount() > 1 {
reason = ctx.GetRemainingArguments(1)
}
// TODO: Implement actual kick functionality
ctx.AddStatusMessage(fmt.Sprintf("Kicked player '%s' with reason: %s", playerName, reason))
return nil
}
// HandleConsoleAnnounce handles the console announce command
func HandleConsoleAnnounce(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: announce <message>")
return nil
}
message := ctx.RawArguments
// TODO: Implement server announcement
ctx.AddMessage(ChannelBroadcast, ColorRed, fmt.Sprintf("[ANNOUNCEMENT] %s", message))
return nil
}
// HandleConsoleBroadcast handles the console broadcast command
func HandleConsoleBroadcast(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: broadcast <message>")
return nil
}
message := ctx.RawArguments
// TODO: Implement server-wide broadcast
ctx.AddMessage(ChannelBroadcast, ColorYellow, fmt.Sprintf("[BROADCAST] %s", message))
return nil
}
// HandleConsoleChannel handles the console channel command
func HandleConsoleChannel(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(2, -1); err != nil {
ctx.AddErrorMessage("Usage: channel <channel_id> <message>")
return nil
}
channelID := ctx.GetArgumentInt(0, 0)
message := ctx.GetRemainingArguments(1)
if channelID < 0 {
ctx.AddErrorMessage("Invalid channel ID")
return nil
}
// TODO: Implement channel message sending
ctx.AddMessage(channelID, ColorWhite, message)
ctx.AddStatusMessage(fmt.Sprintf("Sent message to channel %d: %s", channelID, message))
return nil
}
// HandleConsoleTell handles the console tell command
func HandleConsoleTell(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(2, -1); err != nil {
ctx.AddErrorMessage("Usage: tell <player> <message>")
return nil
}
targetName := ctx.Arguments[0]
message := ctx.GetRemainingArguments(1)
// TODO: Implement tell system
ctx.AddStatusMessage(fmt.Sprintf("Told %s: %s", targetName, message))
return nil
}
// HandleConsoleGuild handles the console guild command
func HandleConsoleGuild(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: guild <list|info|delete> [options]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "list":
// TODO: List all guilds
ctx.AddStatusMessage("Guild listing (not yet implemented)")
case "info":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: guild info <guild_name>")
return nil
}
guildName := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Guild info for '%s' (not yet implemented)", guildName))
case "delete":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: guild delete <guild_name>")
return nil
}
guildName := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Deleted guild '%s' (not yet implemented)", guildName))
default:
ctx.AddErrorMessage("Usage: guild <list|info|delete> [options]")
}
return nil
}
// HandleConsolePlayer handles the console player command
func HandleConsolePlayer(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: player <list|info|modify> [options]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "list":
// TODO: List all players
ctx.AddStatusMessage("Player listing (not yet implemented)")
case "info":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: player info <player_name>")
return nil
}
playerName := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Player info for '%s' (not yet implemented)", playerName))
case "modify":
if ctx.ArgumentCount() < 4 {
ctx.AddErrorMessage("Usage: player modify <player_name> <attribute> <value>")
return nil
}
playerName := ctx.Arguments[1]
attribute := ctx.Arguments[2]
value := ctx.Arguments[3]
ctx.AddStatusMessage(fmt.Sprintf("Modified %s's %s to %s (not yet implemented)",
playerName, attribute, value))
default:
ctx.AddErrorMessage("Usage: player <list|info|modify> [options]")
}
return nil
}
// HandleConsoleSetAdmin handles the console setadmin command
func HandleConsoleSetAdmin(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(2, 2); err != nil {
ctx.AddErrorMessage("Usage: setadmin <player> <admin_level>")
return nil
}
playerName := ctx.Arguments[0]
adminLevel := ctx.GetArgumentInt(1, 0)
if adminLevel < 0 || adminLevel > 300 {
ctx.AddErrorMessage("Admin level must be between 0 and 300")
return nil
}
// TODO: Implement admin level setting
ctx.AddStatusMessage(fmt.Sprintf("Set %s's admin level to %d", playerName, adminLevel))
return nil
}
// HandleConsoleWorld handles the console world command
func HandleConsoleWorld(ctx *CommandContext) error {
// TODO: Show world server statistics
ctx.AddStatusMessage("World Server Status:")
ctx.AddStatusMessage(" Uptime: 1 hour 23 minutes (not yet implemented)")
ctx.AddStatusMessage(" Players Online: 5 (not yet implemented)")
ctx.AddStatusMessage(" Zones Active: 12 (not yet implemented)")
ctx.AddStatusMessage(" Memory Usage: 256MB (not yet implemented)")
return nil
}
// HandleConsoleZone handles the console zone command
func HandleConsoleZone(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: zone <list|info|shutdown> [options]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "list":
// TODO: List all zones
ctx.AddStatusMessage("Zone listing (not yet implemented)")
case "info":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: zone info <zone_name>")
return nil
}
zoneName := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Zone info for '%s' (not yet implemented)", zoneName))
case "shutdown":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: zone shutdown <zone_name>")
return nil
}
zoneName := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Shutting down zone '%s' (not yet implemented)", zoneName))
default:
ctx.AddErrorMessage("Usage: zone <list|info|shutdown> [options]")
}
return nil
}
// HandleConsoleWho handles the console who command
func HandleConsoleWho(ctx *CommandContext) error {
pattern := ""
if ctx.ArgumentCount() > 0 {
pattern = ctx.Arguments[0]
}
// TODO: Implement player listing
if pattern != "" {
ctx.AddStatusMessage(fmt.Sprintf("Players matching '%s':", pattern))
} else {
ctx.AddStatusMessage("Players online:")
}
ctx.AddStatusMessage(" TestPlayer (Zone: Qeynos, Level: 10)")
ctx.AddStatusMessage("Total: 1 player online")
return nil
}
// HandleConsoleGetMOTD handles the console getmotd command
func HandleConsoleGetMOTD(ctx *CommandContext) error {
// TODO: Get actual MOTD from database
ctx.AddStatusMessage("Current MOTD:")
ctx.AddStatusMessage(" Welcome to EQ2Go Server!")
return nil
}
// HandleConsoleSetMOTD handles the console setmotd command
func HandleConsoleSetMOTD(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: setmotd <message>")
return nil
}
message := ctx.RawArguments
// TODO: Set MOTD in database
ctx.AddStatusMessage(fmt.Sprintf("MOTD set to: %s", message))
return nil
}
// HandleConsoleReload handles the console reload command
func HandleConsoleReload(ctx *CommandContext) error {
if err := ctx.ValidateArgumentCount(1, 1); err != nil {
ctx.AddErrorMessage("Usage: reload <spells|quests|zones|items|rules|all>")
return nil
}
reloadType := strings.ToLower(ctx.Arguments[0])
switch reloadType {
case "spells":
ctx.AddStatusMessage("Reloaded spells")
case "quests":
ctx.AddStatusMessage("Reloaded quests")
case "zones":
ctx.AddStatusMessage("Reloaded zones")
case "items":
ctx.AddStatusMessage("Reloaded items")
case "rules":
ctx.AddStatusMessage("Reloaded rules")
case "all":
ctx.AddStatusMessage("Reloaded all server data")
default:
ctx.AddErrorMessage("Usage: reload <spells|quests|zones|items|rules|all>")
}
return nil
}
// HandleConsoleShutdown handles the console shutdown command
func HandleConsoleShutdown(ctx *CommandContext) error {
minutes := 5 // Default 5 minutes
reason := "Server maintenance"
if ctx.ArgumentCount() > 0 {
minutes = ctx.GetArgumentInt(0, 5)
}
if ctx.ArgumentCount() > 1 {
reason = ctx.GetRemainingArguments(1)
}
// TODO: Implement server shutdown
ctx.AddStatusMessage(fmt.Sprintf("Server shutdown scheduled in %d minutes. Reason: %s",
minutes, reason))
return nil
}
// HandleConsoleCancelShutdown handles the console cancelshutdown command
func HandleConsoleCancelShutdown(ctx *CommandContext) error {
// TODO: Implement shutdown cancellation
ctx.AddStatusMessage("Server shutdown cancelled")
return nil
}
// HandleConsoleExit handles the console exit command
func HandleConsoleExit(ctx *CommandContext) error {
// TODO: Implement immediate server exit
ctx.AddStatusMessage("Exiting server immediately...")
return nil
}
// HandleConsoleRules handles the console rules command
func HandleConsoleRules(ctx *CommandContext) error {
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: rules <list|get|set> [options]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "list":
// TODO: List all rules
ctx.AddStatusMessage("Server rules listing (not yet implemented)")
case "get":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: rules get <rule_name>")
return nil
}
ruleName := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Rule '%s' value (not yet implemented)", ruleName))
case "set":
if ctx.ArgumentCount() < 3 {
ctx.AddErrorMessage("Usage: rules set <rule_name> <value>")
return nil
}
ruleName := ctx.Arguments[1]
value := ctx.Arguments[2]
ctx.AddStatusMessage(fmt.Sprintf("Set rule '%s' to '%s' (not yet implemented)",
ruleName, value))
default:
ctx.AddErrorMessage("Usage: rules <list|get|set> [options]")
}
return nil
}
// HandleConsoleTest handles the console test command
func HandleConsoleTest(ctx *CommandContext) error {
ctx.AddStatusMessage("Console test command executed successfully")
if ctx.ArgumentCount() > 0 {
ctx.AddStatusMessage(fmt.Sprintf("Arguments: %s", ctx.RawArguments))
}
// TODO: Add actual test functionality for development
return nil
}

View File

@ -0,0 +1,291 @@
package commands
import (
"fmt"
"strconv"
"strings"
"eq2emu/internal/entity"
"eq2emu/internal/spawn"
)
// NewCommandContext creates a new command context
func NewCommandContext(commandType CommandType, commandName string, arguments []string) *CommandContext {
return &CommandContext{
CommandType: commandType,
CommandName: commandName,
Arguments: arguments,
RawArguments: strings.Join(arguments, " "),
Messages: make([]CommandMessage, 0),
Results: make(map[string]any),
}
}
// WithClient adds a client to the context
func (ctx *CommandContext) WithClient(client ClientInterface) *CommandContext {
ctx.Client = client
if client != nil {
ctx.Player = client.GetPlayer()
ctx.Zone = client.GetZone()
ctx.AdminLevel = client.GetAdminLevel()
}
return ctx
}
// WithPlayer adds a player to the context
func (ctx *CommandContext) WithPlayer(player *entity.Entity) *CommandContext {
ctx.Player = player
return ctx
}
// WithTarget adds a target to the context
func (ctx *CommandContext) WithTarget(target *spawn.Spawn) *CommandContext {
ctx.Target = target
return ctx
}
// WithZone adds a zone to the context
func (ctx *CommandContext) WithZone(zone ZoneInterface) *CommandContext {
ctx.Zone = zone
return ctx
}
// WithAdminLevel sets the admin level for the context
func (ctx *CommandContext) WithAdminLevel(level int) *CommandContext {
ctx.AdminLevel = level
return ctx
}
// GetArgument retrieves an argument by index
func (ctx *CommandContext) GetArgument(index int) (string, bool) {
if index < 0 || index >= len(ctx.Arguments) {
return "", false
}
return ctx.Arguments[index], true
}
// GetArgumentInt retrieves an integer argument by index
func (ctx *CommandContext) GetArgumentInt(index int, defaultValue int) int {
if arg, exists := ctx.GetArgument(index); exists {
if value, err := strconv.Atoi(arg); err == nil {
return value
}
}
return defaultValue
}
// GetArgumentFloat retrieves a float argument by index
func (ctx *CommandContext) GetArgumentFloat(index int, defaultValue float64) float64 {
if arg, exists := ctx.GetArgument(index); exists {
if value, err := strconv.ParseFloat(arg, 64); err == nil {
return value
}
}
return defaultValue
}
// GetArgumentBool retrieves a boolean argument by index
func (ctx *CommandContext) GetArgumentBool(index int, defaultValue bool) bool {
if arg, exists := ctx.GetArgument(index); exists {
switch strings.ToLower(arg) {
case "true", "yes", "on", "1":
return true
case "false", "no", "off", "0":
return false
}
}
return defaultValue
}
// GetRemainingArguments gets all arguments from index onwards as a string
func (ctx *CommandContext) GetRemainingArguments(fromIndex int) string {
if fromIndex < 0 || fromIndex >= len(ctx.Arguments) {
return ""
}
return strings.Join(ctx.Arguments[fromIndex:], " ")
}
// HasArgument checks if an argument exists at the given index
func (ctx *CommandContext) HasArgument(index int) bool {
_, exists := ctx.GetArgument(index)
return exists
}
// ArgumentCount returns the number of arguments
func (ctx *CommandContext) ArgumentCount() int {
return len(ctx.Arguments)
}
// AddMessage adds a message to be sent to the client
func (ctx *CommandContext) AddMessage(channel, color int, message string) {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
ctx.Messages = append(ctx.Messages, CommandMessage{
Channel: channel,
Color: color,
Message: message,
})
}
// AddDefaultMessage adds a message with default channel and color
func (ctx *CommandContext) AddDefaultMessage(message string) {
ctx.AddMessage(ChannelDefault, ColorWhite, message)
}
// AddErrorMessage adds an error message
func (ctx *CommandContext) AddErrorMessage(message string) {
ctx.AddMessage(ChannelError, ColorRed, message)
}
// AddStatusMessage adds a status message
func (ctx *CommandContext) AddStatusMessage(message string) {
ctx.AddMessage(ChannelStatus, ColorYellow, message)
}
// SendMessages sends all queued messages to the client
func (ctx *CommandContext) SendMessages() {
if ctx.Client == nil {
return
}
ctx.mutex.RLock()
messages := make([]CommandMessage, len(ctx.Messages))
copy(messages, ctx.Messages)
ctx.mutex.RUnlock()
for _, msg := range messages {
ctx.Client.SendMessage(msg.Channel, msg.Color, msg.Message)
}
}
// ClearMessages clears all queued messages
func (ctx *CommandContext) ClearMessages() {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
ctx.Messages = ctx.Messages[:0]
}
// SetResult sets a result value
func (ctx *CommandContext) SetResult(name string, value any) {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
if ctx.Results == nil {
ctx.Results = make(map[string]any)
}
ctx.Results[name] = value
}
// GetResult retrieves a result value
func (ctx *CommandContext) GetResult(name string) (any, bool) {
ctx.mutex.RLock()
defer ctx.mutex.RUnlock()
value, exists := ctx.Results[name]
return value, exists
}
// GetResultString retrieves a string result
func (ctx *CommandContext) GetResultString(name string, defaultValue string) string {
if value, exists := ctx.GetResult(name); exists {
if str, ok := value.(string); ok {
return str
}
}
return defaultValue
}
// GetResultInt retrieves an integer result
func (ctx *CommandContext) GetResultInt(name string, defaultValue int) int {
if value, exists := ctx.GetResult(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
}
// ValidateArgumentCount validates that the command has the required number of arguments
func (ctx *CommandContext) ValidateArgumentCount(min, max int) error {
argCount := len(ctx.Arguments)
if argCount < min {
return fmt.Errorf("command '%s' requires at least %d arguments, got %d", ctx.CommandName, min, argCount)
}
if max >= 0 && argCount > max {
return fmt.Errorf("command '%s' accepts at most %d arguments, got %d", ctx.CommandName, max, argCount)
}
return nil
}
// RequirePlayer ensures a player is present in the context
func (ctx *CommandContext) RequirePlayer() error {
if ctx.Player == nil {
return fmt.Errorf("command '%s' requires a player", ctx.CommandName)
}
return nil
}
// RequireTarget ensures a target is present in the context
func (ctx *CommandContext) RequireTarget() error {
if ctx.Target == nil {
return fmt.Errorf("command '%s' requires a target", ctx.CommandName)
}
return nil
}
// RequireZone ensures a zone is present in the context
func (ctx *CommandContext) RequireZone() error {
if ctx.Zone == nil {
return fmt.Errorf("command '%s' requires a zone", ctx.CommandName)
}
return nil
}
// RequireAdminLevel ensures the user has the required admin level
func (ctx *CommandContext) RequireAdminLevel(requiredLevel int) error {
if ctx.AdminLevel < requiredLevel {
return fmt.Errorf("command '%s' requires admin level %d, you have %d",
ctx.CommandName, requiredLevel, ctx.AdminLevel)
}
return nil
}
// GetPlayerName safely gets the player's name
func (ctx *CommandContext) GetPlayerName() string {
if ctx.Player != nil {
return ctx.Player.GetName()
}
if ctx.Client != nil {
return ctx.Client.GetName()
}
return "Unknown"
}
// GetTargetName safely gets the target's name
func (ctx *CommandContext) GetTargetName() string {
if ctx.Target != nil {
return ctx.Target.GetName()
}
return "No Target"
}
// GetZoneName safely gets the zone's name
func (ctx *CommandContext) GetZoneName() string {
if ctx.Zone != nil {
return ctx.Zone.GetName()
}
return "No Zone"
}

View File

@ -0,0 +1,247 @@
package commands
import (
"context"
"fmt"
"strings"
)
// NewCommandManager creates a new command manager instance
func NewCommandManager() *CommandManager {
return &CommandManager{
commands: make(map[string]*Command),
}
}
// Register registers a command with the manager
func (cm *CommandManager) Register(command *Command) error {
if command == nil {
return fmt.Errorf("command cannot be nil")
}
if command.Name == "" {
return fmt.Errorf("command name cannot be empty")
}
if command.Handler == nil {
return fmt.Errorf("command handler cannot be nil")
}
cm.mutex.Lock()
defer cm.mutex.Unlock()
// Normalize command name to lowercase for case-insensitive matching
normalizedName := strings.ToLower(command.Name)
if _, exists := cm.commands[normalizedName]; exists {
return fmt.Errorf("command '%s' is already registered", command.Name)
}
cm.commands[normalizedName] = command
return nil
}
// RegisterSubCommand registers a subcommand under a parent command
func (cm *CommandManager) RegisterSubCommand(parentName string, subCommand *Command) error {
if subCommand == nil {
return fmt.Errorf("subcommand cannot be nil")
}
cm.mutex.Lock()
defer cm.mutex.Unlock()
normalizedParent := strings.ToLower(parentName)
parentCmd, exists := cm.commands[normalizedParent]
if !exists {
return fmt.Errorf("parent command '%s' not found", parentName)
}
if parentCmd.SubCommands == nil {
parentCmd.SubCommands = make(map[string]*Command)
}
normalizedSubName := strings.ToLower(subCommand.Name)
if _, exists := parentCmd.SubCommands[normalizedSubName]; exists {
return fmt.Errorf("subcommand '%s' already exists under '%s'", subCommand.Name, parentName)
}
parentCmd.SubCommands[normalizedSubName] = subCommand
return nil
}
// Unregister removes a command from the manager
func (cm *CommandManager) Unregister(name string) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
normalizedName := strings.ToLower(name)
delete(cm.commands, normalizedName)
}
// HasCommand checks if a command is registered
func (cm *CommandManager) HasCommand(name string) bool {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
normalizedName := strings.ToLower(name)
_, exists := cm.commands[normalizedName]
return exists
}
// GetCommand retrieves a command by name
func (cm *CommandManager) GetCommand(name string) (*Command, bool) {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
normalizedName := strings.ToLower(name)
command, exists := cm.commands[normalizedName]
return command, exists
}
// ListCommands returns all registered command names
func (cm *CommandManager) ListCommands() []string {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
names := make([]string, 0, len(cm.commands))
for name := range cm.commands {
names = append(names, name)
}
return names
}
// ListCommandsByType returns commands filtered by type
func (cm *CommandManager) ListCommandsByType(commandType CommandType) []*Command {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
commands := make([]*Command, 0)
for _, cmd := range cm.commands {
if cmd.Type == commandType {
commands = append(commands, cmd)
}
}
return commands
}
// Execute executes a command with the given context
func (cm *CommandManager) Execute(ctx *CommandContext) error {
if ctx == nil {
return fmt.Errorf("command context cannot be nil")
}
if ctx.CommandName == "" {
return fmt.Errorf("command name cannot be empty")
}
// Find the command
normalizedName := strings.ToLower(ctx.CommandName)
cm.mutex.RLock()
command, exists := cm.commands[normalizedName]
cm.mutex.RUnlock()
if !exists {
return fmt.Errorf("unknown command: %s", ctx.CommandName)
}
// Check admin level requirements
if ctx.AdminLevel < command.RequiredLevel {
return fmt.Errorf("insufficient privileges for command '%s' (required: %d, have: %d)",
command.Name, command.RequiredLevel, ctx.AdminLevel)
}
// Check for subcommands
if len(ctx.Arguments) > 0 && command.SubCommands != nil {
normalizedSubName := strings.ToLower(ctx.Arguments[0])
if subCommand, exists := command.SubCommands[normalizedSubName]; exists {
// Execute subcommand
subCtx := &CommandContext{
Context: ctx.Context,
CommandType: subCommand.Type,
CommandName: subCommand.Name,
Arguments: ctx.Arguments[1:], // Remove subcommand name from arguments
RawArguments: strings.TrimSpace(strings.Join(ctx.Arguments[1:], " ")),
AdminLevel: ctx.AdminLevel,
RequiredLevel: subCommand.RequiredLevel,
Client: ctx.Client,
Player: ctx.Player,
Target: ctx.Target,
Zone: ctx.Zone,
Messages: make([]CommandMessage, 0),
Results: make(map[string]any),
}
// Check subcommand admin level
if ctx.AdminLevel < subCommand.RequiredLevel {
return fmt.Errorf("insufficient privileges for subcommand '%s %s' (required: %d, have: %d)",
command.Name, subCommand.Name, subCommand.RequiredLevel, ctx.AdminLevel)
}
return subCommand.Handler(subCtx)
}
}
// Execute main command
return command.Handler(ctx)
}
// ParseCommand parses a raw command string into a CommandContext
func (cm *CommandManager) ParseCommand(rawCommand string, client ClientInterface) *CommandContext {
// Remove leading slash if present
rawCommand = strings.TrimSpace(rawCommand)
rawCommand = strings.TrimPrefix(rawCommand, "/")
// Split into command and arguments
parts := strings.Fields(rawCommand)
if len(parts) == 0 {
return nil
}
commandName := parts[0]
arguments := parts[1:]
rawArguments := strings.TrimSpace(strings.Join(arguments, " "))
// Determine admin level
adminLevel := AdminLevelPlayer
if client != nil {
adminLevel = client.GetAdminLevel()
}
// Create context
ctx := &CommandContext{
Context: context.Background(),
CommandName: commandName,
Arguments: arguments,
RawArguments: rawArguments,
AdminLevel: adminLevel,
Client: client,
Messages: make([]CommandMessage, 0),
Results: make(map[string]any),
}
// Set additional context fields if client is available
if client != nil {
ctx.Player = client.GetPlayer()
ctx.Zone = client.GetZone()
}
// Determine command type based on name
ctx.CommandType = cm.determineCommandType(commandName)
return ctx
}
// determineCommandType determines the command type based on the command name
func (cm *CommandManager) determineCommandType(commandName string) CommandType {
// Check if command exists and get its type
cm.mutex.RLock()
defer cm.mutex.RUnlock()
normalizedName := strings.ToLower(commandName)
if command, exists := cm.commands[normalizedName]; exists {
return command.Type
}
// Default to player command if not found
return CommandTypePlayer
}

727
internal/commands/player.go Normal file
View File

@ -0,0 +1,727 @@
package commands
import (
"fmt"
"strings"
)
// RegisterPlayerCommands registers all player-level commands
func RegisterPlayerCommands(cm *CommandManager) error {
commands := []*Command{
// Communication commands
{
Name: "say",
Type: CommandTypePlayer,
Description: "Says something to nearby players",
Usage: "/say <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleSay,
},
{
Name: "tell",
Type: CommandTypePlayer,
Description: "Sends a private message to a player",
Usage: "/tell <player> <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleTell,
},
{
Name: "yell",
Type: CommandTypePlayer,
Description: "Yells something to a wider area",
Usage: "/yell <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleYell,
},
{
Name: "shout",
Type: CommandTypePlayer,
Description: "Shouts something zone-wide",
Usage: "/shout <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleShout,
},
{
Name: "ooc",
Type: CommandTypePlayer,
Description: "Sends an out-of-character message",
Usage: "/ooc <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleOOC,
},
{
Name: "emote",
Type: CommandTypePlayer,
Description: "Performs an emote",
Usage: "/emote <action>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleEmote,
},
// Group commands
{
Name: "group",
Type: CommandTypePlayer,
Description: "Group management commands",
Usage: "/group <invite|leave|kick|disband> [player]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleGroup,
},
{
Name: "groupsay",
Type: CommandTypePlayer,
Description: "Says something to your group",
Usage: "/groupsay <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleGroupSay,
},
{
Name: "gsay",
Type: CommandTypePlayer,
Description: "Says something to your group (short form)",
Usage: "/gsay <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleGroupSay,
},
// Guild commands
{
Name: "guild",
Type: CommandTypePlayer,
Description: "Guild management commands",
Usage: "/guild <invite|leave|promote|demote|kick> [player]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleGuild,
},
{
Name: "guildsay",
Type: CommandTypePlayer,
Description: "Says something to your guild",
Usage: "/guildsay <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleGuildSay,
},
{
Name: "officersay",
Type: CommandTypePlayer,
Description: "Says something to guild officers",
Usage: "/officersay <message>",
RequiredLevel: AdminLevelPlayer,
Handler: HandleOfficerSay,
},
// Character commands
{
Name: "who",
Type: CommandTypePlayer,
Description: "Lists players online",
Usage: "/who [name pattern]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleWho,
},
{
Name: "time",
Type: CommandTypePlayer,
Description: "Shows current game time",
Usage: "/time",
RequiredLevel: AdminLevelPlayer,
Handler: HandleTime,
},
{
Name: "afk",
Type: CommandTypePlayer,
Description: "Toggles AFK status",
Usage: "/afk [message]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleAFK,
},
{
Name: "anon",
Type: CommandTypePlayer,
Description: "Toggles anonymous status",
Usage: "/anon",
RequiredLevel: AdminLevelPlayer,
Handler: HandleAnonymous,
},
{
Name: "lfg",
Type: CommandTypePlayer,
Description: "Toggles looking for group status",
Usage: "/lfg",
RequiredLevel: AdminLevelPlayer,
Handler: HandleLFG,
},
{
Name: "location",
Type: CommandTypePlayer,
Description: "Shows current location",
Usage: "/location",
RequiredLevel: AdminLevelPlayer,
Handler: HandleLocation,
},
// Item commands
{
Name: "inventory",
Type: CommandTypePlayer,
Description: "Shows inventory information",
Usage: "/inventory",
RequiredLevel: AdminLevelPlayer,
Handler: HandleInventory,
},
{
Name: "consider",
Type: CommandTypePlayer,
Description: "Considers a target",
Usage: "/consider [target]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleConsider,
},
// Trade commands
{
Name: "trade",
Type: CommandTypePlayer,
Description: "Trade management",
Usage: "/trade <start|accept|reject|cancel> [player]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleTrade,
},
// Quest commands
{
Name: "quest",
Type: CommandTypePlayer,
Description: "Quest management",
Usage: "/quest <list|abandon|share> [quest_id]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleQuest,
},
// Misc commands
{
Name: "help",
Type: CommandTypePlayer,
Description: "Shows available commands",
Usage: "/help [command]",
RequiredLevel: AdminLevelPlayer,
Handler: HandleHelp,
},
{
Name: "quit",
Type: CommandTypePlayer,
Description: "Safely logs out of the game",
Usage: "/quit",
RequiredLevel: AdminLevelPlayer,
Handler: HandleQuit,
},
}
for _, cmd := range commands {
if err := cm.Register(cmd); err != nil {
return fmt.Errorf("failed to register command %s: %w", cmd.Name, err)
}
}
return nil
}
// HandleSay handles the /say command
func HandleSay(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /say <message>")
return nil
}
message := ctx.RawArguments
// TODO: Implement actual zone-wide say broadcast
// For now, just confirm to the player
ctx.AddMessage(ChannelSay, ColorWhite, fmt.Sprintf("You say, \"%s\"", message))
// TODO: Send to nearby players in zone
// zone.SendNearbyMessage(ctx.Player.GetPosition(), ChannelSay,
// fmt.Sprintf("%s says, \"%s\"", playerName, message))
return nil
}
// HandleTell handles the /tell command
func HandleTell(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(2, -1); err != nil {
ctx.AddErrorMessage("Usage: /tell <player> <message>")
return nil
}
targetName := ctx.Arguments[0]
message := ctx.GetRemainingArguments(1)
// TODO: Implement actual tell system
// For now, just show what would be sent
ctx.AddMessage(ChannelPrivateTell, ColorWhite,
fmt.Sprintf("You tell %s, \"%s\"", targetName, message))
// TODO: Find target player and send message
// if targetPlayer := world.FindPlayerByName(targetName); targetPlayer != nil {
// targetPlayer.SendMessage(ChannelPrivateTell,
// fmt.Sprintf("%s tells you, \"%s\"", playerName, message))
// } else {
// ctx.AddErrorMessage(fmt.Sprintf("Player '%s' not found", targetName))
// }
return nil
}
// HandleYell handles the /yell command
func HandleYell(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /yell <message>")
return nil
}
message := ctx.RawArguments
ctx.AddMessage(ChannelYell, ColorWhite, fmt.Sprintf("You yell, \"%s\"", message))
// TODO: Send to wider area in zone
return nil
}
// HandleShout handles the /shout command
func HandleShout(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /shout <message>")
return nil
}
message := ctx.RawArguments
ctx.AddMessage(ChannelShout, ColorWhite, fmt.Sprintf("You shout, \"%s\"", message))
// TODO: Send zone-wide
return nil
}
// HandleOOC handles the /ooc command
func HandleOOC(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /ooc <message>")
return nil
}
message := ctx.RawArguments
playerName := ctx.GetPlayerName()
ctx.AddMessage(ChannelOutOfCharacter, ColorWhite,
fmt.Sprintf("[OOC] %s: %s", playerName, message))
// TODO: Send to zone OOC channel
return nil
}
// HandleEmote handles the /emote command
func HandleEmote(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /emote <action>")
return nil
}
action := ctx.RawArguments
playerName := ctx.GetPlayerName()
ctx.AddMessage(ChannelEmote, ColorWhite, fmt.Sprintf("%s %s", playerName, action))
// TODO: Send emote to nearby players
return nil
}
// HandleGroup handles the /group command
func HandleGroup(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: /group <invite|leave|kick|disband> [player]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "invite":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: /group invite <player>")
return nil
}
targetName := ctx.Arguments[1]
// TODO: Implement group invite
ctx.AddStatusMessage(fmt.Sprintf("Invited %s to group", targetName))
case "leave":
// TODO: Implement group leave
ctx.AddStatusMessage("Left group")
case "kick":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: /group kick <player>")
return nil
}
targetName := ctx.Arguments[1]
// TODO: Implement group kick
ctx.AddStatusMessage(fmt.Sprintf("Kicked %s from group", targetName))
case "disband":
// TODO: Implement group disband
ctx.AddStatusMessage("Disbanded group")
default:
ctx.AddErrorMessage("Usage: /group <invite|leave|kick|disband> [player]")
}
return nil
}
// HandleGroupSay handles the /groupsay and /gsay commands
func HandleGroupSay(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /groupsay <message>")
return nil
}
message := ctx.RawArguments
playerName := ctx.GetPlayerName()
// TODO: Check if player is in a group
ctx.AddMessage(ChannelGroupSay, ColorWhite,
fmt.Sprintf("[Group] %s: %s", playerName, message))
// TODO: Send to group members
return nil
}
// HandleGuild handles the /guild command
func HandleGuild(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if ctx.ArgumentCount() == 0 {
// TODO: Show guild information
ctx.AddStatusMessage("Guild information (not yet implemented)")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "invite":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: /guild invite <player>")
return nil
}
targetName := ctx.Arguments[1]
// TODO: Implement guild invite
ctx.AddStatusMessage(fmt.Sprintf("Invited %s to guild", targetName))
case "leave":
// TODO: Implement guild leave
ctx.AddStatusMessage("Left guild")
default:
ctx.AddErrorMessage("Usage: /guild <invite|leave|promote|demote|kick> [player]")
}
return nil
}
// HandleGuildSay handles the /guildsay command
func HandleGuildSay(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /guildsay <message>")
return nil
}
message := ctx.RawArguments
playerName := ctx.GetPlayerName()
// TODO: Check if player is in a guild
ctx.AddMessage(ChannelGuildSay, ColorWhite,
fmt.Sprintf("[Guild] %s: %s", playerName, message))
return nil
}
// HandleOfficerSay handles the /officersay command
func HandleOfficerSay(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if err := ctx.ValidateArgumentCount(1, -1); err != nil {
ctx.AddErrorMessage("Usage: /officersay <message>")
return nil
}
message := ctx.RawArguments
playerName := ctx.GetPlayerName()
// TODO: Check if player is guild officer
ctx.AddMessage(ChannelOfficerSay, ColorWhite,
fmt.Sprintf("[Officer] %s: %s", playerName, message))
return nil
}
// HandleWho handles the /who command
func HandleWho(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Implement player listing
ctx.AddMessage(ChannelWho, ColorWhite, "Players online:")
ctx.AddMessage(ChannelWho, ColorWhite, fmt.Sprintf(" %s (You)", ctx.GetPlayerName()))
ctx.AddMessage(ChannelWho, ColorWhite, "Total: 1 player online")
return nil
}
// HandleTime handles the /time command
func HandleTime(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Implement game time system
ctx.AddStatusMessage("Game time: 12:00 PM (not yet implemented)")
return nil
}
// HandleAFK handles the /afk command
func HandleAFK(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
afkMessage := ""
if ctx.ArgumentCount() > 0 {
afkMessage = ctx.RawArguments
}
// TODO: Toggle AFK status
if afkMessage != "" {
ctx.AddStatusMessage(fmt.Sprintf("AFK status set with message: %s", afkMessage))
} else {
ctx.AddStatusMessage("AFK status toggled")
}
return nil
}
// HandleAnonymous handles the /anon command
func HandleAnonymous(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Toggle anonymous status
ctx.AddStatusMessage("Anonymous status toggled")
return nil
}
// HandleLFG handles the /lfg command
func HandleLFG(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Toggle LFG status
ctx.AddStatusMessage("Looking for group status toggled")
return nil
}
// HandleLocation handles the /location command
func HandleLocation(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Get actual player position
zoneName := ctx.GetZoneName()
ctx.AddStatusMessage(fmt.Sprintf("Location: %s (coordinates not yet implemented)", zoneName))
return nil
}
// HandleInventory handles the /inventory command
func HandleInventory(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
// TODO: Show inventory information
ctx.AddStatusMessage("Inventory information (not yet implemented)")
return nil
}
// HandleConsider handles the /consider command
func HandleConsider(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
targetName := ctx.GetTargetName()
if targetName == "No Target" && ctx.ArgumentCount() > 0 {
targetName = ctx.Arguments[0]
}
if targetName == "No Target" {
ctx.AddErrorMessage("You must target something or specify a target name")
return nil
}
// TODO: Implement consider system
ctx.AddStatusMessage(fmt.Sprintf("You consider %s... (not yet implemented)", targetName))
return nil
}
// HandleTrade handles the /trade command
func HandleTrade(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: /trade <start|accept|reject|cancel> [player]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "start":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: /trade start <player>")
return nil
}
targetName := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Trade initiated with %s", targetName))
case "accept":
ctx.AddStatusMessage("Trade accepted")
case "reject":
ctx.AddStatusMessage("Trade rejected")
case "cancel":
ctx.AddStatusMessage("Trade cancelled")
default:
ctx.AddErrorMessage("Usage: /trade <start|accept|reject|cancel> [player]")
}
return nil
}
// HandleQuest handles the /quest command
func HandleQuest(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
if ctx.ArgumentCount() == 0 {
ctx.AddErrorMessage("Usage: /quest <list|abandon|share> [quest_id]")
return nil
}
subCommand := strings.ToLower(ctx.Arguments[0])
switch subCommand {
case "list":
// TODO: List active quests
ctx.AddStatusMessage("Active quests (not yet implemented)")
case "abandon":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: /quest abandon <quest_id>")
return nil
}
questID := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Abandoned quest %s", questID))
case "share":
if ctx.ArgumentCount() < 2 {
ctx.AddErrorMessage("Usage: /quest share <quest_id>")
return nil
}
questID := ctx.Arguments[1]
ctx.AddStatusMessage(fmt.Sprintf("Shared quest %s", questID))
default:
ctx.AddErrorMessage("Usage: /quest <list|abandon|share> [quest_id]")
}
return nil
}
// HandleHelp handles the /help command
func HandleHelp(ctx *CommandContext) error {
// TODO: Show available commands based on admin level
ctx.AddMessage(ChannelCommands, ColorWhite, "Available commands:")
ctx.AddMessage(ChannelCommands, ColorWhite, "/say <message> - Say something to nearby players")
ctx.AddMessage(ChannelCommands, ColorWhite, "/tell <player> <message> - Send private message")
ctx.AddMessage(ChannelCommands, ColorWhite, "/who - List online players")
ctx.AddMessage(ChannelCommands, ColorWhite, "/time - Show game time")
ctx.AddMessage(ChannelCommands, ColorWhite, "/location - Show current location")
ctx.AddMessage(ChannelCommands, ColorWhite, "/help - Show this help")
return nil
}
// HandleQuit handles the /quit command
func HandleQuit(ctx *CommandContext) error {
if err := ctx.RequirePlayer(); err != nil {
return err
}
ctx.AddStatusMessage("Logging out safely...")
// TODO: Implement safe logout
// if ctx.Client != nil {
// ctx.Client.Disconnect()
// }
return nil
}

189
internal/commands/types.go Normal file
View File

@ -0,0 +1,189 @@
package commands
import (
"context"
"sync"
"eq2emu/internal/entity"
"eq2emu/internal/spawn"
)
// CommandType represents different types of commands
type CommandType int
const (
CommandTypePlayer CommandType = iota // Player commands (/say, /tell, etc.)
CommandTypeAdmin // Admin commands (/kick, /ban, etc.)
CommandTypeConsole // Console commands (shutdown, reload, etc.)
CommandTypeSpawn // Spawn manipulation commands
CommandTypeZone // Zone management commands
CommandTypeGuild // Guild commands
CommandTypeItem // Item commands
CommandTypeQuest // Quest commands
)
func (ct CommandType) String() string {
switch ct {
case CommandTypePlayer:
return "player"
case CommandTypeAdmin:
return "admin"
case CommandTypeConsole:
return "console"
case CommandTypeSpawn:
return "spawn"
case CommandTypeZone:
return "zone"
case CommandTypeGuild:
return "guild"
case CommandTypeItem:
return "item"
case CommandTypeQuest:
return "quest"
default:
return "unknown"
}
}
// CommandContext provides context for command execution
type CommandContext struct {
// Core context
Context context.Context
// Command information
CommandType CommandType
CommandName string
Arguments []string
RawArguments string
AdminLevel int
RequiredLevel int
// Game objects (nil if not applicable)
Client ClientInterface
Player *entity.Entity
Target *spawn.Spawn
Zone ZoneInterface
// Results and messages
Messages []CommandMessage
Results map[string]any
// Synchronization
mutex sync.RWMutex
}
// CommandMessage represents a message to send to the client
type CommandMessage struct {
Channel int
Color int
Message string
}
// CommandFunction represents a callable command function
type CommandFunction func(ctx *CommandContext) error
// Command represents a registered command
type Command struct {
Name string
Type CommandType
Description string
Usage string
RequiredLevel int
Handler CommandFunction
SubCommands map[string]*Command
}
// CommandManager manages command registration and execution
type CommandManager struct {
commands map[string]*Command
mutex sync.RWMutex
}
// ClientInterface defines the interface for client interactions
type ClientInterface interface {
GetPlayer() *entity.Entity
GetAccountID() int32
GetCharacterID() int32
GetAdminLevel() int
GetName() string
IsInZone() bool
GetZone() ZoneInterface
SendMessage(channel int, color int, message string)
SendPopupMessage(message string)
Disconnect()
}
// ZoneInterface defines the interface for zone interactions
type ZoneInterface interface {
GetID() int32
GetName() string
GetDescription() string
GetPlayers() []*entity.Entity
Shutdown()
SendZoneMessage(channel int, color int, message string)
GetSpawnByName(name string) *spawn.Spawn
GetSpawnByID(id int32) *spawn.Spawn
}
// Chat channel constants (from C++ Commands.h)
const (
ChannelAllText = 0
ChannelGameText = 1
ChannelDefault = 2
ChannelError = 3
ChannelStatus = 4
ChannelMOTD = 5
ChannelChatText = 6
ChannelNearbyCHat = 7
ChannelSay = 8
ChannelShout = 9
ChannelEmote = 10
ChannelYell = 11
ChannelNarrative = 12
ChannelNonPlayerSay = 13
ChannelGroupChat = 14
ChannelGroupSay = 15
ChannelRaidSay = 16
ChannelGuildChat = 17
ChannelGuildSay = 18
ChannelOfficerSay = 19
ChannelGuildMOTD = 20
ChannelPrivateChat = 25
ChannelPrivateTell = 28
ChannelOutOfCharacter = 32
ChannelBroadcast = 92
ChannelWho = 93
ChannelCommands = 94
)
// Chat color constants (from C++ Commands.h)
const (
ColorRed = 3
ColorChatRelation = 4
ColorYellow = 5
ColorNewLoot = 84
ColorNewestLoot = 89
ColorWhite = 254
)
// Admin level constants
const (
AdminLevelPlayer = 0
AdminLevelGuide = 100
AdminLevelGM = 200
AdminLevelAdmin = 300
)
// ErrorHandler defines how command errors should be handled
type ErrorHandler interface {
HandleCommandError(ctx *CommandContext, err error)
}
// CommandLogger defines logging interface for commands
type CommandLogger interface {
LogCommand(ctx *CommandContext, success bool, err error)
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}

View File

@ -16,8 +16,8 @@ func NewEventContext(eventType EventType, eventName, functionName string) *Event
EventType: eventType,
EventName: eventName,
FunctionName: functionName,
Parameters: make(map[string]interface{}),
Results: make(map[string]interface{}),
Parameters: make(map[string]any),
Results: make(map[string]any),
}
}
@ -46,19 +46,19 @@ func (ctx *EventContext) WithQuest(quest *quests.Quest) *EventContext {
}
// WithParameter adds a parameter to the context
func (ctx *EventContext) WithParameter(name string, value interface{}) *EventContext {
func (ctx *EventContext) WithParameter(name string, value any) *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 {
func (ctx *EventContext) WithParameters(params map[string]any) *EventContext {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
for k, v := range params {
ctx.Parameters[k] = v
}
@ -86,10 +86,10 @@ func (ctx *EventContext) GetQuest() *quests.Quest {
}
// GetParameter retrieves a parameter
func (ctx *EventContext) GetParameter(name string) (interface{}, bool) {
func (ctx *EventContext) GetParameter(name string) (any, bool) {
ctx.mutex.RLock()
defer ctx.mutex.RUnlock()
value, exists := ctx.Parameters[name]
return value, exists
}
@ -153,41 +153,41 @@ func (ctx *EventContext) GetParameterBool(name string, defaultValue bool) bool {
}
// SetResult sets a result value
func (ctx *EventContext) SetResult(name string, value interface{}) {
func (ctx *EventContext) SetResult(name string, value any) {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
if ctx.Results == nil {
ctx.Results = make(map[string]interface{})
ctx.Results = make(map[string]any)
}
ctx.Results[name] = value
}
// GetResult retrieves a result value
func (ctx *EventContext) GetResult(name string) (interface{}, bool) {
func (ctx *EventContext) GetResult(name string) (any, 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{}) {
func (ctx *EventContext) Debug(msg string, args ...any) {
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{}) {
func (ctx *EventContext) Info(msg string, args ...any) {
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{}) {
func (ctx *EventContext) Warn(msg string, args ...any) {
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{}) {
func (ctx *EventContext) Error(msg string, args ...any) {
fmt.Printf("[ERROR] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...))
}
}

View File

@ -15,10 +15,7 @@ func SetInt(ctx *events.EventContext) error {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
value := max(ctx.GetParameterInt("value", 0), 0)
// TODO: Implement INT stat when InfoStruct is available
ctx.Debug("Set INT to %d for spawn %s (not yet implemented)", value, spawn.GetName())
@ -33,10 +30,7 @@ func SetWis(ctx *events.EventContext) error {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
value := max(ctx.GetParameterInt("value", 0), 0)
// TODO: Implement WIS stat when InfoStruct is available
ctx.Debug("Set WIS to %d for spawn %s (not yet implemented)", value, spawn.GetName())
@ -51,10 +45,7 @@ func SetSta(ctx *events.EventContext) error {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
value := max(ctx.GetParameterInt("value", 0), 0)
// TODO: Implement STA stat when InfoStruct is available
ctx.Debug("Set STA to %d for spawn %s (not yet implemented)", value, spawn.GetName())
@ -69,10 +60,7 @@ func SetStr(ctx *events.EventContext) error {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
value := max(ctx.GetParameterInt("value", 0), 0)
// TODO: Implement STR stat when InfoStruct is available
ctx.Debug("Set STR to %d for spawn %s (not yet implemented)", value, spawn.GetName())
@ -87,10 +75,7 @@ func SetAgi(ctx *events.EventContext) error {
return fmt.Errorf("no spawn in context")
}
value := ctx.GetParameterInt("value", 0)
if value < 0 {
value = 0
}
value := max(ctx.GetParameterInt("value", 0), 0)
// TODO: Implement AGI stat when InfoStruct is available
ctx.Debug("Set AGI to %d for spawn %s (not yet implemented)", value, spawn.GetName())
@ -236,13 +221,7 @@ func SetLevel(ctx *events.EventContext) error {
return fmt.Errorf("no spawn in context")
}
level := ctx.GetParameterInt("level", 1)
if level < 1 {
level = 1
}
if level > 100 {
level = 100
}
level := min(max(ctx.GetParameterInt("level", 1), 1), 100)
spawn.SetLevel(int16(level))
ctx.Debug("Set level to %d for spawn %s", level, spawn.GetName())
@ -289,9 +268,9 @@ func AddSpellBonus(ctx *events.EventContext) error {
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)",
ctx.Debug("Added spell bonus (type: %d, value: %f, spell: %d) to spawn %s (not yet implemented)",
bonusType, value, spellID, spawn.GetName())
return nil
}
@ -306,9 +285,9 @@ func RemoveSpellBonus(ctx *events.EventContext) error {
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)",
ctx.Debug("Removed spell bonus (type: %d, spell: %d) from spawn %s (not yet implemented)",
bonusType, spellID, spawn.GetName())
return nil
}
@ -323,9 +302,9 @@ func AddSkillBonus(ctx *events.EventContext) error {
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)",
ctx.Debug("Added skill bonus (type: %d, value: %f) to spawn %s (not yet implemented)",
skillType, value, spawn.GetName())
return nil
}
@ -339,9 +318,9 @@ func RemoveSkillBonus(ctx *events.EventContext) error {
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)",
ctx.Debug("Removed skill bonus (type: %d) from spawn %s (not yet implemented)",
skillType, spawn.GetName())
return nil
}
@ -417,13 +396,7 @@ func SetTradeskillLevel(ctx *events.EventContext) error {
return fmt.Errorf("no spawn in context")
}
level := ctx.GetParameterInt("level", 1)
if level < 1 {
level = 1
}
if level > 100 {
level = 100
}
level := min(max(ctx.GetParameterInt("level", 1), 1), 100)
// TODO: Implement tradeskill level when available
ctx.Debug("Set tradeskill level to %d for spawn %s (not yet implemented)", level, spawn.GetName())
@ -472,7 +445,7 @@ func SetModelType(ctx *events.EventContext) error {
}
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
@ -498,7 +471,7 @@ func SetDeity(ctx *events.EventContext) error {
}
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
@ -524,8 +497,8 @@ func SetAlignment(ctx *events.EventContext) error {
}
alignment := ctx.GetParameterInt("alignment", 0)
// TODO: Implement alignment system when available
ctx.Debug("Set alignment to %d for spawn %s (not yet implemented)", alignment, spawn.GetName())
return nil
}
}

View File

@ -12,11 +12,11 @@ import (
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")
}
@ -30,11 +30,11 @@ func Attack(ctx *events.EventContext) error {
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")
}
@ -75,7 +75,7 @@ func GetMostHated(ctx *events.EventContext) error {
func SetTarget(ctx *events.EventContext) error {
spawn := ctx.GetSpawn()
target := ctx.GetTarget()
if spawn == nil {
return fmt.Errorf("no spawn in context")
}
@ -131,11 +131,11 @@ func SetInCombat(ctx *events.EventContext) error {
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")
}
@ -148,9 +148,9 @@ func SpellDamage(ctx *events.EventContext) error {
}
// TODO: Implement spell damage system with damage types, resistances, etc.
ctx.Debug("Spell damage %f (type %d) from %s to %s (not yet implemented)",
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
}
@ -159,11 +159,11 @@ func SpellDamage(ctx *events.EventContext) error {
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")
}
@ -178,9 +178,9 @@ func SpellDamageExt(ctx *events.EventContext) error {
}
// 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)",
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
}
@ -222,11 +222,11 @@ func DamageSpawn(ctx *events.EventContext) error {
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")
}
@ -239,9 +239,9 @@ func ProcDamage(ctx *events.EventContext) error {
}
// TODO: Implement proc damage system
ctx.Debug("Proc damage %f (type %d) from %s to %s (not yet implemented)",
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
}
@ -250,11 +250,11 @@ func ProcDamage(ctx *events.EventContext) error {
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")
}
@ -270,11 +270,11 @@ func ProcHate(ctx *events.EventContext) error {
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")
}
@ -283,7 +283,7 @@ func Knockback(ctx *events.EventContext) error {
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)",
ctx.Debug("Knockback target %s distance %f with vertical %f from %s (not yet implemented)",
target.GetName(), distance, verticalLift, caster.GetName())
return nil
}
@ -291,7 +291,7 @@ func Knockback(ctx *events.EventContext) error {
// 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")
}
@ -365,11 +365,11 @@ func LastSpellAttackHit(ctx *events.EventContext) error {
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")
}
@ -383,11 +383,11 @@ func IsBehind(ctx *events.EventContext) error {
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")
}
@ -401,11 +401,11 @@ func IsFlanking(ctx *events.EventContext) error {
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")
}
@ -435,7 +435,7 @@ func GetEncounter(ctx *events.EventContext) error {
}
// TODO: Implement encounter retrieval
ctx.SetResult("encounter", []interface{}{}) // Empty list for now
ctx.SetResult("encounter", []any{}) // Empty list for now
return nil
}
@ -447,7 +447,7 @@ func GetHateList(ctx *events.EventContext) error {
}
// TODO: Implement hate list retrieval
ctx.SetResult("hate_list", []interface{}{}) // Empty list for now
ctx.SetResult("hate_list", []any{}) // Empty list for now
return nil
}
@ -503,11 +503,11 @@ func GetRunbackDistance(ctx *events.EventContext) error {
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")
}
@ -557,7 +557,7 @@ func Resurrect(ctx *events.EventContext) error {
// Restore HP and power
maxHP := float64(spawn.GetTotalHP())
maxPower := float64(spawn.GetTotalPower())
newHP := maxHP * (hpPercent / 100.0)
newPower := maxPower * (powerPercent / 100.0)
@ -565,7 +565,7 @@ func Resurrect(ctx *events.EventContext) error {
spawn.SetPower(int32(newPower))
spawn.SetAlive(true)
ctx.Debug("Resurrected spawn %s with %.1f%% HP and %.1f%% power",
ctx.Debug("Resurrected spawn %s with %.1f%% HP and %.1f%% power",
spawn.GetName(), hpPercent, powerPercent)
return nil
}
@ -608,4 +608,4 @@ func SetAttackable(ctx *events.EventContext) error {
// TODO: Implement attackable flag
ctx.Debug("Set attackable to %t for spawn %s (not yet implemented)", attackable, spawn.GetName())
return nil
}
}

View File

@ -34,7 +34,7 @@ func SetMaxHP(ctx *events.EventContext) error {
maxHP := ctx.GetParameterFloat("max_hp", 0)
if maxHP < 0 {
return fmt.Errorf("Max HP cannot be negative")
return fmt.Errorf("max HP cannot be negative")
}
spawn.SetTotalHP(int32(maxHP))
@ -74,7 +74,7 @@ func SetMaxPower(ctx *events.EventContext) error {
maxPower := ctx.GetParameterFloat("max_power", 0)
if maxPower < 0 {
return fmt.Errorf("Max power cannot be negative")
return fmt.Errorf("max power cannot be negative")
}
spawn.SetTotalPower(int32(maxPower))
@ -323,7 +323,7 @@ func SpellHeal(ctx *events.EventContext) error {
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)
@ -362,4 +362,4 @@ func IsAlive(ctx *events.EventContext) error {
ctx.SetResult("is_alive", spawn.IsAlive())
return nil
}
}

View File

@ -57,8 +57,8 @@ type EventContext struct {
Quest *quests.Quest
// Parameters and results
Parameters map[string]interface{}
Results map[string]interface{}
Parameters map[string]any
Results map[string]any
// Synchronization
mutex sync.RWMutex
@ -75,8 +75,8 @@ type EventHandler struct {
// 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{})
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}

View File

@ -138,7 +138,7 @@ func createGuildTables(conn *sqlite.Conn) error {
}
// execSQL is a helper to execute SQL with parameters
func execSQL(t *testing.T, pool *sqlitex.Pool, query string, args ...interface{}) {
func execSQL(t *testing.T, pool *sqlitex.Pool, query string, args ...any) {
conn, err := pool.Take(context.Background())
if err != nil {
t.Fatalf("Failed to get connection: %v", err)

View File

@ -36,7 +36,7 @@ func createTestPool(t *testing.T) *sqlitex.Pool {
}
// execSQL is a helper to execute SQL with parameters
func execSQL(t *testing.T, pool *sqlitex.Pool, query string, args ...interface{}) {
func execSQL(t *testing.T, pool *sqlitex.Pool, query string, args ...any) {
conn, err := pool.Take(context.Background())
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
@ -605,8 +605,8 @@ func BenchmarkDatabaseHeroicOPManager_LoadStarters(b *testing.B) {
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
ability5, ability6, name, description)
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`,
i, HOTypeStarter, i%10+1, i*10,
i*1, i*2, i*3, i*4, i*5, i*6,
i, HOTypeStarter, i%10+1, i*10,
i*1, i*2, i*3, i*4, i*5, i*6,
fmt.Sprintf("Starter %d", i), fmt.Sprintf("Description %d", i))
}
@ -718,4 +718,4 @@ func BenchmarkDatabaseHeroicOPManager_SaveHOInstance(b *testing.B) {
b.Fatalf("Failed to save HO instance: %v", err)
}
}
}
}

View File

@ -20,9 +20,9 @@ type LootSystem struct {
// LootSystemConfig holds configuration for the loot system
type LootSystemConfig struct {
DatabasePool *sqlitex.Pool
DatabasePool *sqlitex.Pool
// @TODO: Fix MasterItemListService type import
ItemMasterList interface{} // was items.MasterItemListService
ItemMasterList any // was items.MasterItemListService
PlayerService PlayerService
ZoneService ZoneService
ClientService ClientService
@ -339,7 +339,7 @@ func (ls *LootSystem) ValidateItemsInLootTables() []ValidationError {
for _, drop := range table.Drops {
// @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled
// item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
var item interface{} = nil
var item any = nil
if item == nil {
errors = append(errors, ValidationError{
Type: "missing_item",
@ -395,7 +395,7 @@ func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*Loot
for _, drop := range table.Drops {
// @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled
// item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
var item interface{} = nil
var item any = nil
if item == nil {
continue
}

View File

@ -12,19 +12,19 @@ import (
// LootManager handles all loot generation and management
type LootManager struct {
database *LootDatabase
database *LootDatabase
// @TODO: Fix MasterItemListService type import
itemMasterList interface{} // was items.MasterItemListService
statistics *LootStatistics
itemMasterList any // was items.MasterItemListService
statistics *LootStatistics
treasureChests map[int32]*TreasureChest // chest_id -> TreasureChest
chestIDCounter int32
random *rand.Rand
mutex sync.RWMutex
random *rand.Rand
mutex sync.RWMutex
}
// NewLootManager creates a new loot manager
// @TODO: Fix MasterItemListService type import
func NewLootManager(database *LootDatabase, itemMasterList interface{}) *LootManager {
func NewLootManager(database *LootDatabase, itemMasterList any) *LootManager {
return &LootManager{
database: database,
itemMasterList: itemMasterList,
@ -38,7 +38,7 @@ func NewLootManager(database *LootDatabase, itemMasterList interface{}) *LootMan
// GenerateLoot generates loot for a spawn based on its loot table assignments
func (lm *LootManager) GenerateLoot(spawnID int32, context *LootContext) (*LootResult, error) {
log.Printf("%s Generating loot for spawn %d", LogPrefixGeneration, spawnID)
result := &LootResult{
Items: make([]*items.Item, 0),
Coins: 0,
@ -46,7 +46,7 @@ func (lm *LootManager) GenerateLoot(spawnID int32, context *LootContext) (*LootR
// Get loot tables for this spawn
tableIDs := lm.database.GetSpawnLootTables(spawnID)
// Also check for global loot tables
globalLoot := lm.database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID)
for _, global := range globalLoot {
@ -71,9 +71,9 @@ func (lm *LootManager) GenerateLoot(spawnID int32, context *LootContext) (*LootR
lm.statistics.RecordLoot(tableIDs[0], result) // Use first table for stats
}
log.Printf("%s Generated %d items and %d coins for spawn %d",
log.Printf("%s Generated %d items and %d coins for spawn %d",
LogPrefixGeneration, len(result.Items), result.Coins, spawnID)
return result, nil
}
@ -133,7 +133,7 @@ func (lm *LootManager) processLootTable(tableID int32, context *LootContext, res
// log.Printf("%s Item template %d not found for loot drop", LogPrefixGeneration, drop.ItemID)
// continue
// }
var itemTemplate interface{} = nil
var itemTemplate any = nil
if itemTemplate == nil {
log.Printf("%s Item template %d not found for loot drop (disabled due to type import issue)", LogPrefixGeneration, drop.ItemID)
continue
@ -143,7 +143,7 @@ func (lm *LootManager) processLootTable(tableID int32, context *LootContext, res
// Create item instance
// item := items.NewItemFromTemplate(itemTemplate)
var item *items.Item = nil
// Set charges if specified
if drop.ItemCharges > 0 {
item.Details.Count = drop.ItemCharges
@ -157,8 +157,8 @@ func (lm *LootManager) processLootTable(tableID int32, context *LootContext, res
result.AddItem(item)
itemsGenerated++
log.Printf("%s Generated item %d (%s) from table %d",
log.Printf("%s Generated item %d (%s) from table %d",
LogPrefixGeneration, drop.ItemID, item.Name, tableID)
}
@ -173,7 +173,7 @@ func (lm *LootManager) rollProbability(probability float32) bool {
if probability >= 100.0 {
return true
}
roll := lm.random.Float32() * 100.0
return roll <= probability
}
@ -183,14 +183,14 @@ func (lm *LootManager) generateCoins(minCoin, maxCoin int32) int32 {
if minCoin >= maxCoin {
return minCoin
}
return minCoin + lm.random.Int31n(maxCoin-minCoin+1)
}
// CreateTreasureChest creates a treasure chest for loot
func (lm *LootManager) CreateTreasureChest(spawnID int32, zoneID int32, x, y, z, heading float32,
func (lm *LootManager) CreateTreasureChest(spawnID int32, zoneID int32, x, y, z, heading float32,
lootResult *LootResult, lootRights []uint32) (*TreasureChest, error) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
@ -223,12 +223,12 @@ func (lm *LootManager) CreateTreasureChest(spawnID int32, zoneID int32, x, y, z,
// Store chest
lm.treasureChests[chestID] = chest
// Record statistics
lm.statistics.RecordChest()
log.Printf("%s Created treasure chest %d (%s) at (%.2f, %.2f, %.2f) with %d items and %d coins",
LogPrefixChest, chestID, appearance.Name, x, y, z,
log.Printf("%s Created treasure chest %d (%s) at (%.2f, %.2f, %.2f) with %d items and %d coins",
LogPrefixChest, chestID, appearance.Name, x, y, z,
len(lootResult.GetItems()), lootResult.GetCoins())
return chest, nil
@ -237,13 +237,13 @@ func (lm *LootManager) CreateTreasureChest(spawnID int32, zoneID int32, x, y, z,
// getHighestItemTier finds the highest tier among items
func (lm *LootManager) getHighestItemTier(items []*items.Item) int8 {
var highest int8 = LootTierCommon
for _, item := range items {
if item.Details.Tier > highest {
highest = item.Details.Tier
}
}
return highest
}
@ -251,7 +251,7 @@ func (lm *LootManager) getHighestItemTier(items []*items.Item) int8 {
func (lm *LootManager) GetTreasureChest(chestID int32) *TreasureChest {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
return lm.treasureChests[chestID]
}
@ -259,7 +259,7 @@ func (lm *LootManager) GetTreasureChest(chestID int32) *TreasureChest {
func (lm *LootManager) RemoveTreasureChest(chestID int32) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
delete(lm.treasureChests, chestID)
log.Printf("%s Removed treasure chest %d", LogPrefixChest, chestID)
}
@ -287,10 +287,10 @@ func (lm *LootManager) LootChestItem(chestID int32, playerID uint32, itemUniqueI
chest.LootResult.mutex.Lock()
chest.LootResult.Items = append(chest.LootResult.Items[:i], chest.LootResult.Items[i+1:]...)
chest.LootResult.mutex.Unlock()
log.Printf("%s Player %d looted item %d (%s) from chest %d",
log.Printf("%s Player %d looted item %d (%s) from chest %d",
LogPrefixChest, playerID, item.Details.ItemID, item.Name, chestID)
return item, nil
}
}
@ -323,7 +323,7 @@ func (lm *LootManager) LootChestCoins(chestID int32, playerID uint32) (int32, er
chest.LootResult.Coins = 0
chest.LootResult.mutex.Unlock()
log.Printf("%s Player %d looted %d coins from chest %d",
log.Printf("%s Player %d looted %d coins from chest %d",
LogPrefixChest, playerID, coins, chestID)
return coins, nil
@ -356,7 +356,7 @@ func (lm *LootManager) LootChestAll(chestID int32, playerID uint32) (*LootResult
chest.LootResult.Coins = 0
chest.LootResult.mutex.Unlock()
log.Printf("%s Player %d looted all (%d items, %d coins) from chest %d",
log.Printf("%s Player %d looted all (%d items, %d coins) from chest %d",
LogPrefixChest, playerID, len(result.Items), result.Coins, chestID)
return result, nil
@ -385,12 +385,12 @@ func (lm *LootManager) CleanupExpiredChests() {
for chestID, chest := range lm.treasureChests {
age := now.Sub(chest.Created).Seconds()
// Remove empty chests after ChestDespawnTime
if chest.LootResult.IsEmpty() && age > ChestDespawnTime {
expired = append(expired, chestID)
}
// Force remove all chests after ChestCleanupTime
if age > ChestCleanupTime {
expired = append(expired, chestID)
@ -437,7 +437,7 @@ func (lm *LootManager) GetPlayerChests(playerID uint32) []*TreasureChest {
return chests
}
// GetStatistics returns loot generation statistics
// GetStatistics returns loot generation statistics
func (lm *LootManager) GetStatistics() LootStatistics {
return lm.statistics.GetStatistics()
}
@ -488,6 +488,6 @@ func (lm *LootManager) StartCleanupTimer() {
lm.CleanupExpiredChests()
}
}()
log.Printf("%s Started chest cleanup timer", LogPrefixLoot)
}
}

View File

@ -74,7 +74,7 @@ func (db *SQLiteDatabase) SaveRaceType(modelType int16, raceType *RaceType) erro
`
err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{
Args: []interface{}{modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName},
Args: []any{modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName},
})
if err != nil {
@ -95,7 +95,7 @@ func (db *SQLiteDatabase) DeleteRaceType(modelType int16) error {
query := `DELETE FROM race_types WHERE model_type = ?`
err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{
Args: []interface{}{modelType},
Args: []any{modelType},
})
if err != nil {
@ -146,4 +146,4 @@ func (db *SQLiteDatabase) CreateRaceTypesTable() error {
}
return nil
}
}

View File

@ -359,7 +359,7 @@ func TestSQLiteDatabaseIndexes(t *testing.T) {
var indexExists bool
query := "SELECT name FROM sqlite_master WHERE type='index' AND name=?"
err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{
Args: []interface{}{indexName},
Args: []any{indexName},
ResultFunc: func(stmt *sqlite.Stmt) error {
indexExists = true
return nil
@ -499,4 +499,4 @@ func BenchmarkSQLiteDatabaseLoad(b *testing.B) {
masterList := NewMasterRaceTypeList()
db.LoadRaceTypes(masterList)
}
}
}

View File

@ -43,7 +43,7 @@ func (p *Player) GetPrimaryStat() int32 {
}
// GetTarget returns the player's current target (stub)
func (p *Player) GetTarget() interface{} {
func (p *Player) GetTarget() any {
// TODO: Implement targeting system
return nil
}
@ -125,7 +125,7 @@ func (p *Player) GetInfoStructCoinCopper() int32 {
return playerCoinBreakdown[p.GetCharacterID()]["copper"]
}
// GetCoinSilver returns silver coins (stub)
// GetCoinSilver returns silver coins (stub)
func (p *Player) GetInfoStructCoinSilver() int32 {
if playerCoinBreakdown[p.GetCharacterID()] == nil {
return 0
@ -526,4 +526,4 @@ func (p *Player) RemoveSkillHelper(skillID int32) {
func GetSpellLinkedTimerID(spellData *spells.SpellData) int32 {
// TODO: Implement LinkedTimerID field access
return 0
}
}

View File

@ -7,18 +7,18 @@ import (
// RecipeManager provides high-level recipe system management with database integration
type RecipeManager struct {
db interface{} // Placeholder for database connection
db any // Placeholder for database connection
masterRecipeList *MasterRecipeList
masterRecipeBookList *MasterRecipeBookList
loadedRecipes map[int32]*Recipe
loadedRecipeBooks map[int32]*Recipe
// Statistics
stats RecipeManagerStats
statisticsEnabled bool
// Thread safety
mu sync.RWMutex
mu sync.RWMutex
}
// RecipeManagerStats contains statistics about recipe system operations
@ -28,7 +28,7 @@ type RecipeManagerStats struct {
LoadOperations int32 `json:"load_operations"`
SaveOperations int32 `json:"save_operations"`
ValidationErrors int32 `json:"validation_errors"`
mu sync.RWMutex
}
@ -68,7 +68,7 @@ func (rm *RecipeManager) GetMasterRecipeBookList() *MasterRecipeBookList {
func (rm *RecipeManager) GetRecipe(recipeID int32) *Recipe {
rm.mu.RLock()
defer rm.mu.RUnlock()
return rm.loadedRecipes[recipeID]
}
@ -76,7 +76,7 @@ func (rm *RecipeManager) GetRecipe(recipeID int32) *Recipe {
func (rm *RecipeManager) GetRecipeBook(bookID int32) *Recipe {
rm.mu.RLock()
defer rm.mu.RUnlock()
return rm.loadedRecipeBooks[bookID]
}
@ -88,7 +88,7 @@ func (rm *RecipeManager) GetStatistics() RecipeManagerStats {
rm.stats.mu.RLock()
defer rm.stats.mu.RUnlock()
// Return a copy without the mutex to avoid copying lock value
return RecipeManagerStats{
TotalRecipesLoaded: rm.stats.TotalRecipesLoaded,
@ -148,6 +148,6 @@ func (rm *RecipeManager) Validate() []string {
func (rm *RecipeManager) Size() (recipes int32, recipeBooks int32) {
rm.mu.RLock()
defer rm.mu.RUnlock()
return int32(len(rm.loadedRecipes)), int32(len(rm.loadedRecipeBooks))
}
}