implement more internals

This commit is contained in:
Sky Johnson 2025-07-31 08:40:13 -05:00
parent 16d9636c06
commit 47e6102af1
25 changed files with 6990 additions and 585 deletions

View File

@ -1,131 +0,0 @@
#include "../Worlddatabase.hpp"
#include "../World.h"
extern World world;
void WorldDatabase::LoadHouseZones() {
Query query;
MYSQL_ROW row;
MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT * FROM `houses`");
if (result && mysql_num_rows(result) > 0) {
while ((row = mysql_fetch_row(result))) {
world.AddHouseZone(atoul(row[0]), row[1], atoi64(row[2]), atoul(row[3]), atoi64(row[4]), atoul(row[5]), atoi(row[6]), atoi(row[7]), atoi(row[8]), atoul(row[9]), atoul(row[10]), atof(row[11]), atof(row[12]), atof(row[13]), atof(row[14]));
}
}
}
int64 WorldDatabase::AddPlayerHouse(int32 char_id, int32 house_id, int32 instance_id, int32 upkeep_due) {
Query query;
string insert = string("INSERT INTO character_houses (char_id, house_id, instance_id, upkeep_due) VALUES (%u, %u, %u, %u) ");
query.RunQuery2(Q_INSERT, insert.c_str(), char_id, house_id, instance_id, upkeep_due);
int64 unique_id = query.GetLastInsertedID();
return unique_id;
}
void WorldDatabase::SetHouseUpkeepDue(int32 char_id, int32 house_id, int32 instance_id, int32 upkeep_due) {
Query query;
string update = string("UPDATE character_houses set upkeep_due=%u where char_id = %u and house_id = %u and instance_id = %u");
query.RunQuery2(Q_UPDATE, update.c_str(), upkeep_due, char_id, house_id, instance_id);
}
void WorldDatabase::UpdateHouseEscrow(int32 house_id, int32 instance_id, int64 amount_coins, int32 amount_status) {
Query query;
string update = string("UPDATE character_houses set escrow_coins = %llu, escrow_status = %u where house_id = %u and instance_id = %u");
query.RunQuery2(Q_UPDATE, update.c_str(), amount_coins, amount_status, house_id, instance_id);
}
void WorldDatabase::RemovePlayerHouse(int32 char_id, int32 house_id) {
}
void WorldDatabase::LoadPlayerHouses() {
Query query;
MYSQL_ROW row;
MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT h.id, h.char_id, h.house_id, h.instance_id, h.upkeep_due, h.escrow_coins, h.escrow_status, c.name FROM character_houses h, characters c WHERE h.char_id = c.id");
if (result && mysql_num_rows(result) > 0) {
while ((row = mysql_fetch_row(result))) {
world.AddPlayerHouse(atoul(row[1]), atoul(row[2]), atoi64(row[0]), atoul(row[3]), atoul(row[4]), atoi64(row[5]), atoul(row[6]), row[7]);
}
}
}
void WorldDatabase::LoadDeposits(PlayerHouse* ph)
{
if (!ph)
return;
ph->deposits.clear();
ph->depositsMap.clear();
Query query;
MYSQL_ROW row;
MYSQL_RES* result = query.RunQuery2(Q_SELECT, "select timestamp, amount, last_amount, status, last_status, name from character_house_deposits where house_id = %u and instance_id = %u order by timestamp asc limit 255", ph->house_id, ph->instance_id);
if (result && mysql_num_rows(result) > 0) {
while ((row = mysql_fetch_row(result))) {
Deposit d;
d.timestamp = atoul(row[0]);
int64 outVal = strtoull(row[1], NULL, 0);
d.amount = outVal;
outVal = strtoull(row[2], NULL, 0);
d.last_amount = outVal;
d.status = atoul(row[3]);
d.last_status = atoul(row[4]);
d.name = string(row[5]);
ph->deposits.push_back(d);
ph->depositsMap.insert(make_pair(d.name, d));
}
}
}
void WorldDatabase::LoadHistory(PlayerHouse* ph)
{
if (!ph)
return;
ph->history.clear();
Query query;
MYSQL_ROW row;
MYSQL_RES* result = query.RunQuery2(Q_SELECT, "select timestamp, amount, status, reason, name, pos_flag from character_house_history where house_id = %u and instance_id = %u order by timestamp asc limit 255", ph->house_id, ph->instance_id);
if (result && mysql_num_rows(result) > 0) {
while ((row = mysql_fetch_row(result))) {
HouseHistory h;
h.timestamp = atoul(row[0]);
int64 outVal = strtoull(row[1], NULL, 0);
h.amount = outVal;
h.status = atoul(row[2]);
h.reason = string(row[3]);
h.name = string(row[4]);
h.pos_flag = atoul(row[5]);
ph->history.push_back(h);
}
}
}
void WorldDatabase::AddHistory(PlayerHouse* house, char* name, char* reason, int32 timestamp, int64 amount, int32 status, int8 pos_flag)
{
if (!house)
return;
HouseHistory h(Timer::GetUnixTimeStamp(), amount, string(name), string(reason), status, pos_flag);
house->history.push_back(h);
Query query;
string insert = string("INSERT INTO character_house_history (timestamp, house_id, instance_id, name, amount, status, reason, pos_flag) VALUES (%u, %u, %u, '%s', %llu, %u, '%s', %u) ");
query.RunQuery2(Q_INSERT, insert.c_str(), timestamp, house->house_id, house->instance_id, name, amount, status, reason, pos_flag);
}

View File

@ -1,454 +0,0 @@
#include "../ClientPacketFunctions.h"
#include "../World.h"
#include "../client.h"
#include "../Worlddatabase.hpp"
#include "../Rules/Rules.h"
extern ConfigReader configReader;
extern World world;
extern WorldDatabase database;
extern RuleManager rule_manager;
void ClientPacketFunctions::SendHousePurchase(Client* client, HouseZone* hz, int32 spawnID) {
PacketStruct* packet = configReader.getStruct("WS_PlayerHousePurchase", client->GetVersion());
if (packet) {
int8 disable_alignment_req = rule_manager.GetZoneRule(client->GetCurrentZoneID(), R_Player, DisableHouseAlignmentRequirement)->GetInt8();
packet->setDataByName("house_name", hz->name.c_str());
packet->setDataByName("house_id", hz->id);
packet->setDataByName("spawn_id", spawnID);
packet->setDataByName("purchase_coins", hz->cost_coin);
packet->setDataByName("purchase_status", hz->cost_status);
packet->setDataByName("upkeep_coins", hz->upkeep_coin);
packet->setDataByName("upkeep_status", hz->upkeep_status);
packet->setDataByName("vendor_vault_slots", hz->vault_slots);
string req;
if (hz->alignment > 0 && !disable_alignment_req) {
req = "You must be of ";
if (hz->alignment == 1)
req.append("Good");
else
req.append("Evil");
req.append(" alignment");
}
if (hz->guild_level > 0) {
if (req.length() > 0) {
req.append(", and a guild level of ");
char temp[5];
sprintf(temp, "%i", hz->guild_level);
req.append(temp);
//req.append(std::to_string(static_cast<long long>(hz->guild_level)));
}
else {
req.append("Requires a guild of level ");
char temp[5];
sprintf(temp, "%i", hz->guild_level);
req.append(temp);
//req.append(std::to_string(static_cast<long long>(hz->guild_level)))
req.append(" or above");
}
}
if (req.length() > 0) {
req.append(" in order to purchase a home within the ");
req.append(hz->name);
req.append(".");
}
packet->setDataByName("additional_reqs", req.c_str());
bool enable_buy = true;
if (hz->alignment > 0 && client->GetPlayer()->GetAlignment() != hz->alignment && !disable_alignment_req)
enable_buy = false;
if (hz->guild_level > 0 && (!client->GetPlayer()->GetGuild() || (client->GetPlayer()->GetGuild() && client->GetPlayer()->GetGuild()->GetLevel() < hz->guild_level)))
enable_buy = false;
packet->setDataByName("enable_buy", enable_buy ? 1 : 0);
//packet->PrintPacket();
client->QueuePacket(packet->serialize());
}
safe_delete(packet);
}
void ClientPacketFunctions::SendHousingList(Client* client) {
if(client->GetVersion() <= 561) {
return; // not supported
}
std::vector<PlayerHouse*> houses = world.GetAllPlayerHouses(client->GetCharacterID());
// this packet must be sent first otherwise it blocks out the enter house option after paying upkeep
PacketStruct* packet = configReader.getStruct("WS_CharacterHousingList", client->GetVersion());
if(!packet) {
return;
}
packet->setArrayLengthByName("num_houses", houses.size());
for (int i = 0; i < houses.size(); i++)
{
PlayerHouse* ph = (PlayerHouse*)houses[i];
HouseZone* hz = world.GetHouseZone(ph->house_id);
string name;
name = ph->player_name;
name.append("'s ");
name.append(hz->name);
packet->setArrayDataByName("house_id", ph->unique_id, i);
string zone_name = database.GetZoneName(hz->zone_id);
if(zone_name.length() > 0)
packet->setArrayDataByName("zone", zone_name.c_str(), i);
packet->setArrayDataByName("house_city", hz->name.c_str(), i);
packet->setArrayDataByName("house_address", "", i); // need this pulled from live
packet->setArrayDataByName("house_description", name.c_str(), i);
packet->setArrayDataByName("index", i, i); // they send 2, 4, 6, 8 as the index ID's on the client..
// this seems to be some kind of timestamp, if we keep updating then in conjunction with upkeep_due
// in SendBaseHouseWindow/WS_PlayerHouseBaseScreen being a >0 number we can access 'enter house'
int32 upkeep_due = 0;
if (((sint64)ph->upkeep_due - (sint64)Timer::GetUnixTimeStamp()) > 0)
upkeep_due = ph->upkeep_due - Timer::GetUnixTimeStamp();
if ( client->GetVersion() >= 63119 )
packet->setArrayDataByName("unknown2a", 0xFFFFFFFF, i);
else
packet->setArrayDataByName("unknown2", 0xFFFFFFFF, i);
}
client->QueuePacket(packet->serialize());
safe_delete(packet);
}
void ClientPacketFunctions::SendBaseHouseWindow(Client* client, HouseZone* hz, PlayerHouse* ph, int32 spawnID) {
// if we don't send this then the enter house option won't be available if upkeep is paid
if (!hz || !ph)
{
client->SimpleMessage(CHANNEL_COLOR_RED, "HouseZone or PlayerHouse missing and cannot send SendBaseHouseWindow");
return;
}
string name;
name = ph->player_name;
name.append("'s ");
name.append(hz->name);
if (spawnID)
SendHousingList(client);
int32 upkeep_due = 0;
if (((sint64)ph->upkeep_due - (sint64)Timer::GetUnixTimeStamp()) > 0)
upkeep_due = ph->upkeep_due - Timer::GetUnixTimeStamp();
// need this to enable the "enter house" button
PacketStruct* packet = nullptr;
if(client->GetVersion() > 561 && client->GetCurrentZone()->GetInstanceType() != PERSONAL_HOUSE_INSTANCE
&& client->GetCurrentZone()->GetInstanceType() != GUILD_HOUSE_INSTANCE) {
packet = configReader.getStruct("WS_UpdateHouseAccessDataMsg", client->GetVersion());
if(!packet) {
return; // we need this for these clients or enter house will not work properly
}
if (packet) {
packet->setDataByName("house_id", 0xFFFFFFFFFFFFFFFF);
packet->setDataByName("success", (upkeep_due > 0) ? 0xFFFFFFFF : 0);
packet->setDataByName("unknown2", 0xFFFFFFFF);
packet->setDataByName("unknown3", 0xFFFFFFFF);
}
client->QueuePacket(packet->serialize());
}
safe_delete(packet);
packet = configReader.getStruct("WS_PlayerHouseBaseScreen", client->GetVersion());
if (packet) {
packet->setDataByName("house_id", ph->unique_id);
packet->setDataByName("spawn_id", spawnID);
packet->setDataByName("character_id", client->GetPlayer()->GetCharacterID());
packet->setDataByName("house_name", name.c_str());
packet->setDataByName("zone_name", hz->name.c_str());
packet->setDataByName("upkeep_cost_coins", hz->upkeep_coin);
packet->setDataByName("upkeep_cost_status", hz->upkeep_status);
packet->setDataByName("upkeep_due", upkeep_due);
packet->setDataByName("escrow_balance_coins", ph->escrow_coins);
packet->setDataByName("escrow_balance_status", ph->escrow_status);
// temp - set priv level to owner for now
packet->setDataByName("privlage_level", 4);
// temp - set house type to personal house for now
packet->setDataByName("house_type", 0);
if(client->GetCurrentZone()->GetInstanceType() == PERSONAL_HOUSE_INSTANCE
|| client->GetCurrentZone()->GetInstanceType() == GUILD_HOUSE_INSTANCE) {
packet->setDataByName("inside_house", 1);
packet->setDataByName("public_access_level", 1);
}
packet->setDataByName("num_access", 0);
packet->setDataByName("num_history", 0);
// allows deposits/history to be seen -- at this point seems plausible supposed to be 'inside_house'..?
packet->setDataByName("unknown3", (ph->deposits.size() || ph->history.size()) ? 1 : 0);
packet->setArrayLengthByName("num_deposit", ph->deposits.size());
list<Deposit>::iterator itr;
int d = 0;
for (itr = ph->deposits.begin(); itr != ph->deposits.end(); itr++)
{
packet->setArrayDataByName("deposit_name", itr->name.c_str(), d);
packet->setArrayDataByName("deposit_total_coin", itr->amount, d);
packet->setArrayDataByName("deposit_time_stamp", itr->timestamp, d);
packet->setArrayDataByName("deposit_last_coin", itr->last_amount, d);
packet->setArrayDataByName("deposit_total_status", itr->status, d);
packet->setArrayDataByName("deposit_last_status", itr->last_status, d);
d++;
}
packet->setArrayLengthByName("num_history", ph->history.size());
list<HouseHistory>::iterator hitr;
d = 0;
for (hitr = ph->history.begin(); hitr != ph->history.end(); hitr++)
{
packet->setArrayDataByName("history_name", hitr->name.c_str(), d);
packet->setArrayDataByName("history_coins", hitr->amount, d);
packet->setArrayDataByName("history_status", hitr->status, d);
packet->setArrayDataByName("history_time_stamp", hitr->timestamp, d);
packet->setArrayDataByName("history_reason", hitr->reason.c_str(), d);
packet->setArrayDataByName("history_add_flag", hitr->pos_flag, d);
d++;
}
EQ2Packet* pack = packet->serialize();
//DumpPacket(pack);
client->QueuePacket(pack);
}
safe_delete(packet);
}
void ClientPacketFunctions::SendHouseVisitWindow(Client* client, vector<PlayerHouse*> houses) {
PacketStruct* packet = configReader.getStruct("WS_DisplayVisitScreen", client->GetVersion());
if (packet) {
vector<PlayerHouse*>::iterator itr;
packet->setArrayLengthByName("num_houses", houses.size());
int16 i = 0;
for (itr = houses.begin(); itr != houses.end(); itr++) {
PlayerHouse* ph = *itr;
if (ph) {
HouseZone* hz = world.GetHouseZone(ph->house_id);
if (hz) {
packet->setArrayDataByName("house_id", ph->unique_id, i);
packet->setArrayDataByName("house_owner", ph->player_name.c_str(), i);
packet->setArrayDataByName("house_location", hz->name.c_str(), i);
packet->setArrayDataByName("house_zone", client->GetCurrentZone()->GetZoneName(), i);
if ( string(client->GetPlayer()->GetName()).compare(ph->player_name) == 0 )
packet->setArrayDataByName("access_level", 4, i);
else
packet->setArrayDataByName("access_level", 1, i);
packet->setArrayDataByName("visit_flag", 0, i); // 0 = allowed to visit, 1 = owner hasn't paid upkeep
i++;
}
}
}
client->QueuePacket(packet->serialize());
}
safe_delete(packet);
}
/*
<Struct Name="WS_DisplayVisitScreen" ClientVersion="1193" OpcodeName="OP_DisplayInnVisitScreenMsg">
<Data ElementName="num_houses" Type="int16" Size="1" />
<Data ElementName="visithouse_array" Type="Array" ArraySizeVariable="num_houses">
<Data ElementName="house_id" Type="int64" />
<Data ElementName="house_owner" Type="EQ2_16Bit_String" />
<Data ElementName="house_location" Type="EQ2_16Bit_string" />
<Data ElementName="house_zone" Type="EQ2_16Bit_String" />
<Data ElementName="access_level" Type="int8" Size="1" />
<Data ElementName="unknown3" Type="int8" Size="3" />
<Data ElementName="visit_flag" Type="int8" Size="1" />
</Data>
<Data ElementName="unknown4" Type="int32" Size="1" />
<Data ElementName="unknown5" Type="int8" Size="1" />
</Struct>
*/
void ClientPacketFunctions::SendLocalizedTextMessage(Client* client)
{
/***
-- OP_ReloadLocalizedTxtMsg --
5/26/2020 19:08:41
69.174.200.100 -> 192.168.1.1
0000: 01 FF 63 01 62 00 00 00 1C 00 49 72 6F 6E 74 6F ..c.b.....Ironto
0010: 65 73 20 45 61 73 74 20 4C 61 72 67 65 20 49 6E es East Large In
0020: 6E 20 52 6F 6F 6D 07 01 00 00 00 1C 00 49 72 6F n Room.......Iro
0030: 6E 74 6F 65 73 20 45 61 73 74 20 4C 61 72 67 65 ntoes East Large
0040: 20 49 6E 6E 20 52 6F 6F 6D 07 02 00 00 00 1C 00 Inn Room.......
0050: 49 72 6F 6E 74 6F 65 73 20 45 61 73 74 20 4C 61 Irontoes East La
0060: 72 67 65 20 49 6E 6E 20 52 6F 6F 6D 07 03 00 00 rge Inn Room....
0070: 00 1C 00 49 72 6F 6E 74 6F 65 73 20 45 61 73 74 ...Irontoes East
0080: 20 4C 61 72 67 65 20 49 6E 6E 20 52 6F 6F 6D 07 Large Inn Room.
0090: 04 00 00 00 1C 00 49 72 6F 6E 74 6F 65 73 20 45 ......Irontoes E
00A0: 61 73 74 20 4C 61 72 67 65 20 49 6E 6E 20 52 6F ast Large Inn Ro
00B0: 6F 6D 07 05 00 00 00 1C 00 49 72 6F 6E 74 6F 65 om.......Irontoe
00C0: 73 20 45 61 73 74 20 4C 61 72 67 65 20 49 6E 6E s East Large Inn
00D0: 20 52 6F 6F 6D 07 06 00 00 00 1C 00 49 72 6F 6E Room.......Iron
00E0: 74 6F 65 73 20 45 61 73 74 20 4C 61 72 67 65 20 toes East Large
00F0: 49 6E 6E 20 52 6F 6F 6D 07 07 00 00 00 19 00 51 Inn Room.......Q
0100: 65 79 6E 6F 73 20 47 75 69 6C 64 20 48 61 6C 6C eynos Guild Hall
0110: 2C 20 54 69 65 72 20 31 07 08 00 00 00 16 00 4C , Tier 1.......L
0120: 69 6F 6E 27 73 20 4D 61 6E 65 20 53 75 69 74 65 ion's Mane Suite
0130: 20 52 6F 6F 6D 07 09 00 00 00 16 00 4C 69 6F 6E Room.......Lion
0140: 27 73 20 4D 61 6E 65 20 53 75 69 74 65 20 52 6F 's Mane Suite Ro
0150: 6F 6D 07 0A 00 00 00 16 00 4C 69 6F 6E 27 73 20 om.......Lion's
0160: 4D 61 6E 65 20 53 75 69 74 65 20 52 6F 6F 6D 07 Mane Suite Room.
0170: 0B 00 00 00 16 00 4C 69 6F 6E 27 73 20 4D 61 6E ......Lion's Man
0180: 65 20 53 75 69 74 65 20 52 6F 6F 6D 07 0C 00 00 e Suite Room....
0190: 00 0E 00 32 20 4C 75 63 69 65 20 53 74 72 65 65 ...2 Lucie Stree
01A0: 74 07 0D 00 00 00 0F 00 31 37 20 54 72 61 6E 71 t.......17 Tranq
01B0: 75 69 6C 20 57 61 79 07 0E 00 00 00 0E 00 38 20 uil Way.......8
01C0: 4C 75 63 69 65 20 53 74 72 65 65 74 07 0F 00 00 Lucie Street....
01D0: 00 0F 00 31 32 20 4C 75 63 69 65 20 53 74 72 65 ...12 Lucie Stre
01E0: 65 74 07 10 00 00 00 0F 00 31 38 20 4C 75 63 69 et.......18 Luci
01F0: 65 20 53 74 72 65 65 74 07 11 00 00 00 0F 00 32 e Street.......2
0200: 30 20 4C 75 63 69 65 20 53 74 72 65 65 74 07 12 0 Lucie Street..
0210: 00 00 00 0E 00 33 20 54 72 61 6E 71 75 69 6C 20 .....3 Tranquil
0220: 57 61 79 07 13 00 00 00 0E 00 37 20 54 72 61 6E Way.......7 Tran
0230: 71 75 69 6C 20 57 61 79 07 14 00 00 00 0F 00 31 quil Way.......1
0240: 33 20 54 72 61 6E 71 75 69 6C 20 57 61 79 07 15 3 Tranquil Way..
0250: 00 00 00 0F 00 31 35 20 54 72 61 6E 71 75 69 6C .....15 Tranquil
0260: 20 57 61 79 07 16 00 00 00 19 00 51 65 79 6E 6F Way.......Qeyno
0270: 73 20 47 75 69 6C 64 20 48 61 6C 6C 2C 20 54 69 s Guild Hall, Ti
0280: 65 72 20 32 07 17 00 00 00 0F 00 38 20 45 72 6F er 2.......8 Ero
0290: 6C 6C 69 73 69 20 4C 61 6E 65 07 18 00 00 00 0F llisi Lane......
02A0: 00 35 20 45 72 6F 6C 6C 69 73 69 20 4C 61 6E 65 .5 Erollisi Lane
02B0: 07 19 00 00 00 0E 00 35 20 4B 61 72 61 6E 61 20 .......5 Karana
02C0: 43 6F 75 72 74 07 1A 00 00 00 0D 00 32 20 42 61 Court.......2 Ba
02D0: 79 6C 65 20 43 6F 75 72 74 07 1B 00 00 00 0D 00 yle Court.......
02E0: 34 20 42 61 79 6C 65 20 43 6F 75 72 74 07 1C 00 4 Bayle Court...
02F0: 00 00 16 00 4C 69 6F 6E 27 73 20 4D 61 6E 65 20 ....Lion's Mane
0300: 53 75 69 74 65 20 52 6F 6F 6D 07 1D 00 00 00 16 Suite Room......
0310: 00 4C 69 6F 6E 27 73 20 4D 61 6E 65 20 53 75 69 .Lion's Mane Sui
0320: 74 65 20 52 6F 6F 6D 07 1E 00 00 00 16 00 4C 69 te Room.......Li
0330: 6F 6E 27 73 20 4D 61 6E 65 20 53 75 69 74 65 20 on's Mane Suite
0340: 52 6F 6F 6D 07 1F 00 00 00 16 00 4C 69 6F 6E 27 Room.......Lion'
0350: 73 20 4D 61 6E 65 20 53 75 69 74 65 20 52 6F 6F s Mane Suite Roo
0360: 6D 07 20 00 00 00 0E 00 35 20 4C 75 63 69 65 20 m. .....5 Lucie
0370: 53 74 72 65 65 74 07 21 00 00 00 0F 00 32 30 20 Street.!.....20
0380: 4B 61 72 61 6E 61 20 43 6F 75 72 74 07 22 00 00 Karana Court."..
0390: 00 0E 00 39 20 4C 75 63 69 65 20 53 74 72 65 65 ...9 Lucie Stree
03A0: 74 07 23 00 00 00 0F 00 31 35 20 4C 75 63 69 65 t.#.....15 Lucie
03B0: 20 53 74 72 65 65 74 07 24 00 00 00 0F 00 31 37 Street.$.....17
03C0: 20 4C 75 63 69 65 20 53 74 72 65 65 74 07 25 00 Lucie Street.%.
03D0: 00 00 0F 00 32 31 20 4C 75 63 69 65 20 53 74 72 ....21 Lucie Str
03E0: 65 65 74 07 26 00 00 00 0E 00 36 20 4B 61 72 61 eet.&.....6 Kara
03F0: 6E 61 20 43 6F 75 72 74 07 27 00 00 00 0F 00 31 na Court.'.....1
0400: 32 20 4B 61 72 61 6E 61 20 43 6F 75 72 74 07 28 2 Karana Court.(
0410: 00 00 00 0F 00 31 34 20 4B 61 72 61 6E 61 20 43 .....14 Karana C
0420: 6F 75 72 74 07 29 00 00 00 0F 00 31 38 20 4B 61 ourt.).....18 Ka
0430: 72 61 6E 61 20 43 6F 75 72 74 07 2A 00 00 00 1E rana Court.*....
0440: 00 43 6F 6E 63 6F 72 64 69 75 6D 20 54 6F 77 65 .Concordium Towe
0450: 72 20 4D 61 67 69 63 61 6C 20 4D 61 6E 6F 72 07 r Magical Manor.
0460: 2B 00 00 00 15 00 41 72 63 61 6E 65 20 41 63 61 +.....Arcane Aca
0470: 64 65 6D 79 20 50 6F 72 74 61 6C 07 2C 00 00 00 demy Portal.,...
0480: 13 00 43 6F 75 72 74 20 6F 66 20 74 68 65 20 4D ..Court of the M
0490: 61 73 74 65 72 07 2D 00 00 00 13 00 43 69 74 79 aster.-.....City
04A0: 20 6F 66 20 4D 69 73 74 20 45 73 74 61 74 65 07 of Mist Estate.
04B0: 2E 00 00 00 10 00 44 61 72 6B 6C 69 67 68 74 20 ......Darklight
04C0: 50 61 6C 61 63 65 07 2F 00 00 00 11 00 44 65 65 Palace./.....Dee
04D0: 70 77 61 74 65 72 20 52 65 74 72 65 61 74 07 30 pwater Retreat.0
04E0: 00 00 00 24 00 44 68 61 6C 67 61 72 20 50 72 65 ...$.Dhalgar Pre
04F0: 63 69 70 69 63 65 20 6F 66 20 74 68 65 20 44 65 cipice of the De
0500: 65 70 20 50 6F 72 74 61 6C 07 31 00 00 00 12 00 ep Portal.1.....
0510: 44 69 6D 65 6E 73 69 6F 6E 61 6C 20 50 6F 63 6B Dimensional Pock
0520: 65 74 07 32 00 00 00 0B 00 44 6F 6A 6F 20 50 6F et.2.....Dojo Po
0530: 72 74 61 6C 07 33 00 00 00 21 00 45 6C 61 62 6F rtal.3...!.Elabo
0540: 72 61 74 65 20 45 73 74 61 74 65 20 6F 66 20 55 rate Estate of U
0550: 6E 72 65 73 74 20 50 6F 72 74 61 6C 07 34 00 00 nrest Portal.4..
0560: 00 11 00 45 74 68 65 72 6E 65 72 65 20 45 6E 63 ...Ethernere Enc
0570: 6C 61 76 65 07 35 00 00 00 10 00 45 76 65 72 66 lave.5.....Everf
0580: 72 6F 73 74 20 50 6F 72 74 61 6C 07 36 00 00 00 rost Portal.6...
0590: 16 00 46 65 61 72 66 75 6C 20 52 65 74 72 65 61 ..Fearful Retrea
05A0: 74 20 50 6F 72 74 61 6C 07 37 00 00 00 0F 00 46 t Portal.7.....F
05B0: 65 6C 77 69 74 68 65 20 50 6F 72 74 61 6C 07 38 elwithe Portal.8
05C0: 00 00 00 10 00 46 72 65 65 62 6C 6F 6F 64 20 50 .....Freeblood P
05D0: 6F 72 74 61 6C 07 39 00 00 00 0C 00 46 72 69 67 ortal.9.....Frig
05E0: 68 74 20 4D 61 6E 6F 72 07 3A 00 00 00 11 00 47 ht Manor.:.....G
05F0: 61 6C 6C 65 6F 6E 20 6F 66 20 44 72 65 61 6D 73 alleon of Dreams
0600: 07 3B 00 00 00 14 00 48 61 6C 6C 20 6F 66 20 74 .;.....Hall of t
0610: 68 65 20 43 68 61 6D 70 69 6F 6E 07 3C 00 00 00 he Champion.<...
0620: 10 00 48 75 61 20 4D 65 69 6E 20 52 65 74 72 65 ..Hua Mein Retre
0630: 61 74 07 3D 00 00 00 1C 00 49 73 6C 65 20 6F 66 at.=.....Isle of
0640: 20 52 65 66 75 67 65 20 50 72 65 73 74 69 67 65 Refuge Prestige
0650: 20 48 6F 6D 65 07 3E 00 00 00 0F 00 4B 65 72 61 Home.>.....Kera
0660: 66 79 72 6D 27 73 20 4C 61 69 72 07 3F 00 00 00 fyrm's Lair.?...
0670: 0E 00 4B 72 6F 6D 7A 65 6B 20 50 6F 72 74 61 6C ..Kromzek Portal
0680: 07 40 00 00 00 10 00 4C 61 76 61 73 74 6F 72 6D .@.....Lavastorm
0690: 20 50 6F 72 74 61 6C 07 41 00 00 00 0E 00 4C 69 Portal.A.....Li
06A0: 62 72 61 72 79 20 50 6F 72 74 61 6C 07 42 00 00 brary Portal.B..
06B0: 00 0B 00 4D 61 72 61 20 45 73 74 61 74 65 07 43 ...Mara Estate.C
06C0: 00 00 00 21 00 4D 61 6A 27 44 75 6C 20 41 73 74 ...!.Maj'Dul Ast
06D0: 72 6F 6E 6F 6D 65 72 27 73 20 54 6F 77 65 72 20 ronomer's Tower
06E0: 50 6F 72 74 61 6C 07 44 00 00 00 14 00 4D 61 6A Portal.D.....Maj
06F0: 27 44 75 6C 20 53 75 69 74 65 20 50 6F 72 74 61 'Dul Suite Porta
0700: 6C 07 45 00 00 00 17 00 4D 69 73 74 6D 6F 6F 72 l.E.....Mistmoor
0710: 65 20 43 72 61 67 73 20 45 73 74 61 74 65 73 07 e Crags Estates.
0720: 46 00 00 00 0D 00 4F 61 6B 6D 79 73 74 20 47 6C F.....Oakmyst Gl
0730: 61 64 65 07 47 00 00 00 12 00 4F 70 65 72 61 20 ade.G.....Opera
0740: 48 6F 75 73 65 20 50 6F 72 74 61 6C 07 48 00 00 House Portal.H..
0750: 00 16 00 50 65 72 73 6F 6E 61 6C 20 47 72 6F 74 ...Personal Grot
0760: 74 6F 20 50 6F 72 74 61 6C 07 49 00 00 00 17 00 to Portal.I.....
0770: 52 75 6D 20 52 75 6E 6E 65 72 73 20 43 6F 76 65 Rum Runners Cove
0780: 20 50 6F 72 74 61 6C 07 4A 00 00 00 12 00 50 6C Portal.J.....Pl
0790: 61 6E 65 74 61 72 69 75 6D 20 50 6F 72 74 61 6C anetarium Portal
07A0: 07 4B 00 00 00 14 00 52 65 73 65 61 72 63 68 65 .K.....Researche
07B0: 72 27 73 20 53 61 6E 63 74 75 6D 07 4C 00 00 00 r's Sanctum.L...
07C0: 1E 00 52 65 73 69 64 65 6E 63 65 20 6F 66 20 74 ..Residence of t
07D0: 68 65 20 42 6C 61 64 65 73 20 50 6F 72 74 61 6C he Blades Portal
07E0: 07 4D 00 00 00 16 00 53 61 6E 63 74 75 73 20 53 .M.....Sanctus S
07F0: 65 72 75 20 50 72 6F 6D 65 6E 61 64 65 07 4E 00 eru Promenade.N.
0800: 00 00 22 00 53 61 6E 74 61 20 47 6C 75 67 27 73 ..".Santa Glug's
0810: 20 43 68 65 65 72 66 75 6C 20 48 6F 6C 69 64 61 Cheerful Holida
0820: 79 20 48 6F 6D 65 07 4F 00 00 00 17 00 53 65 63 y Home.O.....Sec
0830: 6C 75 64 65 64 20 53 61 6E 63 74 75 6D 20 50 6F luded Sanctum Po
0840: 72 74 61 6C 07 50 00 00 00 18 00 53 6B 79 62 6C rtal.P.....Skybl
0850: 61 64 65 20 53 6B 69 66 66 20 4C 61 75 6E 63 68 ade Skiff Launch
0860: 70 61 64 07 51 00 00 00 0E 00 53 6E 6F 77 79 20 pad.Q.....Snowy
0870: 44 77 65 6C 6C 69 6E 67 07 52 00 00 00 1D 00 53 Dwelling.R.....S
0880: 70 72 6F 63 6B 65 74 27 73 20 49 6E 74 65 72 6C procket's Interl
0890: 6F 63 6B 69 6E 67 20 50 6C 61 6E 65 07 53 00 00 ocking Plane.S..
08A0: 00 17 00 53 74 6F 72 6D 20 54 6F 77 65 72 20 49 ...Storm Tower I
08B0: 73 6C 65 20 50 6F 72 74 61 6C 07 54 00 00 00 21 sle Portal.T...!
08C0: 00 52 65 6C 69 63 20 54 69 6E 6B 65 72 20 50 72 .Relic Tinker Pr
08D0: 65 73 74 69 67 65 20 48 6F 6D 65 20 50 6F 72 74 estige Home Port
08E0: 61 6C 07 55 00 00 00 10 00 54 65 6E 65 62 72 6F al.U.....Tenebro
08F0: 75 73 20 50 6F 72 74 61 6C 07 56 00 00 00 10 00 us Portal.V.....
0900: 54 68 65 20 42 61 75 62 62 6C 65 73 68 69 72 65 The Baubbleshire
0910: 07 57 00 00 00 0F 00 54 69 6E 6B 65 72 65 72 27 .W.....Tinkerer'
0920: 73 20 49 73 6C 65 07 58 00 00 00 12 00 54 6F 77 s Isle.X.....Tow
0930: 65 72 20 6F 66 20 4B 6E 6F 77 6C 65 64 67 65 07 er of Knowledge.
0940: 59 00 00 00 15 00 55 6E 63 61 6E 6E 79 20 45 73 Y.....Uncanny Es
0950: 74 61 74 65 20 50 6F 72 74 61 6C 07 5A 00 00 00 tate Portal.Z...
0960: 1E 00 56 61 63 61 6E 74 20 45 73 74 61 74 65 20 ..Vacant Estate
0970: 6F 66 20 55 6E 72 65 73 74 20 50 6F 72 74 61 6C of Unrest Portal
0980: 07 5B 00 00 00 18 00 56 61 6C 65 20 6F 66 20 48 .[.....Vale of H
0990: 61 6C 66 70 69 6E 74 20 44 65 6C 69 67 68 74 07 alfpint Delight.
09A0: 5C 00 00 00 26 00 4C 69 6F 6E 27 73 20 4D 61 6E \...&.Lion's Man
09B0: 65 20 56 65 73 74 69 67 65 20 52 6F 6F 6D 20 2D e Vestige Room -
09C0: 20 4E 65 74 74 6C 65 76 69 6C 6C 65 07 5D 00 00 Nettleville.]..
09D0: 00 2C 00 4C 69 6F 6E 27 73 20 4D 61 6E 65 20 56 .,.Lion's Mane V
09E0: 65 73 74 69 67 65 20 52 6F 6F 6D 20 2D 20 53 74 estige Room - St
09F0: 61 72 63 72 65 73 74 20 43 6F 6D 6D 75 6E 65 07 arcrest Commune.
0A00: 5E 00 00 00 29 00 4C 69 6F 6E 27 73 20 4D 61 6E ^...).Lion's Man
0A10: 65 20 56 65 73 74 69 67 65 20 52 6F 6F 6D 20 2D e Vestige Room -
0A20: 20 47 72 61 79 73 74 6F 6E 65 20 59 61 72 64 07 Graystone Yard.
0A30: 5F 00 00 00 2C 00 4C 69 6F 6E 27 73 20 4D 61 6E _...,.Lion's Man
0A40: 65 20 56 65 73 74 69 67 65 20 52 6F 6F 6D 20 2D e Vestige Room -
0A50: 20 43 61 73 74 6C 65 76 69 65 77 20 48 61 6D 6C Castleview Haml
0A60: 65 74 07 60 00 00 00 2A 00 4C 69 6F 6E 27 73 20 et.`...*.Lion's
0A70: 4D 61 6E 65 20 56 65 73 74 69 67 65 20 52 6F 6F Mane Vestige Roo
0A80: 6D 20 2D 20 54 68 65 20 57 69 6C 6C 6F 77 20 57 m - The Willow W
0A90: 6F 6F 64 07 61 00 00 00 2B 00 4C 69 6F 6E 27 73 ood.a...+.Lion's
0AA0: 20 4D 61 6E 65 20 56 65 73 74 69 67 65 20 52 6F Mane Vestige Ro
0AB0: 6F 6D 20 2D 20 54 68 65 20 42 61 75 62 62 6C 65 om - The Baubble
0AC0: 73 68 69 72 65 07 62 00 00 00 FF FF FF FF shire.b.......
*/
}

View File

@ -0,0 +1,157 @@
# Race Types System
The race types system manages creature and NPC classifications in EverQuest II, providing a hierarchical categorization of all non-player entities in the game world.
## Overview
Unlike the character races system (`internal/races`), which handles the 21 playable character races used by both players and NPCs, the race types system classifies all creatures and NPCs into categories and subcategories for game mechanics, AI behavior, combat calculations, and visual representation.
**Note**: This package is located under `internal/npc/race_types` because it specifically deals with NPC creature types. The `internal/races` package remains at the top level because it's used by the Entity system, which is inherited by both players and NPCs.
## Key Components
### Types and Constants (`types.go`, `constants.go`)
- **RaceType**: Core structure containing race type ID, category, subcategory, and model name
- **Constants**: Defines 300+ race type IDs organized into 10 major categories
- **Statistics**: Tracks usage and query patterns for performance monitoring
### Master Race Type List (`master_race_type_list.go`)
- Maps model IDs to race type information
- Thread-safe operations with read/write locking
- Provides lookups by model ID, category, and subcategory
- Maintains statistics on race type usage
### Database Operations (`database.go`)
- Loads race types from the `race_types` table
- Supports CRUD operations for race type management
- Creates necessary database schema and indexes
### Manager (`manager.go`)
- High-level interface for race type operations
- Creature type checking methods (IsUndead, IsDragonkind, etc.)
- Command processing for debugging and administration
- Integration point for other game systems
### Interfaces (`interfaces.go`)
- **RaceTypeProvider**: Core interface for accessing race type information
- **CreatureTypeChecker**: Interface for checking creature classifications
- **RaceTypeAware**: Interface for entities that have a model type
- **NPCRaceTypeAdapter**: Adapter for integrating race types with NPC system
- Additional interfaces for damage, loot, and AI behavior modifications
## Race Type Categories
The system organizes creatures into 10 major categories:
1. **DRAGONKIND** (101-110): Dragons, drakes, wyrms, wyverns
2. **FAY** (111-122): Fae creatures, sprites, wisps, treants
3. **MAGICAL** (123-154): Constructs, golems, elementals, magical beings
4. **MECHANIMAGICAL** (155-157): Clockwork creatures, iron guardians
5. **NATURAL** (158-239): Animals, insects, reptiles, natural creatures
6. **PLANAR** (240-267): Demons, elementals, planar beings
7. **PLANT** (268-274): Carnivorous plants, animated vegetation
8. **SENTIENT** (275-332): Intelligent humanoids, NPC versions of player races
9. **UNDEAD** (333-343): Ghosts, skeletons, zombies, vampires
10. **WERE** (344-347): Werewolves and other lycanthropes
## Usage Examples
### Basic Race Type Lookup
```go
manager := race_types.NewManager(db)
manager.Initialize()
// Get race type for a model
modelID := int16(1234)
raceType := manager.GetRaceType(modelID)
category := manager.GetRaceTypeCategory(modelID)
// Check creature type
if manager.IsUndead(modelID) {
// Apply undead-specific mechanics
}
```
### NPC Integration
```go
// Assuming npc implements RaceTypeAware interface
adapter := race_types.NewNPCRaceTypeAdapter(npc, manager)
if adapter.IsDragonkind() {
// Apply dragon-specific abilities
}
raceInfo := adapter.GetRaceTypeInfo()
fmt.Printf("NPC is a %s (%s)\n", raceInfo.Category, raceInfo.Subcategory)
```
### Category Queries
```go
// Get all creatures in a category
dragons := manager.GetRaceTypesByCategory("DRAGONKIND")
for modelID, raceType := range dragons {
fmt.Printf("Model %d: %s\n", modelID, raceType.ModelName)
}
// Get statistics
stats := manager.GetStatistics()
fmt.Printf("Total race types: %d\n", stats.TotalRaceTypes)
```
## Database Schema
```sql
CREATE TABLE race_types (
model_type INTEGER PRIMARY KEY,
race_id INTEGER NOT NULL CHECK (race_id > 0),
category TEXT,
subcategory TEXT,
model_name TEXT
);
```
## Integration with Other Systems
### NPC System
The race types system integrates with the NPC system to provide:
- Creature classification for AI behavior
- Visual model selection
- Combat mechanics (damage modifiers, resistances)
- Loot table selection
### Combat System
Race types can influence:
- Damage calculations (e.g., bonus damage vs undead)
- Resistance calculations (e.g., dragons resistant to fire)
- Special ability availability
### Spawn System
The spawn system uses race types to:
- Determine which creatures spawn in specific zones
- Apply race-specific spawn behaviors
- Control population distribution
## Performance Considerations
- The master list uses a hash map for O(1) lookups by model ID
- Category and subcategory queries iterate through the list
- Statistics tracking has minimal overhead
- Thread-safe operations ensure data consistency
## Future Enhancements
Potential areas for expansion include:
- Race-specific spell resistances
- Faction relationships based on race types
- Advanced AI behaviors per race category
- Race-specific loot table modifiers
- Integration with scripting system for race-based events
## Differences from C++ Implementation
The Go implementation maintains compatibility with the C++ version while adding:
- Thread-safe operations with proper mutex usage
- Comprehensive statistics tracking
- More extensive validation and error handling
- Additional helper methods for common operations
- Clean interface definitions for system integration

View File

@ -0,0 +1,343 @@
package race_types
// Race type category constants
// Converted from C++ RaceTypes.h defines
const (
// DRAGONKIND category (101-110)
Dragonkind = 101
Dragon = 102
Drake = 103
Drakota = 104
Droag = 105
Faedrake = 106
Sokokar = 107
Wurm = 108
Wyrm = 109
Wyvern = 110
// FAY category (111-122)
Fay = 111
ArasaiNPC = 112
Bixie = 113
Brownie = 114
Dryad = 115
FaeNPC = 116
Fairy = 117
Siren = 118
Spirit = 119
Sprite = 120
Treant = 121 // L&L 8
Wisp = 122
// MAGICAL category (123-154)
Magical = 123
Amorph = 124
Construct = 125
Animation = 126
BoneGolem = 127
Bovoch = 128
CarrionGolem = 129
ClayGolem = 130
Cube = 131
Dervish = 132
Devourer = 133
Gargoyle = 134
Golem = 135
Goo = 136
Harpy = 137
Imp = 138
LivingStatue = 139
Mannequin = 140
Mimic = 141
Moppet = 142
Naga = 143
Nayad = 144
Ooze = 145
Rumbler = 146
RustMonster = 147
Satyr = 148
Scarecrow = 149
Spheroid = 150
TentacleTerror = 151
Tome = 152
Unicorn = 153
WoodElemental = 154
// MECHANIMAGICAL category (155-157)
Mechanimagical = 155
Clockwork = 156
IronGuardian = 157
// NATURAL category (158-239)
Natural = 158
Animal = 159
Aquatic = 160
Avian = 161
Canine = 162
Equine = 163
Feline = 164
Insect = 165
Primate = 166
Reptile = 167
Anemone = 168
Apopheli = 169
Armadillo = 170
Badger = 171
Barracuda = 172
Basilisk = 173
Bat = 174
Bear = 175
Beaver = 176
Beetle = 177
Bovine = 178
Brontotherium = 179
Brute = 180
Camel = 181
Cat = 182
Centipede = 183
Cerberus = 184
Chimera = 185
Chokidai = 186
Cobra = 187
Cockatrice = 188
Crab = 189
Crocodile = 190
Deer = 191
Dragonfly = 192
Duck = 193
Eel = 194
Elephant = 195
FlyingSnake = 196
Frog = 197
Goat = 198
Gorilla = 199
Griffin = 200
Hawk = 201
HiveQueen = 202
Horse = 203
Hyena = 204
KhoalRat = 205
Kybur = 206
Leech = 207
Leopard = 208
Lion = 209
Lizard = 210
Mammoth = 211
MantaRay = 212
MoleRat = 213
Monkey = 214
Mythical = 215
Octopus = 216
OwlBear = 217
Pig = 218
Piranha = 219
Raptor = 220
Rat = 221
Rhinoceros = 222
RockCrawler = 223
SaberTooth = 224
Scorpion = 225
SeaTurtle = 226
Shark = 227
Sheep = 228
Slug = 229
Snake = 230
Spider = 231
Stirge = 232
Swordfish = 233
Tiger = 234
Turtle = 235
Vermin = 236
Vulrich = 237
Wolf = 238
Yeti = 239
// PLANAR category (240-267)
Planar = 240
Abomination = 241
AirElemental = 242
Amygdalan = 243
Avatar = 244
Cyclops = 245
Demon = 246
Djinn = 247
EarthElemental = 248
Efreeti = 249
Elemental = 250
Ethereal = 251
Etherpine = 252
EvilEye = 253
FireElemental = 254
Gazer = 255
Gehein = 256
Geonid = 257
Giant = 258 // L&L 5
Salamander = 259
ShadowedMan = 260
Sphinx = 261
Spore = 262
Succubus = 263
Valkyrie = 264
VoidBeast = 265
WaterElemental = 266
Wraith = 267
// PLANT category (268-274)
Plant = 268
CarnivorousPlant = 269
Catoplebas = 270
Mantrap = 271
RootAbomination = 272
RootHorror = 273
Succulent = 274
// SENTIENT category (275-332)
Sentient = 275
Ashlok = 276
Aviak = 277
BarbarianNPC = 278
BirdMan = 279
BoarFiend = 280
Bugbear = 281
Burynai = 282
Centaur = 283 // L&L 4
Coldain = 284
Dal = 285
DarkElfNPC = 286
Dizok = 287
Drachnid = 288
Drafling = 289
Drolvarg = 290
DwarfNPC = 291
EruditeNPC = 292
Ettin = 293
FreebloodNPC = 294
FroglokNPC = 295
FrostfellElf = 296
FungusMan = 297
Gnoll = 298 // L&L 1
GnomeNPC = 299
Goblin = 300 // L&L 3
Gruengach = 301
HalfElfNPC = 302
HalflingNPC = 303
HighElfNPC = 304
Holgresh = 305
Hooluk = 306
Huamein = 307
HumanNPC = 308
Humanoid = 309
IksarNPC = 310
Kerigdal = 311
KerranNPC = 312
Kobold = 313
LizardMan = 314
Minotaur = 315
OgreNPC = 316
Orc = 317 // L&L 2
Othmir = 318
RatongaNPC = 319
Ravasect = 320
Rendadal = 321
Roekillik = 322
SarnakNPC = 323
Skorpikis = 324
Spiroc = 325
Troglodyte = 326
TrollNPC = 327
Ulthork = 328
Vultak = 329
WoodElfNPC = 330
WraithGuard = 331
Yhalei = 332
// UNDEAD category (333-343)
Undead = 333
Ghost = 334
Ghoul = 335
Gunthak = 336
Horror = 337
Mummy = 338
ShinreeOrcs = 339
Skeleton = 340 // L&L 6
Spectre = 341
VampireNPC = 342
Zombie = 343 // L&L 7
// WERE category (344-347)
Were = 344
AhrounWerewolves = 345
LykulakWerewolves = 346
Werewolf = 347
)
// Category name constants
const (
CategoryDragonkind = "DRAGONKIND"
CategoryFay = "FAY"
CategoryMagical = "MAGICAL"
CategoryMechanimagical = "MECHANIMAGICAL"
CategoryNatural = "NATURAL"
CategoryPlanar = "PLANAR"
CategoryPlant = "PLANT"
CategorySentient = "SENTIENT"
CategoryUndead = "UNDEAD"
CategoryWere = "WERE"
)
// GetRaceTypeCategory returns the base category ID for a given race type ID
// Converted from C++ GetRaceTypeCategory function
func GetRaceTypeCategory(raceTypeID int16) int16 {
switch {
case raceTypeID >= Dragonkind && raceTypeID <= Wyvern:
return Dragonkind
case raceTypeID >= Fay && raceTypeID <= Wisp:
return Fay
case raceTypeID >= Magical && raceTypeID <= WoodElemental:
return Magical
case raceTypeID >= Mechanimagical && raceTypeID <= IronGuardian:
return Mechanimagical
case raceTypeID >= Natural && raceTypeID <= Yeti:
return Natural
case raceTypeID >= Planar && raceTypeID <= Wraith:
return Planar
case raceTypeID >= Plant && raceTypeID <= Succulent:
return Plant
case raceTypeID >= Sentient && raceTypeID <= Yhalei:
return Sentient
case raceTypeID >= Undead && raceTypeID <= Zombie:
return Undead
case raceTypeID >= Were && raceTypeID <= Werewolf:
return Were
default:
return 0
}
}
// GetCategoryName returns the category name for a given category ID
func GetCategoryName(categoryID int16) string {
switch categoryID {
case Dragonkind:
return CategoryDragonkind
case Fay:
return CategoryFay
case Magical:
return CategoryMagical
case Mechanimagical:
return CategoryMechanimagical
case Natural:
return CategoryNatural
case Planar:
return CategoryPlanar
case Plant:
return CategoryPlant
case Sentient:
return CategorySentient
case Undead:
return CategoryUndead
case Were:
return CategoryWere
default:
return ""
}
}

View File

@ -0,0 +1,148 @@
package race_types
import (
"database/sql"
"fmt"
"log"
)
// DatabaseLoader provides database operations for race types
type DatabaseLoader struct {
db *sql.DB
}
// NewDatabaseLoader creates a new database loader
func NewDatabaseLoader(db *sql.DB) *DatabaseLoader {
return &DatabaseLoader{db: db}
}
// LoadRaceTypes loads all race types from the database
// Converted from C++ WorldDatabase::LoadRaceTypes
func (dl *DatabaseLoader) LoadRaceTypes(masterList *MasterRaceTypeList) error {
query := `
SELECT model_type, race_id, category, subcategory, model_name
FROM race_types
WHERE race_id > 0
`
rows, err := dl.db.Query(query)
if err != nil {
return fmt.Errorf("failed to query race types: %w", err)
}
defer rows.Close()
count := 0
for rows.Next() {
var modelType, raceID int16
var category, subcategory, modelName sql.NullString
err := rows.Scan(&modelType, &raceID, &category, &subcategory, &modelName)
if err != nil {
log.Printf("Error scanning race type row: %v", err)
continue
}
// Convert null strings to empty strings
categoryStr := ""
if category.Valid {
categoryStr = category.String
}
subcategoryStr := ""
if subcategory.Valid {
subcategoryStr = subcategory.String
}
modelNameStr := ""
if modelName.Valid {
modelNameStr = modelName.String
}
// Add to master list
if masterList.AddRaceType(modelType, raceID, categoryStr, subcategoryStr, modelNameStr, false) {
count++
}
}
if err := rows.Err(); err != nil {
return fmt.Errorf("error iterating race type rows: %w", err)
}
log.Printf("Loaded %d race types from database", count)
return nil
}
// SaveRaceType saves a single race type to the database
func (dl *DatabaseLoader) SaveRaceType(modelType int16, raceType *RaceType) error {
if raceType == nil || !raceType.IsValid() {
return fmt.Errorf("invalid race type")
}
query := `
INSERT OR REPLACE INTO race_types (model_type, race_id, category, subcategory, model_name)
VALUES (?, ?, ?, ?, ?)
`
_, err := dl.db.Exec(query, modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName)
if err != nil {
return fmt.Errorf("failed to save race type: %w", err)
}
return nil
}
// DeleteRaceType removes a race type from the database
func (dl *DatabaseLoader) DeleteRaceType(modelType int16) error {
query := `DELETE FROM race_types WHERE model_type = ?`
result, err := dl.db.Exec(query, modelType)
if err != nil {
return fmt.Errorf("failed to delete race type: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("race type with model_type %d not found", modelType)
}
return nil
}
// CreateRaceTypesTable creates the race_types table if it doesn't exist
func (dl *DatabaseLoader) CreateRaceTypesTable() error {
query := `
CREATE TABLE IF NOT EXISTS race_types (
model_type INTEGER PRIMARY KEY,
race_id INTEGER NOT NULL,
category TEXT,
subcategory TEXT,
model_name TEXT,
CHECK (race_id > 0)
)
`
_, err := dl.db.Exec(query)
if err != nil {
return fmt.Errorf("failed to create race_types table: %w", err)
}
// Create index on race_id for faster lookups
indexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_race_id ON race_types(race_id)`
_, err = dl.db.Exec(indexQuery)
if err != nil {
return fmt.Errorf("failed to create race_id index: %w", err)
}
// Create index on category for category-based queries
categoryIndexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_category ON race_types(category)`
_, err = dl.db.Exec(categoryIndexQuery)
if err != nil {
return fmt.Errorf("failed to create category index: %w", err)
}
return nil
}

View File

@ -0,0 +1,155 @@
package race_types
// RaceTypeProvider defines the interface for accessing race type information
type RaceTypeProvider interface {
// GetRaceType returns the race type ID for a given model ID
GetRaceType(modelID int16) int16
// GetRaceBaseType returns the base category type for a model ID
GetRaceBaseType(modelID int16) int16
// GetRaceTypeCategory returns the category name for a model ID
GetRaceTypeCategory(modelID int16) string
// GetRaceTypeSubCategory returns the subcategory name for a model ID
GetRaceTypeSubCategory(modelID int16) string
// GetRaceTypeModelName returns the model name for a model ID
GetRaceTypeModelName(modelID int16) string
// GetRaceTypeInfo returns complete race type information for a model ID
GetRaceTypeInfo(modelID int16) *RaceType
}
// CreatureTypeChecker defines the interface for checking creature types
type CreatureTypeChecker interface {
// IsCreatureType checks if a model ID represents a specific creature type
IsCreatureType(modelID int16, creatureType int16) bool
// IsUndead checks if a model ID represents an undead creature
IsUndead(modelID int16) bool
// IsDragonkind checks if a model ID represents a dragon-type creature
IsDragonkind(modelID int16) bool
// IsMagical checks if a model ID represents a magical creature
IsMagical(modelID int16) bool
// IsNatural checks if a model ID represents a natural creature
IsNatural(modelID int16) bool
// IsSentient checks if a model ID represents a sentient being
IsSentient(modelID int16) bool
}
// RaceTypeAware defines the interface for entities that have a race type
type RaceTypeAware interface {
// GetModelType returns the model type ID of the entity
GetModelType() int16
// SetModelType sets the model type ID of the entity
SetModelType(modelType int16)
}
// NPCRaceTypeAdapter provides race type functionality for NPCs
type NPCRaceTypeAdapter struct {
npc RaceTypeAware
raceTypeProvider RaceTypeProvider
}
// NewNPCRaceTypeAdapter creates a new NPC race type adapter
func NewNPCRaceTypeAdapter(npc RaceTypeAware, provider RaceTypeProvider) *NPCRaceTypeAdapter {
return &NPCRaceTypeAdapter{
npc: npc,
raceTypeProvider: provider,
}
}
// GetRaceType returns the race type ID for the NPC
func (a *NPCRaceTypeAdapter) GetRaceType() int16 {
if a.raceTypeProvider == nil {
return 0
}
return a.raceTypeProvider.GetRaceType(a.npc.GetModelType())
}
// GetRaceBaseType returns the base category type for the NPC
func (a *NPCRaceTypeAdapter) GetRaceBaseType() int16 {
if a.raceTypeProvider == nil {
return 0
}
return a.raceTypeProvider.GetRaceBaseType(a.npc.GetModelType())
}
// GetRaceTypeCategory returns the category name for the NPC
func (a *NPCRaceTypeAdapter) GetRaceTypeCategory() string {
if a.raceTypeProvider == nil {
return ""
}
return a.raceTypeProvider.GetRaceTypeCategory(a.npc.GetModelType())
}
// GetRaceTypeInfo returns complete race type information for the NPC
func (a *NPCRaceTypeAdapter) GetRaceTypeInfo() *RaceType {
if a.raceTypeProvider == nil {
return nil
}
return a.raceTypeProvider.GetRaceTypeInfo(a.npc.GetModelType())
}
// IsUndead checks if the NPC is an undead creature
func (a *NPCRaceTypeAdapter) IsUndead() bool {
baseType := a.GetRaceBaseType()
return baseType == Undead
}
// IsDragonkind checks if the NPC is a dragon-type creature
func (a *NPCRaceTypeAdapter) IsDragonkind() bool {
baseType := a.GetRaceBaseType()
return baseType == Dragonkind
}
// IsMagical checks if the NPC is a magical creature
func (a *NPCRaceTypeAdapter) IsMagical() bool {
baseType := a.GetRaceBaseType()
return baseType == Magical || baseType == Mechanimagical
}
// IsNatural checks if the NPC is a natural creature
func (a *NPCRaceTypeAdapter) IsNatural() bool {
baseType := a.GetRaceBaseType()
return baseType == Natural
}
// IsSentient checks if the NPC is a sentient being
func (a *NPCRaceTypeAdapter) IsSentient() bool {
baseType := a.GetRaceBaseType()
return baseType == Sentient
}
// DamageModifier defines an interface for race-type-based damage calculations
type DamageModifier interface {
// GetDamageModifier returns damage modifier based on attacker and target race types
// This could be used for implementing race-specific damage bonuses/penalties
GetDamageModifier(attackerModelID, targetModelID int16) float32
}
// LootModifier defines an interface for race-type-based loot modifications
type LootModifier interface {
// GetLootTableModifier returns loot table modifications based on creature race type
// This could be used for implementing race-specific loot tables
GetLootTableModifier(modelID int16) string
}
// AIBehaviorModifier defines an interface for race-type-based AI behavior
type AIBehaviorModifier interface {
// GetAIBehaviorFlags returns AI behavior flags based on race type
// This could be used for implementing race-specific AI behaviors
GetAIBehaviorFlags(modelID int16) uint32
// GetAggressionLevel returns aggression level based on race type
GetAggressionLevel(modelID int16) int8
// GetFleeHealthPercent returns health percentage at which creature flees
GetFleeHealthPercent(modelID int16) float32
}

View File

@ -0,0 +1,321 @@
package race_types
import (
"database/sql"
"fmt"
"log"
"strings"
"sync"
)
// Manager provides high-level race type management
type Manager struct {
masterList *MasterRaceTypeList
dbLoader *DatabaseLoader
db *sql.DB
// Thread safety for manager operations
mutex sync.RWMutex
}
// NewManager creates a new race type manager
func NewManager(db *sql.DB) *Manager {
return &Manager{
masterList: NewMasterRaceTypeList(),
dbLoader: NewDatabaseLoader(db),
db: db,
}
}
// Initialize sets up the race type system
func (m *Manager) Initialize() error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Create table if needed
if err := m.dbLoader.CreateRaceTypesTable(); err != nil {
return fmt.Errorf("failed to create race types table: %w", err)
}
// Load race types from database
if err := m.dbLoader.LoadRaceTypes(m.masterList); err != nil {
return fmt.Errorf("failed to load race types: %w", err)
}
log.Printf("Race type system initialized with %d race types", m.masterList.Count())
return nil
}
// GetRaceType returns the race type ID for a given model ID
func (m *Manager) GetRaceType(modelID int16) int16 {
return m.masterList.GetRaceType(modelID)
}
// GetRaceTypeInfo returns complete race type information for a model ID
func (m *Manager) GetRaceTypeInfo(modelID int16) *RaceType {
return m.masterList.GetRaceTypeByModelID(modelID)
}
// GetRaceBaseType returns the base category type for a model ID
func (m *Manager) GetRaceBaseType(modelID int16) int16 {
return m.masterList.GetRaceBaseType(modelID)
}
// GetRaceTypeCategory returns the category name for a model ID
func (m *Manager) GetRaceTypeCategory(modelID int16) string {
return m.masterList.GetRaceTypeCategory(modelID)
}
// GetRaceTypeSubCategory returns the subcategory name for a model ID
func (m *Manager) GetRaceTypeSubCategory(modelID int16) string {
return m.masterList.GetRaceTypeSubCategory(modelID)
}
// GetRaceTypeModelName returns the model name for a model ID
func (m *Manager) GetRaceTypeModelName(modelID int16) string {
return m.masterList.GetRaceTypeModelName(modelID)
}
// AddRaceType adds a new race type to the system
func (m *Manager) AddRaceType(modelID int16, raceTypeID int16, category, subcategory, modelName string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Validate input
if raceTypeID <= 0 {
return fmt.Errorf("invalid race type ID: %d", raceTypeID)
}
// Add to master list
if !m.masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, false) {
return fmt.Errorf("race type already exists for model ID %d", modelID)
}
// Save to database
raceType := &RaceType{
RaceTypeID: raceTypeID,
Category: category,
Subcategory: subcategory,
ModelName: modelName,
}
if err := m.dbLoader.SaveRaceType(modelID, raceType); err != nil {
// Rollback from master list
m.masterList.Clear() // This is not ideal but ensures consistency
m.dbLoader.LoadRaceTypes(m.masterList)
return fmt.Errorf("failed to save race type: %w", err)
}
return nil
}
// UpdateRaceType updates an existing race type
func (m *Manager) UpdateRaceType(modelID int16, raceTypeID int16, category, subcategory, modelName string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if exists
if m.masterList.GetRaceType(modelID) == 0 {
return fmt.Errorf("race type not found for model ID %d", modelID)
}
// Update in master list
if !m.masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, true) {
return fmt.Errorf("failed to update race type in master list")
}
// Save to database
raceType := &RaceType{
RaceTypeID: raceTypeID,
Category: category,
Subcategory: subcategory,
ModelName: modelName,
}
if err := m.dbLoader.SaveRaceType(modelID, raceType); err != nil {
// Reload from database to ensure consistency
m.masterList.Clear()
m.dbLoader.LoadRaceTypes(m.masterList)
return fmt.Errorf("failed to update race type in database: %w", err)
}
return nil
}
// RemoveRaceType removes a race type from the system
func (m *Manager) RemoveRaceType(modelID int16) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if exists
if m.masterList.GetRaceType(modelID) == 0 {
return fmt.Errorf("race type not found for model ID %d", modelID)
}
// Delete from database first
if err := m.dbLoader.DeleteRaceType(modelID); err != nil {
return fmt.Errorf("failed to delete race type from database: %w", err)
}
// Reload master list to ensure consistency
m.masterList.Clear()
m.dbLoader.LoadRaceTypes(m.masterList)
return nil
}
// GetRaceTypesByCategory returns all race types for a given category
func (m *Manager) GetRaceTypesByCategory(category string) map[int16]*RaceType {
return m.masterList.GetRaceTypesByCategory(category)
}
// GetRaceTypesBySubcategory returns all race types for a given subcategory
func (m *Manager) GetRaceTypesBySubcategory(subcategory string) map[int16]*RaceType {
return m.masterList.GetRaceTypesBySubcategory(subcategory)
}
// GetAllRaceTypes returns all race types in the system
func (m *Manager) GetAllRaceTypes() map[int16]*RaceType {
return m.masterList.GetAllRaceTypes()
}
// GetStatistics returns race type usage statistics
func (m *Manager) GetStatistics() *Statistics {
return m.masterList.GetStatistics()
}
// IsCreatureType checks if a model ID represents a specific creature type
func (m *Manager) IsCreatureType(modelID int16, creatureType int16) bool {
raceType := m.masterList.GetRaceType(modelID)
if raceType == 0 {
return false
}
// Check exact match
if raceType == creatureType {
return true
}
// Check if it's in the same category
return GetRaceTypeCategory(raceType) == GetRaceTypeCategory(creatureType)
}
// IsUndead checks if a model ID represents an undead creature
func (m *Manager) IsUndead(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID)
return baseType == Undead
}
// IsDragonkind checks if a model ID represents a dragon-type creature
func (m *Manager) IsDragonkind(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID)
return baseType == Dragonkind
}
// IsMagical checks if a model ID represents a magical creature
func (m *Manager) IsMagical(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID)
return baseType == Magical || baseType == Mechanimagical
}
// IsNatural checks if a model ID represents a natural creature
func (m *Manager) IsNatural(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID)
return baseType == Natural
}
// IsSentient checks if a model ID represents a sentient being
func (m *Manager) IsSentient(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID)
return baseType == Sentient
}
// GetCategoryDescription returns a human-readable description of a category
func (m *Manager) GetCategoryDescription(category string) string {
switch strings.ToUpper(category) {
case CategoryDragonkind:
return "Dragons and dragon-like creatures"
case CategoryFay:
return "Fae and fairy-type creatures"
case CategoryMagical:
return "Magical constructs and animated beings"
case CategoryMechanimagical:
return "Mechanical and magical hybrid creatures"
case CategoryNatural:
return "Natural animals and beasts"
case CategoryPlanar:
return "Planar and elemental beings"
case CategoryPlant:
return "Plant-based creatures"
case CategorySentient:
return "Sentient humanoid races"
case CategoryUndead:
return "Undead creatures"
case CategoryWere:
return "Lycanthropes and shape-shifters"
default:
return "Unknown category"
}
}
// ProcessCommand handles race type related commands
func (m *Manager) ProcessCommand(args []string) string {
if len(args) == 0 {
return "Usage: racetype <stats|list|info|category> [args...]"
}
switch strings.ToLower(args[0]) {
case "stats":
stats := m.GetStatistics()
result := fmt.Sprintf("Race Type Statistics:\n")
result += fmt.Sprintf("Total Race Types: %d\n", stats.TotalRaceTypes)
result += fmt.Sprintf("Queries by Model: %d\n", stats.QueriesByModel)
result += fmt.Sprintf("Queries by Category: %d\n", stats.QueriesByCategory)
result += fmt.Sprintf("\nCategory Counts:\n")
for cat, count := range stats.CategoryCounts {
result += fmt.Sprintf(" %s: %d\n", cat, count)
}
return result
case "list":
if len(args) < 2 {
return "Usage: racetype list <category>"
}
raceTypes := m.GetRaceTypesByCategory(args[1])
if len(raceTypes) == 0 {
return fmt.Sprintf("No race types found for category: %s", args[1])
}
result := fmt.Sprintf("Race types in category %s:\n", args[1])
for modelID, rt := range raceTypes {
result += fmt.Sprintf(" Model %d: %s (%d)\n", modelID, rt.ModelName, rt.RaceTypeID)
}
return result
case "info":
if len(args) < 2 {
return "Usage: racetype info <model_id>"
}
var modelID int16
if _, err := fmt.Sscanf(args[1], "%d", &modelID); err != nil {
return "Invalid model ID"
}
rt := m.GetRaceTypeInfo(modelID)
if rt == nil {
return fmt.Sprintf("No race type found for model ID: %d", modelID)
}
return fmt.Sprintf("Model %d:\n Race Type: %d\n Category: %s\n Subcategory: %s\n Model Name: %s\n Base Type: %s (%d)",
modelID, rt.RaceTypeID, rt.Category, rt.Subcategory, rt.ModelName,
GetCategoryName(GetRaceTypeCategory(rt.RaceTypeID)), GetRaceTypeCategory(rt.RaceTypeID))
case "category":
result := "Race Type Categories:\n"
for _, cat := range []string{CategoryDragonkind, CategoryFay, CategoryMagical, CategoryMechanimagical,
CategoryNatural, CategoryPlanar, CategoryPlant, CategorySentient, CategoryUndead, CategoryWere} {
result += fmt.Sprintf(" %s: %s\n", cat, m.GetCategoryDescription(cat))
}
return result
default:
return "Unknown racetype command. Use: stats, list, info, or category"
}
}

View File

@ -0,0 +1,233 @@
package race_types
import (
"strings"
"sync"
)
// MasterRaceTypeList manages the mapping between model IDs and race types
// Converted from C++ MasterRaceTypeList class
type MasterRaceTypeList struct {
// Maps model_id -> RaceType
raceList map[int16]*RaceType
// Thread safety
mutex sync.RWMutex
// Statistics tracking
stats *Statistics
}
// NewMasterRaceTypeList creates a new master race type list
// Converted from C++ MasterRaceTypeList constructor
func NewMasterRaceTypeList() *MasterRaceTypeList {
return &MasterRaceTypeList{
raceList: make(map[int16]*RaceType),
stats: NewStatistics(),
}
}
// AddRaceType adds a race type define to the list
// Converted from C++ AddRaceType method
func (m *MasterRaceTypeList) AddRaceType(modelID int16, raceTypeID int16, category, subcategory, modelName string, allowOverride bool) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if exists and not allowing override
if _, exists := m.raceList[modelID]; exists && !allowOverride {
return false
}
// Create new race type
raceType := &RaceType{
RaceTypeID: raceTypeID,
Category: category,
Subcategory: subcategory,
ModelName: modelName,
}
// Update statistics
if _, exists := m.raceList[modelID]; !exists {
m.stats.TotalRaceTypes++
if category != "" {
m.stats.CategoryCounts[category]++
}
if subcategory != "" {
m.stats.SubcategoryCounts[subcategory]++
}
}
m.raceList[modelID] = raceType
return true
}
// GetRaceType gets the race type for the given model
// Converted from C++ GetRaceType method
func (m *MasterRaceTypeList) GetRaceType(modelID int16) int16 {
m.mutex.RLock()
defer m.mutex.RUnlock()
m.stats.QueriesByModel++
if raceType, exists := m.raceList[modelID]; exists {
return raceType.RaceTypeID
}
return 0
}
// GetRaceTypeCategory gets the category for the given model
// Converted from C++ GetRaceTypeCategory method
func (m *MasterRaceTypeList) GetRaceTypeCategory(modelID int16) string {
m.mutex.RLock()
defer m.mutex.RUnlock()
m.stats.QueriesByCategory++
if raceType, exists := m.raceList[modelID]; exists && raceType.Category != "" {
return raceType.Category
}
return ""
}
// GetRaceTypeSubCategory gets the subcategory for the given model
// Converted from C++ GetRaceTypeSubCategory method
func (m *MasterRaceTypeList) GetRaceTypeSubCategory(modelID int16) string {
m.mutex.RLock()
defer m.mutex.RUnlock()
if raceType, exists := m.raceList[modelID]; exists && raceType.Subcategory != "" {
return raceType.Subcategory
}
return ""
}
// GetRaceTypeModelName gets the model name for the given model
// Converted from C++ GetRaceTypeModelName method
func (m *MasterRaceTypeList) GetRaceTypeModelName(modelID int16) string {
m.mutex.RLock()
defer m.mutex.RUnlock()
if raceType, exists := m.raceList[modelID]; exists && raceType.ModelName != "" {
return raceType.ModelName
}
return ""
}
// GetRaceBaseType gets the base race type for the given model
// Converted from C++ GetRaceBaseType method
func (m *MasterRaceTypeList) GetRaceBaseType(modelID int16) int16 {
m.mutex.RLock()
defer m.mutex.RUnlock()
raceType, exists := m.raceList[modelID]
if !exists {
return 0
}
return GetRaceTypeCategory(raceType.RaceTypeID)
}
// GetRaceTypeByModelID returns the full RaceType structure for a model ID
func (m *MasterRaceTypeList) GetRaceTypeByModelID(modelID int16) *RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
if raceType, exists := m.raceList[modelID]; exists {
return raceType.Copy()
}
return nil
}
// GetAllRaceTypes returns a copy of all race types
func (m *MasterRaceTypeList) GetAllRaceTypes() map[int16]*RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make(map[int16]*RaceType, len(m.raceList))
for modelID, raceType := range m.raceList {
result[modelID] = raceType.Copy()
}
return result
}
// GetRaceTypesByCategory returns all race types for a given category
func (m *MasterRaceTypeList) GetRaceTypesByCategory(category string) map[int16]*RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make(map[int16]*RaceType)
categoryUpper := strings.ToUpper(category)
for modelID, raceType := range m.raceList {
if strings.ToUpper(raceType.Category) == categoryUpper {
result[modelID] = raceType.Copy()
}
}
return result
}
// GetRaceTypesBySubcategory returns all race types for a given subcategory
func (m *MasterRaceTypeList) GetRaceTypesBySubcategory(subcategory string) map[int16]*RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make(map[int16]*RaceType)
subcategoryUpper := strings.ToUpper(subcategory)
for modelID, raceType := range m.raceList {
if strings.ToUpper(raceType.Subcategory) == subcategoryUpper {
result[modelID] = raceType.Copy()
}
}
return result
}
// Count returns the total number of race types
func (m *MasterRaceTypeList) Count() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.raceList)
}
// GetStatistics returns a copy of the statistics
func (m *MasterRaceTypeList) GetStatistics() *Statistics {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Create a copy of the statistics
statsCopy := &Statistics{
TotalRaceTypes: m.stats.TotalRaceTypes,
QueriesByModel: m.stats.QueriesByModel,
QueriesByCategory: m.stats.QueriesByCategory,
CategoryCounts: make(map[string]int64),
SubcategoryCounts: make(map[string]int64),
}
for k, v := range m.stats.CategoryCounts {
statsCopy.CategoryCounts[k] = v
}
for k, v := range m.stats.SubcategoryCounts {
statsCopy.SubcategoryCounts[k] = v
}
return statsCopy
}
// Clear removes all race types from the list
func (m *MasterRaceTypeList) Clear() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.raceList = make(map[int16]*RaceType)
m.stats = NewStatistics()
}

View File

@ -0,0 +1,45 @@
package race_types
// RaceType represents a race type structure for NPCs and creatures
// Converted from C++ RaceTypeStructure
type RaceType struct {
RaceTypeID int16 // The actual race type ID
Category string // Main category (e.g., "DRAGONKIND", "NATURAL")
Subcategory string // Subcategory (e.g., "DRAGON", "ANIMAL")
ModelName string // 3D model name for rendering
}
// Copy creates a deep copy of the RaceType
func (rt *RaceType) Copy() *RaceType {
if rt == nil {
return nil
}
return &RaceType{
RaceTypeID: rt.RaceTypeID,
Category: rt.Category,
Subcategory: rt.Subcategory,
ModelName: rt.ModelName,
}
}
// IsValid checks if the race type has valid data
func (rt *RaceType) IsValid() bool {
return rt != nil && rt.RaceTypeID > 0 && rt.Category != ""
}
// Statistics tracks race type usage and queries
type Statistics struct {
TotalRaceTypes int64 // Total number of race types loaded
QueriesByModel int64 // Number of queries by model ID
QueriesByCategory int64 // Number of queries by category
CategoryCounts map[string]int64 // Count of race types per category
SubcategoryCounts map[string]int64 // Count of race types per subcategory
}
// NewStatistics creates a new statistics tracker
func NewStatistics() *Statistics {
return &Statistics{
CategoryCounts: make(map[string]int64),
SubcategoryCounts: make(map[string]int64),
}
}

30
internal/races/README.md Normal file
View File

@ -0,0 +1,30 @@
# Character Races System
The character races system manages the 21 playable races in EverQuest II that can be used by both player characters and NPCs.
## Overview
This package handles character race definitions, including:
- Race IDs and names for all 21 playable races
- Alignment classifications (good, evil, neutral)
- Race-based stat modifiers and bonuses
- Starting location information
- Race compatibility and restrictions
## Why Not Under /internal/player?
While these are "player races" in the sense that they're the races players can choose at character creation, this package is used by the Entity system which is inherited by both:
- **Players**: For character creation and race-based mechanics
- **NPCs**: For humanoid NPCs that use player race models (e.g., Human_NPC, Dwarf_NPC, etc.)
The Entity system (`internal/entity`) references races for stat calculations, spell bonuses, and other race-based mechanics that apply to both players and NPCs.
## Relationship to Race Types
This package is distinct from the NPC race types system (`internal/npc/race_types`):
- **Character Races** (this package): The 21 playable races (Human, Elf, Dwarf, etc.)
- **Race Types** (`internal/npc/race_types`): Creature classifications (Dragon, Undead, Animal, etc.)
An NPC can have both:
- A character race (if it's a humanoid using a player race model)
- A race type (its creature classification for AI and combat mechanics)

557
internal/recipes/README.md Normal file
View File

@ -0,0 +1,557 @@
# Recipe System
Complete tradeskill recipe management system for EverQuest II, converted from C++ EQ2EMu codebase. Handles recipe definitions, player recipe knowledge, crafting components, and database persistence with modern Go concurrency patterns.
## Overview
The recipe system manages all aspects of tradeskill crafting in EQ2:
- **Recipe Management**: Master recipe lists with complex component relationships
- **Player Recipe Knowledge**: Individual player recipe collections and progress tracking
- **Recipe Books**: Tradeskill recipe book items and learning mechanics
- **Database Integration**: Complete SQLite persistence with efficient loading
- **Component System**: Multi-slot component requirements (primary, fuel, build slots)
- **Crafting Stages**: 5-stage crafting progression with products and byproducts
- **Tradeskill Classes**: Class-based recipe access control and validation
- **Integration Interfaces**: Seamless integration with player, item, client, and event systems
## Core Components
### Recipe Data Structure
```go
type Recipe struct {
// Core recipe identification
ID int32 // Unique recipe ID
SoeID int32 // SOE recipe CRC ID
Name string // Recipe display name
Description string // Recipe description
// Requirements and properties
Level int8 // Required level
Tier int8 // Recipe tier (1-10)
Skill int32 // Required tradeskill ID
Technique int32 // Technique requirement
Knowledge int32 // Knowledge requirement
Classes int32 // Tradeskill class bitmask
// Product information
ProductItemID int32 // Resulting item ID
ProductName string // Product display name
ProductQty int8 // Quantity produced
// Component titles and quantities (6 slots)
PrimaryBuildCompTitle string // Primary component name
Build1CompTitle string // Build slot 1 name
Build2CompTitle string // Build slot 2 name
Build3CompTitle string // Build slot 3 name
Build4CompTitle string // Build slot 4 name
FuelCompTitle string // Fuel component name
// Components map: slot -> list of valid item IDs
// Slots: 0=primary, 1-4=build slots, 5=fuel
Components map[int8][]int32
// Products map: stage -> products/byproducts (5 stages)
Products map[int8]*RecipeProducts
// Player progression
HighestStage int8 // Highest completed stage for player recipes
}
```
### Component Slots
The recipe system uses 6 component slots:
- **Slot 0 (Primary)**: Primary crafting material
- **Slot 1-4 (Build)**: Secondary build components
- **Slot 5 (Fuel)**: Fuel component for crafting device
Each slot can contain multiple valid item IDs, providing flexibility in component selection.
### Crafting Stages
Recipes support 5 crafting stages (0-4), each with potential products and byproducts:
```go
type RecipeProducts struct {
ProductID int32 // Main product item ID
ProductQty int8 // Product quantity
ByproductID int32 // Byproduct item ID
ByproductQty int8 // Byproduct quantity
}
```
### Master Recipe Lists
```go
type MasterRecipeList struct {
recipes map[int32]*Recipe // Recipe ID -> Recipe
mutex sync.RWMutex // Thread-safe access
}
type MasterRecipeBookList struct {
recipeBooks map[int32]*Recipe // Book ID -> Recipe
mutex sync.RWMutex // Thread-safe access
}
```
### Player Recipe Collections
```go
type PlayerRecipeList struct {
recipes map[int32]*Recipe // Player's known recipes
mutex sync.RWMutex // Thread-safe access
}
type PlayerRecipeBookList struct {
recipeBooks map[int32]*Recipe // Player's recipe books
mutex sync.RWMutex // Thread-safe access
}
```
## Database Integration
### RecipeManager
High-level recipe system management with complete database integration:
```go
type RecipeManager struct {
db *database.DB
masterRecipeList *MasterRecipeList
masterRecipeBookList *MasterRecipeBookList
loadedRecipes map[int32]*Recipe
loadedRecipeBooks map[int32]*Recipe
statisticsEnabled bool
stats RecipeManagerStats
}
```
### Key Operations
#### Loading Recipes
```go
// Load all recipes from database with complex component relationships
manager.LoadRecipes()
// Load all recipe books
manager.LoadRecipeBooks()
// Load player-specific recipes
manager.LoadPlayerRecipes(playerRecipeList, characterID)
// Load player recipe books
manager.LoadPlayerRecipeBooks(playerRecipeBookList, characterID)
```
#### Saving Recipe Progress
```go
// Save new recipe for player
manager.SavePlayerRecipe(characterID, recipeID)
// Save recipe book for player
manager.SavePlayerRecipeBook(characterID, recipebookID)
// Update recipe progress
manager.UpdatePlayerRecipe(characterID, recipeID, highestStage)
```
### Database Schema
The system integrates with these key database tables:
- `recipe`: Master recipe definitions
- `recipe_comp_list`: Component list definitions
- `recipe_comp_list_item`: Individual component items
- `recipe_secondary_comp`: Secondary component mappings
- `character_recipes`: Player recipe knowledge
- `character_recipe_books`: Player recipe book ownership
- `items`: Item definitions and recipe books
## Integration Interfaces
### RecipeSystemAdapter
Primary integration interface for external systems:
```go
type RecipeSystemAdapter interface {
// Player Integration
GetPlayerRecipeList(characterID int32) *PlayerRecipeList
LoadPlayerRecipes(characterID int32) error
// Recipe Management
GetRecipe(recipeID int32) *Recipe
ValidateRecipe(recipe *Recipe) bool
// Progress Tracking
UpdateRecipeProgress(characterID int32, recipeID int32, stage int8) error
GetRecipeProgress(characterID int32, recipeID int32) int8
}
```
### Database Adapter
Abstracts database operations for testing and multiple database support:
```go
type DatabaseRecipeAdapter interface {
LoadAllRecipes() ([]*Recipe, error)
LoadPlayerRecipes(characterID int32) ([]*Recipe, error)
SavePlayerRecipe(characterID int32, recipeID int32) error
UpdatePlayerRecipe(characterID int32, recipeID int32, highestStage int8) error
}
```
### Player Integration
```go
type PlayerRecipeAdapter interface {
CanPlayerLearnRecipe(characterID int32, recipeID int32) bool
GetPlayerTradeskillLevel(characterID int32, skillID int32) int32
AwardTradeskillExperience(characterID int32, skillID int32, experience int32) error
}
```
### Item Integration
```go
type ItemRecipeAdapter interface {
ValidateRecipeComponents(recipeID int32) bool
PlayerHasComponents(characterID int32, recipeID int32) bool
ConsumeRecipeComponents(characterID int32, recipeID int32) error
AwardRecipeProduct(characterID int32, itemID int32, quantity int8) error
}
```
### Client Integration
```go
type ClientRecipeAdapter interface {
SendRecipeList(characterID int32) error
SendRecipeLearned(characterID int32, recipeID int32) error
SendTradeskillWindow(characterID int32, deviceID int32) error
SendCraftingResults(characterID int32, success bool, itemID int32, quantity int8) error
}
```
### Event Integration
```go
type EventRecipeAdapter interface {
OnRecipeLearned(characterID int32, recipeID int32) error
OnRecipeCrafted(characterID int32, recipeID int32, success bool) error
CheckCraftingAchievements(characterID int32, recipeID int32) error
}
```
## Usage Examples
### Basic Recipe Management
```go
// Create recipe manager
db, _ := database.Open("world.db")
manager := NewRecipeManager(db)
// Load all recipes and recipe books
manager.LoadRecipes()
manager.LoadRecipeBooks()
// Get a specific recipe
recipe := manager.GetRecipe(12345)
if recipe != nil {
fmt.Printf("Recipe: %s (Level %d, Tier %d)\n",
recipe.Name, recipe.Level, recipe.Tier)
}
// Check recipe validation
if recipe.IsValid() {
fmt.Println("Recipe is valid")
}
```
### Player Recipe Operations
```go
// Load player recipes
playerRecipes := NewPlayerRecipeList()
manager.LoadPlayerRecipes(playerRecipes, characterID)
// Check if player knows a recipe
if playerRecipes.HasRecipe(recipeID) {
fmt.Println("Player knows this recipe")
}
// Learn a new recipe
recipe := manager.GetRecipe(recipeID)
if recipe != nil {
playerCopy := NewRecipeFromRecipe(recipe)
playerRecipes.AddRecipe(playerCopy)
manager.SavePlayerRecipe(characterID, recipeID)
}
```
### Recipe Component Analysis
```go
// Get components for each slot
for slot := int8(0); slot < 6; slot++ {
components := recipe.GetComponentsBySlot(slot)
title := recipe.GetComponentTitleForSlot(slot)
quantity := recipe.GetComponentQuantityForSlot(slot)
fmt.Printf("Slot %d (%s): %d needed\n", slot, title, quantity)
for _, itemID := range components {
fmt.Printf(" - Item ID: %d\n", itemID)
}
}
```
### Crafting Stage Processing
```go
// Process each crafting stage
for stage := int8(0); stage < 5; stage++ {
products := recipe.GetProductsForStage(stage)
if products != nil {
fmt.Printf("Stage %d produces:\n", stage)
fmt.Printf(" Product: %d (qty: %d)\n",
products.ProductID, products.ProductQty)
if products.ByproductID > 0 {
fmt.Printf(" Byproduct: %d (qty: %d)\n",
products.ByproductID, products.ByproductQty)
}
}
}
```
### Integration with Full System
```go
// Create system dependencies
deps := &RecipeSystemDependencies{
Database: &DatabaseRecipeAdapterImpl{},
Player: &PlayerRecipeAdapterImpl{},
Item: &ItemRecipeAdapterImpl{},
Client: &ClientRecipeAdapterImpl{},
Event: &EventRecipeAdapterImpl{},
Crafting: &CraftingRecipeAdapterImpl{},
}
// Create integrated recipe system
adapter := NewRecipeManagerAdapter(db, deps)
adapter.Initialize()
// Handle recipe learning workflow
err := adapter.PlayerLearnRecipe(characterID, recipeID)
if err != nil {
fmt.Printf("Failed to learn recipe: %v\n", err)
}
// Handle recipe book acquisition workflow
err = adapter.PlayerObtainRecipeBook(characterID, bookID)
if err != nil {
fmt.Printf("Failed to obtain recipe book: %v\n", err)
}
```
## Tradeskill Classes
The system supports all EQ2 tradeskill classes with bitmask-based access control:
### Base Classes
- **Provisioner** (food and drink)
- **Woodworker** (wooden items, bows, arrows)
- **Carpenter** (furniture and housing items)
- **Outfitter** (light and medium armor)
- **Armorer** (heavy armor and shields)
- **Weaponsmith** (metal weapons)
- **Tailor** (cloth armor and bags)
- **Scholar** (spells and combat arts)
- **Jeweler** (jewelry)
- **Sage** (advanced spells)
- **Alchemist** (potions and poisons)
- **Craftsman** (general crafting)
- **Tinkerer** (tinkered items)
### Class Validation
```go
// Check if a tradeskill class can use a recipe
canUse := recipe.CanUseRecipeByClass(classID)
// Special handling for "any class" recipes (adornments + artisan)
if recipe.Classes < 4 {
// Any class can use this recipe
}
```
## Crafting Devices
Recipes require specific crafting devices:
- **Stove & Keg**: Provisioning recipes
- **Forge**: Weaponsmithing and armoring
- **Sewing Table & Mannequin**: Tailoring and outfitting
- **Woodworking Table**: Woodworking and carpentry
- **Chemistry Table**: Alchemy
- **Jeweler's Table**: Jewelcrafting
- **Loom & Spinning Wheel**: Advanced tailoring
- **Engraved Desk**: Scribing and sage recipes
- **Work Bench**: Tinkering
## Statistics and Monitoring
### RecipeManagerStats
```go
type RecipeManagerStats struct {
TotalRecipesLoaded int32
TotalRecipeBooksLoaded int32
PlayersWithRecipes int32
LoadOperations int32
SaveOperations int32
}
// Get current statistics
stats := manager.GetStatistics()
fmt.Printf("Loaded %d recipes, %d recipe books\n",
stats.TotalRecipesLoaded, stats.TotalRecipeBooksLoaded)
```
### Validation
```go
// Comprehensive system validation
issues := manager.Validate()
for _, issue := range issues {
fmt.Printf("Validation issue: %s\n", issue)
}
// Recipe-specific validation
if !recipe.IsValid() {
fmt.Println("Recipe has invalid data")
}
```
## Thread Safety
All recipe system components are fully thread-safe:
- **RWMutex Protection**: All maps and collections use read-write mutexes
- **Atomic Operations**: Statistics counters use atomic operations where appropriate
- **Safe Copying**: Recipe copying creates deep copies of all nested data
- **Concurrent Access**: Multiple goroutines can safely read recipe data simultaneously
- **Write Synchronization**: All write operations are properly synchronized
## Performance Considerations
### Efficient Loading
- **Batch Loading**: All recipes loaded in single database query with complex joins
- **Component Resolution**: Components loaded separately and linked to recipes
- **Memory Management**: Recipes cached in memory for fast access
- **Statistics Tracking**: Optional statistics collection for performance monitoring
### Memory Usage
- **Component Maps**: Pre-allocated component arrays for each recipe
- **Product Arrays**: Fixed-size arrays for 5 crafting stages
- **Player Copies**: Player recipe instances are copies, not references
- **Cleanup**: Proper cleanup of database connections and prepared statements
## Error Handling
### Comprehensive Error Types
```go
var (
ErrRecipeNotFound = errors.New("recipe not found")
ErrRecipeBookNotFound = errors.New("recipe book not found")
ErrInvalidRecipeID = errors.New("invalid recipe ID")
ErrDuplicateRecipe = errors.New("duplicate recipe ID")
ErrMissingComponents = errors.New("missing required components")
ErrInsufficientSkill = errors.New("insufficient skill level")
ErrWrongTradeskillClass = errors.New("wrong tradeskill class")
ErrCannotLearnRecipe = errors.New("cannot learn recipe")
ErrCannotUseRecipeBook = errors.New("cannot use recipe book")
)
```
### Error Propagation
All database operations return detailed errors that can be handled appropriately by calling systems.
## Database Schema Compatibility
### C++ EQ2EMu Compatibility
The Go implementation maintains full compatibility with the existing C++ EQ2EMu database schema:
- **Recipe Table**: Direct mapping of all C++ Recipe fields
- **Component System**: Preserves complex component relationships
- **Player Data**: Compatible with existing character recipe storage
- **Query Optimization**: Uses same optimized queries as C++ version
### Migration Support
The system can seamlessly work with existing EQ2EMu databases without requiring schema changes or data migration.
## Future Enhancements
Areas marked for future implementation:
- **Lua Integration**: Recipe validation and custom logic via Lua scripts
- **Advanced Crafting**: Rare material handling and advanced crafting mechanics
- **Recipe Discovery**: Dynamic recipe discovery and experimentation
- **Guild Recipes**: Guild-specific recipe sharing and management
- **Seasonal Recipes**: Time-based recipe availability
- **Recipe Sets**: Collection-based achievements and bonuses
## Testing
### Unit Tests
```go
func TestRecipeValidation(t *testing.T) {
recipe := NewRecipe()
recipe.ID = 12345
recipe.Name = "Test Recipe"
recipe.Level = 50
recipe.Tier = 5
assert.True(t, recipe.IsValid())
}
func TestPlayerRecipeOperations(t *testing.T) {
playerRecipes := NewPlayerRecipeList()
recipe := NewRecipe()
recipe.ID = 12345
assert.True(t, playerRecipes.AddRecipe(recipe))
assert.True(t, playerRecipes.HasRecipe(12345))
assert.Equal(t, 1, playerRecipes.Size())
}
```
### Integration Tests
```go
func TestDatabaseIntegration(t *testing.T) {
db := setupTestDatabase()
manager := NewRecipeManager(db)
err := manager.LoadRecipes()
assert.NoError(t, err)
recipes, books := manager.Size()
assert.Greater(t, recipes, int32(0))
}
```
This recipe system provides a complete, thread-safe, and efficient implementation of EverQuest II tradeskill recipes with modern Go patterns while maintaining compatibility with the existing C++ EQ2EMu architecture.

View File

@ -0,0 +1,119 @@
package recipes
import "errors"
// Component slot constants
const (
SlotPrimary = 0 // Primary component slot
SlotBuild1 = 1 // Build component slot 1
SlotBuild2 = 2 // Build component slot 2
SlotBuild3 = 3 // Build component slot 3
SlotBuild4 = 4 // Build component slot 4
SlotFuel = 5 // Fuel component slot
)
// Crafting stage constants
const (
Stage0 = 0 // First crafting stage
Stage1 = 1
Stage2 = 2
Stage3 = 3
Stage4 = 4 // Final crafting stage
)
// Recipe validation constants
const (
MinRecipeID = 1
MaxRecipeID = 2147483647 // int32 max
MinRecipeLevel = 1
MaxRecipeLevel = 100
MinTier = 1
MaxTier = 10
MaxNameLength = 256
MaxStages = 5
MaxSlots = 6
)
// Tradeskill class bitmasks
// These are used to determine which classes can use a recipe
const (
TradeskillAny = 3 // 1+2: Any class can use
TradeskillAdornment = 1 // Adornment recipes
TradeskillArtisan = 2 // Base artisan recipes
// Base tradeskill classes (bits 0-12)
TradeskillProvisioner = 1 << 0
TradeskillWoodworker = 1 << 1
TradeskillCarpenter = 1 << 2
TradeskillOutfitter = 1 << 3
TradeskillArmorer = 1 << 4
TradeskillWeaponsmith = 1 << 5
TradeskillTailor = 1 << 6
TradeskillScholar = 1 << 7
TradeskillJeweler = 1 << 8
TradeskillSage = 1 << 9
TradeskillAlchemist = 1 << 10
TradeskillCraftsman = 1 << 11
TradeskillTinkerer = 1 << 12
)
// Crafting device types
const (
DeviceNone = ""
DeviceStove = "Stove & Keg"
DeviceForge = "Forge"
DeviceSewingtable = "Sewing Table & Mannequin"
DeviceSawbench = "Woodworking Table"
DeviceAlchemytable = "Chemistry Table"
DeviceJewelersTable = "Jeweler's Table"
DeviceLoomandspool = "Loom & Spinning Wheel"
DeviceEssencealtar = "Engraved Desk"
DeviceTinkerersbench = "Work Bench"
)
// Recipe skill constants (tradeskill IDs)
const (
SkillProvisioning = 40 // Food and drink
SkillWoodworking = 140 // Wooden items, bows, arrows
SkillCarpentry = 80 // Furniture and housing items
SkillOutfitting = 120 // Light and medium armor
SkillArmoring = 20 // Heavy armor and shields
SkillWeaponsmithing = 160 // Metal weapons
SkillTailoring = 140 // Cloth armor and bags
SkillScribing = 340 // Spells and combat arts
SkillJewelcrafting = 280 // Jewelry
SkillAlchemy = 320 // Potions and poisons
SkillTinkering = 360 // Tinkered items
SkillTransmuting = 1289 // Transmutation
SkillAdorning = 1796 // Adornments
)
// Error variables
var (
ErrRecipeNotFound = errors.New("recipe not found")
ErrRecipeBookNotFound = errors.New("recipe book not found")
ErrInvalidRecipeID = errors.New("invalid recipe ID")
ErrInvalidRecipeData = errors.New("invalid recipe data")
ErrDuplicateRecipe = errors.New("duplicate recipe ID")
ErrDuplicateRecipeBook = errors.New("duplicate recipe book ID")
ErrMissingComponents = errors.New("missing required components")
ErrInsufficientSkill = errors.New("insufficient skill level")
ErrWrongTradeskillClass = errors.New("wrong tradeskill class")
ErrWrongDevice = errors.New("wrong crafting device")
ErrCannotLearnRecipe = errors.New("cannot learn recipe")
ErrCannotUseRecipeBook = errors.New("cannot use recipe book")
ErrCraftingInProgress = errors.New("crafting session in progress")
ErrInvalidCraftingStage = errors.New("invalid crafting stage")
ErrCraftingSessionNotFound = errors.New("crafting session not found")
)
// Database table and column names
const (
TableRecipes = "recipe"
TableRecipeComponents = "recipe_comp_list_item"
TableRecipeSecondaryComp = "recipe_secondary_comp"
TableCharacterRecipes = "character_recipes"
TableCharacterRecipeBooks = "character_recipe_books"
TableItems = "items"
TableItemDetailsRecipe = "item_details_recipe_items"
)

View File

@ -0,0 +1,338 @@
package recipes
import "eq2emu/internal/database"
// RecipeSystemAdapter provides integration interfaces for the recipe system
// Enables seamless integration with player, database, item, and client systems
type RecipeSystemAdapter interface {
// Player Integration
GetPlayerRecipeList(characterID int32) *PlayerRecipeList
GetPlayerRecipeBookList(characterID int32) *PlayerRecipeBookList
LoadPlayerRecipes(characterID int32) error
LoadPlayerRecipeBooks(characterID int32) error
// Recipe Management
GetRecipe(recipeID int32) *Recipe
GetRecipeBook(bookID int32) *Recipe
ValidateRecipe(recipe *Recipe) bool
CanPlayerUseRecipe(characterID int32, recipeID int32) bool
// Progress Tracking
UpdateRecipeProgress(characterID int32, recipeID int32, stage int8) error
GetRecipeProgress(characterID int32, recipeID int32) int8
// System Operations
GetStatistics() RecipeManagerStats
Validate() []string
Size() (recipes int32, recipeBooks int32)
}
// DatabaseRecipeAdapter handles database operations for recipes
// Abstracts database interactions for easier testing and multiple database support
type DatabaseRecipeAdapter interface {
// Recipe Loading
LoadAllRecipes() ([]*Recipe, error)
LoadAllRecipeBooks() ([]*Recipe, error)
LoadRecipeComponents(recipeID int32) (map[int8][]int32, error)
// Player Recipe Operations
LoadPlayerRecipes(characterID int32) ([]*Recipe, error)
LoadPlayerRecipeBooks(characterID int32) ([]*Recipe, error)
SavePlayerRecipe(characterID int32, recipeID int32) error
SavePlayerRecipeBook(characterID int32, recipebookID int32) error
UpdatePlayerRecipe(characterID int32, recipeID int32, highestStage int8) error
// Validation and Utilities
RecipeExists(recipeID int32) bool
RecipeBookExists(bookID int32) bool
GetRecipesByTier(tier int8) ([]*Recipe, error)
GetRecipesBySkill(skillID int32) ([]*Recipe, error)
}
// PlayerRecipeAdapter integrates with player management systems
// Provides player-specific recipe functionality and validation
type PlayerRecipeAdapter interface {
// Player Validation
PlayerExists(characterID int32) bool
GetPlayerLevel(characterID int32) int32
GetPlayerTradeskillLevel(characterID int32, skillID int32) int32
GetPlayerClass(characterID int32) int8
// Recipe Access Control
CanPlayerLearnRecipe(characterID int32, recipeID int32) bool
CanPlayerUseRecipeBook(characterID int32, bookID int32) bool
HasPlayerLearnedRecipe(characterID int32, recipeID int32) bool
HasPlayerRecipeBook(characterID int32, bookID int32) bool
// Experience and Progression
AwardTradeskillExperience(characterID int32, skillID int32, experience int32) error
UpdateTradeskillLevel(characterID int32, skillID int32, level int32) error
// Notifications
NotifyRecipeLearned(characterID int32, recipeID int32) error
NotifyRecipeBookObtained(characterID int32, bookID int32) error
}
// ItemRecipeAdapter integrates with item management systems
// Handles item-related recipe operations and validation
type ItemRecipeAdapter interface {
// Item Validation
ItemExists(itemID int32) bool
GetItemName(itemID int32) string
GetItemIcon(itemID int32) int16
GetItemLevel(itemID int32) int32
IsItemTradeskillTool(itemID int32) bool
// Recipe Component Validation
ValidateRecipeComponents(recipeID int32) bool
GetComponentItemName(itemID int32) string
GetComponentQuantityRequired(recipeID int32, itemID int32) int16
// Inventory Integration
PlayerHasComponents(characterID int32, recipeID int32) bool
ConsumeRecipeComponents(characterID int32, recipeID int32) error
AwardRecipeProduct(characterID int32, itemID int32, quantity int8) error
// Recipe Book Items
IsRecipeBookItem(itemID int32) bool
GetRecipeBookRecipes(bookID int32) ([]int32, error)
ConsumeRecipeBook(characterID int32, bookID int32) error
}
// ClientRecipeAdapter handles client communication for recipes
// Manages packet building and client-side recipe display
type ClientRecipeAdapter interface {
// Recipe Display
SendRecipeList(characterID int32) error
SendRecipeBookList(characterID int32) error
SendRecipeDetails(characterID int32, recipeID int32) error
// Recipe Learning
SendRecipeLearned(characterID int32, recipeID int32) error
SendRecipeBookObtained(characterID int32, bookID int32) error
SendRecipeProgress(characterID int32, recipeID int32, stage int8) error
// Tradeskill Interface
SendTradeskillWindow(characterID int32, deviceID int32) error
SendRecipeComponents(characterID int32, recipeID int32) error
SendCraftingResults(characterID int32, success bool, itemID int32, quantity int8) error
// Error Messages
SendRecipeError(characterID int32, errorMessage string) error
SendInsufficientComponents(characterID int32, recipeID int32) error
SendInvalidRecipe(characterID int32, recipeID int32) error
}
// EventRecipeAdapter handles recipe-related event processing
// Manages recipe learning, crafting events, and system notifications
type EventRecipeAdapter interface {
// Recipe Events
OnRecipeLearned(characterID int32, recipeID int32) error
OnRecipeBookObtained(characterID int32, bookID int32) error
OnRecipeCrafted(characterID int32, recipeID int32, success bool) error
OnCraftingStarted(characterID int32, recipeID int32) error
OnCraftingCompleted(characterID int32, recipeID int32, stage int8) error
// Achievement Integration
CheckCraftingAchievements(characterID int32, recipeID int32) error
UpdateCraftingStatistics(characterID int32, recipeID int32, success bool) error
// Guild Integration
NotifyGuildCrafting(characterID int32, recipeID int32) error
UpdateGuildCraftingContributions(characterID int32, recipeID int32) error
}
// RecipeAware interface for entities that can interact with recipes
// Provides basic recipe interaction capabilities for players and NPCs
type RecipeAware interface {
// Recipe Knowledge
GetKnownRecipes() []int32
GetRecipeBooks() []int32
KnowsRecipe(recipeID int32) bool
HasRecipeBook(bookID int32) bool
// Recipe Learning
LearnRecipe(recipeID int32) error
ObtainRecipeBook(bookID int32) error
ForgetRecipe(recipeID int32) error
// Crafting Capabilities
CanCraftRecipe(recipeID int32) bool
GetCraftingLevel(skillID int32) int32
GetMaxCraftingStage(recipeID int32) int8
}
// CraftingRecipeAdapter integrates with crafting system
// Handles active crafting sessions and recipe execution
type CraftingRecipeAdapter interface {
// Crafting Session Management
StartCraftingSession(characterID int32, recipeID int32, deviceID int32) error
ProcessCraftingStage(characterID int32, stage int8) error
CompleteCraftingSession(characterID int32, success bool) error
CancelCraftingSession(characterID int32) error
// Crafting Validation
ValidateCraftingDevice(deviceID int32, recipeID int32) bool
ValidateCraftingComponents(characterID int32, recipeID int32) bool
ValidateCraftingSkill(characterID int32, recipeID int32) bool
// Progress Tracking
GetCraftingProgress(characterID int32) (recipeID int32, stage int8, success bool)
UpdateCraftingStage(characterID int32, stage int8) error
// Resource Management
ConsumeCraftingResources(characterID int32, recipeID int32, stage int8) error
AwardCraftingProducts(characterID int32, recipeID int32, stage int8) error
CalculateCraftingSuccess(characterID int32, recipeID int32, stage int8) bool
}
// RecipeSystemDependencies aggregates all recipe system dependencies
// Provides a single interface for system-wide recipe functionality
type RecipeSystemDependencies struct {
Database DatabaseRecipeAdapter
Player PlayerRecipeAdapter
Item ItemRecipeAdapter
Client ClientRecipeAdapter
Event EventRecipeAdapter
Crafting CraftingRecipeAdapter
RecipeSystem RecipeSystemAdapter
}
// RecipeManagerAdapter provides high-level recipe management operations
// Simplifies recipe system access for external systems
type RecipeManagerAdapter struct {
manager *RecipeManager
dependencies *RecipeSystemDependencies
}
// NewRecipeManagerAdapter creates a new recipe manager adapter with dependencies
func NewRecipeManagerAdapter(db *database.DB, deps *RecipeSystemDependencies) *RecipeManagerAdapter {
return &RecipeManagerAdapter{
manager: NewRecipeManager(db),
dependencies: deps,
}
}
// GetManager returns the underlying recipe manager
func (rma *RecipeManagerAdapter) GetManager() *RecipeManager {
return rma.manager
}
// GetDependencies returns the system dependencies
func (rma *RecipeManagerAdapter) GetDependencies() *RecipeSystemDependencies {
return rma.dependencies
}
// Initialize loads all recipes and recipe books from the database
func (rma *RecipeManagerAdapter) Initialize() error {
if err := rma.manager.LoadRecipes(); err != nil {
return err
}
return rma.manager.LoadRecipeBooks()
}
// PlayerLearnRecipe handles complete recipe learning workflow
func (rma *RecipeManagerAdapter) PlayerLearnRecipe(characterID int32, recipeID int32) error {
// Validate recipe exists
recipe := rma.manager.GetRecipe(recipeID)
if recipe == nil {
return ErrRecipeNotFound
}
// Check player can learn recipe
if rma.dependencies.Player != nil {
if !rma.dependencies.Player.CanPlayerLearnRecipe(characterID, recipeID) {
return ErrCannotLearnRecipe
}
}
// Save recipe to database
if err := rma.manager.SavePlayerRecipe(characterID, recipeID); err != nil {
return err
}
// Notify client
if rma.dependencies.Client != nil {
if err := rma.dependencies.Client.SendRecipeLearned(characterID, recipeID); err != nil {
return err
}
}
// Fire event
if rma.dependencies.Event != nil {
return rma.dependencies.Event.OnRecipeLearned(characterID, recipeID)
}
return nil
}
// PlayerObtainRecipeBook handles complete recipe book acquisition workflow
func (rma *RecipeManagerAdapter) PlayerObtainRecipeBook(characterID int32, bookID int32) error {
// Validate recipe book exists
book := rma.manager.GetRecipeBook(bookID)
if book == nil {
return ErrRecipeBookNotFound
}
// Check player can use recipe book
if rma.dependencies.Player != nil {
if !rma.dependencies.Player.CanPlayerUseRecipeBook(characterID, bookID) {
return ErrCannotUseRecipeBook
}
}
// Save recipe book to database
if err := rma.manager.SavePlayerRecipeBook(characterID, bookID); err != nil {
return err
}
// Consume recipe book item
if rma.dependencies.Item != nil {
if err := rma.dependencies.Item.ConsumeRecipeBook(characterID, bookID); err != nil {
return err
}
}
// Notify client
if rma.dependencies.Client != nil {
if err := rma.dependencies.Client.SendRecipeBookObtained(characterID, bookID); err != nil {
return err
}
}
// Fire event
if rma.dependencies.Event != nil {
return rma.dependencies.Event.OnRecipeBookObtained(characterID, bookID)
}
return nil
}
// ValidateRecipeSystem performs comprehensive system validation
func (rma *RecipeManagerAdapter) ValidateRecipeSystem() []string {
issues := rma.manager.Validate()
// Add dependency validation
if rma.dependencies == nil {
issues = append(issues, "recipe system dependencies not configured")
return issues
}
if rma.dependencies.Database == nil {
issues = append(issues, "database adapter not configured")
}
if rma.dependencies.Player == nil {
issues = append(issues, "player adapter not configured")
}
if rma.dependencies.Item == nil {
issues = append(issues, "item adapter not configured")
}
if rma.dependencies.Client == nil {
issues = append(issues, "client adapter not configured")
}
return issues
}

565
internal/recipes/manager.go Normal file
View File

@ -0,0 +1,565 @@
package recipes
import (
"fmt"
"sync"
"eq2emu/internal/database"
)
// RecipeManager provides high-level recipe system management with database integration
type RecipeManager struct {
db *database.DB
masterRecipeList *MasterRecipeList
masterRecipeBookList *MasterRecipeBookList
loadedRecipes map[int32]*Recipe
loadedRecipeBooks map[int32]*Recipe
mu sync.RWMutex
statisticsEnabled bool
// Statistics
stats RecipeManagerStats
}
// RecipeManagerStats tracks recipe system usage and performance metrics
type RecipeManagerStats struct {
TotalRecipesLoaded int32
TotalRecipeBooksLoaded int32
PlayersWithRecipes int32
LoadOperations int32
SaveOperations int32
mu sync.RWMutex
}
// NewRecipeManager creates a new recipe manager with database integration
func NewRecipeManager(db *database.DB) *RecipeManager {
return &RecipeManager{
db: db,
masterRecipeList: NewMasterRecipeList(),
masterRecipeBookList: NewMasterRecipeBookList(),
loadedRecipes: make(map[int32]*Recipe),
loadedRecipeBooks: make(map[int32]*Recipe),
statisticsEnabled: true,
}
}
// LoadRecipes loads all recipes from the database with complex component relationships
func (rm *RecipeManager) LoadRecipes() error {
rm.mu.Lock()
defer rm.mu.Unlock()
query := `SELECT r.id, r.soe_id, r.level, r.icon, r.skill_level, r.technique, r.knowledge,
r.name, r.description, i.name as book, r.bench, ipc.adventure_classes,
r.stage4_id, r.name, r.stage4_qty, pcl.name as primary_comp_title, r.primary_comp_qty,
fcl.name as fuel_comp_title, r.fuel_comp_qty,
bc.name AS build_comp_title, bc.qty AS build_comp_qty,
bc2.name AS build2_comp_title, bc2.qty AS build2_comp_qty,
bc3.name AS build3_comp_title, bc3.qty AS build3_comp_qty,
bc4.name AS build4_comp_title, bc4.qty AS build4_comp_qty,
r.stage0_id, r.stage1_id, r.stage2_id, r.stage3_id, r.stage4_id,
r.stage0_qty, r.stage1_qty, r.stage2_qty, r.stage3_qty, r.stage4_qty,
r.stage0_byp_id, r.stage1_byp_id, r.stage2_byp_id, r.stage3_byp_id, r.stage4_byp_id,
r.stage0_byp_qty, r.stage1_byp_qty, r.stage2_byp_qty, r.stage3_byp_qty, r.stage4_byp_qty
FROM recipe r
LEFT JOIN ((SELECT recipe_id, soe_recipe_crc FROM item_details_recipe_items GROUP BY soe_recipe_crc) as idri) ON idri.soe_recipe_crc = r.soe_id
LEFT JOIN items i ON idri.recipe_id = i.id
INNER JOIN items ipc ON r.stage4_id = ipc.id
INNER JOIN recipe_comp_list pcl ON r.primary_comp_list = pcl.id
INNER JOIN recipe_comp_list fcl ON r.fuel_comp_list = fcl.id
LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 0) AS bc ON bc.recipe_id = r.id
LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 1) AS bc2 ON bc2.recipe_id = r.id
LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 2) AS bc3 ON bc3.recipe_id = r.id
LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 3) AS bc4 ON bc4.recipe_id = r.id
WHERE r.bHaveAllProducts`
loadedCount := int32(0)
err := rm.db.Query(query, func(row *database.Row) error {
recipe := NewRecipe()
// Column index for scanning
col := 0
recipe.ID = int32(row.Int(col))
col++
recipe.SoeID = int32(row.Int(col))
col++
recipe.Level = int8(row.Int(col))
col++
recipe.Icon = int16(row.Int(col))
col++
recipe.Skill = int32(row.Int(col))
col++
recipe.Technique = int32(row.Int(col))
col++
recipe.Knowledge = int32(row.Int(col))
col++
recipe.Name = row.Text(col)
col++
recipe.Description = row.Text(col)
col++
// Book name (nullable)
if !row.IsNull(col) {
recipe.Book = row.Text(col)
}
col++
// Device (nullable)
if !row.IsNull(col) {
recipe.Device = row.Text(col)
}
col++
recipe.Classes = int32(row.Int(col))
col++
// Product information
recipe.ProductItemID = int32(row.Int(col))
col++
recipe.ProductName = row.Text(col)
col++
recipe.ProductQty = int8(row.Int(col))
col++
// Component titles and quantities
if !row.IsNull(col) {
recipe.PrimaryBuildCompTitle = row.Text(col)
}
col++
recipe.PrimaryCompQty = int16(row.Int(col))
col++
if !row.IsNull(col) {
recipe.FuelCompTitle = row.Text(col)
}
col++
recipe.FuelCompQty = int16(row.Int(col))
col++
// Build component titles and quantities
if !row.IsNull(col) {
recipe.Build1CompTitle = row.Text(col)
}
col++
if !row.IsNull(col + 1) {
recipe.Build1CompQty = int16(row.Int(col + 1))
}
col += 2
if !row.IsNull(col) {
recipe.Build2CompTitle = row.Text(col)
}
col++
if !row.IsNull(col) {
recipe.Build2CompQty = int16(row.Int(col))
}
col++
if !row.IsNull(col) {
recipe.Build3CompTitle = row.Text(col)
}
col++
if !row.IsNull(col) {
recipe.Build3CompQty = int16(row.Int(col))
}
col++
if !row.IsNull(col) {
recipe.Build4CompTitle = row.Text(col)
}
col++
if !row.IsNull(col) {
recipe.Build4CompQty = int16(row.Int(col))
}
col++
// Set tier based on level (C++ logic: level / 10 + 1)
recipe.Tier = recipe.Level/10 + 1
// Initialize products for all stages
for stage := int8(0); stage < 5; stage++ {
stageID := int32(row.Int(col))
stageQty := int8(row.Int(col + 5))
bypassID := int32(row.Int(col + 10))
bypassQty := int8(row.Int(col + 15))
recipe.Products[stage] = &RecipeProducts{
ProductID: stageID,
ProductQty: stageQty,
ByproductID: bypassID,
ByproductQty: bypassQty,
}
col++
}
if rm.masterRecipeList.AddRecipe(recipe) {
rm.loadedRecipes[recipe.ID] = recipe
loadedCount++
}
return nil
})
if err != nil {
return fmt.Errorf("failed to load recipes: %w", err)
}
// Load recipe components after loading recipes
if err := rm.loadRecipeComponents(); err != nil {
return fmt.Errorf("failed to load recipe components: %w", err)
}
// Update statistics
if rm.statisticsEnabled {
rm.stats.mu.Lock()
rm.stats.TotalRecipesLoaded = loadedCount
rm.stats.LoadOperations++
rm.stats.mu.Unlock()
}
return nil
}
// loadRecipeComponents loads component relationships for recipes
func (rm *RecipeManager) loadRecipeComponents() error {
query := `SELECT r.id, pc.item_id AS primary_comp, fc.item_id AS fuel_comp,
sc.item_id as secondary_comp, rsc.index + 1 AS slot
FROM recipe r
INNER JOIN (select comp_list, item_id FROM recipe_comp_list_item) as pc ON r.primary_comp_list = pc.comp_list
INNER JOIN (select comp_list, item_id FROM recipe_comp_list_item) as fc ON r.fuel_comp_list = fc.comp_list
LEFT JOIN recipe_secondary_comp rsc ON rsc.recipe_id = r.id
LEFT JOIN (select comp_list, item_id FROM recipe_comp_list_item) as sc ON rsc.comp_list = sc.comp_list
WHERE r.bHaveAllProducts
ORDER BY r.id, rsc.index ASC`
var currentRecipeID int32
var currentRecipe *Recipe
err := rm.db.Query(query, func(row *database.Row) error {
recipeID := int32(row.Int(0))
primaryComp := int32(row.Int(1))
fuelComp := int32(row.Int(2))
var secondaryComp int32
var slot int8
if !row.IsNull(3) {
secondaryComp = int32(row.Int(3))
}
if !row.IsNull(4) {
slot = int8(row.Int(4))
}
// Get the recipe if it's different from the current one
if currentRecipeID != recipeID {
currentRecipeID = recipeID
currentRecipe = rm.masterRecipeList.GetRecipe(recipeID)
if currentRecipe == nil {
return nil
}
}
if currentRecipe != nil && !row.IsNull(3) && !row.IsNull(4) {
// Add primary component (slot 0)
if !rm.containsComponent(currentRecipe.Components[0], primaryComp) {
currentRecipe.AddBuildComponent(primaryComp, 0, false)
}
// Add fuel component (slot 5)
if !rm.containsComponent(currentRecipe.Components[5], fuelComp) {
currentRecipe.AddBuildComponent(fuelComp, 5, false)
}
// Add secondary component to appropriate slot
if slot >= 1 && slot <= 4 {
if !rm.containsComponent(currentRecipe.Components[slot], secondaryComp) {
currentRecipe.AddBuildComponent(secondaryComp, slot, false)
}
}
}
return nil
})
return err
}
// containsComponent checks if a component ID exists in the component slice
func (rm *RecipeManager) containsComponent(components []int32, componentID int32) bool {
for _, comp := range components {
if comp == componentID {
return true
}
}
return false
}
// LoadRecipeBooks loads all recipe books from the database
func (rm *RecipeManager) LoadRecipeBooks() error {
rm.mu.Lock()
defer rm.mu.Unlock()
query := `SELECT id, name, tradeskill_default_level FROM items WHERE item_type='Recipe'`
loadedCount := int32(0)
err := rm.db.Query(query, func(row *database.Row) error {
recipe := NewRecipe()
recipe.BookID = int32(row.Int(0))
recipe.BookName = row.Text(1)
recipe.Level = int8(row.Int(2))
if rm.masterRecipeBookList.AddRecipeBook(recipe) {
rm.loadedRecipeBooks[recipe.BookID] = recipe
loadedCount++
}
return nil
})
if err != nil {
return fmt.Errorf("failed to load recipe books: %w", err)
}
// Update statistics
if rm.statisticsEnabled {
rm.stats.mu.Lock()
rm.stats.TotalRecipeBooksLoaded = loadedCount
rm.stats.LoadOperations++
rm.stats.mu.Unlock()
}
return nil
}
// LoadPlayerRecipes loads recipes for a specific player from the database
func (rm *RecipeManager) LoadPlayerRecipes(playerRecipeList *PlayerRecipeList, characterID int32) error {
rm.mu.RLock()
defer rm.mu.RUnlock()
query := `SELECT recipe_id, highest_stage FROM character_recipes WHERE char_id = ?`
loadedCount := 0
err := rm.db.Query(query, func(row *database.Row) error {
recipeID := int32(row.Int(0))
highestStage := int8(row.Int(1))
// Get master recipe
masterRecipe := rm.masterRecipeList.GetRecipe(recipeID)
if masterRecipe == nil {
return nil
}
// Create player copy of recipe
playerRecipe := NewRecipeFromRecipe(masterRecipe)
playerRecipe.HighestStage = highestStage
if playerRecipeList.AddRecipe(playerRecipe) {
loadedCount++
}
return nil
}, characterID)
if err != nil {
return fmt.Errorf("failed to load player recipes: %w", err)
}
// Update statistics
if rm.statisticsEnabled {
rm.stats.mu.Lock()
rm.stats.PlayersWithRecipes++
rm.stats.LoadOperations++
rm.stats.mu.Unlock()
}
return nil
}
// LoadPlayerRecipeBooks loads recipe books for a specific player from the database
func (rm *RecipeManager) LoadPlayerRecipeBooks(playerRecipeBookList *PlayerRecipeBookList, characterID int32) (int32, error) {
rm.mu.RLock()
defer rm.mu.RUnlock()
query := `SELECT recipebook_id FROM character_recipe_books WHERE char_id = ? ORDER BY recipebook_id`
count := int32(0)
var lastID int32
err := rm.db.Query(query, func(row *database.Row) error {
recipebookID := int32(row.Int(0))
// Skip duplicates
if recipebookID == lastID {
return nil
}
// Create recipe book entry
recipe := NewRecipe()
recipe.BookID = recipebookID
recipe.BookName = fmt.Sprintf("Recipe Book %d", recipebookID) // TODO: Get actual name from items table
if playerRecipeBookList.AddRecipeBook(recipe) {
count++
}
lastID = recipebookID
return nil
}, characterID)
if err != nil {
return 0, fmt.Errorf("failed to load player recipe books: %w", err)
}
return count, nil
}
// SavePlayerRecipeBook saves a player's recipe book to the database
func (rm *RecipeManager) SavePlayerRecipeBook(characterID int32, recipebookID int32) error {
query := `INSERT INTO character_recipe_books (char_id, recipebook_id) VALUES (?, ?)`
err := rm.db.Exec(query, characterID, recipebookID)
if err != nil {
return fmt.Errorf("failed to save player recipe book: %w", err)
}
// Update statistics
if rm.statisticsEnabled {
rm.stats.mu.Lock()
rm.stats.SaveOperations++
rm.stats.mu.Unlock()
}
return nil
}
// SavePlayerRecipe saves a player's recipe to the database
func (rm *RecipeManager) SavePlayerRecipe(characterID int32, recipeID int32) error {
query := `INSERT INTO character_recipes (char_id, recipe_id) VALUES (?, ?)`
err := rm.db.Exec(query, characterID, recipeID)
if err != nil {
return fmt.Errorf("failed to save player recipe: %w", err)
}
// Update statistics
if rm.statisticsEnabled {
rm.stats.mu.Lock()
rm.stats.SaveOperations++
rm.stats.mu.Unlock()
}
return nil
}
// UpdatePlayerRecipe updates a player's recipe progress in the database
func (rm *RecipeManager) UpdatePlayerRecipe(characterID int32, recipeID int32, highestStage int8) error {
query := `UPDATE character_recipes SET highest_stage = ? WHERE char_id = ? AND recipe_id = ?`
err := rm.db.Exec(query, highestStage, characterID, recipeID)
if err != nil {
return fmt.Errorf("failed to update player recipe: %w", err)
}
// Update statistics
if rm.statisticsEnabled {
rm.stats.mu.Lock()
rm.stats.SaveOperations++
rm.stats.mu.Unlock()
}
return nil
}
// GetMasterRecipeList returns the master recipe list
func (rm *RecipeManager) GetMasterRecipeList() *MasterRecipeList {
rm.mu.RLock()
defer rm.mu.RUnlock()
return rm.masterRecipeList
}
// GetMasterRecipeBookList returns the master recipe book list
func (rm *RecipeManager) GetMasterRecipeBookList() *MasterRecipeBookList {
rm.mu.RLock()
defer rm.mu.RUnlock()
return rm.masterRecipeBookList
}
// GetRecipe retrieves a recipe by ID from the master list
func (rm *RecipeManager) GetRecipe(recipeID int32) *Recipe {
rm.mu.RLock()
defer rm.mu.RUnlock()
return rm.masterRecipeList.GetRecipe(recipeID)
}
// GetRecipeBook retrieves a recipe book by ID
func (rm *RecipeManager) GetRecipeBook(bookID int32) *Recipe {
rm.mu.RLock()
defer rm.mu.RUnlock()
return rm.loadedRecipeBooks[bookID]
}
// GetStatistics returns current recipe system statistics
func (rm *RecipeManager) GetStatistics() RecipeManagerStats {
if !rm.statisticsEnabled {
return RecipeManagerStats{}
}
rm.stats.mu.RLock()
defer rm.stats.mu.RUnlock()
return rm.stats
}
// SetStatisticsEnabled enables or disables statistics collection
func (rm *RecipeManager) SetStatisticsEnabled(enabled bool) {
rm.mu.Lock()
defer rm.mu.Unlock()
rm.statisticsEnabled = enabled
}
// Validate performs comprehensive recipe system validation
func (rm *RecipeManager) Validate() []string {
rm.mu.RLock()
defer rm.mu.RUnlock()
var issues []string
// Validate master recipe list
if rm.masterRecipeList == nil {
issues = append(issues, "master recipe list is nil")
return issues
}
// Validate master recipe book list
if rm.masterRecipeBookList == nil {
issues = append(issues, "master recipe book list is nil")
}
// Check for recipes with invalid data
for _, recipe := range rm.loadedRecipes {
if recipe.ID == 0 {
issues = append(issues, fmt.Sprintf("recipe has invalid ID: %s", recipe.Name))
}
if recipe.Name == "" {
issues = append(issues, fmt.Sprintf("recipe %d has empty name", recipe.ID))
}
if recipe.Level < 0 || recipe.Level > 100 {
issues = append(issues, fmt.Sprintf("recipe %d has invalid level: %d", recipe.ID, recipe.Level))
}
if len(recipe.Products) != 5 {
issues = append(issues, fmt.Sprintf("recipe %d has invalid products array length: %d", recipe.ID, len(recipe.Products)))
}
if len(recipe.Components) != 6 {
issues = append(issues, fmt.Sprintf("recipe %d has invalid components array length: %d", recipe.ID, len(recipe.Components)))
}
}
return issues
}
// Size returns the total count of loaded recipes
func (rm *RecipeManager) Size() (recipes int32, recipeBooks int32) {
rm.mu.RLock()
defer rm.mu.RUnlock()
return int32(len(rm.loadedRecipes)), int32(len(rm.loadedRecipeBooks))
}

View File

@ -0,0 +1,396 @@
package recipes
import (
"strings"
"sync"
)
// MasterRecipeList manages all recipes in the system
// Converted from C++ MasterRecipeList class
type MasterRecipeList struct {
recipes map[int32]*Recipe // Recipe ID -> Recipe
recipesCRC map[int32]*Recipe // SOE CRC ID -> Recipe
nameIndex map[string]*Recipe // Lowercase name -> Recipe
bookIndex map[string][]*Recipe // Lowercase book name -> Recipes
skillIndex map[int32][]*Recipe // Skill ID -> Recipes
tierIndex map[int8][]*Recipe // Tier -> Recipes
mutex sync.RWMutex
stats *Statistics
}
// NewMasterRecipeList creates a new master recipe list
// Converted from C++ MasterRecipeList::MasterRecipeList constructor
func NewMasterRecipeList() *MasterRecipeList {
return &MasterRecipeList{
recipes: make(map[int32]*Recipe),
recipesCRC: make(map[int32]*Recipe),
nameIndex: make(map[string]*Recipe),
bookIndex: make(map[string][]*Recipe),
skillIndex: make(map[int32][]*Recipe),
tierIndex: make(map[int8][]*Recipe),
stats: NewStatistics(),
}
}
// AddRecipe adds a recipe to the master list
// Converted from C++ MasterRecipeList::AddRecipe
func (mrl *MasterRecipeList) AddRecipe(recipe *Recipe) bool {
if recipe == nil || !recipe.IsValid() {
return false
}
mrl.mutex.Lock()
defer mrl.mutex.Unlock()
// Check for duplicate ID
if _, exists := mrl.recipes[recipe.ID]; exists {
return false
}
// Add to main map
mrl.recipes[recipe.ID] = recipe
// Add to CRC map if SOE ID is set
if recipe.SoeID != 0 {
mrl.recipesCRC[recipe.SoeID] = recipe
}
// Add to name index
nameLower := strings.ToLower(strings.TrimSpace(recipe.Name))
if nameLower != "" {
mrl.nameIndex[nameLower] = recipe
}
// Add to book index
bookLower := strings.ToLower(strings.TrimSpace(recipe.Book))
if bookLower != "" {
mrl.bookIndex[bookLower] = append(mrl.bookIndex[bookLower], recipe)
}
// Add to skill index
if recipe.Skill != 0 {
mrl.skillIndex[recipe.Skill] = append(mrl.skillIndex[recipe.Skill], recipe)
}
// Add to tier index
if recipe.Tier > 0 {
mrl.tierIndex[recipe.Tier] = append(mrl.tierIndex[recipe.Tier], recipe)
}
// Update statistics
mrl.stats.TotalRecipes++
mrl.stats.RecipesByTier[recipe.Tier]++
mrl.stats.RecipesBySkill[recipe.Skill]++
return true
}
// GetRecipe retrieves a recipe by ID
// Converted from C++ MasterRecipeList::GetRecipe
func (mrl *MasterRecipeList) GetRecipe(recipeID int32) *Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
if recipe, exists := mrl.recipes[recipeID]; exists {
return recipe
}
return nil
}
// GetRecipeByCRC retrieves a recipe by SOE CRC ID
// Converted from C++ MasterRecipeList::GetRecipeByCRC
func (mrl *MasterRecipeList) GetRecipeByCRC(recipeCRC int32) *Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
if recipe, exists := mrl.recipesCRC[recipeCRC]; exists {
return recipe
}
return nil
}
// GetRecipeByName retrieves a recipe by name (case-insensitive)
// Converted from C++ MasterRecipeList::GetRecipeByName
func (mrl *MasterRecipeList) GetRecipeByName(name string) *Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
nameLower := strings.ToLower(strings.TrimSpace(name))
if recipe, exists := mrl.nameIndex[nameLower]; exists {
return recipe
}
return nil
}
// GetRecipesByBook retrieves all recipes for a given book name
// Converted from C++ MasterRecipeList::GetRecipes
func (mrl *MasterRecipeList) GetRecipesByBook(bookName string) []*Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
bookLower := strings.ToLower(strings.TrimSpace(bookName))
if recipes, exists := mrl.bookIndex[bookLower]; exists {
// Return a copy to prevent external modification
result := make([]*Recipe, len(recipes))
copy(result, recipes)
return result
}
return nil
}
// GetRecipesBySkill retrieves all recipes for a given skill
func (mrl *MasterRecipeList) GetRecipesBySkill(skillID int32) []*Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
if recipes, exists := mrl.skillIndex[skillID]; exists {
// Return a copy to prevent external modification
result := make([]*Recipe, len(recipes))
copy(result, recipes)
return result
}
return nil
}
// GetRecipesByTier retrieves all recipes for a given tier
func (mrl *MasterRecipeList) GetRecipesByTier(tier int8) []*Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
if recipes, exists := mrl.tierIndex[tier]; exists {
// Return a copy to prevent external modification
result := make([]*Recipe, len(recipes))
copy(result, recipes)
return result
}
return nil
}
// GetRecipesByClass retrieves all recipes that can be used by a tradeskill class
func (mrl *MasterRecipeList) GetRecipesByClass(classID int8) []*Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
var result []*Recipe
for _, recipe := range mrl.recipes {
if recipe.CanUseRecipeByClass(classID) {
result = append(result, recipe)
}
}
return result
}
// GetRecipesByLevel retrieves all recipes within a level range
func (mrl *MasterRecipeList) GetRecipesByLevel(minLevel, maxLevel int8) []*Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
mrl.stats.IncrementRecipeLookups()
var result []*Recipe
for _, recipe := range mrl.recipes {
if recipe.Level >= minLevel && recipe.Level <= maxLevel {
result = append(result, recipe)
}
}
return result
}
// RemoveRecipe removes a recipe from the master list
func (mrl *MasterRecipeList) RemoveRecipe(recipeID int32) bool {
mrl.mutex.Lock()
defer mrl.mutex.Unlock()
recipe, exists := mrl.recipes[recipeID]
if !exists {
return false
}
// Remove from main map
delete(mrl.recipes, recipeID)
// Remove from CRC map
if recipe.SoeID != 0 {
delete(mrl.recipesCRC, recipe.SoeID)
}
// Remove from name index
nameLower := strings.ToLower(strings.TrimSpace(recipe.Name))
if nameLower != "" {
delete(mrl.nameIndex, nameLower)
}
// Remove from book index
bookLower := strings.ToLower(strings.TrimSpace(recipe.Book))
if bookLower != "" {
if recipes, exists := mrl.bookIndex[bookLower]; exists {
for i, r := range recipes {
if r.ID == recipeID {
mrl.bookIndex[bookLower] = append(recipes[:i], recipes[i+1:]...)
break
}
}
if len(mrl.bookIndex[bookLower]) == 0 {
delete(mrl.bookIndex, bookLower)
}
}
}
// Remove from skill index
if recipe.Skill != 0 {
if recipes, exists := mrl.skillIndex[recipe.Skill]; exists {
for i, r := range recipes {
if r.ID == recipeID {
mrl.skillIndex[recipe.Skill] = append(recipes[:i], recipes[i+1:]...)
break
}
}
if len(mrl.skillIndex[recipe.Skill]) == 0 {
delete(mrl.skillIndex, recipe.Skill)
}
}
}
// Remove from tier index
if recipe.Tier > 0 {
if recipes, exists := mrl.tierIndex[recipe.Tier]; exists {
for i, r := range recipes {
if r.ID == recipeID {
mrl.tierIndex[recipe.Tier] = append(recipes[:i], recipes[i+1:]...)
break
}
}
if len(mrl.tierIndex[recipe.Tier]) == 0 {
delete(mrl.tierIndex, recipe.Tier)
}
}
}
// Update statistics
mrl.stats.TotalRecipes--
mrl.stats.RecipesByTier[recipe.Tier]--
mrl.stats.RecipesBySkill[recipe.Skill]--
return true
}
// ClearRecipes removes all recipes from the master list
// Converted from C++ MasterRecipeList::ClearRecipes
func (mrl *MasterRecipeList) ClearRecipes() {
mrl.mutex.Lock()
defer mrl.mutex.Unlock()
mrl.recipes = make(map[int32]*Recipe)
mrl.recipesCRC = make(map[int32]*Recipe)
mrl.nameIndex = make(map[string]*Recipe)
mrl.bookIndex = make(map[string][]*Recipe)
mrl.skillIndex = make(map[int32][]*Recipe)
mrl.tierIndex = make(map[int8][]*Recipe)
// Reset statistics
mrl.stats.TotalRecipes = 0
mrl.stats.RecipesByTier = make(map[int8]int32)
mrl.stats.RecipesBySkill = make(map[int32]int32)
}
// Size returns the total number of recipes
// Converted from C++ MasterRecipeList::Size
func (mrl *MasterRecipeList) Size() int32 {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
return int32(len(mrl.recipes))
}
// GetAllRecipes returns all recipes (use with caution for large lists)
func (mrl *MasterRecipeList) GetAllRecipes() map[int32]*Recipe {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Recipe)
for id, recipe := range mrl.recipes {
result[id] = recipe
}
return result
}
// GetRecipeIDs returns all recipe IDs
func (mrl *MasterRecipeList) GetRecipeIDs() []int32 {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
result := make([]int32, 0, len(mrl.recipes))
for id := range mrl.recipes {
result = append(result, id)
}
return result
}
// GetStatistics returns a snapshot of the current statistics
func (mrl *MasterRecipeList) GetStatistics() Statistics {
return mrl.stats.GetSnapshot()
}
// GetSkills returns all skills that have recipes
func (mrl *MasterRecipeList) GetSkills() []int32 {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
result := make([]int32, 0, len(mrl.skillIndex))
for skill := range mrl.skillIndex {
result = append(result, skill)
}
return result
}
// GetTiers returns all tiers that have recipes
func (mrl *MasterRecipeList) GetTiers() []int8 {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
result := make([]int8, 0, len(mrl.tierIndex))
for tier := range mrl.tierIndex {
result = append(result, tier)
}
return result
}
// GetBookNames returns all book names that have recipes
func (mrl *MasterRecipeList) GetBookNames() []string {
mrl.mutex.RLock()
defer mrl.mutex.RUnlock()
result := make([]string, 0, len(mrl.bookIndex))
for book := range mrl.bookIndex {
result = append(result, book)
}
return result
}

362
internal/recipes/recipe.go Normal file
View File

@ -0,0 +1,362 @@
package recipes
import (
"fmt"
"strings"
"sync"
)
// NewRecipe creates a new recipe with default values
// Converted from C++ Recipe::Recipe constructor
func NewRecipe() *Recipe {
return &Recipe{
Components: make(map[int8][]int32),
Products: make(map[int8]*RecipeProducts),
}
}
// NewRecipeFromRecipe creates a copy of another recipe
// Converted from C++ Recipe::Recipe(Recipe *in) copy constructor
func NewRecipeFromRecipe(source *Recipe) *Recipe {
if source == nil {
return NewRecipe()
}
source.mutex.RLock()
defer source.mutex.RUnlock()
recipe := &Recipe{
// Core data
ID: source.ID,
SoeID: source.SoeID,
BookID: source.BookID,
Name: source.Name,
Description: source.Description,
BookName: source.BookName,
Book: source.Book,
Device: source.Device,
// Properties
Level: source.Level,
Tier: source.Tier,
Icon: source.Icon,
Skill: source.Skill,
Technique: source.Technique,
Knowledge: source.Knowledge,
Classes: source.Classes,
DeviceSubType: source.DeviceSubType,
// Unknown fields
Unknown1: source.Unknown1,
Unknown2: source.Unknown2,
Unknown3: source.Unknown3,
Unknown4: source.Unknown4,
// Product information
ProductItemID: source.ProductItemID,
ProductName: source.ProductName,
ProductQty: source.ProductQty,
// Component titles
PrimaryBuildCompTitle: source.PrimaryBuildCompTitle,
Build1CompTitle: source.Build1CompTitle,
Build2CompTitle: source.Build2CompTitle,
Build3CompTitle: source.Build3CompTitle,
Build4CompTitle: source.Build4CompTitle,
FuelCompTitle: source.FuelCompTitle,
// Component quantities
Build1CompQty: source.Build1CompQty,
Build2CompQty: source.Build2CompQty,
Build3CompQty: source.Build3CompQty,
Build4CompQty: source.Build4CompQty,
FuelCompQty: source.FuelCompQty,
PrimaryCompQty: source.PrimaryCompQty,
// Stage information
HighestStage: source.HighestStage,
// Initialize maps
Components: make(map[int8][]int32),
Products: make(map[int8]*RecipeProducts),
}
// Deep copy components
for slot, components := range source.Components {
recipe.Components[slot] = make([]int32, len(components))
copy(recipe.Components[slot], components)
}
// Deep copy products
for stage, products := range source.Products {
if products != nil {
recipe.Products[stage] = &RecipeProducts{
ProductID: products.ProductID,
ByproductID: products.ByproductID,
ProductQty: products.ProductQty,
ByproductQty: products.ByproductQty,
}
}
}
return recipe
}
// AddBuildComponent adds a component to a specific slot for this recipe
// Converted from C++ Recipe::AddBuildComp
func (r *Recipe) AddBuildComponent(itemID int32, slot int8, preferred bool) {
r.mutex.Lock()
defer r.mutex.Unlock()
if slot < 0 || slot >= MaxSlots {
return
}
// Initialize the slot if it doesn't exist
if r.Components[slot] == nil {
r.Components[slot] = make([]int32, 0)
}
// Check if the item is already in this slot
for _, existingID := range r.Components[slot] {
if existingID == itemID {
return // Already exists
}
}
// Add the component
if preferred {
// Add at the beginning for preferred components
r.Components[slot] = append([]int32{itemID}, r.Components[slot]...)
} else {
r.Components[slot] = append(r.Components[slot], itemID)
}
}
// GetTotalBuildComponents returns the total number of component slots used
// Converted from C++ Recipe::GetTotalBuildComponents
func (r *Recipe) GetTotalBuildComponents() int8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
count := int8(0)
for slot := SlotBuild1; slot <= SlotBuild4; slot++ {
if len(r.Components[slot]) > 0 {
count++
}
}
return count
}
// GetItemRequiredQuantity returns the required quantity for a specific item
// Converted from C++ Recipe::GetItemRequiredQuantity
func (r *Recipe) GetItemRequiredQuantity(itemID int32) int8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
// Check each slot for the item
for slot, components := range r.Components {
for _, componentID := range components {
if componentID == itemID {
// Return the quantity based on the slot
switch slot {
case SlotPrimary:
return int8(r.PrimaryCompQty)
case SlotBuild1:
return int8(r.Build1CompQty)
case SlotBuild2:
return int8(r.Build2CompQty)
case SlotBuild3:
return int8(r.Build3CompQty)
case SlotBuild4:
return int8(r.Build4CompQty)
case SlotFuel:
return int8(r.FuelCompQty)
}
}
}
}
return 0 // Not found
}
// CanUseRecipeByClass checks if a tradeskill class can use this recipe
// Converted from C++ Recipe::CanUseRecipeByClass (simplified)
func (r *Recipe) CanUseRecipeByClass(classID int8) bool {
r.mutex.RLock()
defer r.mutex.RUnlock()
// Any can use: bit combination of 1+2 (adornments + artisan)
if r.Classes < 4 {
return true
}
// Check if the class bit is set
return (1<<classID)&r.Classes != 0
}
// IsValid checks if the recipe has valid data
func (r *Recipe) IsValid() bool {
r.mutex.RLock()
defer r.mutex.RUnlock()
if r.ID < MinRecipeID || r.ID > MaxRecipeID {
return false
}
if strings.TrimSpace(r.Name) == "" {
return false
}
if r.Level < MinRecipeLevel || r.Level > MaxRecipeLevel {
return false
}
if r.Tier < MinTier || r.Tier > MaxTier {
return false
}
return true
}
// GetComponentsBySlot returns the component items for a specific slot
func (r *Recipe) GetComponentsBySlot(slot int8) []int32 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if components, exists := r.Components[slot]; exists {
// Return a copy to prevent external modification
result := make([]int32, len(components))
copy(result, components)
return result
}
return nil
}
// GetProductsForStage returns the products for a specific crafting stage
func (r *Recipe) GetProductsForStage(stage int8) *RecipeProducts {
r.mutex.RLock()
defer r.mutex.RUnlock()
if products, exists := r.Products[stage]; exists {
// Return a copy to prevent external modification
return &RecipeProducts{
ProductID: products.ProductID,
ByproductID: products.ByproductID,
ProductQty: products.ProductQty,
ByproductQty: products.ByproductQty,
}
}
return nil
}
// SetProductsForStage sets the products for a specific crafting stage
func (r *Recipe) SetProductsForStage(stage int8, products *RecipeProducts) {
r.mutex.Lock()
defer r.mutex.Unlock()
if stage < Stage0 || stage > Stage4 {
return
}
if products == nil {
delete(r.Products, stage)
return
}
r.Products[stage] = &RecipeProducts{
ProductID: products.ProductID,
ByproductID: products.ByproductID,
ProductQty: products.ProductQty,
ByproductQty: products.ByproductQty,
}
}
// GetComponentTitleForSlot returns the component title for a specific slot
func (r *Recipe) GetComponentTitleForSlot(slot int8) string {
r.mutex.RLock()
defer r.mutex.RUnlock()
switch slot {
case SlotPrimary:
return r.PrimaryBuildCompTitle
case SlotBuild1:
return r.Build1CompTitle
case SlotBuild2:
return r.Build2CompTitle
case SlotBuild3:
return r.Build3CompTitle
case SlotBuild4:
return r.Build4CompTitle
case SlotFuel:
return r.FuelCompTitle
default:
return ""
}
}
// GetComponentQuantityForSlot returns the component quantity for a specific slot
func (r *Recipe) GetComponentQuantityForSlot(slot int8) int16 {
r.mutex.RLock()
defer r.mutex.RUnlock()
switch slot {
case SlotPrimary:
return r.PrimaryCompQty
case SlotBuild1:
return r.Build1CompQty
case SlotBuild2:
return r.Build2CompQty
case SlotBuild3:
return r.Build3CompQty
case SlotBuild4:
return r.Build4CompQty
case SlotFuel:
return r.FuelCompQty
default:
return 0
}
}
// GetInfo returns comprehensive information about the recipe
func (r *Recipe) GetInfo() map[string]interface{} {
r.mutex.RLock()
defer r.mutex.RUnlock()
info := make(map[string]interface{})
info["id"] = r.ID
info["soe_id"] = r.SoeID
info["book_id"] = r.BookID
info["name"] = r.Name
info["description"] = r.Description
info["book_name"] = r.BookName
info["book"] = r.Book
info["device"] = r.Device
info["level"] = r.Level
info["tier"] = r.Tier
info["icon"] = r.Icon
info["skill"] = r.Skill
info["technique"] = r.Technique
info["knowledge"] = r.Knowledge
info["classes"] = r.Classes
info["product_id"] = r.ProductItemID
info["product_name"] = r.ProductName
info["product_qty"] = r.ProductQty
info["highest_stage"] = r.HighestStage
info["total_components"] = r.GetTotalBuildComponents()
info["valid"] = r.IsValid()
return info
}
// String returns a string representation of the recipe
func (r *Recipe) String() string {
r.mutex.RLock()
defer r.mutex.RUnlock()
return fmt.Sprintf("Recipe{ID: %d, Name: %s, Level: %d, Tier: %d, Skill: %d}",
r.ID, r.Name, r.Level, r.Tier, r.Skill)
}

View File

@ -0,0 +1,309 @@
package recipes
import (
"sync"
)
// MasterRecipeBookList manages all recipe books in the system
// Converted from C++ MasterRecipeBookList class
type MasterRecipeBookList struct {
recipeBooks map[int32]*Recipe // Book ID -> Recipe (represents recipe book)
mutex sync.RWMutex
stats *Statistics
}
// NewMasterRecipeBookList creates a new master recipe book list
// Converted from C++ MasterRecipeBookList::MasterRecipeBookList constructor
func NewMasterRecipeBookList() *MasterRecipeBookList {
return &MasterRecipeBookList{
recipeBooks: make(map[int32]*Recipe),
stats: NewStatistics(),
}
}
// AddRecipeBook adds a recipe book to the master list
// Converted from C++ MasterRecipeBookList::AddRecipeBook
func (mrbl *MasterRecipeBookList) AddRecipeBook(recipe *Recipe) bool {
if recipe == nil || recipe.BookID <= 0 {
return false
}
mrbl.mutex.Lock()
defer mrbl.mutex.Unlock()
// Check for duplicate book ID
if _, exists := mrbl.recipeBooks[recipe.BookID]; exists {
return false
}
mrbl.recipeBooks[recipe.BookID] = recipe
mrbl.stats.TotalRecipeBooks++
return true
}
// GetRecipeBook retrieves a recipe book by book ID
// Converted from C++ MasterRecipeBookList::GetRecipeBooks
func (mrbl *MasterRecipeBookList) GetRecipeBook(bookID int32) *Recipe {
mrbl.mutex.RLock()
defer mrbl.mutex.RUnlock()
mrbl.stats.IncrementRecipeBookLookups()
if recipe, exists := mrbl.recipeBooks[bookID]; exists {
return recipe
}
return nil
}
// ClearRecipeBooks removes all recipe books from the master list
// Converted from C++ MasterRecipeBookList::ClearRecipeBooks
func (mrbl *MasterRecipeBookList) ClearRecipeBooks() {
mrbl.mutex.Lock()
defer mrbl.mutex.Unlock()
mrbl.recipeBooks = make(map[int32]*Recipe)
mrbl.stats.TotalRecipeBooks = 0
}
// Size returns the total number of recipe books
// Converted from C++ MasterRecipeBookList::Size
func (mrbl *MasterRecipeBookList) Size() int32 {
mrbl.mutex.RLock()
defer mrbl.mutex.RUnlock()
return int32(len(mrbl.recipeBooks))
}
// GetAllRecipeBooks returns all recipe books
func (mrbl *MasterRecipeBookList) GetAllRecipeBooks() map[int32]*Recipe {
mrbl.mutex.RLock()
defer mrbl.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Recipe)
for id, recipe := range mrbl.recipeBooks {
result[id] = recipe
}
return result
}
// GetStatistics returns a snapshot of the current statistics
func (mrbl *MasterRecipeBookList) GetStatistics() Statistics {
return mrbl.stats.GetSnapshot()
}
// PlayerRecipeList manages recipes for a specific player
// Converted from C++ PlayerRecipeList class
type PlayerRecipeList struct {
recipes map[int32]*Recipe // Recipe ID -> Recipe
mutex sync.RWMutex
}
// NewPlayerRecipeList creates a new player recipe list
// Converted from C++ PlayerRecipeList::PlayerRecipeList constructor
func NewPlayerRecipeList() *PlayerRecipeList {
return &PlayerRecipeList{
recipes: make(map[int32]*Recipe),
}
}
// AddRecipe adds a recipe to the player's list
// Converted from C++ PlayerRecipeList::AddRecipe
func (prl *PlayerRecipeList) AddRecipe(recipe *Recipe) bool {
if recipe == nil || !recipe.IsValid() {
return false
}
prl.mutex.Lock()
defer prl.mutex.Unlock()
// Check for duplicate ID
if _, exists := prl.recipes[recipe.ID]; exists {
return false
}
prl.recipes[recipe.ID] = recipe
return true
}
// GetRecipe retrieves a recipe by ID from the player's list
// Converted from C++ PlayerRecipeList::GetRecipe
func (prl *PlayerRecipeList) GetRecipe(recipeID int32) *Recipe {
prl.mutex.RLock()
defer prl.mutex.RUnlock()
if recipe, exists := prl.recipes[recipeID]; exists {
return recipe
}
return nil
}
// RemoveRecipe removes a recipe from the player's list
// Converted from C++ PlayerRecipeList::RemoveRecipe
func (prl *PlayerRecipeList) RemoveRecipe(recipeID int32) bool {
prl.mutex.Lock()
defer prl.mutex.Unlock()
if _, exists := prl.recipes[recipeID]; exists {
delete(prl.recipes, recipeID)
return true
}
return false
}
// ClearRecipes removes all recipes from the player's list
// Converted from C++ PlayerRecipeList::ClearRecipes
func (prl *PlayerRecipeList) ClearRecipes() {
prl.mutex.Lock()
defer prl.mutex.Unlock()
prl.recipes = make(map[int32]*Recipe)
}
// Size returns the total number of recipes for this player
// Converted from C++ PlayerRecipeList::Size
func (prl *PlayerRecipeList) Size() int32 {
prl.mutex.RLock()
defer prl.mutex.RUnlock()
return int32(len(prl.recipes))
}
// GetRecipes returns all recipes for this player
// Converted from C++ PlayerRecipeList::GetRecipes
func (prl *PlayerRecipeList) GetRecipes() map[int32]*Recipe {
prl.mutex.RLock()
defer prl.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Recipe)
for id, recipe := range prl.recipes {
result[id] = recipe
}
return result
}
// GetRecipesBySkill returns recipes for a specific skill
func (prl *PlayerRecipeList) GetRecipesBySkill(skillID int32) []*Recipe {
prl.mutex.RLock()
defer prl.mutex.RUnlock()
var result []*Recipe
for _, recipe := range prl.recipes {
if recipe.Skill == skillID {
result = append(result, recipe)
}
}
return result
}
// GetRecipesByTier returns recipes for a specific tier
func (prl *PlayerRecipeList) GetRecipesByTier(tier int8) []*Recipe {
prl.mutex.RLock()
defer prl.mutex.RUnlock()
var result []*Recipe
for _, recipe := range prl.recipes {
if recipe.Tier == tier {
result = append(result, recipe)
}
}
return result
}
// PlayerRecipeBookList manages recipe books for a specific player
// Converted from C++ PlayerRecipeBookList class
type PlayerRecipeBookList struct {
recipeBooks map[int32]*Recipe // Book ID -> Recipe
mutex sync.RWMutex
}
// NewPlayerRecipeBookList creates a new player recipe book list
// Converted from C++ PlayerRecipeBookList::PlayerRecipeBookList constructor
func NewPlayerRecipeBookList() *PlayerRecipeBookList {
return &PlayerRecipeBookList{
recipeBooks: make(map[int32]*Recipe),
}
}
// AddRecipeBook adds a recipe book to the player's list
// Converted from C++ PlayerRecipeBookList::AddRecipeBook
func (prbl *PlayerRecipeBookList) AddRecipeBook(recipe *Recipe) bool {
if recipe == nil || recipe.BookID <= 0 {
return false
}
prbl.mutex.Lock()
defer prbl.mutex.Unlock()
// Check for duplicate book ID
if _, exists := prbl.recipeBooks[recipe.BookID]; exists {
return false
}
prbl.recipeBooks[recipe.BookID] = recipe
return true
}
// GetRecipeBook retrieves a recipe book by book ID from the player's list
// Converted from C++ PlayerRecipeBookList::GetRecipeBook
func (prbl *PlayerRecipeBookList) GetRecipeBook(bookID int32) *Recipe {
prbl.mutex.RLock()
defer prbl.mutex.RUnlock()
if recipe, exists := prbl.recipeBooks[bookID]; exists {
return recipe
}
return nil
}
// HasRecipeBook checks if the player has a specific recipe book
// Converted from C++ PlayerRecipeBookList::HasRecipeBook
func (prbl *PlayerRecipeBookList) HasRecipeBook(bookID int32) bool {
prbl.mutex.RLock()
defer prbl.mutex.RUnlock()
_, exists := prbl.recipeBooks[bookID]
return exists
}
// ClearRecipeBooks removes all recipe books from the player's list
// Converted from C++ PlayerRecipeBookList::ClearRecipeBooks
func (prbl *PlayerRecipeBookList) ClearRecipeBooks() {
prbl.mutex.Lock()
defer prbl.mutex.Unlock()
prbl.recipeBooks = make(map[int32]*Recipe)
}
// Size returns the total number of recipe books for this player
func (prbl *PlayerRecipeBookList) Size() int32 {
prbl.mutex.RLock()
defer prbl.mutex.RUnlock()
return int32(len(prbl.recipeBooks))
}
// GetRecipeBooks returns all recipe books for this player
// Converted from C++ PlayerRecipeBookList::GetRecipeBooks
func (prbl *PlayerRecipeBookList) GetRecipeBooks() map[int32]*Recipe {
prbl.mutex.RLock()
defer prbl.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Recipe)
for id, recipe := range prbl.recipeBooks {
result[id] = recipe
}
return result
}

162
internal/recipes/types.go Normal file
View File

@ -0,0 +1,162 @@
package recipes
import (
"sync"
)
// RecipeComponent represents a component required for crafting
// Converted from C++ RecipeComp struct
type RecipeComponent struct {
ItemID int32
Slot int8
}
// RecipeProducts represents the products and byproducts for a crafting stage
// Converted from C++ RecipeProducts struct
type RecipeProducts struct {
ProductID int32
ByproductID int32
ProductQty int8
ByproductQty int8
}
// Recipe represents a crafting recipe in EverQuest II
// Converted from C++ Recipe class
type Recipe struct {
// Core recipe data
ID int32
SoeID int32 // SOE recipe ID (CRC)
BookID int32
Name string
Description string
BookName string
Book string
Device string
// Recipe requirements and properties
Level int8
Tier int8
Icon int16
Skill int32
Technique int32
Knowledge int32
Classes int32 // Bitmask of tradeskill classes
DeviceSubType int8
// Unknown fields from C++ (preserved for compatibility)
Unknown1 int8
Unknown2 int32
Unknown3 int32
Unknown4 int32
// Product information
ProductItemID int32
ProductName string
ProductQty int8
// Component titles
PrimaryBuildCompTitle string
Build1CompTitle string
Build2CompTitle string
Build3CompTitle string
Build4CompTitle string
FuelCompTitle string
// Component quantities
Build1CompQty int16
Build2CompQty int16
Build3CompQty int16
Build4CompQty int16
FuelCompQty int16
PrimaryCompQty int16
// Highest completed stage for player recipes
HighestStage int8
// Components map: slot -> list of item IDs
// Slots: 0=primary, 1-4=build slots, 5=fuel
Components map[int8][]int32
// Products map: stage -> products/byproducts
// Stages: 0-4 (5 total stages)
Products map[int8]*RecipeProducts
// Thread safety
mutex sync.RWMutex
}
// Statistics tracks recipe system usage patterns
type Statistics struct {
TotalRecipes int32
TotalRecipeBooks int32
RecipesByTier map[int8]int32
RecipesBySkill map[int32]int32
RecipeLookups int64
RecipeBookLookups int64
PlayerRecipeLoads int64
ComponentQueries int64
mutex sync.RWMutex
}
// NewStatistics creates a new statistics tracker
func NewStatistics() *Statistics {
return &Statistics{
RecipesByTier: make(map[int8]int32),
RecipesBySkill: make(map[int32]int32),
}
}
// IncrementRecipeLookups increments the recipe lookup counter
func (s *Statistics) IncrementRecipeLookups() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.RecipeLookups++
}
// IncrementRecipeBookLookups increments the recipe book lookup counter
func (s *Statistics) IncrementRecipeBookLookups() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.RecipeBookLookups++
}
// IncrementPlayerRecipeLoads increments the player recipe load counter
func (s *Statistics) IncrementPlayerRecipeLoads() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.PlayerRecipeLoads++
}
// IncrementComponentQueries increments the component query counter
func (s *Statistics) IncrementComponentQueries() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.ComponentQueries++
}
// GetSnapshot returns a snapshot of the current statistics
func (s *Statistics) GetSnapshot() Statistics {
s.mutex.RLock()
defer s.mutex.RUnlock()
snapshot := Statistics{
TotalRecipes: s.TotalRecipes,
TotalRecipeBooks: s.TotalRecipeBooks,
RecipesByTier: make(map[int8]int32),
RecipesBySkill: make(map[int32]int32),
RecipeLookups: s.RecipeLookups,
RecipeBookLookups: s.RecipeBookLookups,
PlayerRecipeLoads: s.PlayerRecipeLoads,
ComponentQueries: s.ComponentQueries,
}
for tier, count := range s.RecipesByTier {
snapshot.RecipesByTier[tier] = count
}
for skill, count := range s.RecipesBySkill {
snapshot.RecipesBySkill[skill] = count
}
return snapshot
}

138
internal/rules/README.md Normal file
View File

@ -0,0 +1,138 @@
# Rules System
The rules system provides configurable game parameters and settings for the EQ2 server. It has been fully converted from the original C++ EQ2EMu implementation to Go.
## Overview
The rules system consists of:
- **Rule Categories**: Major groupings of rules (Player, Combat, World, etc.)
- **Rule Types**: Specific rule types within each category
- **Rule Sets**: Collections of rules that can be switched between
- **Global/Zone Rules**: Global rules apply server-wide, zone rules override for specific zones
## Core Components
### Files
- `constants.go` - Rule categories, types, and constants
- `types.go` - Core data structures (Rule, RuleSet, RuleManagerStats)
- `manager.go` - Main RuleManager implementation with default rules
- `database.go` - Database operations for rule persistence
- `interfaces.go` - Integration interfaces and adapters
- `rules_test.go` - Comprehensive test suite
### Main Types
- `Rule` - Individual rule with category, type, value, and type conversion methods
- `RuleSet` - Collection of rules with ID and name
- `RuleManager` - Central management of all rules and rule sets
- `DatabaseService` - Database operations for rule persistence
## Rule Categories
The system supports 14 rule categories:
1. **Client** - Client-related settings
2. **Faction** - Faction system rules
3. **Guild** - Guild system rules
4. **Player** - Player-related rules (levels, stats, etc.)
5. **PVP** - Player vs Player combat rules
6. **Combat** - Combat system rules
7. **Spawn** - NPC/spawn behavior rules
8. **UI** - User interface rules
9. **World** - Server-wide settings
10. **Zone** - Zone-specific rules
11. **Loot** - Loot system rules
12. **Spells** - Spell system rules
13. **Expansion** - Expansion flags
14. **Discord** - Discord integration settings
## Usage
### Basic Usage
```go
// Create rule manager
ruleManager := rules.NewRuleManager()
// Get a rule value
rule := ruleManager.GetGlobalRule(rules.CategoryPlayer, rules.PlayerMaxLevel)
maxLevel := rule.GetInt32() // Returns 50 (default)
// Get rule by name
rule2 := ruleManager.GetGlobalRuleByName("Player", "MaxLevel")
```
### Database Integration
```go
// Create database service
db, _ := database.Open("rules.db")
dbService := rules.NewDatabaseService(db)
// Create tables
dbService.CreateRulesTables()
// Load rules from database
dbService.LoadRuleSets(ruleManager, false)
```
### Rule Adapters
```go
// Create adapter for zone-specific rules
adapter := rules.NewRuleManagerAdapter(ruleManager, zoneID)
// Get rule with zone override
maxLevel := adapter.GetInt32(rules.CategoryPlayer, rules.PlayerMaxLevel)
```
## Default Rules
The system includes comprehensive default rules matching the original C++ implementation:
- **Player Max Level**: 50
- **Combat Max Range**: 4.0
- **Experience Multiplier**: 1.0
- **And 100+ other rules across all categories**
## Database Schema
The system uses three tables:
- `rulesets` - Rule set definitions
- `ruleset_details` - Individual rule overrides per rule set
- `variables` - System variables including default rule set ID
## Thread Safety
All operations are thread-safe using Go's sync.RWMutex for optimal read performance.
## Performance
- Rule access: ~280ns per operation (benchmark)
- Rule manager creation: ~45μs per operation (benchmark)
- All operations are optimized for high-frequency access
## Testing
Run the comprehensive test suite:
```bash
go test ./internal/rules/ -v
```
## Migration from C++
This is a complete conversion from the original C++ implementation:
- `Rules.h``constants.go` + `types.go`
- `Rules.cpp``manager.go`
- `RulesDB.cpp``database.go`
All functionality has been preserved with Go-native patterns and improvements:
- Better error handling
- Type safety
- Comprehensive interfaces
- Modern testing practices
- Performance optimizations

346
internal/rules/constants.go Normal file
View File

@ -0,0 +1,346 @@
package rules
import "errors"
// RuleCategory defines the major categories of rules in EQ2
type RuleCategory int32
const (
CategoryClient RuleCategory = 0 // Client-related rules
CategoryFaction RuleCategory = 1 // Faction system rules
CategoryGuild RuleCategory = 2 // Guild system rules
CategoryPlayer RuleCategory = 3 // Player-related rules
CategoryPVP RuleCategory = 4 // Player vs Player rules
CategoryCombat RuleCategory = 5 // Combat system rules
CategorySpawn RuleCategory = 6 // Spawn/NPC rules
CategoryUI RuleCategory = 7 // User interface rules
CategoryWorld RuleCategory = 8 // World/server rules
CategoryZone RuleCategory = 9 // Zone-specific rules
CategoryLoot RuleCategory = 10 // Loot system rules
CategorySpells RuleCategory = 11 // Spell system rules
CategoryExpansion RuleCategory = 12 // Expansion flags
CategoryDiscord RuleCategory = 13 // Discord integration
)
// RuleType defines specific rule types within categories
type RuleType int32
// CLIENT RULES
const (
ClientShowWelcomeScreen RuleType = 0 // Show welcome screen to new players
ClientGroupSpellsTimer RuleType = 1 // Group spells update timer
ClientQuestQueueTimer RuleType = 2 // Quest queue processing timer
)
// FACTION RULES
const (
FactionAllowBasedCombat RuleType = 0 // Allow faction-based combat
)
// GUILD RULES
const (
GuildMaxLevel RuleType = 0 // Maximum guild level
GuildMaxPlayers RuleType = 1 // Maximum guild members
)
// PLAYER RULES
const (
PlayerMaxLevel RuleType = 0 // Maximum player level
PlayerMaxLevelOverrideStatus RuleType = 1 // Status required to override max level
PlayerMaxPlayers RuleType = 2 // Maximum players on server
PlayerMaxPlayersOverrideStatus RuleType = 3 // Status required to override max players
PlayerVitalityAmount RuleType = 4 // Vitality bonus amount
PlayerVitalityFrequency RuleType = 5 // Vitality bonus frequency
PlayerMaxAA RuleType = 6 // Maximum total AA points
PlayerMaxClassAA RuleType = 7 // Maximum class AA points
PlayerMaxSubclassAA RuleType = 8 // Maximum subclass AA points
PlayerMaxShadowsAA RuleType = 9 // Maximum shadows AA points
PlayerMaxHeroicAA RuleType = 10 // Maximum heroic AA points
PlayerMaxTradeskillAA RuleType = 11 // Maximum tradeskill AA points
PlayerMaxPrestigeAA RuleType = 12 // Maximum prestige AA points
PlayerMaxTradeskillPrestigeAA RuleType = 13 // Maximum tradeskill prestige AA points
PlayerMaxDragonAA RuleType = 14 // Maximum dragon AA points
PlayerMinLastNameLevel RuleType = 15 // Minimum level for last name
PlayerMaxLastNameLength RuleType = 16 // Maximum last name length
PlayerMinLastNameLength RuleType = 17 // Minimum last name length
PlayerDisableHouseAlignmentRequirement RuleType = 18 // Disable house alignment requirement
PlayerMentorItemDecayRate RuleType = 19 // Item decay rate when mentoring
PlayerTemporaryItemLogoutTime RuleType = 20 // Time for temporary items to decay
PlayerHeirloomItemShareExpiration RuleType = 21 // Heirloom item sharing expiration
PlayerSwimmingSkillMinSpeed RuleType = 22 // Minimum swimming speed
PlayerSwimmingSkillMaxSpeed RuleType = 23 // Maximum swimming speed
PlayerSwimmingSkillMinBreathLength RuleType = 24 // Minimum breath length
PlayerSwimmingSkillMaxBreathLength RuleType = 25 // Maximum breath length
PlayerAutoSkillUpBaseSkills RuleType = 26 // Auto-skill base skills on level
PlayerMaxWeightStrengthMultiplier RuleType = 27 // Strength multiplier for max weight
PlayerBaseWeight RuleType = 28 // Base weight for all classes
PlayerWeightPercentImpact RuleType = 29 // Speed impact per weight percent
PlayerWeightPercentCap RuleType = 30 // Maximum weight impact cap
PlayerCoinWeightPerStone RuleType = 31 // Coin weight per stone
PlayerWeightInflictsSpeed RuleType = 32 // Whether weight affects speed
PlayerLevelMasterySkillMultiplier RuleType = 33 // Level mastery skill multiplier
PlayerTraitTieringSelection RuleType = 34 // Trait tiering selection rules
PlayerClassicTraitLevelTable RuleType = 35 // Use classic trait level table
PlayerTraitFocusSelectLevel RuleType = 36 // Trait focus selection level
PlayerTraitTrainingSelectLevel RuleType = 37 // Trait training selection level
PlayerTraitRaceSelectLevel RuleType = 38 // Trait race selection level
PlayerTraitCharacterSelectLevel RuleType = 39 // Trait character selection level
PlayerStartHPBase RuleType = 40 // Starting HP base
PlayerStartPowerBase RuleType = 41 // Starting power base
PlayerStartHPLevelMod RuleType = 42 // HP level modifier
PlayerStartPowerLevelMod RuleType = 43 // Power level modifier
PlayerAllowEquipCombat RuleType = 44 // Allow equipment changes in combat
PlayerMaxTargetCommandDistance RuleType = 45 // Max distance for target command
PlayerMinSkillMultiplierValue RuleType = 46 // Min skill multiplier value
PlayerHarvestSkillUpMultiplier RuleType = 47 // Harvest skill up multiplier
PlayerMiniDingPercentage RuleType = 48 // Mini ding percentage
)
// PVP RULES
const (
PVPAllowPVP RuleType = 0 // Allow PVP combat
PVPLevelRange RuleType = 1 // PVP level range
PVPInvisPlayerDiscoveryRange RuleType = 2 // Invisible player discovery range
PVPMitigationModByLevel RuleType = 3 // PVP mitigation modifier by level
PVPType RuleType = 4 // PVP type (FFA, alignment, etc.)
)
// COMBAT RULES
const (
CombatMaxRange RuleType = 0 // Maximum combat range
CombatDeathExperienceDebt RuleType = 1 // Experience debt on death
CombatGroupExperienceDebt RuleType = 2 // Share debt with group
CombatPVPDeathExperienceDebt RuleType = 3 // PVP death experience debt
CombatExperienceToDebt RuleType = 4 // Percentage of experience to debt
CombatExperienceDebtRecoveryPercent RuleType = 5 // Debt recovery percentage
CombatExperienceDebtRecoveryPeriod RuleType = 6 // Debt recovery period
CombatEnableSpiritShards RuleType = 7 // Enable spirit shards
CombatSpiritShardSpawnScript RuleType = 8 // Spirit shard spawn script
CombatShardDebtRecoveryPercent RuleType = 9 // Shard debt recovery percentage
CombatShardRecoveryByRadius RuleType = 10 // Shard recovery by radius
CombatShardLifetime RuleType = 11 // Shard lifetime
CombatEffectiveMitigationCapLevel RuleType = 12 // Effective mitigation cap level
CombatCalculatedMitigationCapLevel RuleType = 13 // Calculated mitigation cap level
CombatMitigationLevelEffectivenessMax RuleType = 14 // Max mitigation effectiveness
CombatMitigationLevelEffectivenessMin RuleType = 15 // Min mitigation effectiveness
CombatMaxMitigationAllowed RuleType = 16 // Max mitigation allowed PVE
CombatMaxMitigationAllowedPVP RuleType = 17 // Max mitigation allowed PVP
CombatStrengthNPC RuleType = 18 // NPC strength multiplier
CombatStrengthOther RuleType = 19 // Other strength multiplier
CombatMaxSkillBonusByLevel RuleType = 20 // Max skill bonus by level
CombatLockedEncounterNoAttack RuleType = 21 // Locked encounter no attack
CombatMaxChaseDistance RuleType = 22 // Maximum chase distance
)
// SPAWN RULES
const (
SpawnSpeedMultiplier RuleType = 0 // Speed multiplier
SpawnClassicRegen RuleType = 1 // Use classic regeneration
SpawnHailMovementPause RuleType = 2 // Hail movement pause time
SpawnHailDistance RuleType = 3 // Hail distance
SpawnUseHardCodeWaterModelType RuleType = 4 // Use hardcoded water model type
SpawnUseHardCodeFlyingModelType RuleType = 5 // Use hardcoded flying model type
)
// UI RULES
const (
UIMaxWhoResults RuleType = 0 // Maximum /who results
UIMaxWhoOverrideStatus RuleType = 1 // Status to override max /who results
)
// WORLD RULES
const (
WorldDefaultStartingZoneID RuleType = 0 // Default starting zone ID
WorldEnablePOIDiscovery RuleType = 1 // Enable POI discovery
WorldGamblingTokenItemID RuleType = 2 // Gambling token item ID
WorldGuildAutoJoin RuleType = 3 // Auto join guild
WorldGuildAutoJoinID RuleType = 4 // Auto join guild ID
WorldGuildAutoJoinDefaultRankID RuleType = 5 // Auto join default rank ID
WorldServerLocked RuleType = 6 // Server locked
WorldServerLockedOverrideStatus RuleType = 7 // Server locked override status
WorldSyncZonesWithLogin RuleType = 8 // Sync zones with login
WorldSyncEquipWithLogin RuleType = 9 // Sync equipment with login
WorldUseBannedIPsTable RuleType = 10 // Use banned IPs table
WorldLinkDeadTimer RuleType = 11 // Link dead timer
WorldRemoveDisconnectedClientsTimer RuleType = 12 // Remove disconnected clients timer
WorldPlayerCampTimer RuleType = 13 // Player camp timer
WorldGMCampTimer RuleType = 14 // GM camp timer
WorldAutoAdminPlayers RuleType = 15 // Auto admin players
WorldAutoAdminGMs RuleType = 16 // Auto admin GMs
WorldAutoAdminStatusValue RuleType = 17 // Auto admin status value
WorldDuskTime RuleType = 18 // Dusk time
WorldDawnTime RuleType = 19 // Dawn time
WorldThreadedLoad RuleType = 20 // Threaded loading
WorldTradeskillSuccessChance RuleType = 21 // Tradeskill success chance
WorldTradeskillCritSuccessChance RuleType = 22 // Tradeskill critical success chance
WorldTradeskillFailChance RuleType = 23 // Tradeskill fail chance
WorldTradeskillCritFailChance RuleType = 24 // Tradeskill critical fail chance
WorldTradeskillEventChance RuleType = 25 // Tradeskill event chance
WorldEditorURL RuleType = 26 // Editor URL
WorldEditorIncludeID RuleType = 27 // Editor include ID
WorldEditorOfficialServer RuleType = 28 // Editor official server
WorldSavePaperdollImage RuleType = 29 // Save paperdoll image
WorldSaveHeadshotImage RuleType = 30 // Save headshot image
WorldSendPaperdollImagesToLogin RuleType = 31 // Send paperdoll images to login
WorldTreasureChestDisabled RuleType = 32 // Treasure chest disabled
WorldStartingZoneLanguages RuleType = 33 // Starting zone languages
WorldStartingZoneRuleFlag RuleType = 34 // Starting zone rule flag
WorldEnforceRacialAlignment RuleType = 35 // Enforce racial alignment
WorldMemoryCacheZoneMaps RuleType = 36 // Memory cache zone maps
WorldAutoLockEncounter RuleType = 37 // Auto lock encounter
WorldDisplayItemTiers RuleType = 38 // Display item tiers
WorldLoreAndLegendAccept RuleType = 39 // Lore and legend accept
)
// ZONE RULES
const (
ZoneMinLevelOverrideStatus RuleType = 0 // Min level override status
ZoneMinAccessOverrideStatus RuleType = 1 // Min access override status
ZoneXPMultiplier RuleType = 2 // Experience multiplier
ZoneTSXPMultiplier RuleType = 3 // Tradeskill experience multiplier
ZoneWeatherEnabled RuleType = 4 // Weather enabled
ZoneWeatherType RuleType = 5 // Weather type
ZoneMinWeatherSeverity RuleType = 6 // Min weather severity
ZoneMaxWeatherSeverity RuleType = 7 // Max weather severity
ZoneWeatherChangeFrequency RuleType = 8 // Weather change frequency
ZoneWeatherChangePerInterval RuleType = 9 // Weather change per interval
ZoneWeatherDynamicMaxOffset RuleType = 10 // Weather dynamic max offset
ZoneWeatherChangeChance RuleType = 11 // Weather change chance
ZoneSpawnUpdateTimer RuleType = 12 // Spawn update timer
ZoneCheckAttackPlayer RuleType = 13 // Check attack player
ZoneCheckAttackNPC RuleType = 14 // Check attack NPC
ZoneHOTime RuleType = 15 // Heroic opportunity time
ZoneUseMapUnderworldCoords RuleType = 16 // Use map underworld coords
ZoneMapUnderworldCoordOffset RuleType = 17 // Map underworld coord offset
ZoneSharedMaxPlayers RuleType = 18 // Shared zone max players
ZoneRegenTimer RuleType = 19 // Regeneration timer
ZoneClientSaveTimer RuleType = 20 // Client save timer
ZoneShutdownDelayTimer RuleType = 21 // Shutdown delay timer
ZoneWeatherTimer RuleType = 22 // Weather timer
ZoneSpawnDeleteTimer RuleType = 23 // Spawn delete timer
)
// LOOT RULES
const (
LootRadius RuleType = 0 // Loot pickup radius
LootAutoDisarmChest RuleType = 1 // Auto disarm chest
LootChestTriggerRadiusGroup RuleType = 2 // Chest trigger radius group
LootChestUnlockedTimeDrop RuleType = 3 // Chest unlocked time drop
LootAllowChestUnlockByDropTime RuleType = 4 // Allow chest unlock by drop time
LootChestUnlockedTimeTrap RuleType = 5 // Chest unlocked time trap
LootAllowChestUnlockByTrapTime RuleType = 6 // Allow chest unlock by trap time
LootSkipGrayMob RuleType = 7 // Skip loot from gray mobs
LootDistributionTime RuleType = 8 // Loot distribution time
)
// SPELLS RULES
const (
SpellsNoInterruptBaseChance RuleType = 0 // No interrupt base chance
SpellsEnableFizzleSpells RuleType = 1 // Enable fizzle spells
SpellsDefaultFizzleChance RuleType = 2 // Default fizzle chance
SpellsFizzleMaxSkill RuleType = 3 // Fizzle max skill
SpellsFizzleDefaultSkill RuleType = 4 // Fizzle default skill
SpellsEnableCrossZoneGroupBuffs RuleType = 5 // Enable cross zone group buffs
SpellsEnableCrossZoneTargetBuffs RuleType = 6 // Enable cross zone target buffs
SpellsPlayerSpellSaveStateWaitInterval RuleType = 7 // Player spell save state wait interval
SpellsPlayerSpellSaveStateCap RuleType = 8 // Player spell save state cap
SpellsRequirePreviousTierScribe RuleType = 9 // Require previous tier scribe
SpellsCureSpellID RuleType = 10 // Cure spell ID
SpellsCureCurseSpellID RuleType = 11 // Cure curse spell ID
SpellsCureNoxiousSpellID RuleType = 12 // Cure noxious spell ID
SpellsCureMagicSpellID RuleType = 13 // Cure magic spell ID
SpellsCureTraumaSpellID RuleType = 14 // Cure trauma spell ID
SpellsCureArcaneSpellID RuleType = 15 // Cure arcane spell ID
SpellsMinistrationSkillID RuleType = 16 // Ministration skill ID
SpellsMinistrationPowerReductionMax RuleType = 17 // Ministration power reduction max
SpellsMinistrationPowerReductionSkill RuleType = 18 // Ministration power reduction skill
SpellsMasterSkillReduceSpellResist RuleType = 19 // Master skill reduce spell resist
SpellsUseClassicSpellLevel RuleType = 20 // Use classic spell level
)
// EXPANSION RULES
const (
ExpansionGlobalFlag RuleType = 0 // Global expansion flag
ExpansionHolidayFlag RuleType = 1 // Global holiday flag
)
// DISCORD RULES
const (
DiscordEnabled RuleType = 0 // Discord enabled
DiscordWebhookURL RuleType = 1 // Discord webhook URL
DiscordBotToken RuleType = 2 // Discord bot token
DiscordChannel RuleType = 3 // Discord channel
DiscordListenChan RuleType = 4 // Discord listen channel
)
// Rule validation constants
const (
MaxRuleValueLength = 1024 // Maximum rule value length
MaxRuleCombinedLength = 2048 // Maximum combined rule string length
MaxRuleNameLength = 64 // Maximum rule name length
)
// Database constants
const (
TableRuleSets = "rulesets"
TableRuleSetDetails = "ruleset_details"
TableVariables = "variables"
DefaultRuleSetIDVar = "default_ruleset_id"
)
// Error variables
var (
ErrRuleNotFound = errors.New("rule not found")
ErrRuleSetNotFound = errors.New("rule set not found")
ErrInvalidRuleCategory = errors.New("invalid rule category")
ErrInvalidRuleType = errors.New("invalid rule type")
ErrInvalidRuleValue = errors.New("invalid rule value")
ErrDuplicateRuleSet = errors.New("duplicate rule set")
ErrRuleSetNotActive = errors.New("rule set not active")
ErrGlobalRuleSetNotSet = errors.New("global rule set not set")
ErrZoneRuleSetNotFound = errors.New("zone rule set not found")
ErrRuleValueTooLong = errors.New("rule value too long")
ErrRuleNameTooLong = errors.New("rule name too long")
)
// Rule category names for string conversion
var CategoryNames = map[RuleCategory]string{
CategoryClient: "Client",
CategoryFaction: "Faction",
CategoryGuild: "Guild",
CategoryPlayer: "Player",
CategoryPVP: "PVP",
CategoryCombat: "Combat",
CategorySpawn: "Spawn",
CategoryUI: "UI",
CategoryWorld: "World",
CategoryZone: "Zone",
CategoryLoot: "Loot",
CategorySpells: "Spells",
CategoryExpansion: "Expansion",
CategoryDiscord: "Discord",
}
// Reverse mapping for category name lookup
var CategoryByName = make(map[string]RuleCategory)
func init() {
// Initialize reverse category mapping
for category, name := range CategoryNames {
CategoryByName[name] = category
}
}
// GetCategoryName returns the string name for a rule category
func GetCategoryName(category RuleCategory) string {
if name, exists := CategoryNames[category]; exists {
return name
}
return "Unknown"
}
// GetCategoryByName returns the rule category for a string name
func GetCategoryByName(name string) (RuleCategory, bool) {
category, exists := CategoryByName[name]
return category, exists
}

436
internal/rules/database.go Normal file
View File

@ -0,0 +1,436 @@
package rules
import (
"fmt"
"log"
"strconv"
"eq2emu/internal/database"
)
// DatabaseService handles rule database operations
// Converted from C++ WorldDatabase rule functions
type DatabaseService struct {
db *database.DB
}
// NewDatabaseService creates a new database service instance
func NewDatabaseService(db *database.DB) *DatabaseService {
return &DatabaseService{
db: db,
}
}
// LoadGlobalRuleSet loads the global rule set from database
// Converted from C++ WorldDatabase::LoadGlobalRuleSet()
func (ds *DatabaseService) LoadGlobalRuleSet(ruleManager *RuleManager) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
ruleSetID := int32(0)
// Get the default ruleset ID from variables table
query := "SELECT variable_value FROM variables WHERE variable_name = ?"
row, err := ds.db.QueryRow(query, DefaultRuleSetIDVar)
if err != nil {
return fmt.Errorf("error querying default ruleset ID: %v", err)
}
if row == nil {
log.Printf("[Rules] Variables table is missing %s variable name, using code-default rules", DefaultRuleSetIDVar)
return nil
}
defer row.Close()
variableValue := row.Text(0)
if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil {
ruleSetID = int32(id)
log.Printf("[Rules] Loading Global Ruleset id %d", ruleSetID)
} else {
return fmt.Errorf("invalid ruleset ID format: %s", variableValue)
}
if ruleSetID > 0 {
if !ruleManager.SetGlobalRuleSet(ruleSetID) {
return fmt.Errorf("error loading global rule set - rule set with ID %d does not exist", ruleSetID)
}
}
ruleManager.stats.IncrementDatabaseOperations()
return nil
}
// LoadRuleSets loads all rule sets from database
// Converted from C++ WorldDatabase::LoadRuleSets()
func (ds *DatabaseService) LoadRuleSets(ruleManager *RuleManager, reload bool) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
if reload {
ruleManager.Flush(true)
}
// First load the coded defaults into the global rule set
ruleManager.LoadCodedDefaultsIntoRuleSet(ruleManager.GetGlobalRuleSet())
// Load active rule sets from database
query := "SELECT ruleset_id, ruleset_name FROM rulesets WHERE ruleset_active > 0"
loadedCount := 0
err := ds.db.Query(query, func(row *database.Row) error {
ruleSetID := int32(row.Int64(0))
ruleSetName := row.Text(1)
ruleSet := NewRuleSet()
ruleSet.SetID(ruleSetID)
ruleSet.SetName(ruleSetName)
if ruleManager.AddRuleSet(ruleSet) {
log.Printf("[Rules] Loading rule set '%s' (%d)", ruleSet.GetName(), ruleSet.GetID())
err := ds.LoadRuleSetDetails(ruleManager, ruleSet)
if err != nil {
log.Printf("[Rules] Error loading rule set details for '%s': %v", ruleSetName, err)
return nil // Continue with other rule sets
}
loadedCount++
} else {
log.Printf("[Rules] Unable to add rule set '%s' - ID %d already exists", ruleSetName, ruleSetID)
}
return nil
})
if err != nil {
return fmt.Errorf("error querying rule sets: %v", err)
}
log.Printf("[Rules] Loaded %d Rule Sets", loadedCount)
// Load global rule set
err = ds.LoadGlobalRuleSet(ruleManager)
if err != nil {
return fmt.Errorf("error loading global rule set: %v", err)
}
ruleManager.stats.IncrementDatabaseOperations()
return nil
}
// LoadRuleSetDetails loads the detailed rules for a specific rule set
// Converted from C++ WorldDatabase::LoadRuleSetDetails()
func (ds *DatabaseService) LoadRuleSetDetails(ruleManager *RuleManager, ruleSet *RuleSet) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
if ruleSet == nil {
return fmt.Errorf("rule set is nil")
}
// Copy rules from global rule set (coded defaults) first
ruleSet.CopyRulesInto(ruleManager.GetGlobalRuleSet())
// Load rule overrides from database
query := "SELECT rule_category, rule_type, rule_value FROM ruleset_details WHERE ruleset_id = ?"
loadedRules := 0
err := ds.db.Query(query, func(row *database.Row) error {
categoryName := row.Text(0)
typeName := row.Text(1)
ruleValue := row.Text(2)
// Find the rule by name
rule := ruleSet.GetRuleByName(categoryName, typeName)
if rule == nil {
log.Printf("[Rules] Unknown rule with category '%s' and type '%s'", categoryName, typeName)
return nil // Continue with other rules
}
log.Printf("[Rules] Setting rule category '%s', type '%s' to value: %s", categoryName, typeName, ruleValue)
rule.SetValue(ruleValue)
loadedRules++
return nil
}, ruleSet.GetID())
if err != nil {
return fmt.Errorf("error querying rule set details: %v", err)
}
log.Printf("[Rules] Loaded %d rule overrides for rule set '%s'", loadedRules, ruleSet.GetName())
ruleManager.stats.IncrementDatabaseOperations()
return nil
}
// SaveRuleSet saves a rule set to the database
func (ds *DatabaseService) SaveRuleSet(ruleSet *RuleSet) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
if ruleSet == nil {
return fmt.Errorf("rule set is nil")
}
// Use transaction for atomicity
return ds.db.Transaction(func(tx *database.DB) error {
// Insert or update rule set
query := `INSERT INTO rulesets (ruleset_id, ruleset_name, ruleset_active)
VALUES (?, ?, 1)
ON CONFLICT(ruleset_id) DO UPDATE SET
ruleset_name = excluded.ruleset_name,
ruleset_active = excluded.ruleset_active`
err := tx.Exec(query, ruleSet.GetID(), ruleSet.GetName())
if err != nil {
return fmt.Errorf("error saving rule set: %v", err)
}
// Delete existing rule details
err = tx.Exec("DELETE FROM ruleset_details WHERE ruleset_id = ?", ruleSet.GetID())
if err != nil {
return fmt.Errorf("error deleting existing rule details: %v", err)
}
// Insert rule details
rules := ruleSet.GetRules()
for _, categoryMap := range rules {
for _, rule := range categoryMap {
if rule.IsValid() {
combined := rule.GetCombined()
parts := splitCombined(combined)
if len(parts) == 2 {
query := "INSERT INTO ruleset_details (ruleset_id, rule_category, rule_type, rule_value) VALUES (?, ?, ?, ?)"
err = tx.Exec(query, ruleSet.GetID(), parts[0], parts[1], rule.GetValue())
if err != nil {
return fmt.Errorf("error saving rule detail: %v", err)
}
}
}
}
}
return nil
})
}
// DeleteRuleSet deletes a rule set from the database
func (ds *DatabaseService) DeleteRuleSet(ruleSetID int32) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
// Use transaction for atomicity
return ds.db.Transaction(func(tx *database.DB) error {
// Delete rule details first (foreign key constraint)
err := tx.Exec("DELETE FROM ruleset_details WHERE ruleset_id = ?", ruleSetID)
if err != nil {
return fmt.Errorf("error deleting rule details: %v", err)
}
// Delete rule set
err = tx.Exec("DELETE FROM rulesets WHERE ruleset_id = ?", ruleSetID)
if err != nil {
return fmt.Errorf("error deleting rule set: %v", err)
}
return nil
})
}
// SetDefaultRuleSet sets the default rule set ID in the variables table
func (ds *DatabaseService) SetDefaultRuleSet(ruleSetID int32) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
query := `INSERT INTO variables (variable_name, variable_value, comment)
VALUES (?, ?, 'Default ruleset ID')
ON CONFLICT(variable_name) DO UPDATE SET
variable_value = excluded.variable_value`
err := ds.db.Exec(query, DefaultRuleSetIDVar, strconv.Itoa(int(ruleSetID)))
if err != nil {
return fmt.Errorf("error setting default rule set: %v", err)
}
return nil
}
// GetDefaultRuleSetID gets the default rule set ID from the variables table
func (ds *DatabaseService) GetDefaultRuleSetID() (int32, error) {
if ds.db == nil {
return 0, fmt.Errorf("database not initialized")
}
query := "SELECT variable_value FROM variables WHERE variable_name = ?"
row, err := ds.db.QueryRow(query, DefaultRuleSetIDVar)
if err != nil {
return 0, fmt.Errorf("error querying default ruleset ID: %v", err)
}
if row == nil {
return 0, fmt.Errorf("default ruleset ID not found in variables table")
}
defer row.Close()
variableValue := row.Text(0)
if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil {
return int32(id), nil
}
return 0, fmt.Errorf("invalid ruleset ID format: %s", variableValue)
}
// GetRuleSetList returns a list of all rule sets
func (ds *DatabaseService) GetRuleSetList() ([]RuleSetInfo, error) {
if ds.db == nil {
return nil, fmt.Errorf("database not initialized")
}
query := "SELECT ruleset_id, ruleset_name, ruleset_active FROM rulesets ORDER BY ruleset_id"
var ruleSets []RuleSetInfo
err := ds.db.Query(query, func(row *database.Row) error {
info := RuleSetInfo{
ID: int32(row.Int64(0)),
Name: row.Text(1),
Active: row.Bool(2),
}
ruleSets = append(ruleSets, info)
return nil
})
if err != nil {
return nil, fmt.Errorf("error querying rule sets: %v", err)
}
return ruleSets, nil
}
// ValidateDatabase ensures the required tables exist
func (ds *DatabaseService) ValidateDatabase() error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
// Check if rulesets table exists
query := "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='rulesets'"
row, err := ds.db.QueryRow(query)
if err != nil {
return fmt.Errorf("error checking rulesets table: %v", err)
}
if row == nil || row.Int(0) == 0 {
return fmt.Errorf("rulesets table does not exist")
}
row.Close()
// Check if ruleset_details table exists
query = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='ruleset_details'"
row, err = ds.db.QueryRow(query)
if err != nil {
return fmt.Errorf("error checking ruleset_details table: %v", err)
}
if row == nil || row.Int(0) == 0 {
return fmt.Errorf("ruleset_details table does not exist")
}
row.Close()
// Check if variables table exists
query = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='variables'"
row, err = ds.db.QueryRow(query)
if err != nil {
return fmt.Errorf("error checking variables table: %v", err)
}
if row == nil || row.Int(0) == 0 {
return fmt.Errorf("variables table does not exist")
}
row.Close()
return nil
}
// RuleSetInfo contains basic information about a rule set
type RuleSetInfo struct {
ID int32 `json:"id"`
Name string `json:"name"`
Active bool `json:"active"`
}
// Helper function to split combined rule string
func splitCombined(combined string) []string {
for i, char := range combined {
if char == ':' {
return []string{combined[:i], combined[i+1:]}
}
}
return []string{combined}
}
// CreateRulesTables creates the necessary database tables for rules
func (ds *DatabaseService) CreateRulesTables() error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
// Create rulesets table
createRuleSets := `
CREATE TABLE IF NOT EXISTS rulesets (
ruleset_id INTEGER PRIMARY KEY,
ruleset_name TEXT NOT NULL UNIQUE,
ruleset_active INTEGER NOT NULL DEFAULT 0
)`
err := ds.db.Exec(createRuleSets)
if err != nil {
return fmt.Errorf("error creating rulesets table: %v", err)
}
// Create ruleset_details table
createRuleSetDetails := `
CREATE TABLE IF NOT EXISTS ruleset_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ruleset_id INTEGER NOT NULL,
rule_category TEXT NOT NULL,
rule_type TEXT NOT NULL,
rule_value TEXT NOT NULL,
description TEXT,
FOREIGN KEY (ruleset_id) REFERENCES rulesets(ruleset_id) ON DELETE CASCADE
)`
err = ds.db.Exec(createRuleSetDetails)
if err != nil {
return fmt.Errorf("error creating ruleset_details table: %v", err)
}
// Create variables table if it doesn't exist
createVariables := `
CREATE TABLE IF NOT EXISTS variables (
variable_name TEXT PRIMARY KEY,
variable_value TEXT NOT NULL,
comment TEXT
)`
err = ds.db.Exec(createVariables)
if err != nil {
return fmt.Errorf("error creating variables table: %v", err)
}
// Create indexes for better performance
indexes := []string{
"CREATE INDEX IF NOT EXISTS idx_ruleset_details_ruleset_id ON ruleset_details(ruleset_id)",
"CREATE INDEX IF NOT EXISTS idx_ruleset_details_category_type ON ruleset_details(rule_category, rule_type)",
"CREATE INDEX IF NOT EXISTS idx_rulesets_active ON rulesets(ruleset_active)",
}
for _, indexSQL := range indexes {
err = ds.db.Exec(indexSQL)
if err != nil {
return fmt.Errorf("error creating index: %v", err)
}
}
return nil
}

View File

@ -0,0 +1,301 @@
package rules
// RuleAware defines the interface for entities that can use rules
type RuleAware interface {
// GetRuleManager returns the rule manager instance
GetRuleManager() *RuleManager
// GetZoneID returns the zone ID for zone-specific rules (0 for global)
GetZoneID() int32
}
// RuleProvider defines the interface for providing rule access
type RuleProvider interface {
// GetRule gets a rule by category and type, with optional zone override
GetRule(category RuleCategory, ruleType RuleType, zoneID int32) *Rule
// GetRuleByName gets a rule by category and type names, with optional zone override
GetRuleByName(categoryName string, typeName string, zoneID int32) *Rule
// GetGlobalRule gets a rule from the global rule set
GetGlobalRule(category RuleCategory, ruleType RuleType) *Rule
// GetZoneRule gets a zone-specific rule, falling back to global
GetZoneRule(zoneID int32, category RuleCategory, ruleType RuleType) *Rule
}
// DatabaseInterface defines the interface for rule database operations
type DatabaseInterface interface {
// LoadRuleSets loads all rule sets from database
LoadRuleSets(ruleManager *RuleManager, reload bool) error
// LoadGlobalRuleSet loads the global rule set
LoadGlobalRuleSet(ruleManager *RuleManager) error
// SaveRuleSet saves a rule set to database
SaveRuleSet(ruleSet *RuleSet) error
// DeleteRuleSet deletes a rule set from database
DeleteRuleSet(ruleSetID int32) error
// SetDefaultRuleSet sets the default rule set
SetDefaultRuleSet(ruleSetID int32) error
// GetDefaultRuleSetID gets the default rule set ID
GetDefaultRuleSetID() (int32, error)
// GetRuleSetList gets all rule sets
GetRuleSetList() ([]RuleSetInfo, error)
// ValidateDatabase validates required tables exist
ValidateDatabase() error
// CreateRulesTables creates the necessary database tables
CreateRulesTables() error
}
// ConfigurationProvider defines the interface for configuration-based rule access
type ConfigurationProvider interface {
// GetConfigValue gets a configuration value by key
GetConfigValue(key string) (string, bool)
// SetConfigValue sets a configuration value
SetConfigValue(key string, value string) error
// GetConfigValueInt gets a configuration value as integer
GetConfigValueInt(key string, defaultValue int32) int32
// GetConfigValueFloat gets a configuration value as float
GetConfigValueFloat(key string, defaultValue float64) float64
// GetConfigValueBool gets a configuration value as boolean
GetConfigValueBool(key string, defaultValue bool) bool
}
// RuleValidator defines the interface for rule validation
type RuleValidator interface {
// ValidateRule validates a rule's value
ValidateRule(category RuleCategory, ruleType RuleType, value string) error
// ValidateRuleSet validates an entire rule set
ValidateRuleSet(ruleSet *RuleSet) []error
// GetValidationRules gets validation rules for a category
GetValidationRules(category RuleCategory) map[RuleType]ValidationRule
}
// ValidationRule defines validation criteria for a rule
type ValidationRule struct {
Required bool // Whether the rule is required
MinValue interface{} // Minimum allowed value (for numeric types)
MaxValue interface{} // Maximum allowed value (for numeric types)
ValidValues []string // List of valid string values
Pattern string // Regex pattern for validation
Description string // Description of the rule
}
// RuleEventHandler defines the interface for rule change events
type RuleEventHandler interface {
// OnRuleChanged is called when a rule value changes
OnRuleChanged(category RuleCategory, ruleType RuleType, oldValue string, newValue string)
// OnRuleSetChanged is called when a rule set is modified
OnRuleSetChanged(ruleSetID int32, action string)
// OnGlobalRuleSetChanged is called when the global rule set changes
OnGlobalRuleSetChanged(oldRuleSetID int32, newRuleSetID int32)
}
// RuleCache defines the interface for rule caching
type RuleCache interface {
// GetCachedRule gets a cached rule
GetCachedRule(key string) (*Rule, bool)
// CacheRule caches a rule
CacheRule(key string, rule *Rule, ttl int64)
// InvalidateCache invalidates cached rules
InvalidateCache(pattern string)
// ClearCache clears all cached rules
ClearCache()
// GetCacheStats gets cache statistics
GetCacheStats() CacheStats
}
// CacheStats contains cache performance statistics
type CacheStats struct {
Hits int64 // Number of cache hits
Misses int64 // Number of cache misses
Entries int64 // Number of cached entries
Memory int64 // Memory usage in bytes
HitRatio float64 // Cache hit ratio
LastCleared int64 // Timestamp of last cache clear
}
// RuleManagerAdapter provides a simplified interface to the rule manager
type RuleManagerAdapter struct {
ruleManager *RuleManager
zoneID int32
}
// NewRuleManagerAdapter creates a new rule manager adapter
func NewRuleManagerAdapter(ruleManager *RuleManager, zoneID int32) *RuleManagerAdapter {
return &RuleManagerAdapter{
ruleManager: ruleManager,
zoneID: zoneID,
}
}
// GetRule gets a rule using the adapter's zone ID
func (rma *RuleManagerAdapter) GetRule(category RuleCategory, ruleType RuleType) *Rule {
if rma.zoneID != 0 {
return rma.ruleManager.GetZoneRule(rma.zoneID, category, ruleType)
}
return rma.ruleManager.GetGlobalRule(category, ruleType)
}
// GetRuleByName gets a rule by name using the adapter's zone ID
func (rma *RuleManagerAdapter) GetRuleByName(categoryName string, typeName string) *Rule {
rule := rma.ruleManager.GetGlobalRuleByName(categoryName, typeName)
// If we have a zone ID, check for zone-specific override
if rma.zoneID != 0 && rule != nil {
zoneRule := rma.ruleManager.GetZoneRule(rma.zoneID, rule.GetCategory(), rule.GetType())
if zoneRule != nil {
return zoneRule
}
}
return rule
}
// GetInt32 gets a rule value as int32
func (rma *RuleManagerAdapter) GetInt32(category RuleCategory, ruleType RuleType) int32 {
rule := rma.GetRule(category, ruleType)
if rule != nil {
return rule.GetInt32()
}
return 0
}
// GetFloat64 gets a rule value as float64
func (rma *RuleManagerAdapter) GetFloat64(category RuleCategory, ruleType RuleType) float64 {
rule := rma.GetRule(category, ruleType)
if rule != nil {
return rule.GetFloat64()
}
return 0.0
}
// GetBool gets a rule value as bool
func (rma *RuleManagerAdapter) GetBool(category RuleCategory, ruleType RuleType) bool {
rule := rma.GetRule(category, ruleType)
if rule != nil {
return rule.GetBool()
}
return false
}
// GetString gets a rule value as string
func (rma *RuleManagerAdapter) GetString(category RuleCategory, ruleType RuleType) string {
rule := rma.GetRule(category, ruleType)
if rule != nil {
return rule.GetString()
}
return ""
}
// SetZoneID sets the zone ID for the adapter
func (rma *RuleManagerAdapter) SetZoneID(zoneID int32) {
rma.zoneID = zoneID
}
// GetZoneID gets the current zone ID
func (rma *RuleManagerAdapter) GetZoneID() int32 {
return rma.zoneID
}
// RuleServiceConfig contains configuration for rule services
type RuleServiceConfig struct {
DatabaseEnabled bool // Whether to use database storage
CacheEnabled bool // Whether to enable rule caching
CacheTTL int64 // Cache TTL in seconds
MaxCacheSize int64 // Maximum cache size in bytes
EventHandlers []RuleEventHandler // Event handlers to register
Validators []RuleValidator // Validators to register
}
// RuleService provides high-level rule management functionality
type RuleService struct {
ruleManager *RuleManager
database DatabaseInterface
cache RuleCache
eventHandlers []RuleEventHandler
validators []RuleValidator
config RuleServiceConfig
}
// NewRuleService creates a new rule service
func NewRuleService(config RuleServiceConfig) *RuleService {
return &RuleService{
ruleManager: NewRuleManager(),
eventHandlers: config.EventHandlers,
validators: config.Validators,
config: config,
}
}
// Initialize initializes the rule service
func (rs *RuleService) Initialize() error {
// Load rules from database if enabled
if rs.config.DatabaseEnabled && rs.database != nil {
err := rs.database.LoadRuleSets(rs.ruleManager, false)
if err != nil {
return err
}
}
return nil
}
// GetRuleManager returns the underlying rule manager
func (rs *RuleService) GetRuleManager() *RuleManager {
return rs.ruleManager
}
// GetAdapter returns a rule manager adapter for a specific zone
func (rs *RuleService) GetAdapter(zoneID int32) *RuleManagerAdapter {
return NewRuleManagerAdapter(rs.ruleManager, zoneID)
}
// SetDatabase sets the database service
func (rs *RuleService) SetDatabase(db DatabaseInterface) {
rs.database = db
}
// SetCache sets the cache service
func (rs *RuleService) SetCache(cache RuleCache) {
rs.cache = cache
}
// AddEventHandler adds an event handler
func (rs *RuleService) AddEventHandler(handler RuleEventHandler) {
rs.eventHandlers = append(rs.eventHandlers, handler)
}
// AddValidator adds a validator
func (rs *RuleService) AddValidator(validator RuleValidator) {
rs.validators = append(rs.validators, validator)
}
// Shutdown shuts down the rule service
func (rs *RuleService) Shutdown() error {
// Clear cache if enabled
if rs.cache != nil {
rs.cache.ClearCache()
}
return nil
}

580
internal/rules/manager.go Normal file
View File

@ -0,0 +1,580 @@
package rules
import (
"fmt"
"log"
"sync"
)
// RuleManager manages all rule sets and provides rule lookup functionality
// Converted from C++ RuleManager class
type RuleManager struct {
// Core rule storage
rules map[RuleCategory]map[RuleType]*Rule // Default rules from code
ruleSets map[int32]*RuleSet // Rule sets loaded from database
globalRuleSet *RuleSet // Active global rule set
zoneRuleSets map[int32]*RuleSet // Zone-specific rule sets
blankRule *Rule // Default blank rule
// Thread safety
rulesMutex sync.RWMutex
ruleSetsMutex sync.RWMutex
globalRuleSetMutex sync.RWMutex
zoneRuleSetsMutex sync.RWMutex
// Statistics
stats RuleManagerStats
// Configuration
initialized bool
}
// NewRuleManager creates a new rule manager instance
func NewRuleManager() *RuleManager {
rm := &RuleManager{
rules: make(map[RuleCategory]map[RuleType]*Rule),
ruleSets: make(map[int32]*RuleSet),
globalRuleSet: NewRuleSet(),
zoneRuleSets: make(map[int32]*RuleSet),
blankRule: NewRule(),
initialized: false,
}
rm.Init()
// Load coded defaults into global rule set
rm.LoadCodedDefaultsIntoRuleSet(rm.globalRuleSet)
return rm
}
// Init initializes the rule manager with default rules from code
// Equivalent to C++ RuleManager::Init()
func (rm *RuleManager) Init() {
rm.rulesMutex.Lock()
defer rm.rulesMutex.Unlock()
// Clear existing rules
rm.rules = make(map[RuleCategory]map[RuleType]*Rule)
// Initialize all default rules with their coded values
// This is a direct conversion of the RULE_INIT macro usage in C++
// CLIENT RULES
rm.initRule(CategoryClient, ClientShowWelcomeScreen, "0", "Client:ShowWelcomeScreen")
rm.initRule(CategoryClient, ClientGroupSpellsTimer, "1000", "Client:GroupSpellsTimer")
rm.initRule(CategoryClient, ClientQuestQueueTimer, "50", "Client:QuestQueueTimer")
// FACTION RULES
rm.initRule(CategoryFaction, FactionAllowBasedCombat, "1", "Faction:AllowFactionBasedCombat")
// GUILD RULES
rm.initRule(CategoryGuild, GuildMaxLevel, "50", "Guild:MaxLevel")
rm.initRule(CategoryGuild, GuildMaxPlayers, "-1", "Guild:MaxPlayers")
// PLAYER RULES
rm.initRule(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel")
rm.initRule(CategoryPlayer, PlayerMaxLevelOverrideStatus, "100", "Player:MaxLevelOverrideStatus")
rm.initRule(CategoryPlayer, PlayerVitalityAmount, ".5", "Player:VitalityAmount")
rm.initRule(CategoryPlayer, PlayerVitalityFrequency, "3600", "Player:VitalityFrequency")
rm.initRule(CategoryPlayer, PlayerMaxAA, "320", "Player:MaxAA")
rm.initRule(CategoryPlayer, PlayerMaxClassAA, "100", "Player:MaxClassAA")
rm.initRule(CategoryPlayer, PlayerMaxSubclassAA, "100", "Player:MaxSubclassAA")
rm.initRule(CategoryPlayer, PlayerMaxShadowsAA, "70", "Player:MaxShadowsAA")
rm.initRule(CategoryPlayer, PlayerMaxHeroicAA, "50", "Player:MaxHeroicAA")
rm.initRule(CategoryPlayer, PlayerMaxTradeskillAA, "40", "Player:MaxTradeskillAA")
rm.initRule(CategoryPlayer, PlayerMaxPrestigeAA, "25", "Player:MaxPrestigeAA")
rm.initRule(CategoryPlayer, PlayerMaxTradeskillPrestigeAA, "25", "Player:MaxTradeskillPrestigeAA")
rm.initRule(CategoryPlayer, PlayerMinLastNameLevel, "20", "Player:MinLastNameLevel")
rm.initRule(CategoryPlayer, PlayerMaxLastNameLength, "20", "Player:MaxLastNameLength")
rm.initRule(CategoryPlayer, PlayerMinLastNameLength, "4", "Player:MinLastNameLength")
rm.initRule(CategoryPlayer, PlayerDisableHouseAlignmentRequirement, "1", "Player:DisableHouseAlignmentRequirement")
rm.initRule(CategoryPlayer, PlayerMentorItemDecayRate, ".05", "Player:MentorItemDecayRate")
rm.initRule(CategoryPlayer, PlayerTemporaryItemLogoutTime, "1800.0", "Player:TemporaryItemLogoutTime")
rm.initRule(CategoryPlayer, PlayerHeirloomItemShareExpiration, "172800.0", "Player:HeirloomItemShareExpiration")
rm.initRule(CategoryPlayer, PlayerSwimmingSkillMinSpeed, "20", "Player:SwimmingSkillMinSpeed")
rm.initRule(CategoryPlayer, PlayerSwimmingSkillMaxSpeed, "200", "Player:SwimmingSkillMaxSpeed")
rm.initRule(CategoryPlayer, PlayerSwimmingSkillMinBreathLength, "30", "Player:SwimmingSkillMinBreathLength")
rm.initRule(CategoryPlayer, PlayerSwimmingSkillMaxBreathLength, "1000", "Player:SwimmingSkillMaxBreathLength")
rm.initRule(CategoryPlayer, PlayerAutoSkillUpBaseSkills, "0", "Player:AutoSkillUpBaseSkills")
rm.initRule(CategoryPlayer, PlayerMaxWeightStrengthMultiplier, "2.0", "Player:MaxWeightStrengthMultiplier")
rm.initRule(CategoryPlayer, PlayerBaseWeight, "50", "Player:BaseWeight")
rm.initRule(CategoryPlayer, PlayerWeightPercentImpact, "0.01", "Player:WeightPercentImpact")
rm.initRule(CategoryPlayer, PlayerWeightPercentCap, "0.95", "Player:WeightPercentCap")
rm.initRule(CategoryPlayer, PlayerCoinWeightPerStone, "40.0", "Player:CoinWeightPerStone")
rm.initRule(CategoryPlayer, PlayerWeightInflictsSpeed, "1", "Player:WeightInflictsSpeed")
rm.initRule(CategoryPlayer, PlayerLevelMasterySkillMultiplier, "5", "Player:LevelMasterySkillMultiplier")
rm.initRule(CategoryPlayer, PlayerTraitTieringSelection, "1", "Player:TraitTieringSelection")
rm.initRule(CategoryPlayer, PlayerClassicTraitLevelTable, "1", "Player:ClassicTraitLevelTable")
rm.initRule(CategoryPlayer, PlayerTraitFocusSelectLevel, "9", "Player:TraitFocusSelectLevel")
rm.initRule(CategoryPlayer, PlayerTraitTrainingSelectLevel, "10", "Player:TraitTrainingSelectLevel")
rm.initRule(CategoryPlayer, PlayerTraitRaceSelectLevel, "10", "Player:TraitRaceSelectLevel")
rm.initRule(CategoryPlayer, PlayerTraitCharacterSelectLevel, "10", "Player:TraitCharacterSelectLevel")
rm.initRule(CategoryPlayer, PlayerStartHPBase, "40", "Player:StartHPBase")
rm.initRule(CategoryPlayer, PlayerStartPowerBase, "45", "Player:StartPowerBase")
rm.initRule(CategoryPlayer, PlayerStartHPLevelMod, "2.0", "Player:StartHPLevelMod")
rm.initRule(CategoryPlayer, PlayerStartPowerLevelMod, "2.1", "Player:StartPowerLevelMod")
rm.initRule(CategoryPlayer, PlayerAllowEquipCombat, "1", "Player:AllowPlayerEquipCombat")
rm.initRule(CategoryPlayer, PlayerMaxTargetCommandDistance, "50.0", "Player:MaxTargetCommandDistance")
rm.initRule(CategoryPlayer, PlayerMinSkillMultiplierValue, "30", "Player:MinSkillMultiplierValue")
rm.initRule(CategoryPlayer, PlayerHarvestSkillUpMultiplier, "2.0", "Player:HarvestSkillUpMultiplier")
rm.initRule(CategoryPlayer, PlayerMiniDingPercentage, "10", "Player:MiniDingPercentage")
// PVP RULES
rm.initRule(CategoryPVP, PVPAllowPVP, "0", "PVP:AllowPVP")
rm.initRule(CategoryPVP, PVPLevelRange, "4", "PVP:LevelRange")
rm.initRule(CategoryPVP, PVPInvisPlayerDiscoveryRange, "20", "PVP:InvisPlayerDiscoveryRange")
rm.initRule(CategoryPVP, PVPMitigationModByLevel, "25", "PVP:PVPMitigationModByLevel")
rm.initRule(CategoryPVP, PVPType, "0", "PVP:PVPType")
// COMBAT RULES
rm.initRule(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange")
rm.initRule(CategoryCombat, CombatDeathExperienceDebt, "50.00", "Combat:DeathExperienceDebt")
rm.initRule(CategoryCombat, CombatPVPDeathExperienceDebt, "25.00", "Combat:PVPDeathExperienceDebt")
rm.initRule(CategoryCombat, CombatGroupExperienceDebt, "0", "Combat:GroupExperienceDebt")
rm.initRule(CategoryCombat, CombatExperienceToDebt, "50.00", "Combat:ExperienceToDebt")
rm.initRule(CategoryCombat, CombatExperienceDebtRecoveryPercent, "5.00", "Combat:ExperienceDebtRecoveryPercent")
rm.initRule(CategoryCombat, CombatExperienceDebtRecoveryPeriod, "600", "Combat:ExperienceDebtRecoveryPeriod")
rm.initRule(CategoryCombat, CombatEnableSpiritShards, "1", "Combat:EnableSpiritShards")
rm.initRule(CategoryCombat, CombatSpiritShardSpawnScript, "SpawnScripts/Generic/SpiritShard.lua", "Combat:SpiritShardSpawnScript")
rm.initRule(CategoryCombat, CombatShardDebtRecoveryPercent, "25.00", "Combat:ShardDebtRecoveryPercent")
rm.initRule(CategoryCombat, CombatShardRecoveryByRadius, "1", "Combat:ShardRecoveryByRadius")
rm.initRule(CategoryCombat, CombatShardLifetime, "86400", "Combat:ShardLifetime")
rm.initRule(CategoryCombat, CombatEffectiveMitigationCapLevel, "80", "Combat:EffectiveMitigationCapLevel")
rm.initRule(CategoryCombat, CombatCalculatedMitigationCapLevel, "100", "Combat:CalculatedMitigationCapLevel")
rm.initRule(CategoryCombat, CombatMitigationLevelEffectivenessMax, "1.5", "Combat:MitigationLevelEffectivenessMax")
rm.initRule(CategoryCombat, CombatMitigationLevelEffectivenessMin, ".5", "Combat:MitigationLevelEffectivenessMin")
rm.initRule(CategoryCombat, CombatMaxMitigationAllowed, ".75", "Combat:MaxMitigationAllowed")
rm.initRule(CategoryCombat, CombatMaxMitigationAllowedPVP, ".75", "Combat:MaxMitigationAllowedPVP")
rm.initRule(CategoryCombat, CombatStrengthNPC, "10", "Combat:StrengthNPC")
rm.initRule(CategoryCombat, CombatStrengthOther, "25", "Combat:StrengthOther")
rm.initRule(CategoryCombat, CombatMaxSkillBonusByLevel, "1.5", "Combat:MaxSkillBonusByLevel")
rm.initRule(CategoryCombat, CombatLockedEncounterNoAttack, "1", "Combat:LockedEncounterNoAttack")
rm.initRule(CategoryCombat, CombatMaxChaseDistance, "0.0", "Combat:MaxChaseDistance")
// SPAWN RULES
rm.initRule(CategorySpawn, SpawnSpeedMultiplier, "300", "Spawn:SpeedMultiplier")
rm.initRule(CategorySpawn, SpawnClassicRegen, "0", "Spawn:ClassicRegen")
rm.initRule(CategorySpawn, SpawnHailMovementPause, "5000", "Spawn:HailMovementPause")
rm.initRule(CategorySpawn, SpawnHailDistance, "5", "Spawn:HailDistance")
rm.initRule(CategorySpawn, SpawnUseHardCodeWaterModelType, "1", "Spawn:UseHardCodeWaterModelType")
rm.initRule(CategorySpawn, SpawnUseHardCodeFlyingModelType, "1", "Spawn:UseHardCodeFlyingModelType")
// UI RULES
rm.initRule(CategoryUI, UIMaxWhoResults, "20", "UI:MaxWhoResults")
rm.initRule(CategoryUI, UIMaxWhoOverrideStatus, "200", "UI:MaxWhoOverrideStatus")
// WORLD RULES
rm.initRule(CategoryWorld, WorldDefaultStartingZoneID, "1", "World:DefaultStartingZoneID")
rm.initRule(CategoryWorld, WorldEnablePOIDiscovery, "0", "World:EnablePOIDiscovery")
rm.initRule(CategoryWorld, WorldGamblingTokenItemID, "2", "World:GamblingTokenItemID")
rm.initRule(CategoryWorld, WorldGuildAutoJoin, "0", "World:GuildAutoJoin")
rm.initRule(CategoryWorld, WorldGuildAutoJoinID, "1", "World:GuildAutoJoinID")
rm.initRule(CategoryWorld, WorldGuildAutoJoinDefaultRankID, "7", "World:GuildAutoJoinDefaultRankID")
rm.initRule(CategoryWorld, PlayerMaxPlayers, "-1", "World:MaxPlayers")
rm.initRule(CategoryWorld, PlayerMaxPlayersOverrideStatus, "100", "World:MaxPlayersOverrideStatus")
rm.initRule(CategoryWorld, WorldServerLocked, "0", "World:ServerLocked")
rm.initRule(CategoryWorld, WorldServerLockedOverrideStatus, "10", "World:ServerLockedOverrideStatus")
rm.initRule(CategoryWorld, WorldSyncZonesWithLogin, "1", "World:SyncZonesWithLogin")
rm.initRule(CategoryWorld, WorldSyncEquipWithLogin, "1", "World:SyncEquipWithLogin")
rm.initRule(CategoryWorld, WorldUseBannedIPsTable, "0", "World:UseBannedIPsTable")
rm.initRule(CategoryWorld, WorldLinkDeadTimer, "120000", "World:LinkDeadTimer")
rm.initRule(CategoryWorld, WorldRemoveDisconnectedClientsTimer, "30000", "World:RemoveDisconnectedClientsTimer")
rm.initRule(CategoryWorld, WorldPlayerCampTimer, "20", "World:PlayerCampTimer")
rm.initRule(CategoryWorld, WorldGMCampTimer, "1", "World:GMCampTimer")
rm.initRule(CategoryWorld, WorldAutoAdminPlayers, "0", "World:AutoAdminPlayers")
rm.initRule(CategoryWorld, WorldAutoAdminGMs, "0", "World:AutoAdminGMs")
rm.initRule(CategoryWorld, WorldAutoAdminStatusValue, "10", "World:AutoAdminStatusValue")
rm.initRule(CategoryWorld, WorldDuskTime, "20:00", "World:DuskTime")
rm.initRule(CategoryWorld, WorldDawnTime, "8:00", "World:DawnTime")
rm.initRule(CategoryWorld, WorldThreadedLoad, "0", "World:ThreadedLoad")
rm.initRule(CategoryWorld, WorldTradeskillSuccessChance, "87.0", "World:TradeskillSuccessChance")
rm.initRule(CategoryWorld, WorldTradeskillCritSuccessChance, "2.0", "World:TradeskillCritSuccessChance")
rm.initRule(CategoryWorld, WorldTradeskillFailChance, "10.0", "World:TradeskillFailChance")
rm.initRule(CategoryWorld, WorldTradeskillCritFailChance, "1.0", "World:TradeskillCritFailChance")
rm.initRule(CategoryWorld, WorldTradeskillEventChance, "15.0", "World:TradeskillEventChance")
rm.initRule(CategoryWorld, WorldEditorURL, "www.eq2emulator.net", "World:EditorURL")
rm.initRule(CategoryWorld, WorldEditorIncludeID, "0", "World:EditorIncludeID")
rm.initRule(CategoryWorld, WorldEditorOfficialServer, "0", "World:EditorOfficialServer")
rm.initRule(CategoryWorld, WorldSavePaperdollImage, "1", "World:SavePaperdollImage")
rm.initRule(CategoryWorld, WorldSaveHeadshotImage, "1", "World:SaveHeadshotImage")
rm.initRule(CategoryWorld, WorldSendPaperdollImagesToLogin, "1", "World:SendPaperdollImagesToLogin")
rm.initRule(CategoryWorld, WorldTreasureChestDisabled, "0", "World:TreasureChestDisabled")
rm.initRule(CategoryWorld, WorldStartingZoneLanguages, "0", "World:StartingZoneLanguages")
rm.initRule(CategoryWorld, WorldStartingZoneRuleFlag, "0", "World:StartingZoneRuleFlag")
rm.initRule(CategoryWorld, WorldEnforceRacialAlignment, "1", "World:EnforceRacialAlignment")
rm.initRule(CategoryWorld, WorldMemoryCacheZoneMaps, "0", "World:MemoryCacheZoneMaps")
rm.initRule(CategoryWorld, WorldAutoLockEncounter, "0", "World:AutoLockEncounter")
rm.initRule(CategoryWorld, WorldDisplayItemTiers, "1", "World:DisplayItemTiers")
rm.initRule(CategoryWorld, WorldLoreAndLegendAccept, "0", "World:LoreAndLegendAccept")
// ZONE RULES
rm.initRule(CategoryZone, PlayerMaxPlayers, "100", "Zone:MaxPlayers")
rm.initRule(CategoryZone, ZoneMinLevelOverrideStatus, "1", "Zone:MinZoneLevelOverrideStatus")
rm.initRule(CategoryZone, ZoneMinAccessOverrideStatus, "100", "Zone:MinZoneAccessOverrideStatus")
rm.initRule(CategoryZone, ZoneWeatherEnabled, "1", "Zone:WeatherEnabled")
rm.initRule(CategoryZone, ZoneWeatherType, "0", "Zone:WeatherType")
rm.initRule(CategoryZone, ZoneMinWeatherSeverity, "0.0", "Zone:MinWeatherSeverity")
rm.initRule(CategoryZone, ZoneMaxWeatherSeverity, "1.0", "Zone:MaxWeatherSeverity")
rm.initRule(CategoryZone, ZoneWeatherChangeFrequency, "300", "Zone:WeatherChangeFrequency")
rm.initRule(CategoryZone, ZoneWeatherChangePerInterval, "0.02", "Zone:WeatherChangePerInterval")
rm.initRule(CategoryZone, ZoneWeatherChangeChance, "20", "Zone:WeatherChangeChance")
rm.initRule(CategoryZone, ZoneWeatherDynamicMaxOffset, "0.08", "Zone:WeatherDynamicMaxOffset")
rm.initRule(CategoryZone, ZoneSpawnUpdateTimer, "50", "Zone:SpawnUpdateTimer")
rm.initRule(CategoryZone, ZoneCheckAttackNPC, "2000", "Zone:CheckAttackNPC")
rm.initRule(CategoryZone, ZoneCheckAttackPlayer, "2000", "Zone:CheckAttackPlayer")
rm.initRule(CategoryZone, ZoneHOTime, "10.0", "Zone:HOTime")
rm.initRule(CategoryZone, ZoneRegenTimer, "6000", "Zone:RegenTimer")
rm.initRule(CategoryZone, ZoneClientSaveTimer, "60000", "Zone:ClientSaveTimer")
rm.initRule(CategoryZone, ZoneShutdownDelayTimer, "120000", "Zone:ShutdownDelayTimer")
rm.initRule(CategoryZone, ZoneWeatherTimer, "60000", "Zone:WeatherTimer")
rm.initRule(CategoryZone, ZoneSpawnDeleteTimer, "30000", "Zone:SpawnDeleteTimer")
rm.initRule(CategoryZone, ZoneUseMapUnderworldCoords, "1", "Zone:UseMapUnderworldCoords")
rm.initRule(CategoryZone, ZoneMapUnderworldCoordOffset, "-200.0", "Zone:MapUnderworldCoordOffset")
rm.initRule(CategoryZone, ZoneSharedMaxPlayers, "30", "Zone:SharedZoneMaxPlayers")
// LOOT RULES
rm.initRule(CategoryLoot, LootRadius, "5.0", "Loot:LootRadius")
rm.initRule(CategoryLoot, LootAutoDisarmChest, "1", "Loot:AutoDisarmChest")
rm.initRule(CategoryLoot, LootChestTriggerRadiusGroup, "10.0", "Loot:ChestTriggerRadiusGroup")
rm.initRule(CategoryLoot, LootChestUnlockedTimeDrop, "1200", "Loot:ChestUnlockedTimeDrop")
rm.initRule(CategoryLoot, LootAllowChestUnlockByDropTime, "1", "Loot:AllowChestUnlockByDropTime")
rm.initRule(CategoryLoot, LootChestUnlockedTimeTrap, "600", "Loot:ChestUnlockedTimeTrap")
rm.initRule(CategoryLoot, LootAllowChestUnlockByTrapTime, "1", "Loot:AllowChestUnlockByTrapTime")
rm.initRule(CategoryLoot, LootSkipGrayMob, "1", "Loot:SkipLootGrayMob")
rm.initRule(CategoryLoot, LootDistributionTime, "120", "Loot:LootDistributionTime")
// SPELLS RULES
rm.initRule(CategorySpells, SpellsNoInterruptBaseChance, "50", "Spells:NoInterruptBaseChance")
rm.initRule(CategorySpells, SpellsEnableFizzleSpells, "1", "Spells:EnableFizzleSpells")
rm.initRule(CategorySpells, SpellsDefaultFizzleChance, "10.0", "Spells:DefaultFizzleChance")
rm.initRule(CategorySpells, SpellsFizzleMaxSkill, "1.2", "Spells:FizzleMaxSkill")
rm.initRule(CategorySpells, SpellsFizzleDefaultSkill, ".2", "Spells:FizzleDefaultSkill")
rm.initRule(CategorySpells, SpellsEnableCrossZoneGroupBuffs, "0", "Spells:EnableCrossZoneGroupBuffs")
rm.initRule(CategorySpells, SpellsEnableCrossZoneTargetBuffs, "0", "Spells:EnableCrossZoneTargetBuffs")
rm.initRule(CategorySpells, SpellsPlayerSpellSaveStateWaitInterval, "100", "Spells:PlayerSpellSaveStateWaitInterval")
rm.initRule(CategorySpells, SpellsPlayerSpellSaveStateCap, "1000", "Spells:PlayerSpellSaveStateCap")
rm.initRule(CategorySpells, SpellsRequirePreviousTierScribe, "0", "Spells:RequirePreviousTierScribe")
rm.initRule(CategorySpells, SpellsCureSpellID, "110003", "Spells:CureSpellID")
rm.initRule(CategorySpells, SpellsCureCurseSpellID, "110004", "Spells:CureCurseSpellID")
rm.initRule(CategorySpells, SpellsCureNoxiousSpellID, "110005", "Spells:CureNoxiousSpellID")
rm.initRule(CategorySpells, SpellsCureMagicSpellID, "210006", "Spells:CureMagicSpellID")
rm.initRule(CategorySpells, SpellsCureTraumaSpellID, "0", "Spells:CureTraumaSpellID")
rm.initRule(CategorySpells, SpellsCureArcaneSpellID, "0", "Spells:CureArcaneSpellID")
rm.initRule(CategorySpells, SpellsMinistrationSkillID, "366253016", "Spells:MinistrationSkillID")
rm.initRule(CategorySpells, SpellsMinistrationPowerReductionMax, "15.0", "Spells:MinistrationPowerReductionMax")
rm.initRule(CategorySpells, SpellsMinistrationPowerReductionSkill, "25", "Spells:MinistrationPowerReductionSkill")
rm.initRule(CategorySpells, SpellsMasterSkillReduceSpellResist, "25", "Spells:MasterSkillReduceSpellResist")
rm.initRule(CategorySpells, SpellsUseClassicSpellLevel, "0", "Spells:UseClassicSpellLevel")
// EXPANSION RULES
rm.initRule(CategoryExpansion, ExpansionGlobalFlag, "0", "Expansion:GlobalExpansionFlag")
rm.initRule(CategoryExpansion, ExpansionHolidayFlag, "0", "Expansion:GlobalHolidayFlag")
// DISCORD RULES
rm.initRule(CategoryDiscord, DiscordEnabled, "0", "Discord:DiscordEnabled")
rm.initRule(CategoryDiscord, DiscordWebhookURL, "None", "Discord:DiscordWebhookURL")
rm.initRule(CategoryDiscord, DiscordBotToken, "None", "Discord:DiscordBotToken")
rm.initRule(CategoryDiscord, DiscordChannel, "Discord", "Discord:DiscordChannel")
rm.initRule(CategoryDiscord, DiscordListenChan, "0", "Discord:DiscordListenChan")
rm.initialized = true
log.Printf("[Rules] Initialized rule manager with %d categories", len(rm.rules))
}
// initRule is a helper function equivalent to the RULE_INIT macro
func (rm *RuleManager) initRule(category RuleCategory, ruleType RuleType, value string, combined string) {
if rm.rules[category] == nil {
rm.rules[category] = make(map[RuleType]*Rule)
}
rm.rules[category][ruleType] = NewRuleWithValues(category, ruleType, value, combined)
}
// Flush clears all rule sets and optionally reinitializes with defaults
func (rm *RuleManager) Flush(reinit bool) {
// Clear default rules
rm.rulesMutex.Lock()
rm.rules = make(map[RuleCategory]map[RuleType]*Rule)
rm.rulesMutex.Unlock()
// Clear rule sets
rm.ClearRuleSets()
rm.ClearZoneRuleSets()
if reinit {
rm.Init()
}
rm.initialized = reinit
}
// LoadCodedDefaultsIntoRuleSet loads the coded default rules into a rule set
func (rm *RuleManager) LoadCodedDefaultsIntoRuleSet(ruleSet *RuleSet) {
if ruleSet == nil {
return
}
rm.rulesMutex.RLock()
defer rm.rulesMutex.RUnlock()
for _, typeMap := range rm.rules {
for _, rule := range typeMap {
ruleSet.AddRule(NewRuleFromRule(rule))
}
}
}
// AddRuleSet adds a rule set to the manager
func (rm *RuleManager) AddRuleSet(ruleSet *RuleSet) bool {
if ruleSet == nil {
return false
}
id := ruleSet.GetID()
rm.ruleSetsMutex.Lock()
defer rm.ruleSetsMutex.Unlock()
if _, exists := rm.ruleSets[id]; exists {
return false // Rule set with this ID already exists
}
rm.ruleSets[id] = ruleSet
rm.stats.IncrementRuleSetOperations()
// Update stats
rm.stats.mutex.Lock()
rm.stats.TotalRuleSets = int32(len(rm.ruleSets))
totalRules := int32(0)
for _, rs := range rm.ruleSets {
totalRules += int32(rs.Size())
}
rm.stats.TotalRules = totalRules
rm.stats.mutex.Unlock()
return true
}
// GetNumRuleSets returns the number of rule sets
func (rm *RuleManager) GetNumRuleSets() int32 {
rm.ruleSetsMutex.RLock()
defer rm.ruleSetsMutex.RUnlock()
return int32(len(rm.ruleSets))
}
// ClearRuleSets removes all rule sets
func (rm *RuleManager) ClearRuleSets() {
rm.ruleSetsMutex.Lock()
defer rm.ruleSetsMutex.Unlock()
rm.ruleSets = make(map[int32]*RuleSet)
// Update stats
rm.stats.mutex.Lock()
rm.stats.TotalRuleSets = 0
rm.stats.TotalRules = 0
rm.stats.mutex.Unlock()
}
// SetGlobalRuleSet sets the global rule set by copying from an existing rule set
func (rm *RuleManager) SetGlobalRuleSet(ruleSetID int32) bool {
rm.ruleSetsMutex.RLock()
sourceRuleSet, exists := rm.ruleSets[ruleSetID]
rm.ruleSetsMutex.RUnlock()
if !exists {
return false
}
rm.globalRuleSetMutex.Lock()
defer rm.globalRuleSetMutex.Unlock()
rm.globalRuleSet.CopyRulesInto(sourceRuleSet)
// Update stats
rm.stats.mutex.Lock()
rm.stats.GlobalRuleSetID = ruleSetID
rm.stats.mutex.Unlock()
return true
}
// GetGlobalRule gets a rule from the global rule set by category and type
func (rm *RuleManager) GetGlobalRule(category RuleCategory, ruleType RuleType) *Rule {
rm.globalRuleSetMutex.RLock()
defer rm.globalRuleSetMutex.RUnlock()
rule := rm.globalRuleSet.GetRule(category, ruleType)
if rule != nil {
rm.stats.IncrementRuleGetOperations()
log.Printf("[Rules] Rule: %s, Value: %s", rule.GetCombined(), rule.GetValue())
return rule
}
// Return blank rule if not found (matching C++ behavior)
return rm.blankRule
}
// GetGlobalRuleByName gets a rule from the global rule set by name
func (rm *RuleManager) GetGlobalRuleByName(categoryName string, typeName string) *Rule {
rm.globalRuleSetMutex.RLock()
defer rm.globalRuleSetMutex.RUnlock()
rule := rm.globalRuleSet.GetRuleByName(categoryName, typeName)
if rule != nil {
rm.stats.IncrementRuleGetOperations()
return rule
}
return rm.blankRule
}
// SetZoneRuleSet sets a zone-specific rule set
func (rm *RuleManager) SetZoneRuleSet(zoneID int32, ruleSetID int32) bool {
rm.ruleSetsMutex.RLock()
ruleSet, exists := rm.ruleSets[ruleSetID]
rm.ruleSetsMutex.RUnlock()
if !exists {
return false
}
rm.zoneRuleSetsMutex.Lock()
defer rm.zoneRuleSetsMutex.Unlock()
rm.zoneRuleSets[zoneID] = ruleSet
// Update stats
rm.stats.mutex.Lock()
rm.stats.ZoneRuleSets = int32(len(rm.zoneRuleSets))
rm.stats.mutex.Unlock()
return true
}
// GetZoneRule gets a rule for a specific zone, falling back to global rules
func (rm *RuleManager) GetZoneRule(zoneID int32, category RuleCategory, ruleType RuleType) *Rule {
var rule *Rule
// First try to get the zone-specific rule
if zoneID != 0 {
rm.zoneRuleSetsMutex.RLock()
if zoneRuleSet, exists := rm.zoneRuleSets[zoneID]; exists {
rule = zoneRuleSet.GetRule(category, ruleType)
}
rm.zoneRuleSetsMutex.RUnlock()
}
// Fall back to global rule if zone rule not found
if rule == nil {
rule = rm.GetGlobalRule(category, ruleType)
}
return rule
}
// ClearZoneRuleSets removes all zone-specific rule sets
func (rm *RuleManager) ClearZoneRuleSets() {
rm.zoneRuleSetsMutex.Lock()
defer rm.zoneRuleSetsMutex.Unlock()
rm.zoneRuleSets = make(map[int32]*RuleSet)
// Update stats
rm.stats.mutex.Lock()
rm.stats.ZoneRuleSets = 0
rm.stats.mutex.Unlock()
}
// GetBlankRule returns the blank rule for missing rules
func (rm *RuleManager) GetBlankRule() *Rule {
return rm.blankRule
}
// GetGlobalRuleSet returns the global rule set
func (rm *RuleManager) GetGlobalRuleSet() *RuleSet {
rm.globalRuleSetMutex.RLock()
defer rm.globalRuleSetMutex.RUnlock()
return rm.globalRuleSet
}
// GetRules returns the default rules map
func (rm *RuleManager) GetRules() map[RuleCategory]map[RuleType]*Rule {
rm.rulesMutex.RLock()
defer rm.rulesMutex.RUnlock()
// Return a deep copy to prevent external modification
rulesCopy := make(map[RuleCategory]map[RuleType]*Rule)
for category, typeMap := range rm.rules {
rulesCopy[category] = make(map[RuleType]*Rule)
for ruleType, rule := range typeMap {
rulesCopy[category][ruleType] = NewRuleFromRule(rule)
}
}
return rulesCopy
}
// GetRuleSet returns a rule set by ID
func (rm *RuleManager) GetRuleSet(id int32) *RuleSet {
rm.ruleSetsMutex.RLock()
defer rm.ruleSetsMutex.RUnlock()
if ruleSet, exists := rm.ruleSets[id]; exists {
return ruleSet
}
return nil
}
// GetAllRuleSets returns all rule sets
func (rm *RuleManager) GetAllRuleSets() map[int32]*RuleSet {
rm.ruleSetsMutex.RLock()
defer rm.ruleSetsMutex.RUnlock()
// Return a copy of the map
ruleSetsCopy := make(map[int32]*RuleSet)
for id, ruleSet := range rm.ruleSets {
ruleSetsCopy[id] = ruleSet
}
return ruleSetsCopy
}
// GetStats returns current rule manager statistics
func (rm *RuleManager) GetStats() RuleManagerStats {
return rm.stats.GetSnapshot()
}
// ResetStats resets all statistics counters
func (rm *RuleManager) ResetStats() {
rm.stats.Reset()
}
// IsInitialized returns whether the rule manager has been initialized
func (rm *RuleManager) IsInitialized() bool {
return rm.initialized
}
// ValidateRule validates a rule's category, type, and value
func (rm *RuleManager) ValidateRule(category RuleCategory, ruleType RuleType, value string) error {
if len(value) > MaxRuleValueLength {
return ErrRuleValueTooLong
}
// Additional validation can be added here for specific rule types
return nil
}
// GetRuleInfo returns information about a specific rule
func (rm *RuleManager) GetRuleInfo(category RuleCategory, ruleType RuleType) string {
rule := rm.GetGlobalRule(category, ruleType)
if rule == nil || !rule.IsValid() {
return "Rule not found"
}
return fmt.Sprintf("Rule: %s = %s", rule.GetCombined(), rule.GetValue())
}
// String returns a string representation of the rule manager
func (rm *RuleManager) String() string {
stats := rm.GetStats()
return fmt.Sprintf("RuleManager{RuleSets: %d, Rules: %d, GlobalID: %d, ZoneRuleSets: %d}",
stats.TotalRuleSets, stats.TotalRules, stats.GlobalRuleSetID, stats.ZoneRuleSets)
}

View File

@ -0,0 +1,430 @@
package rules
import (
"testing"
)
// Test Rule creation and basic functionality
func TestRule(t *testing.T) {
// Test NewRule with default values
rule := NewRule()
if rule == nil {
t.Fatal("NewRule() returned nil")
}
if rule.GetCategory() != 0 {
t.Errorf("Expected category 0, got %d", rule.GetCategory())
}
if rule.GetType() != 0 {
t.Errorf("Expected type 0, got %d", rule.GetType())
}
if rule.GetValue() != "" {
t.Errorf("Expected empty value, got %s", rule.GetValue())
}
if rule.GetCombined() != "NONE" {
t.Errorf("Expected combined 'NONE', got %s", rule.GetCombined())
}
// Test NewRuleWithValues
rule2 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel")
if rule2 == nil {
t.Fatal("NewRuleWithValues() returned nil")
}
if rule2.GetCategory() != CategoryPlayer {
t.Errorf("Expected category %d, got %d", CategoryPlayer, rule2.GetCategory())
}
if rule2.GetType() != PlayerMaxLevel {
t.Errorf("Expected type %d, got %d", PlayerMaxLevel, rule2.GetType())
}
if rule2.GetValue() != "50" {
t.Errorf("Expected value '50', got %s", rule2.GetValue())
}
if rule2.GetCombined() != "Player:MaxLevel" {
t.Errorf("Expected combined 'Player:MaxLevel', got %s", rule2.GetCombined())
}
// Test type conversion methods
if rule2.GetInt32() != 50 {
t.Errorf("Expected int32 50, got %d", rule2.GetInt32())
}
if rule2.GetBool() != true {
t.Errorf("Expected bool true, got %t", rule2.GetBool())
}
// Test SetValue
rule2.SetValue("100")
if rule2.GetValue() != "100" {
t.Errorf("Expected value '100' after SetValue, got %s", rule2.GetValue())
}
if rule2.GetInt32() != 100 {
t.Errorf("Expected int32 100 after SetValue, got %d", rule2.GetInt32())
}
}
// Test Rule copy constructor
func TestRuleCopy(t *testing.T) {
original := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange")
copy := NewRuleFromRule(original)
if copy == nil {
t.Fatal("NewRuleFromRule() returned nil")
}
if copy.GetCategory() != original.GetCategory() {
t.Errorf("Copy category mismatch: expected %d, got %d", original.GetCategory(), copy.GetCategory())
}
if copy.GetType() != original.GetType() {
t.Errorf("Copy type mismatch: expected %d, got %d", original.GetType(), copy.GetType())
}
if copy.GetValue() != original.GetValue() {
t.Errorf("Copy value mismatch: expected %s, got %s", original.GetValue(), copy.GetValue())
}
if copy.GetCombined() != original.GetCombined() {
t.Errorf("Copy combined mismatch: expected %s, got %s", original.GetCombined(), copy.GetCombined())
}
// Test that they are independent copies
copy.SetValue("8.0")
if original.GetValue() == copy.GetValue() {
t.Error("Original and copy are not independent after modification")
}
}
// Test RuleSet creation and basic operations
func TestRuleSet(t *testing.T) {
ruleSet := NewRuleSet()
if ruleSet == nil {
t.Fatal("NewRuleSet() returned nil")
}
if ruleSet.GetID() != 0 {
t.Errorf("Expected ID 0, got %d", ruleSet.GetID())
}
if ruleSet.GetName() != "" {
t.Errorf("Expected empty name, got %s", ruleSet.GetName())
}
// Test SetID and SetName
ruleSet.SetID(1)
ruleSet.SetName("Test Rule Set")
if ruleSet.GetID() != 1 {
t.Errorf("Expected ID 1, got %d", ruleSet.GetID())
}
if ruleSet.GetName() != "Test Rule Set" {
t.Errorf("Expected name 'Test Rule Set', got %s", ruleSet.GetName())
}
// Test adding rules
rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel")
rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange")
ruleSet.AddRule(rule1)
ruleSet.AddRule(rule2)
// Test rule retrieval
retrievedRule1 := ruleSet.GetRule(CategoryPlayer, PlayerMaxLevel)
if retrievedRule1 == nil {
t.Fatal("GetRule() returned nil for added rule")
}
if retrievedRule1.GetValue() != "50" {
t.Errorf("Retrieved rule value mismatch: expected '50', got %s", retrievedRule1.GetValue())
}
// Test rule retrieval by name
retrievedRule2 := ruleSet.GetRuleByName("Combat", "MaxCombatRange")
if retrievedRule2 == nil {
t.Fatal("GetRuleByName() returned nil for added rule")
}
if retrievedRule2.GetValue() != "4.0" {
t.Errorf("Retrieved rule value mismatch: expected '4.0', got %s", retrievedRule2.GetValue())
}
// Test Size
if ruleSet.Size() != 2 {
t.Errorf("Expected size 2, got %d", ruleSet.Size())
}
// Test HasRule
if !ruleSet.HasRule(CategoryPlayer, PlayerMaxLevel) {
t.Error("HasRule() returned false for existing rule")
}
if ruleSet.HasRule(CategorySpawn, SpawnSpeedMultiplier) {
t.Error("HasRule() returned true for non-existing rule")
}
}
// Test RuleSet copy functionality
func TestRuleSetCopy(t *testing.T) {
original := NewRuleSet()
original.SetID(1)
original.SetName("Original")
rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel")
rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange")
original.AddRule(rule1)
original.AddRule(rule2)
// Test copy constructor
copy := NewRuleSetFromRuleSet(original)
if copy == nil {
t.Fatal("NewRuleSetFromRuleSet() returned nil")
}
if copy.GetID() != original.GetID() {
t.Errorf("Copy ID mismatch: expected %d, got %d", original.GetID(), copy.GetID())
}
if copy.GetName() != original.GetName() {
t.Errorf("Copy name mismatch: expected %s, got %s", original.GetName(), copy.GetName())
}
if copy.Size() != original.Size() {
t.Errorf("Copy size mismatch: expected %d, got %d", original.Size(), copy.Size())
}
// Test that rules were copied correctly
copyRule := copy.GetRule(CategoryPlayer, PlayerMaxLevel)
if copyRule == nil {
t.Fatal("Copied rule set is missing expected rule")
}
if copyRule.GetValue() != "50" {
t.Errorf("Copied rule value mismatch: expected '50', got %s", copyRule.GetValue())
}
}
// Test RuleManager initialization
func TestRuleManager(t *testing.T) {
ruleManager := NewRuleManager()
if ruleManager == nil {
t.Fatal("NewRuleManager() returned nil")
}
if !ruleManager.IsInitialized() {
t.Error("RuleManager should be initialized after creation")
}
// Test getting a default rule
rule := ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel)
if rule == nil {
t.Fatal("GetGlobalRule() returned nil for default rule")
}
// Should return the default value from Init()
if rule.GetValue() != "50" {
t.Errorf("Expected default value '50', got %s", rule.GetValue())
}
// Test getting rule by name
rule2 := ruleManager.GetGlobalRuleByName("Player", "MaxLevel")
if rule2 == nil {
t.Fatal("GetGlobalRuleByName() returned nil for default rule")
}
if rule2.GetValue() != "50" {
t.Errorf("Expected default value '50', got %s", rule2.GetValue())
}
// Test blank rule for non-existent rule
blankRule := ruleManager.GetGlobalRule(9999, 9999)
if blankRule == nil {
t.Fatal("GetGlobalRule() should return blank rule for non-existent rule")
}
if blankRule.IsValid() {
t.Error("Blank rule should not be valid")
}
}
// Test RuleManager rule set operations
func TestRuleManagerRuleSets(t *testing.T) {
ruleManager := NewRuleManager()
// Create a test rule set
ruleSet := NewRuleSet()
ruleSet.SetID(1)
ruleSet.SetName("Test Set")
// Add some rules to it
rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "60", "Player:MaxLevel")
ruleSet.AddRule(rule1)
// Add the rule set to the manager
if !ruleManager.AddRuleSet(ruleSet) {
t.Fatal("AddRuleSet() returned false")
}
// Test getting the rule set
retrievedRuleSet := ruleManager.GetRuleSet(1)
if retrievedRuleSet == nil {
t.Fatal("GetRuleSet() returned nil")
}
if retrievedRuleSet.GetName() != "Test Set" {
t.Errorf("Retrieved rule set name mismatch: expected 'Test Set', got %s", retrievedRuleSet.GetName())
}
// Test duplicate rule set
duplicateRuleSet := NewRuleSet()
duplicateRuleSet.SetID(1)
duplicateRuleSet.SetName("Duplicate")
if ruleManager.AddRuleSet(duplicateRuleSet) {
t.Error("AddRuleSet() should return false for duplicate ID")
}
// Test GetNumRuleSets
if ruleManager.GetNumRuleSets() != 1 {
t.Errorf("Expected 1 rule set, got %d", ruleManager.GetNumRuleSets())
}
// Test setting global rule set
if !ruleManager.SetGlobalRuleSet(1) {
t.Fatal("SetGlobalRuleSet() returned false")
}
// Test that global rule now returns the overridden value
globalRule := ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel)
if globalRule == nil {
t.Fatal("GetGlobalRule() returned nil after setting global rule set")
}
if globalRule.GetValue() != "60" {
t.Errorf("Expected overridden value '60', got %s", globalRule.GetValue())
}
}
// Test category and type name functions
func TestCategoryNames(t *testing.T) {
// Test GetCategoryName
name := GetCategoryName(CategoryPlayer)
if name != "Player" {
t.Errorf("Expected category name 'Player', got %s", name)
}
// Test unknown category
unknownName := GetCategoryName(9999)
if unknownName != "Unknown" {
t.Errorf("Expected 'Unknown' for invalid category, got %s", unknownName)
}
// Test GetCategoryByName
category, exists := GetCategoryByName("Player")
if !exists {
t.Error("GetCategoryByName() should find 'Player' category")
}
if category != CategoryPlayer {
t.Errorf("Expected category %d, got %d", CategoryPlayer, category)
}
// Test unknown category name
_, exists = GetCategoryByName("NonExistent")
if exists {
t.Error("GetCategoryByName() should not find non-existent category")
}
}
// Test rule validation
func TestRuleValidation(t *testing.T) {
ruleManager := NewRuleManager()
// Test valid rule
err := ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, "50")
if err != nil {
t.Errorf("ValidateRule() returned error for valid rule: %v", err)
}
// Test rule value too long
longValue := make([]byte, MaxRuleValueLength+1)
for i := range longValue {
longValue[i] = 'a'
}
err = ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, string(longValue))
if err != ErrRuleValueTooLong {
t.Errorf("ValidateRule() should return ErrRuleValueTooLong for long value")
}
}
// Test RuleManagerAdapter
func TestRuleManagerAdapter(t *testing.T) {
ruleManager := NewRuleManager()
adapter := NewRuleManagerAdapter(ruleManager, 0)
if adapter == nil {
t.Fatal("NewRuleManagerAdapter() returned nil")
}
// Test basic rule access
rule := adapter.GetRule(CategoryPlayer, PlayerMaxLevel)
if rule == nil {
t.Fatal("Adapter GetRule() returned nil")
}
if rule.GetValue() != "50" {
t.Errorf("Expected value '50', got %s", rule.GetValue())
}
// Test convenience methods
intValue := adapter.GetInt32(CategoryPlayer, PlayerMaxLevel)
if intValue != 50 {
t.Errorf("Expected int32 50, got %d", intValue)
}
boolValue := adapter.GetBool(CategoryPlayer, PlayerMaxLevel)
if !boolValue {
t.Error("Expected bool true for value '50'")
}
stringValue := adapter.GetString(CategoryPlayer, PlayerMaxLevel)
if stringValue != "50" {
t.Errorf("Expected string '50', got %s", stringValue)
}
// Test zone ID
if adapter.GetZoneID() != 0 {
t.Errorf("Expected zone ID 0, got %d", adapter.GetZoneID())
}
adapter.SetZoneID(100)
if adapter.GetZoneID() != 100 {
t.Errorf("Expected zone ID 100 after SetZoneID, got %d", adapter.GetZoneID())
}
}
// Benchmark rule access performance
func BenchmarkRuleAccess(b *testing.B) {
ruleManager := NewRuleManager()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel)
}
}
// Benchmark rule manager creation
func BenchmarkRuleManagerCreation(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = NewRuleManager()
}
}

519
internal/rules/types.go Normal file
View File

@ -0,0 +1,519 @@
package rules
import (
"fmt"
"strconv"
"sync"
)
// Rule represents a single rule with category, type, and value
// Converted from C++ Rule class
type Rule struct {
category RuleCategory // Rule category (Client, Player, etc.)
ruleType RuleType // Rule type within category
value string // Rule value as string
combined string // Combined category:type string
mutex sync.RWMutex // Thread safety
}
// NewRule creates a new rule with default values
func NewRule() *Rule {
return &Rule{
category: 0,
ruleType: 0,
value: "",
combined: "NONE",
}
}
// NewRuleWithValues creates a new rule with specified values
func NewRuleWithValues(category RuleCategory, ruleType RuleType, value string, combined string) *Rule {
return &Rule{
category: category,
ruleType: ruleType,
value: value,
combined: combined,
}
}
// NewRuleFromRule creates a copy of another rule
func NewRuleFromRule(source *Rule) *Rule {
if source == nil {
return NewRule()
}
source.mutex.RLock()
defer source.mutex.RUnlock()
return &Rule{
category: source.category,
ruleType: source.ruleType,
value: source.value,
combined: source.combined,
}
}
// SetValue sets the rule value
func (r *Rule) SetValue(value string) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.value = value
}
// GetCategory returns the rule category
func (r *Rule) GetCategory() RuleCategory {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.category
}
// GetType returns the rule type
func (r *Rule) GetType() RuleType {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.ruleType
}
// GetValue returns the rule value as string
func (r *Rule) GetValue() string {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.value
}
// GetCombined returns the combined category:type string
func (r *Rule) GetCombined() string {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.combined
}
// Type conversion methods matching C++ implementation
// GetInt8 returns the rule value as int8
func (r *Rule) GetInt8() int8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseInt(r.value, 10, 8); err == nil {
return int8(val)
}
return 0
}
// GetInt16 returns the rule value as int16
func (r *Rule) GetInt16() int16 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseInt(r.value, 10, 16); err == nil {
return int16(val)
}
return 0
}
// GetInt32 returns the rule value as int32
func (r *Rule) GetInt32() int32 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseInt(r.value, 10, 32); err == nil {
return int32(val)
}
return 0
}
// GetInt64 returns the rule value as int64
func (r *Rule) GetInt64() int64 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseInt(r.value, 10, 64); err == nil {
return val
}
return 0
}
// GetUInt8 returns the rule value as uint8
func (r *Rule) GetUInt8() uint8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseUint(r.value, 10, 8); err == nil {
return uint8(val)
}
return 0
}
// GetUInt16 returns the rule value as uint16
func (r *Rule) GetUInt16() uint16 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseUint(r.value, 10, 16); err == nil {
return uint16(val)
}
return 0
}
// GetUInt32 returns the rule value as uint32
func (r *Rule) GetUInt32() uint32 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseUint(r.value, 10, 32); err == nil {
return uint32(val)
}
return 0
}
// GetUInt64 returns the rule value as uint64
func (r *Rule) GetUInt64() uint64 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseUint(r.value, 10, 64); err == nil {
return val
}
return 0
}
// GetBool returns the rule value as bool (> 0 = true)
func (r *Rule) GetBool() bool {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseUint(r.value, 10, 32); err == nil {
return val > 0
}
return false
}
// GetFloat32 returns the rule value as float32
func (r *Rule) GetFloat32() float32 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseFloat(r.value, 32); err == nil {
return float32(val)
}
return 0.0
}
// GetFloat64 returns the rule value as float64
func (r *Rule) GetFloat64() float64 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if val, err := strconv.ParseFloat(r.value, 64); err == nil {
return val
}
return 0.0
}
// GetChar returns the first character of the rule value
func (r *Rule) GetChar() byte {
r.mutex.RLock()
defer r.mutex.RUnlock()
if len(r.value) > 0 {
return r.value[0]
}
return 0
}
// GetString returns the rule value as string (same as GetValue)
func (r *Rule) GetString() string {
return r.GetValue()
}
// IsValid checks if the rule has valid data
func (r *Rule) IsValid() bool {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.combined != "" && r.combined != "NONE"
}
// String returns a string representation of the rule
func (r *Rule) String() string {
r.mutex.RLock()
defer r.mutex.RUnlock()
return fmt.Sprintf("Rule{%s: %s}", r.combined, r.value)
}
// RuleSet represents a collection of rules with a name and ID
// Converted from C++ RuleSet class
type RuleSet struct {
id int32 // Rule set ID
name string // Rule set name
rules map[RuleCategory]map[RuleType]*Rule // Rules organized by category and type
mutex sync.RWMutex // Thread safety
}
// NewRuleSet creates a new empty rule set
func NewRuleSet() *RuleSet {
return &RuleSet{
id: 0,
name: "",
rules: make(map[RuleCategory]map[RuleType]*Rule),
}
}
// NewRuleSetFromRuleSet creates a copy of another rule set
func NewRuleSetFromRuleSet(source *RuleSet) *RuleSet {
if source == nil {
return NewRuleSet()
}
ruleSet := &RuleSet{
id: source.GetID(),
name: source.GetName(),
rules: make(map[RuleCategory]map[RuleType]*Rule),
}
// Deep copy all rules
source.mutex.RLock()
for category, typeMap := range source.rules {
ruleSet.rules[category] = make(map[RuleType]*Rule)
for ruleType, rule := range typeMap {
ruleSet.rules[category][ruleType] = NewRuleFromRule(rule)
}
}
source.mutex.RUnlock()
return ruleSet
}
// SetID sets the rule set ID
func (rs *RuleSet) SetID(id int32) {
rs.mutex.Lock()
defer rs.mutex.Unlock()
rs.id = id
}
// SetName sets the rule set name
func (rs *RuleSet) SetName(name string) {
rs.mutex.Lock()
defer rs.mutex.Unlock()
rs.name = name
}
// GetID returns the rule set ID
func (rs *RuleSet) GetID() int32 {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
return rs.id
}
// GetName returns the rule set name
func (rs *RuleSet) GetName() string {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
return rs.name
}
// AddRule adds a rule to the rule set
func (rs *RuleSet) AddRule(rule *Rule) {
if rule == nil {
return
}
rs.mutex.Lock()
defer rs.mutex.Unlock()
category := rule.GetCategory()
ruleType := rule.GetType()
// Initialize category map if it doesn't exist
if rs.rules[category] == nil {
rs.rules[category] = make(map[RuleType]*Rule)
}
// If rule already exists, just update the value
if existingRule, exists := rs.rules[category][ruleType]; exists {
existingRule.SetValue(rule.GetValue())
} else {
// Add new rule
rs.rules[category][ruleType] = rule
}
}
// GetRule retrieves a rule by category and type
func (rs *RuleSet) GetRule(category RuleCategory, ruleType RuleType) *Rule {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
if categoryMap, exists := rs.rules[category]; exists {
if rule, exists := categoryMap[ruleType]; exists {
return rule
}
}
return nil
}
// GetRuleByName retrieves a rule by category and type names
func (rs *RuleSet) GetRuleByName(categoryName string, typeName string) *Rule {
combined := fmt.Sprintf("%s:%s", categoryName, typeName)
rs.mutex.RLock()
defer rs.mutex.RUnlock()
// Search through all rules for matching combined string
for _, categoryMap := range rs.rules {
for _, rule := range categoryMap {
if rule.GetCombined() == combined {
return rule
}
}
}
return nil
}
// CopyRulesInto copies rules from another rule set into this one
func (rs *RuleSet) CopyRulesInto(source *RuleSet) {
if source == nil {
return
}
rs.ClearRules()
rs.mutex.Lock()
defer rs.mutex.Unlock()
source.mutex.RLock()
defer source.mutex.RUnlock()
// Deep copy all rules from source
for category, typeMap := range source.rules {
rs.rules[category] = make(map[RuleType]*Rule)
for ruleType, rule := range typeMap {
rs.rules[category][ruleType] = NewRuleFromRule(rule)
}
}
}
// ClearRules removes all rules from the rule set
func (rs *RuleSet) ClearRules() {
rs.mutex.Lock()
defer rs.mutex.Unlock()
rs.rules = make(map[RuleCategory]map[RuleType]*Rule)
}
// GetRules returns the rules map (for iteration - use carefully)
func (rs *RuleSet) GetRules() map[RuleCategory]map[RuleType]*Rule {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
// Return a deep copy to prevent external modification
rulesCopy := make(map[RuleCategory]map[RuleType]*Rule)
for category, typeMap := range rs.rules {
rulesCopy[category] = make(map[RuleType]*Rule)
for ruleType, rule := range typeMap {
rulesCopy[category][ruleType] = NewRuleFromRule(rule)
}
}
return rulesCopy
}
// Size returns the total number of rules in the rule set
func (rs *RuleSet) Size() int {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
count := 0
for _, typeMap := range rs.rules {
count += len(typeMap)
}
return count
}
// GetRulesByCategory returns all rules in a specific category
func (rs *RuleSet) GetRulesByCategory(category RuleCategory) map[RuleType]*Rule {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
if categoryMap, exists := rs.rules[category]; exists {
// Return a copy to prevent external modification
rulesCopy := make(map[RuleType]*Rule)
for ruleType, rule := range categoryMap {
rulesCopy[ruleType] = NewRuleFromRule(rule)
}
return rulesCopy
}
return make(map[RuleType]*Rule)
}
// HasRule checks if a rule exists in the rule set
func (rs *RuleSet) HasRule(category RuleCategory, ruleType RuleType) bool {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
if categoryMap, exists := rs.rules[category]; exists {
_, exists := categoryMap[ruleType]
return exists
}
return false
}
// String returns a string representation of the rule set
func (rs *RuleSet) String() string {
rs.mutex.RLock()
defer rs.mutex.RUnlock()
return fmt.Sprintf("RuleSet{ID: %d, Name: %s, Rules: %d}", rs.id, rs.name, rs.Size())
}
// RuleManagerStats tracks rule system usage and performance metrics
type RuleManagerStats struct {
TotalRuleSets int32 // Total rule sets loaded
TotalRules int32 // Total rules across all sets
GlobalRuleSetID int32 // Current global rule set ID
ZoneRuleSets int32 // Number of zone-specific rule sets
RuleGetOperations int64 // Number of rule get operations
RuleSetOperations int64 // Number of rule set operations
DatabaseOperations int64 // Number of database operations
mutex sync.RWMutex
}
// IncrementRuleGetOperations increments the rule get counter
func (rms *RuleManagerStats) IncrementRuleGetOperations() {
rms.mutex.Lock()
defer rms.mutex.Unlock()
rms.RuleGetOperations++
}
// IncrementRuleSetOperations increments the rule set counter
func (rms *RuleManagerStats) IncrementRuleSetOperations() {
rms.mutex.Lock()
defer rms.mutex.Unlock()
rms.RuleSetOperations++
}
// IncrementDatabaseOperations increments the database operations counter
func (rms *RuleManagerStats) IncrementDatabaseOperations() {
rms.mutex.Lock()
defer rms.mutex.Unlock()
rms.DatabaseOperations++
}
// GetSnapshot returns a snapshot of the current statistics
func (rms *RuleManagerStats) GetSnapshot() RuleManagerStats {
rms.mutex.RLock()
defer rms.mutex.RUnlock()
return RuleManagerStats{
TotalRuleSets: rms.TotalRuleSets,
TotalRules: rms.TotalRules,
GlobalRuleSetID: rms.GlobalRuleSetID,
ZoneRuleSets: rms.ZoneRuleSets,
RuleGetOperations: rms.RuleGetOperations,
RuleSetOperations: rms.RuleSetOperations,
DatabaseOperations: rms.DatabaseOperations,
}
}
// Reset resets all statistics counters
func (rms *RuleManagerStats) Reset() {
rms.mutex.Lock()
defer rms.mutex.Unlock()
rms.TotalRuleSets = 0
rms.TotalRules = 0
rms.GlobalRuleSetID = 0
rms.ZoneRuleSets = 0
rms.RuleGetOperations = 0
rms.RuleSetOperations = 0
rms.DatabaseOperations = 0
}