mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 12:41:30 +00:00
* [Code] PlayerEventLogs Global to Singleton Cleanup * Fix --------- Co-authored-by: Chris Miles <akkadius1@gmail.com>
1962 lines
59 KiB
C++
1962 lines
59 KiB
C++
/* EQEMu: Everquest Server Emulator
|
|
Copyright (C) 2001-2004 EQEMu Development Team (http://eqemulator.net)
|
|
|
|
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
|
|
*/
|
|
|
|
#include "../common/global_define.h"
|
|
#include "../common/events/player_event_logs.h"
|
|
|
|
#include <algorithm>
|
|
#include <list>
|
|
|
|
#ifndef WIN32
|
|
#include <netinet/in.h> //for htonl
|
|
#endif
|
|
|
|
#include "../common/rulesys.h"
|
|
|
|
#include "queryserv.h"
|
|
#include "quest_parser_collection.h"
|
|
#include "string_ids.h"
|
|
#include "titles.h"
|
|
#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"
|
|
|
|
extern QueryServ* QServ;
|
|
extern WorldServer worldserver;
|
|
|
|
static const EQ::skills::SkillType TradeskillUnknown = EQ::skills::Skill1HBlunt; /* an arbitrary non-tradeskill */
|
|
|
|
void Object::HandleAugmentation(Client* user, const AugmentItem_Struct* in_augment, Object *worldo)
|
|
{
|
|
if (!user || !in_augment) {
|
|
LogError("Client or AugmentItem_Struct not set in Object::HandleAugmentation");
|
|
return;
|
|
}
|
|
|
|
EQ::ItemInstance* container = nullptr;
|
|
|
|
if (worldo) {
|
|
container = worldo->m_inst;
|
|
} else { // Check to see if they have an inventory container type 53 that is used for this.
|
|
EQ::InventoryProfile& user_inv = user->GetInv();
|
|
EQ::ItemInstance* inst = nullptr;
|
|
|
|
inst = user_inv.GetItem(in_augment->container_slot);
|
|
if (inst) {
|
|
const EQ::ItemData* item = inst->GetItem();
|
|
if (item && inst->IsType(EQ::item::ItemClassBag) && (item->BagType == EQ::item::BagTypeAugmentationSealer || item->BagType == RuleI(Inventory, AlternateAugmentationSealer))) { // We have found an appropriate inventory augmentation sealer
|
|
container = inst;
|
|
|
|
// Verify that no more than two items are in container to guarantee no inadvertant wipes.
|
|
uint8 items_found = 0;
|
|
for (uint8 i = EQ::invbag::SLOT_BEGIN; i < EQ::invtype::WORLD_SIZE; i++) {
|
|
const EQ::ItemInstance* inst = container->GetItem(i);
|
|
if (inst) {
|
|
items_found++;
|
|
}
|
|
}
|
|
|
|
if (items_found < 2) {
|
|
user->Message(Chat::Red, "Error: Too few items in augmentation container.");
|
|
return;
|
|
} else if (items_found > 2) {
|
|
user->Message(Chat::Red, "Error: Too many items in augmentation container.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!container) {
|
|
LogError("Player tried to augment an item without a container set");
|
|
user->Message(Chat::Red, "Error: This item is not a container!");
|
|
return;
|
|
}
|
|
|
|
EQ::ItemInstance *tobe_auged = nullptr, *auged_with = nullptr;
|
|
int8 slot = -1;
|
|
|
|
if (container->GetItem(0) && container->GetItem(1)) { // Verify 2 items in the augmentation device
|
|
// Verify 1 item is augmentable and the other is not
|
|
if (container->GetItem(0)->IsAugmentable() && !container->GetItem(1)->IsAugmentable()) {
|
|
tobe_auged = container->GetItem(0);
|
|
auged_with = container->GetItem(1);
|
|
} else if (!container->GetItem(0)->IsAugmentable() && container->GetItem(1)->IsAugmentable()) {
|
|
tobe_auged = container->GetItem(1);
|
|
auged_with = container->GetItem(0);
|
|
} else {
|
|
// Either 2 augmentable items found or none found
|
|
// This should never occur due to client restrictions, but prevent in case of a hack
|
|
user->Message(Chat::Red, "Error: There must be 1 augmentable item in the sealer.");
|
|
return;
|
|
}
|
|
} else { // This happens if the augment button is clicked more than once quickly while augmenting
|
|
if (!container->GetItem(0)) {
|
|
user->Message(Chat::Red, "Error: No item in the first slot of sealer.");
|
|
}
|
|
|
|
if (!container->GetItem(1)) {
|
|
user->Message(Chat::Red, "Error: No item in the second slot of sealer.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (
|
|
RuleB(Inventory, EnforceAugmentRestriction) &&
|
|
user->IsAugmentRestricted(tobe_auged->GetItemType(), auged_with->GetAugmentRestriction())
|
|
) {
|
|
user->MessageString(Chat::Red, AUGMENT_RESTRICTED);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!RuleB(Inventory, AllowMultipleOfSameAugment) &&
|
|
tobe_auged->ContainsAugmentByID(auged_with->GetID())
|
|
) {
|
|
user->Message(Chat::Red, "Error: Cannot put multiple of the same augment in an item.");
|
|
return;
|
|
}
|
|
|
|
bool delete_items = false;
|
|
|
|
EQ::ItemInstance *item_one_to_push = nullptr, *item_two_to_push = nullptr;
|
|
|
|
if (in_augment->augment_slot == -1) { // Adding augment
|
|
if (
|
|
((slot = tobe_auged->AvailableAugmentSlot(auged_with->GetAugmentType())) != -1) &&
|
|
tobe_auged->AvailableWearSlot(auged_with->GetItem()->Slots)
|
|
) {
|
|
tobe_auged->PutAugment(slot, *auged_with);
|
|
|
|
EQ::ItemInstance *aug = tobe_auged->GetAugment(slot);
|
|
if(aug) {
|
|
std::vector<std::any> args;
|
|
args.push_back(aug);
|
|
|
|
if (parse->ItemHasQuestSub(tobe_auged, EVENT_AUGMENT_ITEM)) {
|
|
parse->EventItem(EVENT_AUGMENT_ITEM, user, tobe_auged, nullptr, "", slot, &args);
|
|
}
|
|
|
|
args.assign(1, tobe_auged);
|
|
|
|
if (parse->ItemHasQuestSub(aug, EVENT_AUGMENT_INSERT)) {
|
|
parse->EventItem(EVENT_AUGMENT_INSERT, user, aug, nullptr, "", slot, &args);
|
|
}
|
|
|
|
args.push_back(aug);
|
|
|
|
if (parse->PlayerHasQuestSub(EVENT_AUGMENT_INSERT_CLIENT)) {
|
|
const auto& export_string = fmt::format(
|
|
"{} {} {} {}",
|
|
tobe_auged->GetID(),
|
|
-1,
|
|
aug->GetID(),
|
|
slot
|
|
);
|
|
|
|
parse->EventPlayer(EVENT_AUGMENT_INSERT_CLIENT, user, export_string, 0, &args);
|
|
}
|
|
}
|
|
|
|
item_one_to_push = tobe_auged->Clone();
|
|
delete_items = true;
|
|
} else {
|
|
user->Message(Chat::Red, "Error: No available slot for augment.");
|
|
}
|
|
} else {
|
|
EQ::ItemInstance *old_aug = nullptr;
|
|
bool is_solvent = auged_with->GetItem()->ItemType == EQ::item::ItemTypeAugmentationSolvent;
|
|
if (!is_solvent && auged_with->GetItem()->ItemType != EQ::item::ItemTypeAugmentationDistiller) {
|
|
LogError("Player tried to remove an augment without a solvent or distiller");
|
|
user->Message(Chat::Red, "Error: Missing an augmentation solvent or distiller for removing this augment.");
|
|
|
|
return;
|
|
}
|
|
|
|
EQ::ItemInstance *aug = tobe_auged->GetAugment(in_augment->augment_slot);
|
|
if (aug) {
|
|
if (!is_solvent && auged_with->GetItem()->ID != aug->GetItem()->AugDistiller) {
|
|
LogError("Player tried to safely remove an augment with the wrong distiller (item [{}] vs expected [{}])", auged_with->GetItem()->ID, aug->GetItem()->AugDistiller);
|
|
user->Message(Chat::Red, "Error: Wrong augmentation distiller for safely removing this augment.");
|
|
return;
|
|
}
|
|
std::vector<std::any> args;
|
|
args.push_back(aug);
|
|
|
|
if (parse->ItemHasQuestSub(tobe_auged, EVENT_UNAUGMENT_ITEM)) {
|
|
parse->EventItem(EVENT_UNAUGMENT_ITEM, user, tobe_auged, nullptr, "", slot, &args);
|
|
}
|
|
|
|
args.assign(1, tobe_auged);
|
|
args.push_back(&is_solvent);
|
|
|
|
if (parse->ItemHasQuestSub(aug, EVENT_AUGMENT_REMOVE)) {
|
|
parse->EventItem(EVENT_AUGMENT_REMOVE, user, aug, nullptr, "", slot, &args);
|
|
}
|
|
}
|
|
|
|
if (is_solvent) {
|
|
tobe_auged->DeleteAugment(in_augment->augment_slot);
|
|
} else {
|
|
old_aug = tobe_auged->RemoveAugment(in_augment->augment_slot);
|
|
}
|
|
|
|
item_one_to_push = tobe_auged->Clone();
|
|
if (old_aug) {
|
|
item_two_to_push = old_aug->Clone();
|
|
}
|
|
|
|
delete_items = true;
|
|
}
|
|
|
|
if (delete_items) {
|
|
if (worldo) {
|
|
container->Clear();
|
|
auto outapp = new EQApplicationPacket(OP_ClearObject, sizeof(ClearObject_Struct));
|
|
ClearObject_Struct *cos = (ClearObject_Struct *)outapp->pBuffer;
|
|
cos->Clear = 1;
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
database.DeleteWorldContainer(worldo->m_id, zone->GetZoneID());
|
|
} else { // Delete items in our inventory container...
|
|
for (uint8 i = EQ::invbag::SLOT_BEGIN; i < EQ::invtype::WORLD_SIZE; i++) {
|
|
const EQ::ItemInstance* inst = container->GetItem(i);
|
|
if (inst) {
|
|
user->DeleteItemInInventory(EQ::InventoryProfile::CalcSlotId(in_augment->container_slot, i), 0, true);
|
|
}
|
|
}
|
|
|
|
container->Clear(); // Explicitly mark container as cleared.
|
|
}
|
|
}
|
|
|
|
// Must push items after the items in inventory are deleted - necessary due to lore items...
|
|
if (item_one_to_push) {
|
|
user->PushItemOnCursor(*item_one_to_push, true);
|
|
}
|
|
|
|
if (item_two_to_push) {
|
|
user->PushItemOnCursor(*item_two_to_push, true);
|
|
}
|
|
}
|
|
|
|
// Perform tradeskill combine
|
|
void Object::HandleCombine(Client* user, const NewCombine_Struct* in_combine, Object *worldo)
|
|
{
|
|
if (!user || !in_combine) {
|
|
LogError("Client or NewCombine_Struct not set in Object::HandleCombine");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
LogTradeskills(
|
|
"container_slot [{}] guildtribute_slot [{}]",
|
|
in_combine->container_slot,
|
|
in_combine->guildtribute_slot
|
|
);
|
|
|
|
EQ::InventoryProfile &user_inv = user->GetInv();
|
|
PlayerProfile_Struct &user_pp = user->GetPP();
|
|
EQ::ItemInstance *container = nullptr;
|
|
EQ::ItemInstance *inst = nullptr;
|
|
|
|
uint8 c_type = 0xE8;
|
|
uint32 some_id = 0;
|
|
bool worldcontainer = false;
|
|
|
|
|
|
if (in_combine->container_slot == EQ::invslot::SLOT_TRADESKILL_EXPERIMENT_COMBINE) {
|
|
if(!worldo) {
|
|
user->Message(
|
|
Chat::Red,
|
|
"Error: Server is not aware of the tradeskill container you are attempting to use"
|
|
);
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
c_type = worldo->m_type;
|
|
inst = worldo->m_inst;
|
|
worldcontainer = true;
|
|
// if we're a world container with an item, use that too
|
|
if (inst) {
|
|
const EQ::ItemData *item = inst->GetItem();
|
|
if (item) {
|
|
some_id = item->ID;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
inst = user_inv.GetItem(in_combine->container_slot);
|
|
if (inst) {
|
|
const EQ::ItemData* item = inst->GetItem();
|
|
if (item && inst->IsType(EQ::item::ItemClassBag)) {
|
|
c_type = item->BagType;
|
|
some_id = item->ID;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!inst || !inst->IsType(EQ::item::ItemClassBag)) {
|
|
user->Message(Chat::Red, "Error: Server does not recognize specified tradeskill container");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
container = inst;
|
|
if (container->GetItem() && container->GetItem()->BagType == EQ::item::BagTypeTransformationmold) {
|
|
const EQ::ItemInstance *inst = container->GetItem(0);
|
|
bool AllowAll = RuleB(Inventory, AllowAnyWeaponTransformation);
|
|
if (inst && EQ::ItemInstance::CanTransform(inst->GetItem(), container->GetItem(), AllowAll)) {
|
|
const EQ::ItemData *new_weapon = inst->GetItem();
|
|
user->DeleteItemInInventory(EQ::InventoryProfile::CalcSlotId(in_combine->container_slot, 0), 0, true);
|
|
container->Clear();
|
|
user->SummonItem(
|
|
new_weapon->ID,
|
|
inst->GetCharges(),
|
|
inst->GetAugmentItemID(0),
|
|
inst->GetAugmentItemID(1),
|
|
inst->GetAugmentItemID(2),
|
|
inst->GetAugmentItemID(3),
|
|
inst->GetAugmentItemID(4),
|
|
inst->GetAugmentItemID(5),
|
|
inst->IsAttuned(),
|
|
EQ::invslot::slotCursor,
|
|
container->GetItem()->Icon,
|
|
Strings::ToInt(container->GetItem()->IDFile + 2)
|
|
);
|
|
|
|
user->MessageString(Chat::LightBlue, TRANSFORM_COMPLETE, inst->GetItem()->Name);
|
|
if (RuleB(Inventory, DeleteTransformationMold)) {
|
|
user->DeleteItemInInventory(in_combine->container_slot, 0, true);
|
|
}
|
|
}
|
|
else if (inst) {
|
|
user->MessageString(Chat::LightBlue, TRANSFORM_FAILED, inst->GetItem()->Name);
|
|
}
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
|
|
LogTradeskills(
|
|
"inst_item [{}] container_item [{}]",
|
|
inst->GetItem()->ID,
|
|
container->GetItem()->ID
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
if (container->GetItem() && container->GetItem()->BagType == EQ::item::BagTypeDetransformationmold) {
|
|
LogTradeskillsDetail("Check 1");
|
|
|
|
const EQ::ItemInstance* inst = container->GetItem(0);
|
|
if (inst && inst->GetOrnamentationIcon()) {
|
|
const EQ::ItemData* new_weapon = inst->GetItem();
|
|
user->DeleteItemInInventory(EQ::InventoryProfile::CalcSlotId(in_combine->container_slot, 0), 0, true);
|
|
container->Clear();
|
|
user->SummonItem(new_weapon->ID, inst->GetCharges(), inst->GetAugmentItemID(0), inst->GetAugmentItemID(1), inst->GetAugmentItemID(2), inst->GetAugmentItemID(3), inst->GetAugmentItemID(4), inst->GetAugmentItemID(5), inst->IsAttuned(), EQ::invslot::slotCursor, 0, 0);
|
|
user->MessageString(Chat::LightBlue, TRANSFORM_COMPLETE, inst->GetItem()->Name);
|
|
}
|
|
else if (inst) {
|
|
user->MessageString(Chat::LightBlue, DETRANSFORM_FAILED, inst->GetItem()->Name);
|
|
}
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
DBTradeskillRecipe_Struct spec;
|
|
bool is_augmented = false;
|
|
|
|
if (parse->PlayerHasQuestSub(EVENT_COMBINE)) {
|
|
if (parse->EventPlayer(EVENT_COMBINE, user, std::to_string(in_combine->container_slot), 0) == 1) {
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!content_db.GetTradeRecipe(container, c_type, some_id, user, &spec, &is_augmented)) {
|
|
|
|
LogTradeskillsDetail("Check 2");
|
|
|
|
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);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
// Character hasn't learnt the recipe yet.
|
|
// must_learn:
|
|
// bit 1 (0x01): recipe can't be experimented
|
|
// bit 2 (0x02): can try to experiment but not useable for auto-combine until learnt
|
|
// bit 5 (0x10): no learn message, use unlisted flag to prevent it showing up on search
|
|
// bit 6 (0x20): unlisted recipe flag
|
|
if ((spec.must_learn & 0xF) == 1 && !spec.has_learnt) {
|
|
|
|
LogTradeskillsDetail("Check 3");
|
|
|
|
// Made up message for the client. Just giving a DNC is the other option.
|
|
user->Message(Chat::LightBlue, "You need to learn how to combine these first.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
// Character does not have the required skill.
|
|
if(spec.skill_needed > 0 && user->GetSkill(spec.tradeskill) < spec.skill_needed ) {
|
|
|
|
LogTradeskillsDetail("Check 4");
|
|
|
|
// Notify client.
|
|
user->Message(Chat::LightBlue, "You are not skilled enough.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
//changing from a switch to string of if's since we don't need to iterate through all of the skills in the SkillType enum
|
|
if (spec.tradeskill == EQ::skills::SkillAlchemy) {
|
|
if (user_pp.class_ != Class::Shaman) {
|
|
user->Message(Chat::Red, "This tradeskill can only be performed by a shaman.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
else if (user_pp.level < MIN_LEVEL_ALCHEMY) {
|
|
user->Message(Chat::Red, "You cannot perform alchemy until you reach level %i.", MIN_LEVEL_ALCHEMY);
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
else if (spec.tradeskill == EQ::skills::SkillTinkering) {
|
|
if (user_pp.race != GNOME) {
|
|
user->Message(Chat::Red, "Only gnomes can tinker.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
else if (spec.tradeskill == EQ::skills::SkillMakePoison) {
|
|
if (user_pp.class_ != Class::Rogue) {
|
|
user->Message(Chat::Red, "Only rogues can mix poisons.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if Combine would result in Lore conflict
|
|
if (user->CheckTradeskillLoreConflict(spec.recipe_id)) {
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
// final check for any additional quest requirements .. "check_zone" in this case - exported as variable [validate_type]
|
|
if (parse->PlayerHasQuestSub(EVENT_COMBINE_VALIDATE)) {
|
|
if (parse->EventPlayer(EVENT_COMBINE_VALIDATE, user, fmt::format("check_zone {}", zone->GetZoneID()), spec.recipe_id) != 0) {
|
|
user->Message(
|
|
Chat::Emote,
|
|
"You cannot make this combine because the location requirement has not been met."
|
|
);
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Send acknowledgement packets to client
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
|
|
//now clean out the containers.
|
|
if(worldcontainer){
|
|
container->Clear();
|
|
outapp = new EQApplicationPacket(OP_ClearObject, sizeof(ClearObject_Struct));
|
|
ClearObject_Struct *cos = (ClearObject_Struct *)outapp->pBuffer;
|
|
cos->Clear = 1;
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
database.DeleteWorldContainer(worldo->m_id, zone->GetZoneID());
|
|
} else{
|
|
for (uint8 i = EQ::invbag::SLOT_BEGIN; i < EQ::invtype::WORLD_SIZE; i++) {
|
|
const EQ::ItemInstance* inst = container->GetItem(i);
|
|
if (inst) {
|
|
user->DeleteItemInInventory(EQ::InventoryProfile::CalcSlotId(in_combine->container_slot, i), 0, true);
|
|
}
|
|
}
|
|
container->Clear();
|
|
}
|
|
//do the check and send results...
|
|
bool success = user->TradeskillExecute(&spec);
|
|
|
|
// Learn new recipe message
|
|
// Update Made count
|
|
if (success) {
|
|
if (!spec.has_learnt && ((spec.must_learn&0x10) != 0x10)) {
|
|
user->MessageString(Chat::LightBlue, TRADESKILL_LEARN_RECIPE, spec.name.c_str());
|
|
}
|
|
database.UpdateRecipeMadecount(spec.recipe_id, user->CharacterID(), spec.madecount+1);
|
|
}
|
|
|
|
// Replace the container on success if required.
|
|
//
|
|
|
|
if (success && spec.replace_container) {
|
|
if (worldcontainer) {
|
|
//should report this error, but we dont have the recipe ID, so its not very useful
|
|
LogError("Replace container combine executed in a world container");
|
|
}
|
|
else {
|
|
user->DeleteItemInInventory(in_combine->container_slot, 0, true);
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::COMBINE_SUCCESS)) {
|
|
auto e = PlayerEvent::CombineEvent{
|
|
.recipe_id = spec.recipe_id,
|
|
.recipe_name = spec.name,
|
|
.made_count = spec.madecount,
|
|
.tradeskill_id = (uint32) spec.tradeskill
|
|
};
|
|
RecordPlayerEventLogWithClient(user, PlayerEvent::COMBINE_SUCCESS, e);
|
|
}
|
|
|
|
if (parse->PlayerHasQuestSub(EVENT_COMBINE_SUCCESS)) {
|
|
parse->EventPlayer(EVENT_COMBINE_SUCCESS, user, spec.name, spec.recipe_id);
|
|
}
|
|
}
|
|
else {
|
|
if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::COMBINE_FAILURE)) {
|
|
auto e = PlayerEvent::CombineEvent{
|
|
.recipe_id = spec.recipe_id,
|
|
.recipe_name = spec.name,
|
|
.made_count = spec.madecount,
|
|
.tradeskill_id = (uint32) spec.tradeskill
|
|
};
|
|
RecordPlayerEventLogWithClient(user, PlayerEvent::COMBINE_FAILURE, e);
|
|
}
|
|
|
|
if (parse->PlayerHasQuestSub(EVENT_COMBINE_FAILURE)) {
|
|
parse->EventPlayer(EVENT_COMBINE_FAILURE, user, spec.name, spec.recipe_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Object::HandleAutoCombine(Client* user, const RecipeAutoCombine_Struct* rac) {
|
|
|
|
//get our packet ready, gotta send one no matter what...
|
|
auto outapp = new EQApplicationPacket(OP_RecipeAutoCombine, sizeof(RecipeAutoCombine_Struct));
|
|
RecipeAutoCombine_Struct *outp = (RecipeAutoCombine_Struct *)outapp->pBuffer;
|
|
outp->object_type = rac->object_type;
|
|
outp->some_id = rac->some_id;
|
|
outp->unknown1 = rac->unknown1;
|
|
outp->recipe_id = rac->recipe_id;
|
|
outp->reply_code = 0xFFFFFFF5; //default fail.
|
|
|
|
|
|
//ask the database for the recipe to make sure it exists...
|
|
DBTradeskillRecipe_Struct 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);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
// Character hasn't learnt the recipe yet.
|
|
// This shouldn't happen.
|
|
if ((spec.must_learn&0xf) && !spec.has_learnt) {
|
|
// Made up message for the client. Just giving a DNC is the other option.
|
|
user->Message(Chat::LightBlue, "You need to learn how to combine these first.");
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
// Character does not have the required skill.
|
|
if (spec.skill_needed > 0 && user->GetSkill(spec.tradeskill) < spec.skill_needed) {
|
|
// Notify client.
|
|
user->Message(Chat::Red, "You are not skilled enough.");
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
if (spec.tradeskill == EQ::skills::SkillAlchemy) {
|
|
if (!user->GetClass() == Class::Shaman) {
|
|
user->Message(Chat::Red, "This tradeskill can only be performed by a shaman.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
else if (user->GetLevel() < MIN_LEVEL_ALCHEMY) {
|
|
user->Message(Chat::Red, "You cannot perform alchemy until you reach level %i.", MIN_LEVEL_ALCHEMY);
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
else if (spec.tradeskill == EQ::skills::SkillTinkering) {
|
|
if (user->GetRace() != GNOME) {
|
|
user->Message(Chat::Red, "Only gnomes can tinker.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
else if (spec.tradeskill == EQ::skills::SkillMakePoison) {
|
|
if (!user->GetClass() == Class::Rogue) {
|
|
user->Message(Chat::Red, "Only rogues can mix poisons.");
|
|
auto outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//pull the list of components
|
|
const auto query = fmt::format("SELECT item_id, componentcount "
|
|
"FROM tradeskill_recipe_entries "
|
|
"WHERE componentcount > 0 AND recipe_id = {}",
|
|
rac->recipe_id);
|
|
auto results = content_db.QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() < 1) {
|
|
LogError("Error in HandleAutoCombine: no components returned");
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() > 10) {
|
|
LogError("Error in HandleAutoCombine: too many components returned ([{}])", results.RowCount());
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
uint32 items[10];
|
|
memset(items, 0, sizeof(items));
|
|
uint8 counts[10];
|
|
memset(counts, 0, sizeof(counts));
|
|
|
|
//search for all the items in their inventory
|
|
EQ::InventoryProfile& user_inv = user->GetInv();
|
|
uint8 count = 0;
|
|
uint8 needcount = 0;
|
|
|
|
std::list<int> MissingItems;
|
|
|
|
uint8 needItemIndex = 0;
|
|
for (auto row = results.begin(); row != results.end(); ++row, ++needItemIndex) {
|
|
uint32 item = (uint32)Strings::ToInt(row[0]);
|
|
uint8 num = (uint8) Strings::ToInt(row[1]);
|
|
|
|
needcount += num;
|
|
|
|
//because a HasItem on items with num > 1 only returns the
|
|
//last-most slot... the results of this are useless to us
|
|
//when we go to delete them because we cannot assume it is in a single stack.
|
|
if (user_inv.HasItem(item, num, invWherePersonal) != INVALID_INDEX)
|
|
count += num;
|
|
else
|
|
MissingItems.push_back(item);
|
|
|
|
//dont start deleting anything until we have found it all.
|
|
items[needItemIndex] = item;
|
|
counts[needItemIndex] = num;
|
|
}
|
|
|
|
//make sure we found it all...
|
|
if(count != needcount)
|
|
{
|
|
user->QueuePacket(outapp);
|
|
|
|
safe_delete(outapp);
|
|
|
|
user->MessageString(Chat::Skills, TRADESKILL_MISSING_COMPONENTS);
|
|
|
|
for (auto it = MissingItems.begin(); it != MissingItems.end(); ++it) {
|
|
const EQ::ItemData* item = database.GetItem(*it);
|
|
|
|
if(item)
|
|
user->MessageString(Chat::Skills, TRADESKILL_MISSING_ITEM, item->Name);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Check if Combine would result in Lore conflict
|
|
if (user->CheckTradeskillLoreConflict(rac->recipe_id)) {
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
//now we know they have everything...
|
|
|
|
//remove all the items from the players inventory, with updates...
|
|
int16 slot;
|
|
for(uint8 r = 0; r < results.RowCount(); r++) {
|
|
if(items[r] == 0 || counts[r] == 0)
|
|
continue; //skip empties, could prolly break here
|
|
|
|
//we have to loop here to delete 1 at a time in case its in multiple stacks.
|
|
for(uint8 k = 0; k < counts[r]; k++) {
|
|
slot = user_inv.HasItem(items[r], 1, invWherePersonal);
|
|
if (slot == INVALID_INDEX) {
|
|
//WTF... I just checked this above, but just to be sure...
|
|
//we cant undo the previous deletes without a lot of work.
|
|
//so just call it quits, this shouldent ever happen anyways.
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
const EQ::ItemInstance* inst = user_inv.GetItem(slot);
|
|
|
|
if (inst && !inst->IsStackable())
|
|
user->DeleteItemInInventory(slot, 0, true);
|
|
else
|
|
user->DeleteItemInInventory(slot, 1, true);
|
|
}
|
|
}
|
|
|
|
//otherwise, we found it all...
|
|
outp->reply_code = 0x00000000; //success for finding it...
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
|
|
|
|
//now actually try to make something...
|
|
|
|
bool success = user->TradeskillExecute(&spec);
|
|
|
|
if (success) {
|
|
if (!spec.has_learnt && ((spec.must_learn & 0x10) != 0x10)) {
|
|
user->MessageString(Chat::LightBlue, TRADESKILL_LEARN_RECIPE, spec.name.c_str());
|
|
}
|
|
database.UpdateRecipeMadecount(spec.recipe_id, user->CharacterID(), spec.madecount+1);
|
|
}
|
|
|
|
|
|
//TODO: find in-pack containers in inventory, make sure they are really
|
|
//there, and then use that slot to handle replace_container too.
|
|
if(success && spec.replace_container) {
|
|
// user->DeleteItemInInventory(in_combine->container_slot, 0, true);
|
|
}
|
|
|
|
if (success) {
|
|
if (parse->PlayerHasQuestSub(EVENT_COMBINE_SUCCESS)) {
|
|
parse->EventPlayer(EVENT_COMBINE_SUCCESS, user, spec.name, spec.recipe_id);
|
|
}
|
|
} else {
|
|
if (parse->PlayerHasQuestSub(EVENT_COMBINE_FAILURE)) {
|
|
parse->EventPlayer(EVENT_COMBINE_FAILURE, user, spec.name, spec.recipe_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
auto character_learned_recipe_list = CharRecipeListRepository::GetWhere(
|
|
database,
|
|
fmt::format("char_id = {}", CharacterID())
|
|
);
|
|
|
|
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[4] == nullptr || row[5] == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
uint32 recipe_id = (uint32) Strings::ToInt(row[0]);
|
|
const char *name = row[1];
|
|
uint32 trivial = (uint32) Strings::ToInt(row[2]);
|
|
uint32 comp_count = (uint32) Strings::ToInt(row[3]);
|
|
uint32 tradeskill = (uint16) Strings::ToInt(row[4]);
|
|
uint32 must_learn = (uint16) Strings::ToInt(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
|
|
auto character_learned_recipe = CharRecipeListRepository::GetCharRecipeListEntry(
|
|
character_learned_recipe_list,
|
|
recipe_id
|
|
);
|
|
|
|
if (RuleB(Skills, UseLimitTradeskillSearchSkillDiff) &&
|
|
((int32) trivial - (int32) GetSkill((EQ::skills::SkillType) tradeskill)) >
|
|
RuleI(Skills, MaxTradeskillSearchSkillDiff)) {
|
|
|
|
LogTradeskills("Checking limit recipe_id [{}] name [{}]", recipe_id, name);
|
|
|
|
if (character_learned_recipe.madecount == 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//Skip recipes that must be learned
|
|
if ((must_learn & 0xf) && !character_learned_recipe.recipe_id) {
|
|
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->component_count = comp_count;
|
|
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) {
|
|
|
|
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 "
|
|
"WHERE tre.componentcount > 0 AND tre.recipe_id = %u",
|
|
recipe_id);
|
|
auto results = content_db.QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() < 1) {
|
|
LogError("Error in SendTradeskillDetails: no components returned");
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() > 10) {
|
|
LogError("Error in SendTradeskillDetails: too many components returned ([{}])", results.RowCount());
|
|
return;
|
|
}
|
|
|
|
//biggest this packet can ever be:
|
|
// 64 * 10 + 8 * 10 + 4 + 4 * 10 = 764
|
|
auto buf = new char[775]; // dynamic so we can just give it to EQApplicationPacket
|
|
uint8 r,k;
|
|
|
|
uint32 *header = (uint32 *) buf;
|
|
//Hell if I know why this is in the wrong byte order....
|
|
*header = htonl(recipe_id);
|
|
|
|
char *startblock = buf;
|
|
startblock += sizeof(uint32);
|
|
|
|
uint32 *ffff_start = (uint32 *) startblock;
|
|
//fill in the FFFF's as if there were 0 items
|
|
for(r = 0; r < 10; r++) { // world:item container size related?
|
|
*ffff_start = 0xFFFFFFFF;
|
|
ffff_start++;
|
|
}
|
|
char * datastart = (char *) ffff_start;
|
|
char * cblock = (char *) ffff_start;
|
|
|
|
uint32 *itemptr;
|
|
uint32 *iconptr;
|
|
uint32 len;
|
|
uint32 datalen = 0;
|
|
uint8 count = 0;
|
|
|
|
for(auto row = results.begin(); row != results.end(); ++row) {
|
|
|
|
//watch for references to items which are not in the
|
|
//items table, which the left join will make nullptr...
|
|
if(row[2] == nullptr || row[3] == nullptr)
|
|
continue;
|
|
|
|
uint32 item = (uint32)Strings::ToInt(row[0]);
|
|
uint8 num = (uint8) Strings::ToInt(row[1]);
|
|
uint32 icon = (uint32) Strings::ToInt(row[2]);
|
|
|
|
const char *name = row[3];
|
|
len = strlen(name);
|
|
if(len > 63)
|
|
len = 63;
|
|
|
|
//Hell if I know why these are in the wrong byte order....
|
|
item = htonl(item);
|
|
icon = htonl(icon);
|
|
|
|
//if we get more than 10 items, just start skipping them...
|
|
for(k = 0; k < num && count < 10; k++) { // world:item container size related?
|
|
itemptr = (uint32 *) cblock;
|
|
cblock += sizeof(uint32);
|
|
datalen += sizeof(uint32);
|
|
iconptr = (uint32 *) cblock;
|
|
cblock += sizeof(uint32);
|
|
datalen += sizeof(uint32);
|
|
|
|
*itemptr = item;
|
|
*iconptr = icon;
|
|
strncpy(cblock, name, len);
|
|
|
|
cblock[len] = '\0'; //just making sure.
|
|
cblock += len + 1; //get the null
|
|
datalen += len + 1; //get the null
|
|
count++;
|
|
}
|
|
|
|
}
|
|
|
|
//now move the item data over top of the FFFFs
|
|
uint8 dist = sizeof(uint32) * (10 - count);
|
|
startblock += dist;
|
|
memmove(startblock, datastart, datalen);
|
|
|
|
uint32 total = sizeof(uint32) + dist + datalen;
|
|
|
|
auto outapp = new EQApplicationPacket(OP_RecipeDetails);
|
|
outapp->size = total;
|
|
outapp->pBuffer = (uchar*) buf;
|
|
QueuePacket(outapp);
|
|
DumpPacket(outapp);
|
|
safe_delete(outapp);
|
|
}
|
|
|
|
//returns true on success
|
|
bool Client::TradeskillExecute(DBTradeskillRecipe_Struct *spec) {
|
|
if (!spec) {
|
|
return false;
|
|
}
|
|
|
|
uint16 user_skill = GetSkill(spec->tradeskill);
|
|
|
|
if (RuleI(Skills, TradeSkillClamp) != 0 && user_skill > RuleI(Skills, TradeSkillClamp)) {
|
|
user_skill = RuleI(Skills, TradeSkillClamp);
|
|
}
|
|
|
|
float chance = 0.0;
|
|
float skillup_modifier = 0.0;
|
|
int16 thirdstat = 0;
|
|
int16 stat_modifier = 15;
|
|
uint16 success_modifier = 0;
|
|
|
|
// Rework based on the info on eqtraders.com
|
|
// http://mboards.eqtraders.com/eq/showthread.php?t=22246
|
|
// 09/10/2006 v0.1 (eq4me)
|
|
// 09/11/2006 v0.2 (eq4me)
|
|
// Todo:
|
|
// Implementing AAs
|
|
// Success modifiers based on recipes
|
|
// Skillup modifiers based on the rarity of the ingredients
|
|
|
|
// Some tradeskills are more eqal then others. ;-)
|
|
// If you want to customize the stage1 success rate do it here.
|
|
// Remember: skillup_modifier is (float). Lower is better
|
|
switch(spec->tradeskill) {
|
|
case EQ::skills::SkillFletching:
|
|
skillup_modifier = RuleR(Character, TradeskillUpFletching);
|
|
break;
|
|
case EQ::skills::SkillAlchemy:
|
|
skillup_modifier = RuleR(Character, TradeskillUpAlchemy);
|
|
break;
|
|
case EQ::skills::SkillJewelryMaking:
|
|
skillup_modifier = RuleR(Character, TradeskillUpJewelcrafting);
|
|
break;
|
|
case EQ::skills::SkillPottery:
|
|
skillup_modifier = RuleR(Character, TradeskillUpPottery);
|
|
break;
|
|
case EQ::skills::SkillBaking:
|
|
skillup_modifier = RuleR(Character, TradeskillUpBaking);
|
|
break;
|
|
case EQ::skills::SkillBrewing:
|
|
skillup_modifier = RuleR(Character, TradeskillUpBrewing);
|
|
break;
|
|
case EQ::skills::SkillBlacksmithing:
|
|
skillup_modifier = RuleR(Character, TradeskillUpBlacksmithing);
|
|
break;
|
|
case EQ::skills::SkillResearch:
|
|
skillup_modifier = RuleR(Character, TradeskillUpResearch);
|
|
break;
|
|
case EQ::skills::SkillMakePoison:
|
|
skillup_modifier = RuleR(Character, TradeskillUpMakePoison);
|
|
break;
|
|
case EQ::skills::SkillTinkering:
|
|
skillup_modifier = RuleR(Character, TradeskillUpTinkering);
|
|
break;
|
|
case EQ::skills::SkillTailoring:
|
|
skillup_modifier = RuleR(Character, TradeskillUpTailoring);
|
|
break;
|
|
default:
|
|
skillup_modifier = 2;
|
|
break;
|
|
}
|
|
|
|
// Some tradeskills take the higher of one additional stat beside INT and WIS
|
|
// to determine the skillup rate. Additionally these tradeskills do not have an
|
|
// -15 modifier on their statbonus.
|
|
if (spec->tradeskill == EQ::skills::SkillFletching || spec->tradeskill == EQ::skills::SkillMakePoison) {
|
|
thirdstat = GetDEX();
|
|
stat_modifier = 0;
|
|
}
|
|
else if (spec->tradeskill == EQ::skills::SkillBlacksmithing) {
|
|
thirdstat = GetSTR();
|
|
stat_modifier = 0;
|
|
}
|
|
|
|
int16 higher_from_int_wis = (GetINT() > GetWIS()) ? GetINT() : GetWIS();
|
|
int16 bonusstat = (higher_from_int_wis > thirdstat) ? higher_from_int_wis : thirdstat;
|
|
|
|
std::vector< std::pair<uint32,uint8> >::iterator itr;
|
|
|
|
|
|
//calculate the base success chance
|
|
// For trivials over 68 the chance is (skill - 0.75*trivial) +51.5
|
|
// For trivial up to 68 the chance is (skill - trivial) + 66
|
|
if (spec->trivial >= 68) {
|
|
chance = (user_skill - (0.75*spec->trivial)) + 51.5;
|
|
} else {
|
|
chance = (user_skill - spec->trivial) + 66;
|
|
}
|
|
|
|
int16 over_trivial = (int16)GetRawSkill(spec->tradeskill) - (int16)spec->trivial;
|
|
|
|
//handle caps
|
|
if(spec->nofail) {
|
|
chance = 100; //cannot fail.
|
|
LogTradeskills("This combine cannot fail");
|
|
} else if(over_trivial >= 0) {
|
|
// At reaching trivial the chance goes to 95% going up an additional
|
|
// percent for every 40 skillpoints above the trivial.
|
|
// The success rate is not modified through stats.
|
|
// Mastery AAs are unaccounted for so far.
|
|
// chance_AA = chance + ((100 - chance) * mastery_modifier)
|
|
// But the 95% limit with an additional 1% for every 40 skill points
|
|
// above critical still stands.
|
|
// Mastery modifier is: 10%/25%/50% for rank one/two/three
|
|
chance = 95.0f + (float(user_skill - spec->trivial) / 40.0f);
|
|
|
|
if (RuleB(Skills, TrivialTradeskillCombinesNoFail)) {
|
|
chance = 100;
|
|
}
|
|
|
|
MessageString(Chat::Emote, TRADESKILL_TRIVIAL);
|
|
} else if(chance < 5) {
|
|
// Minimum chance is always 5
|
|
chance = 5;
|
|
} else if(chance > 95) {
|
|
//cap is 95, shouldent reach this before trivial, but just in case.
|
|
chance = 95;
|
|
}
|
|
|
|
LogTradeskills("Current skill: [{}] , Trivial: [{}] , Success chance: [{}] percent", user_skill , spec->trivial , chance);
|
|
LogTradeskills("Bonusstat: [{}] , INT: [{}] , WIS: [{}] , DEX: [{}] , STR: [{}]", bonusstat , GetINT() , GetWIS() , GetDEX() , GetSTR());
|
|
|
|
float res = zone->random.Real(0, 99);
|
|
int aa_chance = 0;
|
|
|
|
aa_chance = spellbonuses.ReduceTradeskillFail[spec->tradeskill] + itembonuses.ReduceTradeskillFail[spec->tradeskill] + aabonuses.ReduceTradeskillFail[spec->tradeskill];
|
|
|
|
const EQ::ItemData* item = nullptr;
|
|
|
|
if (
|
|
(
|
|
spec->tradeskill == EQ::skills::SkillRemoveTraps ||
|
|
GetGM() ||
|
|
(chance > res)
|
|
) ||
|
|
zone->random.Roll(aa_chance)
|
|
) {
|
|
if (GetGM()) {
|
|
Message(Chat::White, "Your GM flag gives you a 100%% chance to succeed in combining this tradeskill.");
|
|
}
|
|
|
|
success_modifier = 1;
|
|
|
|
if (over_trivial < 0) {
|
|
CheckIncreaseTradeskill(bonusstat, stat_modifier, skillup_modifier, success_modifier, spec->tradeskill);
|
|
}
|
|
|
|
MessageString(Chat::LightBlue, TRADESKILL_SUCCEED, spec->name.c_str());
|
|
|
|
LogTradeskills("Tradeskill success");
|
|
|
|
itr = spec->onsuccess.begin();
|
|
while(itr != spec->onsuccess.end() && !spec->quest) {
|
|
|
|
item = database.GetItem(itr->first);
|
|
if (item) {
|
|
CheckItemDiscoverability(itr->first);
|
|
SummonItem(itr->first, itr->second);
|
|
if (GetGroup()) {
|
|
entity_list.MessageGroup(this, true, Chat::Skills, "%s has successfully fashioned %s!", GetName(), item->Name);
|
|
}
|
|
}
|
|
else {
|
|
Log(
|
|
Logs::General,
|
|
Logs::Tradeskills,
|
|
StringFormat(
|
|
"Failure (null item pointer [id: %u, qty: %u]) :: recipe_id:%i tskillid:%i trivial:%i chance:%4.2f in zoneid:%i instid:%i",
|
|
itr->first,
|
|
itr->second,
|
|
spec->recipe_id,
|
|
spec->tradeskill,
|
|
spec->trivial,
|
|
chance,
|
|
GetZoneID(),
|
|
GetInstanceID()
|
|
).c_str()
|
|
);
|
|
}
|
|
|
|
if (RuleB(TaskSystem, EnableTaskSystem)) {
|
|
UpdateTasksForItem(TaskActivityType::TradeSkill, itr->first, itr->second);
|
|
}
|
|
|
|
++itr;
|
|
}
|
|
return(true);
|
|
}
|
|
/* Tradeskill Fail */
|
|
else {
|
|
success_modifier = 2; // Halves the chance
|
|
|
|
if(over_trivial < 0)
|
|
CheckIncreaseTradeskill(bonusstat, stat_modifier, skillup_modifier, success_modifier, spec->tradeskill);
|
|
|
|
MessageString(Chat::Emote,TRADESKILL_FAILED);
|
|
|
|
LogTradeskills("Tradeskill failed");
|
|
if (GetGroup())
|
|
{
|
|
entity_list.MessageGroup(this, true, Chat::Skills,"%s was unsuccessful in %s tradeskill attempt.",GetName(),GetGender() == Gender::Male ? "his" : GetGender() == Gender::Female ? "her" : "its");
|
|
|
|
}
|
|
|
|
itr = spec->onfail.begin();
|
|
while(itr != spec->onfail.end()) {
|
|
//should we check these arguments?
|
|
SummonItem(itr->first, itr->second);
|
|
++itr;
|
|
}
|
|
|
|
/* Salvage Item rolls */
|
|
|
|
// Rolls on each item, is possible to return everything
|
|
int SalvageChance = aabonuses.SalvageChance + itembonuses.SalvageChance + spellbonuses.SalvageChance;
|
|
// Skip check if not a normal TS or if a quest recipe these should be nofail, but check amyways
|
|
if(SalvageChance && spec->tradeskill != 75 && !spec->quest) {
|
|
itr = spec->salvage.begin();
|
|
uint8 sc = 0;
|
|
while(itr != spec->salvage.end()) {
|
|
for (sc = 0; sc < itr->second; sc++) {
|
|
if (zone->random.Roll(SalvageChance)) {
|
|
SummonItem(itr->first, 1);
|
|
}
|
|
}
|
|
++itr;
|
|
}
|
|
}
|
|
|
|
}
|
|
return(false);
|
|
}
|
|
|
|
void Client::CheckIncreaseTradeskill(int16 bonusstat, int16 stat_modifier, float skillup_modifier, uint16 success_modifier, EQ::skills::SkillType tradeskill)
|
|
{
|
|
uint16 current_raw_skill = GetRawSkill(tradeskill);
|
|
|
|
if(!CanIncreaseTradeskill(tradeskill))
|
|
return; //not allowed to go higher.
|
|
uint16 maxskill = MaxSkill(tradeskill);
|
|
|
|
float min_skill_up_chance = RuleR(Character, TradeskillUpMinChance);
|
|
min_skill_up_chance = std::max(min_skill_up_chance, 2.5f);
|
|
|
|
float chance_stage2 = 0;
|
|
|
|
//A successfull combine doubles the stage1 chance for an skillup
|
|
//Some tradeskill are harder than others. See above for more.
|
|
float chance_stage1 = (bonusstat - stat_modifier) / (skillup_modifier * success_modifier);
|
|
chance_stage1 = std::max(min_skill_up_chance, chance_stage1);
|
|
|
|
//In stage2 the only thing that matters is your current unmodified skill
|
|
//and the Character:TradeskillUpMinChance rule.
|
|
if (chance_stage1 > zone->random.Real(0, 99)) {
|
|
if (current_raw_skill < 15) {
|
|
//Always succeed
|
|
chance_stage2 = 100;
|
|
} else if (current_raw_skill < 175) {
|
|
//From skill 16 to 174 your chance of success falls linearly from 92% to 13%.
|
|
chance_stage2 = (200 - current_raw_skill) / 2;
|
|
} else {
|
|
//At skill 175, your chance of success falls linearly from 12.5% to 2.5% at skill 300.
|
|
chance_stage2 = 12.5 - (.08 * (current_raw_skill - 175));
|
|
}
|
|
chance_stage2 = std::max(min_skill_up_chance, chance_stage2);
|
|
}
|
|
|
|
if (chance_stage2 > zone->random.Real(0, 99)) {
|
|
//Only if stage1 and stage2 succeeded you get a skillup.
|
|
SetSkill(tradeskill, current_raw_skill + 1);
|
|
std::string export_string = fmt::format(
|
|
"{} {} {} {}",
|
|
tradeskill,
|
|
current_raw_skill + 1,
|
|
maxskill,
|
|
1
|
|
);
|
|
parse->EventPlayer(EVENT_SKILL_UP, this, export_string, 0);
|
|
if(title_manager.IsNewTradeSkillTitleAvailable(tradeskill, current_raw_skill + 1))
|
|
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);
|
|
}
|
|
|
|
bool ZoneDatabase::GetTradeRecipe(
|
|
const EQ::ItemInstance* container,
|
|
uint8 c_type,
|
|
uint32 some_id,
|
|
Client* c,
|
|
DBTradeskillRecipe_Struct* spec,
|
|
bool* is_augmented
|
|
)
|
|
{
|
|
if (!container) {
|
|
return false;
|
|
}
|
|
|
|
if (!c) {
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
//Could prolly watch for stacks in this loop and handle them properly...
|
|
//just increment sum and count accordingly
|
|
bool first = true;
|
|
std::string buf2;
|
|
uint32 count = 0;
|
|
uint32 sum = 0;
|
|
|
|
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) {
|
|
continue;
|
|
}
|
|
|
|
if (inst->IsAugmented()) {
|
|
*is_augmented = true;
|
|
return false;
|
|
}
|
|
|
|
const auto item = database.GetItem(inst->GetItem()->ID);
|
|
if (!item) {
|
|
LogTradeskills("item [{}] not found!", inst->GetItem()->ID);
|
|
continue;
|
|
}
|
|
|
|
if (first) {
|
|
buf2 += fmt::format("{}", item->ID);
|
|
first = false;
|
|
} else {
|
|
buf2 += fmt::format(", {}", item->ID);
|
|
}
|
|
|
|
sum += item->ID;
|
|
count++;
|
|
|
|
LogTradeskills(
|
|
"Item in container index [{}] item [{}] found [{}]",
|
|
slot_id,
|
|
item->ID,
|
|
count
|
|
);
|
|
}
|
|
|
|
// no items == no recipe
|
|
if (!count) {
|
|
return false;
|
|
}
|
|
|
|
std::string query = fmt::format(
|
|
"SELECT tre.recipe_id FROM tradeskill_recipe_entries AS tre "
|
|
"INNER JOIN tradeskill_recipe AS tr ON (tre.recipe_id = tr.id) "
|
|
"WHERE tr.enabled AND ((tre.item_id IN ({}) AND tre.componentcount > 0) "
|
|
"OR (tre.item_id {} AND tre.iscontainer = 1))"
|
|
"GROUP BY tre.recipe_id HAVING SUM(tre.componentcount) = {} "
|
|
"AND SUM(tre.item_id * tre.componentcount) = {}",
|
|
buf2,
|
|
containers,
|
|
count,
|
|
sum
|
|
);
|
|
|
|
auto results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
LogError("Error in search, query: [{}]", query.c_str());
|
|
LogError("Error in search, error: [{}]", results.ErrorMessage().c_str());
|
|
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) {
|
|
const uint32 recipe_id = Strings::ToUnsignedInt(row[0]);
|
|
if (first) {
|
|
buf2 += fmt::format("{}", recipe_id);
|
|
first = false;
|
|
} else {
|
|
buf2 += fmt::format(", {}", recipe_id);
|
|
}
|
|
|
|
// length limit on buf2
|
|
if (index == 214) { // Maximum number of recipe matches (19 * 215 = 4096)
|
|
LogError(
|
|
"Warning: Too many matches. Unable to search all recipe entries. Searched [{}] of [{}] possible entries",
|
|
index + 1,
|
|
results.RowCount());
|
|
break;
|
|
}
|
|
|
|
++index;
|
|
}
|
|
|
|
query = fmt::format(
|
|
"SELECT tre.recipe_id FROM tradeskill_recipe_entries AS tre "
|
|
"WHERE tre.recipe_id IN ({}) GROUP BY tre.recipe_id HAVING sum(tre.componentcount) = {} "
|
|
"AND sum(tre.item_id * tre.componentcount) = {}",
|
|
buf2,
|
|
count,
|
|
sum
|
|
);
|
|
results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
LogError("Re-query: [{}]", query.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;
|
|
}
|
|
|
|
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!");
|
|
return false;
|
|
}
|
|
|
|
if (results.RowCount() > 1) { //Recipe contents matched more than 1 recipe in this container
|
|
LogError(
|
|
"Combine error: Recipe is not unique! [{}] matches found for container [{}]. Continuing with first recipe match",
|
|
results.RowCount(),
|
|
container_item_id
|
|
);
|
|
}
|
|
}
|
|
|
|
auto row = results.begin();
|
|
const uint32 recipe_id = Strings::ToUnsignedInt(row[0]);
|
|
|
|
//Right here we verify that we actually have ALL of the tradeskill components..
|
|
//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
|
|
query = fmt::format(
|
|
"SELECT item_id, componentcount FROM tradeskill_recipe_entries WHERE componentcount > 0 AND recipe_id = {}",
|
|
recipe_id
|
|
);
|
|
|
|
results = QueryDatabase(query);
|
|
if (!results.Success() || !results.RowCount()) {
|
|
return GetTradeRecipe(recipe_id, c_type, some_id, c, spec);
|
|
}
|
|
|
|
for (auto row : results) {
|
|
int component_count = 0;
|
|
|
|
for (uint8 slot_id = EQ::invbag::SLOT_BEGIN; slot_id < EQ::invtype::WORLD_SIZE; slot_id++) {
|
|
const auto inst = container->GetItem(slot_id);
|
|
if (!inst) {
|
|
continue;
|
|
}
|
|
|
|
if (inst->IsAugmented()) {
|
|
*is_augmented = true;
|
|
return false;
|
|
}
|
|
|
|
const auto item = database.GetItem(inst->GetItem()->ID);
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
|
|
if (item->ID == Strings::ToUnsignedInt(row[0])) {
|
|
component_count++;
|
|
}
|
|
|
|
LogTradeskills(
|
|
"Component count loop [{}] item [{}] recipe component_count [{}]",
|
|
component_count,
|
|
item->ID,
|
|
Strings::ToInt(row[1])
|
|
);
|
|
}
|
|
|
|
if (component_count != Strings::ToInt(row[1])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return GetTradeRecipe(recipe_id, c_type, some_id, c, spec);
|
|
}
|
|
|
|
bool ZoneDatabase::GetTradeRecipe(
|
|
uint32 recipe_id,
|
|
uint8 c_type,
|
|
uint32 some_id,
|
|
Client* c,
|
|
DBTradeskillRecipe_Struct* spec
|
|
)
|
|
{
|
|
if (!c) {
|
|
return false;
|
|
}
|
|
|
|
std::string container_where_filter;
|
|
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
|
|
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 = {}
|
|
AND tradeskill_recipe_entries.item_id {}
|
|
AND tradeskill_recipe.enabled
|
|
GROUP BY
|
|
tradeskill_recipe.id
|
|
),
|
|
recipe_id,
|
|
container_where_filter
|
|
);
|
|
auto results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
LogError("Error, query: [{}]", query.c_str());
|
|
LogError("Error: [{}]", results.ErrorMessage().c_str());
|
|
return false;
|
|
}
|
|
|
|
if (!results.RowCount()) {
|
|
return false;
|
|
}
|
|
|
|
auto row = results.begin();
|
|
|
|
spec->tradeskill = static_cast<EQ::skills::SkillType>(Strings::ToUnsignedInt(row[1]));
|
|
spec->skill_needed = Strings::ToInt(row[2]);
|
|
spec->trivial = Strings::ToUnsignedInt(row[3]);
|
|
spec->nofail = Strings::ToBool(row[4]);
|
|
spec->replace_container = Strings::ToBool(row[5]);
|
|
spec->name = row[6];
|
|
spec->must_learn = Strings::ToUnsignedInt(row[7]);
|
|
spec->quest = Strings::ToBool(row[8]);
|
|
spec->has_learnt = false;
|
|
spec->madecount = 0;
|
|
spec->recipe_id = recipe_id;
|
|
|
|
auto r = CharRecipeListRepository::GetWhere(
|
|
database,
|
|
fmt::format(
|
|
"char_id = {} and recipe_id = {}",
|
|
c->CharacterID(),
|
|
recipe_id
|
|
)
|
|
);
|
|
|
|
if (!r.empty() && r[0].recipe_id) { //If this exists we learned it
|
|
LogTradeskills("made_count [{}]", r[0].madecount);
|
|
|
|
spec->has_learnt = true;
|
|
spec->madecount = static_cast<uint32>(r[0].madecount);
|
|
}
|
|
|
|
//Pull the on-success items...
|
|
query = fmt::format(
|
|
"SELECT item_id, successcount FROM tradeskill_recipe_entries WHERE successcount > 0 AND recipe_id = {}",
|
|
recipe_id
|
|
);
|
|
results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
return false;
|
|
}
|
|
|
|
if (!results.RowCount() && !spec->quest) {
|
|
LogError("Error in success: no success items returned");
|
|
return false;
|
|
}
|
|
|
|
spec->onsuccess.clear();
|
|
for(auto row : results) {
|
|
const uint32 item_id = Strings::ToUnsignedInt(row[0]);
|
|
const uint8 success_count = Strings::ToUnsignedInt(row[1]);
|
|
spec->onsuccess.emplace_back(std::pair<uint32, uint8>(item_id, success_count));
|
|
}
|
|
|
|
spec->onfail.clear();
|
|
|
|
//Pull the on-fail items...
|
|
query = fmt::format(
|
|
"SELECT item_id, failcount FROM tradeskill_recipe_entries WHERE failcount > 0 AND recipe_id = {}",
|
|
recipe_id
|
|
);
|
|
results = QueryDatabase(query);
|
|
if (results.Success()) {
|
|
for (auto row : results) {
|
|
const uint32 item_id = Strings::ToUnsignedInt(row[0]);
|
|
const uint8 fail_count = Strings::ToUnsignedInt(row[1]);
|
|
spec->onfail.emplace_back(std::pair<uint32, uint8>(item_id, fail_count));
|
|
}
|
|
}
|
|
|
|
spec->salvage.clear();
|
|
|
|
// Don't bother with the query if TS is nofail
|
|
if (spec->nofail) {
|
|
return true;
|
|
}
|
|
|
|
// Pull the salvage list
|
|
query = fmt::format(
|
|
"SELECT item_id, salvagecount FROM tradeskill_recipe_entries WHERE salvagecount > 0 AND recipe_id = {}",
|
|
recipe_id
|
|
);
|
|
|
|
results = QueryDatabase(query);
|
|
if (results.Success()) {
|
|
for (auto row : results) {
|
|
const uint32 item_id = Strings::ToUnsignedInt(row[0]);
|
|
const uint8 salvage_count = Strings::ToUnsignedInt(row[1]);
|
|
spec->salvage.emplace_back(std::pair<uint32, uint8>(item_id, salvage_count));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ZoneDatabase::UpdateRecipeMadecount(uint32 recipe_id, uint32 char_id, uint32 madeCount)
|
|
{
|
|
std::string query = StringFormat("INSERT INTO char_recipe_list "
|
|
"SET recipe_id = %u, char_id = %u, madecount = %u "
|
|
"ON DUPLICATE KEY UPDATE madecount = %u;",
|
|
recipe_id, char_id, madeCount, madeCount);
|
|
QueryDatabase(query);
|
|
}
|
|
|
|
void Client::LearnRecipe(uint32 recipe_id)
|
|
{
|
|
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;
|
|
}
|
|
|
|
auto tradeskill_recipe = TradeskillRecipeRepository::FindOne(content_db, recipe_id);
|
|
if (tradeskill_recipe.id == 0) {
|
|
LogError("Invalid recipe [{}]", recipe_id);
|
|
return;
|
|
}
|
|
|
|
LogTradeskills(
|
|
"recipe_id [{}] name [{}] learned [{}]",
|
|
recipe_id,
|
|
tradeskill_recipe.name,
|
|
results.RowCount()
|
|
);
|
|
|
|
auto row = results.begin();
|
|
if (results.RowCount() > 0) {
|
|
return;
|
|
}
|
|
|
|
MessageString(Chat::LightBlue, TRADESKILL_LEARN_RECIPE, tradeskill_recipe.name.c_str());
|
|
|
|
database.QueryDatabase(
|
|
fmt::format(
|
|
"REPLACE INTO char_recipe_list (recipe_id, char_id, madecount) VALUES ({}, {}, 0)",
|
|
recipe_id,
|
|
CharacterID()
|
|
)
|
|
);
|
|
}
|
|
|
|
std::vector<uint32> ZoneDatabase::GetRecipeComponentItemIDs(RecipeCountType count_type, uint32 recipe_id)
|
|
{
|
|
std::vector<uint32> l;
|
|
|
|
const auto& tr = TradeskillRecipeRepository::FindOne(content_db, recipe_id);
|
|
if (!tr.id) {
|
|
return l;
|
|
}
|
|
|
|
std::string c;
|
|
switch (count_type) {
|
|
case RecipeCountType::Success:
|
|
c = "successcount";
|
|
break;
|
|
case RecipeCountType::Fail:
|
|
c = "failcount";
|
|
break;
|
|
case RecipeCountType::Component:
|
|
c = "componentcount";
|
|
break;
|
|
case RecipeCountType::Salvage:
|
|
c = "salvagecount";
|
|
break;
|
|
case RecipeCountType::Container:
|
|
c = "iscontainer";
|
|
break;
|
|
}
|
|
|
|
const auto& tre = TradeskillRecipeEntriesRepository::GetWhere(
|
|
content_db,
|
|
fmt::format(
|
|
"recipe_id = {} AND {} >= 1 ORDER BY id ASC",
|
|
recipe_id,
|
|
c
|
|
)
|
|
);
|
|
if (tre.empty()) {
|
|
return l;
|
|
}
|
|
|
|
for (const auto& e : tre) {
|
|
l.emplace_back(e.item_id);
|
|
}
|
|
|
|
return l;
|
|
}
|
|
|
|
int8 ZoneDatabase::GetRecipeComponentCount(RecipeCountType count_type, uint32 recipe_id, uint32 item_id)
|
|
{
|
|
const auto& tr = TradeskillRecipeRepository::FindOne(content_db, recipe_id);
|
|
if (!tr.id) {
|
|
return -1;
|
|
}
|
|
|
|
const auto& tre = TradeskillRecipeEntriesRepository::GetWhere(
|
|
content_db,
|
|
fmt::format(
|
|
"recipe_id = {} AND item_id = {} ORDER BY id ASC LIMIT 1",
|
|
recipe_id,
|
|
item_id
|
|
)
|
|
);
|
|
if (tre.empty()) {
|
|
return -1;
|
|
}
|
|
|
|
switch (count_type) {
|
|
case RecipeCountType::Success:
|
|
return tre[0].successcount;
|
|
case RecipeCountType::Fail:
|
|
return tre[0].failcount;
|
|
case RecipeCountType::Component:
|
|
return tre[0].componentcount;
|
|
case RecipeCountType::Salvage:
|
|
return tre[0].salvagecount;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
bool Client::CanIncreaseTradeskill(EQ::skills::SkillType tradeskill) {
|
|
uint32 rawskill = GetRawSkill(tradeskill);
|
|
uint16 maxskill = MaxSkill(tradeskill);
|
|
|
|
if (rawskill >= maxskill) //Max skill sanity check
|
|
return false;
|
|
|
|
uint8 Baking = (GetRawSkill(EQ::skills::SkillBaking) > 200) ? 1 : 0;
|
|
uint8 Smithing = (GetRawSkill(EQ::skills::SkillBlacksmithing) > 200) ? 1 : 0;
|
|
uint8 Brewing = (GetRawSkill(EQ::skills::SkillBrewing) > 200) ? 1 : 0;
|
|
uint8 Fletching = (GetRawSkill(EQ::skills::SkillFletching) > 200) ? 1 : 0;
|
|
uint8 Jewelry = (GetRawSkill(EQ::skills::SkillJewelryMaking) > 200) ? 1 : 0;
|
|
uint8 Pottery = (GetRawSkill(EQ::skills::SkillPottery) > 200) ? 1 : 0;
|
|
uint8 Tailoring = (GetRawSkill(EQ::skills::SkillTailoring) > 200) ? 1 : 0;
|
|
uint8 SkillTotal = Baking + Smithing + Brewing + Fletching + Jewelry + Pottery + Tailoring; //Tradeskills above 200
|
|
//New Tanaan AA: Each level allows an additional tradeskill above 200 (first one is free)
|
|
uint8 aaLevel = spellbonuses.TradeSkillMastery + itembonuses.TradeSkillMastery + aabonuses.TradeSkillMastery;
|
|
|
|
switch (tradeskill) {
|
|
case EQ::skills::SkillBaking:
|
|
case EQ::skills::SkillBlacksmithing:
|
|
case EQ::skills::SkillBrewing:
|
|
case EQ::skills::SkillFletching:
|
|
case EQ::skills::SkillJewelryMaking:
|
|
case EQ::skills::SkillPottery:
|
|
case EQ::skills::SkillTailoring:
|
|
if (aaLevel == 6)
|
|
break; //Maxed AA
|
|
if (SkillTotal == 0)
|
|
break; //First tradeskill freebie
|
|
if ((SkillTotal == (aaLevel + 1)) && (rawskill > 200))
|
|
break; //One of the tradeskills already allowed to go over 200
|
|
if ((SkillTotal >= (aaLevel + 1)) && (rawskill >= 200))
|
|
return false; //One or more tradeskills already at or beyond limit
|
|
break;
|
|
default:
|
|
break; //Other skills unchecked and ability to increase assumed true
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ZoneDatabase::EnableRecipe(uint32 recipe_id)
|
|
{
|
|
std::string query = StringFormat("UPDATE tradeskill_recipe SET enabled = 1 "
|
|
"WHERE id = %u;", recipe_id);
|
|
auto results = QueryDatabase(query);
|
|
if (!results.Success())
|
|
|
|
return results.RowsAffected() > 0;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ZoneDatabase::DisableRecipe(uint32 recipe_id)
|
|
{
|
|
std::string query = StringFormat("UPDATE tradeskill_recipe SET enabled = 0 "
|
|
"WHERE id = %u;", recipe_id);
|
|
auto results = QueryDatabase(query);
|
|
if (!results.Success())
|
|
|
|
return results.RowsAffected() > 0;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Client::CheckTradeskillLoreConflict(int32 recipe_id)
|
|
{
|
|
auto recipe_entries = TradeskillRecipeEntriesRepository::GetWhere(
|
|
content_db,
|
|
fmt::format(
|
|
"recipe_id = {} ORDER BY componentcount DESC",
|
|
recipe_id
|
|
)
|
|
);
|
|
if (recipe_entries.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// validate which items from the recipe we will call CheckLoreConflict on
|
|
for (const auto &tre : recipe_entries) {
|
|
if (tre.item_id) {
|
|
auto tre_inst = database.GetItem(tre.item_id);
|
|
|
|
// To compare items we iterate against each item in the recipe that have a loregroup.
|
|
for (auto &tre_update_item : recipe_entries) {
|
|
bool fi_is_valid = tre_update_item.item_id && tre_inst && tre_inst->LoreGroup != 0;
|
|
|
|
if (fi_is_valid) {
|
|
auto tre_update_item_inst = database.GetItem(tre_update_item.item_id);
|
|
bool ei_is_valid = tre_update_item_inst && tre_update_item_inst->LoreGroup != 0;
|
|
|
|
if (ei_is_valid) {
|
|
bool unique_lore_group_match = tre_inst->LoreGroup > 0 && tre_inst->LoreGroup == tre_update_item_inst->LoreGroup;
|
|
bool component_count_is_valid = tre_update_item.componentcount == 0 && tre.componentcount > 0;
|
|
|
|
// If the recipe item is a component, and matches a unique lore group (> 0) or the item_id matches another entry in the recipe
|
|
// zero out the item_id, this will prevent us from doing a lore check inadvertently where
|
|
// the item is a component, and returned on success, fail, salvage.
|
|
// or uses an item that is part of a unique loregroup that returns an item of the same unique loregroup
|
|
if (ei_is_valid && (tre_update_item.item_id == tre.item_id || unique_lore_group_match) && component_count_is_valid) {
|
|
tre_update_item.item_id = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tre_inst) {
|
|
if (tre_inst->LoreGroup == 0 || tre.componentcount > 0 || tre.iscontainer) {
|
|
continue;
|
|
}
|
|
|
|
if (CheckLoreConflict(tre_inst)) {
|
|
EQ::SayLinkEngine linker;
|
|
linker.SetLinkType(EQ::saylink::SayLinkItemData);
|
|
linker.SetItemData(tre_inst);
|
|
auto item_link = linker.GenerateLink();
|
|
MessageString(Chat::Red, TRADESKILL_COMBINE_LORE, item_link.c_str());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|