mirror of
https://github.com/EQEmu/Server.git
synced 2026-05-16 18:52:22 +00:00
[Bots] Add support for Bot scripting. (#2515)
* [Bots] Add support for Bot scripting. # Perl - Add support for `zone/bot.pl` and `zone/bot_v#.pl`. - Add support for `global/global_bot.pl`. - Add `$bot->SignalBot(signal_id)` to Perl. - Add `$bot->OwnerMessage(message)` to Perl. - Add `$entity_list->SignalAllBotsByOwnerCharacterID(character_id, signal_id)` to Perl. - Add `$entity_list->SignalBotByBotID(bot_id, signal_id)` to Perl. - Add `$entity_list->SignalBotByBotName(bot_name, signal_id)` to Perl. - Add `EVENT_SPELL_EFFECT_BOT` to Perl. - Add `EVENT_SPELL_EFFECT_BUFF_TIC_BOT` to Perl. # Lua - Add support for `zone/bot.lua` and `zone/bot_v#.lua`. - Add support for `global/global_bot.lua`. - Add `bot:SignalBot(signal_id)` to Lua. - Add `bot:OwnerMessage(message)` to Lua. - Add `entity_list:SignalAllBotsByOwnerCharacterID(character_id, signal_id)` to Lua. - Add `entity_list:SignalBotByBotID(bot_id, signal_id)` to Lua. - Add `entity_list:SignalBotByBotName(bot_name, signal_id)` to Lua. - Add `EVENT_SPELL_EFFECT_BOT` to Lua. - Add `EVENT_SPELL_EFFECT_BUFF_TIC_BOT` to Lua. # Supported Bot Events 1. `EVENT_CAST` 2. `EVENT_CAST_BEGIN` 3. `EVENT_CAST_ON` 4. `EVENT_COMBAT` 5. `EVENT_DEATH` 6. `EVENT_DEATH_COMPLETE` 7. `EVENT_SAY` 8. `EVENT_SIGNAL` 9. `EVENT_SLAY` 10. `EVENT_SLAY_NPC` 11. `EVENT_SPAWN` 12. `EVENT_TARGET_CHANGE` 13. `EVENT_TIMER` 14. `EVENT_USE_SKILL` # Common - Convert NPC pointers in common events to Mob pointers so bots are supported. - Convert signal IDs to `int` where it wasn't already, allowing negative signals to be sent properly. * Add EVENT_POPUP_RESPONSE. * Cleanup and fix EVENT_COMBAT/EVENT_SLAY/EVENT_NPC_SLAY. * Fix DoNPCEmote calls. * Update attack.cpp * Update event_codes.h * Update bot_command.cpp
This commit is contained in:
+157
-68
@@ -2663,7 +2663,13 @@ void Bot::AI_Process()
|
||||
auto pull_target = bot_owner->GetTarget();
|
||||
if (pull_target) {
|
||||
|
||||
Bot::BotGroupSay(this, "Pulling %s to the group..", pull_target->GetCleanName());
|
||||
BotGroupSay(
|
||||
this,
|
||||
fmt::format(
|
||||
"Pulling {}.",
|
||||
pull_target->GetCleanName()
|
||||
).c_str()
|
||||
);
|
||||
InterruptSpell();
|
||||
WipeHateList();
|
||||
AddToHateList(pull_target, 1);
|
||||
@@ -2822,14 +2828,20 @@ void Bot::AI_Process()
|
||||
bool find_target = true;
|
||||
|
||||
if (assist_mob) {
|
||||
|
||||
if (assist_mob->GetTarget()) {
|
||||
|
||||
if (assist_mob != this) {
|
||||
if (GetTarget() != assist_mob->GetTarget()) {
|
||||
SetTarget(assist_mob->GetTarget());
|
||||
}
|
||||
|
||||
SetTarget(assist_mob->GetTarget());
|
||||
if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) {
|
||||
|
||||
if (
|
||||
HasPet() &&
|
||||
(
|
||||
GetClass() != ENCHANTER ||
|
||||
GetPet()->GetPetType() != petAnimation ||
|
||||
GetAA(aaAnimationEmpathy) >= 2
|
||||
)
|
||||
) {
|
||||
// This artificially inflates pet's target aggro..but, less expensive than checking hate each AI process
|
||||
GetPet()->AddToHateList(assist_mob->GetTarget(), 1);
|
||||
GetPet()->SetTarget(assist_mob->GetTarget());
|
||||
@@ -2837,12 +2849,19 @@ void Bot::AI_Process()
|
||||
}
|
||||
|
||||
find_target = false;
|
||||
}
|
||||
else if (assist_mob != this) {
|
||||
|
||||
SetTarget(nullptr);
|
||||
if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) {
|
||||
} else if (assist_mob != this) {
|
||||
if (GetTarget()) {
|
||||
SetTarget(nullptr);
|
||||
}
|
||||
|
||||
if (
|
||||
HasPet() &&
|
||||
(
|
||||
GetClass() != ENCHANTER ||
|
||||
GetPet()->GetPetType() != petAnimation ||
|
||||
GetAA(aaAnimationEmpathy) >= 1
|
||||
)
|
||||
) {
|
||||
GetPet()->WipeHateList();
|
||||
GetPet()->SetTarget(nullptr);
|
||||
}
|
||||
@@ -2852,16 +2871,23 @@ void Bot::AI_Process()
|
||||
}
|
||||
|
||||
if (find_target) {
|
||||
|
||||
if (IsRooted()) {
|
||||
SetTarget(hate_list.GetClosestEntOnHateList(this, true));
|
||||
}
|
||||
else {
|
||||
|
||||
auto closest = hate_list.GetClosestEntOnHateList(this, true);
|
||||
if (closest) {
|
||||
SetTarget(closest);
|
||||
}
|
||||
} else {
|
||||
// This will keep bots on target for now..but, future updates will allow for rooting/stunning
|
||||
SetTarget(hate_list.GetEscapingEntOnHateList(leash_owner, leash_distance));
|
||||
auto escaping = hate_list.GetEscapingEntOnHateList(leash_owner, leash_distance);
|
||||
if (escaping) {
|
||||
SetTarget(escaping);
|
||||
}
|
||||
|
||||
if (!GetTarget()) {
|
||||
SetTarget(hate_list.GetEntWithMostHateOnList(this, nullptr, true));
|
||||
auto most_hate = hate_list.GetEntWithMostHateOnList(this, nullptr, true);
|
||||
if (most_hate) {
|
||||
SetTarget(most_hate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2884,8 +2910,10 @@ void Bot::AI_Process()
|
||||
|
||||
Mob* tar = GetTarget(); // We should have a target..if not, we're awaiting new orders
|
||||
if (!tar || PASSIVE) {
|
||||
if (GetTarget()) {
|
||||
SetTarget(nullptr);
|
||||
}
|
||||
|
||||
SetTarget(nullptr);
|
||||
WipeHateList();
|
||||
SetAttackFlag(false);
|
||||
SetAttackingFlag(false);
|
||||
@@ -4929,12 +4957,10 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client*
|
||||
//}
|
||||
|
||||
if (!database.botdb.DeleteItemBySlot(GetBotID(), return_iterator.from_bot_slot)) {
|
||||
client->Message(
|
||||
Chat::White,
|
||||
OwnerMessage(
|
||||
fmt::format(
|
||||
"Failed to delete item by slot from slot {} for {}.",
|
||||
return_iterator.from_bot_slot,
|
||||
GetCleanName()
|
||||
"Failed to delete item by slot from slot {}.",
|
||||
return_iterator.from_bot_slot
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
@@ -4947,13 +4973,11 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client*
|
||||
linker.SetItemInst(return_instance);
|
||||
auto item_link = linker.GenerateLink();
|
||||
|
||||
client->Message(
|
||||
Chat::Tell,
|
||||
OwnerMessage(
|
||||
fmt::format(
|
||||
"{} tells you, 'I have returned {}.'",
|
||||
GetCleanName(),
|
||||
"I have returned {}.",
|
||||
item_link
|
||||
).c_str()
|
||||
)
|
||||
);
|
||||
|
||||
client->PutItemInInventory(return_iterator.to_client_slot, *return_instance, true);
|
||||
@@ -4969,12 +4993,10 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client*
|
||||
// TODO: code for stackables
|
||||
|
||||
if (!database.botdb.SaveItemBySlot(this, trade_iterator.to_bot_slot, trade_iterator.trade_item_instance)) {
|
||||
client->Message(
|
||||
Chat::White,
|
||||
OwnerMessage(
|
||||
fmt::format(
|
||||
"Failed to save item by slot to slot {} for {}.",
|
||||
trade_iterator.to_bot_slot,
|
||||
GetCleanName()
|
||||
"Failed to save item by slot to slot {}.",
|
||||
trade_iterator.to_bot_slot
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
@@ -4984,13 +5006,11 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client*
|
||||
linker.SetItemInst(trade_iterator.trade_item_instance);
|
||||
auto item_link = linker.GenerateLink();
|
||||
|
||||
client->Message(
|
||||
Chat::Tell,
|
||||
OwnerMessage(
|
||||
fmt::format(
|
||||
"{} tells you, 'I have accepted {}.'",
|
||||
GetCleanName(),
|
||||
"I have accepted {}.",
|
||||
item_link
|
||||
).c_str()
|
||||
)
|
||||
);
|
||||
|
||||
m_inv.PutItem(trade_iterator.to_bot_slot, *trade_iterator.trade_item_instance);
|
||||
@@ -5101,6 +5121,16 @@ bool Bot::Death(Mob *killerMob, int64 damage, uint16 spell_id, EQ::skills::Skill
|
||||
my_owner->CastToClient()->SetBotPulling(false);
|
||||
}
|
||||
|
||||
const auto export_string = fmt::format(
|
||||
"{} {} {} {}",
|
||||
killerMob ? killerMob->GetID() : 0,
|
||||
damage,
|
||||
spell_id,
|
||||
static_cast<int>(attack_skill)
|
||||
);
|
||||
|
||||
parse->EventBot(EVENT_DEATH_COMPLETE, this, killerMob, export_string, 0);
|
||||
|
||||
entity_list.RemoveBot(GetID());
|
||||
return true;
|
||||
}
|
||||
@@ -5112,7 +5142,7 @@ void Bot::Damage(Mob *from, int64 damage, uint16 spell_id, EQ::skills::SkillType
|
||||
//handle EVENT_ATTACK. Resets after we have not been attacked for 12 seconds
|
||||
if(attacked_timer.Check()) {
|
||||
LogCombat("Triggering EVENT_ATTACK due to attack by [{}]", from->GetName());
|
||||
parse->EventNPC(EVENT_ATTACK, this, from, "", 0);
|
||||
parse->EventBot(EVENT_ATTACK, this, from, "", 0);
|
||||
}
|
||||
|
||||
attacked_timer.Start(CombatEventTimer_expire);
|
||||
@@ -6554,7 +6584,13 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) {
|
||||
|
||||
if(taunting && target && target->IsNPC() && taunt_time) {
|
||||
if(GetTarget() && GetTarget()->GetHateTop() && GetTarget()->GetHateTop() != this) {
|
||||
BotGroupSay(this, "Taunting %s", target->GetCleanName());
|
||||
BotGroupSay(
|
||||
this,
|
||||
fmt::format(
|
||||
"Taunting {}.",
|
||||
target->GetCleanName()
|
||||
).c_str()
|
||||
);
|
||||
Taunt(target->CastToNPC(), false);
|
||||
taunt_timer.Start(TauntReuseTime * 1000);
|
||||
}
|
||||
@@ -7512,27 +7548,49 @@ void Bot::DoBuffTic(const Buffs_Struct &buff, int slot, Mob* caster) {
|
||||
Mob::DoBuffTic(buff, slot, caster);
|
||||
}
|
||||
|
||||
bool Bot::CastSpell(uint16 spell_id, uint16 target_id, EQ::spells::CastingSlot slot, int32 cast_time, int32 mana_cost,
|
||||
uint32* oSpellWillFinish, uint32 item_slot, int16 *resist_adjust, uint32 aa_id) {
|
||||
bool Bot::CastSpell(
|
||||
uint16 spell_id,
|
||||
uint16 target_id,
|
||||
EQ::spells::CastingSlot slot,
|
||||
int32 cast_time,
|
||||
int32 mana_cost,
|
||||
uint32* oSpellWillFinish,
|
||||
uint32 item_slot,
|
||||
int16 *resist_adjust,
|
||||
uint32 aa_id
|
||||
) {
|
||||
bool Result = false;
|
||||
if(zone && !zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))) {
|
||||
if (zone && !zone->IsSpellBlocked(spell_id, glm::vec3(GetPosition()))) {
|
||||
// LogSpells("CastSpell called for spell [{}] ([{}]) on entity [{}], slot [{}], time [{}], mana [{}], from item slot [{}]", spells[spell_id].name, spell_id, target_id, slot, cast_time, mana_cost, (item_slot==0xFFFFFFFF)?999:item_slot);
|
||||
|
||||
if(casting_spell_id == spell_id)
|
||||
if (casting_spell_id == spell_id) {
|
||||
ZeroCastingVars();
|
||||
}
|
||||
|
||||
if(GetClass() != BARD) {
|
||||
if(!IsValidSpell(spell_id) || casting_spell_id || delaytimer || spellend_timer.Enabled() || IsStunned() || IsFeared() || IsMezzed() || (IsSilenced() && !IsDiscipline(spell_id)) || (IsAmnesiad() && IsDiscipline(spell_id))) {
|
||||
if (GetClass() != BARD) {
|
||||
if (
|
||||
!IsValidSpell(spell_id) ||
|
||||
casting_spell_id ||
|
||||
delaytimer ||
|
||||
spellend_timer.Enabled() ||
|
||||
IsStunned() ||
|
||||
IsFeared() ||
|
||||
IsMezzed() ||
|
||||
(IsSilenced() && !IsDiscipline(spell_id)) ||
|
||||
(IsAmnesiad() && IsDiscipline(spell_id))
|
||||
) {
|
||||
LogSpells("Spell casting canceled: not able to cast now. Valid? [{}], casting [{}], waiting? [{}], spellend? [{}], stunned? [{}], feared? [{}], mezed? [{}], silenced? [{}]", IsValidSpell(spell_id), casting_spell_id, delaytimer, spellend_timer.Enabled(), IsStunned(), IsFeared(), IsMezzed(), IsSilenced() );
|
||||
if(IsSilenced() && !IsDiscipline(spell_id))
|
||||
if (IsSilenced() && !IsDiscipline(spell_id)) {
|
||||
MessageString(Chat::White, SILENCED_STRING);
|
||||
}
|
||||
|
||||
if(IsAmnesiad() && IsDiscipline(spell_id))
|
||||
|
||||
if (IsAmnesiad() && IsDiscipline(spell_id)) {
|
||||
MessageString(Chat::White, MELEE_SILENCE);
|
||||
}
|
||||
|
||||
if(casting_spell_id)
|
||||
if (casting_spell_id) {
|
||||
AI_Bot_Event_SpellCastFinished(false, static_cast<uint16>(casting_spell_slot));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -7540,19 +7598,20 @@ bool Bot::CastSpell(uint16 spell_id, uint16 target_id, EQ::spells::CastingSlot s
|
||||
|
||||
if (IsDetrimentalSpell(spell_id) && !zone->CanDoCombat()) {
|
||||
MessageString(Chat::White, SPELL_WOULDNT_HOLD);
|
||||
if(casting_spell_id)
|
||||
if (casting_spell_id) {
|
||||
AI_Bot_Event_SpellCastFinished(false, static_cast<uint16>(casting_spell_slot));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if(DivineAura()) {
|
||||
if (DivineAura()) {
|
||||
LogSpells("Spell casting canceled: cannot cast while Divine Aura is in effect");
|
||||
InterruptSpell(173, 0x121, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(slot < EQ::spells::CastingSlot::MaxGems && !CheckFizzle(spell_id)) {
|
||||
if (slot < EQ::spells::CastingSlot::MaxGems && !CheckFizzle(spell_id)) {
|
||||
int fizzle_msg = IsBardSong(spell_id) ? MISS_NOTE : SPELL_FIZZLE;
|
||||
InterruptSpell(fizzle_msg, 0x121, spell_id);
|
||||
|
||||
@@ -7878,7 +7937,13 @@ bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, EQ::spe
|
||||
bool Bot::DoFinishedSpellGroupTarget(uint16 spell_id, Mob* spellTarget, EQ::spells::CastingSlot slot, bool& stopLogic) {
|
||||
bool isMainGroupMGB = false;
|
||||
if(isMainGroupMGB && (GetClass() != BARD)) {
|
||||
BotGroupSay(this, "MGB %s", spells[spell_id].name);
|
||||
BotGroupSay(
|
||||
this,
|
||||
fmt::format(
|
||||
"Casting {} as a Mass Group Buff.",
|
||||
spells[spell_id].name
|
||||
).c_str()
|
||||
);
|
||||
SpellOnTarget(spell_id, this);
|
||||
entity_list.AESpell(this, this, spell_id, true);
|
||||
} else {
|
||||
@@ -9564,28 +9629,30 @@ Client* EntityList::GetBotOwnerByBotEntityID(uint16 entityID) {
|
||||
return Result;
|
||||
}
|
||||
|
||||
void EntityList::AddBot(Bot *newBot, bool SendSpawnPacket, bool dontqueue) {
|
||||
if(newBot) {
|
||||
newBot->SetID(GetFreeID());
|
||||
newBot->SetSpawned();
|
||||
if(SendSpawnPacket) {
|
||||
if(dontqueue) {
|
||||
void EntityList::AddBot(Bot *new_bot, bool send_spawn_packet, bool dont_queue) {
|
||||
if (new_bot) {
|
||||
new_bot->SetID(GetFreeID());
|
||||
new_bot->SetSpawned();
|
||||
if (send_spawn_packet) {
|
||||
if (dont_queue) {
|
||||
EQApplicationPacket* outapp = new EQApplicationPacket();
|
||||
newBot->CreateSpawnPacket(outapp);
|
||||
new_bot->CreateSpawnPacket(outapp);
|
||||
outapp->priority = 6;
|
||||
QueueClients(newBot, outapp, true);
|
||||
QueueClients(new_bot, outapp, true);
|
||||
safe_delete(outapp);
|
||||
} else {
|
||||
NewSpawn_Struct* ns = new NewSpawn_Struct;
|
||||
memset(ns, 0, sizeof(NewSpawn_Struct));
|
||||
newBot->FillSpawnStruct(ns, newBot);
|
||||
AddToSpawnQueue(newBot->GetID(), &ns);
|
||||
new_bot->FillSpawnStruct(ns, new_bot);
|
||||
AddToSpawnQueue(new_bot->GetID(), &ns);
|
||||
safe_delete(ns);
|
||||
}
|
||||
parse->EventNPC(EVENT_SPAWN, newBot, nullptr, "", 0);
|
||||
|
||||
parse->EventBot(EVENT_SPAWN, new_bot, nullptr, "", 0);
|
||||
}
|
||||
bot_list.push_back(newBot);
|
||||
mob_list.insert(std::pair<uint16, Mob*>(newBot->GetID(), newBot));
|
||||
|
||||
bot_list.push_back(new_bot);
|
||||
mob_list.insert(std::pair<uint16, Mob*>(new_bot->GetID(), new_bot));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10379,6 +10446,28 @@ void Bot::SpawnBotGroupByName(Client* c, std::string botgroup_name, uint32 leade
|
||||
);
|
||||
}
|
||||
|
||||
void Bot::SignalBot(int signal_id)
|
||||
{
|
||||
const auto export_string = fmt::format("{}", signal_id);
|
||||
parse->EventBot(EVENT_SIGNAL, this, nullptr, export_string, 0);
|
||||
}
|
||||
|
||||
void Bot::OwnerMessage(std::string message)
|
||||
{
|
||||
if (!GetBotOwner() || !GetBotOwner()->IsClient()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GetBotOwner()->Message(
|
||||
Chat::Tell,
|
||||
fmt::format(
|
||||
"{} tells you, '{}'",
|
||||
GetCleanName(),
|
||||
message
|
||||
).c_str()
|
||||
);
|
||||
}
|
||||
|
||||
bool Bot::GetBotOwnerDataBuckets()
|
||||
{
|
||||
auto bot_owner = GetBotOwner();
|
||||
|
||||
Reference in New Issue
Block a user