Merge remote-tracking branch 'origin' into eqstream

This commit is contained in:
KimLS 2017-03-11 14:51:33 -08:00
commit 73dc6b090b
18 changed files with 625 additions and 351 deletions

View File

@ -1,5 +1,28 @@
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
Uleat: First step of implementing inventory v2.0
== 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...)

View File

@ -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, 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 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::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,15 @@ 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) {
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)
@ -688,13 +694,14 @@ 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) {
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);
}
@ -703,13 +710,14 @@ 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) {
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);
}
@ -717,21 +725,22 @@ 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) {
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);
}

View File

@ -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, 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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -30,7 +30,6 @@ character_inspect_messages
character_leadership_abilities
character_activities
character_alt_currency
character_backup
character_buffs
character_enabledtasks
character_pet_buffs

View File

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

View File

@ -1302,9 +1302,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

View File

@ -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();
@ -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<glm::vec3>(m_Position), m_PreSummonLocation) < 10)) {
if(!GetTarget() || (IsBotCaster() && !IsBotCasterAtCombatRange(GetTarget())) || (IsBotArcher() && IsArcheryRange(GetTarget())) || (DistanceSquaredNoZ(static_cast<glm::vec3>(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()) {
@ -3224,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) {
@ -3325,243 +3325,411 @@ 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;
std::string acceptedItemName;
ClientTrade(const ItemInstance* item, int16 from, const char* name = "") : tradeItemInstance(item), fromClientSlot(from), toBotSlot(legacy::SLOT_INVALID), adjustStackSize(0), acceptedItemName(name) { }
};
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;
std::string failedItemName;
ClientReturn(const ItemInstance* item, int16 from, const char* name = "") : returnItemInstance(item), fromBotSlot(from), toClientSlot(legacy::SLOT_INVALID), adjustStackSize(0), failedItemName(name) { }
};
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 <slot_id> to clear the spot.");
}
CalcBotStats();
}
}
}
if(inst) {
client->DeleteItemInInventory(i, 0, UpdateClient);
if(!botCanWear[i]) {
client->PushItemOnCursor(*inst, true);
}
}
std::list<ClientTrade> client_trade;
std::list<ClientReturn> 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 (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();
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, trade_instance->GetItem()->Name));
continue;
}
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, trade_instance->GetItem()->Name));
}
// 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 = (GetSkill(EQEmu::skills::SkillDualWield) > 0);
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;
auto equipped_secondary_weapon = m_inv[inventory::slotSecondary];
if (equipped_secondary_weapon)
client_return.push_back(ClientReturn(equipped_secondary_weapon, 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;
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;
}
}
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<ClientTrade>::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->tradeItemInstance->GetItem()->Name));
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;
uint8 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) {
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;
}
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);
if (return_instance)
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 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) {
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();
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) {
@ -3681,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);
}
@ -3692,11 +3861,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());
@ -3739,8 +3919,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)
@ -3786,6 +3968,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;
@ -3794,8 +3977,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);
@ -4076,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;
@ -4884,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));
@ -5426,7 +5611,7 @@ void Bot::SetAttackTimer() {
}
speed = (RuleB(Spells, Jun182014HundredHandsRevamp) ? static_cast<int>(((delay / haste_mod) + ((hhe / 1000.0f) * (delay / haste_mod))) * 100) : static_cast<int>(((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;
@ -6856,20 +7041,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)
@ -7452,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;
@ -7466,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;
@ -7572,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();
@ -7609,21 +7805,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!!");

View File

@ -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);
@ -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<BotSpell> GetBotSpellsForSpellEffect(Bot* botCaster, int spellEffect);
static std::list<BotSpell> GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, int spellEffect, SpellTargetType targetType);
static std::list<BotSpell> GetBotSpellsBySpellType(Bot* botCaster, uint32 spellType);
static std::list<BotSpell_wPriority> 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
@ -657,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();

View File

@ -3894,10 +3894,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;
@ -12554,8 +12563,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())
@ -13038,9 +13064,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
{

View File

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

View File

@ -4156,8 +4156,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();

View File

@ -3291,8 +3291,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();

View File

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