/* EQEmu: EQEmulator Copyright (C) 2001-2026 EQEmu Development Team 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; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 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, see . */ #include "client.h" #include "common/events/player_event_logs.h" #include "common/repositories/char_recipe_list_repository.h" #include "common/repositories/criteria/content_filter_criteria.h" #include "common/repositories/tradeskill_recipe_entries_repository.h" #include "common/repositories/tradeskill_recipe_repository.h" #include "common/rulesys.h" #include "zone/queryserv.h" #include "zone/quest_parser_collection.h" #include "zone/string_ids.h" #include "zone/titles.h" #include "zone/worldserver.h" #include "zone/zonedb.h" #include #include 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 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 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 != 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() != 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->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 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 >::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++) { // 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(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(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(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(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(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 ZoneDatabase::GetRecipeComponentItemIDs(RecipeCountType count_type, uint32 recipe_id) { std::vector 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 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); } }