implement more internals
This commit is contained in:
parent
16d9636c06
commit
47e6102af1
@ -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);
|
|
||||||
}
|
|
@ -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.......
|
|
||||||
*/
|
|
||||||
}
|
|
157
internal/npc/race_types/README.md
Normal file
157
internal/npc/race_types/README.md
Normal 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
|
343
internal/npc/race_types/constants.go
Normal file
343
internal/npc/race_types/constants.go
Normal 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 ""
|
||||||
|
}
|
||||||
|
}
|
148
internal/npc/race_types/database.go
Normal file
148
internal/npc/race_types/database.go
Normal 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
|
||||||
|
}
|
155
internal/npc/race_types/interfaces.go
Normal file
155
internal/npc/race_types/interfaces.go
Normal 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
|
||||||
|
}
|
321
internal/npc/race_types/manager.go
Normal file
321
internal/npc/race_types/manager.go
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
233
internal/npc/race_types/master_race_type_list.go
Normal file
233
internal/npc/race_types/master_race_type_list.go
Normal 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()
|
||||||
|
}
|
45
internal/npc/race_types/types.go
Normal file
45
internal/npc/race_types/types.go
Normal 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
30
internal/races/README.md
Normal 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
557
internal/recipes/README.md
Normal 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.
|
119
internal/recipes/constants.go
Normal file
119
internal/recipes/constants.go
Normal 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"
|
||||||
|
)
|
338
internal/recipes/interfaces.go
Normal file
338
internal/recipes/interfaces.go
Normal 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
565
internal/recipes/manager.go
Normal 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))
|
||||||
|
}
|
396
internal/recipes/master_recipe_list.go
Normal file
396
internal/recipes/master_recipe_list.go
Normal 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
362
internal/recipes/recipe.go
Normal 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)
|
||||||
|
}
|
309
internal/recipes/recipe_books.go
Normal file
309
internal/recipes/recipe_books.go
Normal 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
162
internal/recipes/types.go
Normal 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
138
internal/rules/README.md
Normal 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
346
internal/rules/constants.go
Normal 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
436
internal/rules/database.go
Normal 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
|
||||||
|
}
|
301
internal/rules/interfaces.go
Normal file
301
internal/rules/interfaces.go
Normal 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
580
internal/rules/manager.go
Normal 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)
|
||||||
|
}
|
430
internal/rules/rules_test.go
Normal file
430
internal/rules/rules_test.go
Normal 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
519
internal/rules/types.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user