From 128cc458fd458af68065f33875338960dc6ac058 Mon Sep 17 00:00:00 2001 From: Akkadius Date: Mon, 30 Mar 2020 04:29:52 -0500 Subject: [PATCH] Separate tradeskill queries to not be cross-boundary, clean up logic --- common/CMakeLists.txt | 2 + .../character_recipe_list_repository.h | 117 ++++ .../tradeskill_recipe_repository.h | 134 +++++ zone/client.h | 6 +- zone/client_packet.cpp | 198 ++++--- zone/tradeskills.cpp | 290 ++++++---- zone/trading.cpp | 501 +++++++++--------- 7 files changed, 837 insertions(+), 411 deletions(-) create mode 100644 common/repositories/character_recipe_list_repository.h create mode 100644 common/repositories/tradeskill_recipe_repository.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index e312c202d..ce62fbb00 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -196,6 +196,8 @@ SET(common_headers queue.h races.h random.h + repositories/character_recipe_list_repository.h + repositories/tradeskill_recipe_repository.h rdtsc.h rulesys.h ruletypes.h diff --git a/common/repositories/character_recipe_list_repository.h b/common/repositories/character_recipe_list_repository.h new file mode 100644 index 000000000..d3349cf6a --- /dev/null +++ b/common/repositories/character_recipe_list_repository.h @@ -0,0 +1,117 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EQEMU_CHARACTER_RECIPE_LIST_REPOSITORY_H +#define EQEMU_CHARACTER_RECIPE_LIST_REPOSITORY_H + +#include "../database.h" +#include "../string_util.h" + +class CharacterRecipeListRepository { +public: + struct CharacterRecipeList { + int character_id; + int recipe_id; + int made_count; + }; + + static std::vector Columns() + { + return { + "char_id", + "recipe_id", + "madecount", + }; + } + + static std::string ColumnsRaw() + { + return std::string(implode(", ", Columns())); + } + + static std::string TableName() + { + return std::string("char_recipe_list"); + } + + static std::string BaseSelect() + { + return std::string( + fmt::format( + "SELECT {} FROM {}", + ColumnsRaw(), + TableName() + ) + ); + } + + static CharacterRecipeList GetDefaults() + { + CharacterRecipeList character_recipe_list_entry; + + character_recipe_list_entry.character_id = 0; + character_recipe_list_entry.made_count = 0; + character_recipe_list_entry.recipe_id = 0; + + return character_recipe_list_entry; + } + + static std::vector GetLearnedRecipeList(int character_id) + { + std::vector character_recipe_list; + + auto results = database.QueryDatabase( + fmt::format( + "{} WHERE char_id = {}", + BaseSelect(), + character_id + ) + ); + + for (auto row = results.begin(); row != results.end(); ++row) { + CharacterRecipeList character_recipe_list_entry; + + character_recipe_list_entry.character_id = character_id; + character_recipe_list_entry.recipe_id = atoi(row[1]); + character_recipe_list_entry.made_count = atoi(row[2]); + + character_recipe_list.push_back(character_recipe_list_entry); + } + + return character_recipe_list; + } + + static CharacterRecipeList GetRecipe( + std::vector character_recipe_list, + int recipe_id + ) + { + for (auto &row : character_recipe_list) { + if (row.recipe_id == recipe_id) { + return row; + } + } + + return GetDefaults(); + } + +}; + +#endif //EQEMU_CHARACTER_RECIPE_LIST_REPOSITORY_H diff --git a/common/repositories/tradeskill_recipe_repository.h b/common/repositories/tradeskill_recipe_repository.h new file mode 100644 index 000000000..9cdcb178a --- /dev/null +++ b/common/repositories/tradeskill_recipe_repository.h @@ -0,0 +1,134 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EQEMU_TRADESKILL_RECIPE_REPOSITORY_H +#define EQEMU_TRADESKILL_RECIPE_REPOSITORY_H + +#include "../database.h" +#include "../string_util.h" + +class TradeskillRecipeRepository { +public: + struct TradeskillRecipe { + int id; + std::string name; + int tradeskill; + int skillneeded; + int trivial; + uint8 nofail; + int replace_container; + std::string notes; + uint8 must_learn; + uint8 quest; + uint8 enabled; + }; + + static std::vector Columns() + { + return { + "id", + "name", + "tradeskill", + "skillneeded", + "trivial", + "nofail", + "replace_container", + "notes", + "must_learn", + "quest", + "enabled", + }; + } + + static std::string ColumnsRaw() + { + return std::string(implode(", ", Columns())); + } + + static std::string TableName() + { + return std::string("tradeskill_recipe"); + } + + static std::string BaseSelect() + { + return std::string( + fmt::format( + "SELECT {} FROM {}", + ColumnsRaw(), + TableName() + ) + ); + } + + static TradeskillRecipe New() + { + TradeskillRecipe entry; + + entry.id = 0; + entry.name = ""; + entry.tradeskill = 0; + entry.skillneeded = 0; + entry.trivial = 0; + entry.nofail = 0; + entry.replace_container = 0; + entry.notes = ""; + entry.must_learn = 0; + entry.quest = 0; + entry.enabled = 0; + + return entry; + } + + static TradeskillRecipe GetRecipe(int recipe_id) + { + auto results = content_db.QueryDatabase( + fmt::format( + "{} WHERE id = {}", + BaseSelect(), + recipe_id + ) + ); + + TradeskillRecipe tradeskill_recipe = New(); + + auto row = results.begin(); + if (results.RowCount() == 0) { + return tradeskill_recipe; + } + + tradeskill_recipe.id = atoi(row[0]); + tradeskill_recipe.name = (row[1] ? row[1] : ""); + tradeskill_recipe.tradeskill = atoi(row[2]); + tradeskill_recipe.skillneeded = atoi(row[3]); + tradeskill_recipe.trivial = atoi(row[4]); + tradeskill_recipe.nofail = atoi(row[5]); + tradeskill_recipe.replace_container = atoi(row[6]); + tradeskill_recipe.notes = (row[7] ? row[7] : ""); + tradeskill_recipe.must_learn = atoi(row[8]); + tradeskill_recipe.quest = atoi(row[9]); + tradeskill_recipe.enabled = atoi(row[10]); + + return tradeskill_recipe; + } + +}; + +#endif diff --git a/zone/client.h b/zone/client.h index 58b0d9af8..fa863fae7 100644 --- a/zone/client.h +++ b/zone/client.h @@ -289,7 +289,7 @@ public: const char *message9 = nullptr); void Tell_StringID(uint32 string_id, const char *who, const char *message); void SendColoredText(uint32 color, std::string message); - void SendBazaarResults(uint32 trader_id,uint32 class_,uint32 race,uint32 stat,uint32 slot,uint32 type,char name[64],uint32 minprice,uint32 maxprice); + void SendBazaarResults(uint32 trader_id, uint32 in_class, uint32 in_race, uint32 item_stat, uint32 item_slot, uint32 item_type, char item_name[64], uint32 min_price, uint32 max_price); void SendTraderItem(uint32 item_id,uint16 quantity); uint16 FindTraderItem(int32 SerialNumber,uint16 Quantity); uint32 FindTraderItemSerialNumber(int32 ItemID); @@ -325,7 +325,7 @@ public: void FilteredMessage(Mob *sender, uint32 type, eqFilterType filter, const char* message, ...); void VoiceMacroReceived(uint32 Type, char *Target, uint32 MacroNumber); void SendSound(); - void LearnRecipe(uint32 recipeID); + void LearnRecipe(uint32 recipe_id); bool CanIncreaseTradeskill(EQEmu::skills::SkillType tradeskill); EQApplicationPacket* ReturnItemPacket(int16 slot_id, const EQEmu::ItemInstance* inst, ItemPacketType packet_type); @@ -745,7 +745,7 @@ public: inline uint16 MaxSkill(EQEmu::skills::SkillType skillid) const { return MaxSkill(skillid, GetClass(), GetLevel()); } uint8 SkillTrainLevel(EQEmu::skills::SkillType skillid, uint16 class_); - void TradeskillSearchResults(const std::string &query, unsigned long objtype, unsigned long someid); + void SendTradeskillSearchResults(const std::string &query, unsigned long objtype, unsigned long someid); void SendTradeskillDetails(uint32 recipe_id); bool TradeskillExecute(DBTradeskillRecipe_Struct *spec); void CheckIncreaseTradeskill(int16 bonusstat, int16 stat_modifier, float skillup_modifier, uint16 success_modifier, EQEmu::skills::SkillType tradeskill); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 49b5f7600..9085f1849 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -11828,90 +11828,166 @@ void Client::Handle_OP_RecipesFavorite(const EQApplicationPacket *app) if (first) //no favorites.... return; - const std::string query = StringFormat("SELECT tr.id, tr.name, tr.trivial, " - "SUM(tre.componentcount), crl.madecount,tr.tradeskill " - "FROM tradeskill_recipe AS tr " - "LEFT JOIN tradeskill_recipe_entries AS tre ON tr.id=tre.recipe_id " - "LEFT JOIN (SELECT recipe_id, madecount " - "FROM char_recipe_list " - "WHERE char_id = %u) AS crl ON tr.id=crl.recipe_id " - "WHERE tr.enabled <> 0 AND tr.id IN (%s) " - "AND tr.must_learn & 0x20 <> 0x20 AND " - "((tr.must_learn & 0x3 <> 0 AND crl.madecount IS NOT NULL) " - "OR (tr.must_learn & 0x3 = 0)) " - "GROUP BY tr.id " - "HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 AND SUM(tre.componentcount) <= %u " - "LIMIT 100 ", CharacterID(), favoriteIDs.c_str(), containers.c_str(), combineObjectSlots); + const std::string query = StringFormat( + SQL ( + SELECT + tr.id, + tr.name, + tr.trivial, + SUM(tre.componentcount), + tr.tradeskill + FROM + tradeskill_recipe AS tr + LEFT JOIN tradeskill_recipe_entries AS tre ON tr.id = tre.recipe_id + WHERE + tr.enabled <> 0 + AND tr.id IN (%s) + AND tr.must_learn & 0x20 <> 0x20 + AND ( + ( + tr.must_learn & 0x3 <> 0 + ) + OR (tr.must_learn & 0x3 = 0) + ) + GROUP BY + tr.id + HAVING + sum( + if( + tre.item_id %s + AND tre.iscontainer > 0, + 1, + 0 + ) + ) > 0 + AND SUM(tre.componentcount) <= %u + LIMIT + 100 + ), + favoriteIDs.c_str(), + containers.c_str(), + combineObjectSlots + ); - TradeskillSearchResults(query, tsf->object_type, tsf->some_id); - return; + SendTradeskillSearchResults(query, tsf->object_type, tsf->some_id); } void Client::Handle_OP_RecipesSearch(const EQApplicationPacket *app) { if (app->size != sizeof(RecipesSearch_Struct)) { - LogError("Invalid size for RecipesSearch_Struct: Expected: [{}], Got: [{}]", - sizeof(RecipesSearch_Struct), app->size); + LogError( + "Invalid size for RecipesSearch_Struct: Expected: [{}], Got: [{}]", + sizeof(RecipesSearch_Struct), + app->size + ); + return; } - RecipesSearch_Struct* rss = (RecipesSearch_Struct*)app->pBuffer; - rss->query[55] = '\0'; //just to be sure. + auto* p_recipes_search_struct = (RecipesSearch_Struct*)app->pBuffer; + p_recipes_search_struct->query[55] = '\0'; //just to be sure. + LogTradeskills( + "[Handle_OP_RecipesSearch] Requested search recipes for object_type [{}] some_id [{}]", + p_recipes_search_struct->object_type, + p_recipes_search_struct->some_id + ); - LogDebug("Requested search recipes for: [{}] - [{}]\n", rss->object_type, rss->some_id); - - // make where clause segment for container(s) - char containers[30]; - uint32 combineObjectSlots; - if (rss->some_id == 0) { + char containers_where_clause[30]; + uint32 combine_object_slots; + if (p_recipes_search_struct->some_id == 0) { // world combiner so no item number - snprintf(containers, 29, "= %u", rss->object_type); - combineObjectSlots = 10; + snprintf(containers_where_clause, 29, "= %u", p_recipes_search_struct->object_type); + combine_object_slots = 10; } else { // container in inventory - snprintf(containers, 29, "in (%u,%u)", rss->object_type, rss->some_id); - auto item = database.GetItem(rss->some_id); - if (!item) - { - LogError("Invalid container ID: [{}]. GetItem returned null. Defaulting to BagSlots = 10.\n", rss->some_id); - combineObjectSlots = 10; + snprintf(containers_where_clause, 29, "in (%u,%u)", p_recipes_search_struct->object_type, p_recipes_search_struct->some_id); + auto item = database.GetItem(p_recipes_search_struct->some_id); + if (!item) { + LogError( + "Invalid container ID: [{}]. GetItem returned null. Defaulting to BagSlots = 10.", + p_recipes_search_struct->some_id + ); + combine_object_slots = 10; } - else - { - combineObjectSlots = item->BagSlots; + else { + combine_object_slots = item->BagSlots; } } - std::string searchClause; - - //omit the rlike clause if query is empty - if (rss->query[0] != 0) { + std::string search_clause; + if (p_recipes_search_struct->query[0] != 0) { char buf[120]; //larger than 2X rss->query - database.DoEscapeString(buf, rss->query, strlen(rss->query)); - searchClause = StringFormat("name rlike '%s' AND", buf); + database.DoEscapeString(buf, p_recipes_search_struct->query, strlen(p_recipes_search_struct->query)); + search_clause = StringFormat("name rlike '%s' AND", buf); } //arbitrary limit of 200 recipes, makes sense to me. - const std::string query = StringFormat("SELECT tr.id, tr.name, tr.trivial, " - "SUM(tre.componentcount), crl.madecount,tr.tradeskill " - "FROM tradeskill_recipe AS tr " - "LEFT JOIN tradeskill_recipe_entries AS tre ON tr.id = tre.recipe_id " - "LEFT JOIN (SELECT recipe_id, madecount " - "FROM char_recipe_list WHERE char_id = %u) AS crl ON tr.id=crl.recipe_id " - "WHERE %s tr.trivial >= %u AND tr.trivial <= %u AND tr.enabled <> 0 " - "AND tr.must_learn & 0x20 <> 0x20 " - "AND ((tr.must_learn & 0x3 <> 0 " - "AND crl.madecount IS NOT NULL) " - "OR (tr.must_learn & 0x3 = 0)) " - "GROUP BY tr.id " - "HAVING sum(if(tre.item_id %s AND tre.iscontainer > 0,1,0)) > 0 AND SUM(tre.componentcount) <= %u " - "LIMIT 200 ", - CharacterID(), searchClause.c_str(), - rss->mintrivial, rss->maxtrivial, containers, combineObjectSlots); - TradeskillSearchResults(query, rss->object_type, rss->some_id); - return; + std::string query = fmt::format( + SQL( + SELECT + tr.id, + tr.name, + tr.trivial, + SUM(tre.componentcount), + crl.madecount, + tr.tradeskill + FROM + tradeskill_recipe + AS tr + LEFT + JOIN + tradeskill_recipe_entries + AS + tre + ON + tr.id = tre.recipe_id + LEFT JOIN( + SELECT + recipe_id, + madecount + FROM + char_recipe_list + WHERE + char_id = {} + ) AS crl ON tr.id = crl.recipe_id + WHERE + {} tr.trivial >= {} + AND tr.trivial <= {} + AND tr.enabled <> 0 + AND tr.must_learn & 0x20 <> 0x20 + AND ( + ( + tr.must_learn & 0x3 <> 0 + AND crl.madecount IS NOT NULL + ) + OR (tr.must_learn & 0x3 = 0) + ) + GROUP BY + tr.id + HAVING + sum( + if ( + tre.item_id {} + AND tre.iscontainer > 0, + 1, + 0 + ) + ) > 0 + AND SUM(tre.componentcount) <= {} + LIMIT + 200 + ), + CharacterID(), + search_clause, + p_recipes_search_struct->mintrivial, + p_recipes_search_struct->maxtrivial, + containers_where_clause, + combine_object_slots + ); + + SendTradeskillSearchResults(query, p_recipes_search_struct->object_type, p_recipes_search_struct->some_id); } void Client::Handle_OP_ReloadUI(const EQApplicationPacket *app) diff --git a/zone/tradeskills.cpp b/zone/tradeskills.cpp index 8c9072eba..aba4a8a5d 100644 --- a/zone/tradeskills.cpp +++ b/zone/tradeskills.cpp @@ -33,6 +33,8 @@ #include "string_ids.h" #include "titles.h" #include "zonedb.h" +#include "../common/repositories/character_recipe_list_repository.h" +#include "../common/repositories/tradeskill_recipe_repository.h" extern QueryServ* QServ; @@ -467,7 +469,7 @@ void Object::HandleAutoCombine(Client* user, const RecipeAutoCombine_Struct* rac //ask the database for the recipe to make sure it exists... DBTradeskillRecipe_Struct spec; - if (!database.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->CharacterID(), &spec)) { LogError("Unknown recipe for HandleAutoCombine: [{}]\n", rac->recipe_id); user->QueuePacket(outapp); safe_delete(outapp); @@ -704,56 +706,69 @@ EQEmu::skills::SkillType Object::TypeToSkill(uint32 type) } } -void Client::TradeskillSearchResults(const std::string &query, unsigned long objtype, unsigned long someid) { - - auto results = content_db.QueryDatabase(query); +void Client::SendTradeskillSearchResults( + const std::string &query, + unsigned long objtype, + unsigned long someid +) +{ + auto results = content_db.QueryDatabase(query); if (!results.Success()) { return; } - if(results.RowCount() < 1) - return; //search gave no results... not an error - - if(results.ColumnCount() != 6) { - LogError("Error in TradeskillSearchResults query [{}]: Invalid column count in result", query.c_str()); + if (results.RowCount() < 1) { return; } - for(auto row = results.begin(); row != results.end(); ++row) { - if(row == nullptr || row[0] == nullptr || row[1] == nullptr || row[2] == nullptr || row[3] == nullptr || row[5] == nullptr) - continue; + auto character_learned_recipe_list = CharacterRecipeListRepository::GetLearnedRecipeList(CharacterID()); - uint32 recipe = (uint32)atoi(row[0]); - const char *name = row[1]; - uint32 trivial = (uint32) atoi(row[2]); - uint32 comp_count = (uint32) atoi(row[3]); - uint32 tradeskill = (uint16) atoi(row[5]); + for (auto row = results.begin(); row != results.end(); ++row) { + if (row == nullptr || row[0] == nullptr || row[1] == nullptr || row[2] == nullptr || row[3] == nullptr || + row[5] == nullptr) { + continue; + } + + uint32 recipe_id = (uint32) atoi(row[0]); + const char *name = row[1]; + uint32 trivial = (uint32) atoi(row[2]); + uint32 comp_count = (uint32) atoi(row[3]); + uint32 tradeskill = (uint16) atoi(row[5]); // Skip the recipes that exceed the threshold in skill difference // Recipes that have either been made before or were // explicitly learned are excempt from that limit - if (RuleB(Skills, UseLimitTradeskillSearchSkillDiff) - && ((int32)trivial - (int32)GetSkill((EQEmu::skills::SkillType)tradeskill)) > RuleI(Skills, MaxTradeskillSearchSkillDiff) - && row[4] == nullptr) - continue; + if (RuleB(Skills, UseLimitTradeskillSearchSkillDiff) && + ((int32) trivial - (int32) GetSkill((EQEmu::skills::SkillType) tradeskill)) > + RuleI(Skills, MaxTradeskillSearchSkillDiff)) { - auto outapp = new EQApplicationPacket(OP_RecipeReply, sizeof(RecipeReply_Struct)); + LogTradeskills("Checking limit recipe_id [{}] name [{}]", recipe_id, name); + + auto character_learned_recipe = CharacterRecipeListRepository::GetRecipe( + character_learned_recipe_list, + recipe_id + ); + + if (character_learned_recipe.made_count == 0) { + continue; + } + } + + auto outapp = new EQApplicationPacket(OP_RecipeReply, sizeof(RecipeReply_Struct)); RecipeReply_Struct *reply = (RecipeReply_Struct *) outapp->pBuffer; - reply->object_type = objtype; - reply->some_id = someid; + reply->object_type = objtype; + reply->some_id = someid; reply->component_count = comp_count; - reply->recipe_id = recipe; - reply->trivial = trivial; + reply->recipe_id = recipe_id; + reply->trivial = trivial; strn0cpy(reply->recipe_name, name, sizeof(reply->recipe_name)); FastQueuePacket(&outapp); } - } void Client::SendTradeskillDetails(uint32 recipe_id) { - //pull the list of components std::string query = StringFormat("SELECT tre.item_id,tre.componentcount,i.icon,i.Name " "FROM tradeskill_recipe_entries AS tre " "LEFT JOIN items AS i ON tre.item_id = i.id " @@ -1124,9 +1139,9 @@ void Client::CheckIncreaseTradeskill(int16 bonusstat, int16 stat_modifier, float NotifyNewTitlesAvailable(); } - LogTradeskills("skillup_modifier: [{}] , success_modifier: [{}] , stat modifier: [{}]", skillup_modifier , success_modifier , stat_modifier); - LogTradeskills("Stage1 chance was: [{}] percent", chance_stage1); - LogTradeskills("Stage2 chance was: [{}] percent. 0 percent means stage1 failed", chance_stage2); + LogTradeskills("[CheckIncreaseTradeskill] skillup_modifier: [{}] , success_modifier: [{}] , stat modifier: [{}]", skillup_modifier , success_modifier , stat_modifier); + LogTradeskills("[CheckIncreaseTradeskill] Stage1 chance was: [{}] percent", chance_stage1); + LogTradeskills("[CheckIncreaseTradeskill] Stage2 chance was: [{}] percent. 0 percent means stage1 failed", chance_stage2); } bool ZoneDatabase::GetTradeRecipe(const EQEmu::ItemInstance* container, uint8 c_type, uint32 some_id, @@ -1305,29 +1320,51 @@ bool ZoneDatabase::GetTradeRecipe(const EQEmu::ItemInstance* container, uint8 c_ return GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec); } -bool ZoneDatabase::GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id, - uint32 char_id, DBTradeskillRecipe_Struct *spec) +bool ZoneDatabase::GetTradeRecipe( + uint32 recipe_id, + uint8 c_type, + uint32 some_id, + uint32 char_id, + DBTradeskillRecipe_Struct *spec +) { - // make where clause segment for container(s) - std::string containers; - if (some_id == 0) - containers = StringFormat("= %u", c_type); // world combiner so no item number - else - containers = StringFormat("IN (%u,%u)", c_type, some_id); // container in inventory + std::string container_where_filter; + if (some_id == 0) { + // 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("SELECT tr.id, tr.tradeskill, tr.skillneeded, " - "tr.trivial, tr.nofail, tr.replace_container, " - "tr.name, tr.must_learn, tr.quest, crl.madecount " - "FROM tradeskill_recipe AS tr " - "INNER JOIN tradeskill_recipe_entries AS tre " - "ON tr.id = tre.recipe_id " - "LEFT JOIN (SELECT recipe_id, madecount " - "FROM char_recipe_list WHERE char_id = %u) AS crl " - "ON tr.id = crl.recipe_id " - "WHERE tr.id = %lu AND tre.item_id %s AND tr.enabled " - "GROUP BY tr.id", - char_id, (unsigned long)recipe_id, containers.c_str()); + std::string query = StringFormat( + SQL ( + SELECT + tradeskill_recipe.id, + tradeskill_recipe.tradeskill, + tradeskill_recipe.skillneeded, + tradeskill_recipe.trivial, + tradeskill_recipe.nofail, + tradeskill_recipe.replace_container, + tradeskill_recipe.name, + tradeskill_recipe.must_learn, + tradeskill_recipe.quest + FROM + tradeskill_recipe + INNER JOIN tradeskill_recipe_entries ON tradeskill_recipe.id = tradeskill_recipe_entries.recipe_id + WHERE + tradeskill_recipe.id = %lu + AND tradeskill_recipe_entries.item_id %s + AND tradeskill_recipe.enabled + GROUP BY + tradeskill_recipe.id + ) + , + (unsigned long) recipe_id, + container_where_filter.c_str() + ); auto results = QueryDatabase(query); if (!results.Success()) { LogError("Error in GetTradeRecipe, query: [{}]", query.c_str()); @@ -1335,27 +1372,36 @@ bool ZoneDatabase::GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id return false; } - if(results.RowCount() != 1) - return false;//just not found i guess.. + if (results.RowCount() != 1) { + return false; + } auto row = results.begin(); - spec->tradeskill = (EQEmu::skills::SkillType)atoi(row[1]); - spec->skill_needed = (int16)atoi(row[2]); - spec->trivial = (uint16)atoi(row[3]); - spec->nofail = atoi(row[4]) ? true : false; - spec->replace_container = atoi(row[5]) ? true : false; - spec->name = row[6]; - spec->must_learn = (uint8)atoi(row[7]); - spec->quest = atoi(row[8]) ? true : false; - if (row[9] == nullptr) { - spec->has_learnt = false; - spec->madecount = 0; - } else { + spec->tradeskill = (EQEmu::skills::SkillType) atoi(row[1]); + spec->skill_needed = (int16) atoi(row[2]); + spec->trivial = (uint16) atoi(row[3]); + spec->nofail = atoi(row[4]) ? true : false; + spec->replace_container = atoi(row[5]) ? true : false; + spec->name = row[6]; + spec->must_learn = (uint8) atoi(row[7]); + spec->quest = atoi(row[8]) ? true : false; + spec->has_learnt = false; + spec->madecount = 0; + spec->recipe_id = recipe_id; + + auto character_learned_recipe_list = CharacterRecipeListRepository::GetLearnedRecipeList(char_id); + auto character_learned_recipe = CharacterRecipeListRepository::GetRecipe( + character_learned_recipe_list, + recipe_id + ); + + if (character_learned_recipe.made_count > 0) { + LogTradeskills("[GetTradeRecipe] made_count [{}]", character_learned_recipe.made_count); + spec->has_learnt = true; - spec->madecount = (uint32)atoul(row[9]); + spec->madecount = (uint32)character_learned_recipe.made_count; } - spec->recipe_id = recipe_id; //Pull the on-success items... query = StringFormat("SELECT item_id,successcount FROM tradeskill_recipe_entries " @@ -1379,33 +1425,41 @@ bool ZoneDatabase::GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id spec->onfail.clear(); //Pull the on-fail items... - query = StringFormat("SELECT item_id, failcount FROM tradeskill_recipe_entries " - "WHERE failcount > 0 AND recipe_id = %u", recipe_id); + query = StringFormat( + "SELECT item_id, failcount FROM tradeskill_recipe_entries " + "WHERE failcount > 0 AND recipe_id = %u", recipe_id + ); results = QueryDatabase(query); - if (results.Success()) - for(auto row = results.begin(); row != results.end(); ++row) { - uint32 item = (uint32)atoi(row[0]); - uint8 num = (uint8) atoi(row[1]); - spec->onfail.push_back(std::pair(item, num)); + if (results.Success()) { + for (auto row = results.begin(); row != results.end(); ++row) { + uint32 item = (uint32) atoi(row[0]); + uint8 num = (uint8) atoi(row[1]); + spec->onfail.push_back(std::pair(item, num)); } + } - spec->salvage.clear(); + spec->salvage.clear(); - // Don't bother with the query if TS is nofail - if (spec->nofail) - return true; + // Don't bother with the query if TS is nofail + if (spec->nofail) { + return true; + } // Pull the salvage list - query = StringFormat("SELECT item_id, salvagecount " - "FROM tradeskill_recipe_entries " - "WHERE salvagecount > 0 AND recipe_id = %u", recipe_id); - results = QueryDatabase(query); - if (results.Success()) - for(auto row = results.begin(); row != results.begin(); ++row) { - uint32 item = (uint32)atoi(row[0]); - uint8 num = (uint8)atoi(row[1]); - spec->salvage.push_back(std::pair(item, num)); + query = StringFormat( + "SELECT item_id, salvagecount " + "FROM tradeskill_recipe_entries " + "WHERE salvagecount > 0 AND recipe_id = %u", recipe_id + ); + + results = QueryDatabase(query); + if (results.Success()) { + for (auto row = results.begin(); row != results.begin(); ++row) { + uint32 item = (uint32) atoi(row[0]); + uint8 num = (uint8) atoi(row[1]); + spec->salvage.push_back(std::pair(item, num)); } + } return true; } @@ -1419,43 +1473,57 @@ void ZoneDatabase::UpdateRecipeMadecount(uint32 recipe_id, uint32 char_id, uint3 QueryDatabase(query); } -void Client::LearnRecipe(uint32 recipeID) +void Client::LearnRecipe(uint32 recipe_id) { - std::string query = StringFormat("SELECT tr.name, crl.madecount " - "FROM tradeskill_recipe AS tr " - "LEFT JOIN (SELECT recipe_id, madecount " - "FROM char_recipe_list WHERE char_id = %u) AS crl " - "ON tr.id = crl.recipe_id " - "WHERE tr.id = %u ;", CharacterID(), recipeID); - - // TODO: BOUNDARY REWRITE + std::string query = fmt::format( + SQL( + select + char_id, + recipe_id, + madecount + from + char_recipe_list + where + char_id = {} + and recipe_id = {} + LIMIT 1 + ), + CharacterID(), + recipe_id + ); auto results = database.QueryDatabase(query); if (!results.Success()) { return; } - if (results.RowCount() != 1) { - LogInfo("Client::LearnRecipe - RecipeID: [{}] had [{}] occurences", recipeID, results.RowCount()); + auto tradeskill_recipe = TradeskillRecipeRepository::GetRecipe(recipe_id); + if (tradeskill_recipe.id == 0) { + LogError("Invalid recipe [{}]", recipe_id); return; } + LogTradeskills( + "[LearnRecipe] recipe_id [{}] name [{}] learned [{}]", + recipe_id, + tradeskill_recipe.name, + results.RowCount() + ); + auto row = results.begin(); + if (results.RowCount() > 0) { + return; + } - if (row[0] == nullptr) - return; + MessageString(Chat::LightBlue, TRADESKILL_LEARN_RECIPE, tradeskill_recipe.name.c_str()); - // Only give Learn message if character doesn't know the recipe - if (row[1] != nullptr) - return; - - MessageString(Chat::LightBlue, TRADESKILL_LEARN_RECIPE, row[0]); - // Actually learn the recipe now - query = StringFormat("INSERT INTO char_recipe_list " - "SET recipe_id = %u, char_id = %u, madecount = 0 " - "ON DUPLICATE KEY UPDATE madecount = madecount;", - recipeID, CharacterID()); - results = database.QueryDatabase(query); + database.QueryDatabase( + fmt::format( + "REPLACE INTO char_recipe_list (recipe_id, char_id, madecount) VALUES ({}, {}, 0)", + recipe_id, + CharacterID() + ) + ); } bool Client::CanIncreaseTradeskill(EQEmu::skills::SkillType tradeskill) { diff --git a/zone/trading.cpp b/zone/trading.cpp index 1618e7770..27e4cc112 100644 --- a/zone/trading.cpp +++ b/zone/trading.cpp @@ -1755,286 +1755,315 @@ void Client::SendBazaarWelcome() "or use /buyer to set up your own Buy Lines.", atoi(row[0])); } -void Client::SendBazaarResults(uint32 TraderID, uint32 Class_, uint32 Race, uint32 ItemStat, uint32 Slot, uint32 Type, - char Name[64], uint32 MinPrice, uint32 MaxPrice) { +void Client::SendBazaarResults( + uint32 trader_id, + uint32 in_class, + uint32 in_race, + uint32 item_stat, + uint32 item_slot, + uint32 item_type, + char item_name[64], + uint32 min_price, + uint32 max_price +) +{ + std::string search_values = " COUNT(item_id), trader.*, items.name "; + std::string search_criteria = " WHERE trader.item_id = items.id "; - std::string searchValues = " COUNT(item_id), trader.*, items.name "; - std::string searchCriteria = " WHERE trader.item_id = items.id "; + if (trader_id > 0) { + Client *trader = entity_list.GetClientByID(trader_id); - if(TraderID > 0) { - Client* trader = entity_list.GetClientByID(TraderID); - - if(trader) - searchCriteria.append(StringFormat(" AND trader.char_id = %i", trader->CharacterID())); + if (trader) { + search_criteria.append(StringFormat(" AND trader.char_id = %i", trader->CharacterID())); + } } - if(MinPrice != 0) - searchCriteria.append(StringFormat(" AND trader.item_cost >= %i", MinPrice)); + if (min_price != 0) { + search_criteria.append(StringFormat(" AND trader.item_cost >= %i", min_price)); + } - if(MaxPrice != 0) - searchCriteria.append(StringFormat(" AND trader.item_cost <= %i", MaxPrice)); + if (max_price != 0) { + search_criteria.append(StringFormat(" AND trader.item_cost <= %i", max_price)); + } - if(strlen(Name) > 0) { - char *safeName = RemoveApostrophes(Name); - searchCriteria.append(StringFormat(" AND items.name LIKE '%%%s%%'", safeName)); + if (strlen(item_name) > 0) { + char *safeName = RemoveApostrophes(item_name); + search_criteria.append(StringFormat(" AND items.name LIKE '%%%s%%'", safeName)); safe_delete_array(safeName); } - if(Class_ != 0xFFFFFFFF) - searchCriteria.append(StringFormat(" AND MID(REVERSE(BIN(items.classes)), %i, 1) = 1", Class_)); + if (in_class != 0xFFFFFFFF) { + search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.classes)), %i, 1) = 1", in_class)); + } - if(Race != 0xFFFFFFFF) - searchCriteria.append(StringFormat(" AND MID(REVERSE(BIN(items.races)), %i, 1) = 1", Race)); + if (in_race != 0xFFFFFFFF) { + search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.races)), %i, 1) = 1", in_race)); + } - if(Slot != 0xFFFFFFFF) - searchCriteria.append(StringFormat(" AND MID(REVERSE(BIN(items.slots)), %i, 1) = 1", Slot + 1)); + if (item_slot != 0xFFFFFFFF) { + search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.slots)), %i, 1) = 1", item_slot + 1)); + } - switch(Type){ - case 0xFFFFFFFF: - break; - case 0: - // 1H Slashing - searchCriteria.append(" AND items.itemtype = 0 AND damage > 0"); - break; - case 31: - searchCriteria.append(" AND items.itemclass = 2"); - break; - case 46: - searchCriteria.append(" AND items.spellid > 0 AND items.spellid < 65000"); - break; - case 47: - searchCriteria.append(" AND items.spellid = 998"); - break; - case 48: - searchCriteria.append(" AND items.spellid >= 1298 AND items.spellid <= 1307"); - break; - case 49: - searchCriteria.append(" AND items.focuseffect > 0"); - break; - - default: - searchCriteria.append(StringFormat(" AND items.itemtype = %i", Type)); - } - - switch(ItemStat) { - - case STAT_AC: - searchCriteria.append(" AND items.ac > 0"); - searchValues.append(", items.ac"); + switch (item_type) { + case 0xFFFFFFFF: break; - - case STAT_AGI: - searchCriteria.append(" AND items.aagi > 0"); - searchValues.append(", items.aagi"); + case 0: + // 1H Slashing + search_criteria.append(" AND items.itemtype = 0 AND damage > 0"); break; - - case STAT_CHA: - searchCriteria.append(" AND items.acha > 0"); - searchValues.append(", items.acha"); + case 31: + search_criteria.append(" AND items.itemclass = 2"); break; - - case STAT_DEX: - searchCriteria.append(" AND items.adex > 0"); - searchValues.append(", items.adex"); + case 46: + search_criteria.append(" AND items.spellid > 0 AND items.spellid < 65000"); break; - - case STAT_INT: - searchCriteria.append(" AND items.aint > 0"); - searchValues.append(", items.aint"); + case 47: + search_criteria.append(" AND items.spellid = 998"); break; - - case STAT_STA: - searchCriteria.append(" AND items.asta > 0"); - searchValues.append(", items.asta"); + case 48: + search_criteria.append(" AND items.spellid >= 1298 AND items.spellid <= 1307"); break; - - case STAT_STR: - searchCriteria.append(" AND items.astr > 0"); - searchValues.append(", items.astr"); - break; - - case STAT_WIS: - searchCriteria.append(" AND items.awis > 0"); - searchValues.append(", items.awis"); - break; - - case STAT_COLD: - searchCriteria.append(" AND items.cr > 0"); - searchValues.append(", items.cr"); - break; - - case STAT_DISEASE: - searchCriteria.append(" AND items.dr > 0"); - searchValues.append(", items.dr"); - break; - - case STAT_FIRE: - searchCriteria.append(" AND items.fr > 0"); - searchValues.append(", items.fr"); - break; - - case STAT_MAGIC: - searchCriteria.append(" AND items.mr > 0"); - searchValues.append(", items.mr"); - break; - - case STAT_POISON: - searchCriteria.append(" AND items.pr > 0"); - searchValues.append(", items.pr"); - break; - - case STAT_HP: - searchCriteria.append(" AND items.hp > 0"); - searchValues.append(", items.hp"); - break; - - case STAT_MANA: - searchCriteria.append(" AND items.mana > 0"); - searchValues.append(", items.mana"); - break; - - case STAT_ENDURANCE: - searchCriteria.append(" AND items.endur > 0"); - searchValues.append(", items.endur"); - break; - - case STAT_ATTACK: - searchCriteria.append(" AND items.attack > 0"); - searchValues.append(", items.attack"); - break; - - case STAT_HP_REGEN: - searchCriteria.append(" AND items.regen > 0"); - searchValues.append(", items.regen"); - break; - - case STAT_MANA_REGEN: - searchCriteria.append(" AND items.manaregen > 0"); - searchValues.append(", items.manaregen"); - break; - - case STAT_HASTE: - searchCriteria.append(" AND items.haste > 0"); - searchValues.append(", items.haste"); - break; - - case STAT_DAMAGE_SHIELD: - searchCriteria.append(" AND items.damageshield > 0"); - searchValues.append(", items.damageshield"); + case 49: + search_criteria.append(" AND items.focuseffect > 0"); break; default: - searchValues.append(", 0"); + search_criteria.append(StringFormat(" AND items.itemtype = %i", item_type)); + } + + switch (item_stat) { + + case STAT_AC: + search_criteria.append(" AND items.ac > 0"); + search_values.append(", items.ac"); + break; + + case STAT_AGI: + search_criteria.append(" AND items.aagi > 0"); + search_values.append(", items.aagi"); + break; + + case STAT_CHA: + search_criteria.append(" AND items.acha > 0"); + search_values.append(", items.acha"); + break; + + case STAT_DEX: + search_criteria.append(" AND items.adex > 0"); + search_values.append(", items.adex"); + break; + + case STAT_INT: + search_criteria.append(" AND items.aint > 0"); + search_values.append(", items.aint"); + break; + + case STAT_STA: + search_criteria.append(" AND items.asta > 0"); + search_values.append(", items.asta"); + break; + + case STAT_STR: + search_criteria.append(" AND items.astr > 0"); + search_values.append(", items.astr"); + break; + + case STAT_WIS: + search_criteria.append(" AND items.awis > 0"); + search_values.append(", items.awis"); + break; + + case STAT_COLD: + search_criteria.append(" AND items.cr > 0"); + search_values.append(", items.cr"); + break; + + case STAT_DISEASE: + search_criteria.append(" AND items.dr > 0"); + search_values.append(", items.dr"); + break; + + case STAT_FIRE: + search_criteria.append(" AND items.fr > 0"); + search_values.append(", items.fr"); + break; + + case STAT_MAGIC: + search_criteria.append(" AND items.mr > 0"); + search_values.append(", items.mr"); + break; + + case STAT_POISON: + search_criteria.append(" AND items.pr > 0"); + search_values.append(", items.pr"); + break; + + case STAT_HP: + search_criteria.append(" AND items.hp > 0"); + search_values.append(", items.hp"); + break; + + case STAT_MANA: + search_criteria.append(" AND items.mana > 0"); + search_values.append(", items.mana"); + break; + + case STAT_ENDURANCE: + search_criteria.append(" AND items.endur > 0"); + search_values.append(", items.endur"); + break; + + case STAT_ATTACK: + search_criteria.append(" AND items.attack > 0"); + search_values.append(", items.attack"); + break; + + case STAT_HP_REGEN: + search_criteria.append(" AND items.regen > 0"); + search_values.append(", items.regen"); + break; + + case STAT_MANA_REGEN: + search_criteria.append(" AND items.manaregen > 0"); + search_values.append(", items.manaregen"); + break; + + case STAT_HASTE: + search_criteria.append(" AND items.haste > 0"); + search_values.append(", items.haste"); + break; + + case STAT_DAMAGE_SHIELD: + search_criteria.append(" AND items.damageshield > 0"); + search_values.append(", items.damageshield"); + break; + + default: + search_values.append(", 0"); break; } - std::string query = StringFormat("SELECT %s, SUM(charges), items.stackable " - "FROM trader, items %s GROUP BY items.id, charges, char_id LIMIT %i", - searchValues.c_str(), searchCriteria.c_str(), RuleI(Bazaar, MaxSearchResults)); - auto results = database.QueryDatabase(query); - if (!results.Success()) { + std::string query = StringFormat( + "SELECT %s, SUM(charges), items.stackable " + "FROM trader, items %s GROUP BY items.id, charges, char_id LIMIT %i", + search_values.c_str(), + search_criteria.c_str(), + RuleI(Bazaar, MaxSearchResults) + ); + + auto results = database.QueryDatabase(query); + + if (!results.Success()) { return; - } - - LogTrading("SRCH: [{}]", query.c_str()); - - int Size = 0; - uint32 ID = 0; - - if (results.RowCount() == static_cast(RuleI(Bazaar, MaxSearchResults))) - Message(Chat::Yellow, "Your search reached the limit of %i results. Please narrow your search down by selecting more options.", - RuleI(Bazaar, MaxSearchResults)); - - if(results.RowCount() == 0) { - auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct)); - BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *)outapp2->pBuffer; - brds->TraderID = ID; - brds->Type = BazaarSearchDone; - brds->Unknown008 = 0xFFFFFFFF; - brds->Unknown012 = 0xFFFFFFFF; - brds->Unknown016 = 0xFFFFFFFF; - this->QueuePacket(outapp2); - safe_delete(outapp2); - return; } - Size = results.RowCount() * sizeof(BazaarSearchResults_Struct); - auto buffer = new uchar[Size]; - uchar *bufptr = buffer; - memset(buffer, 0, Size); + LogTrading("SRCH: [{}]", query.c_str()); - int Action = BazaarSearchResults; - uint32 Cost = 0; - int32 SerialNumber = 0; - char temp_buffer[64] = {0}; - int Count = 0; - uint32 StatValue = 0; + int Size = 0; + uint32 ID = 0; - for (auto row = results.begin(); row != results.end(); ++row) { - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Action); - Count = atoi(row[0]); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Count); - SerialNumber = atoi(row[3]); - VARSTRUCT_ENCODE_TYPE(int32, bufptr, SerialNumber); - Client *Trader2 = entity_list.GetClientByCharID(atoi(row[1])); - if (Trader2) { - ID = Trader2->GetID(); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, ID); - } else { - LogTrading("Unable to find trader: [{}]\n", atoi(row[1])); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, 0); - } - Cost = atoi(row[5]); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Cost); - StatValue = atoi(row[8]); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, StatValue); - bool Stackable = atoi(row[10]); - if (Stackable) { - int Charges = atoi(row[9]); - sprintf(temp_buffer, "%s(%i)", row[7], Charges); - } else - sprintf(temp_buffer, "%s(%i)", row[7], Count); + if (results.RowCount() == static_cast(RuleI(Bazaar, MaxSearchResults))) { + Message( + Chat::Yellow, + "Your search reached the limit of %i results. Please narrow your search down by selecting more options.", + RuleI(Bazaar, MaxSearchResults)); + } - memcpy(bufptr, &temp_buffer, strlen(temp_buffer)); + if (results.RowCount() == 0) { + auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct)); + BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *) outapp2->pBuffer; + brds->TraderID = ID; + brds->Type = BazaarSearchDone; + brds->Unknown008 = 0xFFFFFFFF; + brds->Unknown012 = 0xFFFFFFFF; + brds->Unknown016 = 0xFFFFFFFF; + this->QueuePacket(outapp2); + safe_delete(outapp2); + return; + } - bufptr += 64; + Size = results.RowCount() * sizeof(BazaarSearchResults_Struct); + auto buffer = new uchar[Size]; + uchar *bufptr = buffer; + memset(buffer, 0, Size); - // Extra fields for SoD+ - // - if (Trader2) - sprintf(temp_buffer, "%s", Trader2->GetName()); - else - sprintf(temp_buffer, "Unknown"); + int Action = BazaarSearchResults; + uint32 Cost = 0; + int32 SerialNumber = 0; + char temp_buffer[64] = {0}; + int Count = 0; + uint32 StatValue = 0; - memcpy(bufptr, &temp_buffer, strlen(temp_buffer)); + for (auto row = results.begin(); row != results.end(); ++row) { + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Action); + Count = atoi(row[0]); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Count); + SerialNumber = atoi(row[3]); + VARSTRUCT_ENCODE_TYPE(int32, bufptr, SerialNumber); + Client *Trader2 = entity_list.GetClientByCharID(atoi(row[1])); + if (Trader2) { + ID = Trader2->GetID(); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, ID); + } + else { + LogTrading("Unable to find trader: [{}]\n", atoi(row[1])); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, 0); + } + Cost = atoi(row[5]); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Cost); + StatValue = atoi(row[8]); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, StatValue); + bool Stackable = atoi(row[10]); + if (Stackable) { + int Charges = atoi(row[9]); + sprintf(temp_buffer, "%s(%i)", row[7], Charges); + } + else { + sprintf(temp_buffer, "%s(%i)", row[7], Count); + } - bufptr += 64; + memcpy(bufptr, &temp_buffer, strlen(temp_buffer)); - VARSTRUCT_ENCODE_TYPE(uint32, bufptr, atoi(row[1])); // ItemID - } + bufptr += 64; - auto outapp = new EQApplicationPacket(OP_BazaarSearch, Size); + // Extra fields for SoD+ + // + if (Trader2) { + sprintf(temp_buffer, "%s", Trader2->GetName()); + } + else { + sprintf(temp_buffer, "Unknown"); + } - memcpy(outapp->pBuffer, buffer, Size); + memcpy(bufptr, &temp_buffer, strlen(temp_buffer)); - this->QueuePacket(outapp); + bufptr += 64; - safe_delete(outapp); - safe_delete_array(buffer); + VARSTRUCT_ENCODE_TYPE(uint32, bufptr, atoi(row[1])); // ItemID + } - auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct)); - BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *)outapp2->pBuffer; + auto outapp = new EQApplicationPacket(OP_BazaarSearch, Size); - brds->TraderID = ID; - brds->Type = BazaarSearchDone; + memcpy(outapp->pBuffer, buffer, Size); - brds->Unknown008 = 0xFFFFFFFF; - brds->Unknown012 = 0xFFFFFFFF; - brds->Unknown016 = 0xFFFFFFFF; + this->QueuePacket(outapp); - this->QueuePacket(outapp2); + safe_delete(outapp); + safe_delete_array(buffer); - safe_delete(outapp2); + auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct)); + BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *) outapp2->pBuffer; + + brds->TraderID = ID; + brds->Type = BazaarSearchDone; + + brds->Unknown008 = 0xFFFFFFFF; + brds->Unknown012 = 0xFFFFFFFF; + brds->Unknown016 = 0xFFFFFFFF; + + this->QueuePacket(outapp2); + + safe_delete(outapp2); } static void UpdateTraderCustomerItemsAdded(uint32 CustomerID, TraderCharges_Struct* gis, uint32 ItemID) {