From 12d7f242b4aa5af980cc29966c6c27a74130c8e2 Mon Sep 17 00:00:00 2001 From: Uleat Date: Wed, 1 Mar 2017 16:11:17 -0500 Subject: [PATCH 01/16] Tweaked bot caster combat range code a little (they shouldn't pile up unless there are los issues...) --- zone/bot.cpp | 54 ++++++++++++++++++++++++++++++++-------------------- zone/bot.h | 2 +- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/zone/bot.cpp b/zone/bot.cpp index eaba1aeea..172ac3d5d 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -2156,7 +2156,7 @@ void Bot::AI_Process() { if(GetHasBeenSummoned()) { if(IsBotCaster() || IsBotArcher()) { if (AI_movement_timer->Check()) { - if(!GetTarget() || (IsBotCaster() && !IsBotCasterCombatRange(GetTarget())) || (IsBotArcher() && IsArcheryRange(GetTarget())) || (DistanceSquaredNoZ(static_cast(m_Position), m_PreSummonLocation) < 10)) { + if(!GetTarget() || (IsBotCaster() && !IsBotCasterAtCombatRange(GetTarget())) || (IsBotArcher() && IsArcheryRange(GetTarget())) || (DistanceSquaredNoZ(static_cast(m_Position), m_PreSummonLocation) < 10)) { if(GetTarget()) FaceTarget(GetTarget()); @@ -2293,23 +2293,24 @@ void Bot::AI_Process() { ChangeBotArcherWeapons(IsBotArcher()); } - if(IsBotArcher() && atArcheryRange) { - if(IsMoving()) { + if (IsBotArcher() && atArcheryRange) { + if (IsMoving()) { SetHeading(CalculateHeadingToTarget(GetTarget()->GetX(), GetTarget()->GetY())); SetRunAnimSpeed(0); SetCurrentSpeed(0); - if(moved) { + if (moved) { moved = false; SetCurrentSpeed(0); } } atCombatRange = true; - } else if(IsBotCaster() && GetLevel() >= RuleI(Bots, CasterStopMeleeLevel)) { - if(IsBotCasterCombatRange(GetTarget())) - atCombatRange = true; } - else if(DistanceSquared(m_Position, GetTarget()->GetPosition()) <= meleeDistance) + else if (GetLevel() >= RuleI(Bots, CasterStopMeleeLevel) && IsBotCasterAtCombatRange(GetTarget())) { atCombatRange = true; + } + else if (DistanceSquared(m_Position, GetTarget()->GetPosition()) <= meleeDistance) { + atCombatRange = true; + } if(atCombatRange) { if(IsMoving()) { @@ -6856,20 +6857,31 @@ bool Bot::IsArcheryRange(Mob *target) { return result; } -bool Bot::IsBotCasterCombatRange(Mob *target) { - bool result = false; - if(target) { - float range = BotAISpellRange; - range *= range; - range *= .5; - float targetDistance = DistanceSquaredNoZ(m_Position, target->GetPosition()); - if(targetDistance > range) - result = false; - else - result = true; - } +bool Bot::IsBotCasterAtCombatRange(Mob *target) +{ + static const float local[PLAYER_CLASS_COUNT] = { + 0.0f, // WARRIOR + 1156.0f, // CLERIC as DSq value (34 units) + 0.0f, 0.0f, 0.0f, // PALADIN, RANGER, SHADOWKNIGHT + 1764.0f, // DRUID as DSq value (42 units) + 0.0f, 0.0f, 0.0f, // MONK, BARD, ROGUE + 1444.0f, // SHAMAN as DSq value (38 units) + 2916.0f, // NECROMANCER as DSq value (54 units) + 2304.0f, // WIZARD as DSq value (48 units) + 2704.0f, // MAGICIAN as DSq value (52 units) + 2500.0f, // ENCHANTER as DSq value (50 units) + 0.0f, 0.0f // BEASTLORD, BERSERKER + }; - return result; + if (!target) + return false; + if (GetClass() < WARRIOR || GetClass() > BERSERKER) + return false; + + float targetDistance = DistanceSquaredNoZ(m_Position, target->GetPosition()); + if (targetDistance < local[GetClass() - 1]) + return true; + return false; } void Bot::UpdateGroupCastingRoles(const Group* group, bool disband) diff --git a/zone/bot.h b/zone/bot.h index 7bfbf87d0..88ff654df 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -337,7 +337,7 @@ public: bool IsStanding(); int GetBotWalkspeed() const { return (int)((float)_GetWalkSpeed() * 1.786f); } // 1.25 / 0.7 = 1.7857142857142857142857142857143 int GetBotRunspeed() const { return (int)((float)_GetRunSpeed() * 1.786f); } - bool IsBotCasterCombatRange(Mob *target); + bool IsBotCasterAtCombatRange(Mob *target); bool UseDiscipline(uint32 spell_id, uint32 target); uint8 GetNumberNeedingHealedInGroup(uint8 hpr, bool includePets); bool GetNeedsCured(Mob *tar); From 5213e4c7d41b2973604787c35f95c85cc495bd3e Mon Sep 17 00:00:00 2001 From: Akkadius Date: Wed, 1 Mar 2017 15:19:56 -0600 Subject: [PATCH 02/16] Remove table that doesn't exist anymore (PEQ Dumps) [skip ci] --- utils/sql/user_tables.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/sql/user_tables.txt b/utils/sql/user_tables.txt index a2fa028e7..9e35ac5e2 100644 --- a/utils/sql/user_tables.txt +++ b/utils/sql/user_tables.txt @@ -30,7 +30,6 @@ character_inspect_messages character_leadership_abilities character_activities character_alt_currency -character_backup character_buffs character_enabledtasks character_pet_buffs From 15af28720abda0316c7c3723d3bb464e0ef2b4d4 Mon Sep 17 00:00:00 2001 From: Uleat Date: Wed, 1 Mar 2017 19:16:02 -0500 Subject: [PATCH 03/16] Bad logic..not used anyways --- zone/bot.cpp | 15 --------------- zone/bot.h | 10 ++++------ 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/zone/bot.cpp b/zone/bot.cpp index 172ac3d5d..0fc7418e0 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -7621,21 +7621,6 @@ bool Bot::CheckLoreConflict(const EQEmu::ItemData* item) { return (m_inv.HasItemByLoreGroup(item->LoreGroup, invWhereWorn) != INVALID_INDEX); } -bool Bot::GroupHasClass(Group* group, uint8 classId) { - bool result = false; - - if(group) { - for(int counter = 0; counter < MAX_GROUP_MEMBERS; counter++) { - if(group->members[counter] && group->members[counter]->GetClass() & classId) { - result = true; - break; - } - } - } - - return result; -} - bool EntityList::Bot_AICheckCloseBeneficialSpells(Bot* caster, uint8 iChance, float iRange, uint32 iSpellTypes) { if((iSpellTypes&SpellTypes_Detrimental) != 0) { Log.Out(Logs::General, Logs::Error, "Error: detrimental spells requested from AICheckCloseBeneficialSpells!!"); diff --git a/zone/bot.h b/zone/bot.h index 88ff654df..dea5dd8e8 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -461,10 +461,12 @@ public: static int32 GetDisciplineRecastTimer(Bot *caster, int timer_index); static bool CheckDisciplineRecastTimers(Bot *caster, int timer_index); static uint32 GetDisciplineRemainingTime(Bot *caster, int timer_index); + static std::list GetBotSpellsForSpellEffect(Bot* botCaster, int spellEffect); static std::list GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, int spellEffect, SpellTargetType targetType); static std::list GetBotSpellsBySpellType(Bot* botCaster, uint32 spellType); static std::list GetPrioritizedBotSpellsBySpellType(Bot* botCaster, uint32 spellType); + static BotSpell GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType); static BotSpell GetBestBotSpellForFastHeal(Bot* botCaster); static BotSpell GetBestBotSpellForHealOverTime(Bot* botCaster); @@ -476,6 +478,7 @@ public: static BotSpell GetBestBotSpellForGroupHeal(Bot* botCaster); static BotSpell GetBestBotSpellForMagicBasedSlow(Bot* botCaster); static BotSpell GetBestBotSpellForDiseaseBasedSlow(Bot* botCaster); + static Mob* GetFirstIncomingMobToMez(Bot* botCaster, BotSpell botSpell); static BotSpell GetBestBotSpellForMez(Bot* botCaster); static BotSpell GetBestBotMagicianPetSpell(Bot* botCaster); @@ -486,17 +489,12 @@ public: static BotSpell GetDebuffBotSpell(Bot* botCaster, Mob* target); static BotSpell GetBestBotSpellForCure(Bot* botCaster, Mob* target); static BotSpell GetBestBotSpellForResistDebuff(Bot* botCaster, Mob* target); + static NPCType CreateDefaultNPCTypeStructForBot(std::string botName, std::string botLastName, uint8 botLevel, uint16 botRace, uint8 botClass, uint8 gender); // Static Bot Group Methods static bool AddBotToGroup(Bot* bot, Group* group); static bool RemoveBotFromGroup(Bot* bot, Group* group); - static bool GroupHasClass(Group* group, uint8 classId); - static bool GroupHasClericClass(Group* group) { return GroupHasClass(group, CLERIC); } - static bool GroupHasDruidClass(Group* group) { return GroupHasClass(group, DRUID); } - static bool GroupHasShamanClass(Group* group) { return GroupHasClass(group, SHAMAN); } - static bool GroupHasEnchanterClass(Group* group) { return GroupHasClass(group, ENCHANTER); } - static bool GroupHasPriestClass(Group* group) { return GroupHasClass(group, CLERIC | DRUID | SHAMAN); } static void BotGroupSay(Mob *speaker, const char *msg, ...); // "GET" Class Methods From 7d13475bac68b873e36133f3190d21df10686f02 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Thu, 2 Mar 2017 14:31:48 -0500 Subject: [PATCH 04/16] Fix ClearAggro xtarget issue --- zone/entity.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zone/entity.cpp b/zone/entity.cpp index 9f23f63a4..2bc1b99ed 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2980,10 +2980,16 @@ void EntityList::Evade(Mob *who) //removes "targ" from all hate lists, including feigned, in the zone void EntityList::ClearAggro(Mob* targ) { + Client *c = nullptr; + if (targ->IsClient()) + c = targ->CastToClient(); auto it = npc_list.begin(); while (it != npc_list.end()) { - if (it->second->CheckAggro(targ)) + if (it->second->CheckAggro(targ)) { + if (c) + c->RemoveXTarget(it->second, false); it->second->RemoveFromHateList(targ); + } it->second->RemoveFromFeignMemory(targ->CastToClient()); //just in case we feigned ++it; } From 2690d8fed8a054b801e530f698c4262795900f78 Mon Sep 17 00:00:00 2001 From: Uleat Date: Fri, 3 Mar 2017 17:51:02 -0500 Subject: [PATCH 05/16] Added inspect buff cases for bots (ZombieSoul) --- zone/client_packet.cpp | 22 ++++++++++++++++++---- zone/spell_effects.cpp | 8 ++++++-- zone/spells.cpp | 8 ++++++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 5411dbf7a..d7d8bdd0e 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -3895,10 +3895,19 @@ void Client::Handle_OP_BuffRemoveRequest(const EQApplicationPacket *app) Mob *m = nullptr; - if (brrs->EntityID == GetID()) + if (brrs->EntityID == GetID()) { m = this; - else if (brrs->EntityID == GetPetID()) + } + else if (brrs->EntityID == GetPetID()) { m = GetPet(); + } +#ifdef BOTS + else { + Mob* bot_test = entity_list.GetMob(brrs->EntityID); + if (bot_test && bot_test->IsBot() && bot_test->GetOwner() == this) + m = bot_test; + } +#endif if (!m) return; @@ -13042,9 +13051,14 @@ void Client::Handle_OP_TargetCommand(const EQApplicationPacket *app) } } if (GetGM() || RuleB(Spells, AlwaysSendTargetsBuffs) || nt == this || inspect_buffs || (nt->IsClient() && !nt->CastToClient()->GetPVP()) || - (nt->IsPet() && nt->GetOwner() && nt->GetOwner()->IsClient() && !nt->GetOwner()->CastToClient()->GetPVP()) || - (nt->IsMerc() && nt->GetOwner() && nt->GetOwner()->IsClient() && !nt->GetOwner()->CastToClient()->GetPVP())) + (nt->IsPet() && nt->GetOwner() && nt->GetOwner()->IsClient() && !nt->GetOwner()->CastToClient()->GetPVP()) || +#ifdef BOTS + (nt->IsBot() && nt->GetOwner() && nt->GetOwner()->IsClient() && !nt->GetOwner()->CastToClient()->GetPVP()) || // TODO: bot pets +#endif + (nt->IsMerc() && nt->GetOwner() && nt->GetOwner()->IsClient() && !nt->GetOwner()->CastToClient()->GetPVP())) + { nt->SendBuffsToClient(this); + } } else { diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index a2841ee8e..132e6bcdb 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -4155,8 +4155,12 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) if(IsPet() && GetOwner() && GetOwner()->IsClient()) { SendPetBuffsToClient(); } - if((IsClient() && !CastToClient()->GetPVP()) || (IsPet() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP()) || - (IsMerc() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP())) + if((IsClient() && !CastToClient()->GetPVP()) || + (IsPet() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP()) || +#ifdef BOTS + (IsBot() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP()) || +#endif + (IsMerc() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP())) { EQApplicationPacket *outapp = MakeBuffsPacket(); diff --git a/zone/spells.cpp b/zone/spells.cpp index f37f2fc53..0961b3404 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -3290,8 +3290,12 @@ int Mob::AddBuff(Mob *caster, uint16 spell_id, int duration, int32 level_overrid if (IsPet() && GetOwner() && GetOwner()->IsClient()) SendPetBuffsToClient(); - if((IsClient() && !CastToClient()->GetPVP()) || (IsPet() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP()) || - (IsMerc() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP())) + if((IsClient() && !CastToClient()->GetPVP()) || + (IsPet() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP()) || +#ifdef BOTS + (IsBot() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP()) || +#endif + (IsMerc() && GetOwner() && GetOwner()->IsClient() && !GetOwner()->CastToClient()->GetPVP())) { EQApplicationPacket *outapp = MakeBuffsPacket(); From d559e9da1045c9fe69217a94361167862a229dcb Mon Sep 17 00:00:00 2001 From: Uleat Date: Sat, 4 Mar 2017 14:50:32 -0500 Subject: [PATCH 06/16] Fix for bot auto-combat damage (please post any abnormalities) --- zone/attack.cpp | 7 +++++-- zone/bot.cpp | 29 +++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/zone/attack.cpp b/zone/attack.cpp index dc50f23db..c89f217bd 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -1301,9 +1301,12 @@ bool Client::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, b my_hit.min_damage = 0; uint8 mylevel = GetLevel() ? GetLevel() : 1; uint32 hate = 0; - if (weapon) hate = weapon->GetItem()->Damage + weapon->GetItem()->ElemDmgAmt; + if (weapon) + hate = (weapon->GetItem()->Damage + weapon->GetItem()->ElemDmgAmt); + my_hit.base_damage = GetWeaponDamage(other, weapon, &hate); - if (hate == 0 && my_hit.base_damage > 1) hate = my_hit.base_damage; + if (hate == 0 && my_hit.base_damage > 1) + hate = my_hit.base_damage; //if weapon damage > 0 then we know we can hit the target with this weapon //otherwise we cannot and we set the damage to -5 later on diff --git a/zone/bot.cpp b/zone/bot.cpp index 0fc7418e0..06a25af78 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -3693,11 +3693,22 @@ bool Bot::Attack(Mob* other, int Hand, bool FromRiposte, bool IsStrikethrough, b return false; } - if(!GetTarget() || GetTarget() != other) - SetTarget(other); + if ((GetHP() <= 0) || (GetAppearance() == eaDead)) { + SetTarget(nullptr); + Log.Out(Logs::Detail, Logs::Combat, "Attempted to attack %s while unconscious or, otherwise, appearing dead", other->GetCleanName()); + return false; + } - Log.Out(Logs::Detail, Logs::Combat, "Attacking %s with hand %d %s", other?other->GetCleanName():"(nullptr)", Hand, FromRiposte?"(this is a riposte)":""); - if ((IsCasting() && (GetClass() != BARD) && !IsFromSpell) || other == nullptr || (GetHP() < 0) || (GetAppearance() == eaDead) || (!IsAttackAllowed(other))) { + //if(!GetTarget() || GetTarget() != other) // NPC::Attack() doesn't do this + // SetTarget(other); + + // apparently, we always want our target to be 'other'..why not just set it? + SetTarget(other); + // takes more to compare a call result, load for a call, load a compare to address and compare, and finally + // push a value to an address than to just load for a call and push a value to an address. + + Log.Out(Logs::Detail, Logs::Combat, "Attacking %s with hand %d %s", other->GetCleanName(), Hand, (FromRiposte ? "(this is a riposte)" : "")); + if ((IsCasting() && (GetClass() != BARD) && !IsFromSpell) || (!IsAttackAllowed(other))) { if(this->GetOwnerID()) entity_list.MessageClose(this, 1, 200, 10, "%s says, '%s is not a legal target master.'", this->GetCleanName(), this->GetTarget()->GetCleanName()); @@ -3740,8 +3751,10 @@ bool Bot::Attack(Mob* other, int Hand, bool FromRiposte, bool IsStrikethrough, b DamageHitInfo my_hit; AttackAnimation(my_hit.skill, Hand, weapon); Log.Out(Logs::Detail, Logs::Combat, "Attacking with %s in slot %d using skill %d", weapon?weapon->GetItem()->Name:"Fist", Hand, my_hit.skill); - /// Now figure out damage - my_hit.damage_done =0; + + // Now figure out damage + my_hit.damage_done = 0; + my_hit.min_damage = 0; uint8 mylevel = GetLevel() ? GetLevel() : 1; uint32 hate = 0; if (weapon) @@ -3787,6 +3800,7 @@ bool Bot::Attack(Mob* other, int Hand, bool FromRiposte, bool IsStrikethrough, b Log.Out(Logs::Detail, Logs::Combat, "Damage calculated: base %d min damage %d skill %d", my_hit.base_damage, my_hit.min_damage, my_hit.skill); + int hit_chance_bonus = 0; my_hit.offense = offense(my_hit.skill); my_hit.hand = Hand; @@ -3795,8 +3809,11 @@ bool Bot::Attack(Mob* other, int Hand, bool FromRiposte, bool IsStrikethrough, b my_hit.base_damage += opts->damage_flat; hate *= opts->hate_percent; hate += opts->hate_flat; + hit_chance_bonus += opts->hit_chance; } + my_hit.tohit = GetTotalToHit(my_hit.skill, hit_chance_bonus); + DoAttack(other, my_hit, opts); Log.Out(Logs::Detail, Logs::Combat, "Final damage after all reductions: %d", my_hit.damage_done); From becd7b5c2492bc8d9667fb9e665fa9a36abb9cba Mon Sep 17 00:00:00 2001 From: Uleat Date: Sun, 5 Mar 2017 05:12:54 -0500 Subject: [PATCH 07/16] This probably resolves a long-term bug with bots who are conscientious objectors to fighting... (Had a rash of reports concerning this -- obscure attack timer bug within inherited NPC class ctor) --- zone/bot.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/bot.cpp b/zone/bot.cpp index 06a25af78..6a3e774b0 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -5444,7 +5444,7 @@ void Bot::SetAttackTimer() { } speed = (RuleB(Spells, Jun182014HundredHandsRevamp) ? static_cast(((delay / haste_mod) + ((hhe / 1000.0f) * (delay / haste_mod))) * 100) : static_cast(((delay / haste_mod) + ((hhe / 100.0f) * delay)) * 100)); - TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true); + TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true, true); if (i == EQEmu::inventory::slotPrimary) PrimaryWeapon = ItemToUse; From f26b7a4adc83ff6bd390948ac0593b05a54cefec Mon Sep 17 00:00:00 2001 From: Drajor Date: Wed, 8 Mar 2017 06:22:17 +1000 Subject: [PATCH 08/16] Hacky fix for quantity wrapping when stacked items are sold that have a quantity greater than 255. A better solution will need to implemented long term --- zone/client_packet.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index d7d8bdd0e..d4404247e 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -12567,8 +12567,25 @@ void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app) // Now remove the item from the player, this happens regardless of outcome if (!inst->IsStackable()) this->DeleteItemInInventory(mp->itemslot, 0, false); - else - this->DeleteItemInInventory(mp->itemslot, mp->quantity, false); + else { + // HACK: DeleteItemInInventory uses int8 for quantity type. There is no consistent use of types in code in this path so for now iteratively delete from inventory. + if (mp->quantity > 255) { + uint32 temp = mp->quantity; + while (temp > 255 && temp != 0) { + // Delete chunks of 255 + this->DeleteItemInInventory(mp->itemslot, 255, false); + temp -= 255; + } + if (temp != 0) { + // Delete remaining + this->DeleteItemInInventory(mp->itemslot, temp, false); + } + } + else { + this->DeleteItemInInventory(mp->itemslot, mp->quantity, false); + } + } + //This forces the price to show up correctly for charged items. if (inst->IsCharged()) From 09bbfbcc314157e3897a44c9b303bf37d43d7e05 Mon Sep 17 00:00:00 2001 From: Uleat Date: Wed, 8 Mar 2017 08:12:04 -0500 Subject: [PATCH 09/16] Complete rework of the bot trading system (see changelog.txt) --- changelog.txt | 15 + common/inventory_profile.cpp | 31 +- common/inventory_profile.h | 2 +- zone/bot.cpp | 614 ++++++++++++++++++++++------------- zone/bot.h | 2 +- 5 files changed, 416 insertions(+), 248 deletions(-) diff --git a/changelog.txt b/changelog.txt index 208cb0585..76fc0740e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,20 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 03/08/2017 == +Uleat: Complete rework of the bot trading system + - Equipment slot priority can now be tailored..though, a recompile will be required + - All item validations and slot assignments for trades and returns are now performed before any actual item movements occur + - Failed trade/returned items will now go straight into the client's inventory, just like a normal trade transaction + - A 'green' message appears at the end of each successful trade informing the trader of 'accepted' and 'returned' item counts + - Bots respond to the trader directly now instead of using BotGroupSay() + - Bots will still only allow trades from their owner (currently, too high a risk of exploit and/or malicious activity) + - Partial stack movements (i.e., ammo refills) have been scoped..but, not implemented + - I have not been able to reproduce any 'illegal' weapon combinations with this code + - NOTE: The report of item duplication with bot return items appears to be an inventory desync condition + - I experienced this condition both before and after the rework with RoF+ clients (UF- appears ok) + - The bug lies within the actual client inventory system and not with bot trades + - Please post any issues with this change as they arise + == 02/27/2017 == Uleat: Notes on bot movement speed changes: - Clients (players) appear to be on a different speed scale than other entities (NPCs, etc...) diff --git a/common/inventory_profile.cpp b/common/inventory_profile.cpp index ae17a2a17..7c97be706 100644 --- a/common/inventory_profile.cpp +++ b/common/inventory_profile.cpp @@ -635,18 +635,23 @@ int16 EQEmu::InventoryProfile::FindFreeSlot(bool for_bag, bool try_cursor, uint8 } // This is a mix of HasSpaceForItem and FindFreeSlot..due to existing coding behavior, it was better to add a new helper function... -int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst) { +int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst, int16 general_start, int16 bag_start) { // Do not arbitrarily use this function..it is designed for use with Client::ResetTrade() and Client::FinishTrade(). - // If you have a need, use it..but, understand it is not a compatible replacement for InventoryProfile::FindFreeSlot(). + // If you have a need, use it..but, understand it is not a suitable replacement for InventoryProfile::FindFreeSlot(). // // I'll probably implement a bitmask in the new inventory system to avoid having to adjust stack bias + if ((general_start < legacy::GENERAL_BEGIN) || (general_start > legacy::GENERAL_END)) + return INVALID_INDEX; + if ((bag_start < inventory::containerBegin) || (bag_start >= inventory::ContainerCount)) + return INVALID_INDEX; + if (!inst || !inst->GetID()) return INVALID_INDEX; // step 1: find room for bags (caller should really ask for slots for bags first to avoid sending them to cursor..and bag item loss) if (inst->IsClassBag()) { - for (int16 free_slot = legacy::GENERAL_BEGIN; free_slot <= legacy::GENERAL_END; ++free_slot) { + for (int16 free_slot = general_start; free_slot <= legacy::GENERAL_END; ++free_slot) { if (!m_inv[free_slot]) return free_slot; } @@ -656,7 +661,7 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst // step 2: find partial room for stackables if (inst->IsStackable()) { - for (int16 free_slot = legacy::GENERAL_BEGIN; free_slot <= legacy::GENERAL_END; ++free_slot) { + for (int16 free_slot = general_start; free_slot <= legacy::GENERAL_END; ++free_slot) { const ItemInstance* main_inst = m_inv[free_slot]; if (!main_inst) @@ -666,14 +671,14 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst return free_slot; } - for (int16 free_slot = legacy::GENERAL_BEGIN; free_slot <= legacy::GENERAL_END; ++free_slot) { + for (int16 free_slot = general_start; free_slot <= legacy::GENERAL_END; ++free_slot) { const ItemInstance* main_inst = m_inv[free_slot]; if (!main_inst) continue; if (main_inst->IsClassBag()) { // if item-specific containers already have bad items, we won't fix it here... - for (uint8 free_bag_slot = inventory::containerBegin; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { const ItemInstance* sub_inst = main_inst->GetItem(free_bag_slot); if (!sub_inst) @@ -688,13 +693,13 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst // step 3a: find room for container-specific items (ItemClassArrow) if (inst->GetItem()->ItemType == item::ItemTypeArrow) { - for (int16 free_slot = legacy::GENERAL_BEGIN; free_slot <= legacy::GENERAL_END; ++free_slot) { + for (int16 free_slot = general_start; free_slot <= legacy::GENERAL_END; ++free_slot) { const ItemInstance* main_inst = m_inv[free_slot]; if (!main_inst || (main_inst->GetItem()->BagType != item::BagTypeQuiver) || !main_inst->IsClassBag()) continue; - for (uint8 free_bag_slot = inventory::containerBegin; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { if (!main_inst->GetItem(free_bag_slot)) return InventoryProfile::CalcSlotId(free_slot, free_bag_slot); } @@ -703,13 +708,13 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst // step 3b: find room for container-specific items (ItemClassSmallThrowing) if (inst->GetItem()->ItemType == item::ItemTypeSmallThrowing) { - for (int16 free_slot = legacy::GENERAL_BEGIN; free_slot <= legacy::GENERAL_END; ++free_slot) { + for (int16 free_slot = general_start; free_slot <= legacy::GENERAL_END; ++free_slot) { const ItemInstance* main_inst = m_inv[free_slot]; if (!main_inst || (main_inst->GetItem()->BagType != item::BagTypeBandolier) || !main_inst->IsClassBag()) continue; - for (uint8 free_bag_slot = inventory::containerBegin; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { if (!main_inst->GetItem(free_bag_slot)) return InventoryProfile::CalcSlotId(free_slot, free_bag_slot); } @@ -717,21 +722,21 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst } // step 4: just find an empty slot - for (int16 free_slot = legacy::GENERAL_BEGIN; free_slot <= legacy::GENERAL_END; ++free_slot) { + for (int16 free_slot = general_start; free_slot <= legacy::GENERAL_END; ++free_slot) { const ItemInstance* main_inst = m_inv[free_slot]; if (!main_inst) return free_slot; } - for (int16 free_slot = legacy::GENERAL_BEGIN; free_slot <= legacy::GENERAL_END; ++free_slot) { + for (int16 free_slot = general_start; free_slot <= legacy::GENERAL_END; ++free_slot) { const ItemInstance* main_inst = m_inv[free_slot]; if (main_inst && main_inst->IsClassBag()) { if ((main_inst->GetItem()->BagSize < inst->GetItem()->Size) || (main_inst->GetItem()->BagType == item::BagTypeBandolier) || (main_inst->GetItem()->BagType == item::BagTypeQuiver)) continue; - for (uint8 free_bag_slot = inventory::containerBegin; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { if (!main_inst->GetItem(free_bag_slot)) return InventoryProfile::CalcSlotId(free_slot, free_bag_slot); } diff --git a/common/inventory_profile.h b/common/inventory_profile.h index b9ae1db8c..7bb659cff 100644 --- a/common/inventory_profile.h +++ b/common/inventory_profile.h @@ -155,7 +155,7 @@ namespace EQEmu // Locate an available inventory slot int16 FindFreeSlot(bool for_bag, bool try_cursor, uint8 min_size = 0, bool is_arrow = false); - int16 FindFreeSlotForTradeItem(const ItemInstance* inst); + int16 FindFreeSlotForTradeItem(const ItemInstance* inst, int16 general_start = legacy::GENERAL_BEGIN, int16 bag_start = inventory::containerBegin); // Calculate slot_id for an item within a bag static int16 CalcSlotId(int16 slot_id); // Calc parent bag's slot_id diff --git a/zone/bot.cpp b/zone/bot.cpp index 6a3e774b0..32796b88b 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -3225,21 +3225,20 @@ void Bot::BotAddEquipItem(int slot, uint32 id) { } // Erases the specified item from bot the NPC equipment array and from the bot inventory collection. -void Bot::BotRemoveEquipItem(int slot) { - if(slot > 0) { - uint8 materialFromSlot = EQEmu::InventoryProfile::CalcMaterialFromSlot(slot); +void Bot::BotRemoveEquipItem(int16 slot) +{ + uint8 material_slot = EQEmu::InventoryProfile::CalcMaterialFromSlot(slot); - if (materialFromSlot != EQEmu::textures::materialInvalid) { - equipment[slot] = 0; // npc has more than just material slots. Valid material should mean valid inventory index - SendWearChange(materialFromSlot); - if (materialFromSlot == EQEmu::textures::armorChest) - SendWearChange(EQEmu::textures::armorArms); - } - - UpdateEquipmentLight(); - if (UpdateActiveLight()) - SendAppearancePacket(AT_Light, GetActiveLightType()); + if (material_slot != EQEmu::textures::materialInvalid) { + equipment[slot] = 0; // npc has more than just material slots. Valid material should mean valid inventory index + SendWearChange(material_slot); + if (material_slot == EQEmu::textures::armorChest) + SendWearChange(EQEmu::textures::armorArms); } + + UpdateEquipmentLight(); + if (UpdateActiveLight()) + SendAppearancePacket(AT_Light, GetActiveLightType()); } void Bot::BotTradeSwapItem(Client* client, int16 lootSlot, const EQEmu::ItemInstance* inst, const EQEmu::ItemInstance* inst_swap, uint32 equipableSlots, std::string* errorMessage, bool swap) { @@ -3326,243 +3325,392 @@ bool Bot::AddBotToGroup(Bot* bot, Group* group) { } // Completes a trade with a client bot owner -void Bot::FinishTrade(Client* client, BotTradeType tradeType) { - if(client && !client->GetTradeskillObject() && (client->trade->state != Trading)) { - if(tradeType == BotTradeClientNormal) { - // Items being traded are found in the normal trade window used to trade between a Client and a Client or NPC - // Items in this mode are found in slot ids 3000 thru 3003 - thought bots used the full 8-slot window..? - PerformTradeWithClient(EQEmu::legacy::TRADE_BEGIN, EQEmu::legacy::TRADE_END, client); // {3000..3007} - } - else if(tradeType == BotTradeClientNoDropNoTrade) { - // Items being traded are found on the Client's cursor slot, slot id 30. This item can be either a single item or it can be a bag. - // If it is a bag, then we have to search for items in slots 331 thru 340 - PerformTradeWithClient(EQEmu::inventory::slotCursor, EQEmu::inventory::slotCursor, client); +void Bot::FinishTrade(Client* client, BotTradeType tradeType) +{ + if (!client || (GetOwner() != client) || client->GetTradeskillObject() || client->trade->state == Trading) { + if (client) + client->ResetTrade(); + return; + } - // TODO: Add logic here to test if the item in SLOT_CURSOR is a container type, if it is then we need to call the following: - // PerformTradeWithClient(331, 340, client); - } + // these notes are not correct or obselete + if (tradeType == BotTradeClientNormal) { + // Items being traded are found in the normal trade window used to trade between a Client and a Client or NPC + // Items in this mode are found in slot ids 3000 thru 3003 - thought bots used the full 8-slot window..? + PerformTradeWithClient(EQEmu::legacy::TRADE_BEGIN, EQEmu::legacy::TRADE_END, client); // {3000..3007} + } + else if (tradeType == BotTradeClientNoDropNoTrade) { + // Items being traded are found on the Client's cursor slot, slot id 30. This item can be either a single item or it can be a bag. + // If it is a bag, then we have to search for items in slots 331 thru 340 + PerformTradeWithClient(EQEmu::inventory::slotCursor, EQEmu::inventory::slotCursor, client); + + // TODO: Add logic here to test if the item in SLOT_CURSOR is a container type, if it is then we need to call the following: + // PerformTradeWithClient(331, 340, client); } } // Perfoms the actual trade action with a client bot owner -void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* client) { - if(client) { - // TODO: Figure out what the actual max slot id is - const int MAX_SLOT_ID = EQEmu::legacy::TRADE_BAGS_END; // was the old incorrect 3179.. - uint32 items[MAX_SLOT_ID] = {0}; - uint8 charges[MAX_SLOT_ID] = {0}; - bool botCanWear[MAX_SLOT_ID] = {0}; +void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* client) +{ + using namespace EQEmu; - for(int16 i = beginSlotID; i <= endSlotID; ++i) { - bool BotCanWear = false; - bool UpdateClient = false; - bool already_returned = false; + struct ClientTrade { + const ItemInstance* tradeItemInstance; + int16 fromClientSlot; + int16 toBotSlot; + int adjustStackSize; + + ClientTrade(const ItemInstance* item, int16 from) : tradeItemInstance(item), fromClientSlot(from), toBotSlot(legacy::SLOT_INVALID), adjustStackSize(0) { } + }; - EQEmu::InventoryProfile& clientInventory = client->GetInv(); - const EQEmu::ItemInstance* inst = clientInventory[i]; - if(inst) { - items[i] = inst->GetItem()->ID; - charges[i] = inst->GetCharges(); - } + struct ClientReturn { + const ItemInstance* returnItemInstance; + int16 fromBotSlot; + int16 toClientSlot; + int adjustStackSize; + + ClientReturn(const ItemInstance* item, int16 from) : returnItemInstance(item), fromBotSlot(from), toClientSlot(legacy::SLOT_INVALID), adjustStackSize(0) { } + }; - if (i == EQEmu::inventory::slotCursor) - UpdateClient = true; + static const int16 proxyPowerSource = 22; - //EQoffline: will give the items to the bots and change the bot stats - if(inst && (GetBotOwner() == client->CastToMob()) && !IsEngaged()) { - std::string TempErrorMessage; - const EQEmu::ItemData* mWeaponItem = inst->GetItem(); - bool failedLoreCheck = false; - for (int m = EQEmu::inventory::socketBegin; m < EQEmu::inventory::SocketCount; ++m) { - EQEmu::ItemInstance *itm = inst->GetAugment(m); - if(itm) - { - if(CheckLoreConflict(itm->GetItem())) { - failedLoreCheck = true; - } - } - } - if(CheckLoreConflict(mWeaponItem)) { - failedLoreCheck = true; - } - if(failedLoreCheck) { - Message_StringID(0, DUP_LORE); - } - if(!failedLoreCheck && mWeaponItem && inst->IsEquipable(GetBaseRace(), GetClass()) && (GetLevel() >= mWeaponItem->ReqLevel)) { - BotCanWear = true; - botCanWear[i] = BotCanWear; - EQEmu::ItemInstance* swap_item = nullptr; + static const int16 bot_equip_order[(legacy::EQUIPMENT_SIZE + 1)] = { + inventory::slotCharm, inventory::slotEar1, inventory::slotHead, inventory::slotFace, + inventory::slotEar2, inventory::slotNeck, inventory::slotShoulders, inventory::slotArms, + inventory::slotBack, inventory::slotWrist1, inventory::slotWrist2, inventory::slotRange, + inventory::slotHands, inventory::slotPrimary, inventory::slotSecondary, inventory::slotFinger1, + inventory::slotFinger2, inventory::slotChest, inventory::slotLegs, inventory::slotFeet, + inventory::slotWaist, inventory::slotAmmo, proxyPowerSource // inventory::slotPowerSource + }; - const char* equipped[EQEmu::legacy::EQUIPMENT_SIZE + 1] = { "Charm", "Left Ear", "Head", "Face", "Right Ear", "Neck", "Shoulders", "Arms", "Back", - "Left Wrist", "Right Wrist", "Range", "Hands", "Primary Hand", "Secondary Hand", - "Left Finger", "Right Finger", "Chest", "Legs", "Feet", "Waist", "Ammo", "Powersource" }; - bool success = false; - int how_many_slots = 0; - for (int j = EQEmu::legacy::EQUIPMENT_BEGIN; j <= (EQEmu::legacy::EQUIPMENT_END + 1); ++j) { - if((mWeaponItem->Slots & (1 << j))) { - if (j == 22) - j = 9999; + enum { stageStackable = 0, stageEmpty, stageReplaceable }; + + if (!client) { + Emote("NO CLIENT"); + return; + } - how_many_slots++; - if(!GetBotItem(j)) { - if (j == EQEmu::inventory::slotPrimary) { - if (mWeaponItem->IsType2HWeapon()) { - if (GetBotItem(EQEmu::inventory::slotSecondary)) { - if (mWeaponItem && mWeaponItem->IsType2HWeapon()) { - if (client->CheckLoreConflict(GetBotItem(EQEmu::inventory::slotSecondary)->GetItem())) { - failedLoreCheck = true; - } - } - else { - EQEmu::ItemInstance* remove_item = GetBotItem(EQEmu::inventory::slotSecondary); - BotTradeSwapItem(client, EQEmu::inventory::slotSecondary, 0, remove_item, remove_item->GetItem()->Slots, &TempErrorMessage, false); - } - } - } - if(!failedLoreCheck) { - BotTradeAddItem(mWeaponItem->ID, inst, inst->GetCharges(), mWeaponItem->Slots, j, &TempErrorMessage); - success = true; - } - break; - } - else if (j == EQEmu::inventory::slotSecondary) { - if(inst->IsWeapon()) { - if(CanThisClassDualWield()) { - BotTradeAddItem(mWeaponItem->ID, inst, inst->GetCharges(), mWeaponItem->Slots, j, &TempErrorMessage); - success = true; - } - else { - BotGroupSay(this, "I can't Dual Wield yet."); - --how_many_slots; - } - } - else { - BotTradeAddItem(mWeaponItem->ID, inst, inst->GetCharges(), mWeaponItem->Slots, j, &TempErrorMessage); - success = true; - } - if(success) { - if (GetBotItem(EQEmu::inventory::slotPrimary)) { - EQEmu::ItemInstance* remove_item = GetBotItem(EQEmu::inventory::slotPrimary); - if (remove_item->GetItem()->IsType2HWeapon()) { - BotTradeSwapItem(client, EQEmu::inventory::slotPrimary, 0, remove_item, remove_item->GetItem()->Slots, &TempErrorMessage, false); - } - } - break; - } - } - else { - BotTradeAddItem(mWeaponItem->ID, inst, inst->GetCharges(), mWeaponItem->Slots, j, &TempErrorMessage); - success = true; - break; - } - } - } - } - if(!success) { - for (int j = EQEmu::legacy::EQUIPMENT_BEGIN; j <= (EQEmu::legacy::EQUIPMENT_END + 1); ++j) { - if((mWeaponItem->Slots & (1 << j))) { - if (j == 22) - j = 9999; + if (client != GetOwner()) { + client->Message(CC_Red, "You are not the owner of this bot - Trade Canceled."); + client->ResetTrade(); + return; + } + if ((beginSlotID != legacy::TRADE_BEGIN) && (beginSlotID != inventory::slotCursor)) { + client->Message(CC_Red, "Trade request processing from illegal 'begin' slot - Trade Canceled."); + client->ResetTrade(); + return; + } + if ((endSlotID != legacy::TRADE_END) && (endSlotID != inventory::slotCursor)) { + client->Message(CC_Red, "Trade request processing from illegal 'end' slot - Trade Canceled."); + client->ResetTrade(); + return; + } + if (((beginSlotID == inventory::slotCursor) && (endSlotID != inventory::slotCursor)) || ((beginSlotID != inventory::slotCursor) && (endSlotID == inventory::slotCursor))) { + client->Message(CC_Red, "Trade request processing illegal slot range - Trade Canceled."); + client->ResetTrade(); + return; + } + if (endSlotID < beginSlotID) { + client->Message(CC_Red, "Trade request processing in reverse slot order - Trade Canceled."); + client->ResetTrade(); + return; + } + if (client->IsEngaged() || IsEngaged()) { + client->Message(CC_Yellow, "You may not perform a trade while engaged - Trade Canceled!"); + client->ResetTrade(); + return; + } - swap_item = GetBotItem(j); - failedLoreCheck = false; - for (int k = EQEmu::inventory::socketBegin; k < EQEmu::inventory::SocketCount; ++k) { - EQEmu::ItemInstance *itm = swap_item->GetAugment(k); - if(itm) - { - if(client->CheckLoreConflict(itm->GetItem())) { - failedLoreCheck = true; - } - } - } - if(client->CheckLoreConflict(swap_item->GetItem())) { - failedLoreCheck = true; - } - if(!failedLoreCheck) { - if (j == EQEmu::inventory::slotPrimary) { - if (mWeaponItem->IsType2HWeapon()) { - if (GetBotItem(EQEmu::inventory::slotSecondary)) { - if (client->CheckLoreConflict(GetBotItem(EQEmu::inventory::slotSecondary)->GetItem())) { - failedLoreCheck = true; - } - else { - EQEmu::ItemInstance* remove_item = GetBotItem(EQEmu::inventory::slotSecondary); - BotTradeSwapItem(client, EQEmu::inventory::slotSecondary, 0, remove_item, remove_item->GetItem()->Slots, &TempErrorMessage, false); - } - } - } - if(!failedLoreCheck) { - BotTradeSwapItem(client, EQEmu::inventory::slotPrimary, inst, swap_item, mWeaponItem->Slots, &TempErrorMessage); - success = true; - } - break; - } - else if (j == EQEmu::inventory::slotSecondary) { - if(inst->IsWeapon()) { - if(CanThisClassDualWield()) { - BotTradeSwapItem(client, EQEmu::inventory::slotSecondary, inst, swap_item, mWeaponItem->Slots, &TempErrorMessage); - success = true; - } - else { - botCanWear[i] = false; - BotGroupSay(this, "I can't Dual Wield yet."); - } - } - else { - BotTradeSwapItem(client, EQEmu::inventory::slotSecondary, inst, swap_item, mWeaponItem->Slots, &TempErrorMessage); - success = true; - } - if (success && GetBotItem(EQEmu::inventory::slotPrimary)) { - EQEmu::ItemInstance* remove_item = GetBotItem(EQEmu::inventory::slotPrimary); - if (remove_item->GetItem()->IsType2HWeapon()) { - BotTradeSwapItem(client, EQEmu::inventory::slotPrimary, 0, remove_item, remove_item->GetItem()->Slots, &TempErrorMessage, false); - } - } - break; - } - else { - BotTradeSwapItem(client, j, inst, swap_item, mWeaponItem->Slots, &TempErrorMessage); - success = true; - break; - } - } - else { - botCanWear[i] = false; - Message_StringID(0, PICK_LORE); - break; - } - } - } - } - if(success) { - if(how_many_slots > 1) { - Message(300, "If you want this item in a different slot, use #bot inventory remove to clear the spot."); - } - CalcBotStats(); - } - } - } - if(inst) { - client->DeleteItemInInventory(i, 0, UpdateClient); - if(!botCanWear[i]) { - client->PushItemOnCursor(*inst, true); - } - } + std::list client_trade; + std::list client_return; + + // pre-checks for incoming illegal transfers + for (int16 trade_index = beginSlotID; trade_index <= endSlotID; ++trade_index) { + auto trade_instance = client->GetInv()[trade_index]; + if (!trade_instance) + continue; + + if (!trade_instance->GetItem()) { + // TODO: add logging + client->Message(CC_Red, "A server error was encountered while processing client slot %i - Trade Canceled.", trade_index); + client->ResetTrade(); + return; + } + if ((trade_index != inventory::slotCursor) && !trade_instance->IsDroppable()) { + // TODO: add logging + client->Message(CC_Red, "Trade hack detected - Trade Canceled."); + client->ResetTrade(); + return; + } + if (CheckLoreConflict(trade_instance->GetItem())) { + client->Message(CC_Yellow, "This bot already has lore equipment matching the item '%s' - Trade Canceled!", trade_instance->GetItem()->Name); + client->ResetTrade(); + return; } - const EQEmu::ItemData* item2 = 0; - for(int y = beginSlotID; y <= endSlotID; ++y) { - item2 = database.GetItem(items[y]); - if(item2) { - if(botCanWear[y]) { - BotGroupSay(this, "Thank you for the %s, %s!", item2->Name, client->GetName()); - } - else { - BotGroupSay(this, "I can't use this %s!", item2->Name); - } + if (!trade_instance->IsType(item::ItemClassCommon)) { + client_return.push_back(ClientReturn(trade_instance, trade_index)); + continue; + } + if (!trade_instance->IsEquipable(GetBaseRace(), GetClass()) || (GetLevel() < trade_instance->GetItem()->ReqLevel)) { + client_return.push_back(ClientReturn(trade_instance, trade_index)); + continue; + } + + client_trade.push_back(ClientTrade(trade_instance, trade_index)); + } + + // check for incoming lore hacks + for (auto& trade_iterator : client_trade) { + if (!trade_iterator.tradeItemInstance->GetItem()->LoreFlag) + continue; + + for (const auto& check_iterator : client_trade) { + if (check_iterator.fromClientSlot == trade_iterator.fromClientSlot) + continue; + if (!check_iterator.tradeItemInstance->GetItem()->LoreFlag) + continue; + + if ((trade_iterator.tradeItemInstance->GetItem()->LoreGroup == -1) && (check_iterator.tradeItemInstance->GetItem()->ID == trade_iterator.tradeItemInstance->GetItem()->ID)) { + // TODO: add logging + client->Message(CC_Red, "Trade hack detected - Trade Canceled."); + client->ResetTrade(); + return; + } + if ((trade_iterator.tradeItemInstance->GetItem()->LoreGroup > 0) && (check_iterator.tradeItemInstance->GetItem()->LoreGroup == trade_iterator.tradeItemInstance->GetItem()->LoreGroup)) { + // TODO: add logging + client->Message(CC_Red, "Trade hack detected - Trade Canceled."); + client->ResetTrade(); + return; } } } + + // find equipment slots + const bool can_dual_wield = CanThisClassDualWield(); + bool melee_2h_weapon = false; + bool melee_secondary = false; + + //for (unsigned stage_loop = stageStackable; stage_loop <= stageReplaceable; ++stage_loop) { // awaiting implementation + for (unsigned stage_loop = stageEmpty; stage_loop <= stageReplaceable; ++stage_loop) { + for (auto& trade_iterator : client_trade) { + if (trade_iterator.toBotSlot != legacy::SLOT_INVALID) + continue; + + auto trade_instance = trade_iterator.tradeItemInstance; + /*if ((stage_loop == stageStackable) && !trade_instance->IsStackable()) + continue;*/ + + for (auto index : bot_equip_order) { + if (!(trade_instance->GetItem()->Slots & (1 << index))) + continue; + + //if (stage_loop == stageStackable) { + // // TODO: implement + // continue; + //} + + if (stage_loop != stageReplaceable) { + if ((index == proxyPowerSource) && m_inv[inventory::slotPowerSource]) + continue; + else if (m_inv[index]) + continue; + } + + bool slot_taken = false; + for (const auto& check_iterator : client_trade) { + if (check_iterator.fromClientSlot == trade_iterator.fromClientSlot) + continue; + + if (check_iterator.toBotSlot == index) { + slot_taken = true; + break; + } + } + if (slot_taken) + continue; + + if (index == inventory::slotPrimary) { + if (trade_instance->GetItem()->IsType2HWeapon()) { + if (!melee_secondary) { + melee_2h_weapon = true; + + if (m_inv[inventory::slotSecondary]) + client_return.push_back(ClientReturn(m_inv[inventory::slotSecondary], inventory::slotSecondary)); + } + else { + continue; + } + } + } + if (index == inventory::slotSecondary) { + if (!melee_2h_weapon) { + if ((can_dual_wield && trade_instance->GetItem()->IsType1HWeapon()) || trade_instance->GetItem()->IsTypeShield() || !trade_instance->IsWeapon()) + melee_secondary = true; + else + continue; + } + else { + continue; + } + } + + if (index == proxyPowerSource) { + trade_iterator.toBotSlot = inventory::slotPowerSource; + + if (m_inv[inventory::slotPowerSource]) + client_return.push_back(ClientReturn(m_inv[inventory::slotPowerSource], inventory::slotPowerSource)); + } + else { + trade_iterator.toBotSlot = index; + + if (m_inv[index]) + client_return.push_back(ClientReturn(m_inv[index], index)); + } + + break; + } + } + } + + // move unassignable items from trade list to return list + for (std::list::iterator trade_iterator = client_trade.begin(); trade_iterator != client_trade.end();) { + if (trade_iterator->toBotSlot == legacy::SLOT_INVALID) { + client_return.push_back(ClientReturn(trade_iterator->tradeItemInstance, trade_iterator->fromClientSlot)); + trade_iterator = client_trade.erase(trade_iterator); + continue; + } + ++trade_iterator; + } + + // out-going return checks for client + for (auto& return_iterator : client_return) { + auto return_instance = return_iterator.returnItemInstance; + if (!return_instance) + continue; + + if (!return_instance->GetItem()) { + // TODO: add logging + client->Message(CC_Red, "A server error was encountered while processing bot slot %i - Trade Canceled.", return_iterator.fromBotSlot); + client->ResetTrade(); + return; + } + if (client->CheckLoreConflict(return_instance->GetItem())) { + client->Message(CC_Yellow, "You already have lore equipment matching the item '%s' - Trade Canceled!", return_instance->GetItem()->Name); + client->ResetTrade(); + return; + } + + if (return_iterator.fromBotSlot == inventory::slotCursor) { + return_iterator.toClientSlot = inventory::slotCursor; + } + else { + int16 client_search_general = legacy::GENERAL_BEGIN; + int16 client_search_bag = inventory::containerBegin; + + bool run_search = true; + while (run_search) { + int16 client_test_slot = client->GetInv().FindFreeSlotForTradeItem(return_instance, client_search_general, client_search_bag); + if (client_test_slot == legacy::SLOT_INVALID) { + run_search = false; + continue; + } + + bool slot_taken = false; + for (const auto& check_iterator : client_return) { + if (check_iterator.fromBotSlot == return_iterator.fromBotSlot) + continue; + + if ((check_iterator.toClientSlot == client_test_slot) && (client_test_slot != inventory::slotCursor)) { + slot_taken = true; + break; + } + } + if (slot_taken) { + client_search_general = InventoryProfile::CalcSlotId(client_test_slot); + client_search_bag = InventoryProfile::CalcBagIdx(client_test_slot); + + ++client_search_bag; + if (client_search_bag >= inventory::ContainerCount) { + client_search_bag = inventory::containerBegin; + // incrementing this past legacy::GENERAL_END triggers the (client_test_slot == legacy::SLOT_INVALID) at the beginning of the search loop + // ideally, this will never occur because we always start fresh with each loop iteration and should receive SLOT_CURSOR as a return value + ++client_search_general; + } + continue; + } + + return_iterator.toClientSlot = client_test_slot; + run_search = false; + } + } + + if (return_iterator.toClientSlot == legacy::SLOT_INVALID) { + client->Message(CC_Yellow, "You do not have room to complete this trade - Trade Canceled!"); + client->ResetTrade(); + return; + } + } + + // perform actual trades + // returns first since clients have trade slots and bots do not + for (auto& return_iterator : client_return) { + // TODO: code for stackables + + if (return_iterator.fromBotSlot == inventory::slotCursor) { // failed trade return + // no movement action required + } + else if ((return_iterator.fromBotSlot >= legacy::TRADE_BEGIN) && (return_iterator.fromBotSlot <= legacy::TRADE_END)) { // failed trade returns + client->PutItemInInventory(return_iterator.toClientSlot, *return_iterator.returnItemInstance); + client->SendItemPacket(return_iterator.toClientSlot, return_iterator.returnItemInstance, ItemPacketTrade); + client->DeleteItemInInventory(return_iterator.fromBotSlot); + } + else { // successful trade returns + auto return_instance = m_inv.PopItem(return_iterator.fromBotSlot); + if (*return_instance != *return_iterator.returnItemInstance) { + // TODO: add logging + } + + if (!botdb.DeleteItemBySlot(GetBotID(), return_iterator.fromBotSlot)) + client->Message(CC_Red, "%s (slot: %i, name: '%s')", BotDatabase::fail::DeleteItemBySlot(), return_iterator.fromBotSlot, (return_instance ? return_instance->GetItem()->Name : "nullptr")); + + BotRemoveEquipItem(return_iterator.fromBotSlot); + client->PutItemInInventory(return_iterator.toClientSlot, *return_instance, true); + InventoryProfile::MarkDirty(return_instance); + } + return_iterator.returnItemInstance = nullptr; + } + + // trades can now go in as empty slot inserts + for (auto& trade_iterator : client_trade) { + // TODO: code for stackables + + if (!botdb.SaveItemBySlot(this, trade_iterator.toBotSlot, trade_iterator.tradeItemInstance)) + client->Message(CC_Red, "%s (slot: %i, name: '%s')", BotDatabase::fail::SaveItemBySlot(), trade_iterator.toBotSlot, (trade_iterator.tradeItemInstance ? trade_iterator.tradeItemInstance->GetItem()->Name : "nullptr")); + + m_inv.PutItem(trade_iterator.toBotSlot, *trade_iterator.tradeItemInstance); + this->BotAddEquipItem(trade_iterator.toBotSlot, (trade_iterator.tradeItemInstance ? trade_iterator.tradeItemInstance->GetID() : 0)); + client->DeleteItemInInventory(trade_iterator.fromClientSlot, 0, true); + trade_iterator.tradeItemInstance = nullptr; + } + + // trade announcements + for (const auto& return_iterator : client_return) { // item pointers should be nulled by this point + if (((return_iterator.fromBotSlot < legacy::TRADE_BEGIN) || (return_iterator.fromBotSlot > legacy::TRADE_END)) && (return_iterator.fromBotSlot != inventory::slotCursor)) + continue; + + if (client->GetInv()[return_iterator.toClientSlot]) + client->Message(MT_Tell, "%s tells you, \"%s, I can't use this '%s.'\"", GetCleanName(), client->GetName(), client->GetInv()[return_iterator.toClientSlot]->GetItem()->Name); + } + for (const auto& trade_iterator : client_trade) { // item pointers should be nulled by this point + if (m_inv[trade_iterator.toBotSlot]) + client->Message(MT_Tell, "%s tells you, \"Thank you for the '%s,' %s!\"", GetCleanName(), m_inv[trade_iterator.toBotSlot]->GetItem()->Name, client->GetName()); + } + + size_t accepted_count = client_trade.size(); + size_t returned_count = client_return.size(); + + client->Message(CC_Lime, "Trade with '%s' resulted in %i accepted item%s, %i returned item%s.", GetCleanName(), accepted_count, ((accepted_count == 1) ? "" : "s"), returned_count, ((returned_count == 1) ? "" : "s")); } bool Bot::Death(Mob *killerMob, int32 damage, uint16 spell_id, EQEmu::skills::SkillType attack_skill) { diff --git a/zone/bot.h b/zone/bot.h index dea5dd8e8..ca5a7e9ad 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -655,7 +655,7 @@ public: // Publicized private functions static NPCType FillNPCTypeStruct(uint32 botSpellsID, std::string botName, std::string botLastName, uint8 botLevel, uint16 botRace, uint8 botClass, uint8 gender, float size, uint32 face, uint32 hairStyle, uint32 hairColor, uint32 eyeColor, uint32 eyeColor2, uint32 beardColor, uint32 beard, uint32 drakkinHeritage, uint32 drakkinTattoo, uint32 drakkinDetails, int32 hp, int32 mana, int32 mr, int32 cr, int32 dr, int32 fr, int32 pr, int32 corrup, int32 ac, uint32 str, uint32 sta, uint32 dex, uint32 agi, uint32 _int, uint32 wis, uint32 cha, uint32 attack); - void BotRemoveEquipItem(int slot); + void BotRemoveEquipItem(int16 slot); void RemoveBotItemBySlot(uint32 slotID, std::string* errorMessage); uint32 GetTotalPlayTime(); From 999650d368c68507827595a11c0b45fade9d76d0 Mon Sep 17 00:00:00 2001 From: Uleat Date: Thu, 9 Mar 2017 01:55:01 -0500 Subject: [PATCH 10/16] Fixed a few glitches related to bot trading and other affected code --- changelog.txt | 7 ++++ common/inventory_profile.cpp | 16 +++++---- common/inventory_profile.h | 2 +- zone/bot.cpp | 63 +++++++++++++++++++++--------------- 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/changelog.txt b/changelog.txt index 76fc0740e..85ac6c97a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,12 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 03/09/2017 == +Uleat: Fixed a few glitches related to bot trading and other affected code + - Added a temporary fail clause for partial stack transfers to prevent client item overwrites + - Return messages no longer repeat the top cursor item when multiple items are pushed there + - Test slot for client returns is now handled appropriately for parent and bag searches + - FindFreeSlotForTradeItem() now begins at the correct bag index on subsequent parent iterations + == 03/08/2017 == Uleat: Complete rework of the bot trading system - Equipment slot priority can now be tailored..though, a recompile will be required diff --git a/common/inventory_profile.cpp b/common/inventory_profile.cpp index 7c97be706..480f4e98f 100644 --- a/common/inventory_profile.cpp +++ b/common/inventory_profile.cpp @@ -635,7 +635,7 @@ int16 EQEmu::InventoryProfile::FindFreeSlot(bool for_bag, bool try_cursor, uint8 } // This is a mix of HasSpaceForItem and FindFreeSlot..due to existing coding behavior, it was better to add a new helper function... -int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst, int16 general_start, int16 bag_start) { +int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst, int16 general_start, uint8 bag_start) { // Do not arbitrarily use this function..it is designed for use with Client::ResetTrade() and Client::FinishTrade(). // If you have a need, use it..but, understand it is not a suitable replacement for InventoryProfile::FindFreeSlot(). // @@ -643,7 +643,7 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst if ((general_start < legacy::GENERAL_BEGIN) || (general_start > legacy::GENERAL_END)) return INVALID_INDEX; - if ((bag_start < inventory::containerBegin) || (bag_start >= inventory::ContainerCount)) + if (bag_start >= inventory::ContainerCount) return INVALID_INDEX; if (!inst || !inst->GetID()) @@ -678,7 +678,8 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst continue; if (main_inst->IsClassBag()) { // if item-specific containers already have bad items, we won't fix it here... - for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + uint8 _bag_start = (free_slot > general_start) ? inventory::containerBegin : bag_start; + for (uint8 free_bag_slot = _bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { const ItemInstance* sub_inst = main_inst->GetItem(free_bag_slot); if (!sub_inst) @@ -699,7 +700,8 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst if (!main_inst || (main_inst->GetItem()->BagType != item::BagTypeQuiver) || !main_inst->IsClassBag()) continue; - for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + uint8 _bag_start = (free_slot > general_start) ? inventory::containerBegin : bag_start; + for (uint8 free_bag_slot = _bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { if (!main_inst->GetItem(free_bag_slot)) return InventoryProfile::CalcSlotId(free_slot, free_bag_slot); } @@ -714,7 +716,8 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst if (!main_inst || (main_inst->GetItem()->BagType != item::BagTypeBandolier) || !main_inst->IsClassBag()) continue; - for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + uint8 _bag_start = (free_slot > general_start) ? inventory::containerBegin : bag_start; + for (uint8 free_bag_slot = _bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { if (!main_inst->GetItem(free_bag_slot)) return InventoryProfile::CalcSlotId(free_slot, free_bag_slot); } @@ -736,7 +739,8 @@ int16 EQEmu::InventoryProfile::FindFreeSlotForTradeItem(const ItemInstance* inst if ((main_inst->GetItem()->BagSize < inst->GetItem()->Size) || (main_inst->GetItem()->BagType == item::BagTypeBandolier) || (main_inst->GetItem()->BagType == item::BagTypeQuiver)) continue; - for (uint8 free_bag_slot = bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { + uint8 _bag_start = (free_slot > general_start) ? inventory::containerBegin : bag_start; + for (uint8 free_bag_slot = _bag_start; (free_bag_slot < main_inst->GetItem()->BagSlots) && (free_bag_slot < inventory::ContainerCount); ++free_bag_slot) { if (!main_inst->GetItem(free_bag_slot)) return InventoryProfile::CalcSlotId(free_slot, free_bag_slot); } diff --git a/common/inventory_profile.h b/common/inventory_profile.h index 7bb659cff..d8cf6cf7f 100644 --- a/common/inventory_profile.h +++ b/common/inventory_profile.h @@ -155,7 +155,7 @@ namespace EQEmu // Locate an available inventory slot int16 FindFreeSlot(bool for_bag, bool try_cursor, uint8 min_size = 0, bool is_arrow = false); - int16 FindFreeSlotForTradeItem(const ItemInstance* inst, int16 general_start = legacy::GENERAL_BEGIN, int16 bag_start = inventory::containerBegin); + int16 FindFreeSlotForTradeItem(const ItemInstance* inst, int16 general_start = legacy::GENERAL_BEGIN, uint8 bag_start = inventory::containerBegin); // Calculate slot_id for an item within a bag static int16 CalcSlotId(int16 slot_id); // Calc parent bag's slot_id diff --git a/zone/bot.cpp b/zone/bot.cpp index 32796b88b..361a188c1 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -3359,8 +3359,9 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli int16 fromClientSlot; int16 toBotSlot; int adjustStackSize; + std::string acceptedItemName; - ClientTrade(const ItemInstance* item, int16 from) : tradeItemInstance(item), fromClientSlot(from), toBotSlot(legacy::SLOT_INVALID), adjustStackSize(0) { } + ClientTrade(const ItemInstance* item, int16 from, const char* name = "") : tradeItemInstance(item), fromClientSlot(from), toBotSlot(legacy::SLOT_INVALID), adjustStackSize(0), acceptedItemName(name) { } }; struct ClientReturn { @@ -3368,8 +3369,9 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli int16 fromBotSlot; int16 toClientSlot; int adjustStackSize; + std::string failedItemName; - ClientReturn(const ItemInstance* item, int16 from) : returnItemInstance(item), fromBotSlot(from), toClientSlot(legacy::SLOT_INVALID), adjustStackSize(0) { } + ClientReturn(const ItemInstance* item, int16 from, const char* name = "") : returnItemInstance(item), fromBotSlot(from), toClientSlot(legacy::SLOT_INVALID), adjustStackSize(0), failedItemName(name) { } }; static const int16 proxyPowerSource = 22; @@ -3442,6 +3444,11 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli client->ResetTrade(); return; } + if (trade_instance->IsStackable() && (trade_instance->GetCharges() < trade_instance->GetItem()->StackSize)) { // temp until partial stacks are implemented + client->Message(CC_Yellow, "'%s' is only a partially stacked item - Trade Canceled!", trade_instance->GetItem()->Name); + client->ResetTrade(); + return; + } if (CheckLoreConflict(trade_instance->GetItem())) { client->Message(CC_Yellow, "This bot already has lore equipment matching the item '%s' - Trade Canceled!", trade_instance->GetItem()->Name); client->ResetTrade(); @@ -3449,15 +3456,15 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli } if (!trade_instance->IsType(item::ItemClassCommon)) { - client_return.push_back(ClientReturn(trade_instance, trade_index)); + client_return.push_back(ClientReturn(trade_instance, trade_index, trade_instance->GetItem()->Name)); continue; } - if (!trade_instance->IsEquipable(GetBaseRace(), GetClass()) || (GetLevel() < trade_instance->GetItem()->ReqLevel)) { - client_return.push_back(ClientReturn(trade_instance, trade_index)); + if (!trade_instance->IsEquipable(GetBaseRace(), GetClass()) || (GetLevel() < trade_instance->GetItem()->ReqLevel)) { // deity checks will be handled within IsEquipable() + client_return.push_back(ClientReturn(trade_instance, trade_index, trade_instance->GetItem()->Name)); continue; } - client_trade.push_back(ClientTrade(trade_instance, trade_index)); + client_trade.push_back(ClientTrade(trade_instance, trade_index, trade_instance->GetItem()->Name)); } // check for incoming lore hacks @@ -3576,7 +3583,7 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli // move unassignable items from trade list to return list for (std::list::iterator trade_iterator = client_trade.begin(); trade_iterator != client_trade.end();) { if (trade_iterator->toBotSlot == legacy::SLOT_INVALID) { - client_return.push_back(ClientReturn(trade_iterator->tradeItemInstance, trade_iterator->fromClientSlot)); + client_return.push_back(ClientReturn(trade_iterator->tradeItemInstance, trade_iterator->fromClientSlot, trade_iterator->tradeItemInstance->GetItem()->Name)); trade_iterator = client_trade.erase(trade_iterator); continue; } @@ -3606,7 +3613,7 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli } else { int16 client_search_general = legacy::GENERAL_BEGIN; - int16 client_search_bag = inventory::containerBegin; + uint8 client_search_bag = inventory::containerBegin; bool run_search = true; while (run_search) { @@ -3627,16 +3634,23 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli } } if (slot_taken) { - client_search_general = InventoryProfile::CalcSlotId(client_test_slot); - client_search_bag = InventoryProfile::CalcBagIdx(client_test_slot); - - ++client_search_bag; - if (client_search_bag >= inventory::ContainerCount) { - client_search_bag = inventory::containerBegin; - // incrementing this past legacy::GENERAL_END triggers the (client_test_slot == legacy::SLOT_INVALID) at the beginning of the search loop - // ideally, this will never occur because we always start fresh with each loop iteration and should receive SLOT_CURSOR as a return value + if ((client_test_slot >= legacy::GENERAL_BEGIN) && (client_test_slot <= legacy::GENERAL_END)) { ++client_search_general; + client_search_bag = inventory::containerBegin; } + else { + client_search_general = InventoryProfile::CalcSlotId(client_test_slot); + client_search_bag = InventoryProfile::CalcBagIdx(client_test_slot); + + ++client_search_bag; + if (client_search_bag >= inventory::ContainerCount) { + // incrementing this past legacy::GENERAL_END triggers the (client_test_slot == legacy::SLOT_INVALID) at the beginning of the search loop + // ideally, this will never occur because we always start fresh with each loop iteration and should receive SLOT_CURSOR as a return value + ++client_search_general; + client_search_bag = inventory::containerBegin; + } + } + continue; } @@ -3694,17 +3708,14 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli trade_iterator.tradeItemInstance = nullptr; } - // trade announcements - for (const auto& return_iterator : client_return) { // item pointers should be nulled by this point - if (((return_iterator.fromBotSlot < legacy::TRADE_BEGIN) || (return_iterator.fromBotSlot > legacy::TRADE_END)) && (return_iterator.fromBotSlot != inventory::slotCursor)) - continue; - - if (client->GetInv()[return_iterator.toClientSlot]) - client->Message(MT_Tell, "%s tells you, \"%s, I can't use this '%s.'\"", GetCleanName(), client->GetName(), client->GetInv()[return_iterator.toClientSlot]->GetItem()->Name); + // trade messages + for (const auto& return_iterator : client_return) { + if (return_iterator.failedItemName.size()) + client->Message(MT_Tell, "%s tells you, \"%s, I can't use this '%s.'\"", GetCleanName(), client->GetName(), return_iterator.failedItemName.c_str()); } - for (const auto& trade_iterator : client_trade) { // item pointers should be nulled by this point - if (m_inv[trade_iterator.toBotSlot]) - client->Message(MT_Tell, "%s tells you, \"Thank you for the '%s,' %s!\"", GetCleanName(), m_inv[trade_iterator.toBotSlot]->GetItem()->Name, client->GetName()); + for (const auto& trade_iterator : client_trade) { + if (trade_iterator.acceptedItemName.size()) + client->Message(MT_Tell, "%s tells you, \"Thank you for the '%s,' %s!\"", GetCleanName(), trade_iterator.acceptedItemName.c_str(), client->GetName()); } size_t accepted_count = client_trade.size(); From 37d22e17a348c0f1b192dd38c39c855f84e0690f Mon Sep 17 00:00:00 2001 From: Uleat Date: Thu, 9 Mar 2017 02:46:09 -0500 Subject: [PATCH 11/16] First step of implementing inventory v2.0 --- changelog.txt | 1 + common/version.h | 2 +- utils/sql/db_update_manifest.txt | 1 + .../sql/git/required/2017_03_09_inventory_version.sql | 11 +++++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 utils/sql/git/required/2017_03_09_inventory_version.sql diff --git a/changelog.txt b/changelog.txt index 85ac6c97a..073827861 100644 --- a/changelog.txt +++ b/changelog.txt @@ -6,6 +6,7 @@ Uleat: Fixed a few glitches related to bot trading and other affected code - Return messages no longer repeat the top cursor item when multiple items are pushed there - Test slot for client returns is now handled appropriately for parent and bag searches - FindFreeSlotForTradeItem() now begins at the correct bag index on subsequent parent iterations +Uleat: First step of implementing inventory v2.0 == 03/08/2017 == Uleat: Complete rework of the bot trading system diff --git a/common/version.h b/common/version.h index 1eb98558f..60d594dea 100644 --- a/common/version.h +++ b/common/version.h @@ -30,7 +30,7 @@ Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9106 +#define CURRENT_BINARY_DATABASE_VERSION 9107 #ifdef BOTS #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9015 #else diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index c21d00951..428fd52c8 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -360,6 +360,7 @@ 9104|2017_02_09_npc_spells_entries_type_update.sql|SHOW COLUMNS IN `npc_spells_entries` LIKE `type`|contains|smallint(5) unsigned 9105|2017_02_15_bot_spells_entries.sql|SELECT `id` FROM `npc_spells_entries` WHERE `npc_spells_id` >= 701 AND `npc_spells_id` <= 712|not_empty| 9106|2017_02_26_npc_spells_update_for_bots.sql|SELECT * FROM `npc_spells` WHERE `id` = '701' AND `name` = 'Cleric Bot'|not_empty| +9107|2017_03_09_inventory_version.sql|SHOW TABLES LIKE 'inventory_version'|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2017_03_09_inventory_version.sql b/utils/sql/git/required/2017_03_09_inventory_version.sql new file mode 100644 index 000000000..37ef38548 --- /dev/null +++ b/utils/sql/git/required/2017_03_09_inventory_version.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `inventory_version`; + +CREATE TABLE `inventory_version` ( + `version` INT(11) UNSIGNED NOT NULL DEFAULT '0', + `step` INT(11) UNSIGNED NOT NULL DEFAULT '0' +) +COLLATE='latin1_swedish_ci' +ENGINE=MyISAM +; + +INSERT INTO `inventory_version` VALUES (2, 0); From df5d58f43da4e2aeb9ca215796fd7bd82ad60b6d Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Fri, 10 Mar 2017 13:00:22 -0500 Subject: [PATCH 12/16] Temp pets shouldn't spawn when they're targeting a corpse --- zone/aa.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zone/aa.cpp b/zone/aa.cpp index 0f0617393..3c6f11f6e 100644 --- a/zone/aa.cpp +++ b/zone/aa.cpp @@ -42,6 +42,10 @@ void Mob::TemporaryPets(uint16 spell_id, Mob *targ, const char *name_override, u //Dook- swarms and wards + // do nothing if it's a corpse + if (targ != nullptr && targ->IsCorpse()) + return; + PetRecord record; if(!database.GetPetEntry(spells[spell_id].teleport_zone, &record)) { From 3d229e1da1c99f0fb6d4a23cec5cca38b63ec0f0 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Fri, 10 Mar 2017 18:15:08 -0500 Subject: [PATCH 13/16] Aggro Meter on by default now (seemed fine on PEQ) --- common/ruletypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/ruletypes.h b/common/ruletypes.h index 3599a718e..1b2688f53 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -114,7 +114,7 @@ RULE_BOOL(Character, CheckCursorEmptyWhenLooting, true) // If true, a player can RULE_BOOL(Character, MaintainIntoxicationAcrossZones, true) // If true, alcohol effects are maintained across zoning and logging out/in. RULE_BOOL(Character, EnableDiscoveredItems, true) // If enabled, it enables EVENT_DISCOVER_ITEM and also saves character names and timestamps for the first time an item is discovered. RULE_BOOL(Character, EnableXTargetting, true) // Enable Extended Targetting Window, for users with UF and later clients. -RULE_BOOL(Character, EnableAggroMeter, false) // Enable Aggro Meter, for users with RoF and later clients. +RULE_BOOL(Character, EnableAggroMeter, true) // Enable Aggro Meter, for users with RoF and later clients. RULE_BOOL(Character, KeepLevelOverMax, false) // Don't delevel a character that has somehow gone over the level cap RULE_INT(Character, FoodLossPerUpdate, 35) // How much food/water you lose per stamina update RULE_INT(Character, BaseInstrumentSoftCap, 36) // Softcap for instrument mods, 36 commonly referred to as "3.6" as well. From bf3d9b2d02ca203fda51e696b8749c4d911765bb Mon Sep 17 00:00:00 2001 From: Uleat Date: Fri, 10 Mar 2017 18:41:04 -0500 Subject: [PATCH 14/16] Couple of critical fixes for bot trade code --- common/item_data.cpp | 2 +- zone/bot.cpp | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/common/item_data.cpp b/common/item_data.cpp index 3286bf97e..e23f962d2 100644 --- a/common/item_data.cpp +++ b/common/item_data.cpp @@ -195,7 +195,7 @@ bool EQEmu::ItemData::IsClassBook() const bool EQEmu::ItemData::IsType1HWeapon() const { - return ((ItemType == item::ItemType1HBlunt) || (ItemType == item::ItemType1HSlash) || (ItemType == item::ItemType1HPiercing)); + return ((ItemType == item::ItemType1HBlunt) || (ItemType == item::ItemType1HSlash) || (ItemType == item::ItemType1HPiercing) || (ItemType == item::ItemTypeMartial)); } bool EQEmu::ItemData::IsType2HWeapon() const diff --git a/zone/bot.cpp b/zone/bot.cpp index 361a188c1..df5c3ce08 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -3494,7 +3494,7 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli } // find equipment slots - const bool can_dual_wield = CanThisClassDualWield(); + const bool can_dual_wield = (GetSkill(EQEmu::skills::SkillDualWield) > 0); bool melee_2h_weapon = false; bool melee_secondary = false; @@ -3542,8 +3542,9 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli if (!melee_secondary) { melee_2h_weapon = true; - if (m_inv[inventory::slotSecondary]) - client_return.push_back(ClientReturn(m_inv[inventory::slotSecondary], inventory::slotSecondary)); + auto equipped_secondary_weapon = m_inv[inventory::slotSecondary]; + if (equipped_secondary_weapon) + client_return.push_back(ClientReturn(equipped_secondary_weapon, inventory::slotSecondary)); } else { continue; @@ -3552,10 +3553,16 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli } if (index == inventory::slotSecondary) { if (!melee_2h_weapon) { - if ((can_dual_wield && trade_instance->GetItem()->IsType1HWeapon()) || trade_instance->GetItem()->IsTypeShield() || !trade_instance->IsWeapon()) + if ((can_dual_wield && trade_instance->GetItem()->IsType1HWeapon()) || trade_instance->GetItem()->IsTypeShield() || !trade_instance->IsWeapon()) { melee_secondary = true; - else + + auto equipped_primary_weapon = m_inv[inventory::slotPrimary]; + if (equipped_primary_weapon && equipped_primary_weapon->GetItem()->IsType2HWeapon()) + client_return.push_back(ClientReturn(equipped_primary_weapon, inventory::slotPrimary)); + } + else { continue; + } } else { continue; @@ -3681,15 +3688,16 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli } else { // successful trade returns auto return_instance = m_inv.PopItem(return_iterator.fromBotSlot); - if (*return_instance != *return_iterator.returnItemInstance) { - // TODO: add logging - } + //if (*return_instance != *return_iterator.returnItemInstance) { + // // TODO: add logging + //} if (!botdb.DeleteItemBySlot(GetBotID(), return_iterator.fromBotSlot)) client->Message(CC_Red, "%s (slot: %i, name: '%s')", BotDatabase::fail::DeleteItemBySlot(), return_iterator.fromBotSlot, (return_instance ? return_instance->GetItem()->Name : "nullptr")); BotRemoveEquipItem(return_iterator.fromBotSlot); - client->PutItemInInventory(return_iterator.toClientSlot, *return_instance, true); + if (return_instance) + client->PutItemInInventory(return_iterator.toClientSlot, *return_instance, true); InventoryProfile::MarkDirty(return_instance); } return_iterator.returnItemInstance = nullptr; From 262bcf5c2903046a5bc80d23629e58b770978f12 Mon Sep 17 00:00:00 2001 From: "Michael Cook (mackal)" Date: Fri, 10 Mar 2017 19:16:43 -0500 Subject: [PATCH 15/16] Fix merc attack delay --- zone/zonedb.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index ec56b8df8..a36042610 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -2261,7 +2261,7 @@ const NPCType* ZoneDatabase::GetMercType(uint32 id, uint16 raceid, uint32 client tmpNPCType->gender = atoi(row[7]); tmpNPCType->texture = atoi(row[8]); tmpNPCType->helmtexture = atoi(row[9]); - tmpNPCType->attack_delay = atoi(row[10]); + tmpNPCType->attack_delay = atoi(row[10]) * 100; // TODO: fix DB tmpNPCType->STR = atoi(row[11]); tmpNPCType->STA = atoi(row[12]); tmpNPCType->DEX = atoi(row[13]); From 3555791e1fa10635dea79bf97ff0886c4b23d848 Mon Sep 17 00:00:00 2001 From: Uleat Date: Sat, 11 Mar 2017 13:35:30 -0500 Subject: [PATCH 16/16] Multi-line remark statements wreck havoc on visual studio's parser (expensive) --- zone/bot.cpp | 74 ++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/zone/bot.cpp b/zone/bot.cpp index df5c3ce08..bdb8cd627 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -1724,7 +1724,7 @@ bool Bot::LoadPet() bool Bot::SavePet() { - if (!GetPet() || GetPet()->IsFamiliar() /*|| dead*/) + if (!GetPet() || GetPet()->IsFamiliar()) // dead? return true; NPC *pet_inst = GetPet()->CastToNPC(); @@ -3505,8 +3505,8 @@ void Bot::PerformTradeWithClient(int16 beginSlotID, int16 endSlotID, Client* cli continue; auto trade_instance = trade_iterator.tradeItemInstance; - /*if ((stage_loop == stageStackable) && !trade_instance->IsStackable()) - continue;*/ + //if ((stage_loop == stageStackable) && !trade_instance->IsStackable()) + // continue; for (auto index : bot_equip_order) { if (!(trade_instance->GetItem()->Slots & (1 << index))) @@ -3849,7 +3849,8 @@ void Bot::Damage(Mob *from, int32 damage, uint16 spell_id, EQEmu::skills::SkillT } } -void Bot::AddToHateList(Mob* other, uint32 hate /*= 0*/, int32 damage /*= 0*/, bool iYellForHelp /*= true*/, bool bFrenzy /*= false*/, bool iBuffTic /*= false*/) { +//void Bot::AddToHateList(Mob* other, uint32 hate = 0, int32 damage = 0, bool iYellForHelp = true, bool bFrenzy = false, bool iBuffTic = false) +void Bot::AddToHateList(Mob* other, uint32 hate, int32 damage, bool iYellForHelp, bool bFrenzy, bool iBuffTic) { Mob::AddToHateList(other, hate, damage, iYellForHelp, bFrenzy, iBuffTic); } @@ -4261,24 +4262,23 @@ int32 Bot::CalcBotAAFocus(BotfocusType type, uint32 aa_ID, uint32 points, uint16 value = base1; break; } - /* - case SE_SympatheticProc: - { - if(type == focusSympatheticProc) - { - float ProcChance, ProcBonus; - int16 ProcRateMod = base1; //Baseline is 100 for most Sympathetic foci - int32 cast_time = GetActSpellCasttime(spell_id, spells[spell_id].cast_time); - GetSympatheticProcChances(ProcBonus, ProcChance, cast_time, ProcRateMod); + //case SE_SympatheticProc: + //{ + // if(type == focusSympatheticProc) + // { + // float ProcChance, ProcBonus; + // int16 ProcRateMod = base1; //Baseline is 100 for most Sympathetic foci + // int32 cast_time = GetActSpellCasttime(spell_id, spells[spell_id].cast_time); + // GetSympatheticProcChances(ProcBonus, ProcChance, cast_time, ProcRateMod); - if(zone->random.Real(0, 1) <= ProcChance) - value = focus_id; + // if(zone->random.Real(0, 1) <= ProcChance) + // value = focus_id; - else - value = 0; - } - break; - }*/ + // else + // value = 0; + // } + // break; + //} case SE_FcDamageAmt: { if(type == focusFcDamageAmt) value = base1; @@ -5069,14 +5069,14 @@ void Bot::DoSpecialAttackDamage(Mob *who, EQEmu::skills::SkillType skill, int32 CheckNumHitsRemaining(NumHit::OutgoingHitSuccess); //[AA Dragon Punch] value[0] = 100 for 25%, chance value[1] = skill -/* if(aabonuses.SpecialAttackKBProc[0] && aabonuses.SpecialAttackKBProc[1] == skill){ - int kb_chance = 25; - kb_chance += (kb_chance * (100 - aabonuses.SpecialAttackKBProc[0]) / 100); + //if(aabonuses.SpecialAttackKBProc[0] && aabonuses.SpecialAttackKBProc[1] == skill){ + // int kb_chance = 25; + // kb_chance += (kb_chance * (100 - aabonuses.SpecialAttackKBProc[0]) / 100); - if (zone->random.Int(0, 99) < kb_chance) - SpellFinished(904, who, 10, 0, -1, spells[904].ResistDiff); - //who->Stun(100); Kayen: This effect does not stun on live, it only moves the NPC. - }*/ + // if (zone->random.Int(0, 99) < kb_chance) + // SpellFinished(904, who, 10, 0, -1, spells[904].ResistDiff); + // //who->Stun(100); Kayen: This effect does not stun on live, it only moves the NPC. + //} if (HasSkillProcs()) TrySkillProc(who, skill, (ReuseTime * 1000)); @@ -7648,7 +7648,7 @@ void Bot::AddItemBonuses(const EQEmu::ItemInstance *inst, StatBonuses* newbon, b switch(item->BardType) { - case 51: /* All (e.g. Singing Short Sword) */ + case EQEmu::item::ItemTypeAllInstrumentTypes: // (e.g. Singing Short Sword) { if(item->BardValue > newbon->singingMod) newbon->singingMod = item->BardValue; @@ -7662,31 +7662,31 @@ void Bot::AddItemBonuses(const EQEmu::ItemInstance *inst, StatBonuses* newbon, b newbon->windMod = item->BardValue; break; } - case 50: /* Singing */ + case EQEmu::item::ItemTypeSinging: { if(item->BardValue > newbon->singingMod) newbon->singingMod = item->BardValue; break; } - case 23: /* Wind */ + case EQEmu::item::ItemTypeWindInstrument: { if(item->BardValue > newbon->windMod) newbon->windMod = item->BardValue; break; } - case 24: /* stringed */ + case EQEmu::item::ItemTypeStringedInstrument: { if(item->BardValue > newbon->stringedMod) newbon->stringedMod = item->BardValue; break; } - case 25: /* brass */ + case EQEmu::item::ItemTypeBrassInstrument: { if(item->BardValue > newbon->brassMod) newbon->brassMod = item->BardValue; break; } - case 26: /* Percussion */ + case EQEmu::item::ItemTypePercussionInstrument: { if(item->BardValue > newbon->percussionMod) newbon->percussionMod = item->BardValue; @@ -7768,10 +7768,10 @@ void Bot::CalcBotStats(bool showtext) { GetSkill(EQEmu::skills::SkillBrassInstruments), GetSkill(EQEmu::skills::SkillPercussionInstruments), GetSkill(EQEmu::skills::SkillSinging), GetSkill(EQEmu::skills::SkillStringedInstruments), GetSkill(EQEmu::skills::SkillWindInstruments)); } - /*if(this->Save()) - this->GetBotOwner()->CastToClient()->Message(0, "%s saved.", this->GetCleanName()); - else - this->GetBotOwner()->CastToClient()->Message(13, "%s save failed!", this->GetCleanName());*/ + //if(this->Save()) + // this->GetBotOwner()->CastToClient()->Message(0, "%s saved.", this->GetCleanName()); + //else + // this->GetBotOwner()->CastToClient()->Message(13, "%s save failed!", this->GetCleanName()); CalcBonuses();