[Bug Fix] Fix Tradeskill Combines with augmented items (#3490)

* [Bug Fix] Fix Tradeskill Combines with augmented items

# Notes
- Keeps players from doing tradeskill combines with augmented items.
- Behavior is Live-like, we don't need a rule since `EVENT_COMBINE` is separate and processed prior to the recipe check.

* Update tradeskills.cpp

* Update tradeskills.cpp

* Cleanup

* Update tradeskills.cpp
This commit is contained in:
Alex King 2023-07-15 01:39:19 -04:00 committed by GitHub
parent f25e37d0c5
commit 8f6d606f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 213 additions and 177 deletions

View File

@ -720,12 +720,14 @@ EQ::ItemInstance* EQ::ItemInstance::RemoveAugment(uint8 index)
bool EQ::ItemInstance::IsAugmented() bool EQ::ItemInstance::IsAugmented()
{ {
if (!m_item || !m_item->IsClassCommon()) if (!m_item || !m_item->IsClassCommon()) {
return false; return false;
}
for (int index = invaug::SOCKET_BEGIN; index <= invaug::SOCKET_END; ++index) { for (uint8 slot_id = invaug::SOCKET_BEGIN; slot_id <= invaug::SOCKET_END; ++slot_id) {
if (GetAugmentItemID(index)) if (GetAugmentItemID(slot_id)) {
return true; return true;
}
} }
return false; return false;

View File

@ -389,6 +389,7 @@ void Object::HandleCombine(Client* user, const NewCombine_Struct* in_combine, Ob
} }
DBTradeskillRecipe_Struct spec; DBTradeskillRecipe_Struct spec;
bool is_augmented = false;
if (parse->PlayerHasQuestSub(EVENT_COMBINE)) { if (parse->PlayerHasQuestSub(EVENT_COMBINE)) {
if (parse->EventPlayer(EVENT_COMBINE, user, std::to_string(in_combine->container_slot), 0) == 1) { if (parse->EventPlayer(EVENT_COMBINE, user, std::to_string(in_combine->container_slot), 0) == 1) {
@ -399,11 +400,16 @@ void Object::HandleCombine(Client* user, const NewCombine_Struct* in_combine, Ob
} }
} }
if (!content_db.GetTradeRecipe(container, c_type, some_id, user->CharacterID(), &spec)) { if (!content_db.GetTradeRecipe(container, c_type, some_id, user, &spec, &is_augmented)) {
LogTradeskillsDetail("Check 2"); LogTradeskillsDetail("Check 2");
user->MessageString(Chat::Emote,TRADESKILL_NOCOMBINE); if (!is_augmented) {
user->MessageString(Chat::Emote, TRADESKILL_NOCOMBINE);
} else {
user->Message(Chat::Emote, "You must remove augments from all component items before you can attempt this combine.");
}
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0); auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
user->QueuePacket(outapp); user->QueuePacket(outapp);
safe_delete(outapp); safe_delete(outapp);
@ -592,7 +598,7 @@ void Object::HandleAutoCombine(Client* user, const RecipeAutoCombine_Struct* rac
//ask the database for the recipe to make sure it exists... //ask the database for the recipe to make sure it exists...
DBTradeskillRecipe_Struct spec; DBTradeskillRecipe_Struct spec;
if (!content_db.GetTradeRecipe(rac->recipe_id, rac->object_type, rac->some_id, user->CharacterID(), &spec)) { if (!content_db.GetTradeRecipe(rac->recipe_id, rac->object_type, rac->some_id, user, &spec)) {
LogError("Unknown recipe for HandleAutoCombine: [{}]\n", rac->recipe_id); LogError("Unknown recipe for HandleAutoCombine: [{}]\n", rac->recipe_id);
user->QueuePacket(outapp); user->QueuePacket(outapp);
safe_delete(outapp); safe_delete(outapp);
@ -1231,25 +1237,28 @@ void Client::CheckIncreaseTradeskill(int16 bonusstat, int16 stat_modifier, float
} }
bool ZoneDatabase::GetTradeRecipe( bool ZoneDatabase::GetTradeRecipe(
const EQ::ItemInstance *container, const EQ::ItemInstance* container,
uint8 c_type, uint8 c_type,
uint32 some_id, uint32 some_id,
uint32 char_id, Client* c,
DBTradeskillRecipe_Struct *spec DBTradeskillRecipe_Struct* spec,
bool* is_augmented
) )
{ {
if (container == nullptr) { if (!container) {
LogTradeskills("Container null");
return false; return false;
} }
std::string containers;// make where clause segment for container(s) if (!c) {
if (some_id == 0) { return false;
containers = StringFormat("= %u", c_type); // world combiner so no item number }
std::string containers; // make where clause segment for container(s)
if (!some_id) { // world combiner so no item number
containers = fmt::format("= {}", c_type);
} else { // container in inventory
containers = fmt::format("IN ({}, {})", c_type, some_id);
} }
else {
containers = StringFormat("IN (%u,%u)", c_type, some_id);
} // container in inventory
//Could prolly watch for stacks in this loop and handle them properly... //Could prolly watch for stacks in this loop and handle them properly...
//just increment sum and count accordingly //just increment sum and count accordingly
@ -1257,26 +1266,31 @@ bool ZoneDatabase::GetTradeRecipe(
std::string buf2; std::string buf2;
uint32 count = 0; uint32 count = 0;
uint32 sum = 0; uint32 sum = 0;
for (uint8 i = 0; i < 10; i++) { // <watch> TODO: need to determine if this is bound to world/item container size
LogTradeskills("Fetching item [{}]", i);
const EQ::ItemInstance *inst = container->GetItem(i); for (uint8 slot_id = EQ::invbag::SLOT_BEGIN; slot_id < EQ::invbag::SLOT_COUNT; slot_id++) { // <watch> TODO: need to determine if this is bound to world/item container size
LogTradeskills("Fetching item [{}]", slot_id);
const auto inst = container->GetItem(slot_id);
if (!inst) { if (!inst) {
continue; continue;
} }
const EQ::ItemData *item = database.GetItem(inst->GetItem()->ID); if (inst->IsAugmented()) {
*is_augmented = true;
return false;
}
const auto item = database.GetItem(inst->GetItem()->ID);
if (!item) { if (!item) {
LogTradeskills("item [{}] not found!", inst->GetItem()->ID); LogTradeskills("item [{}] not found!", inst->GetItem()->ID);
continue; continue;
} }
if (first) { if (first) {
buf2 += StringFormat("%d", item->ID); buf2 += fmt::format("{}", item->ID);
first = false; first = false;
} } else {
else { buf2 += fmt::format(", {}", item->ID);
buf2 += StringFormat(",%d", item->ID);
} }
sum += item->ID; sum += item->ID;
@ -1284,99 +1298,110 @@ bool ZoneDatabase::GetTradeRecipe(
LogTradeskills( LogTradeskills(
"Item in container index [{}] item [{}] found [{}]", "Item in container index [{}] item [{}] found [{}]",
i, slot_id,
item->ID, item->ID,
count count
); );
} }
//no items == no recipe // no items == no recipe
if (count == 0) { if (!count) {
return false; return false;
} }
std::string query = StringFormat("SELECT tre.recipe_id " std::string query = fmt::format(
"FROM tradeskill_recipe_entries AS tre " "SELECT tre.recipe_id FROM tradeskill_recipe_entries AS tre "
"INNER JOIN tradeskill_recipe AS tr ON (tre.recipe_id = tr.id) " "INNER JOIN tradeskill_recipe AS tr ON (tre.recipe_id = tr.id) "
"WHERE tr.enabled AND (( tre.item_id IN(%s) AND tre.componentcount > 0) " "WHERE tr.enabled AND ((tre.item_id IN ({}) AND tre.componentcount > 0) "
"OR ( tre.item_id %s AND tre.iscontainer=1 ))" "OR (tre.item_id {} AND tre.iscontainer = 1))"
"GROUP BY tre.recipe_id HAVING sum(tre.componentcount) = %u " "GROUP BY tre.recipe_id HAVING SUM(tre.componentcount) = {} "
"AND sum(tre.item_id * tre.componentcount) = %u", "AND SUM(tre.item_id * tre.componentcount) = {}",
buf2.c_str(), containers.c_str(), count, sum); buf2,
auto results = QueryDatabase(query); containers,
count,
sum
);
auto results = QueryDatabase(query);
if (!results.Success()) { if (!results.Success()) {
LogError("Error in GetTradeRecipe search, query: [{}]", query.c_str()); LogError("Error in search, query: [{}]", query.c_str());
LogError("Error in GetTradeRecipe search, error: [{}]", results.ErrorMessage().c_str()); LogError("Error in search, error: [{}]", results.ErrorMessage().c_str());
return false; return false;
} }
if (results.RowCount() > 1) {
//multiple recipes, partial match... do an extra query to get it exact.
//this happens when combining components for a smaller recipe
//which is completely contained within another recipe
first = true;
uint32 index = 0;
buf2 = "";
for (auto row = results.begin(); row != results.end(); ++row, ++index) {
uint32 recipeid = (uint32)Strings::ToInt(row[0]);
if(first) {
buf2 += StringFormat("%u", recipeid);
first = false;
} else
buf2 += StringFormat(",%u", recipeid);
//length limit on buf2
if(index == 214) { //Maximum number of recipe matches (19 * 215 = 4096)
LogError("GetTradeRecipe warning: Too many matches. Unable to search all recipe entries. Searched [{}] of [{}] possible entries", index + 1, results.RowCount());
break;
}
}
query = StringFormat("SELECT tre.recipe_id "
"FROM tradeskill_recipe_entries AS tre "
"WHERE tre.recipe_id IN (%s) "
"GROUP BY tre.recipe_id HAVING sum(tre.componentcount) = %u "
"AND sum(tre.item_id * tre.componentcount) = %u", buf2.c_str(), count, sum
);
results = QueryDatabase(query);
if (!results.Success()) {
LogError("Error in GetTradeRecipe, re-query: [{}]", query.c_str());
LogError("Error in GetTradeRecipe, error: [{}]", results.ErrorMessage().c_str());
return false;
}
}
if (results.RowCount() < 1)
return false;
if (results.RowCount() > 1) { if (results.RowCount() > 1) {
//The recipe is not unique, so we need to compare the container were using. //multiple recipes, partial match... do an extra query to get it exact.
uint32 containerId = 0; //this happens when combining components for a smaller recipe
//which is completely contained within another recipe
first = true;
uint32 index = 0;
buf2 = "";
for (auto row : results) {
const uint32 recipe_id = Strings::ToUnsignedInt(row[0]);
if (first) {
buf2 += fmt::format("{}", recipe_id);
first = false;
} else {
buf2 += fmt::format(", {}", recipe_id);
}
if (some_id) { //Standard container // length limit on buf2
containerId = some_id; if (index == 214) { // Maximum number of recipe matches (19 * 215 = 4096)
} LogError(
else if (c_type) {//World container "Warning: Too many matches. Unable to search all recipe entries. Searched [{}] of [{}] possible entries",
containerId = c_type; index + 1,
} results.RowCount());
else { //Invalid container break;
return false; }
++index;
} }
query = StringFormat( query = fmt::format(
"SELECT tre.recipe_id " "SELECT tre.recipe_id FROM tradeskill_recipe_entries AS tre "
"FROM tradeskill_recipe_entries AS tre " "WHERE tre.recipe_id IN ({}) GROUP BY tre.recipe_id HAVING sum(tre.componentcount) = {} "
"WHERE tre.recipe_id IN (%s) " "AND sum(tre.item_id * tre.componentcount) = {}",
"AND tre.item_id = %u;", buf2.c_str(), containerId buf2,
count,
sum
); );
results = QueryDatabase(query); results = QueryDatabase(query);
if (!results.Success()) { if (!results.Success()) {
LogError("Error in GetTradeRecipe, re-query: [{}]", query.c_str()); LogError("Re-query: [{}]", query.c_str());
LogError("Error in GetTradeRecipe, error: [{}]", results.ErrorMessage().c_str()); LogError("Error: [{}]", results.ErrorMessage().c_str());
return false;
}
}
if (results.RowCount() < 1) {
return false;
}
if (results.RowCount() > 1) { //The recipe is not unique, so we need to compare the container were using.
uint32 container_item_id = 0;
if (some_id) { // Standard container
container_item_id = some_id;
} else if (c_type) { // World container
container_item_id = c_type;
} else { // Invalid container
return false; return false;
} }
if (results.RowCount() == 0) { //Recipe contents matched more than 1 recipe, but not in this container query = fmt::format(
"SELECT tre.recipe_id FROM tradeskill_recipe_entries AS tre WHERE tre.recipe_id IN ({}) AND tre.item_id = {}",
buf2,
container_item_id
);
results = QueryDatabase(query);
if (!results.Success()) {
LogError("Re-query: [{}]", query);
LogError("Error: [{}]", results.ErrorMessage());
return false;
}
if (!results.RowCount()) { //Recipe contents matched more than 1 recipe, but not in this container
LogError("Combine error: Incorrect container is being used!"); LogError("Combine error: Incorrect container is being used!");
return false; return false;
} }
@ -1385,44 +1410,47 @@ bool ZoneDatabase::GetTradeRecipe(
LogError( LogError(
"Combine error: Recipe is not unique! [{}] matches found for container [{}]. Continuing with first recipe match", "Combine error: Recipe is not unique! [{}] matches found for container [{}]. Continuing with first recipe match",
results.RowCount(), results.RowCount(),
containerId container_item_id
); );
} }
} }
auto row = results.begin(); auto row = results.begin();
uint32 recipe_id = (uint32)Strings::ToInt(row[0]); const uint32 recipe_id = Strings::ToUnsignedInt(row[0]);
//Right here we verify that we actually have ALL of the tradeskill components.. //Right here we verify that we actually have ALL of the tradeskill components..
//instead of part which is possible with experimentation. //instead of part which is possible with experimentation.
//This is here because something's up with the query above.. it needs to be rethought out //This is here because something's up with the query above.. it needs to be rethought out
bool has_components = true; query = fmt::format(
query = StringFormat("SELECT item_id, componentcount " "SELECT item_id, componentcount FROM tradeskill_recipe_entries WHERE componentcount > 0 AND recipe_id = {}",
"FROM tradeskill_recipe_entries " recipe_id
"WHERE recipe_id = %i AND componentcount > 0", );
recipe_id);
results = QueryDatabase(query);
if (!results.Success()) {
return GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec);
}
if (results.RowCount() == 0) { results = QueryDatabase(query);
return GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec); if (!results.Success() || !results.RowCount()) {
return GetTradeRecipe(recipe_id, c_type, some_id, c, spec);
} }
for (auto row = results.begin(); row != results.end(); ++row) { for (auto row : results) {
int component_count = 0; int component_count = 0;
for (int x = EQ::invbag::SLOT_BEGIN; x < EQ::invtype::WORLD_SIZE; x++) { for (uint8 slot_id = EQ::invbag::SLOT_BEGIN; slot_id < EQ::invtype::WORLD_SIZE; slot_id++) {
const EQ::ItemInstance* inst = container->GetItem(x); const auto inst = container->GetItem(slot_id);
if(!inst) if (!inst) {
continue; continue;
}
const EQ::ItemData* item = database.GetItem(inst->GetItem()->ID); if (inst->IsAugmented()) {
if (!item) *is_augmented = true;
continue; return false;
}
if (item->ID == Strings::ToInt(row[0])) { const auto item = database.GetItem(inst->GetItem()->ID);
if (!item) {
continue;
}
if (item->ID == Strings::ToUnsignedInt(row[0])) {
component_count++; component_count++;
} }
@ -1439,29 +1467,30 @@ bool ZoneDatabase::GetTradeRecipe(
} }
} }
return GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec); return GetTradeRecipe(recipe_id, c_type, some_id, c, spec);
} }
bool ZoneDatabase::GetTradeRecipe( bool ZoneDatabase::GetTradeRecipe(
uint32 recipe_id, uint32 recipe_id,
uint8 c_type, uint8 c_type,
uint32 some_id, uint32 some_id,
uint32 char_id, Client* c,
DBTradeskillRecipe_Struct *spec DBTradeskillRecipe_Struct* spec
) )
{ {
std::string container_where_filter; if (!c) {
if (some_id == 0) { return false;
// world combiner so no item number
container_where_filter = StringFormat("= %u", c_type);
}
else {
// container in inventory
container_where_filter = StringFormat("IN (%u,%u)", c_type, some_id);
} }
std::string query = StringFormat( std::string container_where_filter;
SQL ( if (!some_id) { // world combiner so no item number
container_where_filter = fmt::format("= {}", c_type);
} else { // container in inventory
container_where_filter = fmt::format("IN ({}, {})", c_type, some_id);
}
std::string query = fmt::format(
SQL(
SELECT SELECT
tradeskill_recipe.id, tradeskill_recipe.id,
tradeskill_recipe.tradeskill, tradeskill_recipe.tradeskill,
@ -1476,85 +1505,91 @@ bool ZoneDatabase::GetTradeRecipe(
tradeskill_recipe tradeskill_recipe
INNER JOIN tradeskill_recipe_entries ON tradeskill_recipe.id = tradeskill_recipe_entries.recipe_id INNER JOIN tradeskill_recipe_entries ON tradeskill_recipe.id = tradeskill_recipe_entries.recipe_id
WHERE WHERE
tradeskill_recipe.id = %lu tradeskill_recipe.id = {}
AND tradeskill_recipe_entries.item_id %s AND tradeskill_recipe_entries.item_id {}
AND tradeskill_recipe.enabled AND tradeskill_recipe.enabled
GROUP BY GROUP BY
tradeskill_recipe.id tradeskill_recipe.id
) ),
, recipe_id,
(unsigned long) recipe_id, container_where_filter
container_where_filter.c_str()
); );
auto results = QueryDatabase(query); auto results = QueryDatabase(query);
if (!results.Success()) { if (!results.Success()) {
LogError("Error in GetTradeRecipe, query: [{}]", query.c_str()); LogError("Error, query: [{}]", query.c_str());
LogError("Error in GetTradeRecipe, error: [{}]", results.ErrorMessage().c_str()); LogError("Error: [{}]", results.ErrorMessage().c_str());
return false; return false;
} }
if (results.RowCount() != 1) { if (!results.RowCount()) {
return false; return false;
} }
auto row = results.begin(); auto row = results.begin();
spec->tradeskill = (EQ::skills::SkillType) Strings::ToInt(row[1]); spec->tradeskill = static_cast<EQ::skills::SkillType>(Strings::ToUnsignedInt(row[1]));
spec->skill_needed = (int16) Strings::ToInt(row[2]); spec->skill_needed = Strings::ToInt(row[2]);
spec->trivial = (uint16) Strings::ToInt(row[3]); spec->trivial = Strings::ToUnsignedInt(row[3]);
spec->nofail = Strings::ToInt(row[4]) ? true : false; spec->nofail = Strings::ToBool(row[4]);
spec->replace_container = Strings::ToInt(row[5]) ? true : false; spec->replace_container = Strings::ToBool(row[5]);
spec->name = row[6]; spec->name = row[6];
spec->must_learn = (uint8) Strings::ToInt(row[7]); spec->must_learn = Strings::ToUnsignedInt(row[7]);
spec->quest = Strings::ToInt(row[8]) ? true : false; spec->quest = Strings::ToBool(row[8]);
spec->has_learnt = false; spec->has_learnt = false;
spec->madecount = 0; spec->madecount = 0;
spec->recipe_id = recipe_id; spec->recipe_id = recipe_id;
auto r = CharRecipeListRepository::GetWhere( auto r = CharRecipeListRepository::GetWhere(
database, database,
fmt::format("char_id = {} and recipe_id = {}", char_id, recipe_id) fmt::format(
"char_id = {} and recipe_id = {}",
c->CharacterID(),
recipe_id
)
); );
if (!r.empty() && r[0].recipe_id) { //If this exists we learned it if (!r.empty() && r[0].recipe_id) { //If this exists we learned it
LogTradeskills("made_count [{}]", r[0].madecount); LogTradeskills("made_count [{}]", r[0].madecount);
spec->has_learnt = true; spec->has_learnt = true;
spec->madecount = (uint32) r[0].madecount; spec->madecount = static_cast<uint32>(r[0].madecount);
} }
//Pull the on-success items... //Pull the on-success items...
query = StringFormat("SELECT item_id,successcount FROM tradeskill_recipe_entries " query = fmt::format(
"WHERE successcount > 0 AND recipe_id = %u", recipe_id); "SELECT item_id, successcount FROM tradeskill_recipe_entries WHERE successcount > 0 AND recipe_id = {}",
results = QueryDatabase(query); recipe_id
);
results = QueryDatabase(query);
if (!results.Success()) { if (!results.Success()) {
return false; return false;
} }
if(results.RowCount() < 1 && !spec->quest) { if (!results.RowCount() && !spec->quest) {
LogError("Error in GetTradeRecept success: no success items returned"); LogError("Error in success: no success items returned");
return false; return false;
} }
spec->onsuccess.clear(); spec->onsuccess.clear();
for(auto row = results.begin(); row != results.end(); ++row) { for(auto row : results) {
uint32 item = (uint32)Strings::ToInt(row[0]); const uint32 item_id = Strings::ToUnsignedInt(row[0]);
uint8 num = (uint8) Strings::ToInt(row[1]); const uint8 success_count = Strings::ToUnsignedInt(row[1]);
spec->onsuccess.emplace_back(std::pair<uint32,uint8>(item, num)); spec->onsuccess.emplace_back(std::pair<uint32, uint8>(item_id, success_count));
} }
spec->onfail.clear(); spec->onfail.clear();
//Pull the on-fail items... //Pull the on-fail items...
query = StringFormat( query = fmt::format(
"SELECT item_id, failcount FROM tradeskill_recipe_entries " "SELECT item_id, failcount FROM tradeskill_recipe_entries WHERE failcount > 0 AND recipe_id = {}",
"WHERE failcount > 0 AND recipe_id = %u", recipe_id recipe_id
); );
results = QueryDatabase(query); results = QueryDatabase(query);
if (results.Success()) { if (results.Success()) {
for (auto row = results.begin(); row != results.end(); ++row) { for (auto row : results) {
uint32 item = (uint32) Strings::ToInt(row[0]); const uint32 item_id = Strings::ToUnsignedInt(row[0]);
uint8 num = (uint8) Strings::ToInt(row[1]); const uint8 fail_count = Strings::ToUnsignedInt(row[1]);
spec->onfail.emplace_back(std::pair<uint32, uint8>(item, num)); spec->onfail.emplace_back(std::pair<uint32, uint8>(item_id, fail_count));
} }
} }
@ -1566,18 +1601,17 @@ bool ZoneDatabase::GetTradeRecipe(
} }
// Pull the salvage list // Pull the salvage list
query = StringFormat( query = fmt::format(
"SELECT item_id, salvagecount " "SELECT item_id, salvagecount FROM tradeskill_recipe_entries WHERE salvagecount > 0 AND recipe_id = {}",
"FROM tradeskill_recipe_entries " recipe_id
"WHERE salvagecount > 0 AND recipe_id = %u", recipe_id
); );
results = QueryDatabase(query); results = QueryDatabase(query);
if (results.Success()) { if (results.Success()) {
for (auto row = results.begin(); row != results.end(); ++row) { for (auto row : results) {
uint32 item = (uint32) Strings::ToInt(row[0]); const uint32 item_id = Strings::ToUnsignedInt(row[0]);
uint8 num = (uint8) Strings::ToInt(row[1]); const uint8 salvage_count = Strings::ToUnsignedInt(row[1]);
spec->salvage.emplace_back(std::pair<uint32, uint8>(item, num)); spec->salvage.emplace_back(std::pair<uint32, uint8>(item_id, salvage_count));
} }
} }

View File

@ -599,8 +599,8 @@ public:
void DeleteMerchantTemp(uint32 npcid, uint32 slot, uint32 zone_id, uint32 instance_id); void DeleteMerchantTemp(uint32 npcid, uint32 slot, uint32 zone_id, uint32 instance_id);
/* Tradeskills */ /* Tradeskills */
bool GetTradeRecipe(const EQ::ItemInstance* container, uint8 c_type, uint32 some_id, uint32 char_id, DBTradeskillRecipe_Struct *spec); bool GetTradeRecipe(const EQ::ItemInstance* container, uint8 c_type, uint32 some_id, Client* c, DBTradeskillRecipe_Struct* spec, bool* is_augmented);
bool GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id, uint32 char_id, DBTradeskillRecipe_Struct *spec); bool GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id, Client* c, DBTradeskillRecipe_Struct* spec);
uint32 GetZoneForage(uint32 ZoneID, uint8 skill); /* for foraging */ uint32 GetZoneForage(uint32 ZoneID, uint8 skill); /* for foraging */
uint32 GetZoneFishing(uint32 ZoneID, uint8 skill, uint32 &npc_id, uint8 &npc_chance); uint32 GetZoneFishing(uint32 ZoneID, uint8 skill, uint32 &npc_id, uint8 &npc_chance);
void UpdateRecipeMadecount(uint32 recipe_id, uint32 char_id, uint32 madecount); void UpdateRecipeMadecount(uint32 recipe_id, uint32 char_id, uint32 madecount);