diff --git a/common/ruletypes.h b/common/ruletypes.h index 0a388b4eb..6dafb82c2 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -599,6 +599,9 @@ RULE_INT(Bots, AllowedGenders, 0x3, "Bitmask of allowed bot genders") RULE_BOOL(Bots, AllowOwnerOptionAltCombat, true, "When option is enabled, bots will use an auto-/shared-aggro combat model") RULE_BOOL(Bots, AllowOwnerOptionAutoDefend, true, "When option is enabled, bots will defend their owner on enemy aggro") RULE_REAL(Bots, LeashDistance, 562500.0f, "Distance a bot is allowed to travel from leash owner before being pulled back (squared value)") +RULE_BOOL(Bots, AllowApplyPoisonCommand, true, "Allows the use of the bot command 'applypoison'") +RULE_BOOL(Bots, AllowApplyPotionCommand, true, "Allows the use of the bot command 'applypotion'") +RULE_BOOL(Bots, RestrictApplyPotionToRogue, true, "Restricts the bot command 'applypotion' to rogue-usable potions (i.e., poisons)") RULE_CATEGORY_END() #endif diff --git a/zone/bot.cpp b/zone/bot.cpp index b9e329292..50620cdd5 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -236,8 +236,157 @@ Bot::Bot(uint32 botID, uint32 botOwnerCharacterID, uint32 botSpellsID, double to LoadAAs(); - if (!database.botdb.LoadBuffs(this) && bot_owner) + // copied from client CompleteConnect() handler - watch for problems + // (may have to move to post-spawn location if certain buffs still don't process correctly) + if (database.botdb.LoadBuffs(this) && bot_owner) { + + //reapply some buffs + uint32 buff_count = GetMaxTotalSlots(); + for (uint32 j1 = 0; j1 < buff_count; j1++) { + if (!IsValidSpell(buffs[j1].spellid)) + continue; + + const SPDat_Spell_Struct& spell = spells[buffs[j1].spellid]; + + int NimbusEffect = GetNimbusEffect(buffs[j1].spellid); + if (NimbusEffect) { + if (!IsNimbusEffectActive(NimbusEffect)) + SendSpellEffect(NimbusEffect, 500, 0, 1, 3000, true); + } + + for (int x1 = 0; x1 < EFFECT_COUNT; x1++) { + switch (spell.effectid[x1]) { + case SE_IllusionCopy: + case SE_Illusion: { + if (spell.base[x1] == -1) { + if (gender == 1) + gender = 0; + else if (gender == 0) + gender = 1; + SendIllusionPacket(GetRace(), gender, 0xFF, 0xFF); + } + else if (spell.base[x1] == -2) // WTF IS THIS + { + if (GetRace() == 128 || GetRace() == 130 || GetRace() <= 12) + SendIllusionPacket(GetRace(), GetGender(), spell.base2[x1], spell.max[x1]); + } + else if (spell.max[x1] > 0) + { + SendIllusionPacket(spell.base[x1], 0xFF, spell.base2[x1], spell.max[x1]); + } + else + { + SendIllusionPacket(spell.base[x1], 0xFF, 0xFF, 0xFF); + } + switch (spell.base[x1]) { + case OGRE: + SendAppearancePacket(AT_Size, 9); + break; + case TROLL: + SendAppearancePacket(AT_Size, 8); + break; + case VAHSHIR: + case BARBARIAN: + SendAppearancePacket(AT_Size, 7); + break; + case HALF_ELF: + case WOOD_ELF: + case DARK_ELF: + case FROGLOK: + SendAppearancePacket(AT_Size, 5); + break; + case DWARF: + SendAppearancePacket(AT_Size, 4); + break; + case HALFLING: + case GNOME: + SendAppearancePacket(AT_Size, 3); + break; + default: + SendAppearancePacket(AT_Size, 6); + break; + } + break; + } + //case SE_SummonHorse: { + // SummonHorse(buffs[j1].spellid); + // //hasmount = true; //this was false, is that the correct thing? + // break; + //} + case SE_Silence: + { + Silence(true); + break; + } + case SE_Amnesia: + { + Amnesia(true); + break; + } + case SE_DivineAura: + { + invulnerable = true; + break; + } + case SE_Invisibility2: + case SE_Invisibility: + { + invisible = true; + SendAppearancePacket(AT_Invis, 1); + break; + } + case SE_Levitate: + { + if (!zone->CanLevitate()) + { + //if (!GetGM()) + //{ + SendAppearancePacket(AT_Levitate, 0); + BuffFadeByEffect(SE_Levitate); + //Message(Chat::Red, "You can't levitate in this zone."); + //} + } + else { + SendAppearancePacket(AT_Levitate, 2); + } + break; + } + case SE_InvisVsUndead2: + case SE_InvisVsUndead: + { + invisible_undead = true; + break; + } + case SE_InvisVsAnimals: + { + invisible_animals = true; + break; + } + case SE_AddMeleeProc: + case SE_WeaponProc: + { + AddProcToWeapon(GetProcID(buffs[j1].spellid, x1), false, 100 + spells[buffs[j1].spellid].base2[x1], buffs[j1].spellid, buffs[j1].casterlevel); + break; + } + case SE_DefensiveProc: + { + AddDefensiveProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].base2[x1], buffs[j1].spellid); + break; + } + case SE_RangedProc: + { + AddRangedProc(GetProcID(buffs[j1].spellid, x1), 100 + spells[buffs[j1].spellid].base2[x1], buffs[j1].spellid); + break; + } + } + } + } + + + } + else { bot_owner->Message(Chat::Red, "&s for '%s'", BotDatabase::fail::LoadBuffs(), GetCleanName()); + } CalcBotStats(false); hp_regen = CalcHPRegen(); @@ -4726,9 +4875,9 @@ bool Bot::Death(Mob *killerMob, int32 damage, uint16 spell_id, EQEmu::skills::Sk Mob *my_owner = GetBotOwner(); if (my_owner && my_owner->IsClient() && my_owner->CastToClient()->GetBotOption(Client::booDeathMarquee)) { if (killerMob) - my_owner->CastToClient()->SendMarqueeMessage(Chat::Yellow, 510, 0, 1000, 3000, StringFormat("%s has been slain by %s", GetCleanName(), killerMob->GetCleanName())); + my_owner->CastToClient()->SendMarqueeMessage(Chat::Red, 510, 0, 1000, 3000, StringFormat("%s has been slain by %s", GetCleanName(), killerMob->GetCleanName())); else - my_owner->CastToClient()->SendMarqueeMessage(Chat::Yellow, 510, 0, 1000, 3000, StringFormat("%s has been slain", GetCleanName())); + my_owner->CastToClient()->SendMarqueeMessage(Chat::Red, 510, 0, 1000, 3000, StringFormat("%s has been slain", GetCleanName())); } Mob *give_exp = hate_list.GetDamageTopOnHateList(this); @@ -8989,6 +9138,20 @@ Bot* EntityList::GetBotByBotName(std::string botName) { return Result; } +Client* EntityList::GetBotOwnerByBotEntityID(uint16 entityID) { + Client* Result = nullptr; + if (entityID > 0) { + for (std::list::iterator botListItr = bot_list.begin(); botListItr != bot_list.end(); ++botListItr) { + Bot* tempBot = *botListItr; + if (tempBot && tempBot->GetID() == entityID) { + Result = tempBot->GetBotOwner()->CastToClient(); + break; + } + } + } + return Result; +} + void EntityList::AddBot(Bot *newBot, bool SendSpawnPacket, bool dontqueue) { if(newBot) { newBot->SetID(GetFreeID()); diff --git a/zone/bot_command.cpp b/zone/bot_command.cpp index 6a0ae2ff0..0f469a03c 100644 --- a/zone/bot_command.cpp +++ b/zone/bot_command.cpp @@ -1321,6 +1321,8 @@ int bot_command_init(void) if ( bot_command_add("actionable", "Lists actionable command arguments and use descriptions", 0, bot_command_actionable) || bot_command_add("aggressive", "Orders a bot to use a aggressive discipline", 0, bot_command_aggressive) || + bot_command_add("applypoison", "Applies cursor-held poison to a rogue bot's weapon", 0, bot_command_apply_poison) || + bot_command_add("applypotion", "Applies cursor-held potion to a bot's effects", 0, bot_command_apply_potion) || bot_command_add("attack", "Orders bots to attack a designated target", 0, bot_command_attack) || bot_command_add("bindaffinity", "Orders a bot to attempt an affinity binding", 0, bot_command_bind_affinity) || bot_command_add("bot", "Lists the available bot management [subcommands]", 0, bot_command_bot) || @@ -2579,6 +2581,166 @@ void bot_command_aggressive(Client *c, const Seperator *sep) c->Message(m_action, "%i of %i bots have used aggressive disciplines", success_count, candidate_count); } +void bot_command_apply_poison(Client *c, const Seperator *sep) +{ + if (helper_command_disabled(c, RuleB(Bots, AllowApplyPoisonCommand), "applypoison")) { + return; + } + if (helper_command_alias_fail(c, "bot_command_apply_poison", sep->arg[0], "applypoison")) { + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { + + c->Message(m_usage, "usage: %s", sep->arg[0]); + return; + } + + Bot *my_rogue_bot = nullptr; + if (c->GetTarget() && c->GetTarget()->IsBot() && c->GetTarget()->CastToBot()->GetBotOwnerCharacterID() == c->CharacterID() && c->GetTarget()->CastToBot()->GetClass() == ROGUE) { + my_rogue_bot = c->GetTarget()->CastToBot(); + } + if (!my_rogue_bot) { + + c->Message(m_fail, "You must target a rogue bot that you own to use this command!"); + return; + } + if (my_rogue_bot->GetLevel() < 18) { + + c->Message(m_fail, "Your rogue bot must be level 18 before %s can apply poison!", (my_rogue_bot->GetGender() == 1 ? "she" : "he")); + return; + } + + const auto poison_instance = c->GetInv().GetItem(EQEmu::invslot::slotCursor); + if (!poison_instance) { + + c->Message(m_fail, "No item found on cursor!"); + return; + } + + auto poison_data = poison_instance->GetItem(); + if (!poison_data) { + + c->Message(m_fail, "No data found for cursor item!"); + return; + } + + if (poison_data->ItemType == EQEmu::item::ItemTypePoison) { + + if ((~poison_data->Races) & GetPlayerRaceBit(my_rogue_bot->GetRace())) { + + c->Message(m_fail, "Invalid race for weapon poison!"); + return; + } + + if (poison_data->Proc.Level2 > my_rogue_bot->GetLevel()) { + + c->Message(m_fail, "This poison is too powerful for your intended target!"); + return; + } + + // generalized from client ApplyPoison handler + double ChanceRoll = zone->random.Real(0, 1); + uint16 poison_skill = 95 + ((my_rogue_bot->GetLevel() - 18) * 5); + if (poison_skill > 200) { + poison_skill = 200; + } + bool apply_poison_chance = (ChanceRoll < (.75 + poison_skill / 1000)); + + if (apply_poison_chance && my_rogue_bot->AddProcToWeapon(poison_data->Proc.Effect, false, (my_rogue_bot->GetDEX() / 100) + 103, POISON_PROC)) { + c->Message(m_action, "Successfully applied %s to %s's weapon.", poison_data->Name, my_rogue_bot->GetCleanName()); + } + else { + c->Message(m_fail, "Failed to apply %s to %s's weapon.", poison_data->Name, my_rogue_bot->GetCleanName()); + } + + c->DeleteItemInInventory(EQEmu::invslot::slotCursor, 1, true); + } + else { + + c->Message(m_fail, "Item on cursor is not a weapon poison!"); + return; + } +} + +void bot_command_apply_potion(Client* c, const Seperator* sep) +{ + if (helper_command_disabled(c, RuleB(Bots, AllowApplyPotionCommand), "applypotion")) { + return; + } + if (helper_command_alias_fail(c, "bot_command_apply_potion", sep->arg[0], "applypotion")) { + return; + } + if (helper_is_help_or_usage(sep->arg[1])) { + + c->Message(m_usage, "usage: %s", sep->arg[0]); + return; + } + + Bot* my_bot = nullptr; + if (c->GetTarget() && c->GetTarget()->IsBot() && c->GetTarget()->CastToBot()->GetBotOwnerCharacterID() == c->CharacterID()) { + my_bot = c->GetTarget()->CastToBot(); + } + if (!my_bot) { + + c->Message(m_fail, "You must target a bot that you own to use this command!"); + return; + } + + const auto potion_instance = c->GetInv().GetItem(EQEmu::invslot::slotCursor); + if (!potion_instance) { + + c->Message(m_fail, "No item found on cursor!"); + return; + } + + auto potion_data = potion_instance->GetItem(); + if (!potion_data) { + + c->Message(m_fail, "No data found for cursor item!"); + return; + } + + if (potion_data->ItemType == EQEmu::item::ItemTypePotion && potion_data->Click.Effect > 0) { + + if (RuleB(Bots, RestrictApplyPotionToRogue) && potion_data->Classes != PLAYER_CLASS_ROGUE_BIT) { + + c->Message(m_fail, "This command is restricted to rogue poison potions only!"); + return; + } + if ((~potion_data->Races) & GetPlayerRaceBit(my_bot->GetRace())) { + + c->Message(m_fail, "Invalid race for potion!"); + return; + } + if ((~potion_data->Classes) & GetPlayerClassBit(my_bot->GetClass())) { + + c->Message(m_fail, "Invalid class for potion!"); + return; + } + + if (potion_data->Click.Level2 > my_bot->GetLevel()) { + + c->Message(m_fail, "This potion is too powerful for your intended target!"); + return; + } + + // TODO: figure out best way to handle casting time/animation + if (my_bot->SpellFinished(potion_data->Click.Effect, my_bot, EQEmu::spells::CastingSlot::Item, 0)) { + c->Message(m_action, "Successfully applied %s to %s's buff effects.", potion_data->Name, my_bot->GetCleanName()); + } + else { + c->Message(m_fail, "Failed to apply %s to %s's buff effects.", potion_data->Name, my_bot->GetCleanName()); + } + + c->DeleteItemInInventory(EQEmu::invslot::slotCursor, 1, true); + } + else { + + c->Message(m_fail, "Item on cursor is not a potion!"); + return; + } +} + void bot_command_attack(Client *c, const Seperator *sep) { if (helper_command_alias_fail(c, "bot_command_attack", sep->arg[0], "attack")) { @@ -3637,6 +3799,16 @@ void bot_command_owner_option(Client *c, const Seperator *sep) "null" "(toggles)" "" + "" + "buffcounter" + "enable | disable" + "marquee message on buff counter change" + "" + "" + "" + "null" + "(toggles)" + "" "" "current" "" @@ -3796,6 +3968,22 @@ void bot_command_owner_option(Client *c, const Seperator *sep) c->Message(m_fail, "Bot owner option 'autodefend' is not allowed on this server."); } } + else if (!owner_option.compare("buffcounter")) { + + if (!argument.compare("enable")) { + c->SetBotOption(Client::booBuffCounter, true); + } + else if (!argument.compare("disable")) { + c->SetBotOption(Client::booBuffCounter, false); + } + else { + c->SetBotOption(Client::booBuffCounter, !c->GetBotOption(Client::booBuffCounter)); + } + + database.botdb.SaveOwnerOption(c->CharacterID(), Client::booBuffCounter, c->GetBotOption(Client::booBuffCounter)); + + c->Message(m_action, "Bot 'buff counter' is now %s.", (c->GetBotOption(Client::booBuffCounter) == true ? "enabled" : "disabled")); + } else if (!owner_option.compare("current")) { std::string window_title = "Current Bot Owner Options Settings"; @@ -3811,13 +3999,15 @@ void bot_command_owner_option(Client *c, const Seperator *sep) "" "spawnmessage" "{}" "" "" "altcombat" "{}" "" "" "autodefend" "{}" "" + "" "buffcounter" "{}" "" "", (c->GetBotOption(Client::booDeathMarquee) ? "enabled" : "disabled"), (c->GetBotOption(Client::booStatsUpdate) ? "enabled" : "disabled"), (c->GetBotOption(Client::booSpawnMessageSay) ? "say" : (c->GetBotOption(Client::booSpawnMessageTell) ? "tell" : "silent")), (c->GetBotOption(Client::booSpawnMessageClassSpecific) ? "class" : "default"), (RuleB(Bots, AllowOwnerOptionAltCombat) ? (c->GetBotOption(Client::booAltCombat) ? "enabled" : "disabled") : "restricted"), - (RuleB(Bots, AllowOwnerOptionAutoDefend) ? (c->GetBotOption(Client::booAutoDefend) ? "enabled" : "disabled") : "restricted") + (RuleB(Bots, AllowOwnerOptionAutoDefend) ? (c->GetBotOption(Client::booAutoDefend) ? "enabled" : "disabled") : "restricted"), + (c->GetBotOption(Client::booBuffCounter) ? "enabled" : "disabled") ); c->SendPopupToClient(window_title.c_str(), window_text.c_str()); @@ -8444,6 +8634,16 @@ bool helper_cast_standard_spell(Bot* casting_bot, Mob* target_mob, int spell_id, return casting_bot->CastSpell(spell_id, target_mob->GetID(), EQEmu::spells::CastingSlot::Gem2, -1, -1, dont_root_before); } +bool helper_command_disabled(Client* bot_owner, bool rule_value, const char* command) +{ + if (rule_value == false) { + bot_owner->Message(m_fail, "Bot command %s is not enabled on this server.", command); + return true; + } + + return false; +} + bool helper_command_alias_fail(Client *bot_owner, const char* command_handler, const char *alias, const char *command) { auto alias_iter = bot_command_aliases.find(&alias[1]); diff --git a/zone/bot_command.h b/zone/bot_command.h index d710e106f..b3f328a33 100644 --- a/zone/bot_command.h +++ b/zone/bot_command.h @@ -553,6 +553,8 @@ void bot_command_log_command(Client *c, const char *message); // bot commands void bot_command_actionable(Client *c, const Seperator *sep); void bot_command_aggressive(Client *c, const Seperator *sep); +void bot_command_apply_poison(Client *c, const Seperator *sep); +void bot_command_apply_potion(Client* c, const Seperator* sep); void bot_command_attack(Client *c, const Seperator *sep); void bot_command_bind_affinity(Client *c, const Seperator *sep); void bot_command_bot(Client *c, const Seperator *sep); @@ -671,6 +673,7 @@ void helper_bot_appearance_form_update(Bot *my_bot); uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_class, uint16 bot_race, uint8 bot_gender); void helper_bot_out_of_combat(Client *bot_owner, Bot *my_bot); bool helper_cast_standard_spell(Bot* casting_bot, Mob* target_mob, int spell_id, bool annouce_cast = true, uint32* dont_root_before = nullptr); +bool helper_command_disabled(Client *bot_owner, bool rule_value, const char *command); bool helper_command_alias_fail(Client *bot_owner, const char* command_handler, const char *alias, const char *command); void helper_command_depart_list(Client* bot_owner, Bot* druid_bot, Bot* wizard_bot, bcst_list* local_list, bool single_flag = false); bool helper_is_help_or_usage(const char* arg); diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index 8496dfcd8..2f6f9c1c6 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -2256,6 +2256,7 @@ bool BotDatabase::SaveOwnerOption(const uint32 owner_id, size_t type, const bool case Client::booSpawnMessageClassSpecific: case Client::booAltCombat: case Client::booAutoDefend: + case Client::booBuffCounter: { query = fmt::format( "REPLACE INTO `bot_owner_options`(`owner_id`, `option_type`, `option_value`) VALUES ('{}', '{}', '{}')", diff --git a/zone/client.cpp b/zone/client.cpp index dae6c8912..911c090dc 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -356,6 +356,7 @@ Client::Client(EQStreamInterface* ieqs) bot_owner_options[booSpawnMessageClassSpecific] = true; bot_owner_options[booAltCombat] = RuleB(Bots, AllowOwnerOptionAltCombat); bot_owner_options[booAutoDefend] = RuleB(Bots, AllowOwnerOptionAutoDefend); + bot_owner_options[booBuffCounter] = false; SetBotPulling(false); SetBotPrecombat(false); diff --git a/zone/client.h b/zone/client.h index 6fa4835c9..82b8c7475 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1640,6 +1640,7 @@ public: booSpawnMessageClassSpecific, booAltCombat, booAutoDefend, + booBuffCounter, _booCount }; diff --git a/zone/entity.h b/zone/entity.h index 6b22f1a4c..7631f0532 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -553,6 +553,7 @@ private: Mob* GetMobByBotID(uint32 botID); Bot* GetBotByBotID(uint32 botID); Bot* GetBotByBotName(std::string botName); + Client* GetBotOwnerByBotEntityID(uint16 entityID); std::list GetBotsByBotOwnerCharacterID(uint32 botOwnerCharacterID); bool Bot_AICheckCloseBeneficialSpells(Bot* caster, uint8 iChance, float iRange, uint32 iSpellTypes); // TODO: Evaluate this closesly in hopes to eliminate diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 406a801d0..2014a643a 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -5662,11 +5662,24 @@ void Mob::CheckNumHitsRemaining(NumHit type, int32 buff_slot, uint16 spell_id) bool bDepleted = false; int buff_max = GetMaxTotalSlots(); +#ifdef BOTS + std::string buff_name; + size_t buff_counter = 0; + bool buff_update = false; +#endif + //Spell specific procs [Type 7,10,11] if (IsValidSpell(spell_id)) { for (int d = 0; d < buff_max; d++) { if (buffs[d].spellid == spell_id && buffs[d].numhits > 0 && spells[buffs[d].spellid].numhitstype == static_cast(type)) { + +#ifdef BOTS + buff_name = spells[buffs[d].spellid].name; + buff_counter = (buffs[d].numhits - 1); + buff_update = true; +#endif + if (--buffs[d].numhits == 0) { CastOnNumHitFade(buffs[d].spellid); if (!TryFadeEffect(d)) @@ -5679,6 +5692,13 @@ void Mob::CheckNumHitsRemaining(NumHit type, int32 buff_slot, uint16 spell_id) } else if (type == NumHit::MatchingSpells) { if (buff_slot >= 0) { if (--buffs[buff_slot].numhits == 0) { + +#ifdef BOTS + buff_name = spells[buffs[buff_slot].spellid].name; + buff_counter = (buffs[buff_slot].numhits - 1); + buff_update = true; +#endif + CastOnNumHitFade(buffs[buff_slot].spellid); if (!TryFadeEffect(buff_slot)) BuffFadeBySlot(buff_slot , true); @@ -5691,6 +5711,13 @@ void Mob::CheckNumHitsRemaining(NumHit type, int32 buff_slot, uint16 spell_id) continue; if (IsValidSpell(buffs[d].spellid) && m_spellHitsLeft[d] == buffs[d].spellid) { + +#ifdef BOTS + buff_name = spells[buffs[d].spellid].name; + buff_counter = (buffs[d].numhits - 1); + buff_update = true; +#endif + if (--buffs[d].numhits == 0) { CastOnNumHitFade(buffs[d].spellid); m_spellHitsLeft[d] = 0; @@ -5706,6 +5733,13 @@ void Mob::CheckNumHitsRemaining(NumHit type, int32 buff_slot, uint16 spell_id) for (int d = 0; d < buff_max; d++) { if (IsValidSpell(buffs[d].spellid) && buffs[d].numhits > 0 && spells[buffs[d].spellid].numhitstype == static_cast(type)) { + +#ifdef BOTS + buff_name = spells[buffs[d].spellid].name; + buff_counter = (buffs[d].numhits - 1); + buff_update = true; +#endif + if (--buffs[d].numhits == 0) { CastOnNumHitFade(buffs[d].spellid); if (!TryFadeEffect(d)) @@ -5716,6 +5750,28 @@ void Mob::CheckNumHitsRemaining(NumHit type, int32 buff_slot, uint16 spell_id) } } } + +#ifdef BOTS + if (IsBot() && buff_update) { + auto bot_owner = entity_list.GetBotOwnerByBotEntityID(GetID()); + if (bot_owner && bot_owner->GetBotOption(Client::booBuffCounter)) { + bot_owner->CastToClient()->SendMarqueeMessage( + Chat::Yellow, + 510, + 0, + 1000, + 3000, + StringFormat( + "%s has [%u] hit%s remaining on '%s'", + GetCleanName(), + buff_counter, + (buff_counter == 1 ? "" : "s"), + buff_name.c_str() + ) + ); + } + } +#endif } //for some stupid reason SK procs return theirs one base off...