#include "AzerothAI.h" #include "Entities/Player.h" #include "World/World.h" #include "Server/WorldSession.h" #include "Server/DBCStores.h" #include "Globals/ObjectMgr.h" #include "Globals/SharedDefines.h" #include "Groups/Group.h" #include "Config/Config.h" #include "Database/DatabaseEnv.h" #include "playerbot/PlayerbotAI.h" #include "playerbot/RandomPlayerbotMgr.h" #include #include #include #include #include #include namespace { struct Resident { uint32_t guid, account; std::string name; bool armor; }; std::map roster; std::atomic armorGuid{0}, armorAccount{0}; std::string sql(std::string text) { CharacterDatabase.escape_string(text); return text; } std::string quoted(const std::string& text) { std::ostringstream out; out << '"'; for (unsigned char c : text) { if (c == '"' || c == '\\') out << '\\' << c; else if (c < 32) out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << unsigned(c); else out << c; } out << '"'; return out.str(); } bool identified(Player* p) { if (!p || !p->IsInWorld()) return false; auto r = roster.find(p->GetGUIDLow()); return r != roster.end() && p->GetName() == r->second.name && p->GetSession()->GetAccountId() == r->second.account && p->GetPlayerbotAI(); } bool managed(Player* p) { return identified(p) && !p->GetPlayerbotAI()->IsRealPlayer() && !p->GetPlayerbotAI()->HasRealPlayerMaster(); } bool safeGroup(Player* p) { Group* g = p->GetGroup(); if (!g) return true; unsigned count = 0; for (GroupReference* r = g->GetFirstMember(); r; r = r->next()) { if (!managed(r->getSource())) return false; ++count; } return count == g->GetMembersCount(); } bool free(Player* p) { return managed(p) && p->IsAlive() && !p->IsInCombat() && !p->IsTaxiFlying() && !p->IsNonMeleeSpellCasted(false); } bool nearby(Player* a, Player* b, float range) { return free(a) && free(b) && a->GetTeam() == b->GetTeam() && a->GetMapId() == b->GetMapId() && a->GetDistance(b) <= range; } void receipt(const std::string& id, bool ok, const std::string& result) { CharacterDatabase.PExecute("UPDATE azerothai_commands SET status='%s',result='%s' WHERE id='%s' AND status='processing'", ok ? "completed" : "failed", sql(result).c_str(), sql(id).c_str()); } void commands() { auto q = CharacterDatabase.Query("SELECT id,guid,account_id,name,kind,argument,peer_guid,created_at,peer_account_id,peer_name,expires_at FROM azerothai_commands WHERE status='pending' ORDER BY created_at LIMIT 8"); if (!q) return; do { Field* f=q->Fetch(); std::string id=f[0].GetString(), kind=f[4].GetString(), arg=f[5].GetString(); // One world-thread consumer claims durably before any game effect. if (!CharacterDatabase.DirectPExecute("UPDATE azerothai_commands SET status='processing' WHERE id='%s' AND status='pending'",sql(id).c_str())) continue; Player* p=sObjectMgr.GetPlayer(ObjectGuid(HIGHGUID_PLAYER,f[1].GetUInt32())); Player* peer=sObjectMgr.GetPlayer(ObjectGuid(HIGHGUID_PLAYER,f[6].GetUInt32())); if (!managed(p) || p->GetSession()->GetAccountId()!=f[2].GetUInt32() || p->GetName()!=f[3].GetString() || std::time(nullptr)-f[7].GetUInt32()>90 || std::time(nullptr)>f[10].GetUInt32()) { receipt(id,false,"Stale command or managed identity unavailable"); continue; } if (f[6].GetUInt32() && (!managed(peer) || peer->GetSession()->GetAccountId()!=f[8].GetUInt32() || peer->GetName()!=f[9].GetString())) {receipt(id,false,"Peer identity changed or protected");continue;} if (kind=="party") { if (!nearby(p,peer,35) || !safeGroup(p) || !safeGroup(peer) || peer->GetGroup() || peer->GetGroupInvite() || p->GetGroupInvite() || (p->GetGroup() && (p->GetGroup()->GetMembersCount()>=5 || p->GetGroup()->GetLeaderGuid()!=p->GetObjectGuid()))) { receipt(id,false,"Party participants unavailable or protected"); continue; } WorldPacket invite(CMSG_GROUP_INVITE,32); invite << std::string(peer->GetName()); p->GetSession()->HandleGroupInviteOpcode(invite); if (peer->GetGroupInvite()) { WorldPacket accept(CMSG_GROUP_ACCEPT,0); peer->GetSession()->HandleGroupAcceptOpcode(accept); } receipt(id,p->GetGroup() && p->GetGroup()==peer->GetGroup(),"Native party invitation processed"); } else if (kind=="recruit") { if (!nearby(p,peer,35) || !p->GetGuildId() || p->GetRank()!=0 || peer->GetGuildId() || peer->GetGuildIdInvited()) {receipt(id,false,"Guild participants unavailable or protected");continue;} WorldPacket invite(CMSG_GUILD_INVITE,32); invite << std::string(peer->GetName());p->GetSession()->HandleGuildInviteOpcode(invite); if(peer->GetGuildIdInvited()==p->GetGuildId()){WorldPacket accept(CMSG_GUILD_ACCEPT,0);peer->GetSession()->HandleGuildAcceptOpcode(accept);} receipt(id,peer->GetGuildId()==p->GetGuildId(),"Native guild invitation processed"); } else if (kind=="leave") { if (!free(p) || !safeGroup(p) || !p->GetGroup() || std::to_string(p->GetGroup()->GetId())!=arg) { receipt(id,false,"Party changed or protected"); continue; } WorldPacket packet(CMSG_GROUP_DISBAND,0); p->GetSession()->HandleGroupDisbandOpcode(packet); receipt(id,!p->GetGroup(),"Native departure processed"); } else if (kind=="say") { if (!nearby(p,peer,25) || arg.empty() || arg.size()>1000) { receipt(id,false,"Conversation participants unavailable nearby"); continue; } p->Say(arg,p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH); receipt(id,true,"In-game speech sent"); } else if (kind=="intent") { if (!p->IsAlive() || p->IsTaxiFlying() || p->IsInCombat() || !safeGroup(p)) { receipt(id,false,"Native recovery, travel or combat takes priority"); continue; } PlayerbotAI* ai=p->GetPlayerbotAI(); bool accepted=false; if (arg=="quest" || arg=="explore") { ai->ChangeStrategy("-stay,+rpg,+grind",BotState::BOT_STATE_NON_COMBAT); ai->DoNextAction(); accepted=true; } else if (arg=="rest") { accepted=ai->DoSpecificAction("eat") || ai->DoSpecificAction("drink"); } else if (arg=="trade") { accepted=ai->DoSpecificAction("repair") || ai->DoSpecificAction("sell") || ai->DoSpecificAction("buy"); } else if (arg=="socialize") { p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE); accepted=true; } receipt(id,accepted,accepted?"Intention submitted to native AI; outcome requires observation":"Native AI did not accept this intention"); } else receipt(id,false,"Unsupported Classic command"); } while(q->NextRow()); } void snapshot(Player* p) { std::ostringstream out; std::string members, nearbyNames; Group* group=p->GetGroup(); if(group) for(GroupReference* r=group->GetFirstMember();r;r=r->next()) if(Player* m=r->getSource()) { if(!members.empty())members+=","; members+=m->GetName(); } for(auto const& r:roster) if(r.first!=p->GetGUIDLow()) { Player* m=sObjectMgr.GetPlayer(ObjectGuid(HIGHGUID_PLAYER,r.first)); if(managed(m)&&m->GetTeam()==p->GetTeam()&&m->GetMapId()==p->GetMapId()&&p->GetDistance(m)<=35) { if(!nearbyNames.empty())nearbyNames+=","; nearbyNames+=m->GetName(); } } out << "{\"isBot\":true,\"characterGuid\":" << p->GetGUIDLow() << ",\"accountId\":" << p->GetSession()->GetAccountId() << ",\"name\":" << quoted(p->GetName()) << ",\"level\":" << p->GetLevel() << ",\"race\":" << unsigned(p->getRace()) << ",\"class\":" << unsigned(p->getClass()) << ",\"team\":" << (p->GetTeam()==ALLIANCE?0:1) << ",\"mapId\":" << p->GetMapId() << ",\"zoneId\":" << p->GetZoneId() << ",\"x\":" << p->GetPositionX() << ",\"y\":" << p->GetPositionY() << ",\"healthPct\":" << p->GetHealthPercent() << ",\"xp\":" << p->GetUInt32Value(PLAYER_XP) << ",\"observedAt\":" << std::time(nullptr) << ",\"isDead\":" << (p->IsAlive()?"false":"true") << ",\"isGhost\":" << (p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST)?"true":"false") << ",\"inCombat\":" << (p->IsInCombat()?"true":"false") << ",\"casting\":" << (p->IsNonMeleeSpellCasted(false)?"true":"false") << ",\"taxi\":" << (p->IsTaxiFlying()?"true":"false") << ",\"humanControlled\":" << (managed(p)?"false":"true") << ",\"groupId\":" << quoted(group?std::to_string(group->GetId()):"") << ",\"groupNames\":" << quoted(members) << ",\"nearbyBots\":" << quoted(nearbyNames) << ",\"guildId\":" << p->GetGuildId() << ",\"guildRank\":" << p->GetRank() << ",\"quests\":["; bool first=true;for(uint8 i=0;iGetQuestSlotQuestId(i);if(id){if(!first)out<<',';out<GetPowerType()==POWER_MANA && p->GetMaxPower(POWER_MANA)) out << ",\"manaPct\":" << 100.0*p->GetPower(POWER_MANA)/p->GetMaxPower(POWER_MANA); if(auto area=GetAreaEntryByAreaID(p->GetZoneId())) out << ",\"zoneName\":" << quoted(area->area_name[0]); out << '}'; CharacterDatabase.PExecute("REPLACE INTO azerothai_snapshots (guid,at,payload) VALUES (%u,UNIX_TIMESTAMP(),'%s')",p->GetGUIDLow(),sql(out.str()).c_str()); } } uint32_t AzerothAIBoundDamage(Unit* victim,uint32_t damage) { if(victim && victim->GetTypeId()==TYPEID_PLAYER && victim->GetGUIDLow()==armorGuid.load()) { Player* p=static_cast(victim); if(p->GetSession()->GetAccountId()==armorAccount.load() && p->GetPlayerbotAI() && !p->GetPlayerbotAI()->IsRealPlayer()) return std::min(damage,p->GetHealth()>1?p->GetHealth()-1:0u); } return damage; } bool AzerothAIAllowBotLogin(uint32_t guid) { if(!sConfig.GetBoolDefault("AzerothAI.RestrictBotLogin",false))return true; auto count=CharacterDatabase.Query("SELECT COUNT(*) FROM azerothai_roster WHERE enabled=1"); if(!count || count->Fetch()[0].GetUInt32()>50)return false; auto identity=CharacterDatabase.PQuery("SELECT c.guid FROM characters c JOIN azerothai_roster r ON r.guid=c.guid AND r.account_id=c.account AND r.name=c.name WHERE r.enabled=1 AND c.guid=%u",guid); return bool(identity); } void AzerothAIUpdate(uint32_t diff) { static uint32_t elapsed=0; elapsed+=diff;if(elapsed<5000)return;elapsed=0; if(!sConfig.GetBoolDefault("AzerothAI.Enable",false)){armorGuid=0;armorAccount=0;return;} static bool started=false; if(!started){if(!CharacterDatabase.DirectExecute("UPDATE azerothai_commands SET status='failed',result='Server restarted during execution; outcome unknown, command not replayed' WHERE status='processing'"))return;started=true;} auto q=CharacterDatabase.Query("SELECT guid,account_id,name,plot_armor FROM azerothai_roster WHERE enabled=1 ORDER BY guid LIMIT 51"); if(!q || q->GetRowCount()>50){roster.clear();armorGuid=0;armorAccount=0;return;} roster.clear();armorGuid=0;armorAccount=0; do { Field* f=q->Fetch();Resident r{f[0].GetUInt32(),f[1].GetUInt32(),f[2].GetString(),f[3].GetBool()};if(!r.guid||!r.account)continue;roster.emplace(r.guid,r);if(r.armor && r.name=="Zaku"){armorAccount=r.account;armorGuid=r.guid;} }while(q->NextRow()); // Population is drawn exclusively from the explicit identity allowlist. static std::map loginAttempt; for(auto const& pair:roster){auto const&r=pair.second;Player* p=sObjectMgr.GetPlayer(ObjectGuid(HIGHGUID_PLAYER,r.guid)); if(!p){if(std::time(nullptr)-loginAttempt[r.guid]>=60){auto identity=CharacterDatabase.PQuery("SELECT guid FROM characters WHERE guid=%u AND account=%u AND name='%s'",r.guid,r.account,sql(r.name).c_str());if(identity){sRandomPlayerbotMgr.AddPlayerBot(r.guid,0);loginAttempt[r.guid]=std::time(nullptr);}}continue;} if(identified(p))snapshot(p); } commands(); }