From 5bfd8f5da2bf82d216627cb2f0c49df10b8d356b Mon Sep 17 00:00:00 2001 From: hg <4683435+hgtw@users.noreply.github.com> Date: Sat, 23 Mar 2024 00:50:06 -0400 Subject: [PATCH] [Tradeskills] Implement learning recipes from books (#4170) This uses the book scribe button to learn recipes in SoF+ clients. For Titanium clients the button is not available so a workaround will need to be added later. Note live gives no feedback when scribing books (at least those tested). --- .../char_recipe_list_repository.h | 18 ++++++++++ zone/client.cpp | 35 ++++++++----------- zone/client.h | 1 + zone/client_packet.cpp | 7 ++-- zone/tradeskills.cpp | 28 +++++++++++++++ 5 files changed, 66 insertions(+), 23 deletions(-) diff --git a/common/repositories/char_recipe_list_repository.h b/common/repositories/char_recipe_list_repository.h index 9435da6cc..ee2cb67b8 100644 --- a/common/repositories/char_recipe_list_repository.h +++ b/common/repositories/char_recipe_list_repository.h @@ -59,6 +59,24 @@ public: return NewEntity(); } + // insert with ON DUPLICATE KEY UPDATE to leave rows that exist unchanged + static int InsertUpdateMany(Database& db, const std::vector& entries) + { + std::vector values; + values.reserve(entries.size()); + + for (const auto& e: entries) + { + values.emplace_back(fmt::format("({},{},{})", e.char_id, e.recipe_id, e.madecount)); + } + + auto results = db.QueryDatabase(fmt::format( + "INSERT INTO {0} (char_id, recipe_id, madecount) VALUES {1} ON DUPLICATE KEY UPDATE {2}={2}", + TableName(), fmt::join(values, ","), PrimaryKey())); + + return results.Success() ? results.RowsAffected() : 0; + } + }; #endif //EQEMU_CHAR_RECIPE_LIST_REPOSITORY_H diff --git a/zone/client.cpp b/zone/client.cpp index b2254d661..db3b76300 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -68,6 +68,7 @@ extern volatile bool RunLoops; #include "../common/repositories/discovered_items_repository.h" #include "../common/repositories/inventory_repository.h" #include "../common/repositories/keyring_repository.h" +#include "../common/repositories/tradeskill_recipe_repository.h" #include "../common/events/player_events.h" #include "../common/events/player_event_logs.h" #include "dialogue_window.h" @@ -2294,29 +2295,23 @@ void Client::ReadBook(BookRequest_Struct *book) { BookText_Struct *out = (BookText_Struct *) outapp->pBuffer; out->window = book->window; - - - if (ClientVersion() >= EQ::versions::ClientVersion::SoF) { - // SoF+ need to look up book type for the output message. - const EQ::ItemInstance *inst = nullptr; - - if (book->invslot <= EQ::invbag::GENERAL_BAGS_END) - { - inst = m_inv[book->invslot]; - } - - if(inst) - out->type = inst->GetItem()->Book; - else - out->type = book->type; - } - else { - out->type = book->type; - } + out->type = book->type; out->invslot = book->invslot; out->target_id = book->target_id; out->can_cast = 0; // todo: implement - out->can_scribe = 0; // todo: implement + out->can_scribe = false; + + if (ClientVersion() >= EQ::versions::ClientVersion::SoF && book->invslot <= EQ::invbag::GENERAL_BAGS_END) + { + const EQ::ItemInstance* inst = m_inv[book->invslot]; + if (inst && inst->GetItem()) + { + auto recipe = TradeskillRecipeRepository::GetWhere(content_db, + fmt::format("learned_by_item_id = {} LIMIT 1", inst->GetItem()->ID)); + out->type = inst->GetItem()->Book; + out->can_scribe = !recipe.empty(); + } + } memcpy(out->booktext, booktxt2.c_str(), length); diff --git a/zone/client.h b/zone/client.h index f86a3a167..dfddcf07d 100644 --- a/zone/client.h +++ b/zone/client.h @@ -350,6 +350,7 @@ public: int GetRecipeMadeCount(uint32 recipe_id); bool HasRecipeLearned(uint32 recipe_id); bool CanIncreaseTradeskill(EQ::skills::SkillType tradeskill); + void ScribeRecipes(uint32_t item_id) const; bool GetRevoked() const { return revoked; } void SetRevoked(bool rev) { revoked = rev; } diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 3e9688148..fd93983fc 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -4168,10 +4168,11 @@ void Client::Handle_OP_BookButton(const EQApplicationPacket* app) BookButton_Struct* book = reinterpret_cast(app->pBuffer); const EQ::ItemInstance* const inst = GetInv().GetItem(book->invslot); - if (inst && inst->GetItem()->Book) + if (inst && inst->GetItem()) { - // todo: if scribe book learn recipes and delete book from inventory - // todo: if cast book use its spell on target and delete book from inventory (unless reusable?) + // todo: cast spell button (unknown if anything on live uses this) + ScribeRecipes(inst->GetItem()->ID); + DeleteItemInInventory(book->invslot, 1, true); } EQApplicationPacket outapp(OP_FinishWindow, 0); diff --git a/zone/tradeskills.cpp b/zone/tradeskills.cpp index 9ea023433..14ea244c5 100644 --- a/zone/tradeskills.cpp +++ b/zone/tradeskills.cpp @@ -34,6 +34,7 @@ #include "zonedb.h" #include "worldserver.h" #include "../common/repositories/char_recipe_list_repository.h" +#include "../common/repositories/criteria/content_filter_criteria.h" #include "../common/repositories/tradeskill_recipe_repository.h" #include "../common/repositories/tradeskill_recipe_entries_repository.h" @@ -1892,4 +1893,31 @@ bool Client::CheckTradeskillLoreConflict(int32 recipe_id) return false; } +void Client::ScribeRecipes(uint32_t item_id) const +{ + if (item_id == 0) + { + return; + } + auto recipes = TradeskillRecipeRepository::GetWhere(content_db, fmt::format( + "learned_by_item_id = {} {}", item_id, ContentFilterCriteria::apply())); + + std::vector learned; + learned.reserve(recipes.size()); + + for (const auto& recipe : recipes) + { + auto entry = CharRecipeListRepository::NewEntity(); + entry.char_id = static_cast(CharacterID()); + entry.recipe_id = recipe.id; + learned.push_back(entry); + } + + if (!learned.empty()) + { + // avoid replacing madecount for recipes the client already has + int rows = CharRecipeListRepository::InsertUpdateMany(database, learned); + LogTradeskills("Client [{}] scribed [{}] recipes from [{}]", CharacterID(), rows, item_id); + } +}