migrate commands package
This commit is contained in:
parent
8bc92f035a
commit
abec68872f
557
CLAUDE.md
557
CLAUDE.md
@ -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
327
internal/commands/README.md
Normal 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
937
internal/commands/admin.go
Normal 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
|
||||
}
|
85
internal/commands/commands.go
Normal file
85
internal/commands/commands.go
Normal 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
|
||||
}
|
614
internal/commands/commands_test.go
Normal file
614
internal/commands/commands_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
629
internal/commands/console.go
Normal file
629
internal/commands/console.go
Normal 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
|
||||
}
|
291
internal/commands/context.go
Normal file
291
internal/commands/context.go
Normal 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"
|
||||
}
|
247
internal/commands/manager.go
Normal file
247
internal/commands/manager.go
Normal 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
727
internal/commands/player.go
Normal 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
189
internal/commands/types.go
Normal 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)
|
||||
}
|
@ -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...))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user