mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-12 17:51:28 +00:00
Changes Speed from float to int. EQ client deals with int step locs better than it does floats according to Haynar's testing. This also contains mob runspeed changes. I recommend you set runspeeds to start in the DB 1.25 for NPCs below 1.25 which will match player runspeeds almost equally. Existing DBs will need to be updated. General Cleanup of MobAI functions. Mobs now change their heading on AIMovement timers if their targets' heading has changed since that time. This prevents players from being able to land backstabs inbetween mob swings. Charmed/feared players now send the appropriate packet, there was a missing CastToClient() in spells that was missing. Mob runspeed can no longer be snared to 0%, instead, 1% of their base runspeed is the maximum. Roots apply as roots instead of a modifier under this code. There is going to be bugs with this code. It's better we push through it than revert it. Sanctuary has been running this for a good week and we've worked through the issues. Misc updates: Exported some variables to perl, including: EVENT_ITE_CLICK_CAST: EVENT_ITEM_CLICK: spell_id - returns the spell_id of the click effect. return value - cancels the cast. EVENT_DROP_ITEM: quantity - returns the # of items dropped in the packet. If the item has charges, charges are returned here instead. itemname - name of the item being dropped itemid - id of the item being droppped spell_id - spell_id associated with the item's click effect. slotid - the inventory slot id of the item being dropped. return value - cancels the item from being dropped. Added Perl function: CalcEXP. Calculates the experience you would gain for an NPC that cons a specific con value to you. Fixed a bug where you would receive the group experience bonus and group experience messages for simply being in a group, regardless of the player being in the same zone as you.
1579 lines
51 KiB
C++
1579 lines
51 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 <stdlib.h>
|
|
#include <list>
|
|
|
|
#ifndef WIN32
|
|
#include <netinet/in.h> //for htonl
|
|
#endif
|
|
|
|
#include "../common/rulesys.h"
|
|
#include "../common/string_util.h"
|
|
|
|
#include "queryserv.h"
|
|
#include "quest_parser_collection.h"
|
|
#include "string_ids.h"
|
|
#include "titles.h"
|
|
#include "zonedb.h"
|
|
|
|
extern QueryServ* QServ;
|
|
|
|
static const SkillUseTypes TradeskillUnknown = Skill1HBlunt; /* an arbitrary non-tradeskill */
|
|
|
|
void Object::HandleAugmentation(Client* user, const AugmentItem_Struct* in_augment, Object *worldo)
|
|
{
|
|
if (!user || !in_augment)
|
|
{
|
|
Log.Out(Logs::General, Logs::Error, "Client or AugmentItem_Struct not set in Object::HandleAugmentation");
|
|
return;
|
|
}
|
|
|
|
ItemInst* 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.
|
|
Inventory& user_inv = user->GetInv();
|
|
ItemInst* inst = nullptr;
|
|
|
|
inst = user_inv.GetItem(in_augment->container_slot);
|
|
if (inst)
|
|
{
|
|
const Item_Struct* item = inst->GetItem();
|
|
if (item && inst->IsType(ItemClassContainer) && item->BagType == 53)
|
|
{
|
|
// 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 itemsFound = 0;
|
|
for (uint8 i = MAIN_BEGIN; i < EmuConstants::MAP_WORLD_SIZE; i++)
|
|
{
|
|
const ItemInst* inst = container->GetItem(i);
|
|
if (inst)
|
|
{
|
|
itemsFound++;
|
|
}
|
|
}
|
|
|
|
if (itemsFound != 2)
|
|
{
|
|
user->Message(13, "Error: Too many/few items in augmentation container.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!container)
|
|
{
|
|
Log.Out(Logs::General, Logs::Error, "Player tried to augment an item without a container set.");
|
|
user->Message(13, "Error: This item is not a container!");
|
|
return;
|
|
}
|
|
|
|
ItemInst *tobe_auged = nullptr, *auged_with = nullptr;
|
|
int8 slot=-1;
|
|
|
|
// Verify 2 items in the augmentation device
|
|
if (container->GetItem(0) && container->GetItem(1))
|
|
{
|
|
// 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(13, "Error: 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(13, "Error: No item in slot 0 of sealer");
|
|
}
|
|
if (!container->GetItem(1))
|
|
{
|
|
user->Message(13, "Error: No item in slot 1 of sealer");
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool deleteItems = false;
|
|
|
|
ItemInst *itemOneToPush = nullptr, *itemTwoToPush = nullptr;
|
|
|
|
// Adding augment
|
|
if (in_augment->augment_slot == -1)
|
|
{
|
|
if (((slot=tobe_auged->AvailableAugmentSlot(auged_with->GetAugmentType()))!=-1) &&
|
|
(tobe_auged->AvailableWearSlot(auged_with->GetItem()->Slots)))
|
|
{
|
|
tobe_auged->PutAugment(slot, *auged_with);
|
|
|
|
ItemInst *aug = tobe_auged->GetAugment(slot);
|
|
if(aug) {
|
|
std::vector<EQEmu::Any> args;
|
|
args.push_back(aug);
|
|
parse->EventItem(EVENT_AUGMENT_ITEM, user, tobe_auged, nullptr, "", slot, &args);
|
|
|
|
args.assign(1, tobe_auged);
|
|
parse->EventItem(EVENT_AUGMENT_INSERT, user, aug, nullptr, "", slot, &args);
|
|
}
|
|
|
|
itemOneToPush = tobe_auged->Clone();
|
|
deleteItems = true;
|
|
}
|
|
else
|
|
{
|
|
user->Message(13, "Error: No available slot for augment");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ItemInst *old_aug = nullptr;
|
|
const uint32 id = auged_with->GetID();
|
|
ItemInst *aug = tobe_auged->GetAugment(in_augment->augment_slot);
|
|
if(aug) {
|
|
std::vector<EQEmu::Any> args;
|
|
args.push_back(aug);
|
|
parse->EventItem(EVENT_UNAUGMENT_ITEM, user, tobe_auged, nullptr, "", slot, &args);
|
|
|
|
args.assign(1, tobe_auged);
|
|
bool destroyed = false;
|
|
if(id == 40408 || id == 40409 || id == 40410) {
|
|
destroyed = true;
|
|
}
|
|
|
|
args.push_back(&destroyed);
|
|
|
|
parse->EventItem(EVENT_AUGMENT_REMOVE, user, aug, nullptr, "", slot, &args);
|
|
}
|
|
|
|
if(id == 40408 || id == 40409 || id == 40410)
|
|
tobe_auged->DeleteAugment(in_augment->augment_slot);
|
|
else
|
|
old_aug = tobe_auged->RemoveAugment(in_augment->augment_slot);
|
|
|
|
itemOneToPush = tobe_auged->Clone();
|
|
if (old_aug)
|
|
itemTwoToPush = old_aug->Clone();
|
|
|
|
|
|
|
|
deleteItems = true;
|
|
}
|
|
|
|
if (deleteItems)
|
|
{
|
|
if (worldo)
|
|
{
|
|
container->Clear();
|
|
EQApplicationPacket* 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 = MAIN_BEGIN; i < EmuConstants::MAP_WORLD_SIZE; i++)
|
|
{
|
|
const ItemInst* inst = container->GetItem(i);
|
|
if (inst)
|
|
{
|
|
user->DeleteItemInInventory(Inventory::CalcSlotId(in_augment->container_slot,i),0,true);
|
|
}
|
|
}
|
|
// Explicitly mark container as cleared.
|
|
container->Clear();
|
|
}
|
|
}
|
|
|
|
// Must push items after the items in inventory are deleted - necessary due to lore items...
|
|
if (itemOneToPush)
|
|
{
|
|
user->PushItemOnCursor(*itemOneToPush, true);
|
|
}
|
|
|
|
if (itemTwoToPush)
|
|
{
|
|
user->PushItemOnCursor(*itemTwoToPush, true);
|
|
}
|
|
|
|
}
|
|
|
|
// Perform tradeskill combine
|
|
void Object::HandleCombine(Client* user, const NewCombine_Struct* in_combine, Object *worldo)
|
|
{
|
|
if (!user || !in_combine) {
|
|
Log.Out(Logs::General, Logs::Error, "Client or NewCombine_Struct not set in Object::HandleCombine");
|
|
return;
|
|
}
|
|
|
|
Inventory& user_inv = user->GetInv();
|
|
PlayerProfile_Struct& user_pp = user->GetPP();
|
|
ItemInst* container = nullptr;
|
|
ItemInst* inst = nullptr;
|
|
uint8 c_type = 0xE8;
|
|
uint32 some_id = 0;
|
|
bool worldcontainer=false;
|
|
|
|
if (in_combine->container_slot == legacy::SLOT_TRADESKILL) {
|
|
if(!worldo) {
|
|
user->Message(13, "Error: Server is not aware of the tradeskill container you are attempting to use");
|
|
return;
|
|
}
|
|
c_type = worldo->m_type;
|
|
inst = worldo->m_inst;
|
|
worldcontainer=true;
|
|
}
|
|
else {
|
|
inst = user_inv.GetItem(in_combine->container_slot);
|
|
if (inst) {
|
|
const Item_Struct* item = inst->GetItem();
|
|
if (item && inst->IsType(ItemClassContainer)) {
|
|
c_type = item->BagType;
|
|
some_id = item->ID;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!inst || !inst->IsType(ItemClassContainer)) {
|
|
user->Message(13, "Error: Server does not recognize specified tradeskill container");
|
|
return;
|
|
}
|
|
|
|
container = inst;
|
|
if (container->GetItem() && container->GetItem()->BagType == BagTypeTransformationmold) {
|
|
const ItemInst* inst = container->GetItem(0);
|
|
bool AllowAll = RuleB(Inventory, AllowAnyWeaponTransformation);
|
|
if (inst && ItemInst::CanTransform(inst->GetItem(), container->GetItem(), AllowAll)) {
|
|
const Item_Struct* new_weapon = inst->GetItem();
|
|
user->DeleteItemInInventory(Inventory::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(), MainCursor, container->GetItem()->Icon, atoi(container->GetItem()->IDFile + 2));
|
|
user->Message_StringID(4, TRANSFORM_COMPLETE, inst->GetItem()->Name);
|
|
if (RuleB(Inventory, DeleteTransformationMold))
|
|
user->DeleteItemInInventory(in_combine->container_slot, 0, true);
|
|
}
|
|
else if (inst) {
|
|
user->Message_StringID(4, TRANSFORM_FAILED, inst->GetItem()->Name);
|
|
}
|
|
EQApplicationPacket* outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
if (container->GetItem() && container->GetItem()->BagType == BagTypeDetransformationmold) {
|
|
const ItemInst* inst = container->GetItem(0);
|
|
if (inst && inst->GetOrnamentationIcon() && inst->GetOrnamentationIcon()) {
|
|
const Item_Struct* new_weapon = inst->GetItem();
|
|
user->DeleteItemInInventory(Inventory::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(), MainCursor, 0, 0);
|
|
user->Message_StringID(4, TRANSFORM_COMPLETE, inst->GetItem()->Name);
|
|
}
|
|
else if (inst) {
|
|
user->Message_StringID(4, DETRANSFORM_FAILED, inst->GetItem()->Name);
|
|
}
|
|
EQApplicationPacket* outapp = new EQApplicationPacket(OP_TradeSkillCombine, 0);
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
DBTradeskillRecipe_Struct spec;
|
|
if (!database.GetTradeRecipe(container, c_type, some_id, user->CharacterID(), &spec)) {
|
|
user->Message_StringID(MT_Emote,TRADESKILL_NOCOMBINE);
|
|
EQApplicationPacket* 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) {
|
|
// Made up message for the client. Just giving a DNC is the other option.
|
|
user->Message(4, "You need to learn how to combine these first.");
|
|
EQApplicationPacket* 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 ) {
|
|
// Notify client.
|
|
user->Message(4, "You are not skilled enough.");
|
|
EQApplicationPacket* 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 == SkillAlchemy) {
|
|
if (user_pp.class_ != SHAMAN) {
|
|
user->Message(13, "This tradeskill can only be performed by a shaman.");
|
|
return;
|
|
}
|
|
else if (user_pp.level < MIN_LEVEL_ALCHEMY) {
|
|
user->Message(13, "You cannot perform alchemy until you reach level %i.", MIN_LEVEL_ALCHEMY);
|
|
return;
|
|
}
|
|
}
|
|
else if (spec.tradeskill == SkillTinkering) {
|
|
if (user_pp.race != GNOME) {
|
|
user->Message(13, "Only gnomes can tinker.");
|
|
return;
|
|
}
|
|
}
|
|
else if (spec.tradeskill == SkillMakePoison) {
|
|
if (user_pp.class_ != ROGUE) {
|
|
user->Message(13, "Only rogues can mix poisons.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Send acknowledgement packets to client
|
|
EQApplicationPacket* 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 = MAIN_BEGIN; i < EmuConstants::MAP_WORLD_SIZE; i++) {
|
|
const ItemInst* inst = container->GetItem(i);
|
|
if (inst) {
|
|
user->DeleteItemInInventory(Inventory::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->Message_StringID(4, 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
|
|
Log.Out(Logs::General, Logs::Error, "Replace container combine executed in a world container.");
|
|
}
|
|
else
|
|
user->DeleteItemInInventory(in_combine->container_slot, 0, true);
|
|
}
|
|
if (success)
|
|
parse->EventPlayer(EVENT_COMBINE_SUCCESS, user, spec.name.c_str(), spec.recipe_id);
|
|
else
|
|
parse->EventPlayer(EVENT_COMBINE_FAILURE, user, spec.name.c_str(), spec.recipe_id);
|
|
}
|
|
|
|
void Object::HandleAutoCombine(Client* user, const RecipeAutoCombine_Struct* rac) {
|
|
|
|
//get our packet ready, gotta send one no matter what...
|
|
EQApplicationPacket* 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 (!database.GetTradeRecipe(rac->recipe_id, rac->object_type, rac->some_id, user->CharacterID(), &spec)) {
|
|
Log.Out(Logs::General, Logs::Error, "Unknown recipe for HandleAutoCombine: %u\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(4, "You need to learn how to combine these first.");
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
//pull the list of components
|
|
std::string query = StringFormat("SELECT tre.item_id, tre.componentcount "
|
|
"FROM tradeskill_recipe_entries AS tre "
|
|
"WHERE tre.componentcount > 0 AND tre.recipe_id = %u",
|
|
rac->recipe_id);
|
|
auto results = database.QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() < 1) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in HandleAutoCombine: no components returned");
|
|
user->QueuePacket(outapp);
|
|
safe_delete(outapp);
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() > 10) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in HandleAutoCombine: too many components returned (%u)", 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
|
|
Inventory& 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)atoi(row[0]);
|
|
uint8 num = (uint8) atoi(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->Message_StringID(MT_Skills, TRADESKILL_MISSING_COMPONENTS);
|
|
|
|
for(std::list<int>::iterator it = MissingItems.begin(); it != MissingItems.end(); ++it)
|
|
{
|
|
const Item_Struct* item = database.GetItem(*it);
|
|
|
|
if(item)
|
|
user->Message_StringID(MT_Skills, TRADESKILL_MISSING_ITEM, item->Name);
|
|
}
|
|
|
|
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 ItemInst* 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->Message_StringID(4, 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)
|
|
parse->EventPlayer(EVENT_COMBINE_SUCCESS, user, spec.name.c_str(), spec.recipe_id);
|
|
else
|
|
parse->EventPlayer(EVENT_COMBINE_FAILURE, user, spec.name.c_str(), spec.recipe_id);
|
|
}
|
|
|
|
SkillUseTypes Object::TypeToSkill(uint32 type)
|
|
{
|
|
switch(type) // grouped and ordered by SkillUseTypes name - new types need to be verified for proper SkillUseTypes and use
|
|
{
|
|
/*SkillAlchemy*/
|
|
case BagTypeMedicineBag: { return SkillAlchemy; }
|
|
|
|
/*SkillBaking*/
|
|
// case BagTypeMixingBowl: // No idea...
|
|
case BagTypeOven: { return SkillBaking; }
|
|
|
|
/*SkillBlacksmithing*/
|
|
case BagTypeForge:
|
|
// case BagTypeKoadaDalForge:
|
|
case BagTypeTeirDalForge:
|
|
case BagTypeOggokForge:
|
|
case BagTypeStormguardForge:
|
|
// case BagTypeAkanonForge:
|
|
// case BagTypeNorthmanForge:
|
|
// case BagTypeCabilisForge:
|
|
// case BagTypeFreeportForge:
|
|
// case BagTypeRoyalQeynosForge:
|
|
// case BagTypeTrollForge:
|
|
case BagTypeFierDalForge:
|
|
case BagTypeValeForge: { return SkillBlacksmithing; } // Delete return if BagTypeGuktaForge enabled
|
|
// case BagTypeErudForge:
|
|
// case BagTypeGuktaForge: { return SkillBlacksmithing; }
|
|
|
|
/*SkillBrewing*/
|
|
// case BagTypeIceCreamChurn: // No idea...
|
|
case BagTypeBrewBarrel: { return SkillBrewing; }
|
|
|
|
/*SkillFishing*/
|
|
case BagTypeTackleBox: { return SkillFishing; }
|
|
|
|
/*SkillFletching*/
|
|
case BagTypeFletchingKit: { return SkillFletching; } // Delete return if BagTypeFierDalFletchingKit enabled
|
|
// case BagTypeFierDalFletchingKit: { return SkillFletching; }
|
|
|
|
/*SkillJewelryMaking*/
|
|
case BagTypeJewelersKit: { return SkillJewelryMaking; }
|
|
|
|
/*SkillMakePoison*/
|
|
// This is a guess and needs to be verified... (Could be SkillAlchemy)
|
|
// case BagTypeMortar: { return SkillMakePoison; }
|
|
|
|
/*SkillPottery*/
|
|
case BagTypePotteryWheel:
|
|
case BagTypeKiln: { return SkillPottery; } // Delete return if BagTypeIksarPotteryWheel enabled
|
|
// case BagTypeIksarPotteryWheel: { return SkillPottery; }
|
|
|
|
/*SkillResearch*/
|
|
// case BagTypeLexicon:
|
|
case BagTypeWizardsLexicon:
|
|
case BagTypeMagesLexicon:
|
|
case BagTypeNecromancersLexicon:
|
|
case BagTypeEnchantersLexicon: { return SkillResearch; } // Delete return if BagTypeConcordanceofResearch enabled
|
|
// case BagTypeConcordanceofResearch: { return SkillResearch; }
|
|
|
|
/*SkillTailoring*/
|
|
case BagTypeSewingKit: { return SkillTailoring; } // Delete return if BagTypeFierDalTailoringKit enabled
|
|
// case BagTypeHalflingTailoringKit:
|
|
// case BagTypeErudTailoringKit:
|
|
// case BagTypeFierDalTailoringKit: { return SkillTailoring; }
|
|
|
|
/*SkillTinkering*/
|
|
case BagTypeToolBox: { return SkillTinkering; }
|
|
|
|
/*Undefined*/
|
|
default: { break; }
|
|
}
|
|
|
|
return TradeskillUnknown;
|
|
}
|
|
|
|
void Client::TradeskillSearchResults(const std::string query, unsigned long objtype, unsigned long someid) {
|
|
|
|
auto results = database.QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() < 1)
|
|
return; //search gave no results... not an error
|
|
|
|
if(results.ColumnCount() != 6) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in TradeskillSearchResults query '%s': Invalid column count in result", query.c_str());
|
|
return;
|
|
}
|
|
|
|
for(auto row = results.begin(); row != results.end(); ++row) {
|
|
if(row == nullptr || row[0] == nullptr || row[1] == nullptr || row[2] == nullptr || row[3] == nullptr || row[5] == nullptr)
|
|
continue;
|
|
|
|
uint32 recipe = (uint32)atoi(row[0]);
|
|
const char *name = row[1];
|
|
uint32 trivial = (uint32) atoi(row[2]);
|
|
uint32 comp_count = (uint32) atoi(row[3]);
|
|
uint32 tradeskill = (uint16) atoi(row[5]);
|
|
|
|
// Skip the recipes that exceed the threshold in skill difference
|
|
// Recipes that have either been made before or were
|
|
// explicitly learned are excempt from that limit
|
|
if (RuleB(Skills, UseLimitTradeskillSearchSkillDiff)
|
|
&& ((int32)trivial - (int32)GetSkill((SkillUseTypes)tradeskill)) > RuleI(Skills, MaxTradeskillSearchSkillDiff)
|
|
&& row[4] == nullptr)
|
|
continue;
|
|
|
|
EQApplicationPacket* 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;
|
|
reply->trivial = trivial;
|
|
strn0cpy(reply->recipe_name, name, sizeof(reply->recipe_name));
|
|
FastQueuePacket(&outapp);
|
|
}
|
|
|
|
}
|
|
|
|
void Client::SendTradeskillDetails(uint32 recipe_id) {
|
|
|
|
//pull the list of components
|
|
std::string query = StringFormat("SELECT tre.item_id,tre.componentcount,i.icon,i.Name "
|
|
"FROM tradeskill_recipe_entries AS tre "
|
|
"LEFT JOIN items AS i ON tre.item_id = i.id "
|
|
"WHERE tre.componentcount > 0 AND tre.recipe_id = %u",
|
|
recipe_id);
|
|
auto results = database.QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() < 1) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in SendTradeskillDetails: no components returned");
|
|
return;
|
|
}
|
|
|
|
if(results.RowCount() > 10) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in SendTradeskillDetails: too many components returned (%u)", results.RowCount());
|
|
return;
|
|
}
|
|
|
|
//biggest this packet can ever be:
|
|
// 64 * 10 + 8 * 10 + 4 + 4 * 10 = 764
|
|
char *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)atoi(row[0]);
|
|
uint8 num = (uint8) atoi(row[1]);
|
|
uint32 icon = (uint32) atoi(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;
|
|
|
|
EQApplicationPacket* 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 == nullptr)
|
|
return(false);
|
|
|
|
uint16 user_skill = GetSkill(spec->tradeskill);
|
|
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 SkillFletching:
|
|
skillup_modifier = RuleI(Character, TradeskillUpFletching);
|
|
break;
|
|
case SkillAlchemy:
|
|
skillup_modifier = RuleI(Character, TradeskillUpAlchemy);
|
|
break;
|
|
case SkillJewelryMaking:
|
|
skillup_modifier = RuleI(Character, TradeskillUpJewelcrafting);
|
|
break;
|
|
case SkillPottery:
|
|
skillup_modifier = RuleI(Character, TradeskillUpPottery);
|
|
break;
|
|
case SkillBaking:
|
|
skillup_modifier = RuleI(Character, TradeskillUpBaking);
|
|
break;
|
|
case SkillBrewing:
|
|
skillup_modifier = RuleI(Character, TradeskillUpBrewing);
|
|
break;
|
|
case SkillBlacksmithing:
|
|
skillup_modifier = RuleI(Character, TradeskillUpBlacksmithing);
|
|
break;
|
|
case SkillResearch:
|
|
skillup_modifier = RuleI(Character, TradeskillUpResearch);
|
|
break;
|
|
case SkillMakePoison:
|
|
skillup_modifier = RuleI(Character, TradeskillUpMakePoison);
|
|
break;
|
|
case SkillTinkering:
|
|
skillup_modifier = RuleI(Character, TradeskillUpTinkering);
|
|
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 == SkillFletching || spec->tradeskill == SkillMakePoison) {
|
|
thirdstat = GetDEX();
|
|
stat_modifier = 0;
|
|
} else if (spec->tradeskill == 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.
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "...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);
|
|
Message_StringID(MT_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;
|
|
}
|
|
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "...Current skill: %d , Trivial: %d , Success chance: %f percent", user_skill , spec->trivial , chance);
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "...Bonusstat: %d , INT: %d , WIS: %d , DEX: %d , STR: %d", bonusstat , GetINT() , GetWIS() , GetDEX() , GetSTR());
|
|
|
|
float res = zone->random.Real(0, 99);
|
|
int aa_chance = 0;
|
|
|
|
//AA modifiers
|
|
//can we do this with nested switches?
|
|
if(spec->tradeskill == SkillAlchemy){
|
|
switch(GetAA(aaAlchemyMastery)){
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(spec->tradeskill == SkillJewelryMaking){
|
|
switch(GetAA(aaJewelCraftMastery)){
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
const Item_Struct* item = nullptr;
|
|
|
|
if (spec->tradeskill == SkillBlacksmithing) {
|
|
switch(GetAA(aaBlacksmithingMastery)) {
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (spec->tradeskill == SkillBaking) {
|
|
switch(GetAA(aaBakingMastery)) {
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (spec->tradeskill == SkillBrewing) {
|
|
switch(GetAA(aaBrewingMastery)) {
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (spec->tradeskill == SkillFletching) {
|
|
switch(GetAA(aaFletchingMastery2)) {
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (spec->tradeskill == SkillPottery) {
|
|
switch(GetAA(aaPotteryMastery)) {
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (spec->tradeskill == SkillTailoring) {
|
|
switch(GetAA(aaTailoringMastery)) {
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (spec->tradeskill == SkillResearch) {
|
|
switch(GetAA(aaArcaneTongues)) {
|
|
case 1:
|
|
aa_chance = 10;
|
|
break;
|
|
case 2:
|
|
aa_chance = 25;
|
|
break;
|
|
case 3:
|
|
aa_chance = 50;
|
|
break;
|
|
}
|
|
}
|
|
|
|
chance = mod_tradeskill_chance(chance, spec);
|
|
|
|
if (((spec->tradeskill==75) || GetGM() || (chance > res)) || zone->random.Roll(aa_chance)) {
|
|
success_modifier = 1;
|
|
|
|
if(over_trivial < 0)
|
|
CheckIncreaseTradeskill(bonusstat, stat_modifier, skillup_modifier, success_modifier, spec->tradeskill);
|
|
|
|
Message_StringID(4, TRADESKILL_SUCCEED, spec->name.c_str());
|
|
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "Tradeskill success");
|
|
|
|
itr = spec->onsuccess.begin();
|
|
while(itr != spec->onsuccess.end() && !spec->quest) {
|
|
//should we check this crap?
|
|
SummonItem(itr->first, itr->second);
|
|
item = database.GetItem(itr->first);
|
|
if (this->GetGroup()) {
|
|
entity_list.MessageGroup(this, true, MT_Skills, "%s has successfully fashioned %s!", GetName(), item->Name);
|
|
}
|
|
|
|
/* QS: Player_Log_Trade_Skill_Events */
|
|
if (RuleB(QueryServ, PlayerLogTradeSkillEvents)){
|
|
std::string event_desc = StringFormat("Success :: fashioned recipe_id:%i tskillid:%i trivial:%i chance:%4.2f in zoneid:%i instid:%i", spec->recipe_id, spec->tradeskill, spec->trivial, chance, this->GetZoneID(), this->GetInstanceID());
|
|
QServ->PlayerLogEvent(Player_Log_Trade_Skill_Events, this->CharacterID(), event_desc);
|
|
}
|
|
|
|
if(RuleB(TaskSystem, EnableTaskSystem))
|
|
UpdateTasksForItem(ActivityTradeSkill, 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);
|
|
|
|
Message_StringID(MT_Emote,TRADESKILL_FAILED);
|
|
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "Tradeskill failed");
|
|
if (this->GetGroup())
|
|
{
|
|
entity_list.MessageGroup(this,true,MT_Skills,"%s was unsuccessful in %s tradeskill attempt.",GetName(),this->GetGender() == 0 ? "his" : this->GetGender() == 1 ? "her" : "its");
|
|
|
|
}
|
|
|
|
/* QS: Player_Log_Trade_Skill_Events */
|
|
if (RuleB(QueryServ, PlayerLogTradeSkillEvents)){
|
|
std::string event_desc = StringFormat("Failed :: recipe_id:%i tskillid:%i trivial:%i chance:%4.2f in zoneid:%i instid:%i", spec->recipe_id, spec->tradeskill, spec->trivial, chance, this->GetZoneID(), this->GetInstanceID());
|
|
QServ->PlayerLogEvent(Player_Log_Trade_Skill_Events, this->CharacterID(), event_desc);
|
|
}
|
|
|
|
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, SkillUseTypes tradeskill)
|
|
{
|
|
uint16 current_raw_skill = GetRawSkill(tradeskill);
|
|
|
|
if(!CanIncreaseTradeskill(tradeskill))
|
|
return; //not allowed to go higher.
|
|
|
|
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);
|
|
|
|
//In stage2 the only thing that matters is your current unmodified skill.
|
|
//If you want to customize here you probbably need to implement your own
|
|
//formula instead of tweaking the below one.
|
|
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 = mod_tradeskill_skillup(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);
|
|
|
|
if(title_manager.IsNewTradeSkillTitleAvailable(tradeskill, current_raw_skill + 1))
|
|
NotifyNewTitlesAvailable();
|
|
}
|
|
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "...skillup_modifier: %f , success_modifier: %d , stat modifier: %d", skillup_modifier , success_modifier , stat_modifier);
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "...Stage1 chance was: %f percent", chance_stage1);
|
|
Log.Out(Logs::Detail, Logs::Tradeskills, "...Stage2 chance was: %f percent. 0 percent means stage1 failed", chance_stage2);
|
|
}
|
|
|
|
bool ZoneDatabase::GetTradeRecipe(const ItemInst* container, uint8 c_type, uint32 some_id,
|
|
uint32 char_id, DBTradeskillRecipe_Struct *spec)
|
|
{
|
|
if (container == nullptr)
|
|
return false;
|
|
|
|
std::string containers;// make where clause segment for container(s)
|
|
if (some_id == 0)
|
|
containers = StringFormat("= %u", c_type); // world combiner so no item number
|
|
else
|
|
containers = StringFormat("IN (%u,%u)", c_type, some_id); // container in inventory
|
|
|
|
//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 i = 0; i < 10; i++) { // <watch> TODO: need to determine if this is bound to world/item container size
|
|
const ItemInst* inst = container->GetItem(i);
|
|
if (!inst)
|
|
continue;
|
|
|
|
const Item_Struct* item = GetItem(inst->GetItem()->ID);
|
|
if (!item)
|
|
continue;
|
|
|
|
if(first) {
|
|
buf2 += StringFormat("%d", item->ID);
|
|
first = false;
|
|
} else
|
|
buf2 += StringFormat(",%d", item->ID);
|
|
|
|
sum += item->ID;
|
|
count++;
|
|
}
|
|
|
|
if(count == 0)
|
|
return false; //no items == no recipe
|
|
|
|
std::string query = StringFormat("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(%s) AND tre.componentcount > 0) "
|
|
"OR ( tre.item_id %s AND tre.iscontainer=1 ))"
|
|
"GROUP BY tre.recipe_id HAVING sum(tre.componentcount) = %u "
|
|
"AND sum(tre.item_id * tre.componentcount) = %u",
|
|
buf2.c_str(), containers.c_str(), count, sum);
|
|
auto results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe search, query: %s", query.c_str());
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe search, error: %s", 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.begin(); row != results.end(); ++row, ++index) {
|
|
uint32 recipeid = (uint32)atoi(row[0]);
|
|
if(first) {
|
|
buf2 += StringFormat("%u", recipeid);
|
|
first = false;
|
|
} else
|
|
buf2 += StringFormat(",%u", recipeid);
|
|
|
|
//length limit on buf2
|
|
if(index == 214) { //Maximum number of recipe matches (19 * 215 = 4096)
|
|
Log.Out(Logs::General, Logs::Error, "GetTradeRecipe warning: Too many matches. Unable to search all recipe entries. Searched %u of %u possible entries.", index + 1, results.RowCount());
|
|
break;
|
|
}
|
|
}
|
|
|
|
query = StringFormat("SELECT tre.recipe_id "
|
|
"FROM tradeskill_recipe_entries AS tre "
|
|
"WHERE tre.recipe_id IN (%s) "
|
|
"GROUP BY tre.recipe_id HAVING sum(tre.componentcount) = %u "
|
|
"AND sum(tre.item_id * tre.componentcount) = %u", buf2.c_str(), count, sum);
|
|
results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe, re-query: %s", query.c_str());
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe, error: %s", 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 containerId = 0;
|
|
|
|
if(some_id) //Standard container
|
|
containerId = some_id;
|
|
else if(c_type)//World container
|
|
containerId = c_type;
|
|
else //Invalid container
|
|
return false;
|
|
|
|
query = StringFormat("SELECT tre.recipe_id "
|
|
"FROM tradeskill_recipe_entries AS tre "
|
|
"WHERE tre.recipe_id IN (%s) "
|
|
"AND tre.item_id = %u;", buf2.c_str(), containerId);
|
|
results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe, re-query: %s", query.c_str());
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe, error: %s", results.ErrorMessage().c_str());
|
|
return false;
|
|
}
|
|
|
|
if(results.RowCount() == 0) { //Recipe contents matched more than 1 recipe, but not in this container
|
|
Log.Out(Logs::General, Logs::Error, "Combine error: Incorrect container is being used!");
|
|
return false;
|
|
}
|
|
|
|
if (results.RowCount() > 1) //Recipe contents matched more than 1 recipe in this container
|
|
Log.Out(Logs::General, Logs::Error, "Combine error: Recipe is not unique! %u matches found for container %u. Continuing with first recipe match.", results.RowCount(), containerId);
|
|
|
|
}
|
|
|
|
auto row = results.begin();
|
|
uint32 recipe_id = (uint32)atoi(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
|
|
bool has_components = true;
|
|
query = StringFormat("SELECT item_id, componentcount "
|
|
"FROM tradeskill_recipe_entries "
|
|
"WHERE recipe_id = %i AND componentcount > 0",
|
|
recipe_id);
|
|
results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
return GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec);
|
|
}
|
|
|
|
if (results.RowCount() == 0)
|
|
return GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec);
|
|
|
|
for (auto row = results.begin(); row != results.end(); ++row) {
|
|
int ccnt = 0;
|
|
|
|
for(int x = MAIN_BEGIN; x < EmuConstants::MAP_WORLD_SIZE; x++) {
|
|
const ItemInst* inst = container->GetItem(x);
|
|
if(!inst)
|
|
continue;
|
|
|
|
const Item_Struct* item = GetItem(inst->GetItem()->ID);
|
|
if (!item)
|
|
continue;
|
|
|
|
if(item->ID == atoi(row[0]))
|
|
ccnt++;
|
|
}
|
|
|
|
if(ccnt != atoi(row[1]))
|
|
return false;
|
|
}
|
|
|
|
return GetTradeRecipe(recipe_id, c_type, some_id, char_id, spec);
|
|
}
|
|
|
|
bool ZoneDatabase::GetTradeRecipe(uint32 recipe_id, uint8 c_type, uint32 some_id,
|
|
uint32 char_id, DBTradeskillRecipe_Struct *spec)
|
|
{
|
|
|
|
// make where clause segment for container(s)
|
|
std::string containers;
|
|
if (some_id == 0)
|
|
containers = StringFormat("= %u", c_type); // world combiner so no item number
|
|
else
|
|
containers = StringFormat("IN (%u,%u)", c_type, some_id); // container in inventory
|
|
|
|
std::string query = StringFormat("SELECT tr.id, tr.tradeskill, tr.skillneeded, "
|
|
"tr.trivial, tr.nofail, tr.replace_container, "
|
|
"tr.name, tr.must_learn, tr.quest, crl.madecount "
|
|
"FROM tradeskill_recipe AS tr "
|
|
"INNER JOIN tradeskill_recipe_entries AS tre "
|
|
"ON tr.id = tre.recipe_id "
|
|
"LEFT JOIN (SELECT recipe_id, madecount "
|
|
"FROM char_recipe_list WHERE char_id = %u) AS crl "
|
|
"ON tr.id = crl.recipe_id "
|
|
"WHERE tr.id = %lu AND tre.item_id %s AND tr.enabled "
|
|
"GROUP BY tr.id",
|
|
char_id, (unsigned long)recipe_id, containers.c_str());
|
|
auto results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe, query: %s", query.c_str());
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecipe, error: %s", results.ErrorMessage().c_str());
|
|
return false;
|
|
}
|
|
|
|
if(results.RowCount() != 1)
|
|
return false;//just not found i guess..
|
|
|
|
auto row = results.begin();
|
|
spec->tradeskill = (SkillUseTypes)atoi(row[1]);
|
|
spec->skill_needed = (int16)atoi(row[2]);
|
|
spec->trivial = (uint16)atoi(row[3]);
|
|
spec->nofail = atoi(row[4]) ? true : false;
|
|
spec->replace_container = atoi(row[5]) ? true : false;
|
|
spec->name = row[6];
|
|
spec->must_learn = (uint8)atoi(row[7]);
|
|
spec->quest = atoi(row[8]) ? true : false;
|
|
|
|
if (row[9] == nullptr) {
|
|
spec->has_learnt = false;
|
|
spec->madecount = 0;
|
|
} else {
|
|
spec->has_learnt = true;
|
|
spec->madecount = (uint32)atoul(row[9]);
|
|
}
|
|
spec->recipe_id = recipe_id;
|
|
|
|
//Pull the on-success items...
|
|
query = StringFormat("SELECT item_id,successcount FROM tradeskill_recipe_entries "
|
|
"WHERE successcount > 0 AND recipe_id = %u", recipe_id);
|
|
results = QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
return false;
|
|
}
|
|
|
|
if(results.RowCount() < 1) {
|
|
Log.Out(Logs::General, Logs::Error, "Error in GetTradeRecept success: no success items returned");
|
|
return false;
|
|
}
|
|
|
|
spec->onsuccess.clear();
|
|
for(auto row = results.begin(); row != results.end(); ++row) {
|
|
uint32 item = (uint32)atoi(row[0]);
|
|
uint8 num = (uint8) atoi(row[1]);
|
|
spec->onsuccess.push_back(std::pair<uint32,uint8>(item, num));
|
|
}
|
|
|
|
spec->onfail.clear();
|
|
//Pull the on-fail items...
|
|
query = StringFormat("SELECT item_id, failcount FROM tradeskill_recipe_entries "
|
|
"WHERE failcount > 0 AND recipe_id = %u", recipe_id);
|
|
results = QueryDatabase(query);
|
|
if (results.Success())
|
|
for(auto row = results.begin(); row != results.end(); ++row) {
|
|
uint32 item = (uint32)atoi(row[0]);
|
|
uint8 num = (uint8) atoi(row[1]);
|
|
spec->onfail.push_back(std::pair<uint32,uint8>(item, num));
|
|
}
|
|
|
|
spec->salvage.clear();
|
|
|
|
// Don't bother with the query if TS is nofail
|
|
if (spec->nofail)
|
|
return true;
|
|
|
|
// Pull the salvage list
|
|
query = StringFormat("SELECT item_id, salvagecount "
|
|
"FROM tradeskill_recipe_entries "
|
|
"WHERE salvagecount > 0 AND recipe_id = %u", recipe_id);
|
|
results = QueryDatabase(query);
|
|
if (results.Success())
|
|
for(auto row = results.begin(); row != results.begin(); ++row) {
|
|
uint32 item = (uint32)atoi(row[0]);
|
|
uint8 num = (uint8)atoi(row[1]);
|
|
spec->salvage.push_back(std::pair<uint32,uint8>(item, num));
|
|
}
|
|
|
|
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 recipeID)
|
|
{
|
|
std::string query = StringFormat("SELECT tr.name, crl.madecount "
|
|
"FROM tradeskill_recipe AS tr "
|
|
"LEFT JOIN (SELECT recipe_id, madecount "
|
|
"FROM char_recipe_list WHERE char_id = %u) AS crl "
|
|
"ON tr.id = crl.recipe_id "
|
|
"WHERE tr.id = %u ;", CharacterID(), recipeID);
|
|
auto results = database.QueryDatabase(query);
|
|
if (!results.Success()) {
|
|
return;
|
|
}
|
|
|
|
if (results.RowCount() != 1) {
|
|
Log.Out(Logs::General, Logs::Normal, "Client::LearnRecipe - RecipeID: %d had %d occurences.", recipeID, results.RowCount());
|
|
return;
|
|
}
|
|
|
|
auto row = results.begin();
|
|
|
|
if (row[0] == nullptr)
|
|
return;
|
|
|
|
// Only give Learn message if character doesn't know the recipe
|
|
if (row[1] != nullptr)
|
|
return;
|
|
|
|
Message_StringID(4, TRADESKILL_LEARN_RECIPE, row[0]);
|
|
// Actually learn the recipe now
|
|
query = StringFormat("INSERT INTO char_recipe_list "
|
|
"SET recipe_id = %u, char_id = %u, madecount = 0 "
|
|
"ON DUPLICATE KEY UPDATE madecount = madecount;",
|
|
recipeID, CharacterID());
|
|
results = database.QueryDatabase(query);
|
|
}
|
|
|
|
bool Client::CanIncreaseTradeskill(SkillUseTypes tradeskill) {
|
|
uint32 rawskill = GetRawSkill(tradeskill);
|
|
uint16 maxskill = MaxSkill(tradeskill);
|
|
|
|
if (rawskill >= maxskill) //Max skill sanity check
|
|
return false;
|
|
|
|
uint8 Baking = (GetRawSkill(SkillBaking) > 200) ? 1 : 0;
|
|
uint8 Smithing = (GetRawSkill(SkillBlacksmithing) > 200) ? 1 : 0;
|
|
uint8 Brewing = (GetRawSkill(SkillBrewing) > 200) ? 1 : 0;
|
|
uint8 Fletching = (GetRawSkill(SkillFletching) > 200) ? 1 : 0;
|
|
uint8 Jewelry = (GetRawSkill(SkillJewelryMaking) > 200) ? 1 : 0;
|
|
uint8 Pottery = (GetRawSkill(SkillPottery) > 200) ? 1 : 0;
|
|
uint8 Tailoring = (GetRawSkill(SkillTailoring) > 200) ? 1 : 0;
|
|
uint8 SkillTotal = Baking + Smithing + Brewing + Fletching + Jewelry + Pottery + Tailoring; //Tradeskills above 200
|
|
uint32 aaLevel = GetAA(aaNewTanaanCraftingMastery); //New Tanaan AA: Each level allows an additional tradeskill above 200 (first one is free)
|
|
|
|
switch (tradeskill) {
|
|
case SkillBaking:
|
|
case SkillBlacksmithing:
|
|
case SkillBrewing:
|
|
case SkillFletching:
|
|
case SkillJewelryMaking:
|
|
case SkillPottery:
|
|
case 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;
|
|
}
|