converted more internals

This commit is contained in:
Sky Johnson 2025-07-30 15:29:01 -05:00
parent 0c048db2d5
commit 1082b47942
52 changed files with 13206 additions and 318 deletions

View File

@ -83,6 +83,14 @@ go run golang.org/x/vuln/cmd/govulncheck@latest ./...
- `trade/` - Player trading system with item/coin exchange and validation - `trade/` - Player trading system with item/coin exchange and validation
- `object/` - Interactive objects extending spawn system (merchants, transporters, devices, collectors) - `object/` - Interactive objects extending spawn system (merchants, transporters, devices, collectors)
- `races/` - Race system with all EQ2 races, alignment classification, stat modifiers, and entity integration - `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
### Network Protocol ### Network Protocol
EverQuest II UDP protocol with reliability layer, RC4 encryption, CRC validation, connection management, packet combining. EverQuest II UDP protocol with reliability layer, RC4 encryption, CRC validation, connection management, packet combining.
@ -101,6 +109,8 @@ XML-driven packet definitions with version-specific formats, conditional fields,
**Core Data Structures:** **Core Data Structures:**
- `internal/common/types.go`: EQ2-specific types (EQ2Color, EQ2Equipment, AppearanceData, etc.) - `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:** **Network Implementation:**
- `internal/udp/server.go`: Multi-connection UDP server - `internal/udp/server.go`: Multi-connection UDP server
@ -157,6 +167,66 @@ XML-driven packet definitions with version-specific formats, conditional fields,
- `internal/races/integration.go`: Entity system integration with RaceAware interface - `internal/races/integration.go`: Entity system integration with RaceAware interface
- `internal/races/manager.go`: High-level race management with statistics and command processing - `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
**Packet Definitions:** **Packet Definitions:**
- `internal/packets/xml/`: XML packet structure definitions - `internal/packets/xml/`: XML packet structure definitions
- `internal/packets/PARSER.md`: Packet definition language documentation - `internal/packets/PARSER.md`: Packet definition language documentation
@ -217,6 +287,26 @@ Command-line flags override JSON configuration.
**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. **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.
All systems are converted from C++ with TODO comments marking areas for future implementation (LUA integration, advanced mechanics, etc.). All systems are converted from C++ with TODO comments marking areas for future implementation (LUA integration, advanced mechanics, etc.).
**Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility. **Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility.

582
internal/GroundSpawn.cpp Normal file
View File

@ -0,0 +1,582 @@
/*
EQ2Emulator: Everquest II Server Emulator
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
This file is part of EQ2Emulator.
EQ2Emulator is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
EQ2Emulator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
*/
#include "GroundSpawn.h"
#include "World.h"
#include "Spells.h"
#include "Rules/Rules.h"
#include "../common/MiscFunctions.h"
#include "../common/Log.h"
extern ConfigReader configReader;
extern MasterSpellList master_spell_list;
extern World world;
extern RuleManager rule_manager;
GroundSpawn::GroundSpawn(){
packet_num = 0;
appearance.difficulty = 0;
spawn_type = 2;
appearance.pos.state = 129;
number_harvests = 0;
num_attempts_per_harvest = 0;
groundspawn_id = 0;
MHarvest.SetName("GroundSpawn::MHarvest");
MHarvestUse.SetName("GroundSpawn::MHarvestUse");
randomize_heading = true; // we by default randomize heading of groundspawns DB overrides
}
GroundSpawn::~GroundSpawn(){
}
EQ2Packet* GroundSpawn::serialize(Player* player, int16 version){
return spawn_serialize(player, version);
}
int8 GroundSpawn::GetNumberHarvests(){
return number_harvests;
}
void GroundSpawn::SetNumberHarvests(int8 val){
number_harvests = val;
}
int8 GroundSpawn::GetAttemptsPerHarvest(){
return num_attempts_per_harvest;
}
void GroundSpawn::SetAttemptsPerHarvest(int8 val){
num_attempts_per_harvest = val;
}
int32 GroundSpawn::GetGroundSpawnEntryID(){
return groundspawn_id;
}
void GroundSpawn::SetGroundSpawnEntryID(int32 val){
groundspawn_id = val;
}
void GroundSpawn::SetCollectionSkill(const char* val){
if(val)
collection_skill = string(val);
}
const char* GroundSpawn::GetCollectionSkill(){
return collection_skill.c_str();
}
void GroundSpawn::ProcessHarvest(Client* client) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Process harvesting for player '%s' (%u)", client->GetPlayer()->GetName(), client->GetPlayer()->GetID());
MHarvest.lock();
vector<GroundSpawnEntry*>* groundspawn_entries = GetZone()->GetGroundSpawnEntries(groundspawn_id);
vector<GroundSpawnEntryItem*>* groundspawn_items = GetZone()->GetGroundSpawnEntryItems(groundspawn_id);
Item* master_item = 0;
Item* master_rare = 0;
Item* item = 0;
Item* item_rare = 0;
int16 lowest_skill_level = 0;
int16 table_choice = 0;
int32 item_choice = 0;
int32 rare_choice = 0;
int8 harvest_type = 0;
int32 item_harvested = 0;
int8 reward_total = 1;
int32 rare_harvested = 0;
int8 rare_item = 0;
bool is_collection = false;
if (!groundspawn_entries || !groundspawn_items) {
LogWrite(GROUNDSPAWN__ERROR, 3, "GSpawn", "No groundspawn entries or items assigned to groundspawn id: %u", groundspawn_id);
client->Message(CHANNEL_COLOR_RED, "Error: There are no groundspawn entries or items assigned to groundspawn id: %u", groundspawn_id);
MHarvest.unlock();
return;
}
if (number_harvests == 0) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Total harvests depleated for groundspawn id: %u", groundspawn_id);
client->SimpleMessage(CHANNEL_COLOR_RED, "Error: This spawn has nothing more to harvest!");
MHarvest.unlock();
return;
}
Skill* skill = 0;
if (collection_skill == "Collecting") {
skill = client->GetPlayer()->GetSkillByName("Gathering");
is_collection = true;
}
else
skill = client->GetPlayer()->GetSkillByName(collection_skill.c_str()); // Fix: #576 - don't skill up yet with GetSkillByName(skill, true), we might be trying to harvest low level
if (!skill) {
LogWrite(GROUNDSPAWN__WARNING, 3, "GSpawn", "Player '%s' lacks the skill: '%s'", client->GetPlayer()->GetName(), collection_skill.c_str());
client->Message(CHANNEL_COLOR_RED, "Error: You do not have the '%s' skill!", collection_skill.c_str());
MHarvest.unlock();
return;
}
int16 totalSkill = skill->current_val;
int32 skillID = master_item_list.GetItemStatIDByName(collection_skill);
int16 max_skill_req_groundspawn = rule_manager.GetZoneRule(client->GetCurrentZoneID(), R_Player, MinSkillMultiplierValue)->GetInt16();
if(max_skill_req_groundspawn < 1) // can't be 0
max_skill_req_groundspawn = 1;
if(skillID != 0xFFFFFFFF)
{
((Entity*)client->GetPlayer())->MStats.lock();
totalSkill += ((Entity*)client->GetPlayer())->stats[skillID];
((Entity*)client->GetPlayer())->MStats.unlock();
}
for (int8 i = 0; i < num_attempts_per_harvest; i++) {
vector<GroundSpawnEntry*> mod_groundspawn_entries;
if (groundspawn_entries) {
vector<GroundSpawnEntry*> highest_match;
vector<GroundSpawnEntry*>::iterator itr;
GroundSpawnEntry* entry = 0; // current data
GroundSpawnEntry* selected_table = 0; // selected table data
// first, iterate through groundspawn_entries, discard tables player cannot use
for (itr = groundspawn_entries->begin(); itr != groundspawn_entries->end(); itr++) {
entry = *itr;
if(entry->min_skill_level > max_skill_req_groundspawn)
max_skill_req_groundspawn = entry->min_skill_level;
// if player lacks skill, skip table
if (entry->min_skill_level > totalSkill)
continue;
// if bonus, but player lacks level, skip table
if (entry->bonus_table && (client->GetPlayer()->GetLevel() < entry->min_adventure_level))
continue;
// build modified entries table
mod_groundspawn_entries.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 5, "GSpawn", "Keeping groundspawn_entry: %i", entry->min_skill_level);
}
// if anything remains, find lowest min_skill_level in remaining set(s)
if (mod_groundspawn_entries.size() > 0) {
vector<GroundSpawnEntry*>::iterator itr;
GroundSpawnEntry* entry = 0;
for (itr = mod_groundspawn_entries.begin(); itr != mod_groundspawn_entries.end(); itr++) {
entry = *itr;
// find the low range of available tables for random roll
if (lowest_skill_level > entry->min_skill_level || lowest_skill_level == 0)
lowest_skill_level = entry->min_skill_level;
}
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Lowest Skill Level: %i", lowest_skill_level);
}
else {
// if no tables chosen, you must lack the skills
// TODO: move this check to LUA when harvest command is first selected
client->Message(CHANNEL_COLOR_RED, "You lack the skills to harvest this node!");
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "All groundspawn_entry tables tossed! No Skills? Something broke?");
MHarvest.unlock();
return;
}
// now roll to see which table to use
table_choice = MakeRandomInt(lowest_skill_level, totalSkill);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random INT for Table by skill level: %i", table_choice);
int16 highest_score = 0;
for (itr = mod_groundspawn_entries.begin(); itr != mod_groundspawn_entries.end(); itr++) {
entry = *itr;
// determines the highest min_skill_level in the current set of tables (if multiple tables)
if (table_choice >= entry->min_skill_level && (highest_score == 0 || highest_score < table_choice)) {
// removes old highest for the new one
highest_match.clear();
highest_score = entry->min_skill_level;
}
// if the score = level, push into highest_match set
if (highest_score == entry->min_skill_level)
highest_match.push_back(entry);
}
// if there is STILL more than 1 table player qualifies for, rand() and pick one
if (highest_match.size() > 1) {
int16 rand_index = rand() % highest_match.size();
selected_table = highest_match.at(rand_index);
}
else if (highest_match.size() > 0)
selected_table = highest_match.at(0);
// by this point, we should have 1 table who's min skill matches the score (selected_table)
if (selected_table) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Using Table: %i, %i, %i, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %i",
selected_table->min_skill_level,
selected_table->min_adventure_level,
selected_table->bonus_table,
selected_table->harvest1,
selected_table->harvest3,
selected_table->harvest5,
selected_table->harvest_imbue,
selected_table->harvest_rare,
selected_table->harvest10,
selected_table->harvest_coin);
// roll 1-100 for chance-to-harvest percentage
float chance = MakeRandomFloat(0, 100);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random FLOAT for harvest percentages: %.2f", chance);
// starting with the lowest %, select a harvest type + reward qty
if (chance <= selected_table->harvest10 && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 10 items + Rare Item from table : %i", selected_table->min_skill_level);
harvest_type = 6;
reward_total = 10;
}
else if (chance <= selected_table->harvest_rare && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest Rare Item from table : %i", selected_table->min_skill_level);
harvest_type = 5;
}
else if (chance <= selected_table->harvest_imbue && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest Imbue Item from table : %i", selected_table->min_skill_level);
harvest_type = 4;
}
else if (chance <= selected_table->harvest5 && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 5 Items from table : %i", selected_table->min_skill_level);
harvest_type = 3;
reward_total = 5;
}
else if (chance <= selected_table->harvest3 && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 3 Items from table : %i", selected_table->min_skill_level);
harvest_type = 2;
reward_total = 3;
}
else if (chance <= selected_table->harvest1 || totalSkill >= skill->max_val || is_collection) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 1 Item from table : %i", selected_table->min_skill_level);
harvest_type = 1;
}
else
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest nothing...");
float node_maxskill_multiplier = rule_manager.GetZoneRule(client->GetCurrentZoneID(), R_Player, HarvestSkillUpMultiplier)->GetFloat();
if(node_maxskill_multiplier <= 0.0f) {
node_maxskill_multiplier = 1.0f;
}
int16 skillup_max_skill_allowed = (int16)((float)max_skill_req_groundspawn*node_maxskill_multiplier);
if (!is_collection && skill && skill->current_val < skillup_max_skill_allowed) {
skill = client->GetPlayer()->GetSkillByName(collection_skill.c_str(), true); // Fix: #576 - skill up after min skill and adv level checks
}
}
// once you know how many and what type of item to harvest, pick an item from the list
if (harvest_type) {
vector<GroundSpawnEntryItem*> mod_groundspawn_items;
vector<GroundSpawnEntryItem*> mod_groundspawn_rares;
vector<GroundSpawnEntryItem*> mod_groundspawn_imbue;
vector<GroundSpawnEntryItem*>::iterator itr;
GroundSpawnEntryItem* entry = 0;
// iterate through groundspawn_items, discard items player cannot roll for
for (itr = groundspawn_items->begin(); itr != groundspawn_items->end(); itr++) {
entry = *itr;
// if this is a Rare, or an Imbue, but is_rare flag is 0, skip item
if ((harvest_type == 5 || harvest_type == 4) && entry->is_rare == 0)
continue;
// if it is a 1, 3, or 5 and is_rare = 1, skip
else if (harvest_type < 4 && entry->is_rare == 1)
continue;
// if the grid_id on the item matches player grid, or is 0, keep the item
if (!entry->grid_id || (entry->grid_id == client->GetPlayer()->GetLocation())) {
// build modified entries table
if ((entry->is_rare == 1 && harvest_type == 5) || (entry->is_rare == 1 && harvest_type == 6)) {
// if the matching item is rare, or harvest10 push to mod rares
mod_groundspawn_rares.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_rare_item: %u", entry->item_id);
}
if (entry->is_rare == 0 && harvest_type != 4 && harvest_type != 5) {
// if the matching item is normal,or harvest 10 push to mod items
mod_groundspawn_items.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_common_item: %u", entry->item_id);
}
if (entry->is_rare == 2 && harvest_type == 4) {
// if the matching item is imbue item, push to mod imbue
mod_groundspawn_imbue.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_imbue_item: %u", entry->item_id);
}
}
}
// if any items remain in the list, random to see which one gets awarded
if (mod_groundspawn_items.size() > 0) {
// roll to see which item index to use
item_choice = rand() % mod_groundspawn_items.size();
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random INT for which item to award: %i", item_choice);
// set item_id to be awarded
item_harvested = mod_groundspawn_items[item_choice]->item_id;
// if reward is rare, set flag
rare_item = mod_groundspawn_items[item_choice]->is_rare;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID to award: %u, Rare = %i", item_harvested, item_rare);
// if 10+rare, handle additional "rare" reward
if (harvest_type == 6) {
// make sure there is a rare table to choose from!
if (mod_groundspawn_rares.size() > 0) {
// roll to see which rare index to use
rare_choice = rand() % mod_groundspawn_rares.size();
// set (rare) item_id to be awarded
rare_harvested = mod_groundspawn_rares[rare_choice]->item_id;
// we're picking a rare here, so obviously this is true ;)
rare_item = 1;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "RARE Item ID to award: %u", rare_harvested);
}
else {
// all rare entries were eliminated above, or none are assigned. Either way, shouldn't be here!
LogWrite(GROUNDSPAWN__ERROR, 3, "GSpawn", "Groundspawn Entry for '%s' (%i) has no RARE items!", GetName(), GetID());
}
}
}
else if (mod_groundspawn_rares.size() > 0) {
// roll to see which rare index to use
item_choice = rand() % mod_groundspawn_rares.size();
// set (rare) item_id to be awarded
item_harvested = mod_groundspawn_rares[item_choice]->item_id;
// we're picking a rare here, so obviously this is true ;)
rare_item = 1;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "RARE Item ID to award: %u", rare_harvested);
}
else if (mod_groundspawn_imbue.size() > 0) {
// roll to see which rare index to use
item_choice = rand() % mod_groundspawn_imbue.size();
// set (rare) item_id to be awarded
item_harvested = mod_groundspawn_imbue[item_choice]->item_id;
// we're picking a rare here, so obviously this is true ;)
rare_item = 0;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "imbue Item ID to award: %u", rare_harvested);
}
else {
// all item entries were eliminated above, or none are assigned. Either way, shouldn't be here!
LogWrite(GROUNDSPAWN__ERROR, 0, "GSpawn", "Groundspawn Entry for '%s' (%i) has no items!", GetName(), GetID());
}
// if an item was harvested, send updates to client, add item to inventory
if (item_harvested) {
char tmp[200] = { 0 };
// set Normal item harvested
master_item = master_item_list.GetItem(item_harvested);
if (master_item) {
// set details of Normal item
item = new Item(master_item);
// set how many of this item the player receives
item->details.count = reward_total;
// chat box update for normal item (todo: verify output text)
client->Message(CHANNEL_HARVESTING, "You %s %i %s from the %s.", GetHarvestMessageName(true).c_str(), item->details.count, item->CreateItemLink(client->GetVersion(), true).c_str(), GetName());
// add Normal item to player inventory
bool itemDeleted = false;
client->AddItem(item, &itemDeleted);
if(!itemDeleted) {
//Check if the player has a harvesting quest for this
client->GetPlayer()->CheckQuestsHarvestUpdate(item, reward_total);
// if this is a 10+rare, handle sepErately
if (harvest_type == 6 && rare_item == 1) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is Normal. Qty %i", item_harvested, item->details.count);
// send Normal harvest message to client
sprintf(tmp, "\\#64FFFFYou have %s:\12\\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str());
client->SendPopupMessage(10, tmp, "ui_harvested_normal", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_ITEMS_HARVESTED, item->details.count);
// set Rare item harvested
master_rare = master_item_list.GetItem(rare_harvested);
if (master_rare) {
// set details of Rare item
item_rare = new Item(master_rare);
// count of Rare is always 1
item_rare->details.count = 1;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is RARE!", rare_harvested);
// send Rare harvest message to client
sprintf(tmp, "\\#FFFF6ERare item found!\12%s: \\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item_rare->details.count, item_rare->name.c_str());
client->Message(CHANNEL_HARVESTING, "You have found a rare item!");
client->SendPopupMessage(11, tmp, "ui_harvested_rare", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_RARES_HARVESTED, item_rare->details.count);
// chat box update for rare item (todo: verify output text)
client->Message(CHANNEL_HARVESTING, "You %s %i %s from the %s.", GetHarvestMessageName(true).c_str(), item_rare->details.count, item->CreateItemLink(client->GetVersion(), true).c_str(), GetName());
// add Rare item to player inventory
client->AddItem(item_rare);
//Check if the player has a harvesting quest for this
client->GetPlayer()->CheckQuestsHarvestUpdate(item_rare, 1);
}
}
else if (rare_item == 1) {
// if harvest signaled rare or imbue type
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is RARE! Qty: %i", item_harvested, item->details.count);
// send Rare harvest message to client
sprintf(tmp, "\\#FFFF6ERare item found!\12%s: \\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str());
client->Message(CHANNEL_HARVESTING, "You have found a rare item!");
client->SendPopupMessage(11, tmp, "ui_harvested_rare", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_RARES_HARVESTED, item->details.count);
}
else {
// send Normal harvest message to client
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is Normal. Qty %i", item_harvested, item->details.count);
sprintf(tmp, "\\#64FFFFYou have %s:\12\\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str());
client->SendPopupMessage(10, tmp, "ui_harvested_normal", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_ITEMS_HARVESTED, item->details.count);
}
}
}
else {
// error!
LogWrite(GROUNDSPAWN__ERROR, 0, "GSpawn", "Error: Item ID Not Found - %u", item_harvested);
client->Message(CHANNEL_COLOR_RED, "Error: Unable to find item id %u", item_harvested);
}
// decrement # of pulls on this node before it despawns
number_harvests--;
}
else {
// if no item harvested
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "No item_harvested");
client->Message(CHANNEL_HARVESTING, "You failed to %s anything from %s.", GetHarvestMessageName(true, true).c_str(), GetName());
}
}
else {
// if no harvest type
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "No harvest_type");
client->Message(CHANNEL_HARVESTING, "You failed to %s anything from %s.", GetHarvestMessageName(true, true).c_str(), GetName());
}
}
} // cycle through num_attempts_per_harvest
MHarvest.unlock();
LogWrite(GROUNDSPAWN__DEBUG, 0, "GSpawn", "Process harvest complete for player '%s' (%u)", client->GetPlayer()->GetName(), client->GetPlayer()->GetID());
}
string GroundSpawn::GetHarvestMessageName(bool present_tense, bool failure){
string ret = "";
if((collection_skill == "Gathering" ||collection_skill == "Collecting") && !present_tense)
ret = "gathered";
else if(collection_skill == "Gathering" || collection_skill == "Collecting")
ret = "gather";
else if(collection_skill == "Mining" && !present_tense)
ret = "mined";
else if(collection_skill == "Mining")
ret = "mine";
else if (collection_skill == "Fishing" && !present_tense)
ret = "fished";
else if(collection_skill == "Fishing")
ret = "fish";
else if(collection_skill == "Trapping" && !present_tense && !failure)
ret = "acquired";
else if(collection_skill == "Trapping" && failure)
ret = "trap";
else if(collection_skill == "Trapping")
ret = "acquire";
else if(collection_skill == "Foresting" && !present_tense)
ret = "forested";
else if(collection_skill == "Foresting")
ret = "forest";
else if (collection_skill == "Collecting")
ret = "collect";
return ret;
}
string GroundSpawn::GetHarvestSpellType(){
string ret = "";
if(collection_skill == "Gathering" || collection_skill == "Collecting")
ret = "gather";
else if(collection_skill == "Mining")
ret = "mine";
else if(collection_skill == "Trapping")
ret = "trap";
else if(collection_skill == "Foresting")
ret = "chop";
else if(collection_skill == "Fishing")
ret = "fish";
return ret;
}
string GroundSpawn::GetHarvestSpellName() {
string ret = "";
if (collection_skill == "Collecting")
ret = "Gathering";
else
ret = collection_skill;
return ret;
}
void GroundSpawn::HandleUse(Client* client, string type){
if(!client || (client->GetVersion() > 561 && type.length() == 0)) // older clients do not send the type
return;
//The following check disables the use of the groundspawn if spawn access is not granted
if (client) {
bool meets_quest_reqs = MeetsSpawnAccessRequirements(client->GetPlayer());
if (!meets_quest_reqs && (GetQuestsRequiredOverride() & 2) == 0)
return;
else if (meets_quest_reqs && appearance.show_command_icon != 1)
return;
}
MHarvestUse.lock();
std::string typeLwr = ToLower(type);
if(client->GetVersion() <= 561 && (typeLwr == "" || typeLwr == "collect" || typeLwr == "gather" || typeLwr == "chop" || typeLwr == "mine"))
type = GetHarvestSpellType();
if (type == GetHarvestSpellType() && MeetsSpawnAccessRequirements(client->GetPlayer())) {
Spell* spell = master_spell_list.GetSpellByName(GetHarvestSpellName().c_str());
if (spell)
client->GetCurrentZone()->ProcessSpell(spell, client->GetPlayer(), client->GetPlayer()->GetTarget(), true, true);
}
else if (appearance.show_command_icon == 1 && MeetsSpawnAccessRequirements(client->GetPlayer())) {
EntityCommand* entity_command = FindEntityCommand(type);
if (entity_command)
client->GetCurrentZone()->ProcessEntityCommand(entity_command, client->GetPlayer(), client->GetPlayer()->GetTarget());
}
MHarvestUse.unlock();
}

86
internal/GroundSpawn.h Normal file
View File

@ -0,0 +1,86 @@
/*
EQ2Emulator: Everquest II Server Emulator
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
This file is part of EQ2Emulator.
EQ2Emulator is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
EQ2Emulator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __EQ2_GroundSpawn__
#define __EQ2_GroundSpawn__
#include "Spawn.h"
#include "client.h"
#include "../common/Mutex.h"
class GroundSpawn : public Spawn {
public:
GroundSpawn();
virtual ~GroundSpawn();
GroundSpawn* Copy(){
GroundSpawn* new_spawn = new GroundSpawn();
new_spawn->size = size;
new_spawn->SetPrimaryCommands(&primary_command_list);
new_spawn->SetSecondaryCommands(&secondary_command_list);
new_spawn->database_id = database_id;
new_spawn->primary_command_list_id = primary_command_list_id;
new_spawn->secondary_command_list_id = secondary_command_list_id;
memcpy(&new_spawn->appearance, &appearance, sizeof(AppearanceData));
new_spawn->faction_id = faction_id;
new_spawn->target = 0;
new_spawn->SetTotalHP(GetTotalHP());
new_spawn->SetTotalPower(GetTotalPower());
new_spawn->SetHP(GetHP());
new_spawn->SetPower(GetPower());
new_spawn->SetNumberHarvests(number_harvests);
new_spawn->SetAttemptsPerHarvest(num_attempts_per_harvest);
new_spawn->SetGroundSpawnEntryID(groundspawn_id);
new_spawn->SetCollectionSkill(collection_skill.c_str());
SetQuestsRequired(new_spawn);
new_spawn->forceMapCheck = forceMapCheck;
new_spawn->SetOmittedByDBFlag(IsOmittedByDBFlag());
new_spawn->SetLootTier(GetLootTier());
new_spawn->SetLootDropType(GetLootDropType());
new_spawn->SetRandomizeHeading(GetRandomizeHeading());
return new_spawn;
}
bool IsGroundSpawn(){ return true; }
EQ2Packet* serialize(Player* player, int16 version);
int8 GetNumberHarvests();
void SetNumberHarvests(int8 val);
int8 GetAttemptsPerHarvest();
void SetAttemptsPerHarvest(int8 val);
int32 GetGroundSpawnEntryID();
void SetGroundSpawnEntryID(int32 val);
void ProcessHarvest(Client* client);
void SetCollectionSkill(const char* val);
const char* GetCollectionSkill();
string GetHarvestMessageName(bool present_tense = false, bool failure = false);
string GetHarvestSpellType();
string GetHarvestSpellName();
void HandleUse(Client* client, string type);
void SetRandomizeHeading(bool val) { randomize_heading = val; }
bool GetRandomizeHeading() { return randomize_heading; }
private:
int8 number_harvests;
int8 num_attempts_per_harvest;
int32 groundspawn_id;
string collection_skill;
Mutex MHarvest;
Mutex MHarvestUse;
bool randomize_heading;
};
#endif

View File

@ -0,0 +1,288 @@
package appearances
import (
"fmt"
"sync"
)
// Appearances manages a collection of appearance objects with thread-safe operations
type Appearances struct {
appearanceMap map[int32]*Appearance // Map of appearance ID to appearance
mutex sync.RWMutex // Thread safety for concurrent access
}
// NewAppearances creates a new appearances manager
func NewAppearances() *Appearances {
return &Appearances{
appearanceMap: make(map[int32]*Appearance),
}
}
// Reset clears all appearances from the manager
func (a *Appearances) Reset() {
a.ClearAppearances()
}
// ClearAppearances removes all appearances from the manager
func (a *Appearances) ClearAppearances() {
a.mutex.Lock()
defer a.mutex.Unlock()
// Clear the map - Go's garbage collector will handle cleanup
a.appearanceMap = make(map[int32]*Appearance)
}
// InsertAppearance adds an appearance to the manager
func (a *Appearances) InsertAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
a.mutex.Lock()
defer a.mutex.Unlock()
a.appearanceMap[appearance.GetID()] = appearance
return nil
}
// FindAppearanceByID retrieves an appearance by its ID
func (a *Appearances) FindAppearanceByID(id int32) *Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
if appearance, exists := a.appearanceMap[id]; exists {
return appearance
}
return nil
}
// HasAppearance checks if an appearance exists by ID
func (a *Appearances) HasAppearance(id int32) bool {
a.mutex.RLock()
defer a.mutex.RUnlock()
_, exists := a.appearanceMap[id]
return exists
}
// GetAppearanceCount returns the total number of appearances
func (a *Appearances) GetAppearanceCount() int {
a.mutex.RLock()
defer a.mutex.RUnlock()
return len(a.appearanceMap)
}
// GetAllAppearances returns a copy of all appearances
func (a *Appearances) GetAllAppearances() map[int32]*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Appearance)
for id, appearance := range a.appearanceMap {
result[id] = appearance
}
return result
}
// GetAppearanceIDs returns all appearance IDs
func (a *Appearances) GetAppearanceIDs() []int32 {
a.mutex.RLock()
defer a.mutex.RUnlock()
ids := make([]int32, 0, len(a.appearanceMap))
for id := range a.appearanceMap {
ids = append(ids, id)
}
return ids
}
// FindAppearancesByName finds all appearances with names containing the given substring
func (a *Appearances) FindAppearancesByName(nameSubstring string) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for _, appearance := range a.appearanceMap {
if contains(appearance.GetName(), nameSubstring) {
results = append(results, appearance)
}
}
return results
}
// FindAppearancesByMinClient finds all appearances with a specific minimum client version
func (a *Appearances) FindAppearancesByMinClient(minClient int16) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for _, appearance := range a.appearanceMap {
if appearance.GetMinClientVersion() == minClient {
results = append(results, appearance)
}
}
return results
}
// GetCompatibleAppearances returns all appearances compatible with the given client version
func (a *Appearances) GetCompatibleAppearances(clientVersion int16) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for _, appearance := range a.appearanceMap {
if appearance.IsCompatibleWithClient(clientVersion) {
results = append(results, appearance)
}
}
return results
}
// RemoveAppearance removes an appearance by ID
func (a *Appearances) RemoveAppearance(id int32) bool {
a.mutex.Lock()
defer a.mutex.Unlock()
if _, exists := a.appearanceMap[id]; exists {
delete(a.appearanceMap, id)
return true
}
return false
}
// UpdateAppearance updates an existing appearance or inserts it if it doesn't exist
func (a *Appearances) UpdateAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
a.mutex.Lock()
defer a.mutex.Unlock()
a.appearanceMap[appearance.GetID()] = appearance
return nil
}
// GetAppearancesByIDRange returns all appearances within the given ID range (inclusive)
func (a *Appearances) GetAppearancesByIDRange(minID, maxID int32) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for id, appearance := range a.appearanceMap {
if id >= minID && id <= maxID {
results = append(results, appearance)
}
}
return results
}
// ValidateAppearances checks all appearances for consistency
func (a *Appearances) ValidateAppearances() []string {
a.mutex.RLock()
defer a.mutex.RUnlock()
var issues []string
for id, appearance := range a.appearanceMap {
if appearance == nil {
issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id))
continue
}
if appearance.GetID() != id {
issues = append(issues, fmt.Sprintf("Appearance ID mismatch: map key %d != appearance ID %d", id, appearance.GetID()))
}
if len(appearance.GetName()) == 0 {
issues = append(issues, fmt.Sprintf("Appearance ID %d has empty name", id))
}
if appearance.GetMinClientVersion() < 0 {
issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion()))
}
}
return issues
}
// IsValid returns true if all appearances are valid
func (a *Appearances) IsValid() bool {
issues := a.ValidateAppearances()
return len(issues) == 0
}
// GetStatistics returns statistics about the appearance collection
func (a *Appearances) GetStatistics() map[string]interface{} {
a.mutex.RLock()
defer a.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_appearances"] = len(a.appearanceMap)
// Count by minimum client version
versionCounts := make(map[int16]int)
for _, appearance := range a.appearanceMap {
versionCounts[appearance.GetMinClientVersion()]++
}
stats["appearances_by_min_client"] = versionCounts
// Find ID range
if len(a.appearanceMap) > 0 {
var minID, maxID int32
first := true
for id := range a.appearanceMap {
if first {
minID = id
maxID = id
first = false
} else {
if id < minID {
minID = id
}
if id > maxID {
maxID = id
}
}
}
stats["min_id"] = minID
stats["max_id"] = maxID
stats["id_range"] = maxID - minID
}
return stats
}
// contains checks if a string contains a substring (case-sensitive)
func contains(str, substr string) bool {
if len(substr) == 0 {
return true
}
if len(str) < len(substr) {
return false
}
for i := 0; i <= len(str)-len(substr); i++ {
if str[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@ -0,0 +1,13 @@
package appearances
// Hash search constants
const (
// Maximum number of iterations to find an entry in hash table
HashSearchMax = 20
)
// Client version constants for appearance compatibility
const (
MinimumClientVersion = 0
DefaultClientVersion = 283
)

View File

@ -0,0 +1,308 @@
package appearances
import (
"fmt"
"sync"
)
// Database interface for appearance persistence
type Database interface {
LoadAllAppearances() ([]*Appearance, error)
SaveAppearance(appearance *Appearance) error
DeleteAppearance(id int32) error
LoadAppearancesByClientVersion(minClientVersion int16) ([]*Appearance, error)
}
// Logger interface for appearance logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// AppearanceProvider interface for entities that provide appearances
type AppearanceProvider interface {
GetAppearanceID() int32
SetAppearanceID(id int32)
GetAppearance() *Appearance
IsCompatibleWithClient(clientVersion int16) bool
}
// AppearanceAware interface for entities that use appearances
type AppearanceAware interface {
GetAppearanceManager() *Manager
FindAppearanceByID(id int32) *Appearance
GetCompatibleAppearances(clientVersion int16) []*Appearance
}
// Client interface for appearance-related client operations
type Client interface {
GetVersion() int16
SendAppearanceUpdate(appearanceID int32) error
}
// AppearanceCache interface for caching appearance data
type AppearanceCache interface {
Get(id int32) *Appearance
Set(id int32, appearance *Appearance)
Remove(id int32)
Clear()
GetSize() int
}
// EntityAppearanceAdapter provides appearance functionality for entities
type EntityAppearanceAdapter struct {
entity Entity
appearanceID int32
manager *Manager
logger Logger
}
// Entity interface for things that can have appearances
type Entity interface {
GetID() int32
GetName() string
GetDatabaseID() int32
}
// NewEntityAppearanceAdapter creates a new entity appearance adapter
func NewEntityAppearanceAdapter(entity Entity, manager *Manager, logger Logger) *EntityAppearanceAdapter {
return &EntityAppearanceAdapter{
entity: entity,
appearanceID: 0,
manager: manager,
logger: logger,
}
}
// GetAppearanceID returns the entity's appearance ID
func (eaa *EntityAppearanceAdapter) GetAppearanceID() int32 {
return eaa.appearanceID
}
// SetAppearanceID sets the entity's appearance ID
func (eaa *EntityAppearanceAdapter) SetAppearanceID(id int32) {
eaa.appearanceID = id
if eaa.logger != nil {
eaa.logger.LogDebug("Entity %d (%s): Set appearance ID to %d",
eaa.entity.GetID(), eaa.entity.GetName(), id)
}
}
// GetAppearance returns the entity's appearance object
func (eaa *EntityAppearanceAdapter) GetAppearance() *Appearance {
if eaa.appearanceID == 0 {
return nil
}
if eaa.manager == nil {
if eaa.logger != nil {
eaa.logger.LogError("Entity %d (%s): No appearance manager available",
eaa.entity.GetID(), eaa.entity.GetName())
}
return nil
}
return eaa.manager.FindAppearanceByID(eaa.appearanceID)
}
// IsCompatibleWithClient checks if the entity's appearance is compatible with client version
func (eaa *EntityAppearanceAdapter) IsCompatibleWithClient(clientVersion int16) bool {
appearance := eaa.GetAppearance()
if appearance == nil {
return true // No appearance means compatible with all clients
}
return appearance.IsCompatibleWithClient(clientVersion)
}
// GetAppearanceName returns the name of the entity's appearance
func (eaa *EntityAppearanceAdapter) GetAppearanceName() string {
appearance := eaa.GetAppearance()
if appearance == nil {
return ""
}
return appearance.GetName()
}
// ValidateAppearance validates that the entity's appearance exists and is valid
func (eaa *EntityAppearanceAdapter) ValidateAppearance() error {
if eaa.appearanceID == 0 {
return nil // No appearance is valid
}
appearance := eaa.GetAppearance()
if appearance == nil {
return fmt.Errorf("appearance ID %d not found", eaa.appearanceID)
}
return nil
}
// UpdateAppearance updates the entity's appearance from the manager
func (eaa *EntityAppearanceAdapter) UpdateAppearance(id int32) error {
if eaa.manager == nil {
return fmt.Errorf("no appearance manager available")
}
appearance := eaa.manager.FindAppearanceByID(id)
if appearance == nil {
return fmt.Errorf("appearance ID %d not found", id)
}
eaa.SetAppearanceID(id)
if eaa.logger != nil {
eaa.logger.LogInfo("Entity %d (%s): Updated appearance to %d (%s)",
eaa.entity.GetID(), eaa.entity.GetName(), id, appearance.GetName())
}
return nil
}
// SendAppearanceToClient sends the appearance to a client
func (eaa *EntityAppearanceAdapter) SendAppearanceToClient(client Client) error {
if client == nil {
return fmt.Errorf("client is nil")
}
if eaa.appearanceID == 0 {
return nil // No appearance to send
}
// Check client compatibility
if !eaa.IsCompatibleWithClient(client.GetVersion()) {
if eaa.logger != nil {
eaa.logger.LogWarning("Entity %d (%s): Appearance %d not compatible with client version %d",
eaa.entity.GetID(), eaa.entity.GetName(), eaa.appearanceID, client.GetVersion())
}
return fmt.Errorf("appearance not compatible with client version %d", client.GetVersion())
}
return client.SendAppearanceUpdate(eaa.appearanceID)
}
// SimpleAppearanceCache is a basic in-memory appearance cache
type SimpleAppearanceCache struct {
cache map[int32]*Appearance
mutex sync.RWMutex
}
// NewSimpleAppearanceCache creates a new simple appearance cache
func NewSimpleAppearanceCache() *SimpleAppearanceCache {
return &SimpleAppearanceCache{
cache: make(map[int32]*Appearance),
}
}
// Get retrieves an appearance from cache
func (sac *SimpleAppearanceCache) Get(id int32) *Appearance {
sac.mutex.RLock()
defer sac.mutex.RUnlock()
return sac.cache[id]
}
// Set stores an appearance in cache
func (sac *SimpleAppearanceCache) Set(id int32, appearance *Appearance) {
sac.mutex.Lock()
defer sac.mutex.Unlock()
sac.cache[id] = appearance
}
// Remove removes an appearance from cache
func (sac *SimpleAppearanceCache) Remove(id int32) {
sac.mutex.Lock()
defer sac.mutex.Unlock()
delete(sac.cache, id)
}
// Clear removes all appearances from cache
func (sac *SimpleAppearanceCache) Clear() {
sac.mutex.Lock()
defer sac.mutex.Unlock()
sac.cache = make(map[int32]*Appearance)
}
// GetSize returns the number of cached appearances
func (sac *SimpleAppearanceCache) GetSize() int {
sac.mutex.RLock()
defer sac.mutex.RUnlock()
return len(sac.cache)
}
// CachedAppearanceManager wraps a Manager with caching functionality
type CachedAppearanceManager struct {
*Manager
cache AppearanceCache
}
// NewCachedAppearanceManager creates a new cached appearance manager
func NewCachedAppearanceManager(manager *Manager, cache AppearanceCache) *CachedAppearanceManager {
return &CachedAppearanceManager{
Manager: manager,
cache: cache,
}
}
// FindAppearanceByID finds an appearance with caching
func (cam *CachedAppearanceManager) FindAppearanceByID(id int32) *Appearance {
// Check cache first
if appearance := cam.cache.Get(id); appearance != nil {
return appearance
}
// Load from manager
appearance := cam.Manager.FindAppearanceByID(id)
if appearance != nil {
// Cache the result
cam.cache.Set(id, appearance)
}
return appearance
}
// AddAppearance adds an appearance and updates cache
func (cam *CachedAppearanceManager) AddAppearance(appearance *Appearance) error {
err := cam.Manager.AddAppearance(appearance)
if err == nil {
// Update cache
cam.cache.Set(appearance.GetID(), appearance)
}
return err
}
// UpdateAppearance updates an appearance and cache
func (cam *CachedAppearanceManager) UpdateAppearance(appearance *Appearance) error {
err := cam.Manager.UpdateAppearance(appearance)
if err == nil {
// Update cache
cam.cache.Set(appearance.GetID(), appearance)
}
return err
}
// RemoveAppearance removes an appearance and updates cache
func (cam *CachedAppearanceManager) RemoveAppearance(id int32) error {
err := cam.Manager.RemoveAppearance(id)
if err == nil {
// Remove from cache
cam.cache.Remove(id)
}
return err
}
// ClearCache clears the appearance cache
func (cam *CachedAppearanceManager) ClearCache() {
cam.cache.Clear()
}

View File

@ -0,0 +1,392 @@
package appearances
import (
"fmt"
"sync"
)
// Manager provides high-level management of the appearance system
type Manager struct {
appearances *Appearances
database Database
logger Logger
mutex sync.RWMutex
// Statistics
totalLookups int64
successfulLookups int64
failedLookups int64
cacheHits int64
cacheMisses int64
}
// NewManager creates a new appearance manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
appearances: NewAppearances(),
database: database,
logger: logger,
}
}
// Initialize loads appearances from database
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing appearance manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty appearance list")
}
return nil
}
appearances, err := m.database.LoadAllAppearances()
if err != nil {
return fmt.Errorf("failed to load appearances from database: %w", err)
}
for _, appearance := range appearances {
if err := m.appearances.InsertAppearance(appearance); err != nil {
if m.logger != nil {
m.logger.LogError("Failed to insert appearance %d: %v", appearance.GetID(), err)
}
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d appearances from database", len(appearances))
}
return nil
}
// GetAppearances returns the appearances collection
func (m *Manager) GetAppearances() *Appearances {
return m.appearances
}
// FindAppearanceByID finds an appearance by ID with statistics tracking
func (m *Manager) FindAppearanceByID(id int32) *Appearance {
m.mutex.Lock()
m.totalLookups++
m.mutex.Unlock()
appearance := m.appearances.FindAppearanceByID(id)
m.mutex.Lock()
if appearance != nil {
m.successfulLookups++
m.cacheHits++
} else {
m.failedLookups++
m.cacheMisses++
}
m.mutex.Unlock()
if m.logger != nil && appearance == nil {
m.logger.LogDebug("Appearance lookup failed for ID: %d", id)
}
return appearance
}
// AddAppearance adds a new appearance
func (m *Manager) AddAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
// Validate the appearance
if len(appearance.GetName()) == 0 {
return fmt.Errorf("appearance name cannot be empty")
}
if appearance.GetID() <= 0 {
return fmt.Errorf("appearance ID must be positive")
}
// Check if appearance already exists
if m.appearances.HasAppearance(appearance.GetID()) {
return fmt.Errorf("appearance with ID %d already exists", appearance.GetID())
}
// Add to collection
if err := m.appearances.InsertAppearance(appearance); err != nil {
return fmt.Errorf("failed to insert appearance: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveAppearance(appearance); err != nil {
// Remove from collection if database save failed
m.appearances.RemoveAppearance(appearance.GetID())
return fmt.Errorf("failed to save appearance to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Added appearance %d: %s (min client: %d)",
appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion())
}
return nil
}
// UpdateAppearance updates an existing appearance
func (m *Manager) UpdateAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
// Check if appearance exists
if !m.appearances.HasAppearance(appearance.GetID()) {
return fmt.Errorf("appearance with ID %d does not exist", appearance.GetID())
}
// Update in collection
if err := m.appearances.UpdateAppearance(appearance); err != nil {
return fmt.Errorf("failed to update appearance: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveAppearance(appearance); err != nil {
return fmt.Errorf("failed to save appearance to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Updated appearance %d: %s", appearance.GetID(), appearance.GetName())
}
return nil
}
// RemoveAppearance removes an appearance
func (m *Manager) RemoveAppearance(id int32) error {
// Check if appearance exists
if !m.appearances.HasAppearance(id) {
return fmt.Errorf("appearance with ID %d does not exist", id)
}
// Remove from database first if available
if m.database != nil {
if err := m.database.DeleteAppearance(id); err != nil {
return fmt.Errorf("failed to delete appearance from database: %w", err)
}
}
// Remove from collection
if !m.appearances.RemoveAppearance(id) {
return fmt.Errorf("failed to remove appearance from collection")
}
if m.logger != nil {
m.logger.LogInfo("Removed appearance %d", id)
}
return nil
}
// GetCompatibleAppearances returns appearances compatible with client version
func (m *Manager) GetCompatibleAppearances(clientVersion int16) []*Appearance {
return m.appearances.GetCompatibleAppearances(clientVersion)
}
// SearchAppearancesByName searches for appearances by name substring
func (m *Manager) SearchAppearancesByName(nameSubstring string) []*Appearance {
return m.appearances.FindAppearancesByName(nameSubstring)
}
// GetStatistics returns appearance system statistics
func (m *Manager) GetStatistics() map[string]interface{} {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Get basic appearance statistics
stats := m.appearances.GetStatistics()
// Add manager statistics
stats["total_lookups"] = m.totalLookups
stats["successful_lookups"] = m.successfulLookups
stats["failed_lookups"] = m.failedLookups
stats["cache_hits"] = m.cacheHits
stats["cache_misses"] = m.cacheMisses
if m.totalLookups > 0 {
stats["success_rate"] = float64(m.successfulLookups) / float64(m.totalLookups) * 100
stats["cache_hit_rate"] = float64(m.cacheHits) / float64(m.totalLookups) * 100
}
return stats
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalLookups = 0
m.successfulLookups = 0
m.failedLookups = 0
m.cacheHits = 0
m.cacheMisses = 0
}
// ValidateAllAppearances validates all appearances in the system
func (m *Manager) ValidateAllAppearances() []string {
return m.appearances.ValidateAppearances()
}
// ReloadFromDatabase reloads all appearances from database
func (m *Manager) ReloadFromDatabase() error {
if m.database == nil {
return fmt.Errorf("no database available")
}
// Clear current appearances
m.appearances.ClearAppearances()
// Reload from database
return m.Initialize()
}
// GetAppearanceCount returns the total number of appearances
func (m *Manager) GetAppearanceCount() int {
return m.appearances.GetAppearanceCount()
}
// ProcessCommand handles appearance-related commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "search":
return m.handleSearchCommand(args)
case "info":
return m.handleInfoCommand(args)
case "reload":
return m.handleReloadCommand(args)
default:
return "", fmt.Errorf("unknown appearance command: %s", command)
}
}
// handleStatsCommand shows appearance system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Appearance System Statistics:\n"
result += fmt.Sprintf("Total Appearances: %d\n", stats["total_appearances"])
result += fmt.Sprintf("Total Lookups: %d\n", stats["total_lookups"])
result += fmt.Sprintf("Successful Lookups: %d\n", stats["successful_lookups"])
result += fmt.Sprintf("Failed Lookups: %d\n", stats["failed_lookups"])
if successRate, exists := stats["success_rate"]; exists {
result += fmt.Sprintf("Success Rate: %.1f%%\n", successRate)
}
if cacheHitRate, exists := stats["cache_hit_rate"]; exists {
result += fmt.Sprintf("Cache Hit Rate: %.1f%%\n", cacheHitRate)
}
if minID, exists := stats["min_id"]; exists {
result += fmt.Sprintf("ID Range: %d - %d\n", minID, stats["max_id"])
}
return result, nil
}
// handleValidateCommand validates all appearances
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateAllAppearances()
if len(issues) == 0 {
return "All appearances are valid.", nil
}
result := fmt.Sprintf("Found %d issues with appearances:\n", len(issues))
for i, issue := range issues {
if i >= 10 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
// handleSearchCommand searches for appearances by name
func (m *Manager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search term required")
}
searchTerm := args[0]
results := m.SearchAppearancesByName(searchTerm)
if len(results) == 0 {
return fmt.Sprintf("No appearances found matching '%s'.", searchTerm), nil
}
result := fmt.Sprintf("Found %d appearances matching '%s':\n", len(results), searchTerm)
for i, appearance := range results {
if i >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s (min client: %d)\n",
appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion())
}
return result, nil
}
// handleInfoCommand shows information about a specific appearance
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("appearance ID required")
}
var appearanceID int32
if _, err := fmt.Sscanf(args[0], "%d", &appearanceID); err != nil {
return "", fmt.Errorf("invalid appearance ID: %s", args[0])
}
appearance := m.FindAppearanceByID(appearanceID)
if appearance == nil {
return fmt.Sprintf("Appearance %d not found.", appearanceID), nil
}
result := fmt.Sprintf("Appearance Information:\n")
result += fmt.Sprintf("ID: %d\n", appearance.GetID())
result += fmt.Sprintf("Name: %s\n", appearance.GetName())
result += fmt.Sprintf("Min Client Version: %d\n", appearance.GetMinClientVersion())
return result, nil
}
// handleReloadCommand reloads appearances from database
func (m *Manager) handleReloadCommand(args []string) (string, error) {
if err := m.ReloadFromDatabase(); err != nil {
return "", fmt.Errorf("failed to reload appearances: %w", err)
}
count := m.GetAppearanceCount()
return fmt.Sprintf("Successfully reloaded %d appearances from database.", count), nil
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down appearance manager...")
}
// Clear appearances
m.appearances.ClearAppearances()
}

View File

@ -0,0 +1,65 @@
package appearances
// Appearance represents a single appearance with ID, name, and client version requirements
type Appearance struct {
id int32 // Appearance ID
name string // Appearance name
minClient int16 // Minimum client version required
}
// NewAppearance creates a new appearance with the given parameters
func NewAppearance(id int32, name string, minClientVersion int16) *Appearance {
if len(name) == 0 {
return nil
}
return &Appearance{
id: id,
name: name,
minClient: minClientVersion,
}
}
// GetID returns the appearance ID
func (a *Appearance) GetID() int32 {
return a.id
}
// GetName returns the appearance name
func (a *Appearance) GetName() string {
return a.name
}
// GetMinClientVersion returns the minimum client version required
func (a *Appearance) GetMinClientVersion() int16 {
return a.minClient
}
// GetNameString returns the name as a string (alias for GetName for C++ compatibility)
func (a *Appearance) GetNameString() string {
return a.name
}
// SetName sets the appearance name
func (a *Appearance) SetName(name string) {
a.name = name
}
// SetMinClientVersion sets the minimum client version
func (a *Appearance) SetMinClientVersion(version int16) {
a.minClient = version
}
// IsCompatibleWithClient returns true if the appearance is compatible with the given client version
func (a *Appearance) IsCompatibleWithClient(clientVersion int16) bool {
return clientVersion >= a.minClient
}
// Clone creates a copy of the appearance
func (a *Appearance) Clone() *Appearance {
return &Appearance{
id: a.id,
name: a.name,
minClient: a.minClient,
}
}

View File

@ -1,199 +0,0 @@
/*
EQ2Emulator: Everquest II Server Emulator
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
This file is part of EQ2Emulator.
EQ2Emulator is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
EQ2Emulator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../common/debug.h"
#include "../common/Log.h"
#include "classes.h"
#include "../common/MiscFunctions.h"
#include <algorithm>
Classes::Classes(){
class_map["COMMONER"] = 0;
class_map["FIGHTER"] = 1;
class_map["WARRIOR"] = 2;
class_map["GUARDIAN"] = 3;
class_map["BERSERKER"] = 4;
class_map["BRAWLER"] = 5;
class_map["MONK"] = 6;
class_map["BRUISER"] = 7;
class_map["CRUSADER"] = 8;
class_map["SHADOWKNIGHT"] = 9;
class_map["PALADIN"] = 10;
class_map["PRIEST"] = 11;
class_map["CLERIC"] = 12;
class_map["TEMPLAR"] = 13;
class_map["INQUISITOR"] = 14;
class_map["DRUID"] = 15;
class_map["WARDEN"] = 16;
class_map["FURY"] = 17;
class_map["SHAMAN"] = 18;
class_map["MYSTIC"] = 19;
class_map["DEFILER"] = 20;
class_map["MAGE"] = 21;
class_map["SORCERER"] = 22;
class_map["WIZARD"] = 23;
class_map["WARLOCK"] = 24;
class_map["ENCHANTER"] = 25;
class_map["ILLUSIONIST"] = 26;
class_map["COERCER"] = 27;
class_map["SUMMONER"] = 28;
class_map["CONJUROR"] = 29;
class_map["NECROMANCER"] = 30;
class_map["SCOUT"] = 31;
class_map["ROGUE"] = 32;
class_map["SWASHBUCKLER"] = 33;
class_map["BRIGAND"] = 34;
class_map["BARD"] = 35;
class_map["TROUBADOR"] = 36;
class_map["DIRGE"] = 37;
class_map["PREDATOR"] = 38;
class_map["RANGER"] = 39;
class_map["ASSASSIN"] = 40;
class_map["ANIMALIST"] = 41;
class_map["BEASTLORD"] = 42;
class_map["SHAPER"] = 43;
class_map["CHANNELER"] = 44;
class_map["ARTISAN"] = 45;
class_map["CRAFTSMAN"] = 46;
class_map["PROVISIONER"] = 47;
class_map["WOODWORKER"] = 48;
class_map["CARPENTER"] = 49;
class_map["OUTFITTER"] = 50;
class_map["ARMORER"] = 51;
class_map["WEAPONSMITH"] = 52;
class_map["TAILOR"] = 53;
class_map["SCHOLAR"] = 54;
class_map["JEWELER"] = 55;
class_map["SAGE"] = 56;
class_map["ALCHEMIST"] = 57;
}
int8 Classes::GetBaseClass(int8 class_id) {
int8 ret = 0;
if(class_id>=WARRIOR && class_id <= PALADIN)
ret = FIGHTER;
if((class_id>=CLERIC && class_id <= DEFILER) || (class_id == SHAPER || class_id == CHANNELER))
ret = PRIEST;
if(class_id>=SORCERER && class_id <= NECROMANCER)
ret = MAGE;
if(class_id>=ROGUE && class_id <= BEASTLORD)
ret = SCOUT;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning base class ID: %i", __FUNCTION__, ret);
return ret;
}
int8 Classes::GetSecondaryBaseClass(int8 class_id){
int8 ret = 0;
if(class_id==GUARDIAN || class_id == BERSERKER)
ret = WARRIOR;
if(class_id==MONK || class_id == BRUISER)
ret = BRAWLER;
if(class_id==SHADOWKNIGHT || class_id == PALADIN)
ret = CRUSADER;
if(class_id==TEMPLAR || class_id == INQUISITOR)
ret = CLERIC;
if(class_id==WARDEN || class_id == FURY)
ret = DRUID;
if(class_id==MYSTIC || class_id == DEFILER)
ret = SHAMAN;
if(class_id==WIZARD || class_id == WARLOCK)
ret = SORCERER;
if(class_id==ILLUSIONIST || class_id == COERCER)
ret = ENCHANTER;
if(class_id==CONJUROR || class_id == NECROMANCER)
ret = SUMMONER;
if(class_id==SWASHBUCKLER || class_id == BRIGAND)
ret = ROGUE;
if(class_id==TROUBADOR || class_id == DIRGE)
ret = BARD;
if(class_id==RANGER || class_id == ASSASSIN)
ret = PREDATOR;
if(class_id==BEASTLORD)
ret = ANIMALIST;
if(class_id == CHANNELER)
ret = SHAPER;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning secondary class ID: %i", __FUNCTION__, ret);
return ret;
}
int8 Classes::GetTSBaseClass(int8 class_id) {
int8 ret = 0;
if (class_id + 42 >= ARTISAN)
ret = ARTISAN - 44;
else
ret = class_id;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning base tradeskill class ID: %i", __FUNCTION__, ret);
return ret;
}
int8 Classes::GetSecondaryTSBaseClass(int8 class_id) {
int8 ret = class_id + 42;
if (ret == ARTISAN)
ret = ARTISAN - 44;
else if (ret >= CRAFTSMAN && ret < OUTFITTER)
ret = CRAFTSMAN - 44;
else if (ret >= OUTFITTER && ret < SCHOLAR)
ret = OUTFITTER - 44;
else if (ret >= SCHOLAR)
ret = SCHOLAR - 44;
else
ret = class_id;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning secondary tradeskill class ID: %i", __FUNCTION__, ret);
return ret;
}
sint8 Classes::GetClassID(const char* name){
string class_name = string(name);
class_name = ToUpper(class_name);
if(class_map.count(class_name) == 1) {
LogWrite(WORLD__DEBUG, 5, "World", "%s returning class ID: %i for class name %s", __FUNCTION__, class_map[class_name], class_name.c_str());
return class_map[class_name];
}
LogWrite(WORLD__WARNING, 0, "World", "Could not find class_id in function: %s (return -1)", __FUNCTION__);
return -1;
}
const char* Classes::GetClassName(int8 class_id){
map<string, int8>::iterator itr;
for(itr = class_map.begin(); itr != class_map.end(); itr++){
if(itr->second == class_id) {
LogWrite(WORLD__DEBUG, 5, "World", "%s returning class name: %s for class_id %i", __FUNCTION__, itr->first.c_str(), class_id);
return itr->first.c_str();
}
}
LogWrite(WORLD__WARNING, 0, "World", "Could not find class name in function: %s (return 0)", __FUNCTION__);
return 0;
}
string Classes::GetClassNameCase(int8 class_id) {
map<string, int8>::iterator itr;
for (itr = class_map.begin(); itr != class_map.end(); itr++){
if (itr->second == class_id) {
string class_name = string(itr->first);
transform(itr->first.begin() + 1, itr->first.end(), class_name.begin() + 1, ::tolower);
class_name[0] = ::toupper(class_name[0]);
LogWrite(WORLD__DEBUG, 5, "World", "%s returning class name: %s for class_id %i", __FUNCTION__, class_name.c_str(), class_id);
return class_name;
}
}
LogWrite(WORLD__WARNING, 0, "World", "Could not find class name in function: %s (return blank)", __FUNCTION__);
return "";
}

View File

@ -1,119 +0,0 @@
/*
EQ2Emulator: Everquest II Server Emulator
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
This file is part of EQ2Emulator.
EQ2Emulator is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
EQ2Emulator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CLASSES_CH
#define CLASSES_CH
#include "../common/types.h"
#include <map>
using namespace std;
#define COMMONER 0
#define FIGHTER 1
#define WARRIOR 2
#define GUARDIAN 3
#define BERSERKER 4
#define BRAWLER 5
#define MONK 6
#define BRUISER 7
#define CRUSADER 8
#define SHADOWKNIGHT 9
#define PALADIN 10
#define PRIEST 11
#define CLERIC 12
#define TEMPLAR 13
#define INQUISITOR 14
#define DRUID 15
#define WARDEN 16
#define FURY 17
#define SHAMAN 18
#define MYSTIC 19
#define DEFILER 20
#define MAGE 21
#define SORCERER 22
#define WIZARD 23
#define WARLOCK 24
#define ENCHANTER 25
#define ILLUSIONIST 26
#define COERCER 27
#define SUMMONER 28
#define CONJUROR 29
#define NECROMANCER 30
#define SCOUT 31
#define ROGUE 32
#define SWASHBUCKLER 33
#define BRIGAND 34
#define BARD 35
#define TROUBADOR 36
#define DIRGE 37
#define PREDATOR 38
#define RANGER 39
#define ASSASSIN 40
#define ANIMALIST 41
#define BEASTLORD 42
#define SHAPER 43
#define CHANNELER 44
//Tradeskills
// 0 - transmuting/tinkering
#define ARTISAN 45 // 1
#define CRAFTSMAN 46 // 2
#define PROVISIONER 47 // 3
#define WOODWORKER 48 // 4
#define CARPENTER 49 // 5
#define OUTFITTER 50 // 6
#define ARMORER 51 // 7
#define WEAPONSMITH 52 // 8
#define TAILOR 53 // 9
#define SCHOLAR 54 // 10
#define JEWELER 55 // 11
#define SAGE 56 // 12
#define ALCHEMIST 57 // 13
//43 - artisan
//44 - craftsman
//45 - provisioner
//46 - Woodworker
//47 - carpenter
//48 - armorer
//49 - weaponsmith
//50 - tailor
//51 -
//52 - jeweler
//53 - sage
//54 - alch
#define CLASSIC_MAX_ADVENTURE_CLASS 40 // there is a 41, but its 'scantestbase'
#define CLASSIC_MAX_TRADESKILL_CLASS 13
#define MAX_CLASSES 58
class Classes {
public:
Classes();
char* GetEQClassName(int8 class_, int8 level);
const char* GetClassName(int8 class_id);
string GetClassNameCase(int8 class_id);
sint8 GetClassID(const char* name);
int8 GetBaseClass(int8 class_id);
int8 GetSecondaryBaseClass(int8 class_id);
int8 GetTSBaseClass(int8 class_id);
int8 GetSecondaryTSBaseClass(int8 class_id);
private:
map<string, int8> class_map;
};
#endif

366
internal/classes/classes.go Normal file
View File

@ -0,0 +1,366 @@
package classes
import (
"strings"
"sync"
)
// Classes manages class information and lookups
// Converted from C++ Classes class
type Classes struct {
// Class name to ID mapping (uppercase keys)
classMap map[string]int8
// ID to display name mapping for friendly names
displayNameMap map[int8]string
// Thread safety
mutex sync.RWMutex
}
// NewClasses creates a new classes manager with all EQ2 classes
// Converted from C++ Classes::Classes constructor
func NewClasses() *Classes {
classes := &Classes{
classMap: make(map[string]int8),
displayNameMap: make(map[int8]string),
}
classes.initializeClasses()
return classes
}
// initializeClasses sets up all class mappings
func (c *Classes) initializeClasses() {
// Initialize class name to ID mappings (from C++ constructor)
c.classMap[ClassNameCommoner] = ClassCommoner
c.classMap[ClassNameFighter] = ClassFighter
c.classMap[ClassNameWarrior] = ClassWarrior
c.classMap[ClassNameGuardian] = ClassGuardian
c.classMap[ClassNameBerserker] = ClassBerserker
c.classMap[ClassNameBrawler] = ClassBrawler
c.classMap[ClassNameMonk] = ClassMonk
c.classMap[ClassNameBruiser] = ClassBruiser
c.classMap[ClassNameCrusader] = ClassCrusader
c.classMap[ClassNameShadowknight] = ClassShadowknight
c.classMap[ClassNamePaladin] = ClassPaladin
c.classMap[ClassNamePriest] = ClassPriest
c.classMap[ClassNameCleric] = ClassCleric
c.classMap[ClassNameTemplar] = ClassTemplar
c.classMap[ClassNameInquisitor] = ClassInquisitor
c.classMap[ClassNameDruid] = ClassDruid
c.classMap[ClassNameWarden] = ClassWarden
c.classMap[ClassNameFury] = ClassFury
c.classMap[ClassNameShaman] = ClassShaman
c.classMap[ClassNameMystic] = ClassMystic
c.classMap[ClassNameDefiler] = ClassDefiler
c.classMap[ClassNameMage] = ClassMage
c.classMap[ClassNameSorcerer] = ClassSorcerer
c.classMap[ClassNameWizard] = ClassWizard
c.classMap[ClassNameWarlock] = ClassWarlock
c.classMap[ClassNameEnchanter] = ClassEnchanter
c.classMap[ClassNameIllusionist] = ClassIllusionist
c.classMap[ClassNameCoercer] = ClassCoercer
c.classMap[ClassNameSummoner] = ClassSummoner
c.classMap[ClassNameConjuror] = ClassConjuror
c.classMap[ClassNameNecromancer] = ClassNecromancer
c.classMap[ClassNameScout] = ClassScout
c.classMap[ClassNameRogue] = ClassRogue
c.classMap[ClassNameSwashbuckler] = ClassSwashbuckler
c.classMap[ClassNameBrigand] = ClassBrigand
c.classMap[ClassNameBard] = ClassBard
c.classMap[ClassNameTroubador] = ClassTroubador
c.classMap[ClassNameDirge] = ClassDirge
c.classMap[ClassNamePredator] = ClassPredator
c.classMap[ClassNameRanger] = ClassRanger
c.classMap[ClassNameAssassin] = ClassAssassin
c.classMap[ClassNameAnimalist] = ClassAnimalist
c.classMap[ClassNameBeastlord] = ClassBeastlord
c.classMap[ClassNameShaper] = ClassShaper
c.classMap[ClassNameChanneler] = ClassChanneler
c.classMap[ClassNameArtisan] = ClassArtisan
c.classMap[ClassNameCraftsman] = ClassCraftsman
c.classMap[ClassNameProvisioner] = ClassProvisioner
c.classMap[ClassNameWoodworker] = ClassWoodworker
c.classMap[ClassNameCarpenter] = ClassCarpenter
c.classMap[ClassNameOutfitter] = ClassOutfitter
c.classMap[ClassNameArmorer] = ClassArmorer
c.classMap[ClassNameWeaponsmith] = ClassWeaponsmith
c.classMap[ClassNameTailor] = ClassTailor
c.classMap[ClassNameScholar] = ClassScholar
c.classMap[ClassNameJeweler] = ClassJeweler
c.classMap[ClassNameSage] = ClassSage
c.classMap[ClassNameAlchemist] = ClassAlchemist
// Initialize display names
c.displayNameMap[ClassCommoner] = DisplayNameCommoner
c.displayNameMap[ClassFighter] = DisplayNameFighter
c.displayNameMap[ClassWarrior] = DisplayNameWarrior
c.displayNameMap[ClassGuardian] = DisplayNameGuardian
c.displayNameMap[ClassBerserker] = DisplayNameBerserker
c.displayNameMap[ClassBrawler] = DisplayNameBrawler
c.displayNameMap[ClassMonk] = DisplayNameMonk
c.displayNameMap[ClassBruiser] = DisplayNameBruiser
c.displayNameMap[ClassCrusader] = DisplayNameCrusader
c.displayNameMap[ClassShadowknight] = DisplayNameShadowknight
c.displayNameMap[ClassPaladin] = DisplayNamePaladin
c.displayNameMap[ClassPriest] = DisplayNamePriest
c.displayNameMap[ClassCleric] = DisplayNameCleric
c.displayNameMap[ClassTemplar] = DisplayNameTemplar
c.displayNameMap[ClassInquisitor] = DisplayNameInquisitor
c.displayNameMap[ClassDruid] = DisplayNameDruid
c.displayNameMap[ClassWarden] = DisplayNameWarden
c.displayNameMap[ClassFury] = DisplayNameFury
c.displayNameMap[ClassShaman] = DisplayNameShaman
c.displayNameMap[ClassMystic] = DisplayNameMystic
c.displayNameMap[ClassDefiler] = DisplayNameDefiler
c.displayNameMap[ClassMage] = DisplayNameMage
c.displayNameMap[ClassSorcerer] = DisplayNameSorcerer
c.displayNameMap[ClassWizard] = DisplayNameWizard
c.displayNameMap[ClassWarlock] = DisplayNameWarlock
c.displayNameMap[ClassEnchanter] = DisplayNameEnchanter
c.displayNameMap[ClassIllusionist] = DisplayNameIllusionist
c.displayNameMap[ClassCoercer] = DisplayNameCoercer
c.displayNameMap[ClassSummoner] = DisplayNameSummoner
c.displayNameMap[ClassConjuror] = DisplayNameConjuror
c.displayNameMap[ClassNecromancer] = DisplayNameNecromancer
c.displayNameMap[ClassScout] = DisplayNameScout
c.displayNameMap[ClassRogue] = DisplayNameRogue
c.displayNameMap[ClassSwashbuckler] = DisplayNameSwashbuckler
c.displayNameMap[ClassBrigand] = DisplayNameBrigand
c.displayNameMap[ClassBard] = DisplayNameBard
c.displayNameMap[ClassTroubador] = DisplayNameTroubador
c.displayNameMap[ClassDirge] = DisplayNameDirge
c.displayNameMap[ClassPredator] = DisplayNamePredator
c.displayNameMap[ClassRanger] = DisplayNameRanger
c.displayNameMap[ClassAssassin] = DisplayNameAssassin
c.displayNameMap[ClassAnimalist] = DisplayNameAnimalist
c.displayNameMap[ClassBeastlord] = DisplayNameBeastlord
c.displayNameMap[ClassShaper] = DisplayNameShaper
c.displayNameMap[ClassChanneler] = DisplayNameChanneler
c.displayNameMap[ClassArtisan] = DisplayNameArtisan
c.displayNameMap[ClassCraftsman] = DisplayNameCraftsman
c.displayNameMap[ClassProvisioner] = DisplayNameProvisioner
c.displayNameMap[ClassWoodworker] = DisplayNameWoodworker
c.displayNameMap[ClassCarpenter] = DisplayNameCarpenter
c.displayNameMap[ClassOutfitter] = DisplayNameOutfitter
c.displayNameMap[ClassArmorer] = DisplayNameArmorer
c.displayNameMap[ClassWeaponsmith] = DisplayNameWeaponsmith
c.displayNameMap[ClassTailor] = DisplayNameTailor
c.displayNameMap[ClassScholar] = DisplayNameScholar
c.displayNameMap[ClassJeweler] = DisplayNameJeweler
c.displayNameMap[ClassSage] = DisplayNameSage
c.displayNameMap[ClassAlchemist] = DisplayNameAlchemist
}
// GetClassID returns the class ID for a given class name
// Converted from C++ Classes::GetClassID
func (c *Classes) GetClassID(name string) int8 {
c.mutex.RLock()
defer c.mutex.RUnlock()
className := strings.ToUpper(strings.TrimSpace(name))
if classID, exists := c.classMap[className]; exists {
return classID
}
return -1 // Invalid class
}
// GetClassName returns the uppercase class name for a given ID
// Converted from C++ Classes::GetClassName
func (c *Classes) GetClassName(classID int8) string {
c.mutex.RLock()
defer c.mutex.RUnlock()
// Search through class map to find the name
for name, id := range c.classMap {
if id == classID {
return name
}
}
return "" // Invalid class ID
}
// GetClassNameCase returns the friendly display name for a given class ID
// Converted from C++ Classes::GetClassNameCase
func (c *Classes) GetClassNameCase(classID int8) string {
c.mutex.RLock()
defer c.mutex.RUnlock()
if displayName, exists := c.displayNameMap[classID]; exists {
return displayName
}
return "" // Invalid class ID
}
// GetBaseClass returns the base class ID for a given class
// Converted from C++ Classes::GetBaseClass
func (c *Classes) GetBaseClass(classID int8) int8 {
if classID >= ClassWarrior && classID <= ClassPaladin {
return ClassFighter
}
if (classID >= ClassCleric && classID <= ClassDefiler) || (classID == ClassShaper || classID == ClassChanneler) {
return ClassPriest
}
if classID >= ClassSorcerer && classID <= ClassNecromancer {
return ClassMage
}
if classID >= ClassRogue && classID <= ClassBeastlord {
return ClassScout
}
return ClassCommoner // Default for unknown classes
}
// GetSecondaryBaseClass returns the secondary base class ID for specialized classes
// Converted from C++ Classes::GetSecondaryBaseClass
func (c *Classes) GetSecondaryBaseClass(classID int8) int8 {
switch classID {
case ClassGuardian, ClassBerserker:
return ClassWarrior
case ClassMonk, ClassBruiser:
return ClassBrawler
case ClassShadowknight, ClassPaladin:
return ClassCrusader
case ClassTemplar, ClassInquisitor:
return ClassCleric
case ClassWarden, ClassFury:
return ClassDruid
case ClassMystic, ClassDefiler:
return ClassShaman
case ClassWizard, ClassWarlock:
return ClassSorcerer
case ClassIllusionist, ClassCoercer:
return ClassEnchanter
case ClassConjuror, ClassNecromancer:
return ClassSummoner
case ClassSwashbuckler, ClassBrigand:
return ClassRogue
case ClassTroubador, ClassDirge:
return ClassBard
case ClassRanger, ClassAssassin:
return ClassPredator
case ClassBeastlord:
return ClassAnimalist
case ClassChanneler:
return ClassShaper
}
return ClassCommoner // Default for unknown classes
}
// GetTSBaseClass returns the tradeskill base class ID
// Converted from C++ Classes::GetTSBaseClass
func (c *Classes) GetTSBaseClass(classID int8) int8 {
if classID+42 >= ClassArtisan {
return ClassArtisan - 44
}
return classID
}
// GetSecondaryTSBaseClass returns the secondary tradeskill base class ID
// Converted from C++ Classes::GetSecondaryTSBaseClass
func (c *Classes) GetSecondaryTSBaseClass(classID int8) int8 {
ret := classID + 42
if ret == ClassArtisan {
return ClassArtisan - 44
} else if ret >= ClassCraftsman && ret < ClassOutfitter {
return ClassCraftsman - 44
} else if ret >= ClassOutfitter && ret < ClassScholar {
return ClassOutfitter - 44
} else if ret >= ClassScholar {
return ClassScholar - 44
}
return classID
}
// IsValidClassID checks if a class ID is valid
func (c *Classes) IsValidClassID(classID int8) bool {
return classID >= MinClassID && classID <= MaxClassID
}
// GetAllClasses returns all class IDs and their display names
func (c *Classes) GetAllClasses() map[int8]string {
c.mutex.RLock()
defer c.mutex.RUnlock()
result := make(map[int8]string)
for classID, displayName := range c.displayNameMap {
result[classID] = displayName
}
return result
}
// IsAdventureClass checks if a class is an adventure class
func (c *Classes) IsAdventureClass(classID int8) bool {
return classID >= ClassCommoner && classID <= ClassChanneler
}
// IsTradeskillClass checks if a class is a tradeskill class
func (c *Classes) IsTradeskillClass(classID int8) bool {
return classID >= ClassArtisan && classID <= ClassAlchemist
}
// GetClassType returns the type of class (adventure, tradeskill, etc.)
func (c *Classes) GetClassType(classID int8) string {
if c.IsAdventureClass(classID) {
return ClassTypeAdventure
}
if c.IsTradeskillClass(classID) {
return ClassTypeTradeskill
}
return ClassTypeSpecial
}
// GetClassCount returns the total number of classes
func (c *Classes) GetClassCount() int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return len(c.displayNameMap)
}
// GetClassInfo returns comprehensive information about a class
func (c *Classes) GetClassInfo(classID int8) map[string]interface{} {
c.mutex.RLock()
defer c.mutex.RUnlock()
info := make(map[string]interface{})
if !c.IsValidClassID(classID) {
info["valid"] = false
return info
}
info["valid"] = true
info["class_id"] = classID
info["name"] = c.GetClassName(classID)
info["display_name"] = c.GetClassNameCase(classID)
info["base_class"] = c.GetBaseClass(classID)
info["secondary_base_class"] = c.GetSecondaryBaseClass(classID)
info["type"] = c.GetClassType(classID)
info["is_adventure"] = c.IsAdventureClass(classID)
info["is_tradeskill"] = c.IsTradeskillClass(classID)
return info
}
// Global classes instance
var globalClasses *Classes
var initClassesOnce sync.Once
// GetGlobalClasses returns the global classes manager (singleton)
func GetGlobalClasses() *Classes {
initClassesOnce.Do(func() {
globalClasses = NewClasses()
})
return globalClasses
}

View File

@ -0,0 +1,226 @@
package classes
// Adventure class ID constants converted from C++ classes.h
const (
// Base classes
ClassCommoner = 0
ClassFighter = 1
ClassPriest = 11
ClassMage = 21
ClassScout = 31
// Fighter subclasses
ClassWarrior = 2
ClassGuardian = 3
ClassBerserker = 4
ClassBrawler = 5
ClassMonk = 6
ClassBruiser = 7
ClassCrusader = 8
ClassShadowknight = 9
ClassPaladin = 10
// Priest subclasses
ClassCleric = 12
ClassTemplar = 13
ClassInquisitor = 14
ClassDruid = 15
ClassWarden = 16
ClassFury = 17
ClassShaman = 18
ClassMystic = 19
ClassDefiler = 20
// Mage subclasses
ClassSorcerer = 22
ClassWizard = 23
ClassWarlock = 24
ClassEnchanter = 25
ClassIllusionist = 26
ClassCoercer = 27
ClassSummoner = 28
ClassConjuror = 29
ClassNecromancer = 30
// Scout subclasses
ClassRogue = 32
ClassSwashbuckler = 33
ClassBrigand = 34
ClassBard = 35
ClassTroubador = 36
ClassDirge = 37
ClassPredator = 38
ClassRanger = 39
ClassAssassin = 40
ClassAnimalist = 41
ClassBeastlord = 42
// Special classes
ClassShaper = 43
ClassChanneler = 44
)
// Tradeskill class ID constants
const (
// Base tradeskill classes
ClassArtisan = 45
ClassCraftsman = 46
ClassOutfitter = 50
ClassScholar = 54
// Craftsman subclasses
ClassProvisioner = 47
ClassWoodworker = 48
ClassCarpenter = 49
// Outfitter subclasses
ClassArmorer = 51
ClassWeaponsmith = 52
ClassTailor = 53
// Scholar subclasses
ClassJeweler = 55
ClassSage = 56
ClassAlchemist = 57
)
// Class validation constants
const (
MaxClassID = 57
MinClassID = 0
DefaultClassID = ClassCommoner
ClassicMaxAdventureClass = 40 // Classic adventure classes (0-40)
ClassicMaxTradeskillClass = 13 // Classic tradeskill progression (0-13)
MaxClasses = 58 // Total number of classes
)
// Class type categories
const (
ClassTypeAdventure = "adventure"
ClassTypeTradeskill = "tradeskill"
ClassTypeSpecial = "special"
)
// Class name constants for lookup (uppercase keys from C++)
const (
ClassNameCommoner = "COMMONER"
ClassNameFighter = "FIGHTER"
ClassNameWarrior = "WARRIOR"
ClassNameGuardian = "GUARDIAN"
ClassNameBerserker = "BERSERKER"
ClassNameBrawler = "BRAWLER"
ClassNameMonk = "MONK"
ClassNameBruiser = "BRUISER"
ClassNameCrusader = "CRUSADER"
ClassNameShadowknight = "SHADOWKNIGHT"
ClassNamePaladin = "PALADIN"
ClassNamePriest = "PRIEST"
ClassNameCleric = "CLERIC"
ClassNameTemplar = "TEMPLAR"
ClassNameInquisitor = "INQUISITOR"
ClassNameDruid = "DRUID"
ClassNameWarden = "WARDEN"
ClassNameFury = "FURY"
ClassNameShaman = "SHAMAN"
ClassNameMystic = "MYSTIC"
ClassNameDefiler = "DEFILER"
ClassNameMage = "MAGE"
ClassNameSorcerer = "SORCERER"
ClassNameWizard = "WIZARD"
ClassNameWarlock = "WARLOCK"
ClassNameEnchanter = "ENCHANTER"
ClassNameIllusionist = "ILLUSIONIST"
ClassNameCoercer = "COERCER"
ClassNameSummoner = "SUMMONER"
ClassNameConjuror = "CONJUROR"
ClassNameNecromancer = "NECROMANCER"
ClassNameScout = "SCOUT"
ClassNameRogue = "ROGUE"
ClassNameSwashbuckler = "SWASHBUCKLER"
ClassNameBrigand = "BRIGAND"
ClassNameBard = "BARD"
ClassNameTroubador = "TROUBADOR"
ClassNameDirge = "DIRGE"
ClassNamePredator = "PREDATOR"
ClassNameRanger = "RANGER"
ClassNameAssassin = "ASSASSIN"
ClassNameAnimalist = "ANIMALIST"
ClassNameBeastlord = "BEASTLORD"
ClassNameShaper = "SHAPER"
ClassNameChanneler = "CHANNELER"
ClassNameArtisan = "ARTISAN"
ClassNameCraftsman = "CRAFTSMAN"
ClassNameProvisioner = "PROVISIONER"
ClassNameWoodworker = "WOODWORKER"
ClassNameCarpenter = "CARPENTER"
ClassNameOutfitter = "OUTFITTER"
ClassNameArmorer = "ARMORER"
ClassNameWeaponsmith = "WEAPONSMITH"
ClassNameTailor = "TAILOR"
ClassNameScholar = "SCHOLAR"
ClassNameJeweler = "JEWELER"
ClassNameSage = "SAGE"
ClassNameAlchemist = "ALCHEMIST"
)
// Class display names (proper case)
const (
DisplayNameCommoner = "Commoner"
DisplayNameFighter = "Fighter"
DisplayNameWarrior = "Warrior"
DisplayNameGuardian = "Guardian"
DisplayNameBerserker = "Berserker"
DisplayNameBrawler = "Brawler"
DisplayNameMonk = "Monk"
DisplayNameBruiser = "Bruiser"
DisplayNameCrusader = "Crusader"
DisplayNameShadowknight = "Shadowknight"
DisplayNamePaladin = "Paladin"
DisplayNamePriest = "Priest"
DisplayNameCleric = "Cleric"
DisplayNameTemplar = "Templar"
DisplayNameInquisitor = "Inquisitor"
DisplayNameDruid = "Druid"
DisplayNameWarden = "Warden"
DisplayNameFury = "Fury"
DisplayNameShaman = "Shaman"
DisplayNameMystic = "Mystic"
DisplayNameDefiler = "Defiler"
DisplayNameMage = "Mage"
DisplayNameSorcerer = "Sorcerer"
DisplayNameWizard = "Wizard"
DisplayNameWarlock = "Warlock"
DisplayNameEnchanter = "Enchanter"
DisplayNameIllusionist = "Illusionist"
DisplayNameCoercer = "Coercer"
DisplayNameSummoner = "Summoner"
DisplayNameConjuror = "Conjuror"
DisplayNameNecromancer = "Necromancer"
DisplayNameScout = "Scout"
DisplayNameRogue = "Rogue"
DisplayNameSwashbuckler = "Swashbuckler"
DisplayNameBrigand = "Brigand"
DisplayNameBard = "Bard"
DisplayNameTroubador = "Troubador"
DisplayNameDirge = "Dirge"
DisplayNamePredator = "Predator"
DisplayNameRanger = "Ranger"
DisplayNameAssassin = "Assassin"
DisplayNameAnimalist = "Animalist"
DisplayNameBeastlord = "Beastlord"
DisplayNameShaper = "Shaper"
DisplayNameChanneler = "Channeler"
DisplayNameArtisan = "Artisan"
DisplayNameCraftsman = "Craftsman"
DisplayNameProvisioner = "Provisioner"
DisplayNameWoodworker = "Woodworker"
DisplayNameCarpenter = "Carpenter"
DisplayNameOutfitter = "Outfitter"
DisplayNameArmorer = "Armorer"
DisplayNameWeaponsmith = "Weaponsmith"
DisplayNameTailor = "Tailor"
DisplayNameScholar = "Scholar"
DisplayNameJeweler = "Jeweler"
DisplayNameSage = "Sage"
DisplayNameAlchemist = "Alchemist"
)

View File

@ -0,0 +1,352 @@
package classes
import (
"fmt"
)
// ClassAware interface for entities that have class information
type ClassAware interface {
GetClass() int8
SetClass(int8)
}
// EntityWithClass interface extends ClassAware with additional entity properties
type EntityWithClass interface {
ClassAware
GetID() int32
GetName() string
GetLevel() int8
}
// ClassIntegration provides class-related functionality for other systems
type ClassIntegration struct {
classes *Classes
utils *ClassUtils
}
// NewClassIntegration creates a new class integration helper
func NewClassIntegration() *ClassIntegration {
return &ClassIntegration{
classes: GetGlobalClasses(),
utils: NewClassUtils(),
}
}
// ValidateEntityClass validates an entity's class and provides detailed information
func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string, map[string]interface{}) {
classID := entity.GetClass()
if !ci.classes.IsValidClassID(classID) {
return false, fmt.Sprintf("Invalid class ID: %d", classID), nil
}
classInfo := ci.classes.GetClassInfo(classID)
return true, "Valid class", classInfo
}
// GetEntityClassInfo returns comprehensive class information for an entity
func (ci *ClassIntegration) GetEntityClassInfo(entity EntityWithClass) map[string]interface{} {
info := make(map[string]interface{})
// Basic entity info
info["entity_id"] = entity.GetID()
info["entity_name"] = entity.GetName()
info["entity_level"] = entity.GetLevel()
// Class information
classID := entity.GetClass()
classInfo := ci.classes.GetClassInfo(classID)
info["class"] = classInfo
// Additional class-specific info
info["description"] = ci.utils.GetClassDescription(classID)
info["eq_class_name"] = ci.utils.GetEQClassName(classID, entity.GetLevel())
info["progression"] = ci.utils.GetClassProgression(classID)
info["aliases"] = ci.utils.GetClassAliases(classID)
info["is_base_class"] = ci.utils.IsBaseClass(classID)
info["is_secondary_base"] = ci.utils.IsSecondaryBaseClass(classID)
return info
}
// ChangeEntityClass changes an entity's class with validation
func (ci *ClassIntegration) ChangeEntityClass(entity ClassAware, newClassID int8) error {
if !ci.classes.IsValidClassID(newClassID) {
return fmt.Errorf("invalid class ID: %d", newClassID)
}
oldClassID := entity.GetClass()
// Validate the class transition
if valid, reason := ci.utils.ValidateClassTransition(oldClassID, newClassID); !valid {
return fmt.Errorf("class change not allowed: %s", reason)
}
// Perform the class change
entity.SetClass(newClassID)
return nil
}
// GetRandomClassForEntity returns a random class appropriate for an entity
func (ci *ClassIntegration) GetRandomClassForEntity(classType string) int8 {
return ci.utils.GetRandomClassByType(classType)
}
// CheckClassCompatibility checks if two entities' classes are compatible for grouping
func (ci *ClassIntegration) CheckClassCompatibility(entity1, entity2 ClassAware) bool {
class1 := entity1.GetClass()
class2 := entity2.GetClass()
if !ci.classes.IsValidClassID(class1) || !ci.classes.IsValidClassID(class2) {
return false
}
// Same class is always compatible
if class1 == class2 {
return true
}
// Check if they share the same base class (good for grouping)
base1 := ci.classes.GetBaseClass(class1)
base2 := ci.classes.GetBaseClass(class2)
// Different base classes can group together (provides diversity)
// Same base class provides synergy
return true // For now, all classes are compatible for grouping
}
// FormatEntityClass returns a formatted class name for an entity
func (ci *ClassIntegration) FormatEntityClass(entity EntityWithClass, format string) string {
classID := entity.GetClass()
level := entity.GetLevel()
switch format {
case "eq":
return ci.utils.GetEQClassName(classID, level)
default:
return ci.utils.FormatClassName(classID, format)
}
}
// GetEntityBaseClass returns an entity's base class
func (ci *ClassIntegration) GetEntityBaseClass(entity ClassAware) int8 {
classID := entity.GetClass()
return ci.classes.GetBaseClass(classID)
}
// GetEntitySecondaryBaseClass returns an entity's secondary base class
func (ci *ClassIntegration) GetEntitySecondaryBaseClass(entity ClassAware) int8 {
classID := entity.GetClass()
return ci.classes.GetSecondaryBaseClass(classID)
}
// IsEntityAdventureClass checks if an entity has an adventure class
func (ci *ClassIntegration) IsEntityAdventureClass(entity ClassAware) bool {
classID := entity.GetClass()
return ci.classes.IsAdventureClass(classID)
}
// IsEntityTradeskillClass checks if an entity has a tradeskill class
func (ci *ClassIntegration) IsEntityTradeskillClass(entity ClassAware) bool {
classID := entity.GetClass()
return ci.classes.IsTradeskillClass(classID)
}
// GetEntitiesByClass filters entities by class
func (ci *ClassIntegration) GetEntitiesByClass(entities []ClassAware, classID int8) []ClassAware {
result := make([]ClassAware, 0)
for _, entity := range entities {
if entity.GetClass() == classID {
result = append(result, entity)
}
}
return result
}
// GetEntitiesByBaseClass filters entities by base class
func (ci *ClassIntegration) GetEntitiesByBaseClass(entities []ClassAware, baseClassID int8) []ClassAware {
result := make([]ClassAware, 0)
for _, entity := range entities {
if ci.GetEntityBaseClass(entity) == baseClassID {
result = append(result, entity)
}
}
return result
}
// GetEntitiesByClassType filters entities by class type (adventure/tradeskill)
func (ci *ClassIntegration) GetEntitiesByClassType(entities []ClassAware, classType string) []ClassAware {
result := make([]ClassAware, 0)
for _, entity := range entities {
classID := entity.GetClass()
if ci.classes.GetClassType(classID) == classType {
result = append(result, entity)
}
}
return result
}
// ValidateClassForRace checks if a class/race combination is valid
func (ci *ClassIntegration) ValidateClassForRace(classID, raceID int8) (bool, string) {
if !ci.classes.IsValidClassID(classID) {
return false, "Invalid class"
}
// Use the utility function (which currently allows all combinations)
if ci.utils.ValidateClassForRace(classID, raceID) {
return true, ""
}
className := ci.classes.GetClassNameCase(classID)
return false, fmt.Sprintf("Class %s cannot be race %d", className, raceID)
}
// GetClassStartingStats returns the starting stats for a class
func (ci *ClassIntegration) GetClassStartingStats(classID int8) map[string]int16 {
// Base stats that all classes start with
baseStats := map[string]int16{
"strength": 50,
"stamina": 50,
"agility": 50,
"wisdom": 50,
"intelligence": 50,
}
// Apply class modifiers based on class type and role
switch ci.classes.GetBaseClass(classID) {
case ClassFighter:
baseStats["strength"] += 5
baseStats["stamina"] += 5
baseStats["intelligence"] -= 3
case ClassPriest:
baseStats["wisdom"] += 5
baseStats["intelligence"] += 3
baseStats["strength"] -= 2
case ClassMage:
baseStats["intelligence"] += 5
baseStats["wisdom"] += 3
baseStats["strength"] -= 3
baseStats["stamina"] -= 2
case ClassScout:
baseStats["agility"] += 5
baseStats["stamina"] += 3
baseStats["wisdom"] -= 2
}
// Fine-tune for specific secondary base classes
switch ci.classes.GetSecondaryBaseClass(classID) {
case ClassWarrior:
baseStats["strength"] += 2
baseStats["stamina"] += 2
case ClassBrawler:
baseStats["agility"] += 2
baseStats["strength"] += 1
case ClassCrusader:
baseStats["wisdom"] += 2
baseStats["strength"] += 1
case ClassCleric:
baseStats["wisdom"] += 3
case ClassDruid:
baseStats["wisdom"] += 2
baseStats["intelligence"] += 1
case ClassShaman:
baseStats["wisdom"] += 2
baseStats["stamina"] += 1
case ClassSorcerer:
baseStats["intelligence"] += 3
case ClassEnchanter:
baseStats["intelligence"] += 2
baseStats["agility"] += 1
case ClassSummoner:
baseStats["intelligence"] += 2
baseStats["wisdom"] += 1
case ClassRogue:
baseStats["agility"] += 3
case ClassBard:
baseStats["agility"] += 2
baseStats["intelligence"] += 1
case ClassPredator:
baseStats["agility"] += 2
baseStats["stamina"] += 1
}
return baseStats
}
// CreateClassSpecificEntity creates entity data with class-specific properties
func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]interface{} {
if !ci.classes.IsValidClassID(classID) {
return nil
}
entityData := make(map[string]interface{})
// Basic class info
entityData["class_id"] = classID
entityData["class_name"] = ci.classes.GetClassNameCase(classID)
entityData["class_type"] = ci.classes.GetClassType(classID)
// Starting stats
entityData["starting_stats"] = ci.GetClassStartingStats(classID)
// Class progression
entityData["progression"] = ci.utils.GetClassProgression(classID)
// Class description
entityData["description"] = ci.utils.GetClassDescription(classID)
// Role information
entityData["base_class"] = ci.classes.GetBaseClass(classID)
entityData["secondary_base_class"] = ci.classes.GetSecondaryBaseClass(classID)
return entityData
}
// GetClassSelectionData returns data for class selection UI
func (ci *ClassIntegration) GetClassSelectionData() map[string]interface{} {
data := make(map[string]interface{})
// All available adventure classes (exclude tradeskill for character creation)
allClasses := ci.classes.GetAllClasses()
adventureClasses := make([]map[string]interface{}, 0)
for classID, displayName := range allClasses {
if ci.classes.IsAdventureClass(classID) {
classData := map[string]interface{}{
"id": classID,
"name": displayName,
"type": ci.classes.GetClassType(classID),
"description": ci.utils.GetClassDescription(classID),
"base_class": ci.classes.GetBaseClass(classID),
"secondary_base_class": ci.classes.GetSecondaryBaseClass(classID),
"starting_stats": ci.GetClassStartingStats(classID),
"progression": ci.utils.GetClassProgression(classID),
"is_base_class": ci.utils.IsBaseClass(classID),
}
adventureClasses = append(adventureClasses, classData)
}
}
data["adventure_classes"] = adventureClasses
data["statistics"] = ci.utils.GetClassStatistics()
return data
}
// Global class integration instance
var globalClassIntegration *ClassIntegration
// GetGlobalClassIntegration returns the global class integration helper
func GetGlobalClassIntegration() *ClassIntegration {
if globalClassIntegration == nil {
globalClassIntegration = NewClassIntegration()
}
return globalClassIntegration
}

455
internal/classes/manager.go Normal file
View File

@ -0,0 +1,455 @@
package classes
import (
"fmt"
"sync"
)
// ClassManager provides high-level class management functionality
type ClassManager struct {
classes *Classes
utils *ClassUtils
integration *ClassIntegration
// Statistics tracking
classUsageStats map[int8]int32 // Track how often each class is used
// Thread safety
mutex sync.RWMutex
}
// NewClassManager creates a new class manager
func NewClassManager() *ClassManager {
return &ClassManager{
classes: GetGlobalClasses(),
utils: NewClassUtils(),
integration: NewClassIntegration(),
classUsageStats: make(map[int8]int32),
}
}
// RegisterClassUsage tracks class usage for statistics
func (cm *ClassManager) RegisterClassUsage(classID int8) {
if !cm.classes.IsValidClassID(classID) {
return
}
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.classUsageStats[classID]++
}
// GetClassUsageStats returns class usage statistics
func (cm *ClassManager) GetClassUsageStats() map[int8]int32 {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
// Return a copy to prevent external modification
stats := make(map[int8]int32)
for classID, count := range cm.classUsageStats {
stats[classID] = count
}
return stats
}
// GetMostPopularClass returns the most frequently used class
func (cm *ClassManager) GetMostPopularClass() (int8, int32) {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
var mostPopularClass int8 = -1
var maxUsage int32 = 0
for classID, usage := range cm.classUsageStats {
if usage > maxUsage {
maxUsage = usage
mostPopularClass = classID
}
}
return mostPopularClass, maxUsage
}
// GetLeastPopularClass returns the least frequently used class
func (cm *ClassManager) GetLeastPopularClass() (int8, int32) {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
var leastPopularClass int8 = -1
var minUsage int32 = -1
for classID, usage := range cm.classUsageStats {
if minUsage == -1 || usage < minUsage {
minUsage = usage
leastPopularClass = classID
}
}
return leastPopularClass, minUsage
}
// ResetUsageStats clears all usage statistics
func (cm *ClassManager) ResetUsageStats() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.classUsageStats = make(map[int8]int32)
}
// ProcessClassCommand handles class-related commands
func (cm *ClassManager) ProcessClassCommand(command string, args []string) (string, error) {
switch command {
case "list":
return cm.handleListCommand(args)
case "info":
return cm.handleInfoCommand(args)
case "random":
return cm.handleRandomCommand(args)
case "stats":
return cm.handleStatsCommand(args)
case "search":
return cm.handleSearchCommand(args)
case "progression":
return cm.handleProgressionCommand(args)
default:
return "", fmt.Errorf("unknown class command: %s", command)
}
}
// handleListCommand lists classes by criteria
func (cm *ClassManager) handleListCommand(args []string) (string, error) {
if len(args) == 0 {
// List all classes
allClasses := cm.classes.GetAllClasses()
result := "All Classes:\n"
for classID, displayName := range allClasses {
classType := cm.classes.GetClassType(classID)
baseClass := cm.classes.GetBaseClass(classID)
baseClassName := cm.classes.GetClassNameCase(baseClass)
result += fmt.Sprintf("%d: %s (%s, Base: %s)\n", classID, displayName, classType, baseClassName)
}
return result, nil
}
// List classes by type
classType := args[0]
allClasses := cm.classes.GetAllClasses()
result := fmt.Sprintf("%s Classes:\n", classType)
count := 0
for classID, displayName := range allClasses {
if cm.classes.GetClassType(classID) == classType {
baseClass := cm.classes.GetBaseClass(classID)
baseClassName := cm.classes.GetClassNameCase(baseClass)
result += fmt.Sprintf("%d: %s (Base: %s)\n", classID, displayName, baseClassName)
count++
}
}
if count == 0 {
return fmt.Sprintf("No classes found for type: %s", classType), nil
}
return result, nil
}
// handleInfoCommand provides detailed information about a class
func (cm *ClassManager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("class name or ID required")
}
// Try to parse as class name or ID
classID := cm.utils.ParseClassName(args[0])
if classID == -1 {
return fmt.Sprintf("Invalid class: %s", args[0]), nil
}
classInfo := cm.classes.GetClassInfo(classID)
if !classInfo["valid"].(bool) {
return fmt.Sprintf("Invalid class ID: %d", classID), nil
}
result := fmt.Sprintf("Class Information:\n")
result += fmt.Sprintf("ID: %d\n", classID)
result += fmt.Sprintf("Name: %s\n", classInfo["display_name"])
result += fmt.Sprintf("Type: %s\n", classInfo["type"])
result += fmt.Sprintf("Base Class: %s\n", cm.classes.GetClassNameCase(classInfo["base_class"].(int8)))
if secondaryBase := classInfo["secondary_base_class"].(int8); secondaryBase != DefaultClassID {
result += fmt.Sprintf("Secondary Base: %s\n", cm.classes.GetClassNameCase(secondaryBase))
}
result += fmt.Sprintf("Description: %s\n", cm.utils.GetClassDescription(classID))
// Add progression path
progression := cm.utils.GetClassProgression(classID)
if len(progression) > 1 {
result += "Progression Path: "
progressionNames := make([]string, len(progression))
for i, progClassID := range progression {
progressionNames[i] = cm.classes.GetClassNameCase(progClassID)
}
result += fmt.Sprintf("%s\n", cm.utils.FormatClassList(progression, " → "))
}
// Add starting stats
startingStats := cm.integration.GetClassStartingStats(classID)
if len(startingStats) > 0 {
result += "Starting Stats:\n"
for stat, value := range startingStats {
result += fmt.Sprintf(" %s: %d\n", stat, value)
}
}
// Add usage statistics if available
cm.mutex.RLock()
usage, hasUsage := cm.classUsageStats[classID]
cm.mutex.RUnlock()
if hasUsage {
result += fmt.Sprintf("Usage Count: %d\n", usage)
}
return result, nil
}
// handleRandomCommand generates random classes
func (cm *ClassManager) handleRandomCommand(args []string) (string, error) {
classType := ClassTypeAdventure
if len(args) > 0 {
classType = args[0]
}
classID := cm.utils.GetRandomClassByType(classType)
if classID == -1 {
return "Failed to generate random class", nil
}
displayName := cm.classes.GetClassNameCase(classID)
actualType := cm.classes.GetClassType(classID)
return fmt.Sprintf("Random %s Class: %s (ID: %d)", actualType, displayName, classID), nil
}
// handleStatsCommand shows class system statistics
func (cm *ClassManager) handleStatsCommand(args []string) (string, error) {
systemStats := cm.utils.GetClassStatistics()
usageStats := cm.GetClassUsageStats()
result := "Class System Statistics:\n"
result += fmt.Sprintf("Total Classes: %d\n", systemStats["total_classes"])
result += fmt.Sprintf("Adventure Classes: %d\n", systemStats["adventure_classes"])
result += fmt.Sprintf("Tradeskill Classes: %d\n", systemStats["tradeskill_classes"])
result += fmt.Sprintf("Special Classes: %d\n", systemStats["special_classes"])
if len(usageStats) > 0 {
result += "\nUsage Statistics:\n"
mostPopular, maxUsage := cm.GetMostPopularClass()
leastPopular, minUsage := cm.GetLeastPopularClass()
if mostPopular != -1 {
mostPopularName := cm.classes.GetClassNameCase(mostPopular)
result += fmt.Sprintf("Most Popular: %s (%d uses)\n", mostPopularName, maxUsage)
}
if leastPopular != -1 {
leastPopularName := cm.classes.GetClassNameCase(leastPopular)
result += fmt.Sprintf("Least Popular: %s (%d uses)\n", leastPopularName, minUsage)
}
}
// Show base class distribution
if baseDistribution, exists := systemStats["base_class_distribution"]; exists {
result += "\nBase Class Distribution:\n"
distribution := baseDistribution.(map[string][]string)
for baseClass, subClasses := range distribution {
result += fmt.Sprintf("%s: %d subclasses\n", baseClass, len(subClasses))
}
}
return result, nil
}
// handleSearchCommand searches for classes by pattern
func (cm *ClassManager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search pattern required")
}
pattern := args[0]
matchingClasses := cm.utils.GetClassesByPattern(pattern)
if len(matchingClasses) == 0 {
return fmt.Sprintf("No classes found matching pattern: %s", pattern), nil
}
result := fmt.Sprintf("Classes matching '%s':\n", pattern)
for _, classID := range matchingClasses {
displayName := cm.classes.GetClassNameCase(classID)
classType := cm.classes.GetClassType(classID)
baseClass := cm.classes.GetBaseClass(classID)
baseClassName := cm.classes.GetClassNameCase(baseClass)
result += fmt.Sprintf("%d: %s (%s, Base: %s)\n", classID, displayName, classType, baseClassName)
}
return result, nil
}
// handleProgressionCommand shows class progression information
func (cm *ClassManager) handleProgressionCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("class name or ID required")
}
classID := cm.utils.ParseClassName(args[0])
if classID == -1 {
return fmt.Sprintf("Invalid class: %s", args[0]), nil
}
progression := cm.utils.GetClassProgression(classID)
if len(progression) <= 1 {
return fmt.Sprintf("Class %s has no progression path", cm.classes.GetClassNameCase(classID)), nil
}
result := fmt.Sprintf("Progression Path for %s:\n", cm.classes.GetClassNameCase(classID))
for i, stepClassID := range progression {
stepName := cm.classes.GetClassNameCase(stepClassID)
if i == 0 {
result += fmt.Sprintf("1. %s (Starting Class)\n", stepName)
} else if i == len(progression)-1 {
result += fmt.Sprintf("%d. %s (Final Class)\n", i+1, stepName)
} else {
result += fmt.Sprintf("%d. %s\n", i+1, stepName)
}
}
return result, nil
}
// ValidateEntityClasses validates classes for a collection of entities
func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]interface{} {
validationResults := make(map[string]interface{})
validCount := 0
invalidCount := 0
classDistribution := make(map[int8]int)
for i, entity := range entities {
classID := entity.GetClass()
isValid := cm.classes.IsValidClassID(classID)
if isValid {
validCount++
classDistribution[classID]++
} else {
invalidCount++
}
// Track invalid entities
if !isValid {
if validationResults["invalid_entities"] == nil {
validationResults["invalid_entities"] = make([]map[string]interface{}, 0)
}
invalidList := validationResults["invalid_entities"].([]map[string]interface{})
invalidList = append(invalidList, map[string]interface{}{
"index": i,
"class_id": classID,
})
validationResults["invalid_entities"] = invalidList
}
}
validationResults["total_entities"] = len(entities)
validationResults["valid_count"] = validCount
validationResults["invalid_count"] = invalidCount
validationResults["class_distribution"] = classDistribution
return validationResults
}
// GetClassRecommendations returns class recommendations for character creation
func (cm *ClassManager) GetClassRecommendations(preferences map[string]interface{}) []int8 {
recommendations := make([]int8, 0)
// Check for class type preference
if classType, exists := preferences["class_type"]; exists {
if typeStr, ok := classType.(string); ok {
allClasses := cm.classes.GetAllClasses()
for classID := range allClasses {
if cm.classes.GetClassType(classID) == typeStr {
recommendations = append(recommendations, classID)
}
}
}
}
// Check for base class preference
if baseClass, exists := preferences["base_class"]; exists {
if baseClassID, ok := baseClass.(int8); ok {
subClasses := cm.utils.GetClassesByBaseClass(baseClassID)
recommendations = append(recommendations, subClasses...)
}
}
// Check for specific stat preferences
if preferredStats, exists := preferences["preferred_stats"]; exists {
if stats, ok := preferredStats.([]string); ok {
allClasses := cm.classes.GetAllClasses()
for classID := range allClasses {
startingStats := cm.integration.GetClassStartingStats(classID)
// Check if this class has bonuses in preferred stats
hasPreferredBonus := false
for _, preferredStat := range stats {
if statValue, exists := startingStats[preferredStat]; exists && statValue > 52 { // Above base of 50 + minor bonus
hasPreferredBonus = true
break
}
}
if hasPreferredBonus {
recommendations = append(recommendations, classID)
}
}
}
}
// If no specific preferences, recommend popular classes
if len(recommendations) == 0 {
// Get usage stats and recommend most popular classes
usageStats := cm.GetClassUsageStats()
if len(usageStats) > 0 {
// Sort by usage and take top classes
// For simplicity, just return all classes with usage > 0
for classID, usage := range usageStats {
if usage > 0 {
recommendations = append(recommendations, classID)
}
}
}
// If still no recommendations, return a default set of beginner-friendly classes
if len(recommendations) == 0 {
recommendations = []int8{ClassWarrior, ClassCleric, ClassWizard, ClassRogue}
}
}
return recommendations
}
// Global class manager instance
var globalClassManager *ClassManager
var initClassManagerOnce sync.Once
// GetGlobalClassManager returns the global class manager (singleton)
func GetGlobalClassManager() *ClassManager {
initClassManagerOnce.Do(func() {
globalClassManager = NewClassManager()
})
return globalClassManager
}

451
internal/classes/utils.go Normal file
View File

@ -0,0 +1,451 @@
package classes
import (
"fmt"
"math/rand"
"strings"
)
// ClassUtils provides utility functions for class operations
type ClassUtils struct {
classes *Classes
}
// NewClassUtils creates a new class utilities instance
func NewClassUtils() *ClassUtils {
return &ClassUtils{
classes: GetGlobalClasses(),
}
}
// ParseClassName attempts to parse a class name from various input formats
func (cu *ClassUtils) ParseClassName(input string) int8 {
if input == "" {
return -1
}
// Try direct lookup first
classID := cu.classes.GetClassID(input)
if classID != -1 {
return classID
}
// Try with common variations
variations := []string{
strings.ToUpper(input),
strings.ReplaceAll(strings.ToUpper(input), " ", ""),
strings.ReplaceAll(strings.ToUpper(input), "_", ""),
strings.ReplaceAll(strings.ToUpper(input), "-", ""),
}
for _, variation := range variations {
if classID := cu.classes.GetClassID(variation); classID != -1 {
return classID
}
}
// Try matching against friendly names (case insensitive)
inputLower := strings.ToLower(input)
allClasses := cu.classes.GetAllClasses()
for classID, displayName := range allClasses {
if strings.ToLower(displayName) == inputLower {
return classID
}
}
return -1 // Not found
}
// FormatClassName returns a properly formatted class name
func (cu *ClassUtils) FormatClassName(classID int8, format string) string {
switch strings.ToLower(format) {
case "display", "friendly", "proper":
return cu.classes.GetClassNameCase(classID)
case "upper", "uppercase":
return cu.classes.GetClassName(classID)
case "lower", "lowercase":
return strings.ToLower(cu.classes.GetClassName(classID))
default:
return cu.classes.GetClassNameCase(classID) // Default to friendly name
}
}
// GetRandomClassByType returns a random class of the specified type
func (cu *ClassUtils) GetRandomClassByType(classType string) int8 {
allClasses := cu.classes.GetAllClasses()
validClasses := make([]int8, 0)
for classID := range allClasses {
if cu.classes.GetClassType(classID) == classType {
validClasses = append(validClasses, classID)
}
}
if len(validClasses) == 0 {
return DefaultClassID
}
return validClasses[rand.Intn(len(validClasses))]
}
// GetRandomAdventureClass returns a random adventure class
func (cu *ClassUtils) GetRandomAdventureClass() int8 {
return cu.GetRandomClassByType(ClassTypeAdventure)
}
// GetRandomTradeskillClass returns a random tradeskill class
func (cu *ClassUtils) GetRandomTradeskillClass() int8 {
return cu.GetRandomClassByType(ClassTypeTradeskill)
}
// ValidateClassForRace checks if a class is valid for a specific race
// This is a placeholder for future race-class restrictions
func (cu *ClassUtils) ValidateClassForRace(classID, raceID int8) bool {
// TODO: Implement race-class restrictions when race system is available
// For now, all classes can be all races
return cu.classes.IsValidClassID(classID)
}
// GetClassDescription returns a description of the class
func (cu *ClassUtils) GetClassDescription(classID int8) string {
// This would typically come from a database or configuration
// For now, provide basic descriptions based on class
switch classID {
case ClassCommoner:
return "A starting class for all characters before choosing their path."
case ClassFighter:
return "Warriors who excel in melee combat and defense."
case ClassWarrior:
return "Masters of weapons and armor, the ultimate melee combatants."
case ClassGuardian:
return "Defensive warriors who protect their allies with shield and sword."
case ClassBerserker:
return "Rage-fueled fighters who sacrifice defense for devastating attacks."
case ClassBrawler:
return "Hand-to-hand combat specialists who fight with fists and focus."
case ClassMonk:
return "Disciplined fighters who use martial arts and inner peace."
case ClassBruiser:
return "Brutal brawlers who overwhelm enemies with raw power."
case ClassCrusader:
return "Holy warriors who blend combat prowess with divine magic."
case ClassShadowknight:
return "Dark knights who wield unholy magic alongside martial skill."
case ClassPaladin:
return "Champions of good who protect the innocent with sword and spell."
case ClassPriest:
return "Divine casters who channel the power of the gods."
case ClassCleric:
return "Healers and supporters who keep their allies alive and fighting."
case ClassTemplar:
return "Protective priests who shield allies from harm."
case ClassInquisitor:
return "Militant clerics who combine healing with righteous fury."
case ClassDruid:
return "Nature priests who harness the power of the natural world."
case ClassWarden:
return "Protective druids who shield allies with nature's blessing."
case ClassFury:
return "Destructive druids who unleash nature's wrath upon enemies."
case ClassShaman:
return "Spirit-workers who commune with ancestors and totems."
case ClassMystic:
return "Supportive shamans who provide wards and spiritual guidance."
case ClassDefiler:
return "Dark shamans who corrupt and weaken their enemies."
case ClassMage:
return "Wielders of arcane magic who bend reality to their will."
case ClassSorcerer:
return "Destructive mages who specialize in damaging spells."
case ClassWizard:
return "Scholarly sorcerers who master the elements."
case ClassWarlock:
return "Dark sorcerers who deal in forbidden magic."
case ClassEnchanter:
return "Mind-controlling mages who manipulate enemies and allies."
case ClassIllusionist:
return "Deceptive enchanters who confuse and misdirect."
case ClassCoercer:
return "Dominating enchanters who force enemies to obey."
case ClassSummoner:
return "Mages who call forth creatures to fight for them."
case ClassConjuror:
return "Elemental summoners who command earth and air."
case ClassNecromancer:
return "Death mages who raise undead minions and drain life."
case ClassScout:
return "Agile fighters who rely on speed and cunning."
case ClassRogue:
return "Stealthy combatants who strike from the shadows."
case ClassSwashbuckler:
return "Dashing rogues who fight with finesse and flair."
case ClassBrigand:
return "Brutal rogues who prefer dirty fighting tactics."
case ClassBard:
return "Musical combatants who inspire allies and demoralize foes."
case ClassTroubador:
return "Supportive bards who strengthen their allies."
case ClassDirge:
return "Dark bards who weaken enemies with haunting melodies."
case ClassPredator:
return "Hunters who excel at tracking and ranged combat."
case ClassRanger:
return "Nature-loving predators who protect the wilderness."
case ClassAssassin:
return "Deadly predators who eliminate targets with precision."
case ClassAnimalist:
return "Beast masters who fight alongside animal companions."
case ClassBeastlord:
return "Animalists who have formed powerful bonds with their pets."
case ClassShaper:
return "Mystic priests who manipulate spiritual energy."
case ClassChanneler:
return "Shapers who focus spiritual power through channeling."
case ClassArtisan:
return "Crafters who create useful items for adventurers."
case ClassCraftsman:
return "Specialized artisans who work with physical materials."
case ClassProvisioner:
return "Food and drink specialists who create consumables."
case ClassWoodworker:
return "Crafters who work with wood to create furniture and tools."
case ClassCarpenter:
return "Master woodworkers who create complex wooden items."
case ClassOutfitter:
return "Equipment crafters who create armor and weapons."
case ClassArmorer:
return "Specialists in creating protective armor."
case ClassWeaponsmith:
return "Masters of weapon crafting and enhancement."
case ClassTailor:
return "Cloth workers who create clothing and soft armor."
case ClassScholar:
return "Academic crafters who create magical and scholarly items."
case ClassJeweler:
return "Specialists in creating jewelry and accessories."
case ClassSage:
return "Book and scroll crafters who preserve knowledge."
case ClassAlchemist:
return "Potion makers who brew magical elixirs and potions."
default:
return "An unknown class with mysterious abilities."
}
}
// GetClassProgression returns the class progression path
func (cu *ClassUtils) GetClassProgression(classID int8) []int8 {
progression := make([]int8, 0)
// Always start with Commoner (except for Commoner itself)
if classID != ClassCommoner {
progression = append(progression, ClassCommoner)
}
// Add base class if different from current
baseClass := cu.classes.GetBaseClass(classID)
if baseClass != classID && baseClass != ClassCommoner {
progression = append(progression, baseClass)
}
// Add secondary base class if different
secondaryBase := cu.classes.GetSecondaryBaseClass(classID)
if secondaryBase != classID && secondaryBase != baseClass && secondaryBase != ClassCommoner {
progression = append(progression, secondaryBase)
}
// Add the final class
progression = append(progression, classID)
return progression
}
// GetClasssByBaseClass returns all classes that belong to a base class
func (cu *ClassUtils) GetClasssByBaseClass(baseClassID int8) []int8 {
result := make([]int8, 0)
allClasses := cu.classes.GetAllClasses()
for classID := range allClasses {
if cu.classes.GetBaseClass(classID) == baseClassID {
result = append(result, classID)
}
}
return result
}
// GetClassesBySecondaryBase returns all classes that belong to a secondary base class
func (cu *ClassUtils) GetClassesBySecondaryBase(secondaryBaseID int8) []int8 {
result := make([]int8, 0)
allClasses := cu.classes.GetAllClasses()
for classID := range allClasses {
if cu.classes.GetSecondaryBaseClass(classID) == secondaryBaseID {
result = append(result, classID)
}
}
return result
}
// GetClassesByPattern returns classes matching a name pattern
func (cu *ClassUtils) GetClassesByPattern(pattern string) []int8 {
pattern = strings.ToLower(pattern)
result := make([]int8, 0)
allClasses := cu.classes.GetAllClasses()
for classID, displayName := range allClasses {
if strings.Contains(strings.ToLower(displayName), pattern) {
result = append(result, classID)
}
}
return result
}
// ValidateClassTransition checks if a class change is allowed
func (cu *ClassUtils) ValidateClassTransition(fromClassID, toClassID int8) (bool, string) {
if !cu.classes.IsValidClassID(fromClassID) {
return false, "Invalid source class"
}
if !cu.classes.IsValidClassID(toClassID) {
return false, "Invalid target class"
}
if fromClassID == toClassID {
return false, "Cannot change to the same class"
}
// Basic progression validation - can only advance, not go backward
fromProgression := cu.GetClassProgression(fromClassID)
toProgression := cu.GetClassProgression(toClassID)
// Check if the target class is a valid advancement
if len(toProgression) <= len(fromProgression) {
return false, "Cannot regress to a lower tier class"
}
// Check if the progressions are compatible (share the same base path)
for i := 0; i < len(fromProgression); i++ {
if i >= len(toProgression) || fromProgression[i] != toProgression[i] {
return false, "Incompatible class progression paths"
}
}
return true, ""
}
// GetClassAliases returns common aliases for a class
func (cu *ClassUtils) GetClassAliases(classID int8) []string {
aliases := make([]string, 0)
switch classID {
case ClassShadowknight:
aliases = append(aliases, "SK", "Shadow Knight", "Dark Knight")
case ClassSwashbuckler:
aliases = append(aliases, "Swash", "Swashy")
case ClassTroubador:
aliases = append(aliases, "Troub", "Troubadour")
case ClassIllusionist:
aliases = append(aliases, "Illy", "Illusion")
case ClassConjuror:
aliases = append(aliases, "Conj", "Conjurer")
case ClassNecromancer:
aliases = append(aliases, "Necro", "Nec")
case ClassBeastlord:
aliases = append(aliases, "BL", "Beast Lord")
case ClassWeaponsmith:
aliases = append(aliases, "WS", "Weapon Smith")
}
// Always include the official names
aliases = append(aliases, cu.classes.GetClassName(classID))
aliases = append(aliases, cu.classes.GetClassNameCase(classID))
return aliases
}
// GetClassStatistics returns statistics about the class system
func (cu *ClassUtils) GetClassStatistics() map[string]interface{} {
stats := make(map[string]interface{})
allClasses := cu.classes.GetAllClasses()
stats["total_classes"] = len(allClasses)
adventureCount := 0
tradeskillCount := 0
specialCount := 0
for classID := range allClasses {
switch cu.classes.GetClassType(classID) {
case ClassTypeAdventure:
adventureCount++
case ClassTypeTradeskill:
tradeskillCount++
default:
specialCount++
}
}
stats["adventure_classes"] = adventureCount
stats["tradeskill_classes"] = tradeskillCount
stats["special_classes"] = specialCount
// Base class distribution
baseClassDistribution := make(map[string][]string)
for classID, displayName := range allClasses {
if cu.classes.IsAdventureClass(classID) {
baseClassID := cu.classes.GetBaseClass(classID)
baseClassName := cu.classes.GetClassNameCase(baseClassID)
baseClassDistribution[baseClassName] = append(baseClassDistribution[baseClassName], displayName)
}
}
stats["base_class_distribution"] = baseClassDistribution
return stats
}
// FormatClassList returns a formatted string of class names
func (cu *ClassUtils) FormatClassList(classIDs []int8, separator string) string {
if len(classIDs) == 0 {
return ""
}
names := make([]string, len(classIDs))
for i, classID := range classIDs {
names[i] = cu.classes.GetClassNameCase(classID)
}
return strings.Join(names, separator)
}
// GetEQClassName returns the EQ-style class name for a given class and level
// This is a placeholder for the original C++ GetEQClassName functionality
func (cu *ClassUtils) GetEQClassName(classID int8, level int8) string {
// TODO: Implement level-based class names when level system is available
// For now, just return the display name
return cu.classes.GetClassNameCase(classID)
}
// GetStartingClass returns the appropriate starting class for character creation
func (cu *ClassUtils) GetStartingClass() int8 {
return ClassCommoner
}
// IsBaseClass checks if a class is a base class (Fighter, Priest, Mage, Scout)
func (cu *ClassUtils) IsBaseClass(classID int8) bool {
return classID == ClassFighter || classID == ClassPriest || classID == ClassMage || classID == ClassScout
}
// IsSecondaryBaseClass checks if a class is a secondary base class
func (cu *ClassUtils) IsSecondaryBaseClass(classID int8) bool {
// Check if any class has this as their secondary base
allClasses := cu.classes.GetAllClasses()
for checkClassID := range allClasses {
if cu.classes.GetSecondaryBaseClass(checkClassID) == classID && checkClassID != classID {
return true
}
}
return false
}

View File

@ -0,0 +1,261 @@
package common
import (
"fmt"
"strings"
"sync"
)
// Variable represents a configuration variable with name, value, and optional comment
type Variable struct {
name string
value string
comment string
}
// NewVariable creates a new variable
func NewVariable(name, value, comment string) *Variable {
return &Variable{
name: name,
value: value,
comment: comment,
}
}
// GetName returns the variable name
func (v *Variable) GetName() string {
return v.name
}
// GetValue returns the variable value
func (v *Variable) GetValue() string {
return v.value
}
// GetComment returns the variable comment
func (v *Variable) GetComment() string {
return v.comment
}
// GetNameValuePair returns the name and value as a single string
func (v *Variable) GetNameValuePair() string {
return fmt.Sprintf("%s %s", v.name, v.value)
}
// SetValue updates the variable value
func (v *Variable) SetValue(value string) {
v.value = value
}
// Variables manages a collection of configuration variables
type Variables struct {
variables map[string]*Variable
mutex sync.RWMutex
}
// NewVariables creates a new variables manager
func NewVariables() *Variables {
return &Variables{
variables: make(map[string]*Variable),
}
}
// AddVariable adds a variable to the collection
func (v *Variables) AddVariable(variable *Variable) {
v.mutex.Lock()
defer v.mutex.Unlock()
v.variables[variable.name] = variable
}
// FindVariable finds a variable by exact name
func (v *Variables) FindVariable(name string) *Variable {
v.mutex.RLock()
defer v.mutex.RUnlock()
return v.variables[name]
}
// GetVariable is an alias for FindVariable for convenience
func (v *Variables) GetVariable(name string) *Variable {
return v.FindVariable(name)
}
// GetVariables returns all variables that contain the partial name
func (v *Variables) GetVariables(partialName string) []*Variable {
v.mutex.RLock()
defer v.mutex.RUnlock()
results := make([]*Variable, 0)
partialLower := strings.ToLower(partialName)
for name, variable := range v.variables {
if strings.Contains(strings.ToLower(name), partialLower) {
results = append(results, variable)
}
}
return results
}
// GetAllVariables returns all variables in the collection
func (v *Variables) GetAllVariables() map[string]*Variable {
v.mutex.RLock()
defer v.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[string]*Variable)
for name, variable := range v.variables {
result[name] = variable
}
return result
}
// GetVariableNames returns all variable names
func (v *Variables) GetVariableNames() []string {
v.mutex.RLock()
defer v.mutex.RUnlock()
names := make([]string, 0, len(v.variables))
for name := range v.variables {
names = append(names, name)
}
return names
}
// SetVariable sets or updates a variable value
func (v *Variables) SetVariable(name, value string) {
v.mutex.Lock()
defer v.mutex.Unlock()
if variable, exists := v.variables[name]; exists {
variable.SetValue(value)
} else {
// Create new variable if it doesn't exist
v.variables[name] = NewVariable(name, value, "")
}
}
// DeleteVariable removes a variable by name
func (v *Variables) DeleteVariable(name string) bool {
v.mutex.Lock()
defer v.mutex.Unlock()
if _, exists := v.variables[name]; exists {
delete(v.variables, name)
return true
}
return false
}
// ClearVariables removes all variables
func (v *Variables) ClearVariables() {
v.mutex.Lock()
defer v.mutex.Unlock()
v.variables = make(map[string]*Variable)
}
// Count returns the number of variables
func (v *Variables) Count() int {
v.mutex.RLock()
defer v.mutex.RUnlock()
return len(v.variables)
}
// HasVariable checks if a variable exists
func (v *Variables) HasVariable(name string) bool {
v.mutex.RLock()
defer v.mutex.RUnlock()
_, exists := v.variables[name]
return exists
}
// GetVariableValue returns just the value of a variable, or empty string if not found
func (v *Variables) GetVariableValue(name string) string {
if variable := v.FindVariable(name); variable != nil {
return variable.GetValue()
}
return ""
}
// GetVariableValueWithDefault returns the value of a variable, or a default if not found
func (v *Variables) GetVariableValueWithDefault(name, defaultValue string) string {
if variable := v.FindVariable(name); variable != nil {
return variable.GetValue()
}
return defaultValue
}
// GetVariableAsInt attempts to parse a variable value as an integer
func (v *Variables) GetVariableAsInt(name string, defaultValue int) int {
if variable := v.FindVariable(name); variable != nil {
var intValue int
if _, err := fmt.Sscanf(variable.GetValue(), "%d", &intValue); err == nil {
return intValue
}
}
return defaultValue
}
// GetVariableAsFloat attempts to parse a variable value as a float
func (v *Variables) GetVariableAsFloat(name string, defaultValue float64) float64 {
if variable := v.FindVariable(name); variable != nil {
var floatValue float64
if _, err := fmt.Sscanf(variable.GetValue(), "%f", &floatValue); err == nil {
return floatValue
}
}
return defaultValue
}
// GetVariableAsBool attempts to parse a variable value as a boolean
func (v *Variables) GetVariableAsBool(name string, defaultValue bool) bool {
if variable := v.FindVariable(name); variable != nil {
value := strings.ToLower(variable.GetValue())
switch value {
case "true", "1", "yes", "on", "enabled":
return true
case "false", "0", "no", "off", "disabled":
return false
}
}
return defaultValue
}
// Clone creates a deep copy of the variables collection
func (v *Variables) Clone() *Variables {
v.mutex.RLock()
defer v.mutex.RUnlock()
newVars := NewVariables()
for name, variable := range v.variables {
newVar := NewVariable(variable.name, variable.value, variable.comment)
newVars.variables[name] = newVar
}
return newVars
}
// Merge merges another Variables collection into this one
// If overwrite is true, existing variables will be overwritten
func (v *Variables) Merge(other *Variables, overwrite bool) {
if other == nil {
return
}
v.mutex.Lock()
defer v.mutex.Unlock()
otherVars := other.GetAllVariables()
for name, variable := range otherVars {
if _, exists := v.variables[name]; !exists || overwrite {
v.variables[name] = NewVariable(variable.name, variable.value, variable.comment)
}
}
}

View File

@ -0,0 +1,378 @@
package common
import (
"fmt"
"strings"
"sync"
)
// VisualState represents a visual animation state
type VisualState struct {
id int
name string
}
// NewVisualState creates a new visual state
func NewVisualState(id int, name string) *VisualState {
return &VisualState{
id: id,
name: name,
}
}
// GetID returns the visual state ID
func (vs *VisualState) GetID() int {
return vs.id
}
// GetName returns the visual state name
func (vs *VisualState) GetName() string {
return vs.name
}
// Emote represents an emote with visual state and messages
type Emote struct {
name string
visualState int32
message string
targetedMessage string
}
// NewEmote creates a new emote
func NewEmote(name string, visualState int32, message, targetedMessage string) *Emote {
return &Emote{
name: name,
visualState: visualState,
message: message,
targetedMessage: targetedMessage,
}
}
// GetName returns the emote name
func (e *Emote) GetName() string {
return e.name
}
// GetVisualState returns the visual state ID
func (e *Emote) GetVisualState() int32 {
return e.visualState
}
// GetMessage returns the emote message
func (e *Emote) GetMessage() string {
return e.message
}
// GetTargetedMessage returns the targeted emote message
func (e *Emote) GetTargetedMessage() string {
return e.targetedMessage
}
// VersionRange represents a min/max version range
type VersionRange struct {
minVersion int32
maxVersion int32
}
// NewVersionRange creates a new version range
func NewVersionRange(min, max int32) *VersionRange {
return &VersionRange{
minVersion: min,
maxVersion: max,
}
}
// GetMinVersion returns the minimum version
func (vr *VersionRange) GetMinVersion() int32 {
return vr.minVersion
}
// GetMaxVersion returns the maximum version
func (vr *VersionRange) GetMaxVersion() int32 {
return vr.maxVersion
}
// InRange checks if a version is within this range
func (vr *VersionRange) InRange(version int32) bool {
return version >= vr.minVersion && (vr.maxVersion == 0 || version <= vr.maxVersion)
}
// EmoteVersionRange manages emotes across different client versions
type EmoteVersionRange struct {
name string
versionMap map[*VersionRange]*Emote
mutex sync.RWMutex
}
// NewEmoteVersionRange creates a new emote version range
func NewEmoteVersionRange(name string) *EmoteVersionRange {
return &EmoteVersionRange{
name: name,
versionMap: make(map[*VersionRange]*Emote),
}
}
// GetName returns the emote range name
func (evr *EmoteVersionRange) GetName() string {
return evr.name
}
// AddVersionRange adds an emote for a specific version range
func (evr *EmoteVersionRange) AddVersionRange(minVersion, maxVersion int32, name string, visualState int32, message, targetedMessage string) error {
evr.mutex.Lock()
defer evr.mutex.Unlock()
// Check for duplicate ranges
for vr := range evr.versionMap {
if evr.rangesOverlap(vr, minVersion, maxVersion) {
return fmt.Errorf("duplicate emote mapping of %s with range min %d max %d, existing found with range min %d max %d",
evr.name, minVersion, maxVersion, vr.minVersion, vr.maxVersion)
}
}
vr := NewVersionRange(minVersion, maxVersion)
emote := NewEmote(name, visualState, message, targetedMessage)
evr.versionMap[vr] = emote
return nil
}
// rangesOverlap checks if two version ranges overlap
func (evr *EmoteVersionRange) rangesOverlap(existing *VersionRange, minVersion, maxVersion int32) bool {
// Check various overlap conditions
if existing.minVersion <= minVersion && maxVersion <= existing.maxVersion {
return true
}
if existing.minVersion <= minVersion && existing.maxVersion == 0 {
return true
}
if existing.minVersion == 0 && maxVersion <= existing.maxVersion {
return true
}
return false
}
// FindEmoteByVersion finds the emote for a specific client version
func (evr *EmoteVersionRange) FindEmoteByVersion(version int32) *Emote {
evr.mutex.RLock()
defer evr.mutex.RUnlock()
for vr, emote := range evr.versionMap {
if vr.InRange(version) {
return emote
}
}
return nil
}
// VisualStates manages all visual states, emotes, and spell visuals
type VisualStates struct {
visualStateMap map[string]*VisualState
emoteMap map[string]*EmoteVersionRange
emoteMapID map[int32]*EmoteVersionRange
spellMap map[string]*EmoteVersionRange
spellMapID map[int32]*EmoteVersionRange
mutex sync.RWMutex
}
// NewVisualStates creates a new visual states manager
func NewVisualStates() *VisualStates {
return &VisualStates{
visualStateMap: make(map[string]*VisualState),
emoteMap: make(map[string]*EmoteVersionRange),
emoteMapID: make(map[int32]*EmoteVersionRange),
spellMap: make(map[string]*EmoteVersionRange),
spellMapID: make(map[int32]*EmoteVersionRange),
}
}
// InsertVisualState adds a visual state
func (vs *VisualStates) InsertVisualState(state *VisualState) {
vs.mutex.Lock()
defer vs.mutex.Unlock()
vs.visualStateMap[state.name] = state
}
// FindVisualState finds a visual state by name
func (vs *VisualStates) FindVisualState(name string) *VisualState {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
return vs.visualStateMap[name]
}
// InsertEmoteRange adds an emote range
func (vs *VisualStates) InsertEmoteRange(emote *EmoteVersionRange, animationID int32) {
vs.mutex.Lock()
defer vs.mutex.Unlock()
vs.emoteMap[emote.name] = emote
vs.emoteMapID[animationID] = emote
}
// FindEmoteRange finds an emote range by name
func (vs *VisualStates) FindEmoteRange(name string) *EmoteVersionRange {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
return vs.emoteMap[name]
}
// FindEmote finds an emote by name and version
func (vs *VisualStates) FindEmote(name string, version int32) *Emote {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
if emoteRange, exists := vs.emoteMap[name]; exists {
return emoteRange.FindEmoteByVersion(version)
}
return nil
}
// FindEmoteRangeByID finds an emote range by ID
func (vs *VisualStates) FindEmoteRangeByID(id int32) *EmoteVersionRange {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
return vs.emoteMapID[id]
}
// FindEmoteByID finds an emote by visual ID and version
func (vs *VisualStates) FindEmoteByID(visualID int32, version int32) *Emote {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
if emoteRange, exists := vs.emoteMapID[visualID]; exists {
return emoteRange.FindEmoteByVersion(version)
}
return nil
}
// InsertSpellVisualRange adds a spell visual range
func (vs *VisualStates) InsertSpellVisualRange(emote *EmoteVersionRange, spellVisualID int32) {
vs.mutex.Lock()
defer vs.mutex.Unlock()
vs.spellMap[emote.name] = emote
vs.spellMapID[spellVisualID] = emote
}
// FindSpellVisualRange finds a spell visual range by name
func (vs *VisualStates) FindSpellVisualRange(name string) *EmoteVersionRange {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
return vs.spellMap[name]
}
// FindSpellVisualRangeByID finds a spell visual range by ID
func (vs *VisualStates) FindSpellVisualRangeByID(id int32) *EmoteVersionRange {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
return vs.spellMapID[id]
}
// FindSpellVisual finds a spell visual by name and version
func (vs *VisualStates) FindSpellVisual(name string, version int32) *Emote {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
if spellRange, exists := vs.spellMap[name]; exists {
return spellRange.FindEmoteByVersion(version)
}
return nil
}
// FindSpellVisualByID finds a spell visual by ID and version
func (vs *VisualStates) FindSpellVisualByID(visualID int32, version int32) *Emote {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
if spellRange, exists := vs.spellMapID[visualID]; exists {
return spellRange.FindEmoteByVersion(version)
}
return nil
}
// ClearVisualStates clears all visual states
func (vs *VisualStates) ClearVisualStates() {
vs.mutex.Lock()
defer vs.mutex.Unlock()
vs.visualStateMap = make(map[string]*VisualState)
}
// ClearEmotes clears all emotes
func (vs *VisualStates) ClearEmotes() {
vs.mutex.Lock()
defer vs.mutex.Unlock()
vs.emoteMap = make(map[string]*EmoteVersionRange)
vs.emoteMapID = make(map[int32]*EmoteVersionRange)
}
// ClearSpellVisuals clears all spell visuals
func (vs *VisualStates) ClearSpellVisuals() {
vs.mutex.Lock()
defer vs.mutex.Unlock()
vs.spellMap = make(map[string]*EmoteVersionRange)
vs.spellMapID = make(map[int32]*EmoteVersionRange)
}
// Reset clears all data
func (vs *VisualStates) Reset() {
vs.ClearVisualStates()
vs.ClearEmotes()
vs.ClearSpellVisuals()
}
// GetEmoteList returns a list of all emote names
func (vs *VisualStates) GetEmoteList() []string {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
names := make([]string, 0, len(vs.emoteMap))
for name := range vs.emoteMap {
names = append(names, name)
}
return names
}
// GetVisualStateList returns a list of all visual state names
func (vs *VisualStates) GetVisualStateList() []string {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
names := make([]string, 0, len(vs.visualStateMap))
for name := range vs.visualStateMap {
names = append(names, name)
}
return names
}
// FindEmoteByPartialName finds emotes that contain the partial name
func (vs *VisualStates) FindEmoteByPartialName(partial string) []*EmoteVersionRange {
vs.mutex.RLock()
defer vs.mutex.RUnlock()
partial = strings.ToLower(partial)
results := make([]*EmoteVersionRange, 0)
for name, emote := range vs.emoteMap {
if strings.Contains(strings.ToLower(name), partial) {
results = append(results, emote)
}
}
return results
}

View File

@ -659,6 +659,23 @@ func (e *Entity) ProcessEffects() {
// TODO: Handle effect-based stat changes // TODO: Handle effect-based stat changes
} }
// Class system integration adapters
// GetClass returns the entity's primary class (ClassAware interface compatibility)
func (e *Entity) GetClass() int8 {
return e.infoStruct.GetClass1()
}
// SetClass sets the entity's primary class (ClassAware interface compatibility)
func (e *Entity) SetClass(classID int8) {
e.infoStruct.SetClass1(classID)
}
// GetLevel returns the entity's level (EntityWithClass interface compatibility)
func (e *Entity) GetLevel() int8 {
return int8(e.infoStruct.GetLevel())
}
// TODO: Additional methods to implement: // TODO: Additional methods to implement:
// - Combat calculation methods (damage, healing, etc.) // - Combat calculation methods (damage, healing, etc.)
// - Equipment bonus application methods // - Equipment bonus application methods

View File

@ -0,0 +1,46 @@
package factions
// Faction value constants
const (
// Maximum and minimum faction values
MaxFactionValue = 50000
MinFactionValue = -50000
// Special faction ID ranges
SpecialFactionIDMax = 10 // Faction IDs <= 10 are special (not real factions)
// Faction consideration (con) ranges
MinCon = -4 // Hostile
MaxCon = 4 // Ally
// Con value thresholds
ConNeutralMin = -9999
ConNeutralMax = 9999
ConAllyMin = 40000
ConHostileMax = -40000
// Con calculation multiplier
ConMultiplier = 10000
ConRemainder = 9999
// Percentage calculation constants
PercentMultiplier = 100
PercentNeutralOffset = 10000
PercentNeutralDivisor = 20000
)
// Attack threshold - factions with con <= this value should attack
const AttackThreshold = -4
// Default faction consideration values
const (
ConKOS = -4 // Kill on sight
ConThreat = -3 // Threatening
ConDubious = -2 // Dubiously
ConAppre = -1 // Apprehensive
ConIndiff = 0 // Indifferent
ConAmiable = 1 // Amiable
ConKindly = 2 // Kindly
ConWarmly = 3 // Warmly
ConAlly = 4 // Ally
)

View File

@ -0,0 +1,371 @@
package factions
import (
"fmt"
"sync"
)
// Database interface for faction persistence
type Database interface {
LoadAllFactions() ([]*Faction, error)
SaveFaction(faction *Faction) error
DeleteFaction(factionID int32) error
LoadHostileFactionRelations() ([]*FactionRelation, error)
LoadFriendlyFactionRelations() ([]*FactionRelation, error)
SaveFactionRelation(relation *FactionRelation) error
DeleteFactionRelation(factionID, relatedFactionID int32, isHostile bool) error
}
// Logger interface for faction logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// FactionRelation represents a relationship between two factions
type FactionRelation struct {
FactionID int32 // Primary faction ID
HostileFactionID int32 // Hostile faction ID (if this is a hostile relation)
FriendlyFactionID int32 // Friendly faction ID (if this is a friendly relation)
}
// Client interface for faction-related client operations
type Client interface {
GetVersion() int16
SendFactionUpdate(factionData []byte) error
GetCharacterID() int32
}
// Player interface for faction-related player operations
type Player interface {
GetFactionSystem() *PlayerFaction
GetCharacterID() int32
SendMessage(message string)
}
// FactionAware interface for entities that interact with factions
type FactionAware interface {
GetFactionID() int32
SetFactionID(factionID int32)
GetFactionStanding(playerFaction *PlayerFaction) int8
ShouldAttackPlayer(playerFaction *PlayerFaction) bool
}
// FactionProvider interface for systems that provide faction information
type FactionProvider interface {
GetMasterFactionList() *MasterFactionList
GetFaction(factionID int32) *Faction
GetFactionByName(name string) *Faction
CreatePlayerFaction() *PlayerFaction
}
// EntityFactionAdapter provides faction functionality for entities
type EntityFactionAdapter struct {
entity Entity
factionID int32
manager *Manager
logger Logger
mutex sync.RWMutex
}
// Entity interface for things that can have faction affiliations
type Entity interface {
GetID() int32
GetName() string
GetDatabaseID() int32
}
// NewEntityFactionAdapter creates a new entity faction adapter
func NewEntityFactionAdapter(entity Entity, manager *Manager, logger Logger) *EntityFactionAdapter {
return &EntityFactionAdapter{
entity: entity,
factionID: 0,
manager: manager,
logger: logger,
}
}
// GetFactionID returns the entity's faction ID
func (efa *EntityFactionAdapter) GetFactionID() int32 {
efa.mutex.RLock()
defer efa.mutex.RUnlock()
return efa.factionID
}
// SetFactionID sets the entity's faction ID
func (efa *EntityFactionAdapter) SetFactionID(factionID int32) {
efa.mutex.Lock()
defer efa.mutex.Unlock()
efa.factionID = factionID
if efa.logger != nil {
efa.logger.LogDebug("Entity %d (%s): Set faction ID to %d",
efa.entity.GetID(), efa.entity.GetName(), factionID)
}
}
// GetFaction returns the entity's faction object
func (efa *EntityFactionAdapter) GetFaction() *Faction {
factionID := efa.GetFactionID()
if factionID == 0 {
return nil
}
if efa.manager == nil {
if efa.logger != nil {
efa.logger.LogError("Entity %d (%s): No faction manager available",
efa.entity.GetID(), efa.entity.GetName())
}
return nil
}
return efa.manager.GetFaction(factionID)
}
// GetFactionStanding returns the consideration level with a player
func (efa *EntityFactionAdapter) GetFactionStanding(playerFaction *PlayerFaction) int8 {
factionID := efa.GetFactionID()
if factionID == 0 || playerFaction == nil {
return ConIndiff // Indifferent if no faction or player faction
}
return playerFaction.GetCon(factionID)
}
// ShouldAttackPlayer returns true if the entity should attack the player based on faction
func (efa *EntityFactionAdapter) ShouldAttackPlayer(playerFaction *PlayerFaction) bool {
factionID := efa.GetFactionID()
if factionID == 0 || playerFaction == nil {
return false // Don't attack if no faction
}
return playerFaction.ShouldAttack(factionID)
}
// GetFactionName returns the name of the entity's faction
func (efa *EntityFactionAdapter) GetFactionName() string {
faction := efa.GetFaction()
if faction == nil {
return ""
}
return faction.Name
}
// IsHostileToFaction returns true if this entity's faction is hostile to another faction
func (efa *EntityFactionAdapter) IsHostileToFaction(otherFactionID int32) bool {
factionID := efa.GetFactionID()
if factionID == 0 || efa.manager == nil {
return false
}
hostileFactions := efa.manager.GetMasterFactionList().GetHostileFactions(factionID)
for _, hostileID := range hostileFactions {
if hostileID == otherFactionID {
return true
}
}
return false
}
// IsFriendlyToFaction returns true if this entity's faction is friendly to another faction
func (efa *EntityFactionAdapter) IsFriendlyToFaction(otherFactionID int32) bool {
factionID := efa.GetFactionID()
if factionID == 0 || efa.manager == nil {
return false
}
friendlyFactions := efa.manager.GetMasterFactionList().GetFriendlyFactions(factionID)
for _, friendlyID := range friendlyFactions {
if friendlyID == otherFactionID {
return true
}
}
return false
}
// ValidateFaction validates that the entity's faction exists and is valid
func (efa *EntityFactionAdapter) ValidateFaction() error {
factionID := efa.GetFactionID()
if factionID == 0 {
return nil // No faction is valid
}
faction := efa.GetFaction()
if faction == nil {
return fmt.Errorf("faction ID %d not found", factionID)
}
if !faction.IsValid() {
return fmt.Errorf("faction ID %d is invalid", factionID)
}
return nil
}
// PlayerFactionManager handles faction interactions for a player
type PlayerFactionManager struct {
playerFaction *PlayerFaction
manager *Manager
player Player
logger Logger
mutex sync.RWMutex
}
// NewPlayerFactionManager creates a new player faction manager
func NewPlayerFactionManager(player Player, manager *Manager, logger Logger) *PlayerFactionManager {
return &PlayerFactionManager{
playerFaction: manager.CreatePlayerFaction(),
manager: manager,
player: player,
logger: logger,
}
}
// GetPlayerFaction returns the player's faction system
func (pfm *PlayerFactionManager) GetPlayerFaction() *PlayerFaction {
return pfm.playerFaction
}
// IncreaseFaction increases a faction and records statistics
func (pfm *PlayerFactionManager) IncreaseFaction(factionID int32, amount int32) bool {
result := pfm.playerFaction.IncreaseFaction(factionID, amount)
if result {
pfm.manager.RecordFactionIncrease(factionID)
if pfm.logger != nil {
pfm.logger.LogDebug("Player %d: Increased faction %d by %d",
pfm.player.GetCharacterID(), factionID, amount)
}
}
return result
}
// DecreaseFaction decreases a faction and records statistics
func (pfm *PlayerFactionManager) DecreaseFaction(factionID int32, amount int32) bool {
result := pfm.playerFaction.DecreaseFaction(factionID, amount)
if result {
pfm.manager.RecordFactionDecrease(factionID)
if pfm.logger != nil {
pfm.logger.LogDebug("Player %d: Decreased faction %d by %d",
pfm.player.GetCharacterID(), factionID, amount)
}
}
return result
}
// SetFactionValue sets a faction to a specific value
func (pfm *PlayerFactionManager) SetFactionValue(factionID int32, value int32) bool {
result := pfm.playerFaction.SetFactionValue(factionID, value)
if pfm.logger != nil {
pfm.logger.LogDebug("Player %d: Set faction %d to %d",
pfm.player.GetCharacterID(), factionID, value)
}
return result
}
// SendFactionUpdates sends pending faction updates to the client
func (pfm *PlayerFactionManager) SendFactionUpdates(client Client) error {
if client == nil {
return fmt.Errorf("client is nil")
}
if !pfm.playerFaction.HasPendingUpdates() {
return nil // No updates needed
}
packet, err := pfm.playerFaction.FactionUpdate(client.GetVersion())
if err != nil {
return fmt.Errorf("failed to build faction update packet: %w", err)
}
if packet != nil {
if err := client.SendFactionUpdate(packet); err != nil {
return fmt.Errorf("failed to send faction update: %w", err)
}
if pfm.logger != nil {
pfm.logger.LogDebug("Player %d: Sent faction updates to client",
pfm.player.GetCharacterID())
}
}
return nil
}
// GetFactionStanding returns the player's standing with a faction
func (pfm *PlayerFactionManager) GetFactionStanding(factionID int32) int8 {
return pfm.playerFaction.GetCon(factionID)
}
// GetFactionValue returns the player's value with a faction
func (pfm *PlayerFactionManager) GetFactionValue(factionID int32) int32 {
return pfm.playerFaction.GetFactionValue(factionID)
}
// ShouldAttackFaction returns true if the player should attack entities of a faction
func (pfm *PlayerFactionManager) ShouldAttackFaction(factionID int32) bool {
return pfm.playerFaction.ShouldAttack(factionID)
}
// LoadPlayerFactions loads faction data from database
func (pfm *PlayerFactionManager) LoadPlayerFactions(database Database) error {
if database == nil {
return fmt.Errorf("database is nil")
}
// TODO: Implement database loading when database system is integrated
// factionData, err := database.LoadPlayerFactions(pfm.player.GetCharacterID())
// if err != nil {
// return fmt.Errorf("failed to load player factions: %w", err)
// }
//
// for factionID, value := range factionData {
// pfm.playerFaction.SetFactionValue(factionID, value)
// }
if pfm.logger != nil {
pfm.logger.LogInfo("Player %d: Loaded faction data from database",
pfm.player.GetCharacterID())
}
return nil
}
// SavePlayerFactions saves faction data to database
func (pfm *PlayerFactionManager) SavePlayerFactions(database Database) error {
if database == nil {
return fmt.Errorf("database is nil")
}
factionValues := pfm.playerFaction.GetFactionValues()
// TODO: Implement database saving when database system is integrated
// for factionID, value := range factionValues {
// if err := database.SavePlayerFaction(pfm.player.GetCharacterID(), factionID, value); err != nil {
// return fmt.Errorf("failed to save faction %d: %w", factionID, err)
// }
// }
if pfm.logger != nil {
pfm.logger.LogInfo("Player %d: Saved %d faction values to database",
pfm.player.GetCharacterID(), len(factionValues))
}
return nil
}

View File

@ -0,0 +1,488 @@
package factions
import (
"fmt"
"sync"
)
// Manager provides high-level management of the faction system
type Manager struct {
masterFactionList *MasterFactionList
database Database
logger Logger
mutex sync.RWMutex
// Statistics
totalFactionChanges int64
factionIncreases int64
factionDecreases int64
factionLookups int64
playersWithFactions int64
changesByFaction map[int32]int64 // Faction ID -> total changes
}
// NewManager creates a new faction manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
masterFactionList: NewMasterFactionList(),
database: database,
logger: logger,
changesByFaction: make(map[int32]int64),
}
}
// Initialize loads factions from database
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing faction manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty faction list")
}
return nil
}
// Load factions
factions, err := m.database.LoadAllFactions()
if err != nil {
return fmt.Errorf("failed to load factions from database: %w", err)
}
for _, faction := range factions {
if err := m.masterFactionList.AddFaction(faction); err != nil {
if m.logger != nil {
m.logger.LogError("Failed to add faction %d (%s): %v", faction.ID, faction.Name, err)
}
}
}
// Load faction relationships
if err := m.loadFactionRelationships(); err != nil {
if m.logger != nil {
m.logger.LogWarning("Failed to load faction relationships: %v", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d factions from database", len(factions))
}
return nil
}
// loadFactionRelationships loads hostile and friendly faction relationships
func (m *Manager) loadFactionRelationships() error {
if m.database == nil {
return nil
}
// Load hostile relationships
hostileRelations, err := m.database.LoadHostileFactionRelations()
if err != nil {
return fmt.Errorf("failed to load hostile faction relations: %w", err)
}
for _, relation := range hostileRelations {
m.masterFactionList.AddHostileFaction(relation.FactionID, relation.HostileFactionID)
}
// Load friendly relationships
friendlyRelations, err := m.database.LoadFriendlyFactionRelations()
if err != nil {
return fmt.Errorf("failed to load friendly faction relations: %w", err)
}
for _, relation := range friendlyRelations {
m.masterFactionList.AddFriendlyFaction(relation.FactionID, relation.FriendlyFactionID)
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d hostile and %d friendly faction relationships",
len(hostileRelations), len(friendlyRelations))
}
return nil
}
// GetMasterFactionList returns the master faction list
func (m *Manager) GetMasterFactionList() *MasterFactionList {
return m.masterFactionList
}
// CreatePlayerFaction creates a new player faction system
func (m *Manager) CreatePlayerFaction() *PlayerFaction {
m.mutex.Lock()
m.playersWithFactions++
m.mutex.Unlock()
return NewPlayerFaction(m.masterFactionList)
}
// GetFaction returns a faction by ID
func (m *Manager) GetFaction(factionID int32) *Faction {
m.mutex.Lock()
m.factionLookups++
m.mutex.Unlock()
return m.masterFactionList.GetFaction(factionID)
}
// GetFactionByName returns a faction by name
func (m *Manager) GetFactionByName(name string) *Faction {
m.mutex.Lock()
m.factionLookups++
m.mutex.Unlock()
return m.masterFactionList.GetFactionByName(name)
}
// AddFaction adds a new faction
func (m *Manager) AddFaction(faction *Faction) error {
if faction == nil {
return fmt.Errorf("faction cannot be nil")
}
// Add to master list
if err := m.masterFactionList.AddFaction(faction); err != nil {
return fmt.Errorf("failed to add faction to master list: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveFaction(faction); err != nil {
// Remove from master list if database save failed
m.masterFactionList.RemoveFaction(faction.ID)
return fmt.Errorf("failed to save faction to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Added faction %d: %s (%s)", faction.ID, faction.Name, faction.Type)
}
return nil
}
// UpdateFaction updates an existing faction
func (m *Manager) UpdateFaction(faction *Faction) error {
if faction == nil {
return fmt.Errorf("faction cannot be nil")
}
// Update in master list
if err := m.masterFactionList.UpdateFaction(faction); err != nil {
return fmt.Errorf("failed to update faction in master list: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveFaction(faction); err != nil {
return fmt.Errorf("failed to save faction to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Updated faction %d: %s", faction.ID, faction.Name)
}
return nil
}
// RemoveFaction removes a faction
func (m *Manager) RemoveFaction(factionID int32) error {
// Check if faction exists
if !m.masterFactionList.HasFaction(factionID) {
return fmt.Errorf("faction with ID %d does not exist", factionID)
}
// Remove from database first if available
if m.database != nil {
if err := m.database.DeleteFaction(factionID); err != nil {
return fmt.Errorf("failed to delete faction from database: %w", err)
}
}
// Remove from master list
if !m.masterFactionList.RemoveFaction(factionID) {
return fmt.Errorf("failed to remove faction from master list")
}
if m.logger != nil {
m.logger.LogInfo("Removed faction %d", factionID)
}
return nil
}
// RecordFactionIncrease records a faction increase for statistics
func (m *Manager) RecordFactionIncrease(factionID int32) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalFactionChanges++
m.factionIncreases++
m.changesByFaction[factionID]++
}
// RecordFactionDecrease records a faction decrease for statistics
func (m *Manager) RecordFactionDecrease(factionID int32) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalFactionChanges++
m.factionDecreases++
m.changesByFaction[factionID]++
}
// GetStatistics returns faction system statistics
func (m *Manager) GetStatistics() map[string]interface{} {
m.mutex.RLock()
defer m.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_factions"] = m.masterFactionList.GetFactionCount()
stats["total_faction_changes"] = m.totalFactionChanges
stats["faction_increases"] = m.factionIncreases
stats["faction_decreases"] = m.factionDecreases
stats["faction_lookups"] = m.factionLookups
stats["players_with_factions"] = m.playersWithFactions
// Copy changes by faction
changeStats := make(map[int32]int64)
for factionID, count := range m.changesByFaction {
changeStats[factionID] = count
}
stats["changes_by_faction"] = changeStats
return stats
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalFactionChanges = 0
m.factionIncreases = 0
m.factionDecreases = 0
m.factionLookups = 0
m.playersWithFactions = 0
m.changesByFaction = make(map[int32]int64)
}
// ValidateAllFactions validates all factions in the system
func (m *Manager) ValidateAllFactions() []string {
return m.masterFactionList.ValidateFactions()
}
// ReloadFromDatabase reloads all factions from database
func (m *Manager) ReloadFromDatabase() error {
if m.database == nil {
return fmt.Errorf("no database available")
}
// Clear current factions
m.masterFactionList.Clear()
// Reload from database
return m.Initialize()
}
// GetFactionCount returns the total number of factions
func (m *Manager) GetFactionCount() int32 {
return m.masterFactionList.GetFactionCount()
}
// ProcessCommand handles faction-related commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "list":
return m.handleListCommand(args)
case "info":
return m.handleInfoCommand(args)
case "reload":
return m.handleReloadCommand(args)
case "search":
return m.handleSearchCommand(args)
default:
return "", fmt.Errorf("unknown faction command: %s", command)
}
}
// handleStatsCommand shows faction system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Faction System Statistics:\n"
result += fmt.Sprintf("Total Factions: %d\n", stats["total_factions"])
result += fmt.Sprintf("Total Faction Changes: %d\n", stats["total_faction_changes"])
result += fmt.Sprintf("Faction Increases: %d\n", stats["faction_increases"])
result += fmt.Sprintf("Faction Decreases: %d\n", stats["faction_decreases"])
result += fmt.Sprintf("Faction Lookups: %d\n", stats["faction_lookups"])
result += fmt.Sprintf("Players with Factions: %d\n", stats["players_with_factions"])
return result, nil
}
// handleValidateCommand validates all factions
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateAllFactions()
if len(issues) == 0 {
return "All factions are valid.", nil
}
result := fmt.Sprintf("Found %d issues with factions:\n", len(issues))
for i, issue := range issues {
if i >= 10 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
// handleListCommand lists factions
func (m *Manager) handleListCommand(args []string) (string, error) {
factions := m.masterFactionList.GetAllFactions()
if len(factions) == 0 {
return "No factions loaded.", nil
}
result := fmt.Sprintf("Factions (%d):\n", len(factions))
count := 0
for _, faction := range factions {
if count >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s (%s)\n", faction.ID, faction.Name, faction.Type)
count++
}
return result, nil
}
// handleInfoCommand shows information about a specific faction
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("faction ID or name required")
}
var faction *Faction
// Try to parse as ID first
var factionID int32
if _, err := fmt.Sscanf(args[0], "%d", &factionID); err == nil {
faction = m.GetFaction(factionID)
} else {
// Try as name
faction = m.GetFactionByName(args[0])
}
if faction == nil {
return fmt.Sprintf("Faction '%s' not found.", args[0]), nil
}
result := fmt.Sprintf("Faction Information:\n")
result += fmt.Sprintf("ID: %d\n", faction.ID)
result += fmt.Sprintf("Name: %s\n", faction.Name)
result += fmt.Sprintf("Type: %s\n", faction.Type)
result += fmt.Sprintf("Description: %s\n", faction.Description)
result += fmt.Sprintf("Default Value: %d\n", faction.DefaultValue)
result += fmt.Sprintf("Positive Change: %d\n", faction.PositiveChange)
result += fmt.Sprintf("Negative Change: %d\n", faction.NegativeChange)
// Show relationships if any
hostiles := m.masterFactionList.GetHostileFactions(faction.ID)
if len(hostiles) > 0 {
result += fmt.Sprintf("Hostile Factions: %v\n", hostiles)
}
friendlies := m.masterFactionList.GetFriendlyFactions(faction.ID)
if len(friendlies) > 0 {
result += fmt.Sprintf("Friendly Factions: %v\n", friendlies)
}
return result, nil
}
// handleReloadCommand reloads factions from database
func (m *Manager) handleReloadCommand(args []string) (string, error) {
if err := m.ReloadFromDatabase(); err != nil {
return "", fmt.Errorf("failed to reload factions: %w", err)
}
count := m.GetFactionCount()
return fmt.Sprintf("Successfully reloaded %d factions from database.", count), nil
}
// handleSearchCommand searches for factions by name or type
func (m *Manager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search term required")
}
searchTerm := args[0]
factions := m.masterFactionList.GetAllFactions()
var results []*Faction
// Search by name or type
for _, faction := range factions {
if contains(faction.Name, searchTerm) || contains(faction.Type, searchTerm) {
results = append(results, faction)
}
}
if len(results) == 0 {
return fmt.Sprintf("No factions found matching '%s'.", searchTerm), nil
}
result := fmt.Sprintf("Found %d factions matching '%s':\n", len(results), searchTerm)
for i, faction := range results {
if i >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s (%s)\n", faction.ID, faction.Name, faction.Type)
}
return result, nil
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down faction manager...")
}
// Clear factions
m.masterFactionList.Clear()
}
// contains checks if a string contains a substring (case-sensitive)
func contains(str, substr string) bool {
if len(substr) == 0 {
return true
}
if len(str) < len(substr) {
return false
}
for i := 0; i <= len(str)-len(substr); i++ {
if str[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@ -0,0 +1,387 @@
package factions
import (
"fmt"
"sync"
)
// MasterFactionList manages all factions in the game
type MasterFactionList struct {
globalFactionList map[int32]*Faction // Factions by ID
factionNameList map[string]*Faction // Factions by name
hostileFactions map[int32][]int32 // Hostile faction relationships
friendlyFactions map[int32][]int32 // Friendly faction relationships
mutex sync.RWMutex // Thread safety
}
// NewMasterFactionList creates a new master faction list
func NewMasterFactionList() *MasterFactionList {
return &MasterFactionList{
globalFactionList: make(map[int32]*Faction),
factionNameList: make(map[string]*Faction),
hostileFactions: make(map[int32][]int32),
friendlyFactions: make(map[int32][]int32),
}
}
// Clear removes all factions and relationships
func (mfl *MasterFactionList) Clear() {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
// Clear all maps - Go's garbage collector will handle cleanup
mfl.globalFactionList = make(map[int32]*Faction)
mfl.factionNameList = make(map[string]*Faction)
mfl.hostileFactions = make(map[int32][]int32)
mfl.friendlyFactions = make(map[int32][]int32)
}
// GetDefaultFactionValue returns the default value for a faction
func (mfl *MasterFactionList) GetDefaultFactionValue(factionID int32) int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists && faction != nil {
return faction.DefaultValue
}
return 0
}
// GetFaction returns a faction by name
func (mfl *MasterFactionList) GetFactionByName(name string) *Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
return mfl.factionNameList[name]
}
// GetFaction returns a faction by ID
func (mfl *MasterFactionList) GetFaction(id int32) *Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[id]; exists {
return faction
}
return nil
}
// AddFaction adds a faction to the master list
func (mfl *MasterFactionList) AddFaction(faction *Faction) error {
if faction == nil {
return fmt.Errorf("faction cannot be nil")
}
if !faction.IsValid() {
return fmt.Errorf("faction is not valid")
}
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
mfl.globalFactionList[faction.ID] = faction
mfl.factionNameList[faction.Name] = faction
return nil
}
// GetIncreaseAmount returns the default increase amount for a faction
func (mfl *MasterFactionList) GetIncreaseAmount(factionID int32) int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists && faction != nil {
return int32(faction.PositiveChange)
}
return 0
}
// GetDecreaseAmount returns the default decrease amount for a faction
func (mfl *MasterFactionList) GetDecreaseAmount(factionID int32) int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists && faction != nil {
return int32(faction.NegativeChange)
}
return 0
}
// GetFactionCount returns the total number of factions
func (mfl *MasterFactionList) GetFactionCount() int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
return int32(len(mfl.globalFactionList))
}
// AddHostileFaction adds a hostile relationship between factions
func (mfl *MasterFactionList) AddHostileFaction(factionID, hostileFactionID int32) {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
mfl.hostileFactions[factionID] = append(mfl.hostileFactions[factionID], hostileFactionID)
}
// AddFriendlyFaction adds a friendly relationship between factions
func (mfl *MasterFactionList) AddFriendlyFaction(factionID, friendlyFactionID int32) {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
mfl.friendlyFactions[factionID] = append(mfl.friendlyFactions[factionID], friendlyFactionID)
}
// GetFriendlyFactions returns all friendly factions for a given faction
func (mfl *MasterFactionList) GetFriendlyFactions(factionID int32) []int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if factions, exists := mfl.friendlyFactions[factionID]; exists {
// Return a copy to prevent external modification
result := make([]int32, len(factions))
copy(result, factions)
return result
}
return nil
}
// GetHostileFactions returns all hostile factions for a given faction
func (mfl *MasterFactionList) GetHostileFactions(factionID int32) []int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if factions, exists := mfl.hostileFactions[factionID]; exists {
// Return a copy to prevent external modification
result := make([]int32, len(factions))
copy(result, factions)
return result
}
return nil
}
// GetFactionNameByID returns the faction name for a given ID
func (mfl *MasterFactionList) GetFactionNameByID(factionID int32) string {
if factionID > 0 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists {
return faction.Name
}
}
return ""
}
// HasFaction checks if a faction exists by ID
func (mfl *MasterFactionList) HasFaction(factionID int32) bool {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
_, exists := mfl.globalFactionList[factionID]
return exists
}
// HasFactionByName checks if a faction exists by name
func (mfl *MasterFactionList) HasFactionByName(name string) bool {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
_, exists := mfl.factionNameList[name]
return exists
}
// GetAllFactions returns a copy of all factions
func (mfl *MasterFactionList) GetAllFactions() map[int32]*Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
result := make(map[int32]*Faction)
for id, faction := range mfl.globalFactionList {
result[id] = faction
}
return result
}
// GetFactionIDs returns all faction IDs
func (mfl *MasterFactionList) GetFactionIDs() []int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
ids := make([]int32, 0, len(mfl.globalFactionList))
for id := range mfl.globalFactionList {
ids = append(ids, id)
}
return ids
}
// GetFactionsByType returns all factions of a specific type
func (mfl *MasterFactionList) GetFactionsByType(factionType string) []*Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
var result []*Faction
for _, faction := range mfl.globalFactionList {
if faction.Type == factionType {
result = append(result, faction)
}
}
return result
}
// RemoveFaction removes a faction by ID
func (mfl *MasterFactionList) RemoveFaction(factionID int32) bool {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
faction, exists := mfl.globalFactionList[factionID]
if !exists {
return false
}
// Remove from both maps
delete(mfl.globalFactionList, factionID)
delete(mfl.factionNameList, faction.Name)
// Remove from relationship maps
delete(mfl.hostileFactions, factionID)
delete(mfl.friendlyFactions, factionID)
// Remove references to this faction in other faction's relationships
for id, hostiles := range mfl.hostileFactions {
newHostiles := make([]int32, 0, len(hostiles))
for _, hostileID := range hostiles {
if hostileID != factionID {
newHostiles = append(newHostiles, hostileID)
}
}
mfl.hostileFactions[id] = newHostiles
}
for id, friendlies := range mfl.friendlyFactions {
newFriendlies := make([]int32, 0, len(friendlies))
for _, friendlyID := range friendlies {
if friendlyID != factionID {
newFriendlies = append(newFriendlies, friendlyID)
}
}
mfl.friendlyFactions[id] = newFriendlies
}
return true
}
// UpdateFaction updates an existing faction
func (mfl *MasterFactionList) UpdateFaction(faction *Faction) error {
if faction == nil {
return fmt.Errorf("faction cannot be nil")
}
if !faction.IsValid() {
return fmt.Errorf("faction is not valid")
}
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
// Check if faction exists
oldFaction, exists := mfl.globalFactionList[faction.ID]
if !exists {
return fmt.Errorf("faction with ID %d does not exist", faction.ID)
}
// If name changed, update name map
if oldFaction.Name != faction.Name {
delete(mfl.factionNameList, oldFaction.Name)
mfl.factionNameList[faction.Name] = faction
}
// Update faction
mfl.globalFactionList[faction.ID] = faction
return nil
}
// ValidateFactions checks all factions for consistency
func (mfl *MasterFactionList) ValidateFactions() []string {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
var issues []string
// Check for nil factions
for id, faction := range mfl.globalFactionList {
if faction == nil {
issues = append(issues, fmt.Sprintf("Faction ID %d is nil", id))
continue
}
if !faction.IsValid() {
issues = append(issues, fmt.Sprintf("Faction ID %d is invalid", id))
}
if faction.ID != id {
issues = append(issues, fmt.Sprintf("Faction ID mismatch: map key %d != faction ID %d", id, faction.ID))
}
}
// Check name map consistency
for name, faction := range mfl.factionNameList {
if faction == nil {
issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name))
continue
}
if faction.Name != name {
issues = append(issues, fmt.Sprintf("Faction name mismatch: map key '%s' != faction name '%s'", name, faction.Name))
}
// Check if this faction exists in the ID map
if _, exists := mfl.globalFactionList[faction.ID]; !exists {
issues = append(issues, fmt.Sprintf("Faction '%s' (ID %d) exists in name map but not in ID map", name, faction.ID))
}
}
// Check relationship consistency
for factionID, hostiles := range mfl.hostileFactions {
if _, exists := mfl.globalFactionList[factionID]; !exists {
issues = append(issues, fmt.Sprintf("Hostile relationship defined for non-existent faction %d", factionID))
}
for _, hostileID := range hostiles {
if _, exists := mfl.globalFactionList[hostileID]; !exists {
issues = append(issues, fmt.Sprintf("Faction %d has hostile relationship with non-existent faction %d", factionID, hostileID))
}
}
}
for factionID, friendlies := range mfl.friendlyFactions {
if _, exists := mfl.globalFactionList[factionID]; !exists {
issues = append(issues, fmt.Sprintf("Friendly relationship defined for non-existent faction %d", factionID))
}
for _, friendlyID := range friendlies {
if _, exists := mfl.globalFactionList[friendlyID]; !exists {
issues = append(issues, fmt.Sprintf("Faction %d has friendly relationship with non-existent faction %d", factionID, friendlyID))
}
}
}
return issues
}
// IsValid returns true if all factions are valid
func (mfl *MasterFactionList) IsValid() bool {
issues := mfl.ValidateFactions()
return len(issues) == 0
}

View File

@ -0,0 +1,349 @@
package factions
import (
"sync"
)
// PlayerFaction manages faction standing for a single player
type PlayerFaction struct {
factionValues map[int32]int32 // Faction ID -> current value
factionPercent map[int32]int8 // Faction ID -> percentage within con level
factionUpdateNeeded []int32 // Factions that need client updates
masterFactionList *MasterFactionList
updateMutex sync.Mutex // Thread safety for updates
mutex sync.RWMutex // Thread safety for faction data
}
// NewPlayerFaction creates a new player faction system
func NewPlayerFaction(masterFactionList *MasterFactionList) *PlayerFaction {
return &PlayerFaction{
factionValues: make(map[int32]int32),
factionPercent: make(map[int32]int8),
factionUpdateNeeded: make([]int32, 0),
masterFactionList: masterFactionList,
}
}
// GetMaxValue returns the maximum faction value for a given consideration level
func (pf *PlayerFaction) GetMaxValue(con int8) int32 {
if con < 0 {
return int32(con) * ConMultiplier
}
return (int32(con) * ConMultiplier) + ConRemainder
}
// GetMinValue returns the minimum faction value for a given consideration level
func (pf *PlayerFaction) GetMinValue(con int8) int32 {
if con <= 0 {
return (int32(con) * ConMultiplier) - ConRemainder
}
return int32(con) * ConMultiplier
}
// ShouldAttack returns true if the player should attack based on faction
func (pf *PlayerFaction) ShouldAttack(factionID int32) bool {
return pf.GetCon(factionID) <= AttackThreshold
}
// GetCon returns the consideration level (-4 to 4) for a faction
func (pf *PlayerFaction) GetCon(factionID int32) int8 {
// Special faction IDs have predefined cons
if factionID <= SpecialFactionIDMax {
if factionID == 0 {
return ConIndiff
}
return int8(factionID - 5)
}
value := pf.GetFactionValue(factionID)
// Neutral range
if value >= ConNeutralMin && value <= ConNeutralMax {
return ConIndiff
}
// Maximum ally
if value >= ConAllyMin {
return ConAlly
}
// Maximum hostile
if value <= ConHostileMax {
return ConKOS
}
// Calculate con based on value
return int8(value / ConMultiplier)
}
// GetPercent returns the percentage within the current consideration level
func (pf *PlayerFaction) GetPercent(factionID int32) int8 {
// Special factions have no percentage
if factionID <= SpecialFactionIDMax {
return 0
}
con := pf.GetCon(factionID)
value := pf.GetFactionValue(factionID)
if con != ConIndiff {
// Make value positive for calculation
if value <= 0 {
value *= -1
}
// Make con positive for calculation
if con < 0 {
con *= -1
}
// Calculate percentage within the con level
value -= int32(con) * ConMultiplier
value *= PercentMultiplier
return int8(value / ConMultiplier)
} else {
// Neutral range calculation
value += PercentNeutralOffset
value *= PercentMultiplier
return int8(value / PercentNeutralDivisor)
}
}
// FactionUpdate builds a faction update packet for the client
func (pf *PlayerFaction) FactionUpdate(version int16) ([]byte, error) {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
if len(pf.factionUpdateNeeded) == 0 {
return nil, nil
}
// This is a placeholder for packet building
// In the full implementation, this would use the PacketStruct system:
// packet := configReader.getStruct("WS_FactionUpdate", version)
// packet.setArrayLengthByName("num_factions", len(pf.factionUpdateNeeded))
// for i, factionID := range pf.factionUpdateNeeded {
// faction := pf.masterFactionList.GetFaction(factionID)
// if faction != nil {
// packet.setArrayDataByName("faction_id", faction.ID, i)
// packet.setArrayDataByName("name", faction.Name, i)
// packet.setArrayDataByName("description", faction.Description, i)
// packet.setArrayDataByName("category", faction.Type, i)
// packet.setArrayDataByName("con", pf.GetCon(faction.ID), i)
// packet.setArrayDataByName("percentage", pf.GetPercent(faction.ID), i)
// packet.setArrayDataByName("value", pf.GetFactionValue(faction.ID), i)
// }
// }
// return packet.serialize()
// Clear update list
pf.factionUpdateNeeded = pf.factionUpdateNeeded[:0]
// Return empty packet for now
return make([]byte, 0), nil
}
// GetFactionValue returns the current faction value for a faction
func (pf *PlayerFaction) GetFactionValue(factionID int32) int32 {
// Special factions always return 0
if factionID <= SpecialFactionIDMax {
return 0
}
pf.mutex.RLock()
defer pf.mutex.RUnlock()
// Return current value or 0 if not set
// Note: The C++ code has a comment about always returning the default value,
// but the actual implementation returns the stored value or 0
return pf.factionValues[factionID]
}
// ShouldIncrease returns true if the faction can be increased
func (pf *PlayerFaction) ShouldIncrease(factionID int32) bool {
if factionID <= SpecialFactionIDMax {
return false
}
if pf.masterFactionList == nil {
return false
}
return pf.masterFactionList.GetIncreaseAmount(factionID) != 0
}
// ShouldDecrease returns true if the faction can be decreased
func (pf *PlayerFaction) ShouldDecrease(factionID int32) bool {
if factionID <= SpecialFactionIDMax {
return false
}
if pf.masterFactionList == nil {
return false
}
return pf.masterFactionList.GetDecreaseAmount(factionID) != 0
}
// IncreaseFaction increases a faction value
func (pf *PlayerFaction) IncreaseFaction(factionID int32, amount int32) bool {
// Special factions cannot be changed
if factionID <= SpecialFactionIDMax {
return true
}
pf.mutex.Lock()
defer pf.mutex.Unlock()
// Use default amount if not specified
if amount == 0 && pf.masterFactionList != nil {
amount = pf.masterFactionList.GetIncreaseAmount(factionID)
}
// Increase the faction value
pf.factionValues[factionID] += amount
canContinue := true
// Cap at maximum value
if pf.factionValues[factionID] >= MaxFactionValue {
pf.factionValues[factionID] = MaxFactionValue
canContinue = false
}
// Mark for update
pf.addFactionUpdateNeeded(factionID)
return canContinue
}
// DecreaseFaction decreases a faction value
func (pf *PlayerFaction) DecreaseFaction(factionID int32, amount int32) bool {
// Special factions cannot be changed
if factionID <= SpecialFactionIDMax {
return true
}
pf.mutex.Lock()
defer pf.mutex.Unlock()
// Use default amount if not specified
if amount == 0 && pf.masterFactionList != nil {
amount = pf.masterFactionList.GetDecreaseAmount(factionID)
}
// Cannot decrease if no amount specified
if amount == 0 {
return false
}
// Decrease the faction value
pf.factionValues[factionID] -= amount
canContinue := true
// Cap at minimum value
if pf.factionValues[factionID] <= MinFactionValue {
pf.factionValues[factionID] = MinFactionValue
canContinue = false
}
// Mark for update
pf.addFactionUpdateNeeded(factionID)
return canContinue
}
// SetFactionValue sets a faction to a specific value
func (pf *PlayerFaction) SetFactionValue(factionID int32, value int32) bool {
pf.mutex.Lock()
defer pf.mutex.Unlock()
pf.factionValues[factionID] = value
// Mark for update
pf.addFactionUpdateNeeded(factionID)
return true
}
// GetFactionValues returns a copy of all faction values
func (pf *PlayerFaction) GetFactionValues() map[int32]int32 {
pf.mutex.RLock()
defer pf.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]int32)
for id, value := range pf.factionValues {
result[id] = value
}
return result
}
// HasFaction returns true if the player has a value for the given faction
func (pf *PlayerFaction) HasFaction(factionID int32) bool {
pf.mutex.RLock()
defer pf.mutex.RUnlock()
_, exists := pf.factionValues[factionID]
return exists
}
// GetFactionCount returns the number of factions the player has values for
func (pf *PlayerFaction) GetFactionCount() int {
pf.mutex.RLock()
defer pf.mutex.RUnlock()
return len(pf.factionValues)
}
// ClearFactionValues removes all faction values
func (pf *PlayerFaction) ClearFactionValues() {
pf.mutex.Lock()
defer pf.mutex.Unlock()
pf.factionValues = make(map[int32]int32)
pf.factionPercent = make(map[int32]int8)
}
// addFactionUpdateNeeded marks a faction as needing an update (internal use, assumes lock held)
func (pf *PlayerFaction) addFactionUpdateNeeded(factionID int32) {
// Note: This method assumes the mutex is already held by the caller
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
pf.factionUpdateNeeded = append(pf.factionUpdateNeeded, factionID)
}
// GetPendingUpdates returns factions that need client updates
func (pf *PlayerFaction) GetPendingUpdates() []int32 {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
if len(pf.factionUpdateNeeded) == 0 {
return nil
}
// Return a copy
result := make([]int32, len(pf.factionUpdateNeeded))
copy(result, pf.factionUpdateNeeded)
return result
}
// ClearPendingUpdates clears the pending update list
func (pf *PlayerFaction) ClearPendingUpdates() {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
pf.factionUpdateNeeded = pf.factionUpdateNeeded[:0]
}
// HasPendingUpdates returns true if there are pending faction updates
func (pf *PlayerFaction) HasPendingUpdates() bool {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
return len(pf.factionUpdateNeeded) > 0
}

108
internal/factions/types.go Normal file
View File

@ -0,0 +1,108 @@
package factions
// Faction represents a single faction with its properties
type Faction struct {
ID int32 // Faction ID
Name string // Faction name
Type string // Faction type/category
Description string // Faction description
NegativeChange int16 // Amount faction decreases by default
PositiveChange int16 // Amount faction increases by default
DefaultValue int32 // Default faction value for new characters
}
// NewFaction creates a new faction with the given parameters
func NewFaction(id int32, name, factionType, description string) *Faction {
return &Faction{
ID: id,
Name: name,
Type: factionType,
Description: description,
NegativeChange: 0,
PositiveChange: 0,
DefaultValue: 0,
}
}
// GetID returns the faction ID
func (f *Faction) GetID() int32 {
return f.ID
}
// GetName returns the faction name
func (f *Faction) GetName() string {
return f.Name
}
// GetType returns the faction type
func (f *Faction) GetType() string {
return f.Type
}
// GetDescription returns the faction description
func (f *Faction) GetDescription() string {
return f.Description
}
// GetNegativeChange returns the default decrease amount
func (f *Faction) GetNegativeChange() int16 {
return f.NegativeChange
}
// GetPositiveChange returns the default increase amount
func (f *Faction) GetPositiveChange() int16 {
return f.PositiveChange
}
// GetDefaultValue returns the default faction value
func (f *Faction) GetDefaultValue() int32 {
return f.DefaultValue
}
// SetNegativeChange sets the default decrease amount
func (f *Faction) SetNegativeChange(amount int16) {
f.NegativeChange = amount
}
// SetPositiveChange sets the default increase amount
func (f *Faction) SetPositiveChange(amount int16) {
f.PositiveChange = amount
}
// SetDefaultValue sets the default faction value
func (f *Faction) SetDefaultValue(value int32) {
f.DefaultValue = value
}
// Clone creates a copy of the faction
func (f *Faction) Clone() *Faction {
return &Faction{
ID: f.ID,
Name: f.Name,
Type: f.Type,
Description: f.Description,
NegativeChange: f.NegativeChange,
PositiveChange: f.PositiveChange,
DefaultValue: f.DefaultValue,
}
}
// IsValid returns true if the faction has valid data
func (f *Faction) IsValid() bool {
return f.ID > 0 && len(f.Name) > 0
}
// IsSpecialFaction returns true if this is a special faction (ID <= 10)
func (f *Faction) IsSpecialFaction() bool {
return f.ID <= SpecialFactionIDMax
}
// CanIncrease returns true if this faction can be increased
func (f *Faction) CanIncrease() bool {
return !f.IsSpecialFaction() && f.PositiveChange != 0
}
// CanDecrease returns true if this faction can be decreased
func (f *Faction) CanDecrease() bool {
return !f.IsSpecialFaction() && f.NegativeChange != 0
}

View File

@ -0,0 +1,76 @@
package ground_spawn
// Harvest type constants
const (
HarvestTypeNone = 0
HarvestType1Item = 1
HarvestType3Items = 2
HarvestType5Items = 3
HarvestTypeImbue = 4
HarvestTypeRare = 5
HarvestType10AndRare = 6
)
// Harvest skill constants
const (
SkillGathering = "Gathering"
SkillCollecting = "Collecting"
SkillMining = "Mining"
SkillFishing = "Fishing"
SkillTrapping = "Trapping"
SkillForesting = "Foresting"
)
// Harvest skill spell types
const (
SpellTypeGather = "gather"
SpellTypeMine = "mine"
SpellTypeTrap = "trap"
SpellTypeChop = "chop"
SpellTypeFish = "fish"
)
// Harvest result constants
const (
HarvestResultSuccess = iota
HarvestResultFailed
HarvestResultNoSkill
HarvestResultDepleted
HarvestResultNoItems
)
// Item rarity flags
const (
ItemRarityNormal = 0
ItemRarityRare = 1
ItemRarityImbue = 2
)
// Ground spawn state constants
const (
StateAvailable = iota
StateDepleted
StateRespawning
)
// Default spawn configuration
const (
DefaultDifficulty = 0
DefaultSpawnType = 2
DefaultState = 129
DefaultAttemptsPerHarvest = 1
DefaultNumberHarvests = 1
DefaultRandomizeHeading = true
)
// Harvest message channels (placeholder values)
const (
ChannelHarvesting = 15
ChannelColorRed = 13
)
// Statistical tracking
const (
StatPlayerItemsHarvested = 1
StatPlayerRaresHarvested = 2
)

View File

@ -0,0 +1,627 @@
package ground_spawn
import (
"fmt"
"math/rand"
"strings"
"sync"
"time"
"eq2emu/internal/spawn"
)
// NewGroundSpawn creates a new ground spawn instance
func NewGroundSpawn(config GroundSpawnConfig) *GroundSpawn {
baseSpawn := spawn.NewSpawn()
gs := &GroundSpawn{
Spawn: baseSpawn,
numberHarvests: config.NumberHarvests,
numAttemptsPerHarvest: config.AttemptsPerHarvest,
groundspawnID: config.GroundSpawnID,
collectionSkill: config.CollectionSkill,
randomizeHeading: config.RandomizeHeading,
}
// Configure base spawn properties
gs.SetName(config.Name)
gs.SetSpawnType(DefaultSpawnType)
gs.SetDifficulty(DefaultDifficulty)
gs.SetState(DefaultState)
// Set position
gs.SetX(config.Location.X)
gs.SetY(config.Location.Y)
gs.SetZ(config.Location.Z)
if config.RandomizeHeading {
gs.SetHeading(rand.Float32() * 360.0)
} else {
gs.SetHeading(config.Location.Heading)
}
return gs
}
// Copy creates a deep copy of the ground spawn
func (gs *GroundSpawn) Copy() *GroundSpawn {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
newSpawn := &GroundSpawn{
Spawn: gs.Spawn.Copy().(*spawn.Spawn),
numberHarvests: gs.numberHarvests,
numAttemptsPerHarvest: gs.numAttemptsPerHarvest,
groundspawnID: gs.groundspawnID,
collectionSkill: gs.collectionSkill,
randomizeHeading: gs.randomizeHeading,
}
return newSpawn
}
// IsGroundSpawn returns true (implements spawn interface)
func (gs *GroundSpawn) IsGroundSpawn() bool {
return true
}
// GetNumberHarvests returns the number of harvests remaining
func (gs *GroundSpawn) GetNumberHarvests() int8 {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
return gs.numberHarvests
}
// SetNumberHarvests sets the number of harvests remaining
func (gs *GroundSpawn) SetNumberHarvests(val int8) {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
gs.numberHarvests = val
}
// GetAttemptsPerHarvest returns attempts per harvest session
func (gs *GroundSpawn) GetAttemptsPerHarvest() int8 {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
return gs.numAttemptsPerHarvest
}
// SetAttemptsPerHarvest sets attempts per harvest session
func (gs *GroundSpawn) SetAttemptsPerHarvest(val int8) {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
gs.numAttemptsPerHarvest = val
}
// GetGroundSpawnEntryID returns the database entry ID
func (gs *GroundSpawn) GetGroundSpawnEntryID() int32 {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
return gs.groundspawnID
}
// SetGroundSpawnEntryID sets the database entry ID
func (gs *GroundSpawn) SetGroundSpawnEntryID(val int32) {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
gs.groundspawnID = val
}
// GetCollectionSkill returns the required harvesting skill
func (gs *GroundSpawn) GetCollectionSkill() string {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
return gs.collectionSkill
}
// SetCollectionSkill sets the required harvesting skill
func (gs *GroundSpawn) SetCollectionSkill(skill string) {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
gs.collectionSkill = skill
}
// GetRandomizeHeading returns whether heading should be randomized
func (gs *GroundSpawn) GetRandomizeHeading() bool {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
return gs.randomizeHeading
}
// SetRandomizeHeading sets whether heading should be randomized
func (gs *GroundSpawn) SetRandomizeHeading(val bool) {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
gs.randomizeHeading = val
}
// IsDepleted returns true if the ground spawn has no harvests remaining
func (gs *GroundSpawn) IsDepleted() bool {
return gs.GetNumberHarvests() <= 0
}
// IsAvailable returns true if the ground spawn can be harvested
func (gs *GroundSpawn) IsAvailable() bool {
return gs.GetNumberHarvests() > 0 && gs.IsAlive()
}
// GetHarvestMessageName returns the appropriate harvest verb based on skill
func (gs *GroundSpawn) GetHarvestMessageName(presentTense bool, failure bool) string {
skill := strings.ToLower(gs.GetCollectionSkill())
switch skill {
case "gathering", "collecting":
if presentTense {
return "gather"
}
return "gathered"
case "mining":
if presentTense {
return "mine"
}
return "mined"
case "fishing":
if presentTense {
return "fish"
}
return "fished"
case "trapping":
if failure {
return "trap"
}
if presentTense {
return "acquire"
}
return "acquired"
case "foresting":
if presentTense {
return "forest"
}
return "forested"
default:
if presentTense {
return "collect"
}
return "collected"
}
}
// GetHarvestSpellType returns the spell type for harvesting
func (gs *GroundSpawn) GetHarvestSpellType() string {
skill := strings.ToLower(gs.GetCollectionSkill())
switch skill {
case "gathering", "collecting":
return SpellTypeGather
case "mining":
return SpellTypeMine
case "trapping":
return SpellTypeTrap
case "foresting":
return SpellTypeChop
case "fishing":
return SpellTypeFish
default:
return SpellTypeGather
}
}
// GetHarvestSpellName returns the spell name for harvesting
func (gs *GroundSpawn) GetHarvestSpellName() string {
skill := gs.GetCollectionSkill()
if skill == SkillCollecting {
return SkillGathering
}
return skill
}
// ProcessHarvest handles the complex harvesting logic
func (gs *GroundSpawn) ProcessHarvest(context *HarvestContext) (*HarvestResult, error) {
if context == nil {
return nil, fmt.Errorf("harvest context cannot be nil")
}
if context.Player == nil {
return nil, fmt.Errorf("player cannot be nil")
}
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
// Check if ground spawn is depleted
if gs.numberHarvests <= 0 {
return &HarvestResult{
Success: false,
MessageText: "This spawn has nothing more to harvest!",
}, nil
}
// Validate harvest data
if context.GroundSpawnEntries == nil || len(context.GroundSpawnEntries) == 0 {
return &HarvestResult{
Success: false,
MessageText: fmt.Sprintf("Error: No groundspawn entries assigned to groundspawn id: %d", gs.groundspawnID),
}, nil
}
if context.GroundSpawnItems == nil || len(context.GroundSpawnItems) == 0 {
return &HarvestResult{
Success: false,
MessageText: fmt.Sprintf("Error: No groundspawn items assigned to groundspawn id: %d", gs.groundspawnID),
}, nil
}
// Validate player skill
if context.PlayerSkill == nil {
return &HarvestResult{
Success: false,
MessageText: fmt.Sprintf("Error: You do not have the '%s' skill!", gs.collectionSkill),
}, nil
}
result := &HarvestResult{
Success: true,
ItemsAwarded: make([]*HarvestedItem, 0),
}
// Process each harvest attempt
for attempt := int8(0); attempt < gs.numAttemptsPerHarvest; attempt++ {
attemptResult := gs.processHarvestAttempt(context)
if attemptResult != nil {
result.ItemsAwarded = append(result.ItemsAwarded, attemptResult.ItemsAwarded...)
if attemptResult.SkillGained {
result.SkillGained = true
}
}
}
// Decrement harvest count
gs.numberHarvests--
return result, nil
}
// processHarvestAttempt handles a single harvest attempt
func (gs *GroundSpawn) processHarvestAttempt(context *HarvestContext) *HarvestResult {
// Filter available harvest tables based on player skill and level
availableTables := gs.filterHarvestTables(context)
if len(availableTables) == 0 {
return &HarvestResult{
Success: false,
MessageText: "You lack the skills to harvest this node!",
}
}
// Select harvest table based on skill roll
selectedTable := gs.selectHarvestTable(availableTables, context.TotalSkill)
if selectedTable == nil {
return &HarvestResult{
Success: false,
MessageText: "Failed to determine harvest table",
}
}
// Determine harvest type based on table percentages
harvestType := gs.determineHarvestType(selectedTable, context.IsCollection)
if harvestType == HarvestTypeNone {
return &HarvestResult{
Success: false,
MessageText: fmt.Sprintf("You failed to %s anything from %s.",
gs.GetHarvestMessageName(true, true), gs.GetName()),
}
}
// Award items based on harvest type
items := gs.awardHarvestItems(harvestType, context.GroundSpawnItems, context.Player)
// Handle skill progression
skillGained := gs.handleSkillProgression(context, selectedTable)
return &HarvestResult{
Success: len(items) > 0,
HarvestType: harvestType,
ItemsAwarded: items,
SkillGained: skillGained,
}
}
// filterHarvestTables filters tables based on player capabilities
func (gs *GroundSpawn) filterHarvestTables(context *HarvestContext) []*GroundSpawnEntry {
var filtered []*GroundSpawnEntry
for _, entry := range context.GroundSpawnEntries {
// Check skill requirement
if entry.MinSkillLevel > context.TotalSkill {
continue
}
// Check level requirement for bonus tables
if entry.BonusTable && context.Player.GetLevel() < entry.MinAdventureLevel {
continue
}
filtered = append(filtered, entry)
}
return filtered
}
// selectHarvestTable selects a harvest table based on skill level
func (gs *GroundSpawn) selectHarvestTable(tables []*GroundSpawnEntry, totalSkill int16) *GroundSpawnEntry {
if len(tables) == 0 {
return nil
}
// Find lowest skill requirement
lowestSkill := int16(32767)
for _, table := range tables {
if table.MinSkillLevel < lowestSkill {
lowestSkill = table.MinSkillLevel
}
}
// Roll for table selection
tableChoice := int16(rand.Intn(int(totalSkill-lowestSkill+1))) + lowestSkill
// Find best matching table
var bestTable *GroundSpawnEntry
bestScore := int16(0)
for _, table := range tables {
if tableChoice >= table.MinSkillLevel && table.MinSkillLevel > bestScore {
bestTable = table
bestScore = table.MinSkillLevel
}
}
// If multiple tables match, pick randomly
var matches []*GroundSpawnEntry
for _, table := range tables {
if table.MinSkillLevel == bestScore {
matches = append(matches, table)
}
}
if len(matches) > 1 {
return matches[rand.Intn(len(matches))]
}
return bestTable
}
// determineHarvestType determines what type of harvest occurs
func (gs *GroundSpawn) determineHarvestType(table *GroundSpawnEntry, isCollection bool) int8 {
chance := rand.Float32() * 100.0
// Collection items always get 1 item
if isCollection {
return HarvestType1Item
}
// Check harvest types in order of rarity (most rare first)
if chance <= table.Harvest10 {
return HarvestType10AndRare
}
if chance <= table.HarvestRare {
return HarvestTypeRare
}
if chance <= table.HarvestImbue {
return HarvestTypeImbue
}
if chance <= table.Harvest5 {
return HarvestType5Items
}
if chance <= table.Harvest3 {
return HarvestType3Items
}
if chance <= table.Harvest1 {
return HarvestType1Item
}
return HarvestTypeNone
}
// awardHarvestItems awards items based on harvest type
func (gs *GroundSpawn) awardHarvestItems(harvestType int8, availableItems []*GroundSpawnEntryItem, player *Player) []*HarvestedItem {
var items []*HarvestedItem
// Filter items based on harvest type and player location
normalItems := gs.filterItems(availableItems, ItemRarityNormal, player.GetLocation())
rareItems := gs.filterItems(availableItems, ItemRarityRare, player.GetLocation())
imbueItems := gs.filterItems(availableItems, ItemRarityImbue, player.GetLocation())
switch harvestType {
case HarvestType1Item:
items = gs.selectRandomItems(normalItems, 1)
case HarvestType3Items:
items = gs.selectRandomItems(normalItems, 3)
case HarvestType5Items:
items = gs.selectRandomItems(normalItems, 5)
case HarvestTypeImbue:
items = gs.selectRandomItems(imbueItems, 1)
case HarvestTypeRare:
items = gs.selectRandomItems(rareItems, 1)
case HarvestType10AndRare:
normal := gs.selectRandomItems(normalItems, 10)
rare := gs.selectRandomItems(rareItems, 1)
items = append(normal, rare...)
}
return items
}
// filterItems filters items by rarity and grid restriction
func (gs *GroundSpawn) filterItems(items []*GroundSpawnEntryItem, rarity int8, playerGrid int32) []*GroundSpawnEntryItem {
var filtered []*GroundSpawnEntryItem
for _, item := range items {
if item.IsRare != rarity {
continue
}
// Check grid restriction
if item.GridID != 0 && item.GridID != playerGrid {
continue
}
filtered = append(filtered, item)
}
return filtered
}
// selectRandomItems randomly selects items from available list
func (gs *GroundSpawn) selectRandomItems(items []*GroundSpawnEntryItem, quantity int16) []*HarvestedItem {
if len(items) == 0 {
return nil
}
var result []*HarvestedItem
for i := int16(0); i < quantity; i++ {
selectedItem := items[rand.Intn(len(items))]
harvestedItem := &HarvestedItem{
ItemID: selectedItem.ItemID,
Quantity: selectedItem.Quantity,
IsRare: selectedItem.IsRare == ItemRarityRare,
Name: fmt.Sprintf("Item_%d", selectedItem.ItemID), // Placeholder
}
result = append(result, harvestedItem)
}
return result
}
// handleSkillProgression manages skill increases from harvesting
func (gs *GroundSpawn) handleSkillProgression(context *HarvestContext, table *GroundSpawnEntry) bool {
if context.IsCollection {
return false // Collections don't give skill
}
if context.PlayerSkill == nil {
return false
}
// Check if player skill is already at max for this node
maxSkillAllowed := int16(float32(context.MaxSkillRequired) * 1.0) // TODO: Use skill multiplier rule
if context.PlayerSkill.GetCurrentValue() >= maxSkillAllowed {
return false
}
// Award skill increase (placeholder implementation)
// TODO: Integrate with actual skill system when available
return true
}
// HandleUse processes player interaction with the ground spawn
func (gs *GroundSpawn) HandleUse(client Client, useType string) error {
if client == nil {
return fmt.Errorf("client cannot be nil")
}
gs.harvestUseMutex.Lock()
defer gs.harvestUseMutex.Unlock()
// Check spawn access requirements
if !gs.MeetsSpawnAccessRequirements(client.GetPlayer()) {
return nil // Silently ignore if requirements not met
}
// Normalize use type
useType = strings.ToLower(strings.TrimSpace(useType))
// Handle older clients that don't send use type
if client.GetVersion() <= 561 && useType == "" {
useType = gs.GetHarvestSpellType()
}
// Check if this is a harvest action
expectedSpellType := gs.GetHarvestSpellType()
if useType == expectedSpellType {
return gs.handleHarvestUse(client)
}
// Handle other command interactions
if gs.HasCommandIcon() {
return gs.handleCommandUse(client, useType)
}
return nil
}
// handleHarvestUse processes harvest-specific use
func (gs *GroundSpawn) handleHarvestUse(client Client) error {
spellName := gs.GetHarvestSpellName()
// TODO: Integrate with spell system when available
// spell := masterSpellList.GetSpellByName(spellName)
// if spell != nil {
// zone.ProcessSpell(spell, player, target, true, true)
// }
if client.GetLogger() != nil {
client.GetLogger().LogDebug("Player %s attempting to harvest %s using spell %s",
client.GetPlayer().GetName(), gs.GetName(), spellName)
}
return nil
}
// handleCommandUse processes command-specific use
func (gs *GroundSpawn) handleCommandUse(client Client, command string) error {
// TODO: Integrate with entity command system when available
// entityCommand := gs.FindEntityCommand(command)
// if entityCommand != nil {
// zone.ProcessEntityCommand(entityCommand, player, target)
// }
if client.GetLogger() != nil {
client.GetLogger().LogDebug("Player %s using command %s on %s",
client.GetPlayer().GetName(), command, gs.GetName())
}
return nil
}
// Serialize creates a packet representation of the ground spawn
func (gs *GroundSpawn) Serialize(player *Player, version int16) ([]byte, error) {
// Use base spawn serialization
return gs.Spawn.Serialize(player, version)
}
// Respawn resets the ground spawn to harvestable state
func (gs *GroundSpawn) Respawn() {
gs.harvestMutex.Lock()
defer gs.harvestMutex.Unlock()
// Reset harvest count to default
gs.numberHarvests = DefaultNumberHarvests
// Randomize heading if configured
if gs.randomizeHeading {
gs.SetHeading(rand.Float32() * 360.0)
}
// Mark as alive
gs.SetAlive(true)
}

View File

@ -0,0 +1,260 @@
package ground_spawn
// Database interface for ground spawn persistence
type Database interface {
LoadGroundSpawnEntries(groundspawnID int32) ([]*GroundSpawnEntry, error)
LoadGroundSpawnItems(groundspawnID int32) ([]*GroundSpawnEntryItem, error)
SaveGroundSpawn(gs *GroundSpawn) error
LoadAllGroundSpawns() ([]*GroundSpawn, error)
DeleteGroundSpawn(id int32) error
}
// Logger interface for ground spawn logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// Player interface for ground spawn interactions
type Player interface {
GetID() int32
GetName() string
GetLevel() int16
GetLocation() int32
GetSkillByName(skillName string) *Skill
CheckQuestsHarvestUpdate(item *Item, quantity int16)
UpdatePlayerStatistic(statType int32, amount int32)
SendMessage(message string)
}
// Client interface for client communication
type Client interface {
GetPlayer() *Player
GetVersion() int16
GetLogger() Logger
GetCurrentZoneID() int32
Message(channel int32, message string, args ...interface{})
SimpleMessage(channel int32, message string)
SendPopupMessage(type_ int32, message string, sound string, duration float32, r, g, b int32)
AddItem(item *Item, itemDeleted *bool) error
}
// Skill interface for skill management
type Skill interface {
GetCurrentValue() int16
GetMaxValue() int16
GetName() string
IncreaseSkill(amount int16) bool
}
// Item interface for harvest rewards
type Item interface {
GetID() int32
GetName() string
GetCount() int16
SetCount(count int16)
CreateItemLink(version int16, color bool) string
}
// Zone interface for zone-specific operations
type Zone interface {
GetID() int32
GetGroundSpawnEntries(groundspawnID int32) []*GroundSpawnEntry
GetGroundSpawnEntryItems(groundspawnID int32) []*GroundSpawnEntryItem
ProcessSpell(spell *Spell, caster *Player, target *Player, harvest bool, fromItem bool)
ProcessEntityCommand(command *EntityCommand, player *Player, target *Player)
}
// Spell interface for harvest spells
type Spell interface {
GetID() int32
GetName() string
GetType() string
}
// EntityCommand interface for ground spawn commands
type EntityCommand interface {
GetID() int32
GetName() string
GetCommand() string
}
// Rules interface for game rules and configuration
type Rules interface {
GetZoneRule(zoneID int32, category string, ruleName string) *Rule
}
// Rule interface for individual rule values
type Rule interface {
GetInt16() int16
GetFloat() float32
GetBool() bool
GetString() string
}
// GroundSpawnProvider interface for systems that provide ground spawn functionality
type GroundSpawnProvider interface {
GetGroundSpawn(id int32) *GroundSpawn
CreateGroundSpawn(config GroundSpawnConfig) *GroundSpawn
GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn
ProcessHarvest(gs *GroundSpawn, player *Player) (*HarvestResult, error)
}
// HarvestHandler interface for handling harvest events
type HarvestHandler interface {
OnHarvestStart(gs *GroundSpawn, player *Player) error
OnHarvestComplete(gs *GroundSpawn, player *Player, result *HarvestResult) error
OnHarvestFailed(gs *GroundSpawn, player *Player, reason string) error
OnGroundSpawnDepleted(gs *GroundSpawn) error
}
// ItemProvider interface for item creation and management
type ItemProvider interface {
GetItem(itemID int32) *Item
CreateItem(itemID int32, quantity int16) *Item
GetItemName(itemID int32) string
}
// SkillProvider interface for skill management
type SkillProvider interface {
GetPlayerSkill(player *Player, skillName string) *Skill
GetSkillIDByName(skillName string) int32
IncreasePlayerSkill(player *Player, skillName string, amount int16) bool
}
// SpawnProvider interface for spawn system integration
type SpawnProvider interface {
CreateSpawn() interface{}
GetSpawn(id int32) interface{}
RegisterGroundSpawn(gs *GroundSpawn) error
UnregisterGroundSpawn(id int32) error
}
// GroundSpawnAware interface for entities that can interact with ground spawns
type GroundSpawnAware interface {
CanHarvest(gs *GroundSpawn) bool
GetHarvestSkill(skillName string) *Skill
GetHarvestModifiers() *HarvestModifiers
OnHarvestResult(result *HarvestResult)
}
// PlayerGroundSpawnAdapter provides ground spawn functionality for players
type PlayerGroundSpawnAdapter struct {
player *Player
manager *Manager
logger Logger
}
// NewPlayerGroundSpawnAdapter creates a new player ground spawn adapter
func NewPlayerGroundSpawnAdapter(player *Player, manager *Manager, logger Logger) *PlayerGroundSpawnAdapter {
return &PlayerGroundSpawnAdapter{
player: player,
manager: manager,
logger: logger,
}
}
// CanHarvest returns true if the player can harvest the ground spawn
func (pgsa *PlayerGroundSpawnAdapter) CanHarvest(gs *GroundSpawn) bool {
if gs == nil || pgsa.player == nil {
return false
}
// Check if ground spawn is available
if !gs.IsAvailable() {
return false
}
// Check if player has required skill
skill := pgsa.player.GetSkillByName(gs.GetCollectionSkill())
if skill == nil {
return false
}
// TODO: Add additional checks (quest requirements, level, etc.)
return true
}
// GetHarvestSkill returns the player's skill for a specific harvest type
func (pgsa *PlayerGroundSpawnAdapter) GetHarvestSkill(skillName string) *Skill {
if pgsa.player == nil {
return nil
}
return pgsa.player.GetSkillByName(skillName)
}
// GetHarvestModifiers returns harvest modifiers for the player
func (pgsa *PlayerGroundSpawnAdapter) GetHarvestModifiers() *HarvestModifiers {
// TODO: Calculate modifiers based on player stats, equipment, buffs, etc.
return &HarvestModifiers{
SkillMultiplier: 1.0,
RareChanceBonus: 0.0,
QuantityMultiplier: 1.0,
LuckModifier: 0,
}
}
// OnHarvestResult handles harvest result notifications
func (pgsa *PlayerGroundSpawnAdapter) OnHarvestResult(result *HarvestResult) {
if result == nil || pgsa.player == nil {
return
}
if result.Success && len(result.ItemsAwarded) > 0 {
if pgsa.logger != nil {
pgsa.logger.LogDebug("Player %s successfully harvested %d items",
pgsa.player.GetName(), len(result.ItemsAwarded))
}
}
}
// HarvestEventAdapter adapts harvest events for different systems
type HarvestEventAdapter struct {
handler HarvestHandler
logger Logger
}
// NewHarvestEventAdapter creates a new harvest event adapter
func NewHarvestEventAdapter(handler HarvestHandler, logger Logger) *HarvestEventAdapter {
return &HarvestEventAdapter{
handler: handler,
logger: logger,
}
}
// ProcessHarvestEvent processes a harvest event
func (hea *HarvestEventAdapter) ProcessHarvestEvent(eventType string, gs *GroundSpawn, player *Player, data interface{}) {
if hea.handler == nil {
return
}
switch eventType {
case "harvest_start":
if err := hea.handler.OnHarvestStart(gs, player); err != nil && hea.logger != nil {
hea.logger.LogError("Harvest start handler failed: %v", err)
}
case "harvest_complete":
if result, ok := data.(*HarvestResult); ok {
if err := hea.handler.OnHarvestComplete(gs, player, result); err != nil && hea.logger != nil {
hea.logger.LogError("Harvest complete handler failed: %v", err)
}
}
case "harvest_failed":
if reason, ok := data.(string); ok {
if err := hea.handler.OnHarvestFailed(gs, player, reason); err != nil && hea.logger != nil {
hea.logger.LogError("Harvest failed handler failed: %v", err)
}
}
case "ground_spawn_depleted":
if err := hea.handler.OnGroundSpawnDepleted(gs); err != nil && hea.logger != nil {
hea.logger.LogError("Ground spawn depleted handler failed: %v", err)
}
}
}

View File

@ -0,0 +1,592 @@
package ground_spawn
import (
"fmt"
"sync"
"time"
)
// NewManager creates a new ground spawn manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
groundSpawns: make(map[int32]*GroundSpawn),
spawnsByZone: make(map[int32][]*GroundSpawn),
entriesByID: make(map[int32][]*GroundSpawnEntry),
itemsByID: make(map[int32][]*GroundSpawnEntryItem),
respawnQueue: make(map[int32]time.Time),
database: database,
logger: logger,
harvestsBySkill: make(map[string]int64),
}
}
// Initialize loads ground spawn data from database
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing ground spawn manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty ground spawn list")
}
return nil
}
// Load ground spawns from database
groundSpawns, err := m.database.LoadAllGroundSpawns()
if err != nil {
return fmt.Errorf("failed to load ground spawns from database: %w", err)
}
m.mutex.Lock()
defer m.mutex.Unlock()
for _, gs := range groundSpawns {
m.groundSpawns[gs.GetID()] = gs
// Group by zone (placeholder - zone ID would come from spawn location)
zoneID := int32(1) // TODO: Get actual zone ID from spawn
m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs)
// Load harvest entries and items
if err := m.loadGroundSpawnData(gs); err != nil && m.logger != nil {
m.logger.LogWarning("Failed to load data for ground spawn %d: %v", gs.GetID(), err)
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d ground spawns from database", len(groundSpawns))
}
return nil
}
// loadGroundSpawnData loads entries and items for a ground spawn
func (m *Manager) loadGroundSpawnData(gs *GroundSpawn) error {
groundspawnID := gs.GetGroundSpawnEntryID()
// Load harvest entries
entries, err := m.database.LoadGroundSpawnEntries(groundspawnID)
if err != nil {
return fmt.Errorf("failed to load entries for groundspawn %d: %w", groundspawnID, err)
}
m.entriesByID[groundspawnID] = entries
// Load harvest items
items, err := m.database.LoadGroundSpawnItems(groundspawnID)
if err != nil {
return fmt.Errorf("failed to load items for groundspawn %d: %w", groundspawnID, err)
}
m.itemsByID[groundspawnID] = items
return nil
}
// CreateGroundSpawn creates a new ground spawn
func (m *Manager) CreateGroundSpawn(config GroundSpawnConfig) *GroundSpawn {
gs := NewGroundSpawn(config)
m.mutex.Lock()
defer m.mutex.Unlock()
// Generate ID (placeholder implementation)
newID := int32(len(m.groundSpawns) + 1)
gs.SetID(newID)
// Store ground spawn
m.groundSpawns[newID] = gs
// Group by zone
zoneID := int32(1) // TODO: Get actual zone ID from config.Location
m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs)
if m.logger != nil {
m.logger.LogInfo("Created ground spawn %d: %s", newID, gs.GetName())
}
return gs
}
// GetGroundSpawn returns a ground spawn by ID
func (m *Manager) GetGroundSpawn(id int32) *GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.groundSpawns[id]
}
// GetGroundSpawnsByZone returns all ground spawns in a zone
func (m *Manager) GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
spawns := m.spawnsByZone[zoneID]
if spawns == nil {
return []*GroundSpawn{}
}
// Return a copy to prevent external modification
result := make([]*GroundSpawn, len(spawns))
copy(result, spawns)
return result
}
// ProcessHarvest handles harvesting for a player
func (m *Manager) ProcessHarvest(gs *GroundSpawn, player *Player) (*HarvestResult, error) {
if gs == nil {
return nil, fmt.Errorf("ground spawn cannot be nil")
}
if player == nil {
return nil, fmt.Errorf("player cannot be nil")
}
// Record statistics
m.mutex.Lock()
m.totalHarvests++
skill := gs.GetCollectionSkill()
m.harvestsBySkill[skill]++
m.mutex.Unlock()
// Build harvest context
context, err := m.buildHarvestContext(gs, player)
if err != nil {
return nil, fmt.Errorf("failed to build harvest context: %w", err)
}
// Process the harvest
result, err := gs.ProcessHarvest(context)
if err != nil {
return nil, fmt.Errorf("harvest processing failed: %w", err)
}
// Update statistics
if result != nil && result.Success {
m.mutex.Lock()
m.successfulHarvests++
// Count rare items
for _, item := range result.ItemsAwarded {
if item.IsRare {
m.rareItemsHarvested++
}
}
if result.SkillGained {
m.skillUpsGenerated++
}
m.mutex.Unlock()
}
// Handle respawn if depleted
if gs.IsDepleted() {
m.scheduleRespawn(gs)
}
return result, nil
}
// buildHarvestContext creates a harvest context for processing
func (m *Manager) buildHarvestContext(gs *GroundSpawn, player *Player) (*HarvestContext, error) {
groundspawnID := gs.GetGroundSpawnEntryID()
m.mutex.RLock()
entries := m.entriesByID[groundspawnID]
items := m.itemsByID[groundspawnID]
m.mutex.RUnlock()
if entries == nil || len(entries) == 0 {
return nil, fmt.Errorf("no harvest entries found for groundspawn %d", groundspawnID)
}
if items == nil || len(items) == 0 {
return nil, fmt.Errorf("no harvest items found for groundspawn %d", groundspawnID)
}
// Get player skill
skillName := gs.GetCollectionSkill()
if skillName == SkillCollecting {
skillName = SkillGathering // Collections use gathering skill
}
playerSkill := player.GetSkillByName(skillName)
if playerSkill == nil {
return nil, fmt.Errorf("player lacks required skill: %s", skillName)
}
// Calculate total skill (base + bonuses)
totalSkill := playerSkill.GetCurrentValue()
// TODO: Add stat bonuses when stat system is integrated
// Find max skill required
var maxSkillRequired int16
for _, entry := range entries {
if entry.MinSkillLevel > maxSkillRequired {
maxSkillRequired = entry.MinSkillLevel
}
}
return &HarvestContext{
Player: player,
GroundSpawn: gs,
PlayerSkill: playerSkill,
TotalSkill: totalSkill,
GroundSpawnEntries: entries,
GroundSpawnItems: items,
IsCollection: gs.GetCollectionSkill() == SkillCollecting,
MaxSkillRequired: maxSkillRequired,
}, nil
}
// scheduleRespawn schedules a ground spawn for respawn
func (m *Manager) scheduleRespawn(gs *GroundSpawn) {
if gs == nil {
return
}
// TODO: Get respawn timer from configuration or database
respawnDelay := 5 * time.Minute // Default 5 minutes
respawnTime := time.Now().Add(respawnDelay)
m.mutex.Lock()
m.respawnQueue[gs.GetID()] = respawnTime
m.mutex.Unlock()
if m.logger != nil {
m.logger.LogDebug("Scheduled ground spawn %d for respawn at %v", gs.GetID(), respawnTime)
}
}
// ProcessRespawns handles ground spawn respawning
func (m *Manager) ProcessRespawns() {
now := time.Now()
var toRespawn []int32
m.mutex.Lock()
for spawnID, respawnTime := range m.respawnQueue {
if now.After(respawnTime) {
toRespawn = append(toRespawn, spawnID)
delete(m.respawnQueue, spawnID)
}
}
m.mutex.Unlock()
// Respawn outside of lock
for _, spawnID := range toRespawn {
if gs := m.GetGroundSpawn(spawnID); gs != nil {
gs.Respawn()
if m.logger != nil {
m.logger.LogDebug("Ground spawn %d respawned", spawnID)
}
}
}
}
// GetStatistics returns ground spawn system statistics
func (m *Manager) GetStatistics() *HarvestStatistics {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Count spawns by zone
spawnsByZone := make(map[int32]int)
for zoneID, spawns := range m.spawnsByZone {
spawnsByZone[zoneID] = len(spawns)
}
// Copy harvests by skill
harvestsBySkill := make(map[string]int64)
for skill, count := range m.harvestsBySkill {
harvestsBySkill[skill] = count
}
return &HarvestStatistics{
TotalHarvests: m.totalHarvests,
SuccessfulHarvests: m.successfulHarvests,
RareItemsHarvested: m.rareItemsHarvested,
SkillUpsGenerated: m.skillUpsGenerated,
HarvestsBySkill: harvestsBySkill,
ActiveGroundSpawns: len(m.groundSpawns),
GroundSpawnsByZone: spawnsByZone,
}
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalHarvests = 0
m.successfulHarvests = 0
m.rareItemsHarvested = 0
m.skillUpsGenerated = 0
m.harvestsBySkill = make(map[string]int64)
}
// AddGroundSpawn adds a ground spawn to the manager
func (m *Manager) AddGroundSpawn(gs *GroundSpawn) error {
if gs == nil {
return fmt.Errorf("ground spawn cannot be nil")
}
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if ID is already used
if _, exists := m.groundSpawns[gs.GetID()]; exists {
return fmt.Errorf("ground spawn with ID %d already exists", gs.GetID())
}
m.groundSpawns[gs.GetID()] = gs
// Group by zone (placeholder)
zoneID := int32(1) // TODO: Get actual zone ID
m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs)
// Load harvest data if database is available
if m.database != nil {
if err := m.loadGroundSpawnData(gs); err != nil && m.logger != nil {
m.logger.LogWarning("Failed to load data for ground spawn %d: %v", gs.GetID(), err)
}
}
return nil
}
// RemoveGroundSpawn removes a ground spawn from the manager
func (m *Manager) RemoveGroundSpawn(id int32) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
gs, exists := m.groundSpawns[id]
if !exists {
return false
}
delete(m.groundSpawns, id)
delete(m.respawnQueue, id)
// Remove from zone list
// TODO: Get actual zone ID from ground spawn
zoneID := int32(1)
if spawns, exists := m.spawnsByZone[zoneID]; exists {
for i, spawn := range spawns {
if spawn.GetID() == id {
m.spawnsByZone[zoneID] = append(spawns[:i], spawns[i+1:]...)
break
}
}
}
// Clean up harvest data
if gs != nil {
groundspawnID := gs.GetGroundSpawnEntryID()
delete(m.entriesByID, groundspawnID)
delete(m.itemsByID, groundspawnID)
}
return true
}
// GetGroundSpawnCount returns the total number of ground spawns
func (m *Manager) GetGroundSpawnCount() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.groundSpawns)
}
// GetActiveGroundSpawns returns all active (harvestable) ground spawns
func (m *Manager) GetActiveGroundSpawns() []*GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
var active []*GroundSpawn
for _, gs := range m.groundSpawns {
if gs.IsAvailable() {
active = append(active, gs)
}
}
return active
}
// GetDepletedGroundSpawns returns all depleted ground spawns
func (m *Manager) GetDepletedGroundSpawns() []*GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
var depleted []*GroundSpawn
for _, gs := range m.groundSpawns {
if gs.IsDepleted() {
depleted = append(depleted, gs)
}
}
return depleted
}
// ProcessCommand handles ground spawn management commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "list":
return m.handleListCommand(args)
case "respawn":
return m.handleRespawnCommand(args)
case "info":
return m.handleInfoCommand(args)
case "reload":
return m.handleReloadCommand(args)
default:
return "", fmt.Errorf("unknown ground spawn command: %s", command)
}
}
// handleStatsCommand shows ground spawn system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Ground Spawn System Statistics:\n"
result += fmt.Sprintf("Total Harvests: %d\n", stats.TotalHarvests)
result += fmt.Sprintf("Successful Harvests: %d\n", stats.SuccessfulHarvests)
result += fmt.Sprintf("Rare Items Harvested: %d\n", stats.RareItemsHarvested)
result += fmt.Sprintf("Skill Ups Generated: %d\n", stats.SkillUpsGenerated)
result += fmt.Sprintf("Active Ground Spawns: %d\n", stats.ActiveGroundSpawns)
if len(stats.HarvestsBySkill) > 0 {
result += "\nHarvests by Skill:\n"
for skill, count := range stats.HarvestsBySkill {
result += fmt.Sprintf(" %s: %d\n", skill, count)
}
}
return result, nil
}
// handleListCommand lists ground spawns
func (m *Manager) handleListCommand(args []string) (string, error) {
count := m.GetGroundSpawnCount()
if count == 0 {
return "No ground spawns loaded.", nil
}
active := m.GetActiveGroundSpawns()
depleted := m.GetDepletedGroundSpawns()
result := fmt.Sprintf("Ground Spawns (Total: %d, Active: %d, Depleted: %d):\n",
count, len(active), len(depleted))
// Show first 10 active spawns
shown := 0
for _, gs := range active {
if shown >= 10 {
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s (%s) - %d harvests remaining\n",
gs.GetID(), gs.GetName(), gs.GetCollectionSkill(), gs.GetNumberHarvests())
shown++
}
return result, nil
}
// handleRespawnCommand respawns ground spawns
func (m *Manager) handleRespawnCommand(args []string) (string, error) {
if len(args) > 0 {
// Respawn specific ground spawn
var spawnID int32
if _, err := fmt.Sscanf(args[0], "%d", &spawnID); err != nil {
return "", fmt.Errorf("invalid ground spawn ID: %s", args[0])
}
gs := m.GetGroundSpawn(spawnID)
if gs == nil {
return fmt.Sprintf("Ground spawn %d not found.", spawnID), nil
}
gs.Respawn()
return fmt.Sprintf("Ground spawn %d respawned.", spawnID), nil
}
// Respawn all depleted spawns
depleted := m.GetDepletedGroundSpawns()
for _, gs := range depleted {
gs.Respawn()
}
return fmt.Sprintf("Respawned %d depleted ground spawns.", len(depleted)), nil
}
// handleInfoCommand shows information about a specific ground spawn
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("ground spawn ID required")
}
var spawnID int32
if _, err := fmt.Sscanf(args[0], "%d", &spawnID); err != nil {
return "", fmt.Errorf("invalid ground spawn ID: %s", args[0])
}
gs := m.GetGroundSpawn(spawnID)
if gs == nil {
return fmt.Sprintf("Ground spawn %d not found.", spawnID), nil
}
result := fmt.Sprintf("Ground Spawn Information:\n")
result += fmt.Sprintf("ID: %d\n", gs.GetID())
result += fmt.Sprintf("Name: %s\n", gs.GetName())
result += fmt.Sprintf("Collection Skill: %s\n", gs.GetCollectionSkill())
result += fmt.Sprintf("Harvests Remaining: %d\n", gs.GetNumberHarvests())
result += fmt.Sprintf("Attempts per Harvest: %d\n", gs.GetAttemptsPerHarvest())
result += fmt.Sprintf("Ground Spawn Entry ID: %d\n", gs.GetGroundSpawnEntryID())
result += fmt.Sprintf("Available: %v\n", gs.IsAvailable())
result += fmt.Sprintf("Depleted: %v\n", gs.IsDepleted())
return result, nil
}
// handleReloadCommand reloads ground spawns from database
func (m *Manager) handleReloadCommand(args []string) (string, error) {
if m.database == nil {
return "", fmt.Errorf("no database available")
}
// Clear current data
m.mutex.Lock()
m.groundSpawns = make(map[int32]*GroundSpawn)
m.spawnsByZone = make(map[int32][]*GroundSpawn)
m.entriesByID = make(map[int32][]*GroundSpawnEntry)
m.itemsByID = make(map[int32][]*GroundSpawnEntryItem)
m.respawnQueue = make(map[int32]time.Time)
m.mutex.Unlock()
// Reload from database
if err := m.Initialize(); err != nil {
return "", fmt.Errorf("failed to reload ground spawns: %w", err)
}
count := m.GetGroundSpawnCount()
return fmt.Sprintf("Successfully reloaded %d ground spawns from database.", count), nil
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down ground spawn manager...")
}
m.mutex.Lock()
defer m.mutex.Unlock()
// Clear all data
m.groundSpawns = make(map[int32]*GroundSpawn)
m.spawnsByZone = make(map[int32][]*GroundSpawn)
m.entriesByID = make(map[int32][]*GroundSpawnEntry)
m.itemsByID = make(map[int32][]*GroundSpawnEntryItem)
m.respawnQueue = make(map[int32]time.Time)
}

View File

@ -0,0 +1,137 @@
package ground_spawn
import (
"sync"
"time"
"eq2emu/internal/common"
"eq2emu/internal/spawn"
)
// GroundSpawn represents a harvestable resource node in the game world
type GroundSpawn struct {
*spawn.Spawn // Embed spawn for base functionality
numberHarvests int8 // Number of harvests remaining
numAttemptsPerHarvest int8 // Attempts per harvest session
groundspawnID int32 // Database ID for this groundspawn entry
collectionSkill string // Required skill for harvesting
randomizeHeading bool // Whether to randomize heading on spawn
harvestMutex sync.Mutex // Thread safety for harvest operations
harvestUseMutex sync.Mutex // Thread safety for use operations
}
// GroundSpawnEntry represents harvest table data from database
type GroundSpawnEntry struct {
MinSkillLevel int16 // Minimum skill level required
MinAdventureLevel int16 // Minimum adventure level required
BonusTable bool // Whether this is a bonus table
Harvest1 float32 // Chance for 1 item (percentage)
Harvest3 float32 // Chance for 3 items (percentage)
Harvest5 float32 // Chance for 5 items (percentage)
HarvestImbue float32 // Chance for imbue item (percentage)
HarvestRare float32 // Chance for rare item (percentage)
Harvest10 float32 // Chance for 10 + rare items (percentage)
HarvestCoin float32 // Chance for coin reward (percentage)
}
// GroundSpawnEntryItem represents items that can be harvested
type GroundSpawnEntryItem struct {
ItemID int32 // Item database ID
IsRare int8 // 0=normal, 1=rare, 2=imbue
GridID int32 // Grid restriction (0=any)
Quantity int16 // Item quantity (usually 1)
}
// HarvestResult represents the outcome of a harvest attempt
type HarvestResult struct {
Success bool // Whether harvest succeeded
HarvestType int8 // Type of harvest achieved
ItemsAwarded []*HarvestedItem // Items given to player
MessageText string // Message to display to player
SkillGained bool // Whether skill was gained
Error error // Any error that occurred
}
// HarvestedItem represents an item awarded from harvesting
type HarvestedItem struct {
ItemID int32 // Database item ID
Quantity int16 // Number of items
IsRare bool // Whether this is a rare item
Name string // Item name for messages
}
// HarvestContext contains all data needed for a harvest operation
type HarvestContext struct {
Player *Player // Player attempting harvest
GroundSpawn *GroundSpawn // The ground spawn being harvested
PlayerSkill *Skill // Player's harvesting skill
TotalSkill int16 // Total skill including bonuses
GroundSpawnEntries []*GroundSpawnEntry // Available harvest tables
GroundSpawnItems []*GroundSpawnEntryItem // Available harvest items
IsCollection bool // Whether this is collection harvesting
MaxSkillRequired int16 // Maximum skill required for any table
}
// SpawnLocation represents a spawn position with grid information
type SpawnLocation struct {
X float32 // World X coordinate
Y float32 // World Y coordinate
Z float32 // World Z coordinate
Heading float32 // Spawn heading/rotation
GridID int32 // Grid zone identifier
}
// HarvestModifiers contains modifiers that affect harvesting
type HarvestModifiers struct {
SkillMultiplier float32 // Skill gain multiplier
RareChanceBonus float32 // Bonus to rare item chance
QuantityMultiplier float32 // Quantity multiplier
LuckModifier int16 // Player luck modifier
}
// GroundSpawnConfig contains configuration for ground spawn creation
type GroundSpawnConfig struct {
GroundSpawnID int32 // Database entry ID
CollectionSkill string // Required harvesting skill
NumberHarvests int8 // Harvests before depletion
AttemptsPerHarvest int8 // Attempts per harvest session
RandomizeHeading bool // Randomize spawn heading
RespawnTimer time.Duration // Time before respawn
Location SpawnLocation // Spawn position
Name string // Display name
Description string // Spawn description
}
// Manager manages all ground spawn operations
type Manager struct {
groundSpawns map[int32]*GroundSpawn // Active ground spawns by ID
spawnsByZone map[int32][]*GroundSpawn // Ground spawns by zone ID
entriesByID map[int32][]*GroundSpawnEntry // Harvest entries by groundspawn ID
itemsByID map[int32][]*GroundSpawnEntryItem // Harvest items by groundspawn ID
respawnQueue map[int32]time.Time // Respawn timestamps
database Database // Database interface
logger Logger // Logging interface
mutex sync.RWMutex // Thread safety
// Statistics
totalHarvests int64 // Total harvest attempts
successfulHarvests int64 // Successful harvests
rareItemsHarvested int64 // Rare items harvested
skillUpsGenerated int64 // Skill increases given
harvestsBySkill map[string]int64 // Harvests by skill type
}
// HarvestStatistics contains harvest system statistics
type HarvestStatistics struct {
TotalHarvests int64 `json:"total_harvests"`
SuccessfulHarvests int64 `json:"successful_harvests"`
RareItemsHarvested int64 `json:"rare_items_harvested"`
SkillUpsGenerated int64 `json:"skill_ups_generated"`
HarvestsBySkill map[string]int64 `json:"harvests_by_skill"`
ActiveGroundSpawns int `json:"active_ground_spawns"`
GroundSpawnsByZone map[int32]int `json:"ground_spawns_by_zone"`
}

View File

@ -0,0 +1,31 @@
package sign
// Sign type constants
const (
SignTypeGeneric = 0
SignTypeZone = 1
)
// Default spawn settings for signs
const (
DefaultSpawnType = 2 // Signs are spawn type 2
DefaultActivityStatus = 64 // Activity status for signs
DefaultPosState = 1 // Position state
DefaultDifficulty = 0 // No difficulty for signs
)
// Channel colors for messages (these would be defined elsewhere in a real implementation)
const (
ChannelColorYellow = 15 // Yellow text channel
)
// Sign database constants
const (
MaxSignTitleLength = 255
MaxSignDescriptionLength = 1024
)
// Distance checking constants
const (
DefaultSignDistance = 0.0 // 0 = no distance limit
)

226
internal/sign/interfaces.go Normal file
View File

@ -0,0 +1,226 @@
package sign
import "eq2emu/internal/spawn"
// Player interface for sign interactions
type Player interface {
GetDistance(target *spawn.Spawn) float32
SetX(x float32)
SetY(y float32)
SetZ(z float32)
SetHeading(heading float32)
GetZone() Zone
GetTarget() *spawn.Spawn
}
// Client interface for sign interactions
type Client interface {
GetPlayer() Player
GetCharacterID() int32
GetDatabase() Database
GetCurrentZone() Zone
SetTemporaryTransportID(id int32)
SimpleMessage(channel int32, message string)
Message(channel int32, format string, args ...interface{})
CheckZoneAccess(zoneName string) bool
TryZoneInstance(zoneID int32, useDefaults bool) bool
Zone(zoneName string, useDefaults bool) error
ProcessTeleport(sign *Sign, destinations []TransportDestination, transporterID int32) error
}
// Zone interface for sign interactions
type Zone interface {
GetTransporters(client Client, transporterID int32) ([]TransportDestination, error)
ProcessEntityCommand(command *EntityCommand, player Player, target *spawn.Spawn) error
}
// Database interface for sign persistence
type Database interface {
GetZoneName(zoneID int32) (string, error)
GetCharacterName(charID int32) (string, error)
SaveSignMark(charID int32, widgetID int32, charName string, client Client) error
LoadSigns(zoneID int32) ([]*Sign, error)
SaveSign(sign *Sign) error
DeleteSign(signID int32) error
}
// TransportDestination represents a transport destination
type TransportDestination struct {
ID int32
Name string
Description string
ZoneID int32
X float32
Y float32
Z float32
Heading float32
}
// EntityCommand represents a command that can be executed on an entity
type EntityCommand struct {
ID int32
Command string
Name string
Description string
}
// Logger interface for sign logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// SignSpawn provides sign functionality for spawn entities
type SignSpawn struct {
*spawn.Spawn
*Sign
}
// NewSignSpawn creates a new sign spawn wrapper
func NewSignSpawn(baseSpawn *spawn.Spawn) *SignSpawn {
sign := NewSign()
sign.Spawn = baseSpawn
return &SignSpawn{
Spawn: baseSpawn,
Sign: sign,
}
}
// IsSign returns true since this is a sign
func (ss *SignSpawn) IsSign() bool {
return true
}
// HandleUse delegates to the sign's HandleUse method
func (ss *SignSpawn) HandleUse(client Client, command string) error {
return ss.Sign.HandleUse(client, command)
}
// Copy creates a copy of the sign spawn
func (ss *SignSpawn) Copy() *SignSpawn {
newSign := ss.Sign.Copy()
newSpawn := ss.Spawn.Copy()
return &SignSpawn{
Spawn: newSpawn,
Sign: newSign,
}
}
// SignAware interface for entities that can interact with signs
type SignAware interface {
GetSign() *Sign
IsSign() bool
HandleSignUse(client Client, command string) error
}
// SignAdapter provides sign functionality for any entity
type SignAdapter struct {
entity Entity
sign *Sign
logger Logger
}
// Entity interface for things that can have sign functionality
type Entity interface {
GetID() int32
GetName() string
GetDatabaseID() int32
}
// NewSignAdapter creates a new sign adapter
func NewSignAdapter(entity Entity, logger Logger) *SignAdapter {
return &SignAdapter{
entity: entity,
sign: NewSign(),
logger: logger,
}
}
// GetSign returns the sign
func (sa *SignAdapter) GetSign() *Sign {
return sa.sign
}
// IsSign returns true since this has sign functionality
func (sa *SignAdapter) IsSign() bool {
return true
}
// HandleSignUse handles sign usage
func (sa *SignAdapter) HandleSignUse(client Client, command string) error {
if sa.logger != nil {
sa.logger.LogDebug("Entity %d (%s): Handling sign use with command '%s'",
sa.entity.GetID(), sa.entity.GetName(), command)
}
return sa.sign.HandleUse(client, command)
}
// SetSignTitle sets the sign title
func (sa *SignAdapter) SetSignTitle(title string) {
sa.sign.SetSignTitle(title)
if sa.logger != nil {
sa.logger.LogDebug("Entity %d (%s): Set sign title to '%s'",
sa.entity.GetID(), sa.entity.GetName(), title)
}
}
// SetSignDescription sets the sign description
func (sa *SignAdapter) SetSignDescription(description string) {
sa.sign.SetSignDescription(description)
if sa.logger != nil {
sa.logger.LogDebug("Entity %d (%s): Set sign description",
sa.entity.GetID(), sa.entity.GetName())
}
}
// SetSignType sets the sign type
func (sa *SignAdapter) SetSignType(signType int8) {
sa.sign.SetSignType(signType)
if sa.logger != nil {
sa.logger.LogDebug("Entity %d (%s): Set sign type to %d",
sa.entity.GetID(), sa.entity.GetName(), signType)
}
}
// SetZoneTransport configures the sign for zone transport
func (sa *SignAdapter) SetZoneTransport(zoneID int32, x, y, z, heading float32) {
sa.sign.SetSignType(SignTypeZone)
sa.sign.SetSignZoneID(zoneID)
sa.sign.SetSignZoneX(x)
sa.sign.SetSignZoneY(y)
sa.sign.SetSignZoneZ(z)
sa.sign.SetSignZoneHeading(heading)
if sa.logger != nil {
sa.logger.LogDebug("Entity %d (%s): Configured zone transport to zone %d at (%.2f, %.2f, %.2f)",
sa.entity.GetID(), sa.entity.GetName(), zoneID, x, y, z)
}
}
// SetSignDistance sets the interaction distance
func (sa *SignAdapter) SetSignDistance(distance float32) {
sa.sign.SetSignDistance(distance)
if sa.logger != nil {
sa.logger.LogDebug("Entity %d (%s): Set sign distance to %.2f",
sa.entity.GetID(), sa.entity.GetName(), distance)
}
}
// Validate validates the sign configuration
func (sa *SignAdapter) Validate() []string {
return sa.sign.Validate()
}
// IsValid returns true if the sign is valid
func (sa *SignAdapter) IsValid() bool {
return sa.sign.IsValid()
}

471
internal/sign/manager.go Normal file
View File

@ -0,0 +1,471 @@
package sign
import (
"fmt"
"sync"
)
// Manager provides high-level management of the sign system
type Manager struct {
signs map[int32]*Sign // Signs by ID
signsByZone map[int32][]*Sign // Signs by zone ID
signsByWidget map[int32]*Sign // Signs by widget ID
database Database
logger Logger
mutex sync.RWMutex
// Statistics
totalSigns int64
signsByType map[int8]int64 // Sign type -> count
signInteractions int64
zoneTransports int64
transporterUses int64
}
// NewManager creates a new sign manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
signs: make(map[int32]*Sign),
signsByZone: make(map[int32][]*Sign),
signsByWidget: make(map[int32]*Sign),
database: database,
logger: logger,
signsByType: make(map[int8]int64),
}
}
// Initialize loads signs from database
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing sign manager...")
}
// TODO: Load all signs from database when database system is integrated
// This would typically iterate through all zones and load their signs
return nil
}
// LoadZoneSigns loads signs for a specific zone
func (m *Manager) LoadZoneSigns(zoneID int32) error {
if m.database == nil {
return fmt.Errorf("database is nil")
}
signs, err := m.database.LoadSigns(zoneID)
if err != nil {
return fmt.Errorf("failed to load signs for zone %d: %w", zoneID, err)
}
m.mutex.Lock()
defer m.mutex.Unlock()
for _, sign := range signs {
m.addSignUnsafe(sign)
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d signs for zone %d", len(signs), zoneID)
}
return nil
}
// AddSign adds a sign to the manager
func (m *Manager) AddSign(sign *Sign) error {
if sign == nil {
return fmt.Errorf("sign is nil")
}
// Validate the sign
if issues := sign.Validate(); len(issues) > 0 {
return fmt.Errorf("sign validation failed: %v", issues)
}
m.mutex.Lock()
defer m.mutex.Unlock()
m.addSignUnsafe(sign)
if m.logger != nil {
m.logger.LogInfo("Added sign %d (widget %d) of type %d",
sign.Spawn.GetDatabaseID(), sign.GetWidgetID(), sign.GetSignType())
}
return nil
}
// addSignUnsafe adds a sign without locking (internal use)
func (m *Manager) addSignUnsafe(sign *Sign) {
signID := sign.Spawn.GetDatabaseID()
widgetID := sign.GetWidgetID()
// Add to main collection
m.signs[signID] = sign
// Add to widget collection
if widgetID > 0 {
m.signsByWidget[widgetID] = sign
}
// Add to zone collection
if sign.Spawn != nil {
zoneID := sign.Spawn.GetZone()
m.signsByZone[zoneID] = append(m.signsByZone[zoneID], sign)
}
// Update statistics
m.totalSigns++
m.signsByType[sign.GetSignType()]++
}
// RemoveSign removes a sign from the manager
func (m *Manager) RemoveSign(signID int32) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
sign, exists := m.signs[signID]
if !exists {
return false
}
// Remove from main collection
delete(m.signs, signID)
// Remove from widget collection
if sign.GetWidgetID() > 0 {
delete(m.signsByWidget, sign.GetWidgetID())
}
// Remove from zone collection
if sign.Spawn != nil {
zoneID := sign.Spawn.GetZone()
if zoneSigns, exists := m.signsByZone[zoneID]; exists {
for i, zoneSign := range zoneSigns {
if zoneSign == sign {
m.signsByZone[zoneID] = append(zoneSigns[:i], zoneSigns[i+1:]...)
break
}
}
}
}
// Update statistics
m.totalSigns--
m.signsByType[sign.GetSignType()]--
if m.logger != nil {
m.logger.LogInfo("Removed sign %d (widget %d)", signID, sign.GetWidgetID())
}
return true
}
// GetSign returns a sign by ID
func (m *Manager) GetSign(signID int32) *Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.signs[signID]
}
// GetSignByWidget returns a sign by widget ID
func (m *Manager) GetSignByWidget(widgetID int32) *Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.signsByWidget[widgetID]
}
// GetZoneSigns returns all signs in a zone
func (m *Manager) GetZoneSigns(zoneID int32) []*Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
signs := m.signsByZone[zoneID]
// Return a copy to prevent external modification
result := make([]*Sign, len(signs))
copy(result, signs)
return result
}
// GetSignsByType returns all signs of a specific type
func (m *Manager) GetSignsByType(signType int8) []*Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
var result []*Sign
for _, sign := range m.signs {
if sign.GetSignType() == signType {
result = append(result, sign)
}
}
return result
}
// SaveSign saves a sign to database
func (m *Manager) SaveSign(sign *Sign) error {
if m.database == nil {
return fmt.Errorf("database is nil")
}
if sign == nil {
return fmt.Errorf("sign is nil")
}
err := m.database.SaveSign(sign)
if err != nil {
return fmt.Errorf("failed to save sign: %w", err)
}
if m.logger != nil {
m.logger.LogDebug("Saved sign %d to database", sign.Spawn.GetDatabaseID())
}
return nil
}
// HandleSignUse processes sign usage and records statistics
func (m *Manager) HandleSignUse(sign *Sign, client Client, command string) error {
if sign == nil {
return fmt.Errorf("sign is nil")
}
m.mutex.Lock()
m.signInteractions++
m.mutex.Unlock()
err := sign.HandleUse(client, command)
// Record specific interaction types for statistics
if err == nil {
m.mutex.Lock()
if sign.IsZoneSign() {
m.zoneTransports++
}
if sign.Spawn != nil && sign.Spawn.GetTransporterID() > 0 {
m.transporterUses++
}
m.mutex.Unlock()
}
if m.logger != nil {
if err != nil {
m.logger.LogError("Sign %d use failed: %v", sign.Spawn.GetDatabaseID(), err)
} else {
m.logger.LogDebug("Sign %d used successfully by character %d",
sign.Spawn.GetDatabaseID(), client.GetCharacterID())
}
}
return err
}
// GetStatistics returns sign system statistics
func (m *Manager) GetStatistics() map[string]interface{} {
m.mutex.RLock()
defer m.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_signs"] = m.totalSigns
stats["sign_interactions"] = m.signInteractions
stats["zone_transports"] = m.zoneTransports
stats["transporter_uses"] = m.transporterUses
// Copy sign type statistics
typeStats := make(map[int8]int64)
for signType, count := range m.signsByType {
typeStats[signType] = count
}
stats["signs_by_type"] = typeStats
// Zone statistics
zoneStats := make(map[int32]int)
for zoneID, signs := range m.signsByZone {
zoneStats[zoneID] = len(signs)
}
stats["signs_by_zone"] = zoneStats
return stats
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.signInteractions = 0
m.zoneTransports = 0
m.transporterUses = 0
}
// ValidateAllSigns validates all signs in the system
func (m *Manager) ValidateAllSigns() map[int32][]string {
m.mutex.RLock()
defer m.mutex.RUnlock()
issues := make(map[int32][]string)
for signID, sign := range m.signs {
if signIssues := sign.Validate(); len(signIssues) > 0 {
issues[signID] = signIssues
}
}
return issues
}
// GetSignCount returns the total number of signs
func (m *Manager) GetSignCount() int64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.totalSigns
}
// GetSignTypeCount returns the number of signs of a specific type
func (m *Manager) GetSignTypeCount(signType int8) int64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.signsByType[signType]
}
// ProcessCommand handles sign-related commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "list":
return m.handleListCommand(args)
case "info":
return m.handleInfoCommand(args)
default:
return "", fmt.Errorf("unknown sign command: %s", command)
}
}
// handleStatsCommand shows sign system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Sign System Statistics:\n"
result += fmt.Sprintf("Total Signs: %d\n", stats["total_signs"])
result += fmt.Sprintf("Sign Interactions: %d\n", stats["sign_interactions"])
result += fmt.Sprintf("Zone Transports: %d\n", stats["zone_transports"])
result += fmt.Sprintf("Transporter Uses: %d\n", stats["transporter_uses"])
typeStats := stats["signs_by_type"].(map[int8]int64)
result += fmt.Sprintf("Generic Signs: %d\n", typeStats[SignTypeGeneric])
result += fmt.Sprintf("Zone Signs: %d\n", typeStats[SignTypeZone])
return result, nil
}
// handleValidateCommand validates all signs
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateAllSigns()
if len(issues) == 0 {
return "All signs are valid.", nil
}
result := fmt.Sprintf("Found issues with %d signs:\n", len(issues))
count := 0
for signID, signIssues := range issues {
if count >= 10 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf("Sign %d:\n", signID)
for _, issue := range signIssues {
result += fmt.Sprintf(" - %s\n", issue)
}
count++
}
return result, nil
}
// handleListCommand lists signs
func (m *Manager) handleListCommand(args []string) (string, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
if len(m.signs) == 0 {
return "No signs loaded.", nil
}
result := fmt.Sprintf("Signs (%d):\n", len(m.signs))
count := 0
for signID, sign := range m.signs {
if count >= 20 { // Limit output
result += "... (and more)\n"
break
}
typeName := "Generic"
if sign.GetSignType() == SignTypeZone {
typeName = "Zone"
}
result += fmt.Sprintf(" %d: %s (%s, Widget: %d)\n",
signID, sign.GetSignTitle(), typeName, sign.GetWidgetID())
count++
}
return result, nil
}
// handleInfoCommand shows information about a specific sign
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("sign ID required")
}
// Parse sign ID
var signID int32
if _, err := fmt.Sscanf(args[0], "%d", &signID); err != nil {
return "", fmt.Errorf("invalid sign ID: %s", args[0])
}
sign := m.GetSign(signID)
if sign == nil {
return fmt.Sprintf("Sign %d not found.", signID), nil
}
result := fmt.Sprintf("Sign Information:\n")
result += fmt.Sprintf("ID: %d\n", signID)
result += fmt.Sprintf("Widget ID: %d\n", sign.GetWidgetID())
result += fmt.Sprintf("Type: %d\n", sign.GetSignType())
result += fmt.Sprintf("Title: %s\n", sign.GetSignTitle())
result += fmt.Sprintf("Description: %s\n", sign.GetSignDescription())
result += fmt.Sprintf("Language: %d\n", sign.GetLanguage())
if sign.IsZoneSign() {
result += fmt.Sprintf("Zone ID: %d\n", sign.GetSignZoneID())
result += fmt.Sprintf("Zone Coords: (%.2f, %.2f, %.2f)\n",
sign.GetSignZoneX(), sign.GetSignZoneY(), sign.GetSignZoneZ())
result += fmt.Sprintf("Zone Heading: %.2f\n", sign.GetSignZoneHeading())
result += fmt.Sprintf("Distance: %.2f\n", sign.GetSignDistance())
}
result += fmt.Sprintf("Include Location: %t\n", sign.GetIncludeLocation())
result += fmt.Sprintf("Include Heading: %t\n", sign.GetIncludeHeading())
return result, nil
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down sign manager...")
}
// Nothing to clean up currently, but placeholder for future cleanup
}

297
internal/sign/sign.go Normal file
View File

@ -0,0 +1,297 @@
package sign
import (
"fmt"
"math/rand"
"strings"
"eq2emu/internal/spawn"
)
// Copy creates a deep copy of the sign with size randomization
func (s *Sign) Copy() *Sign {
newSign := NewSign()
// Copy spawn data
if s.Spawn != nil {
// Handle size randomization like the C++ version
if s.Spawn.GetSizeOffset() > 0 {
offset := s.Spawn.GetSizeOffset() + 1
tmpSize := int32(s.Spawn.GetSize()) + (rand.Int31n(int32(offset)) - rand.Int31n(int32(offset)))
if tmpSize < 0 {
tmpSize = 1
} else if tmpSize >= 0xFFFF {
tmpSize = 0xFFFF
}
newSign.Spawn.SetSize(int16(tmpSize))
} else {
newSign.Spawn.SetSize(s.Spawn.GetSize())
}
// Copy other spawn properties
newSign.Spawn.SetDatabaseID(s.Spawn.GetDatabaseID())
newSign.Spawn.SetMerchantID(s.Spawn.GetMerchantID())
newSign.Spawn.SetMerchantType(s.Spawn.GetMerchantType())
// TODO: Copy appearance data when spawn system is fully integrated
// TODO: Copy command lists when command system is integrated
// TODO: Copy transporter ID, sounds, loot properties, etc.
}
// Copy sign-specific properties
newSign.widgetID = s.widgetID
newSign.widgetX = s.widgetX
newSign.widgetY = s.widgetY
newSign.widgetZ = s.widgetZ
newSign.signType = s.signType
newSign.title = s.title
newSign.description = s.description
newSign.language = s.language
newSign.zoneX = s.zoneX
newSign.zoneY = s.zoneY
newSign.zoneZ = s.zoneZ
newSign.zoneHeading = s.zoneHeading
newSign.zoneID = s.zoneID
newSign.signDistance = s.signDistance
newSign.includeLocation = s.includeLocation
newSign.includeHeading = s.includeHeading
return newSign
}
// Serialize creates a packet for sending the sign to a client
func (s *Sign) Serialize(player Player, version int16) ([]byte, error) {
// Delegate to spawn serialization
if s.Spawn != nil {
return s.Spawn.Serialize(player, version)
}
return nil, fmt.Errorf("spawn is nil")
}
// HandleUse processes player interaction with the sign
func (s *Sign) HandleUse(client Client, command string) error {
if client == nil {
return fmt.Errorf("client is nil")
}
player := client.GetPlayer()
if player == nil {
return fmt.Errorf("player is nil")
}
// Check quest requirements if this is from a client (not script)
if !s.meetsQuestRequirements(client) {
return nil // Silently fail if quest requirements not met
}
// Handle transporter functionality first
if s.Spawn != nil && s.Spawn.GetTransporterID() > 0 {
return s.handleTransporter(client)
}
// Handle zone transport signs
if s.signType == SignTypeZone && s.zoneID > 0 {
return s.handleZoneTransport(client)
}
// Handle entity commands
if len(command) > 0 {
return s.handleEntityCommand(client, command)
}
return nil
}
// meetsQuestRequirements checks if the player meets quest requirements to use the sign
func (s *Sign) meetsQuestRequirements(client Client) bool {
// This is a placeholder implementation
// In the full implementation, this would check:
// - MeetsSpawnAccessRequirements(client.GetPlayer())
// - GetQuestsRequiredOverride() flags
// - appearance.show_command_icon
// For now, assume all requirements are met
return true
}
// handleTransporter processes transporter functionality
func (s *Sign) handleTransporter(client Client) error {
zone := client.GetPlayer().GetZone()
if zone == nil {
return fmt.Errorf("player not in zone")
}
transporterID := s.Spawn.GetTransporterID()
// Get transport destinations
destinations, err := zone.GetTransporters(client, transporterID)
if err != nil {
return fmt.Errorf("failed to get transporters: %w", err)
}
if len(destinations) > 0 {
client.SetTemporaryTransportID(0)
return client.ProcessTeleport(s, destinations, transporterID)
}
return nil
}
// handleZoneTransport processes zone transport functionality
func (s *Sign) handleZoneTransport(client Client) error {
player := client.GetPlayer()
// Check distance if sign has distance requirement
if s.signDistance > 0 {
distance := player.GetDistance(s.Spawn)
if distance > s.signDistance {
client.SimpleMessage(ChannelColorYellow, "You are too far away!")
return nil
}
}
// Get zone name from database
zoneName, err := client.GetDatabase().GetZoneName(s.zoneID)
if err != nil || len(zoneName) == 0 {
client.Message(ChannelColorYellow, "Unable to find zone with ID: %d", s.zoneID)
return fmt.Errorf("zone not found: %d", s.zoneID)
}
// Check zone access
if !client.CheckZoneAccess(zoneName) {
return nil // Access denied (client handles message)
}
// Set coordinates if sign has valid zone coordinates
useZoneDefaults := !s.HasZoneCoordinates()
if !useZoneDefaults {
player.SetX(s.zoneX)
player.SetY(s.zoneY)
player.SetZ(s.zoneZ)
player.SetHeading(s.zoneHeading)
} else {
client.SimpleMessage(ChannelColorYellow, "Invalid zone in coords, taking you to a safe point.")
}
// Try instanced zone first, then regular zone
if !client.TryZoneInstance(s.zoneID, useZoneDefaults) {
return client.Zone(zoneName, useZoneDefaults)
}
return nil
}
// handleEntityCommand processes entity commands
func (s *Sign) handleEntityCommand(client Client, command string) error {
if s.Spawn == nil {
return fmt.Errorf("spawn is nil")
}
entityCommand := s.Spawn.FindEntityCommand(command)
if entityCommand == nil {
return nil // Command not found
}
// Handle mark command specially
if strings.ToLower(entityCommand.Command) == "mark" {
return s.handleMarkCommand(client)
}
// Process the entity command
zone := client.GetCurrentZone()
if zone == nil {
return fmt.Errorf("player not in zone")
}
player := client.GetPlayer()
target := player.GetTarget()
return zone.ProcessEntityCommand(entityCommand, player, target)
}
// handleMarkCommand processes the mark command for marking signs
func (s *Sign) handleMarkCommand(client Client) error {
charID := client.GetCharacterID()
charName, err := client.GetDatabase().GetCharacterName(charID)
if err != nil {
return fmt.Errorf("failed to get character name: %w", err)
}
return client.GetDatabase().SaveSignMark(charID, s.widgetID, charName, client)
}
// GetDisplayText returns the formatted display text for the sign
func (s *Sign) GetDisplayText() string {
var text strings.Builder
if s.HasTitle() {
text.WriteString(s.title)
}
if s.HasDescription() {
if text.Len() > 0 {
text.WriteByte('\n')
}
text.WriteString(s.description)
}
// Add location information if requested
if s.includeLocation && s.HasZoneCoordinates() {
if text.Len() > 0 {
text.WriteByte('\n')
}
text.WriteString(fmt.Sprintf("Location: %.2f, %.2f, %.2f", s.zoneX, s.zoneY, s.zoneZ))
}
// Add heading information if requested
if s.includeHeading && s.zoneHeading != 0 {
if text.Len() > 0 {
text.WriteByte('\n')
}
text.WriteString(fmt.Sprintf("Heading: %.2f", s.zoneHeading))
}
return text.String()
}
// Validate checks if the sign configuration is valid
func (s *Sign) Validate() []string {
var issues []string
if s.Spawn == nil {
issues = append(issues, "Sign has no spawn data")
return issues
}
if s.widgetID == 0 {
issues = append(issues, "Sign has no widget ID")
}
if len(s.title) > MaxSignTitleLength {
issues = append(issues, fmt.Sprintf("Sign title too long: %d > %d", len(s.title), MaxSignTitleLength))
}
if len(s.description) > MaxSignDescriptionLength {
issues = append(issues, fmt.Sprintf("Sign description too long: %d > %d", len(s.description), MaxSignDescriptionLength))
}
if s.signType == SignTypeZone {
if s.zoneID == 0 {
issues = append(issues, "Zone sign has no zone ID")
}
if s.signDistance < 0 {
issues = append(issues, "Sign distance cannot be negative")
}
}
return issues
}
// IsValid returns true if the sign configuration is valid
func (s *Sign) IsValid() bool {
issues := s.Validate()
return len(issues) == 0
}

235
internal/sign/types.go Normal file
View File

@ -0,0 +1,235 @@
package sign
import "eq2emu/internal/spawn"
// Sign represents a clickable sign in the game world that extends Spawn
type Sign struct {
*spawn.Spawn // Embed spawn for basic functionality
// Widget properties
widgetID int32 // Widget identifier
widgetX float32 // Widget X coordinate
widgetY float32 // Widget Y coordinate
widgetZ float32 // Widget Z coordinate
// Sign properties
signType int8 // Type of sign (generic or zone)
title string // Sign title
description string // Sign description
language int8 // Language of the sign text
// Zone transport properties
zoneX float32 // Target zone X coordinate
zoneY float32 // Target zone Y coordinate
zoneZ float32 // Target zone Z coordinate
zoneHeading float32 // Target zone heading
zoneID int32 // Target zone ID
signDistance float32 // Maximum interaction distance
// Display options
includeLocation bool // Whether to include location in display
includeHeading bool // Whether to include heading in display
}
// NewSign creates a new sign with default values
func NewSign() *Sign {
baseSpawn := spawn.NewSpawn()
// Set spawn-specific defaults for signs
baseSpawn.SetSpawnType(DefaultSpawnType)
// TODO: Set appearance properties when spawn system is integrated
// appearance.pos.state = DefaultPosState
// appearance.difficulty = DefaultDifficulty
// appearance.activity_status = DefaultActivityStatus
return &Sign{
Spawn: baseSpawn,
widgetID: 0,
widgetX: 0,
widgetY: 0,
widgetZ: 0,
signType: SignTypeGeneric,
title: "",
description: "",
language: 0,
zoneX: 0,
zoneY: 0,
zoneZ: 0,
zoneHeading: 0,
zoneID: 0,
signDistance: DefaultSignDistance,
includeLocation: false,
includeHeading: false,
}
}
// IsSign returns true since this is a sign
func (s *Sign) IsSign() bool {
return true
}
// Widget ID methods
func (s *Sign) GetWidgetID() int32 {
return s.widgetID
}
func (s *Sign) SetWidgetID(id int32) {
s.widgetID = id
}
// Widget position methods
func (s *Sign) GetWidgetX() float32 {
return s.widgetX
}
func (s *Sign) SetWidgetX(x float32) {
s.widgetX = x
}
func (s *Sign) GetWidgetY() float32 {
return s.widgetY
}
func (s *Sign) SetWidgetY(y float32) {
s.widgetY = y
}
func (s *Sign) GetWidgetZ() float32 {
return s.widgetZ
}
func (s *Sign) SetWidgetZ(z float32) {
s.widgetZ = z
}
// Sign type methods
func (s *Sign) GetSignType() int8 {
return s.signType
}
func (s *Sign) SetSignType(signType int8) {
s.signType = signType
}
// Title and description methods
func (s *Sign) GetSignTitle() string {
return s.title
}
func (s *Sign) SetSignTitle(title string) {
s.title = title
}
func (s *Sign) GetSignDescription() string {
return s.description
}
func (s *Sign) SetSignDescription(description string) {
s.description = description
}
// Language methods
func (s *Sign) GetLanguage() int8 {
return s.language
}
func (s *Sign) SetLanguage(language int8) {
s.language = language
}
// Zone transport methods
func (s *Sign) GetSignZoneX() float32 {
return s.zoneX
}
func (s *Sign) SetSignZoneX(x float32) {
s.zoneX = x
}
func (s *Sign) GetSignZoneY() float32 {
return s.zoneY
}
func (s *Sign) SetSignZoneY(y float32) {
s.zoneY = y
}
func (s *Sign) GetSignZoneZ() float32 {
return s.zoneZ
}
func (s *Sign) SetSignZoneZ(z float32) {
s.zoneZ = z
}
func (s *Sign) GetSignZoneHeading() float32 {
return s.zoneHeading
}
func (s *Sign) SetSignZoneHeading(heading float32) {
s.zoneHeading = heading
}
func (s *Sign) GetSignZoneID() int32 {
return s.zoneID
}
func (s *Sign) SetSignZoneID(zoneID int32) {
s.zoneID = zoneID
}
func (s *Sign) GetSignDistance() float32 {
return s.signDistance
}
func (s *Sign) SetSignDistance(distance float32) {
s.signDistance = distance
}
// Display option methods
func (s *Sign) GetIncludeLocation() bool {
return s.includeLocation
}
func (s *Sign) SetIncludeLocation(include bool) {
s.includeLocation = include
}
func (s *Sign) GetIncludeHeading() bool {
return s.includeHeading
}
func (s *Sign) SetIncludeHeading(include bool) {
s.includeHeading = include
}
// SetSignIcon sets the sign's icon (delegates to spawn appearance)
func (s *Sign) SetSignIcon(icon int8) {
// TODO: Implement when spawn appearance system is integrated
// s.appearance.icon = icon
}
// HasZoneCoordinates returns true if the sign has valid zone coordinates
func (s *Sign) HasZoneCoordinates() bool {
return !(s.zoneX == 0 && s.zoneY == 0 && s.zoneZ == 0 && s.zoneHeading == 0)
}
// HasTitle returns true if the sign has a title
func (s *Sign) HasTitle() bool {
return len(s.title) > 0
}
// HasDescription returns true if the sign has a description
func (s *Sign) HasDescription() bool {
return len(s.description) > 0
}
// IsZoneSign returns true if this is a zone transport sign
func (s *Sign) IsZoneSign() bool {
return s.signType == SignTypeZone
}
// IsGenericSign returns true if this is a generic sign
func (s *Sign) IsGenericSign() bool {
return s.signType == SignTypeGeneric
}

View File

@ -0,0 +1,74 @@
package skills
// Skill type constants
const (
SkillTypeWeaponry = 1
SkillTypeSpellcasting = 2
SkillTypeAvoidance = 3
SkillTypeArmor = 4
SkillTypeShield = 5
SkillTypeHarvesting = 6
SkillTypeArtisan = 7
SkillTypeCraftsman = 8
SkillTypeOutfitter = 9
SkillTypeScholar = 10
SkillTypeGeneral = 13
SkillTypeLanguage = 14
SkillTypeClass = 15
SkillTypeCombat = 16
SkillTypeWeapon = 17
SkillTypeTSKnowledge = 18
)
// DoF (Desert of Flames) skill type constants
const (
SkillTypeGeneralDoF = 11
SkillTypeLanguageDoF = 12
SkillTypeClassDoF = 13
SkillTypeCombatDoF = 14
SkillTypeWeaponDoF = 15
SkillTypeTSKnowledgeDoF = 16
)
// Special skill IDs
const (
SkillIDSculpting = 1039865549
SkillIDArtistry = 3881305672
SkillIDFletching = 3076004370
SkillIDMetalworking = 4032608519
SkillIDMetalshaping = 3108933728
SkillIDTailoring = 2082133324
SkillIDChemistry = 2557647574
SkillIDArtificing = 3330500131
SkillIDScribing = 773137566
)
// Skills that update current_value to max_value when max_value is updated
const (
SkillIDDualwield = 1852383242
SkillIDFists = 3177806075
SkillIDDestroying = 3429135390
SkillIDMagicAffinity = 2072844078
)
// Weapon skill IDs
const (
SkillIDGreatsword = 2292577688 // 2h slashing
SkillIDGreatspear = 2380184628 // 2h piercing
SkillIDStaff = 3180399725 // 2h crushing
)
// Disarm skill check results
const (
DisarmSuccess = 1
DisarmFail = 0
DisarmTrigger = -1
)
// Skill increase constants
const (
// Base skill increase chance percentage (at skill level 1)
BaseSkillIncreasePercent = 20
// Max skill level for calculating increase chances
MaxSkillLevelForIncrease = 400
)

View File

@ -0,0 +1,253 @@
package skills
import "fmt"
// SkillAware interface for entities that have skills
type SkillAware interface {
GetSkillList() *PlayerSkillList
GetSkillByName(name string) *Skill
GetSkill(skillID int32) *Skill
HasSkill(skillID int32) bool
IncreaseSkill(skillName string, amount int32) error
}
// PacketSender interface for sending skill packets to clients
type PacketSender interface {
QueuePacket(packet []byte)
GetVersion() int32
}
// Database interface for skill persistence
type Database interface {
LoadPlayerSkills(characterID int32) ([]*Skill, error)
SavePlayerSkill(characterID int32, skill *Skill) error
LoadMasterSkills() ([]*Skill, error)
SaveMasterSkill(skill *Skill) error
}
// Logger interface for skill system logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
}
// EntitySkillAdapter provides skill functionality for entities
type EntitySkillAdapter struct {
skillList *PlayerSkillList
entityID int32
logger Logger
}
// NewEntitySkillAdapter creates a new entity skill adapter
func NewEntitySkillAdapter(entityID int32, logger Logger) *EntitySkillAdapter {
return &EntitySkillAdapter{
skillList: NewPlayerSkillList(),
entityID: entityID,
logger: logger,
}
}
// GetSkillList returns the player's skill list
func (esa *EntitySkillAdapter) GetSkillList() *PlayerSkillList {
return esa.skillList
}
// GetSkillByName returns a skill by name
func (esa *EntitySkillAdapter) GetSkillByName(name string) *Skill {
return esa.skillList.GetSkillByName(name)
}
// GetSkill returns a skill by ID
func (esa *EntitySkillAdapter) GetSkill(skillID int32) *Skill {
return esa.skillList.GetSkill(skillID)
}
// HasSkill checks if the entity has a skill
func (esa *EntitySkillAdapter) HasSkill(skillID int32) bool {
return esa.skillList.HasSkill(skillID)
}
// IncreaseSkill increases a skill by name
func (esa *EntitySkillAdapter) IncreaseSkill(skillName string, amount int32) error {
skill := esa.skillList.GetSkillByName(skillName)
if skill == nil {
if esa.logger != nil {
esa.logger.LogError("Entity %d: Skill '%s' not found for increase", esa.entityID, skillName)
}
return fmt.Errorf("skill '%s' not found", skillName)
}
esa.skillList.IncreaseSkill(skill, int16(amount))
if esa.logger != nil {
esa.logger.LogDebug("Entity %d: Increased skill '%s' by %d (now %d/%d)",
esa.entityID, skillName, amount, skill.CurrentVal, skill.MaxVal)
}
return nil
}
// AddSkill adds a new skill to the entity
func (esa *EntitySkillAdapter) AddSkill(skill *Skill) {
if skill == nil {
return
}
esa.skillList.AddSkill(skill)
if esa.logger != nil {
esa.logger.LogDebug("Entity %d: Added skill '%s' (ID: %d)", esa.entityID, skill.Name.Data, skill.SkillID)
}
}
// RemoveSkill removes a skill from the entity
func (esa *EntitySkillAdapter) RemoveSkill(skill *Skill) {
if skill == nil {
return
}
esa.skillList.RemoveSkill(skill)
if esa.logger != nil {
esa.logger.LogDebug("Entity %d: Removed skill '%s' (ID: %d)", esa.entityID, skill.Name.Data, skill.SkillID)
}
}
// GetSkillValue returns a skill's current value including bonuses
func (esa *EntitySkillAdapter) GetSkillValue(skillID int32) int16 {
skill := esa.skillList.GetSkill(skillID)
if skill == nil {
return 0
}
return esa.skillList.CalculateSkillValue(skillID, skill.CurrentVal)
}
// GetSkillMaxValue returns a skill's max value including bonuses
func (esa *EntitySkillAdapter) GetSkillMaxValue(skillID int32) int16 {
skill := esa.skillList.GetSkill(skillID)
if skill == nil {
return 0
}
return esa.skillList.CalculateSkillMaxValue(skillID, skill.MaxVal)
}
// ApplySkillBonus applies a skill bonus from a spell
func (esa *EntitySkillAdapter) ApplySkillBonus(spellID int32, skillID int32, value float32) {
esa.skillList.AddSkillBonus(spellID, skillID, value)
if esa.logger != nil {
esa.logger.LogDebug("Entity %d: Applied skill bonus from spell %d to skill %d: %f",
esa.entityID, spellID, skillID, value)
}
}
// RemoveSkillBonus removes skill bonuses from a spell
func (esa *EntitySkillAdapter) RemoveSkillBonus(spellID int32) {
esa.skillList.RemoveSkillBonus(spellID)
if esa.logger != nil {
esa.logger.LogDebug("Entity %d: Removed skill bonuses from spell %d", esa.entityID, spellID)
}
}
// CheckSkillIncrease attempts to increase a skill
func (esa *EntitySkillAdapter) CheckSkillIncrease(skillID int32) bool {
skill := esa.skillList.GetSkill(skillID)
if skill == nil {
return false
}
increased := esa.skillList.CheckSkillIncrease(skill)
if increased && esa.logger != nil {
esa.logger.LogInfo("Entity %d: Skill '%s' increased to %d/%d",
esa.entityID, skill.Name.Data, skill.CurrentVal, skill.MaxVal)
}
return increased
}
// GetSaveNeededSkills returns skills that need database saving
func (esa *EntitySkillAdapter) GetSaveNeededSkills() []*Skill {
return esa.skillList.GetSaveNeededSkills()
}
// GetSkillUpdates returns skills that need client updates
func (esa *EntitySkillAdapter) GetSkillUpdates() []*Skill {
return esa.skillList.GetSkillUpdates()
}
// HasSkillUpdates returns whether there are pending skill updates
func (esa *EntitySkillAdapter) HasSkillUpdates() bool {
return esa.skillList.HasSkillUpdates()
}
// SendSkillPacket sends skill updates to a client
func (esa *EntitySkillAdapter) SendSkillPacket(sender PacketSender) error {
if sender == nil {
return fmt.Errorf("packet sender is nil")
}
packet, err := esa.skillList.GetSkillPacket(int16(sender.GetVersion()))
if err != nil {
return fmt.Errorf("failed to build skill packet: %w", err)
}
sender.QueuePacket(packet)
if esa.logger != nil {
esa.logger.LogDebug("Entity %d: Sent skill packet to client (version %d)",
esa.entityID, sender.GetVersion())
}
return nil
}
// LoadSkillsFromDatabase loads skills from database (placeholder)
func (esa *EntitySkillAdapter) LoadSkillsFromDatabase(db Database, characterID int32) error {
if db == nil {
return fmt.Errorf("database is nil")
}
skills, err := db.LoadPlayerSkills(characterID)
if err != nil {
return fmt.Errorf("failed to load player skills: %w", err)
}
for _, skill := range skills {
esa.skillList.AddSkill(skill)
}
if esa.logger != nil {
esa.logger.LogInfo("Entity %d: Loaded %d skills from database", esa.entityID, len(skills))
}
return nil
}
// SaveSkillsToDatabase saves skills to database (placeholder)
func (esa *EntitySkillAdapter) SaveSkillsToDatabase(db Database, characterID int32) error {
if db == nil {
return fmt.Errorf("database is nil")
}
saveSkills := esa.GetSaveNeededSkills()
for _, skill := range saveSkills {
if err := db.SavePlayerSkill(characterID, skill); err != nil {
if esa.logger != nil {
esa.logger.LogError("Entity %d: Failed to save skill %s: %v", esa.entityID, skill.Name.Data, err)
}
return fmt.Errorf("failed to save skill %s: %w", skill.Name.Data, err)
}
}
if len(saveSkills) > 0 && esa.logger != nil {
esa.logger.LogInfo("Entity %d: Saved %d skills to database", esa.entityID, len(saveSkills))
}
return nil
}

283
internal/skills/manager.go Normal file
View File

@ -0,0 +1,283 @@
package skills
import (
"fmt"
"sync"
)
// Manager provides high-level management of the skills system
type Manager struct {
masterSkillList *MasterSkillList
mutex sync.RWMutex
// Statistics
totalSkillUps int64
skillUpsByType map[int32]int64 // Skill type -> count
skillUpsBySkill map[int32]int64 // Skill ID -> count
playersWithSkills int64
}
// NewManager creates a new skills manager
func NewManager() *Manager {
return &Manager{
masterSkillList: NewMasterSkillList(),
skillUpsByType: make(map[int32]int64),
skillUpsBySkill: make(map[int32]int64),
}
}
// Initialize loads skills data (placeholder for database loading)
func (m *Manager) Initialize() error {
// TODO: Load skills from database when database system is integrated
// This would typically load all skills from a skills table
return nil
}
// GetMasterSkillList returns the master skill list
func (m *Manager) GetMasterSkillList() *MasterSkillList {
return m.masterSkillList
}
// AddSkillToMaster adds a skill to the master list
func (m *Manager) AddSkillToMaster(skill *Skill) {
m.masterSkillList.AddSkill(skill)
}
// GetSkill returns a skill from the master list by ID
func (m *Manager) GetSkill(skillID int32) *Skill {
return m.masterSkillList.GetSkill(skillID)
}
// GetSkillByName returns a skill from the master list by name
func (m *Manager) GetSkillByName(skillName string) *Skill {
return m.masterSkillList.GetSkillByName(skillName)
}
// CreatePlayerSkillList creates a new player skill list
func (m *Manager) CreatePlayerSkillList() *PlayerSkillList {
m.mutex.Lock()
m.playersWithSkills++
m.mutex.Unlock()
return NewPlayerSkillList()
}
// RecordSkillUp records a skill increase for statistics
func (m *Manager) RecordSkillUp(skillID int32, skillType int32) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalSkillUps++
m.skillUpsByType[skillType]++
m.skillUpsBySkill[skillID]++
}
// GetStatistics returns skill system statistics
func (m *Manager) GetStatistics() map[string]interface{} {
m.mutex.RLock()
defer m.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_skill_ups"] = m.totalSkillUps
stats["players_with_skills"] = m.playersWithSkills
stats["total_skills_in_master"] = m.masterSkillList.GetSkillCount()
// Copy skill type statistics
typeStats := make(map[int32]int64)
for skillType, count := range m.skillUpsByType {
typeStats[skillType] = count
}
stats["skill_ups_by_type"] = typeStats
// Copy individual skill statistics
skillStats := make(map[int32]int64)
for skillID, count := range m.skillUpsBySkill {
skillStats[skillID] = count
}
stats["skill_ups_by_skill"] = skillStats
return stats
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalSkillUps = 0
m.playersWithSkills = 0
m.skillUpsByType = make(map[int32]int64)
m.skillUpsBySkill = make(map[int32]int64)
}
// GetSkillsByType returns all skills of a specific type
func (m *Manager) GetSkillsByType(skillType int32) []*Skill {
return m.masterSkillList.GetSkillsByType(skillType)
}
// GetSkillTypeCount returns the number of skills of a specific type
func (m *Manager) GetSkillTypeCount(skillType int32) int {
skills := m.GetSkillsByType(skillType)
return len(skills)
}
// GetSkillUpCount returns the total number of skill ups for a skill
func (m *Manager) GetSkillUpCount(skillID int32) int64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.skillUpsBySkill[skillID]
}
// GetSkillTypeUpCount returns the total number of skill ups for a skill type
func (m *Manager) GetSkillTypeUpCount(skillType int32) int64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.skillUpsByType[skillType]
}
// ValidateSkillData validates that all skills in the master list are properly configured
func (m *Manager) ValidateSkillData() []string {
skills := m.masterSkillList.GetAllSkills()
issues := make([]string, 0)
if len(skills) == 0 {
issues = append(issues, "No skills configured in master list")
return issues
}
for skillID, skill := range skills {
if skill == nil {
issues = append(issues, fmt.Sprintf("Skill ID %d is nil", skillID))
continue
}
if skill.SkillID != skillID {
issues = append(issues, fmt.Sprintf("Skill %d has mismatched ID: %d", skillID, skill.SkillID))
}
if skill.Name.Data == "" {
issues = append(issues, fmt.Sprintf("Skill %d has empty name", skillID))
}
if skill.SkillType == 0 {
issues = append(issues, fmt.Sprintf("Skill %d (%s) has no skill type", skillID, skill.Name.Data))
}
if skill.MaxVal < 0 {
issues = append(issues, fmt.Sprintf("Skill %d (%s) has negative max value: %d", skillID, skill.Name.Data, skill.MaxVal))
}
if skill.CurrentVal < 0 {
issues = append(issues, fmt.Sprintf("Skill %d (%s) has negative current value: %d", skillID, skill.Name.Data, skill.CurrentVal))
}
if skill.CurrentVal > skill.MaxVal {
issues = append(issues, fmt.Sprintf("Skill %d (%s) has current value (%d) greater than max value (%d)",
skillID, skill.Name.Data, skill.CurrentVal, skill.MaxVal))
}
}
return issues
}
// ProcessCommand handles skill-related commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "list":
return m.handleListCommand(args)
case "info":
return m.handleInfoCommand(args)
default:
return "", fmt.Errorf("unknown skills command: %s", command)
}
}
// handleStatsCommand shows skill system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Skills System Statistics:\n"
result += fmt.Sprintf("Total Skills in Master List: %d\n", stats["total_skills_in_master"])
result += fmt.Sprintf("Players with Skills: %d\n", stats["players_with_skills"])
result += fmt.Sprintf("Total Skill Ups: %d\n", stats["total_skill_ups"])
return result, nil
}
// handleValidateCommand validates skill data
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateSkillData()
if len(issues) == 0 {
return "All skill data is valid.", nil
}
result := fmt.Sprintf("Found %d issues with skill data:\n", len(issues))
for i, issue := range issues {
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
// handleListCommand lists skills
func (m *Manager) handleListCommand(args []string) (string, error) {
skills := m.masterSkillList.GetAllSkills()
if len(skills) == 0 {
return "No skills configured.", nil
}
result := fmt.Sprintf("Skills (%d):\n", len(skills))
count := 0
for _, skill := range skills {
if count >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s (Type: %d)\n", skill.SkillID, skill.Name.Data, skill.SkillType)
count++
}
return result, nil
}
// handleInfoCommand shows information about a specific skill
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("skill name or ID required")
}
skillName := args[0]
skill := m.GetSkillByName(skillName)
if skill == nil {
return fmt.Sprintf("Skill '%s' not found.", skillName), nil
}
result := fmt.Sprintf("Skill Information:\n")
result += fmt.Sprintf("ID: %d\n", skill.SkillID)
result += fmt.Sprintf("Name: %s\n", skill.Name.Data)
result += fmt.Sprintf("Short Name: %s\n", skill.ShortName.Data)
result += fmt.Sprintf("Type: %d\n", skill.SkillType)
result += fmt.Sprintf("Description: %s\n", skill.Description.Data)
result += fmt.Sprintf("Max Value: %d\n", skill.MaxVal)
result += fmt.Sprintf("Current Value: %d\n", skill.CurrentVal)
result += fmt.Sprintf("Active: %t\n", skill.ActiveSkill)
upCount := m.GetSkillUpCount(skill.SkillID)
result += fmt.Sprintf("Total Skill Ups Recorded: %d\n", upCount)
return result, nil
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
// Nothing to clean up currently, but placeholder for future cleanup
}

View File

@ -0,0 +1,202 @@
package skills
import (
"sync"
)
// MasterSkillList manages the master list of all available skills
type MasterSkillList struct {
skills map[int32]*Skill // All skills by ID
populatePackets map[int16][]byte // Cached packets by version
mutex sync.RWMutex // Thread safety
}
// NewMasterSkillList creates a new master skill list
func NewMasterSkillList() *MasterSkillList {
return &MasterSkillList{
skills: make(map[int32]*Skill),
populatePackets: make(map[int16][]byte),
}
}
// AddSkill adds a skill to the master list
func (msl *MasterSkillList) AddSkill(skill *Skill) {
if skill == nil {
return
}
msl.mutex.Lock()
defer msl.mutex.Unlock()
msl.skills[skill.SkillID] = skill
// Clear cached packets when skills change
msl.populatePackets = make(map[int16][]byte)
}
// GetSkillCount returns the total number of skills
func (msl *MasterSkillList) GetSkillCount() int16 {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
return int16(len(msl.skills))
}
// GetAllSkills returns a copy of all skills
func (msl *MasterSkillList) GetAllSkills() map[int32]*Skill {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
// Return a copy to prevent external modification
skills := make(map[int32]*Skill)
for id, skill := range msl.skills {
skills[id] = skill
}
return skills
}
// GetSkill returns a skill by ID
func (msl *MasterSkillList) GetSkill(skillID int32) *Skill {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
if skill, exists := msl.skills[skillID]; exists {
return skill
}
return nil
}
// GetSkillByName returns a skill by name (case-insensitive)
func (msl *MasterSkillList) GetSkillByName(skillName string) *Skill {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
// Convert to lowercase for comparison
lowerName := toLower(skillName)
for _, skill := range msl.skills {
if toLower(skill.Name.Data) == lowerName {
return skill
}
}
return nil
}
// GetPopulateSkillsPacket builds a packet containing all skills for a client version
func (msl *MasterSkillList) GetPopulateSkillsPacket(version int16) ([]byte, error) {
msl.mutex.Lock()
defer msl.mutex.Unlock()
// Check if we have a cached packet for this version
if packet, exists := msl.populatePackets[version]; exists {
// Return a copy of the cached packet
result := make([]byte, len(packet))
copy(result, packet)
return result, nil
}
// Build the packet - this is a placeholder implementation
// In the full implementation, this would use the PacketStruct system
// to build a proper WS_SkillMap packet for the given version
packet := msl.buildSkillMapPacket(version)
// Cache the packet
msl.populatePackets[version] = packet
// Return a copy
result := make([]byte, len(packet))
copy(result, packet)
return result, nil
}
// buildSkillMapPacket builds a WS_SkillMap packet
func (msl *MasterSkillList) buildSkillMapPacket(version int16) []byte {
// This is a placeholder implementation
// In a real implementation, this would use the PacketStruct system:
// packet := configReader.getStruct("WS_SkillMap", version)
// packet.setArrayLengthByName("skill_count", len(msl.skills))
// for i, skill := range msl.skills {
// packet.setArrayDataByName("skill_id", skill.SkillID, i)
// packet.setArrayDataByName("short_name", &skill.ShortName, i)
// packet.setArrayDataByName("name", &skill.Name, i)
// packet.setArrayDataByName("description", &skill.Description, i)
// }
// return packet.serialize()
// For now, return an empty packet
return make([]byte, 0)
}
// RemoveSkill removes a skill from the master list
func (msl *MasterSkillList) RemoveSkill(skillID int32) {
msl.mutex.Lock()
defer msl.mutex.Unlock()
delete(msl.skills, skillID)
// Clear cached packets when skills change
msl.populatePackets = make(map[int16][]byte)
}
// ClearSkills removes all skills from the master list
func (msl *MasterSkillList) ClearSkills() {
msl.mutex.Lock()
defer msl.mutex.Unlock()
msl.skills = make(map[int32]*Skill)
msl.populatePackets = make(map[int16][]byte)
}
// GetSkillsByType returns all skills of a specific type
func (msl *MasterSkillList) GetSkillsByType(skillType int32) []*Skill {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
var skills []*Skill
for _, skill := range msl.skills {
if skill.SkillType == skillType {
skills = append(skills, skill)
}
}
return skills
}
// HasSkill checks if a skill exists in the master list
func (msl *MasterSkillList) HasSkill(skillID int32) bool {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
_, exists := msl.skills[skillID]
return exists
}
// GetSkillIDs returns all skill IDs
func (msl *MasterSkillList) GetSkillIDs() []int32 {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
ids := make([]int32, 0, len(msl.skills))
for id := range msl.skills {
ids = append(ids, id)
}
return ids
}
// toLower converts a string to lowercase (simple implementation)
func toLower(s string) string {
result := make([]byte, len(s))
for i, c := range []byte(s) {
if c >= 'A' && c <= 'Z' {
result[i] = c + 32
} else {
result[i] = c
}
}
return string(result)
}

View File

@ -0,0 +1,420 @@
package skills
import (
"math/rand"
"sync"
)
// PlayerSkillList manages skills for a specific player
type PlayerSkillList struct {
skills map[int32]*Skill // Player's skills by ID
nameSkillMap map[string]*Skill // Skills by name for quick lookup
skillUpdates []*Skill // Skills needing updates
skillBonusList map[int32]*SkillBonus // Skill bonuses by spell ID
// Packet data for skill updates
origPacket []byte
xorPacket []byte
origPacketSize int16
packetCount int16
hasUpdates bool
mutex sync.RWMutex // Thread safety for skills/nameMap
updatesMutex sync.Mutex // Thread safety for updates
bonusMutex sync.RWMutex // Thread safety for bonuses
}
// NewPlayerSkillList creates a new player skill list
func NewPlayerSkillList() *PlayerSkillList {
return &PlayerSkillList{
skills: make(map[int32]*Skill),
nameSkillMap: make(map[string]*Skill),
skillUpdates: make([]*Skill, 0),
skillBonusList: make(map[int32]*SkillBonus),
hasUpdates: false,
}
}
// AddSkill adds a skill to the player's skill list
func (psl *PlayerSkillList) AddSkill(newSkill *Skill) {
if newSkill == nil {
return
}
psl.mutex.Lock()
defer psl.mutex.Unlock()
// Remove old skill if it exists
if oldSkill, exists := psl.skills[newSkill.SkillID]; exists {
// TODO: Set Lua user data stale when LuaInterface is integrated
_ = oldSkill
}
psl.skills[newSkill.SkillID] = newSkill
// Clear name map cache so it gets rebuilt
psl.nameSkillMap = make(map[string]*Skill)
}
// RemoveSkill removes a skill from the player's skill list
func (psl *PlayerSkillList) RemoveSkill(skill *Skill) {
if skill == nil {
return
}
psl.mutex.Lock()
defer psl.mutex.Unlock()
// TODO: Set Lua user data stale when LuaInterface is integrated
skill.ActiveSkill = false
// Clear name map cache
psl.nameSkillMap = make(map[string]*Skill)
}
// GetAllSkills returns all player skills
func (psl *PlayerSkillList) GetAllSkills() map[int32]*Skill {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
// Return a copy to prevent external modification
skills := make(map[int32]*Skill)
for id, skill := range psl.skills {
skills[id] = skill
}
return skills
}
// HasSkill checks if player has a specific skill
func (psl *PlayerSkillList) HasSkill(skillID int32) bool {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
skill, exists := psl.skills[skillID]
return exists && skill.ActiveSkill
}
// GetSkill returns a skill by ID
func (psl *PlayerSkillList) GetSkill(skillID int32) *Skill {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
if skill, exists := psl.skills[skillID]; exists && skill.ActiveSkill {
return skill
}
return nil
}
// GetSkillByName returns a skill by name
func (psl *PlayerSkillList) GetSkillByName(name string) *Skill {
psl.mutex.Lock()
defer psl.mutex.Unlock()
// Build name map if empty
if len(psl.nameSkillMap) == 0 {
for _, skill := range psl.skills {
if skill.ActiveSkill {
psl.nameSkillMap[skill.Name.Data] = skill
}
}
}
if skill, exists := psl.nameSkillMap[name]; exists {
return skill
}
return nil
}
// IncreaseSkill increases a skill's current value
func (psl *PlayerSkillList) IncreaseSkill(skill *Skill, amount int16) {
if skill == nil {
return
}
skill.PreviousVal = skill.CurrentVal
skill.CurrentVal += amount
if skill.CurrentVal > skill.MaxVal {
skill.MaxVal = skill.CurrentVal
}
psl.AddSkillUpdateNeeded(skill)
skill.SaveNeeded = true
}
// IncreaseSkillByID increases a skill's current value by ID
func (psl *PlayerSkillList) IncreaseSkillByID(skillID int32, amount int16) {
skill := psl.GetSkill(skillID)
psl.IncreaseSkill(skill, amount)
}
// DecreaseSkill decreases a skill's current value
func (psl *PlayerSkillList) DecreaseSkill(skill *Skill, amount int16) {
if skill == nil {
return
}
skill.PreviousVal = skill.CurrentVal
if skill.CurrentVal < amount {
skill.CurrentVal = 0
} else {
skill.CurrentVal -= amount
}
skill.SaveNeeded = true
psl.AddSkillUpdateNeeded(skill)
}
// DecreaseSkillByID decreases a skill's current value by ID
func (psl *PlayerSkillList) DecreaseSkillByID(skillID int32, amount int16) {
skill := psl.GetSkill(skillID)
psl.DecreaseSkill(skill, amount)
}
// SetSkill sets a skill's current value
func (psl *PlayerSkillList) SetSkill(skill *Skill, value int16, sendUpdate bool) {
if skill == nil {
return
}
skill.PreviousVal = skill.CurrentVal
skill.CurrentVal = value
if skill.CurrentVal > skill.MaxVal {
skill.MaxVal = skill.CurrentVal
}
skill.SaveNeeded = true
if sendUpdate {
psl.AddSkillUpdateNeeded(skill)
}
}
// SetSkillByID sets a skill's current value by ID
func (psl *PlayerSkillList) SetSkillByID(skillID int32, value int16, sendUpdate bool) {
skill := psl.GetSkill(skillID)
psl.SetSkill(skill, value, sendUpdate)
}
// IncreaseSkillCap increases a skill's maximum value
func (psl *PlayerSkillList) IncreaseSkillCap(skill *Skill, amount int16) {
if skill == nil {
return
}
skill.MaxVal += amount
skill.SaveNeeded = true
}
// IncreaseSkillCapByID increases a skill's maximum value by ID
func (psl *PlayerSkillList) IncreaseSkillCapByID(skillID int32, amount int16) {
skill := psl.GetSkill(skillID)
psl.IncreaseSkillCap(skill, amount)
}
// DecreaseSkillCap decreases a skill's maximum value
func (psl *PlayerSkillList) DecreaseSkillCap(skill *Skill, amount int16) {
if skill == nil {
return
}
if skill.MaxVal < amount {
skill.MaxVal = 0
} else {
skill.MaxVal -= amount
}
// Adjust current value if it exceeds new max
if skill.CurrentVal > skill.MaxVal {
skill.PreviousVal = skill.CurrentVal
skill.CurrentVal = skill.MaxVal
}
psl.AddSkillUpdateNeeded(skill)
skill.SaveNeeded = true
}
// DecreaseSkillCapByID decreases a skill's maximum value by ID
func (psl *PlayerSkillList) DecreaseSkillCapByID(skillID int32, amount int16) {
skill := psl.GetSkill(skillID)
psl.DecreaseSkillCap(skill, amount)
}
// SetSkillCap sets a skill's maximum value
func (psl *PlayerSkillList) SetSkillCap(skill *Skill, value int16) {
if skill == nil {
return
}
skill.MaxVal = value
// Adjust current value if it exceeds new max
if skill.CurrentVal > skill.MaxVal {
skill.PreviousVal = skill.CurrentVal
skill.CurrentVal = skill.MaxVal
}
psl.AddSkillUpdateNeeded(skill)
skill.SaveNeeded = true
}
// SetSkillCapByID sets a skill's maximum value by ID
func (psl *PlayerSkillList) SetSkillCapByID(skillID int32, value int16) {
skill := psl.GetSkill(skillID)
psl.SetSkillCap(skill, value)
}
// SetSkillValuesByType sets all skills of a type to a specific value
func (psl *PlayerSkillList) SetSkillValuesByType(skillType int8, value int16, sendUpdate bool) {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
for _, skill := range psl.skills {
if skill != nil && skill.SkillType == int32(skillType) {
psl.SetSkill(skill, value, sendUpdate)
}
}
}
// SetSkillCapsByType sets all skill caps of a type to a specific value
func (psl *PlayerSkillList) SetSkillCapsByType(skillType int8, value int16) {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
for _, skill := range psl.skills {
if skill != nil && skill.SkillType == int32(skillType) {
psl.SetSkillCap(skill, value)
}
}
}
// IncreaseSkillCapsByType increases all skill caps of a type
func (psl *PlayerSkillList) IncreaseSkillCapsByType(skillType int8, value int16) {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
for _, skill := range psl.skills {
if skill != nil && skill.SkillType == int32(skillType) {
psl.IncreaseSkillCap(skill, value)
}
}
}
// IncreaseAllSkillCaps increases all skill caps
func (psl *PlayerSkillList) IncreaseAllSkillCaps(value int16) {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
for _, skill := range psl.skills {
if skill != nil {
psl.IncreaseSkillCap(skill, value)
}
}
}
// CheckSkillIncrease checks if a skill should increase and does so if successful
func (psl *PlayerSkillList) CheckSkillIncrease(skill *Skill) bool {
if skill == nil || skill.CurrentVal >= skill.MaxVal {
return false
}
// Calculate increase chance: skill level 1 = 20%, 100 = 10%, 400 = 4%
percent := int8((100.0 / float32(50 + skill.CurrentVal)) * 10.0)
if rand.Intn(100) < int(percent) {
psl.IncreaseSkill(skill, 1)
return true
}
return false
}
// AddSkillUpdateNeeded marks a skill as needing an update packet
func (psl *PlayerSkillList) AddSkillUpdateNeeded(skill *Skill) {
if skill == nil {
return
}
psl.updatesMutex.Lock()
defer psl.updatesMutex.Unlock()
psl.skillUpdates = append(psl.skillUpdates, skill)
psl.hasUpdates = true
}
// HasSkillUpdates returns whether there are pending skill updates
func (psl *PlayerSkillList) HasSkillUpdates() bool {
psl.updatesMutex.Lock()
defer psl.updatesMutex.Unlock()
return psl.hasUpdates
}
// GetSkillUpdates returns and clears pending skill updates
func (psl *PlayerSkillList) GetSkillUpdates() []*Skill {
psl.updatesMutex.Lock()
defer psl.updatesMutex.Unlock()
if len(psl.skillUpdates) == 0 {
return nil
}
updates := make([]*Skill, len(psl.skillUpdates))
copy(updates, psl.skillUpdates)
// Clear the updates
psl.skillUpdates = psl.skillUpdates[:0]
psl.hasUpdates = false
return updates
}
// GetSaveNeededSkills returns skills that need to be saved to database
func (psl *PlayerSkillList) GetSaveNeededSkills() []*Skill {
psl.mutex.RLock()
defer psl.mutex.RUnlock()
var saveNeeded []*Skill
for _, skill := range psl.skills {
if skill.SaveNeeded {
saveNeeded = append(saveNeeded, skill)
skill.SaveNeeded = false // Clear the flag
}
}
return saveNeeded
}
// ResetPackets clears cached packet data
func (psl *PlayerSkillList) ResetPackets() {
psl.updatesMutex.Lock()
defer psl.updatesMutex.Unlock()
psl.origPacket = nil
psl.xorPacket = nil
psl.origPacketSize = 0
psl.packetCount = 0
}
// GetSkillPacket builds a skill update packet for the client
func (psl *PlayerSkillList) GetSkillPacket(version int16) ([]byte, error) {
psl.mutex.Lock()
defer psl.mutex.Unlock()
// This is a placeholder implementation
// In the full implementation, this would use the PacketStruct system
// to build a WS_UpdateSkillBook packet with all player skills
// TODO: Implement packet building using PacketStruct system
// packet := configReader.getStruct("WS_UpdateSkillBook", version)
// [complex packet building logic here]
// For now, return empty packet
return make([]byte, 0), nil
}

View File

@ -0,0 +1,175 @@
package skills
// AddSkillBonus adds a skill bonus from a spell
func (psl *PlayerSkillList) AddSkillBonus(spellID int32, skillID int32, value float32) {
if value == 0 {
return
}
psl.bonusMutex.Lock()
defer psl.bonusMutex.Unlock()
// Get or create skill bonus entry for this spell
skillBonus, exists := psl.skillBonusList[spellID]
if !exists {
skillBonus = &SkillBonus{
SpellID: spellID,
Skills: make(map[int32]*SkillBonusValue),
}
psl.skillBonusList[spellID] = skillBonus
}
// Add or update the skill bonus value
if skillBonus.Skills[skillID] == nil {
skillBonus.Skills[skillID] = &SkillBonusValue{
SkillID: skillID,
Value: value,
}
}
}
// GetSkillBonus returns skill bonus for a spell
func (psl *PlayerSkillList) GetSkillBonus(spellID int32) *SkillBonus {
psl.bonusMutex.RLock()
defer psl.bonusMutex.RUnlock()
if bonus, exists := psl.skillBonusList[spellID]; exists {
return bonus
}
return nil
}
// RemoveSkillBonus removes all skill bonuses from a spell
func (psl *PlayerSkillList) RemoveSkillBonus(spellID int32) {
psl.bonusMutex.Lock()
defer psl.bonusMutex.Unlock()
if skillBonus, exists := psl.skillBonusList[spellID]; exists {
// Clean up skill bonus values
for _, bonusValue := range skillBonus.Skills {
_ = bonusValue // In C++, this would be safe_delete(bonusValue)
}
delete(psl.skillBonusList, spellID)
}
}
// CalculateSkillValue calculates a skill's value including bonuses
func (psl *PlayerSkillList) CalculateSkillValue(skillID int32, currentVal int16) int16 {
if currentVal <= 5 {
return currentVal
}
psl.bonusMutex.RLock()
defer psl.bonusMutex.RUnlock()
newVal := currentVal
// Apply all skill bonuses
for _, skillBonus := range psl.skillBonusList {
if bonusValue, exists := skillBonus.Skills[skillID]; exists {
newVal += int16(bonusValue.Value)
}
}
return newVal
}
// CalculateSkillMaxValue calculates a skill's max value including bonuses
func (psl *PlayerSkillList) CalculateSkillMaxValue(skillID int32, maxVal int16) int16 {
psl.bonusMutex.RLock()
defer psl.bonusMutex.RUnlock()
newVal := maxVal
// Apply all skill bonuses to max value
for _, skillBonus := range psl.skillBonusList {
if bonusValue, exists := skillBonus.Skills[skillID]; exists {
newVal += int16(bonusValue.Value)
}
}
return newVal
}
// GetAllSkillBonuses returns all skill bonuses (for debugging/admin)
func (psl *PlayerSkillList) GetAllSkillBonuses() map[int32]*SkillBonus {
psl.bonusMutex.RLock()
defer psl.bonusMutex.RUnlock()
// Return a copy to prevent external modification
bonuses := make(map[int32]*SkillBonus)
for spellID, bonus := range psl.skillBonusList {
// Deep copy the skill bonus
newBonus := &SkillBonus{
SpellID: bonus.SpellID,
Skills: make(map[int32]*SkillBonusValue),
}
for skillID, bonusValue := range bonus.Skills {
newBonus.Skills[skillID] = &SkillBonusValue{
SkillID: bonusValue.SkillID,
Value: bonusValue.Value,
}
}
bonuses[spellID] = newBonus
}
return bonuses
}
// RemoveAllSkillBonuses removes all skill bonuses (for cleanup)
func (psl *PlayerSkillList) RemoveAllSkillBonuses() {
psl.bonusMutex.Lock()
defer psl.bonusMutex.Unlock()
// Clean up all skill bonuses
for spellID := range psl.skillBonusList {
if skillBonus, exists := psl.skillBonusList[spellID]; exists {
for _, bonusValue := range skillBonus.Skills {
_ = bonusValue // In C++, this would be safe_delete(bonusValue)
}
}
}
psl.skillBonusList = make(map[int32]*SkillBonus)
}
// GetSkillBonusTotal returns the total bonus for a specific skill
func (psl *PlayerSkillList) GetSkillBonusTotal(skillID int32) float32 {
psl.bonusMutex.RLock()
defer psl.bonusMutex.RUnlock()
var total float32
for _, skillBonus := range psl.skillBonusList {
if bonusValue, exists := skillBonus.Skills[skillID]; exists {
total += bonusValue.Value
}
}
return total
}
// HasSkillBonuses returns whether the player has any skill bonuses
func (psl *PlayerSkillList) HasSkillBonuses() bool {
psl.bonusMutex.RLock()
defer psl.bonusMutex.RUnlock()
return len(psl.skillBonusList) > 0
}
// GetSpellsWithSkillBonuses returns all spell IDs that provide skill bonuses
func (psl *PlayerSkillList) GetSpellsWithSkillBonuses() []int32 {
psl.bonusMutex.RLock()
defer psl.bonusMutex.RUnlock()
spellIDs := make([]int32, 0, len(psl.skillBonusList))
for spellID := range psl.skillBonusList {
spellIDs = append(spellIDs, spellID)
}
return spellIDs
}

156
internal/skills/types.go Normal file
View File

@ -0,0 +1,156 @@
package skills
import "eq2emu/internal/common"
// SkillBonusValue represents a single skill bonus value
type SkillBonusValue struct {
SkillID int32 // Skill being modified
Value float32 // Bonus value
}
// SkillBonus represents skill bonuses from a spell
type SkillBonus struct {
SpellID int32 // Spell providing the bonus
Skills map[int32]*SkillBonusValue // Map of skill ID to bonus value
}
// Skill represents a character skill
type Skill struct {
SkillID int32 // Unique skill identifier
CurrentVal int16 // Current skill value
PreviousVal int16 // Previous skill value (for deltas)
MaxVal int16 // Maximum skill value
SkillType int32 // Skill category type
Display int8 // Display setting
ShortName common.EQ2String16 // Short skill name
Name common.EQ2String16 // Full skill name
Description common.EQ2String16 // Skill description
SaveNeeded bool // Whether skill needs database save
ActiveSkill bool // Whether skill is active/usable
}
// NewSkill creates a new skill with default values
func NewSkill() *Skill {
return &Skill{
SkillID: 0,
CurrentVal: 0,
PreviousVal: 0,
MaxVal: 0,
SkillType: 0,
Display: 0,
SaveNeeded: false,
ActiveSkill: true,
}
}
// NewSkillFromSkill creates a copy of an existing skill
func NewSkillFromSkill(skill *Skill) *Skill {
if skill == nil {
return NewSkill()
}
return &Skill{
SkillID: skill.SkillID,
CurrentVal: skill.CurrentVal,
PreviousVal: skill.CurrentVal, // Copy current as previous
MaxVal: skill.MaxVal,
SkillType: skill.SkillType,
Display: skill.Display,
ShortName: skill.ShortName,
Name: skill.Name,
Description: skill.Description,
SaveNeeded: false,
ActiveSkill: true,
}
}
// CheckDisarmSkill checks disarm skill against a chest
// Returns 1 for success, 0 for fail (no trigger), -1 for fail with trigger
func (s *Skill) CheckDisarmSkill(targetLevel int16, chestDifficulty int8) int {
if chestDifficulty < 2 {
return DisarmSuccess // No triggers on easy chests
}
if targetLevel < 1 {
targetLevel = 1
}
chestDiffResult := int32(targetLevel) * int32(chestDifficulty)
baseDifficulty := float32(15.0)
failThreshold := float32(10.0)
// Calculate success chance
chance := (100.0 - baseDifficulty) * (float32(s.CurrentVal) / float32(chestDiffResult))
if chance > (100.0 - baseDifficulty) {
chance = 100.0 - baseDifficulty
}
// Roll d100
roll := makeRandomFloat(0, 100)
if roll <= chance {
return DisarmSuccess
} else if roll > (chance + failThreshold) {
return DisarmTrigger
}
return DisarmFail
}
// GetCurrentValue returns the current skill value
func (s *Skill) GetCurrentValue() int32 {
return int32(s.CurrentVal)
}
// GetMaxValue returns the maximum skill value
func (s *Skill) GetMaxValue() int32 {
return int32(s.MaxVal)
}
// GetName returns the skill name
func (s *Skill) GetName() string {
return s.Name.Data
}
// GetShortName returns the skill short name
func (s *Skill) GetShortName() string {
return s.ShortName.Data
}
// GetDescription returns the skill description
func (s *Skill) GetDescription() string {
return s.Description.Data
}
// GetSkillType returns the skill type
func (s *Skill) GetSkillType() int32 {
return s.SkillType
}
// IsActive returns whether the skill is active
func (s *Skill) IsActive() bool {
return s.ActiveSkill
}
// NeedsSave returns whether the skill needs to be saved
func (s *Skill) NeedsSave() bool {
return s.SaveNeeded
}
// SetSaveNeeded marks the skill as needing to be saved
func (s *Skill) SetSaveNeeded(needed bool) {
s.SaveNeeded = needed
}
// SetActive sets whether the skill is active
func (s *Skill) SetActive(active bool) {
s.ActiveSkill = active
}
// makeRandomFloat generates a random float between min and max
// TODO: Replace with proper random number generation when integrated
func makeRandomFloat(min, max float32) float32 {
// Placeholder implementation
return min + ((max - min) / 2.0)
}

View File

@ -0,0 +1,54 @@
package transmute
// Item flags that disqualify items from transmutation
const (
NoZone = 1 << 0 // NO_ZONE flag
NoValue = 1 << 1 // NO_VALUE flag
Temporary = 1 << 2 // TEMPORARY flag
NoDestroy = 1 << 3 // NO_DESTROY flag
NoTransmute = 1 << 14 // NO_TRANSMUTE flag (16384)
)
// Item flags2 that disqualify items from transmutation
const (
Ornate = 1 << 0 // ORNATE flag
)
// Item tiers/rarities
const (
ItemTagTreasured = 4
ItemTagLegendary = 5
ItemTagFabled = 6
ItemTagMythical = 7
ItemTagCelestial = 8
)
// Transmutation probabilities (percentages)
const (
BothItemsChancePercent = 15 // Chance to get both common and rare materials
CommonMatChancePercent = 75 // Chance to get common material (if not both)
RareMatChancePercent = 25 // Chance to get rare material (if not both)
)
// Skill up constants
const (
SkillUpPercentChanceMax = 50 // Base chance for skill up at max item level
SkillUpLevelDifPenalty = 20 // Percent decrease per level difference
MaxSkillUpLevelDif = 10 // Maximum level difference for skill up
)
// Spell constants
const (
TransmuteItemSpellID = 5163 // Spell ID for the transmute item spell
)
// Message channel constants (from C++)
const (
ChannelColorRed = 0
ChannelYellow = 89
)
// Request types
const (
RequestTypeTransmuteItem = 1
)

View File

@ -0,0 +1,249 @@
package transmute
import (
"fmt"
)
// DatabaseImpl provides a default implementation of the Database interface
type DatabaseImpl struct {
// Database connection or query executor would go here
// This is a placeholder implementation
}
// NewDatabase creates a new database implementation
func NewDatabase() *DatabaseImpl {
return &DatabaseImpl{}
}
// LoadTransmutingTiers loads transmuting tiers from the database
func (db *DatabaseImpl) LoadTransmutingTiers() ([]*TransmutingTier, error) {
// This is a placeholder implementation
// In a real implementation, this would query the database:
// SELECT min_level, max_level, fragment, powder, infusion, mana FROM transmuting
// For now, return some example tiers that match typical EQ2 level ranges
tiers := []*TransmutingTier{
{
MinLevel: 1,
MaxLevel: 9,
FragmentID: 1001, // Example fragment item ID
PowderID: 1002, // Example powder item ID
InfusionID: 1003, // Example infusion item ID
ManaID: 1004, // Example mana item ID
},
{
MinLevel: 10,
MaxLevel: 19,
FragmentID: 1005,
PowderID: 1006,
InfusionID: 1007,
ManaID: 1008,
},
{
MinLevel: 20,
MaxLevel: 29,
FragmentID: 1009,
PowderID: 1010,
InfusionID: 1011,
ManaID: 1012,
},
{
MinLevel: 30,
MaxLevel: 39,
FragmentID: 1013,
PowderID: 1014,
InfusionID: 1015,
ManaID: 1016,
},
{
MinLevel: 40,
MaxLevel: 49,
FragmentID: 1017,
PowderID: 1018,
InfusionID: 1019,
ManaID: 1020,
},
{
MinLevel: 50,
MaxLevel: 59,
FragmentID: 1021,
PowderID: 1022,
InfusionID: 1023,
ManaID: 1024,
},
{
MinLevel: 60,
MaxLevel: 69,
FragmentID: 1025,
PowderID: 1026,
InfusionID: 1027,
ManaID: 1028,
},
{
MinLevel: 70,
MaxLevel: 79,
FragmentID: 1029,
PowderID: 1030,
InfusionID: 1031,
ManaID: 1032,
},
{
MinLevel: 80,
MaxLevel: 89,
FragmentID: 1033,
PowderID: 1034,
InfusionID: 1035,
ManaID: 1036,
},
{
MinLevel: 90,
MaxLevel: 100,
FragmentID: 1037,
PowderID: 1038,
InfusionID: 1039,
ManaID: 1040,
},
}
return tiers, nil
}
// TODO: When integrating with a real database system, replace this with actual database queries
// Example SQL implementation would look like:
/*
func (db *DatabaseImpl) LoadTransmutingTiers() ([]*TransmutingTier, error) {
query := `SELECT min_level, max_level, fragment, powder, infusion, mana FROM transmuting ORDER BY min_level`
rows, err := db.connection.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to query transmuting tiers: %w", err)
}
defer rows.Close()
var tiers []*TransmutingTier
for rows.Next() {
tier := &TransmutingTier{}
err := rows.Scan(
&tier.MinLevel,
&tier.MaxLevel,
&tier.FragmentID,
&tier.PowderID,
&tier.InfusionID,
&tier.ManaID,
)
if err != nil {
return nil, fmt.Errorf("failed to scan transmuting tier: %w", err)
}
tiers = append(tiers, tier)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating transmuting tiers: %w", err)
}
return tiers, nil
}
*/
// SaveTransmutingTier saves a transmuting tier to the database
func (db *DatabaseImpl) SaveTransmutingTier(tier *TransmutingTier) error {
// Placeholder implementation
// In a real implementation:
// INSERT INTO transmuting (min_level, max_level, fragment, powder, infusion, mana) VALUES (?, ?, ?, ?, ?, ?)
// OR UPDATE if exists
if tier == nil {
return fmt.Errorf("tier cannot be nil")
}
// Validate tier data
if tier.MinLevel <= 0 || tier.MaxLevel <= 0 {
return fmt.Errorf("invalid level range: %d-%d", tier.MinLevel, tier.MaxLevel)
}
if tier.MinLevel > tier.MaxLevel {
return fmt.Errorf("min level (%d) cannot be greater than max level (%d)", tier.MinLevel, tier.MaxLevel)
}
if tier.FragmentID <= 0 || tier.PowderID <= 0 || tier.InfusionID <= 0 || tier.ManaID <= 0 {
return fmt.Errorf("all material IDs must be positive")
}
// TODO: Actual database save operation
return nil
}
// DeleteTransmutingTier deletes a transmuting tier from the database
func (db *DatabaseImpl) DeleteTransmutingTier(minLevel, maxLevel int32) error {
// Placeholder implementation
// In a real implementation:
// DELETE FROM transmuting WHERE min_level = ? AND max_level = ?
if minLevel <= 0 || maxLevel <= 0 {
return fmt.Errorf("invalid level range: %d-%d", minLevel, maxLevel)
}
// TODO: Actual database delete operation
return nil
}
// GetTransmutingTierByLevel gets a specific transmuting tier by level range
func (db *DatabaseImpl) GetTransmutingTierByLevel(itemLevel int32) (*TransmutingTier, error) {
// Placeholder implementation
// In a real implementation:
// SELECT min_level, max_level, fragment, powder, infusion, mana FROM transmuting WHERE min_level <= ? AND max_level >= ?
tiers, err := db.LoadTransmutingTiers()
if err != nil {
return nil, err
}
for _, tier := range tiers {
if tier.MinLevel <= itemLevel && tier.MaxLevel >= itemLevel {
return tier, nil
}
}
return nil, fmt.Errorf("no transmuting tier found for level %d", itemLevel)
}
// UpdateTransmutingTier updates an existing transmuting tier
func (db *DatabaseImpl) UpdateTransmutingTier(oldMinLevel, oldMaxLevel int32, newTier *TransmutingTier) error {
// Placeholder implementation
// In a real implementation:
// UPDATE transmuting SET min_level=?, max_level=?, fragment=?, powder=?, infusion=?, mana=? WHERE min_level=? AND max_level=?
if newTier == nil {
return fmt.Errorf("new tier cannot be nil")
}
// Validate the new tier
if err := db.SaveTransmutingTier(newTier); err != nil {
return fmt.Errorf("invalid new tier data: %w", err)
}
// TODO: Actual database update operation
return nil
}
// TransmutingTierExists checks if a transmuting tier exists for the given level range
func (db *DatabaseImpl) TransmutingTierExists(minLevel, maxLevel int32) (bool, error) {
// Placeholder implementation
// In a real implementation:
// SELECT COUNT(*) FROM transmuting WHERE min_level = ? AND max_level = ?
tiers, err := db.LoadTransmutingTiers()
if err != nil {
return false, err
}
for _, tier := range tiers {
if tier.MinLevel == minLevel && tier.MaxLevel == maxLevel {
return true, nil
}
}
return false, nil
}

View File

@ -0,0 +1,350 @@
package transmute
import (
"fmt"
"sync"
"time"
)
// Manager provides high-level management of the transmutation system
type Manager struct {
transmuter *Transmuter
database Database
requestTimeout time.Duration
cleanupTicker *time.Ticker
mutex sync.RWMutex
// Statistics
totalTransmutes int64
successfulTransmutes int64
failedTransmutes int64
materialCounts map[int32]int64 // Material ID -> count produced
}
// NewManager creates a new transmutation manager
func NewManager(database Database, itemMaster ItemMaster, spellMaster SpellMaster, packetBuilder PacketBuilder) *Manager {
transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder)
manager := &Manager{
transmuter: transmuter,
database: database,
requestTimeout: 5 * time.Minute, // Requests expire after 5 minutes
materialCounts: make(map[int32]int64),
}
// Start cleanup routine
manager.cleanupTicker = time.NewTicker(1 * time.Minute)
go manager.cleanupRoutine()
return manager
}
// Initialize loads transmuting data from database
func (m *Manager) Initialize() error {
return m.transmuter.LoadTransmutingTiers(m.database)
}
// CreateItemRequest creates a new transmutation item selection request
func (m *Manager) CreateItemRequest(client Client, player Player) (int32, error) {
return m.transmuter.CreateItemRequest(client, player)
}
// HandleItemResponse handles the player's item selection response
func (m *Manager) HandleItemResponse(client Client, player Player, requestID int32, itemID int32) error {
return m.transmuter.HandleItemResponse(client, player, requestID, itemID)
}
// HandleConfirmResponse handles the player's confirmation response
func (m *Manager) HandleConfirmResponse(client Client, player Player, itemID int32) error {
return m.transmuter.HandleConfirmResponse(client, player, itemID)
}
// CompleteTransmutation completes the transmutation process
func (m *Manager) CompleteTransmutation(client Client, player Player) error {
m.mutex.Lock()
m.totalTransmutes++
m.mutex.Unlock()
err := m.transmuter.CompleteTransmutation(client, player)
m.mutex.Lock()
if err != nil {
m.failedTransmutes++
} else {
m.successfulTransmutes++
}
m.mutex.Unlock()
return err
}
// IsItemTransmutable checks if an item can be transmuted
func (m *Manager) IsItemTransmutable(item Item) bool {
return m.transmuter.IsItemTransmutable(item)
}
// GetTransmutingTiers returns the current transmuting tiers
func (m *Manager) GetTransmutingTiers() []*TransmutingTier {
return m.transmuter.GetTransmutingTiers()
}
// ReloadTransmutingTiers reloads transmuting tiers from database
func (m *Manager) ReloadTransmutingTiers() error {
return m.transmuter.LoadTransmutingTiers(m.database)
}
// GetStatistics returns transmutation statistics
func (m *Manager) GetStatistics() map[string]interface{} {
m.mutex.RLock()
defer m.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_transmutes"] = m.totalTransmutes
stats["successful_transmutes"] = m.successfulTransmutes
stats["failed_transmutes"] = m.failedTransmutes
if m.totalTransmutes > 0 {
stats["success_rate"] = float64(m.successfulTransmutes) / float64(m.totalTransmutes) * 100
}
// Copy material counts
materialStats := make(map[int32]int64)
for matID, count := range m.materialCounts {
materialStats[matID] = count
}
stats["material_counts"] = materialStats
return stats
}
// RecordMaterialProduced records that a material was produced (for statistics)
func (m *Manager) RecordMaterialProduced(materialID int32, count int32) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.materialCounts[materialID] += int64(count)
}
// GetMaterialProductionCount returns how many of a material have been produced
func (m *Manager) GetMaterialProductionCount(materialID int32) int64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.materialCounts[materialID]
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalTransmutes = 0
m.successfulTransmutes = 0
m.failedTransmutes = 0
m.materialCounts = make(map[int32]int64)
}
// ValidateTransmutingSetup validates that all transmuting tiers are properly configured
func (m *Manager) ValidateTransmutingSetup() []string {
tiers := m.GetTransmutingTiers()
issues := make([]string, 0)
if len(tiers) == 0 {
issues = append(issues, "No transmuting tiers configured")
return issues
}
// Check for gaps or overlaps in level ranges
for i, tier := range tiers {
if tier.MinLevel <= 0 {
issues = append(issues, fmt.Sprintf("Tier %d has invalid min level: %d", i, tier.MinLevel))
}
if tier.MaxLevel < tier.MinLevel {
issues = append(issues, fmt.Sprintf("Tier %d has max level (%d) less than min level (%d)",
i, tier.MaxLevel, tier.MinLevel))
}
if tier.FragmentID <= 0 {
issues = append(issues, fmt.Sprintf("Tier %d has invalid fragment ID: %d", i, tier.FragmentID))
}
if tier.PowderID <= 0 {
issues = append(issues, fmt.Sprintf("Tier %d has invalid powder ID: %d", i, tier.PowderID))
}
if tier.InfusionID <= 0 {
issues = append(issues, fmt.Sprintf("Tier %d has invalid infusion ID: %d", i, tier.InfusionID))
}
if tier.ManaID <= 0 {
issues = append(issues, fmt.Sprintf("Tier %d has invalid mana ID: %d", i, tier.ManaID))
}
// Check for overlaps with other tiers
for j, otherTier := range tiers {
if i != j {
if (tier.MinLevel <= otherTier.MaxLevel && tier.MaxLevel >= otherTier.MinLevel) {
issues = append(issues, fmt.Sprintf("Tier %d (levels %d-%d) overlaps with tier %d (levels %d-%d)",
i, tier.MinLevel, tier.MaxLevel, j, otherTier.MinLevel, otherTier.MaxLevel))
}
}
}
}
return issues
}
// GetTierForItemLevel returns the transmuting tier for a given item level
func (m *Manager) GetTierForItemLevel(itemLevel int32) *TransmutingTier {
tiers := m.GetTransmutingTiers()
for _, tier := range tiers {
if tier.MinLevel <= itemLevel && tier.MaxLevel >= itemLevel {
return tier
}
}
return nil
}
// GetTransmutableItems returns all transmutable items from a player's inventory
func (m *Manager) GetTransmutableItems(player Player) []Item {
itemList := player.GetItemList()
transmutable := make([]Item, 0)
for _, item := range itemList {
if item != nil && m.IsItemTransmutable(item) {
transmutable = append(transmutable, item)
}
}
return transmutable
}
// CalculateRequiredSkill calculates the transmuting skill required for an item
func (m *Manager) CalculateRequiredSkill(item Item) int32 {
itemLevel := item.GetAdventureDefaultLevel()
if itemLevel <= 5 {
return 0
}
return (itemLevel - 5) * 5
}
// CanPlayerTransmuteItem checks if a player can transmute a specific item
func (m *Manager) CanPlayerTransmuteItem(player Player, item Item) (bool, string) {
if !m.IsItemTransmutable(item) {
return false, fmt.Sprintf("%s is not transmutable", item.GetName())
}
requiredSkill := m.CalculateRequiredSkill(item)
skill := player.GetSkillByName("Transmuting")
currentSkill := int32(0)
if skill != nil {
currentSkill = skill.GetCurrentValue() + player.GetStat(ItemStatTransmuting)
}
if currentSkill < requiredSkill {
return false, fmt.Sprintf("Need %d Transmuting skill, have %d", requiredSkill, currentSkill)
}
return true, ""
}
// cleanupRoutine runs periodically to cleanup expired requests
func (m *Manager) cleanupRoutine() {
for range m.cleanupTicker.C {
// TODO: Implement request cleanup based on timestamps
// For now, this is a placeholder for future cleanup logic
// In a full implementation, we'd track request timestamps
// and remove requests older than the timeout period
}
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.cleanupTicker != nil {
m.cleanupTicker.Stop()
}
}
// ProcessCommand handles transmutation-related commands
func (m *Manager) ProcessCommand(command string, args []string, client Client, player Player) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "reload":
return m.handleReloadCommand(args)
case "tiers":
return m.handleTiersCommand(args)
default:
return "", fmt.Errorf("unknown transmute command: %s", command)
}
}
// handleStatsCommand shows transmutation statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Transmutation Statistics:\n"
result += fmt.Sprintf("Total Transmutes: %d\n", stats["total_transmutes"])
result += fmt.Sprintf("Successful: %d\n", stats["successful_transmutes"])
result += fmt.Sprintf("Failed: %d\n", stats["failed_transmutes"])
if successRate, exists := stats["success_rate"]; exists {
result += fmt.Sprintf("Success Rate: %.1f%%\n", successRate)
}
return result, nil
}
// handleValidateCommand validates the transmuting setup
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateTransmutingSetup()
if len(issues) == 0 {
return "Transmuting setup is valid.", nil
}
result := fmt.Sprintf("Found %d issues with transmuting setup:\n", len(issues))
for i, issue := range issues {
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
// handleReloadCommand reloads transmuting data
func (m *Manager) handleReloadCommand(args []string) (string, error) {
err := m.ReloadTransmutingTiers()
if err != nil {
return "", fmt.Errorf("failed to reload transmuting tiers: %w", err)
}
return "Transmuting tiers reloaded successfully.", nil
}
// handleTiersCommand shows transmuting tier information
func (m *Manager) handleTiersCommand(args []string) (string, error) {
tiers := m.GetTransmutingTiers()
if len(tiers) == 0 {
return "No transmuting tiers configured.", nil
}
result := fmt.Sprintf("Transmuting Tiers (%d):\n", len(tiers))
for i, tier := range tiers {
result += fmt.Sprintf("%d. Levels %d-%d: Fragment(%d) Powder(%d) Infusion(%d) Mana(%d)\n",
i+1, tier.MinLevel, tier.MaxLevel, tier.FragmentID, tier.PowderID, tier.InfusionID, tier.ManaID)
}
return result, nil
}
// Constants for stat types - these would typically be defined elsewhere
const (
ItemStatTransmuting = 1 // Placeholder - actual value depends on stat system
)

View File

@ -0,0 +1,168 @@
package transmute
import (
"fmt"
)
// PacketBuilderImpl provides a default implementation of the PacketBuilder interface
type PacketBuilderImpl struct {
// Packet configuration or builder would go here
// This is a placeholder implementation
}
// NewPacketBuilder creates a new packet builder implementation
func NewPacketBuilder() *PacketBuilderImpl {
return &PacketBuilderImpl{}
}
// BuildItemRequestPacket builds a packet for transmutable item selection
func (pb *PacketBuilderImpl) BuildItemRequestPacket(requestID int32, items []int32, version int32) ([]byte, error) {
// This is a placeholder implementation
// In a real implementation, this would use the PacketStruct system:
// PacketStruct* p = configReader.getStruct("WS_EqTargetItemCmd", version)
// p->setDataByName("request_id", requestID)
// p->setDataByName("request_type", REQUEST_TYPE_TRANSMUTE_ITEM)
// p->setDataByName("unknownff", 0xff)
// p->setArrayLengthByName("item_array_size", len(items))
// for i, itemID := range items {
// p->setArrayDataByName("item_id", itemID, i)
// }
// return p->serialize()
if len(items) == 0 {
return nil, fmt.Errorf("no transmutable items found")
}
// TODO: Build actual packet using packet structure system
// For now, return a placeholder packet
packet := make([]byte, 0)
return packet, nil
}
// BuildConfirmationPacket builds a confirmation dialog packet
func (pb *PacketBuilderImpl) BuildConfirmationPacket(requestID int32, item Item, version int32) ([]byte, error) {
// This is a placeholder implementation
// In a real implementation, this would use the PacketStruct system:
// PacketStruct* p = configReader.getStruct("WS_ChoiceWindow", version)
// message := fmt.Sprintf("Are you sure you want to transmute the %s?", item.GetName())
// p->setMediumStringByName("text", message)
// p->setMediumStringByName("accept_text", "OK")
// acceptCommand := fmt.Sprintf("targetitem %d %d 1", requestID, item.GetUniqueID())
// cancelCommand := fmt.Sprintf("targetitem %d %d", requestID, item.GetUniqueID())
// p->setMediumStringByName("accept_command", acceptCommand)
// p->setMediumStringByName("cancel_text", "Cancel")
// p->setMediumStringByName("cancel_command", cancelCommand)
// return p->serialize()
if item == nil {
return nil, fmt.Errorf("item cannot be nil")
}
// TODO: Build actual packet using packet structure system
// For now, return a placeholder packet
packet := make([]byte, 0)
return packet, nil
}
// BuildRewardPacket builds a quest completion/reward packet
func (pb *PacketBuilderImpl) BuildRewardPacket(items []Item, version int32) ([]byte, error) {
// This is a placeholder implementation
// In a real implementation, this would use the PacketStruct system:
// PacketStruct* packet = configReader.getStruct("WS_QuestComplete", version)
// packet->setDataByName("title", "Item Transmuted!")
// packet->setArrayLengthByName("num_rewards", len(items))
// for i, item := range items {
// packet->setArrayDataByName("reward_id", item.GetID(), i)
// if version < 860 {
// packet->setItemArrayDataByName("item", item, player, i, 0, -1)
// } else if version < 1193 {
// packet->setItemArrayDataByName("item", item, player, i)
// } else {
// packet->setItemArrayDataByName("item", item, player, i, 0, 2)
// }
// }
// return packet->serialize()
if len(items) == 0 {
return nil, fmt.Errorf("no reward items provided")
}
// TODO: Build actual packet using packet structure system
// For now, return a placeholder packet
packet := make([]byte, 0)
return packet, nil
}
// TODO: When integrating with the real packet system, these methods would look like:
/*
// Example of actual packet building using the EQ2 packet structure system
func (pb *PacketBuilderImpl) BuildItemRequestPacket(requestID int32, items []int32, version int32) ([]byte, error) {
// Get the packet structure for this version
packetStruct := pb.configReader.GetStruct("WS_EqTargetItemCmd", version)
if packetStruct == nil {
return nil, fmt.Errorf("could not find packet struct WS_EqTargetItemCmd for version %d", version)
}
// Set the basic fields
packetStruct.SetDataByName("request_id", requestID)
packetStruct.SetDataByName("request_type", REQUEST_TYPE_TRANSMUTE_ITEM)
packetStruct.SetDataByName("unknownff", 0xff)
// Set the item array
packetStruct.SetArrayLengthByName("item_array_size", len(items))
for i, itemID := range items {
packetStruct.SetArrayDataByName("item_id", itemID, i)
}
// Serialize and return
return packetStruct.Serialize()
}
func (pb *PacketBuilderImpl) BuildConfirmationPacket(requestID int32, item Item, version int32) ([]byte, error) {
packetStruct := pb.configReader.GetStruct("WS_ChoiceWindow", version)
if packetStruct == nil {
return nil, fmt.Errorf("could not find packet struct WS_ChoiceWindow for version %d", version)
}
// Build the confirmation message
message := fmt.Sprintf("Are you sure you want to transmute the %s?", item.GetName())
packetStruct.SetMediumStringByName("text", message)
packetStruct.SetMediumStringByName("accept_text", "OK")
// Build the command strings
acceptCommand := fmt.Sprintf("targetitem %d %d 1", requestID, item.GetUniqueID())
cancelCommand := fmt.Sprintf("targetitem %d %d", requestID, item.GetUniqueID())
packetStruct.SetMediumStringByName("accept_command", acceptCommand)
packetStruct.SetMediumStringByName("cancel_text", "Cancel")
packetStruct.SetMediumStringByName("cancel_command", cancelCommand)
return packetStruct.Serialize()
}
func (pb *PacketBuilderImpl) BuildRewardPacket(items []Item, version int32) ([]byte, error) {
packetStruct := pb.configReader.GetStruct("WS_QuestComplete", version)
if packetStruct == nil {
return nil, fmt.Errorf("could not find packet struct WS_QuestComplete for version %d", version)
}
packetStruct.SetDataByName("title", "Item Transmuted!")
packetStruct.SetArrayLengthByName("num_rewards", len(items))
for i, item := range items {
packetStruct.SetArrayDataByName("reward_id", item.GetID(), i)
// Version-specific item serialization
if version < 860 {
packetStruct.SetItemArrayDataByName("item", item, nil, i, 0, -1)
} else if version < 1193 {
packetStruct.SetItemArrayDataByName("item", item, nil, i)
} else {
packetStruct.SetItemArrayDataByName("item", item, nil, i, 0, 2)
}
}
return packetStruct.Serialize()
}
*/

View File

@ -0,0 +1,415 @@
package transmute
import (
"fmt"
"math"
"math/rand"
"sync"
)
// Transmuter manages the transmutation system
type Transmuter struct {
tiers []*TransmutingTier
activeRequests map[int32]*TransmuteRequest
itemMaster ItemMaster
spellMaster SpellMaster
packetBuilder PacketBuilder
mutex sync.RWMutex
requestMutex sync.Mutex
}
// SpellMaster represents the spell system interface
type SpellMaster interface {
GetSpell(spellID int32, tier int32) Spell
}
// NewTransmuter creates a new transmuter instance
func NewTransmuter(itemMaster ItemMaster, spellMaster SpellMaster, packetBuilder PacketBuilder) *Transmuter {
return &Transmuter{
tiers: make([]*TransmutingTier, 0),
activeRequests: make(map[int32]*TransmuteRequest),
itemMaster: itemMaster,
spellMaster: spellMaster,
packetBuilder: packetBuilder,
}
}
// LoadTransmutingTiers loads transmuting tiers from database
func (t *Transmuter) LoadTransmutingTiers(database Database) error {
t.mutex.Lock()
defer t.mutex.Unlock()
tiers, err := database.LoadTransmutingTiers()
if err != nil {
return fmt.Errorf("failed to load transmuting tiers: %w", err)
}
t.tiers = tiers
return nil
}
// GetTransmutingTiers returns a copy of the transmuting tiers
func (t *Transmuter) GetTransmutingTiers() []*TransmutingTier {
t.mutex.RLock()
defer t.mutex.RUnlock()
// Return a copy to prevent external modification
tiers := make([]*TransmutingTier, len(t.tiers))
for i, tier := range t.tiers {
tiers[i] = &TransmutingTier{
MinLevel: tier.MinLevel,
MaxLevel: tier.MaxLevel,
FragmentID: tier.FragmentID,
PowderID: tier.PowderID,
InfusionID: tier.InfusionID,
ManaID: tier.ManaID,
}
}
return tiers
}
// IsItemTransmutable checks if an item can be transmuted
func (t *Transmuter) IsItemTransmutable(item Item) bool {
// Item level > 0 AND Item is not LORE_EQUP, LORE, NO_VALUE etc AND item rarity is >= 5
// (4 is treasured but the rarity used for journeyman spells)
// Flag 16384 is NO-TRANSMUTE
disqualifyFlags := NoZone | NoValue | Temporary | NoDestroy | NoTransmute
disqualifyFlags2 := Ornate
if item.GetAdventureDefaultLevel() > 0 &&
(item.GetItemFlags()&disqualifyFlags) == 0 &&
(item.GetItemFlags2()&disqualifyFlags2) == 0 &&
item.GetTier() >= ItemTagLegendary &&
item.GetStackCount() <= 1 {
return true
}
return false
}
// CreateItemRequest creates a new transmutation item selection request
func (t *Transmuter) CreateItemRequest(client Client, player Player) (int32, error) {
// Generate unique request ID
var requestID int32
for {
// Generate random signed 32-bit integer (excluding 0)
requestID = rand.Int31()
if requestID != 0 && rand.Intn(2) == 1 {
requestID = -requestID // Make it negative sometimes like C++
}
if requestID != 0 {
break
}
}
// Get player's item list
itemList := player.GetItemList()
transmutables := make([]int32, 0)
// Find all transmutable items
for itemID, item := range itemList {
if item != nil && t.IsItemTransmutable(item) {
transmutables = append(transmutables, itemID)
}
}
// Build and send packet
packet, err := t.packetBuilder.BuildItemRequestPacket(requestID, transmutables, client.GetVersion())
if err != nil {
return 0, fmt.Errorf("failed to build item request packet: %w", err)
}
client.QueuePacket(packet)
client.SetTransmuteID(requestID)
// Store the request
t.requestMutex.Lock()
t.activeRequests[requestID] = &TransmuteRequest{
RequestID: requestID,
ClientID: 0, // TODO: Get client ID when available
Phase: PhaseItemSelection,
}
t.requestMutex.Unlock()
return requestID, nil
}
// HandleItemResponse handles the player's item selection response
func (t *Transmuter) HandleItemResponse(client Client, player Player, requestID int32, itemID int32) error {
// Find the item
item := player.GetItemFromUniqueID(itemID)
if item == nil {
client.SimpleMessage(ChannelColorRed, "Could not find the item you wish to transmute. Please try again.")
return fmt.Errorf("item not found: %d", itemID)
}
// Verify item is transmutable
if !t.IsItemTransmutable(item) {
client.Message(ChannelColorRed, "%s is not transmutable.", item.GetName())
return fmt.Errorf("item not transmutable: %s", item.GetName())
}
// Check transmuting skill requirement
itemLevel := item.GetAdventureDefaultLevel()
skill := player.GetSkillByName("Transmuting")
requiredSkill := int32(math.Max(float64(itemLevel-5), 0) * 5)
itemStatBonus := player.GetStat(ItemStatTransmuting) // TODO: Define this constant
currentSkill := int32(0)
if skill != nil {
currentSkill = skill.GetCurrentValue() + itemStatBonus
}
if skill == nil || currentSkill < requiredSkill {
client.Message(ChannelColorRed, "You need at least %d Transmuting skill to transmute the %s. You have %d Transmuting skill.",
requiredSkill, item.GetName(), currentSkill)
return fmt.Errorf("insufficient transmuting skill: need %d, have %d", requiredSkill, currentSkill)
}
// Update request state
t.requestMutex.Lock()
if request, exists := t.activeRequests[requestID]; exists {
request.ItemID = itemID
request.Phase = PhaseConfirmation
}
t.requestMutex.Unlock()
client.SetTransmuteID(itemID)
// Send confirmation request
return t.SendConfirmRequest(client, requestID, item)
}
// SendConfirmRequest sends a confirmation dialog to the client
func (t *Transmuter) SendConfirmRequest(client Client, requestID int32, item Item) error {
packet, err := t.packetBuilder.BuildConfirmationPacket(requestID, item, client.GetVersion())
if err != nil {
client.SimpleMessage(ChannelColorRed, "Struct error for transmutation. Let a dev know.")
return fmt.Errorf("failed to build confirmation packet: %w", err)
}
client.QueuePacket(packet)
return nil
}
// HandleConfirmResponse handles the player's confirmation response
func (t *Transmuter) HandleConfirmResponse(client Client, player Player, itemID int32) error {
// Find the item
item := player.GetItemFromUniqueID(itemID)
if item == nil {
client.SimpleMessage(ChannelColorRed, "Item no longer exists!")
return fmt.Errorf("item no longer exists: %d", itemID)
}
client.SetTransmuteID(itemID)
// Get the zone
zone := player.GetZone()
if zone == nil {
return fmt.Errorf("player not in zone")
}
// Get the transmute spell
spell := t.spellMaster.GetSpell(TransmuteItemSpellID, 1)
if spell == nil {
return fmt.Errorf("could not find transmute item spell: %d", TransmuteItemSpellID)
}
// Process the spell (this will call CompleteTransmutation when finished)
return zone.ProcessSpell(spell, player)
}
// CompleteTransmutation completes the transmutation process
func (t *Transmuter) CompleteTransmutation(client Client, player Player) error {
itemID := client.GetTransmuteID()
item := player.GetItemFromUniqueID(itemID)
if item == nil {
client.SimpleMessage(ChannelColorRed, "Item no longer exists!")
return fmt.Errorf("item no longer exists: %d", itemID)
}
// Determine materials based on item level and tier
result, err := t.calculateTransmuteResult(item)
if err != nil {
client.SimpleMessage(ChannelColorRed, "Could not complete transmutation! Tell a dev!")
return fmt.Errorf("failed to calculate transmute result: %w", err)
}
if !result.Success {
client.SimpleMessage(ChannelColorRed, result.ErrorMessage)
return fmt.Errorf("transmutation failed: %s", result.ErrorMessage)
}
// Remove the original item
if !player.RemoveItem(item, true) {
return fmt.Errorf("failed to remove transmuted item")
}
// Send completion message
client.Message(ChannelYellow, "You transmute %s and create: ", item.CreateItemLink(client.GetVersion(), false))
// Add the resulting materials
rewardItems := make([]Item, 0, 2)
if result.CommonMaterial != nil {
result.CommonMaterial.SetCount(1)
client.Message(ChannelYellow, " %s", result.CommonMaterial.CreateItemLink(client.GetVersion(), false))
var itemDeleted bool
if err := client.AddItem(result.CommonMaterial, &itemDeleted); err != nil {
return fmt.Errorf("failed to add common material: %w", err)
}
if !itemDeleted {
rewardItems = append(rewardItems, result.CommonMaterial)
}
}
if result.RareMaterial != nil {
result.RareMaterial.SetCount(1)
client.Message(ChannelYellow, " %s", result.RareMaterial.CreateItemLink(client.GetVersion(), false))
var itemDeleted bool
if err := client.AddItem(result.RareMaterial, &itemDeleted); err != nil {
return fmt.Errorf("failed to add rare material: %w", err)
}
if !itemDeleted {
rewardItems = append(rewardItems, result.RareMaterial)
}
}
// Send reward packet if there are items
if len(rewardItems) > 0 {
packet, err := t.packetBuilder.BuildRewardPacket(rewardItems, client.GetVersion())
if err == nil {
client.QueuePacket(packet)
}
}
// Handle skill up
return t.handleSkillUp(player, item)
}
// calculateTransmuteResult determines what materials are produced from transmutation
func (t *Transmuter) calculateTransmuteResult(item Item) (*TransmuteResult, error) {
t.mutex.RLock()
defer t.mutex.RUnlock()
itemLevel := item.GetAdventureDefaultLevel()
var tier *TransmutingTier
// Find the correct tier
for _, t := range t.tiers {
if t.MinLevel <= itemLevel && t.MaxLevel >= itemLevel {
tier = t
break
}
}
if tier == nil {
return &TransmuteResult{
Success: false,
ErrorMessage: "No transmuting tier found for item level",
}, nil
}
// Determine material types based on item tier
itemTier := item.GetTier()
var commonMatID, rareMatID int32
if itemTier >= ItemTagFabled {
commonMatID = tier.InfusionID
rareMatID = tier.ManaID
} else if itemTier >= ItemTagLegendary {
commonMatID = tier.PowderID
rareMatID = tier.InfusionID
} else {
commonMatID = tier.FragmentID
rareMatID = tier.PowderID
}
if commonMatID == 0 || rareMatID == 0 {
return &TransmuteResult{
Success: false,
ErrorMessage: "Invalid material IDs for transmutation",
}, nil
}
// Do the loot roll
result := &TransmuteResult{Success: true}
roll := rand.Intn(100) + 1
if roll <= BothItemsChancePercent {
// Both items
result.CommonMaterial = t.itemMaster.CreateItem(commonMatID)
result.RareMaterial = t.itemMaster.CreateItem(rareMatID)
} else if roll <= CommonMatChancePercent {
// Common material only
result.CommonMaterial = t.itemMaster.CreateItem(commonMatID)
} else {
// Rare material only
result.RareMaterial = t.itemMaster.CreateItem(rareMatID)
}
return result, nil
}
// handleSkillUp processes potential skill increases from transmutation
func (t *Transmuter) handleSkillUp(player Player, item Item) error {
skill := player.GetSkillByName("Transmuting")
if skill == nil {
return fmt.Errorf("unable to find transmuting skill for player %s", player.GetName())
}
// Calculate skill up chance
itemLevel := item.GetAdventureDefaultLevel()
maxTransLevel := skill.GetCurrentValue()/5 + 5
levelDif := int32(maxTransLevel) - itemLevel
// No skill up if level difference is too high or skill is maxed
if levelDif > MaxSkillUpLevelDif || skill.GetCurrentValue() >= skill.GetMaxValue() {
return nil
}
// Calculate skill up probability
// 50% base chance at max item level, 20% decrease per level difference
baseChance := float64(SkillUpPercentChanceMax)
penalty := 0.0
if itemLevel > 5 {
penalty = float64(levelDif) * 0.2
}
requiredRoll := int32(baseChance * (1.0 - penalty))
roll := rand.Intn(100) + 1
if int32(roll) <= requiredRoll {
return player.IncreaseSkill("Transmuting", 1)
}
return nil
}
// CleanupRequest removes a completed or expired request
func (t *Transmuter) CleanupRequest(requestID int32) {
t.requestMutex.Lock()
defer t.requestMutex.Unlock()
delete(t.activeRequests, requestID)
}
// GetActiveRequest returns an active request by ID
func (t *Transmuter) GetActiveRequest(requestID int32) *TransmuteRequest {
t.requestMutex.Lock()
defer t.requestMutex.Unlock()
if request, exists := t.activeRequests[requestID]; exists {
// Return a copy to prevent external modification
return &TransmuteRequest{
RequestID: request.RequestID,
ClientID: request.ClientID,
ItemID: request.ItemID,
Phase: request.Phase,
}
}
return nil
}

111
internal/transmute/types.go Normal file
View File

@ -0,0 +1,111 @@
package transmute
// TransmutingTier represents a level range and associated material IDs for transmutation
type TransmutingTier struct {
MinLevel int32 // Minimum item level for this tier
MaxLevel int32 // Maximum item level for this tier
FragmentID int32 // Item ID for fragments (lowest tier materials)
PowderID int32 // Item ID for powder (mid tier materials)
InfusionID int32 // Item ID for infusions (high tier materials)
ManaID int32 // Item ID for mana (highest tier materials)
}
// TransmuteRequest represents an active transmutation request
type TransmuteRequest struct {
RequestID int32 // Unique request identifier
ClientID int32 // Client making the request
ItemID int32 // Item being transmuted (if in confirmation phase)
Phase TransmutePhase
}
// TransmutePhase represents the current phase of transmutation
type TransmutePhase int
const (
PhaseItemSelection TransmutePhase = iota // Player selecting item to transmute
PhaseConfirmation // Player confirming transmutation
PhaseProcessing // Transmutation in progress
PhaseComplete // Transmutation completed
)
// TransmuteResult represents the outcome of a transmutation
type TransmuteResult struct {
Success bool // Whether transmutation was successful
CommonMaterial *Item // Common material received (if any)
RareMaterial *Item // Rare material received (if any)
ErrorMessage string // Error message if unsuccessful
SkillIncrease bool // Whether player received skill increase
}
// Item represents the minimal item interface needed for transmutation
type Item interface {
GetID() int32
GetUniqueID() int32
GetName() string
GetAdventureDefaultLevel() int32
GetItemFlags() int32
GetItemFlags2() int32
GetTier() int32
GetStackCount() int32
CreateItemLink(version int32, detailed bool) string
SetCount(count int32)
}
// Player represents the minimal player interface needed for transmutation
type Player interface {
GetItemList() map[int32]Item
GetItemFromUniqueID(uniqueID int32) Item
GetSkillByName(skillName string) Skill
GetStat(statType int32) int32
GetName() string
GetZone() Zone
RemoveItem(item Item, deleteItem bool) bool
AddItem(item Item) (bool, error)
IncreaseSkill(skillName string, amount int32) error
}
// Client represents the minimal client interface needed for transmutation
type Client interface {
GetVersion() int32
GetTransmuteID() int32
SetTransmuteID(id int32)
QueuePacket(packet []byte)
SimpleMessage(channel int32, message string)
Message(channel int32, format string, args ...interface{})
AddItem(item Item, itemDeleted *bool) error
}
// Skill represents a player skill
type Skill interface {
GetCurrentValue() int32
GetMaxValue() int32
}
// Zone represents a game zone
type Zone interface {
ProcessSpell(spell Spell, caster Player) error
}
// Spell represents a spell that can be cast
type Spell interface {
GetID() int32
GetName() string
}
// Database represents the database interface for transmutation
type Database interface {
LoadTransmutingTiers() ([]*TransmutingTier, error)
}
// PacketBuilder represents the interface for building packets
type PacketBuilder interface {
BuildItemRequestPacket(requestID int32, items []int32, version int32) ([]byte, error)
BuildConfirmationPacket(requestID int32, item Item, version int32) ([]byte, error)
BuildRewardPacket(items []Item, version int32) ([]byte, error)
}
// ItemMaster represents the master item list interface
type ItemMaster interface {
GetItem(itemID int32) Item
CreateItem(itemID int32) Item
}

307
internal/widget/actions.go Normal file
View File

@ -0,0 +1,307 @@
package widget
import (
"eq2emu/internal/spawn"
)
// WidgetAction represents an action that can be performed on a widget
type WidgetAction interface {
Execute(w *Widget, caller *spawn.Spawn) error
}
// OpenDoor opens the widget (door/lift)
func (w *Widget) OpenDoor() {
w.mutex.Lock()
defer w.mutex.Unlock()
// Set heading if specified
if w.openHeading >= 0 {
w.SetHeading(w.openHeading)
}
// Handle position changes
openX := w.openX
openY := w.openY
openZ := w.openZ
if openX != 0 || openY != 0 || openZ != 0 {
x := w.GetX()
y := w.GetY()
z := w.GetZ()
// Use open positions if specified
if openX != 0 {
x = openX
}
if openY != 0 {
y = openY
}
if openZ != 0 {
z = openZ
}
// Add movement to the open position
// Speed of 4 units per second (from C++)
w.AddRunningLocation(x, y, z, 4)
// Calculate movement duration
diff := calculateDistance(w.GetX(), w.GetY(), w.GetZ(), x, y, z)
if diff < 0 {
diff = -diff
}
// Schedule timer for movement completion
// TODO: Zone will need to handle widget timers
// GetZone()->AddWidgetTimer(this, diff / 4)
}
// Update activity status for non-lifts
if w.widgetType != WidgetTypeLift {
w.SetActivityStatus(DefaultActivityOpen)
}
// Set open state
w.isOpen = true
// Schedule auto-close timer if duration is set
if w.openDuration > 0 {
// TODO: Zone will need to handle widget timers
// GetZone()->AddWidgetTimer(this, open_duration)
}
// TODO: Notify zone of spawn changes
// GetZone()->SendSpawnChanges(this)
}
// CloseDoor closes the widget (door/lift)
func (w *Widget) CloseDoor() {
w.mutex.Lock()
defer w.mutex.Unlock()
// Set heading
if w.closedHeading > 0 {
w.SetHeading(w.closedHeading)
} else if w.openHeading >= 0 {
// Fall back to original heading
w.SetHeading(w.GetSpawnOrigHeading())
}
// Update activity status for non-lifts
if w.widgetType != WidgetTypeLift {
w.SetActivityStatus(DefaultActivityClosed)
}
// Handle position changes
if w.closeX != 0 || w.closeY != 0 || w.closeZ != 0 || w.openX != 0 || w.openY != 0 || w.openZ != 0 {
// Default to original spawn position
x := w.GetSpawnOrigX()
y := w.GetSpawnOrigY()
z := w.GetSpawnOrigZ()
// Use close positions if specified
if w.closeX != 0 {
x = w.closeX
}
if w.closeY != 0 {
y = w.closeY
}
if w.closeZ != 0 {
z = w.closeZ
}
// Add movement to the close position
w.AddRunningLocation(x, y, z, 4)
// Calculate movement duration
diff := calculateDistance(w.GetX(), w.GetY(), w.GetZ(), x, y, z)
if diff < 0 {
diff = -diff
}
// Schedule timer for movement completion
// TODO: Zone will need to handle widget timers
// GetZone()->AddWidgetTimer(this, diff / 4)
}
// Set closed state
w.isOpen = false
// TODO: Notify zone of spawn changes
// GetZone()->SendSpawnChanges(this)
}
// ProcessUse processes the use of this widget
func (w *Widget) ProcessUse(caller *spawn.Spawn) {
// Skip if this is a lift that's currently in use
if w.widgetType == WidgetTypeLift {
// TODO: Check if widget has active timer in zone
// if GetZone()->HasWidgetTimer(this) return
}
// TODO: Call spawn script for custom handling
// if GetZone()->CallSpawnScript(this, SPAWN_SCRIPT_USEDOOR, caller, "", is_open)
// return // handled in lua
// Default behavior: toggle open/closed state
wasOpen := w.IsOpen()
if wasOpen {
w.CloseDoor()
} else {
w.OpenDoor()
}
// Play appropriate sound
if w.IsOpen() && w.openSound != "" {
// TODO: Play sound through zone
// GetZone()->PlaySoundFile(0, openSound, widgetX, widgetY, widgetZ)
} else if !w.IsOpen() && w.closeSound != "" {
// TODO: Play sound through zone
// GetZone()->PlaySoundFile(0, closeSound, widgetX, widgetY, widgetZ)
}
}
// HandleTimerUpdate handles widget timer updates
func (w *Widget) HandleTimerUpdate() {
// Lifts don't auto-close
if w.widgetType == WidgetTypeLift {
return
}
// Auto-close open doors
if w.widgetType == WidgetTypeDoor && w.IsOpen() {
w.HandleUse(nil, "")
}
}
// HandleUse handles widget interaction from a client
func (w *Widget) HandleUse(client ClientInterface, command string) {
// Handle override widget type for scripted behavior
overrideWidgetType := w.widgetType
// Client validation
if client != nil {
// TODO: Check quest requirements
// meetsQuestReqs := w.MeetsSpawnAccessRequirements(client.GetPlayer())
// if !meetsQuestReqs && (w.GetQuestsRequiredOverride() & 2) == 0 {
// return
// }
// if meetsQuestReqs && w.GetShowCommandIcon() != 1 {
// return
// }
}
// Handle transporter functionality
if client != nil && w.GetTransporterID() > 0 {
// TODO: Handle transporter destinations
// client.SetTemporaryTransportID(0)
// destinations := GetZone()->GetTransporters(client, w.GetTransporterID())
// if len(destinations) > 0 {
// client.ProcessTeleport(w, destinations, w.GetTransporterID())
// return
// }
}
// Skip house commands for certain operations
skipHouseCommands := isCommand(command, "access") || isCommand(command, "visit")
// Handle door/lift widgets
if !skipHouseCommands && (overrideWidgetType == WidgetTypeDoor || overrideWidgetType == WidgetTypeLift) {
// Resolve action spawn if needed
if w.actionSpawn == nil && w.actionSpawnID > 0 {
// TODO: Get spawn from zone
// spawn := GetZone()->GetSpawnByDatabaseID(w.actionSpawnID)
// if spawn != nil && spawn.IsWidget() {
// w.actionSpawn = spawn.(*Widget)
// }
}
// Resolve linked spawn if needed
if w.linkedSpawn == nil && w.linkedSpawnID > 0 {
// TODO: Get spawn from zone
// spawn := GetZone()->GetSpawnByDatabaseID(w.linkedSpawnID)
// if spawn != nil && spawn.IsWidget() {
// w.linkedSpawn = spawn.(*Widget)
// }
}
// Process linked spawns
widget := w
if w.linkedSpawn != nil {
widget = w.linkedSpawn
// Fire the first door
var caller *spawn.Spawn
if client != nil {
caller = client.GetPlayer()
}
w.ProcessUse(caller)
} else if w.actionSpawn != nil {
widget = w.actionSpawn
// Resolve action spawn's linked spawn if needed
if widget.linkedSpawn == nil && widget.linkedSpawnID > 0 {
// TODO: Get spawn from zone
// spawn := GetZone()->GetSpawnByDatabaseID(widget.linkedSpawnID)
// if spawn != nil && spawn.IsWidget() {
// widget.linkedSpawn = spawn.(*Widget)
// }
}
// Process linked spawn first
if widget.linkedSpawn != nil {
var caller *spawn.Spawn
if client != nil {
caller = client.GetPlayer()
}
widget.linkedSpawn.ProcessUse(caller)
}
}
// Process the main widget
var caller *spawn.Spawn
if client != nil {
caller = client.GetPlayer()
}
widget.ProcessUse(caller)
} else if client != nil && isCommand(command, "access") && w.houseID > 0 {
// Handle house access
// TODO: Implement house access functionality
// This involves PlayerHouse and HouseZone systems
} else if client != nil && isCommand(command, "visit") && w.houseID > 0 {
// Handle house visit
// TODO: Implement house visit functionality
} else if client != nil && command != "" {
// Handle other entity commands
// TODO: Process entity commands
// entityCommand := w.FindEntityCommand(command)
// if entityCommand != nil {
// GetZone()->ProcessEntityCommand(entityCommand, client.GetPlayer(), client.GetPlayer().GetTarget())
// }
}
}
// SetLinkedSpawn sets the linked spawn widget
func (w *Widget) SetLinkedSpawn(linked *Widget) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.linkedSpawn = linked
}
// GetLinkedSpawn returns the linked spawn widget
func (w *Widget) GetLinkedSpawn() *Widget {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.linkedSpawn
}
// SetActionSpawn sets the action spawn widget
func (w *Widget) SetActionSpawn(action *Widget) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.actionSpawn = action
}
// GetActionSpawn returns the action spawn widget
func (w *Widget) GetActionSpawn() *Widget {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.actionSpawn
}

View File

@ -0,0 +1,38 @@
package widget
// Widget type constants
const (
WidgetTypeGeneric = 0
WidgetTypeDoor = 1
WidgetTypeLift = 2
)
// Widget state constants
const (
WidgetStateClosed = 0
WidgetStateOpen = 1
)
// Default widget values
const (
DefaultOpenHeading = -1
DefaultClosedHeading = -1
DefaultOpenDuration = 0
DefaultActivityOpen = 0
DefaultActivityClosed = 64
)
// Widget type names for display
var WidgetTypeNames = map[int8]string{
WidgetTypeGeneric: "Generic",
WidgetTypeDoor: "Door",
WidgetTypeLift: "Lift",
}
// GetWidgetTypeNameByTypeID returns the display name for a widget type
func GetWidgetTypeNameByTypeID(typeID int8) string {
if name, exists := WidgetTypeNames[typeID]; exists {
return name
}
return "Generic"
}

View File

@ -0,0 +1,109 @@
package widget
import (
"eq2emu/internal/spawn"
)
// ClientInterface represents the minimal client interface needed by widgets
type ClientInterface interface {
GetPlayer() *spawn.Spawn
SetTemporaryTransportID(id int32)
ProcessTeleport(widget *Widget, destinations []interface{}, transporterID int32)
GetVersion() int32
GetCurrentZone() ZoneInterface
}
// ZoneInterface represents the minimal zone interface needed by widgets
type ZoneInterface interface {
HasWidgetTimer(widget *Widget) bool
AddWidgetTimer(widget *Widget, duration float32)
SendSpawnChanges(s *spawn.Spawn)
PlaySoundFile(unknown int32, soundFile string, x, y, z float32)
CallSpawnScript(s *spawn.Spawn, scriptType string, caller *spawn.Spawn, extra string, state bool) bool
GetSpawnByDatabaseID(id int32) *spawn.Spawn
GetTransporters(client ClientInterface, transporterID int32) []interface{}
ProcessEntityCommand(command interface{}, player *spawn.Spawn, target *spawn.Spawn)
GetInstanceID() int32
GetInstanceType() int32
SendHouseItems(client ClientInterface)
}
// WidgetSpawn provides integration between Widget and Spawn systems
type WidgetSpawn struct {
*spawn.Spawn
*Widget
}
// NewWidgetSpawn creates a new widget spawn wrapper
func NewWidgetSpawn() *WidgetSpawn {
widget := NewWidget()
return &WidgetSpawn{
Spawn: widget.Spawn,
Widget: widget,
}
}
// IsWidget returns true for widget spawns
func (ws *WidgetSpawn) IsWidget() bool {
return true
}
// Copy creates a copy of the widget spawn
func (ws *WidgetSpawn) Copy() *WidgetSpawn {
newWidget := ws.Widget.Copy()
return &WidgetSpawn{
Spawn: newWidget.Spawn,
Widget: newWidget,
}
}
// WidgetManager interface for managing widgets in a zone
type WidgetManager interface {
AddWidget(widget *Widget)
RemoveWidget(widgetID int32)
GetWidget(widgetID int32) *Widget
GetWidgetByDatabaseID(databaseID int32) *Widget
GetAllWidgets() []*Widget
ProcessWidgetTimers()
}
// WidgetTimer represents a timer for widget actions
type WidgetTimer struct {
Widget *Widget
Duration float32
Callback func(*Widget)
}
// WidgetState represents the current state of a widget
type WidgetState struct {
IsOpen bool
Position spawn.Position
Heading float32
ActivityStatus int32
}
// GetState returns the current state of the widget
func (w *Widget) GetState() WidgetState {
w.mutex.RLock()
defer w.mutex.RUnlock()
return WidgetState{
IsOpen: w.isOpen,
Position: spawn.Position{X: w.GetX(), Y: w.GetY(), Z: w.GetZ()},
Heading: w.GetHeading(),
ActivityStatus: w.GetActivityStatus(),
}
}
// RestoreState restores the widget to a previous state
func (w *Widget) RestoreState(state WidgetState) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.isOpen = state.IsOpen
w.SetX(state.Position.X)
w.SetY(state.Position.Y)
w.SetZ(state.Position.Z)
w.SetHeading(state.Heading)
w.SetActivityStatus(state.ActivityStatus)
}

313
internal/widget/manager.go Normal file
View File

@ -0,0 +1,313 @@
package widget
import (
"sync"
"time"
)
// Manager manages widgets within a zone
type Manager struct {
widgets map[int32]*Widget // Widget ID -> Widget
widgetsByDBID map[int32]*Widget // Database ID -> Widget
widgetTimers map[*Widget]*time.Timer // Active timers for widgets
timerCallbacks map[*Widget]func() // Timer callbacks
mutex sync.RWMutex
timerMutex sync.Mutex
}
// NewManager creates a new widget manager
func NewManager() *Manager {
return &Manager{
widgets: make(map[int32]*Widget),
widgetsByDBID: make(map[int32]*Widget),
widgetTimers: make(map[*Widget]*time.Timer),
timerCallbacks: make(map[*Widget]func()),
}
}
// AddWidget adds a widget to the manager
func (m *Manager) AddWidget(widget *Widget) {
m.mutex.Lock()
defer m.mutex.Unlock()
if widget.GetWidgetID() > 0 {
m.widgets[widget.GetWidgetID()] = widget
}
if widget.GetDatabaseID() > 0 {
m.widgetsByDBID[widget.GetDatabaseID()] = widget
}
}
// RemoveWidget removes a widget from the manager
func (m *Manager) RemoveWidget(widgetID int32) {
m.mutex.Lock()
defer m.mutex.Unlock()
if widget, exists := m.widgets[widgetID]; exists {
// Cancel any active timers
m.cancelWidgetTimer(widget)
// Remove from maps
delete(m.widgets, widgetID)
if widget.GetDatabaseID() > 0 {
delete(m.widgetsByDBID, widget.GetDatabaseID())
}
}
}
// GetWidget gets a widget by ID
func (m *Manager) GetWidget(widgetID int32) *Widget {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.widgets[widgetID]
}
// GetWidgetByDatabaseID gets a widget by database ID
func (m *Manager) GetWidgetByDatabaseID(databaseID int32) *Widget {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.widgetsByDBID[databaseID]
}
// GetAllWidgets returns all widgets
func (m *Manager) GetAllWidgets() []*Widget {
m.mutex.RLock()
defer m.mutex.RUnlock()
widgets := make([]*Widget, 0, len(m.widgets))
for _, widget := range m.widgets {
widgets = append(widgets, widget)
}
return widgets
}
// GetWidgetsByType returns all widgets of a specific type
func (m *Manager) GetWidgetsByType(widgetType int8) []*Widget {
m.mutex.RLock()
defer m.mutex.RUnlock()
widgets := make([]*Widget, 0)
for _, widget := range m.widgets {
if widget.GetWidgetType() == widgetType {
widgets = append(widgets, widget)
}
}
return widgets
}
// AddWidgetTimer adds a timer for a widget
func (m *Manager) AddWidgetTimer(widget *Widget, seconds float32, callback func()) {
m.timerMutex.Lock()
defer m.timerMutex.Unlock()
// Cancel existing timer if any
m.cancelWidgetTimerLocked(widget)
// Create new timer
duration := time.Duration(seconds * float32(time.Second))
timer := time.AfterFunc(duration, func() {
m.handleWidgetTimer(widget)
})
m.widgetTimers[widget] = timer
if callback != nil {
m.timerCallbacks[widget] = callback
} else {
// Default callback for auto-close
m.timerCallbacks[widget] = func() {
widget.HandleTimerUpdate()
}
}
}
// HasWidgetTimer checks if a widget has an active timer
func (m *Manager) HasWidgetTimer(widget *Widget) bool {
m.timerMutex.Lock()
defer m.timerMutex.Unlock()
_, exists := m.widgetTimers[widget]
return exists
}
// CancelWidgetTimer cancels a widget's timer
func (m *Manager) CancelWidgetTimer(widget *Widget) {
m.timerMutex.Lock()
defer m.timerMutex.Unlock()
m.cancelWidgetTimerLocked(widget)
}
// cancelWidgetTimerLocked cancels a timer (must hold timerMutex)
func (m *Manager) cancelWidgetTimerLocked(widget *Widget) {
if timer, exists := m.widgetTimers[widget]; exists {
timer.Stop()
delete(m.widgetTimers, widget)
delete(m.timerCallbacks, widget)
}
}
// cancelWidgetTimer cancels a timer (must hold timerMutex)
func (m *Manager) cancelWidgetTimer(widget *Widget) {
m.timerMutex.Lock()
defer m.timerMutex.Unlock()
m.cancelWidgetTimerLocked(widget)
}
// handleWidgetTimer handles a widget timer expiration
func (m *Manager) handleWidgetTimer(widget *Widget) {
m.timerMutex.Lock()
callback, exists := m.timerCallbacks[widget]
if exists {
delete(m.widgetTimers, widget)
delete(m.timerCallbacks, widget)
}
m.timerMutex.Unlock()
// Execute callback outside of lock
if callback != nil {
callback()
}
}
// ProcessWidgetTimers processes all widget timers (called periodically)
func (m *Manager) ProcessWidgetTimers() {
// Timers are handled automatically by time.AfterFunc
// This method is here for compatibility but doesn't need to do anything
}
// GetDoorWidgets returns all door widgets
func (m *Manager) GetDoorWidgets() []*Widget {
return m.GetWidgetsByType(WidgetTypeDoor)
}
// GetLiftWidgets returns all lift widgets
func (m *Manager) GetLiftWidgets() []*Widget {
return m.GetWidgetsByType(WidgetTypeLift)
}
// GetOpenWidgets returns all open widgets
func (m *Manager) GetOpenWidgets() []*Widget {
m.mutex.RLock()
defer m.mutex.RUnlock()
widgets := make([]*Widget, 0)
for _, widget := range m.widgets {
if widget.IsOpen() {
widgets = append(widgets, widget)
}
}
return widgets
}
// GetWidgetsByHouseID returns all widgets for a house
func (m *Manager) GetWidgetsByHouseID(houseID int32) []*Widget {
m.mutex.RLock()
defer m.mutex.RUnlock()
widgets := make([]*Widget, 0)
for _, widget := range m.widgets {
if widget.GetHouseID() == houseID {
widgets = append(widgets, widget)
}
}
return widgets
}
// GetLinkedWidgets returns all widgets linked to the given widget
func (m *Manager) GetLinkedWidgets(widget *Widget) []*Widget {
m.mutex.RLock()
defer m.mutex.RUnlock()
linked := make([]*Widget, 0)
// Add linked spawn
if widget.GetLinkedSpawnID() > 0 {
if linkedWidget := m.widgetsByDBID[widget.GetLinkedSpawnID()]; linkedWidget != nil {
linked = append(linked, linkedWidget)
}
}
// Add action spawn
if widget.GetActionSpawnID() > 0 {
if actionWidget := m.widgetsByDBID[widget.GetActionSpawnID()]; actionWidget != nil {
linked = append(linked, actionWidget)
}
}
// Find widgets that link to this one
for _, w := range m.widgets {
if w.GetLinkedSpawnID() == widget.GetDatabaseID() ||
w.GetActionSpawnID() == widget.GetDatabaseID() {
linked = append(linked, w)
}
}
return linked
}
// ResolveLinkedSpawns resolves all linked spawn references
func (m *Manager) ResolveLinkedSpawns() {
m.mutex.RLock()
defer m.mutex.RUnlock()
for _, widget := range m.widgets {
// Resolve linked spawn
if widget.GetLinkedSpawnID() > 0 && widget.GetLinkedSpawn() == nil {
if linked := m.widgetsByDBID[widget.GetLinkedSpawnID()]; linked != nil {
widget.SetLinkedSpawn(linked)
}
}
// Resolve action spawn
if widget.GetActionSpawnID() > 0 && widget.GetActionSpawn() == nil {
if action := m.widgetsByDBID[widget.GetActionSpawnID()]; action != nil {
widget.SetActionSpawn(action)
}
}
}
}
// Clear removes all widgets and cancels all timers
func (m *Manager) Clear() {
m.mutex.Lock()
defer m.mutex.Unlock()
// Cancel all timers
m.timerMutex.Lock()
for widget, timer := range m.widgetTimers {
timer.Stop()
delete(m.widgetTimers, widget)
delete(m.timerCallbacks, widget)
}
m.timerMutex.Unlock()
// Clear maps
m.widgets = make(map[int32]*Widget)
m.widgetsByDBID = make(map[int32]*Widget)
}
// GetStatistics returns widget statistics
func (m *Manager) GetStatistics() map[string]interface{} {
m.mutex.RLock()
defer m.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_widgets"] = len(m.widgets)
stats["door_count"] = len(m.GetDoorWidgets())
stats["lift_count"] = len(m.GetLiftWidgets())
stats["open_count"] = len(m.GetOpenWidgets())
m.timerMutex.Lock()
stats["active_timers"] = len(m.widgetTimers)
m.timerMutex.Unlock()
return stats
}

498
internal/widget/widget.go Normal file
View File

@ -0,0 +1,498 @@
package widget
import (
"math"
"strings"
"sync"
"eq2emu/internal/spawn"
)
// Widget represents an interactive spawn object like doors and lifts
// Extends the base Spawn with widget-specific functionality
type Widget struct {
*spawn.Spawn // Embedded spawn for basic functionality
// Widget identification
widgetID int32 // Unique widget identifier
widgetType int8 // Type of widget (door, lift, etc.)
// Widget positioning
widgetX float32 // Widget-specific X coordinate
widgetY float32 // Widget-specific Y coordinate
widgetZ float32 // Widget-specific Z coordinate
includeLocation bool // Whether to include location in updates
includeHeading bool // Whether to include heading in updates
// Door/movement states
isOpen bool // Current open/closed state
openHeading float32 // Heading when open
closedHeading float32 // Heading when closed
openX float32 // X position when open
openY float32 // Y position when open
openZ float32 // Z position when open
closeX float32 // X position when closed
closeY float32 // Y position when closed (from close_y in C++)
closeZ float32 // Z position when closed
// Linked widgets
actionSpawn *Widget // Spawn triggered by this widget
actionSpawnID int32 // ID of action spawn
linkedSpawn *Widget // Linked widget (opens/closes together)
linkedSpawnID int32 // ID of linked spawn
// Sounds and timing
openSound string // Sound played when opening
closeSound string // Sound played when closing
openDuration int16 // How long widget stays open (seconds)
// House integration
houseID int32 // Associated house ID
// Multi-floor lift support
multiFloorLift bool // Whether this is a multi-floor lift
// Thread safety
mutex sync.RWMutex
}
// NewWidget creates a new widget instance
func NewWidget() *Widget {
w := &Widget{
Spawn: spawn.NewSpawn(),
widgetID: 0,
widgetType: WidgetTypeGeneric,
widgetX: 0,
widgetY: 0,
widgetZ: 0,
includeLocation: true,
includeHeading: true,
isOpen: false,
openHeading: DefaultOpenHeading,
closedHeading: DefaultClosedHeading,
openX: 0,
openY: 0,
openZ: 0,
closeX: 0,
closeY: 0,
closeZ: 0,
openDuration: DefaultOpenDuration,
multiFloorLift: false,
}
// Set spawn-specific defaults for widgets
w.SetSpawnType(2) // Widget spawn type
w.SetActivityStatus(DefaultActivityClosed)
w.SetPosState(1)
return w
}
// IsWidget returns true to identify this as a widget
func (w *Widget) IsWidget() bool {
return true
}
// Widget ID methods
// GetWidgetID returns the widget ID
func (w *Widget) GetWidgetID() int32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.widgetID
}
// SetWidgetID sets the widget ID
func (w *Widget) SetWidgetID(id int32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.widgetID = id
}
// Widget position methods
// GetWidgetX returns the widget X coordinate
func (w *Widget) GetWidgetX() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.widgetX
}
// SetWidgetX sets the widget X coordinate
func (w *Widget) SetWidgetX(x float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.widgetX = x
}
// GetWidgetY returns the widget Y coordinate
func (w *Widget) GetWidgetY() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.widgetY
}
// SetWidgetY sets the widget Y coordinate
func (w *Widget) SetWidgetY(y float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.widgetY = y
}
// GetWidgetZ returns the widget Z coordinate
func (w *Widget) GetWidgetZ() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.widgetZ
}
// SetWidgetZ sets the widget Z coordinate
func (w *Widget) SetWidgetZ(z float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.widgetZ = z
}
// Location/heading inclusion methods
// GetIncludeLocation returns whether to include location in updates
func (w *Widget) GetIncludeLocation() bool {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.includeLocation
}
// SetIncludeLocation sets whether to include location in updates
func (w *Widget) SetIncludeLocation(include bool) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.includeLocation = include
}
// GetIncludeHeading returns whether to include heading in updates
func (w *Widget) GetIncludeHeading() bool {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.includeHeading
}
// SetIncludeHeading sets whether to include heading in updates
func (w *Widget) SetIncludeHeading(include bool) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.includeHeading = include
}
// Widget type methods
// GetWidgetType returns the widget type
func (w *Widget) GetWidgetType() int8 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.widgetType
}
// SetWidgetType sets the widget type
func (w *Widget) SetWidgetType(widgetType int8) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.widgetType = widgetType
}
// SetWidgetIcon sets the widget icon (appearance)
func (w *Widget) SetWidgetIcon(icon int8) {
w.SetIcon(icon)
}
// Open/close state methods
// IsOpen returns whether the widget is open
func (w *Widget) IsOpen() bool {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.isOpen
}
// setOpenState sets the open state (internal use)
func (w *Widget) setOpenState(open bool) {
w.isOpen = open
}
// Heading methods
// GetOpenHeading returns the heading when open
func (w *Widget) GetOpenHeading() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.openHeading
}
// SetOpenHeading sets the heading when open
func (w *Widget) SetOpenHeading(heading float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.openHeading = heading
}
// GetClosedHeading returns the heading when closed
func (w *Widget) GetClosedHeading() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.closedHeading
}
// SetClosedHeading sets the heading when closed
func (w *Widget) SetClosedHeading(heading float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.closedHeading = heading
}
// Position methods for open/close states
// GetOpenX returns the X position when open
func (w *Widget) GetOpenX() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.openX
}
// SetOpenX sets the X position when open
func (w *Widget) SetOpenX(x float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.openX = x
}
// GetOpenY returns the Y position when open
func (w *Widget) GetOpenY() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.openY
}
// SetOpenY sets the Y position when open
func (w *Widget) SetOpenY(y float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.openY = y
}
// GetOpenZ returns the Z position when open
func (w *Widget) GetOpenZ() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.openZ
}
// SetOpenZ sets the Z position when open
func (w *Widget) SetOpenZ(z float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.openZ = z
}
// GetCloseX returns the X position when closed
func (w *Widget) GetCloseX() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.closeX
}
// SetCloseX sets the X position when closed
func (w *Widget) SetCloseX(x float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.closeX = x
}
// GetCloseY returns the Y position when closed
func (w *Widget) GetCloseY() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.closeY
}
// SetCloseY sets the Y position when closed
func (w *Widget) SetCloseY(y float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.closeY = y
}
// GetCloseZ returns the Z position when closed
func (w *Widget) GetCloseZ() float32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.closeZ
}
// SetCloseZ sets the Z position when closed
func (w *Widget) SetCloseZ(z float32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.closeZ = z
}
// Linked spawn methods
// GetActionSpawnID returns the action spawn ID
func (w *Widget) GetActionSpawnID() int32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.actionSpawnID
}
// SetActionSpawnID sets the action spawn ID
func (w *Widget) SetActionSpawnID(id int32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.actionSpawnID = id
}
// GetLinkedSpawnID returns the linked spawn ID
func (w *Widget) GetLinkedSpawnID() int32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.linkedSpawnID
}
// SetLinkedSpawnID sets the linked spawn ID
func (w *Widget) SetLinkedSpawnID(id int32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.linkedSpawnID = id
}
// Sound methods
// GetOpenSound returns the sound played when opening
func (w *Widget) GetOpenSound() string {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.openSound
}
// SetOpenSound sets the sound played when opening
func (w *Widget) SetOpenSound(sound string) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.openSound = sound
}
// GetCloseSound returns the sound played when closing
func (w *Widget) GetCloseSound() string {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.closeSound
}
// SetCloseSound sets the sound played when closing
func (w *Widget) SetCloseSound(sound string) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.closeSound = sound
}
// Duration methods
// GetOpenDuration returns how long the widget stays open
func (w *Widget) GetOpenDuration() int16 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.openDuration
}
// SetOpenDuration sets how long the widget stays open
func (w *Widget) SetOpenDuration(duration int16) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.openDuration = duration
}
// House methods
// GetHouseID returns the associated house ID
func (w *Widget) GetHouseID() int32 {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.houseID
}
// SetHouseID sets the associated house ID
func (w *Widget) SetHouseID(id int32) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.houseID = id
}
// Multi-floor lift methods
// GetMultiFloorLift returns whether this is a multi-floor lift
func (w *Widget) GetMultiFloorLift() bool {
w.mutex.RLock()
defer w.mutex.RUnlock()
return w.multiFloorLift
}
// SetMultiFloorLift sets whether this is a multi-floor lift
func (w *Widget) SetMultiFloorLift(multiFloor bool) {
w.mutex.Lock()
defer w.mutex.Unlock()
w.multiFloorLift = multiFloor
}
// Copy creates a copy of this widget
func (w *Widget) Copy() *Widget {
w.mutex.RLock()
defer w.mutex.RUnlock()
newWidget := NewWidget()
// Copy spawn data
w.CopySpawnData(newWidget.Spawn)
// Handle open state for appearance
if w.openY > 0 {
newWidget.SetPosState(0)
}
// Copy widget-specific data
newWidget.widgetID = w.widgetID
newWidget.widgetType = w.widgetType
newWidget.widgetX = w.widgetX
newWidget.widgetY = w.widgetY
newWidget.widgetZ = w.widgetZ
newWidget.includeLocation = w.includeLocation
newWidget.includeHeading = w.includeHeading
newWidget.openHeading = w.openHeading
newWidget.closedHeading = w.closedHeading
newWidget.openX = w.openX
newWidget.openY = w.openY
newWidget.openZ = w.openZ
newWidget.closeX = w.closeX
newWidget.closeY = w.closeY
newWidget.closeZ = w.closeZ
newWidget.openSound = w.openSound
newWidget.closeSound = w.closeSound
newWidget.openDuration = w.openDuration
newWidget.actionSpawnID = w.actionSpawnID
newWidget.linkedSpawnID = w.linkedSpawnID
newWidget.houseID = w.houseID
newWidget.multiFloorLift = w.multiFloorLift
return newWidget
}
// calculateDistance calculates 3D distance between two points
func calculateDistance(x1, y1, z1, x2, y2, z2 float32) float32 {
dx := x2 - x1
dy := y2 - y1
dz := z2 - z1
return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz)))
}
// Helper method to check if a string command matches (case-insensitive)
func isCommand(command, expected string) bool {
return strings.EqualFold(strings.TrimSpace(command), expected)
}