[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).
This commit is contained in:
hg 2024-03-23 00:50:06 -04:00 committed by GitHub
parent 96830b4a19
commit 5bfd8f5da2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 66 additions and 23 deletions

View File

@ -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<CharRecipeList>& entries)
{
std::vector<std::string> 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

View File

@ -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);

View File

@ -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; }

View File

@ -4168,10 +4168,11 @@ void Client::Handle_OP_BookButton(const EQApplicationPacket* app)
BookButton_Struct* book = reinterpret_cast<BookButton_Struct*>(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);

View File

@ -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<CharRecipeListRepository::CharRecipeList> learned;
learned.reserve(recipes.size());
for (const auto& recipe : recipes)
{
auto entry = CharRecipeListRepository::NewEntity();
entry.char_id = static_cast<int32_t>(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);
}
}