diff --git a/modules/mod-mxwow-bosskill/src/mxwow_bosskill.cpp b/modules/mod-mxwow-bosskill/src/mxwow_bosskill.cpp index 85d8fc3..b3fe628 100644 --- a/modules/mod-mxwow-bosskill/src/mxwow_bosskill.cpp +++ b/modules/mod-mxwow-bosskill/src/mxwow_bosskill.cpp @@ -18,7 +18,7 @@ public: void OnPlayerLogin(Player* player) override { - ChatHandler(player->GetSession()).SendSysMessage("This server is running the |cff4CFF00MxW Boss Kill |rmodule."); + } void OnPlayerCreatureKill(Player* player, Creature* boss) { diff --git a/modules/mod-mxwow-bounty/src/mxwow_bounty.cpp b/modules/mod-mxwow-bounty/src/mxwow_bounty.cpp index 435d2b7..ff3be49 100644 --- a/modules/mod-mxwow-bounty/src/mxwow_bounty.cpp +++ b/modules/mod-mxwow-bounty/src/mxwow_bounty.cpp @@ -18,8 +18,7 @@ mxwow_bounty() : PlayerScript("mxwow_bounty") { } void OnPlayerLogin(Player* player) override { - if (sConfigMgr->GetOption("MxWoW_Bounty.Enabled", true)) { return; } - ChatHandler(player->GetSession()).SendSysMessage("This server is running the |cff4CFF00MxW Bounty |rmodule."); + } void OnPlayerCreatureKill(Player* player, Creature* creature) { @@ -75,10 +74,9 @@ mxwow_bounty() : PlayerScript("mxwow_bounty") { } } if (formatedAmountGold == 0 && formatedAmountSilver == 0 && formatedAmountCopper > 0) { ss << "|cffabeeff[MxW] Vous obtenez une récompense de |cffb87333" << formatedAmountCopper << "c |cffabeeffpour avoir tué " << cName << " (" << cLevel << ")."; - } - ChatHandler(player->GetSession()).PSendSysMessage(ss.str().c_str()); - + } player->ModifyMoney(+int32(giveAmount)); + ChatHandler(player->GetSession()).PSendSysMessage(ss.str().c_str()); } else { Group* group = player->GetGroup(); @@ -94,12 +92,11 @@ mxwow_bounty() : PlayerScript("mxwow_bounty") { } } if (formatedAmountGold == 0 && formatedAmountSilver == 0 && formatedAmountCopper > 0) { ss << "|cffabeeff[MxW] Vous obtenez une récompense de |cffb87333" << formatedAmountCopper << "c |cffabeeffpour avoir tué " << cName << " (" << cLevel << ")."; - } - ChatHandler(p->GetSession()).PSendSysMessage(ss.str().c_str()); - + } p->ModifyMoney(+int32(giveAmount)); + ChatHandler(p->GetSession()).PSendSysMessage(ss.str().c_str()); } - } + } } }; diff --git a/modules/mod-mxwow-honorable/src/mxwow-honorable.cpp b/modules/mod-mxwow-honorable/src/mxwow-honorable.cpp index ec34c24..d605727 100644 --- a/modules/mod-mxwow-honorable/src/mxwow-honorable.cpp +++ b/modules/mod-mxwow-honorable/src/mxwow-honorable.cpp @@ -19,8 +19,7 @@ mxwow_honorable() : PlayerScript("mxwow_honorable") { } void OnPlayerLogin(Player* player) override { - if (!sConfigMgr->GetOption("MxWoW_Honorable.Enabled", true)) { return; } - ChatHandler(player->GetSession()).SendSysMessage("This server is running the |cff4CFF00MxW Honorable |rmodule."); + } void OnPlayerPVPKill(Player* player, Player* victim) { @@ -41,7 +40,7 @@ mxwow_honorable() : PlayerScript("mxwow_honorable") { } std::string pName = player->GetName(); uint32 pLevel = player->GetLevel(); uint32 piLevel = player->GetAverageItemLevelForDF(); - float pLife = player->GetHealthPct(); + float pLife = round(player->GetHealthPct()); bool pGrouped = player->GetGroup(); std::string vName = victim->GetName(); @@ -56,6 +55,7 @@ mxwow_honorable() : PlayerScript("mxwow_honorable") { } else { ss << "|cffabeeff[MxW][|cfffc0303PvP|cffabeeff][|cfffc0303" << mapName << "|cffabeeff] [" << GetPlayerFactionByRace(player) << "|cffabeeff][" << pLevel << "][" << GetPlayerColor(player) << pName << "|cffabeeff][iL" << piLevel << "][" << pLife << "%] |cfffc0303a tué|cffabeeff [" << GetPlayerFactionByRace(victim) << "|cffabeeff][" << vLevel << "][" << GetPlayerColor(victim) << vName << "|cffabeeff][iL" << viLevel << "]."; player->ModifyMoney(+int32(base)); + ManageHonorableKill(player, victim); } sWorldSessionMgr->SendServerMessage(SERVER_MSG_STRING, ss.str().c_str()); @@ -127,6 +127,43 @@ mxwow_honorable() : PlayerScript("mxwow_honorable") { } } } + void ManageHonorableKill(Player* player, Player* victim) { + uint32 base = sConfigMgr->GetOption("MxWoW_Honorable.Kill.Copper", true); + uint32 killQty; + std::ostringstream ss; + std::ostringstream ssV; + uint32 kId = player->GetGUID().GetRawValue(); + std::string kName = player->GetName(); + uint32 kGuildId = 0; + std::string kGuildName = ""; + uint32 vId = victim->GetGUID().GetRawValue(); + std::string vName = victim->GetName(); + uint32 vGuildId = 0; + std::string vGuildName = ""; + uint32 kLevel = player->GetLevel(); + uint32 vLevel = victim->GetLevel(); + + QueryResult queryPlayerHonorable = LoginDatabase.Query("SELECT * FROM mxw_honorable_kill WHERE kguid = {} AND vguid = {}", kId, vId); + if (queryPlayerHonorable) { + killQty = (*queryPlayerHonorable)[4].Get(); + killQty++; + if (player->GetGuild()) { kGuildId = player->GetGuildId(); kGuildName = player->GetGuildName(); } + if (victim->GetGuild()) { vGuildId = victim->GetGuildId(); vGuildName = victim->GetGuildName(); } + LoginDatabase.Execute("UPDATE mxw_honorable_kill SET kguildid='{}',vguildid='{}',killcount='{}',last_kill_klevel='{}',last_kill_vlevel='{}' WHERE kguid='{}' AND vguid='{}'", kGuildId, vGuildId, killQty, kLevel, vLevel, kId, vId); + ss << "|cffabeeff[MxW][|cfffc0303PvP|cffabeeff] Vous avez tué " << GetPlayerColor(victim) << vName << " |cffabeeffde façon honorable à |cfffc0303" << killQty << " |cffabeeffreprises. Ça vous rapporte "<< FormatMoney(base) << "g."; + ssV << "|cffabeeff[MxW][|cfffc0303PvP|cffabeeff] Vous avez été tué |cfffc0303" << killQty << " |cffabeefffois par " << GetPlayerColor(player) << vName << "|cffabeeffde."; + } + else { + if(player->GetGuild()){ kGuildId = player->GetGuildId(); kGuildName = player->GetGuildName(); } + if (victim->GetGuild()) { vGuildId = victim->GetGuildId(); vGuildName = victim->GetGuildName(); } + LoginDatabase.Execute("INSERT INTO mxw_honorable_kill (kguid, kguildid, vguid, vguildid, killcount, last_kill_klevel, last_kill_vlevel) VALUES ({}, {}, {}, {}, {}, {}, {})", kId, kGuildId, vId, vGuildId, 1, kLevel, vLevel); + ss << "|cffabeeff[MxW][|cfffc0303PvP|cffabeeff] Vous avez tué " << GetPlayerColor(victim) << vName << " |cffabeeffde façon honorable à |cfffc03031 |cffabeeffreprise. Ça vous rapporte " << FormatMoney(base) << "g."; + ssV << "|cffabeeff[MxW][|cfffc0303PvP|cffabeeff] Vous avez été tué |cfffc03031 |cffabeefffois par " << GetPlayerColor(player) << vName << "|cffabeeffde."; + } + ChatHandler(player->GetSession()).PSendSysMessage(ss.str().c_str()); + ChatHandler(victim->GetSession()).PSendSysMessage(ssV.str().c_str()); + } + void ManageDishonorableKill(Player* player, const Player* victim) { uint32 killQty; @@ -136,18 +173,18 @@ mxwow_honorable() : PlayerScript("mxwow_honorable") { } uint32 vId = victim->GetGUID().GetRawValue(); uint64 kCopper = GetCharCopper(kId); uint32 loss; - QueryResult queryPlayerDishonorable = LoginDatabase.Query("SELECT * FROM mxw_dishonorable_penalty WHERE kguid = {} AND vguid = {}", kId, vId); + QueryResult queryPlayerDishonorable = LoginDatabase.Query("SELECT * FROM mxw_dishonorable_kill WHERE kguid = {} AND vguid = {}", kId, vId); if (queryPlayerDishonorable) { killQty = (*queryPlayerDishonorable)[2].Get(); killQty++; loss = (kCopper * killQty) / 100; - LoginDatabase.Execute("UPDATE mxw_dishonorable_penalty SET killcount='{}' WHERE kguid='{}' AND vguid='{}'", killQty, kId, vId); + LoginDatabase.Execute("UPDATE mxw_dishonorable_kill SET killcount='{}' WHERE kguid='{}' AND vguid='{}'", killQty, kId, vId); ss << "|cffabeeff[MxW] Vous avez tué "<< GetPlayerColor(victim) << vName << " |cffabeeffde façon déshonorable à |cfffc0303"<< killQty <<" |cffabeeffreprises. Vous subissez une perte de " << FormatMoney(loss) << "g"; } else { loss = (kCopper * 1) / 100; - LoginDatabase.Execute("INSERT INTO mxw_dishonorable_penalty (kguid, vguid, killcount) VALUES ({}, {}, {})", kId, vId, 1); + LoginDatabase.Execute("INSERT INTO mxw_dishonorable_kill (kguid, vguid, killcount) VALUES ({}, {}, {})", kId, vId, 1); ss << "|cffabeeff[MxW] Vous avez tué " << GetPlayerColor(victim) << vName << " |cffabeeffde façon déshonorable à |cfffc03031 |cffabeeffreprise. Vous subissez une perte de " << FormatMoney(loss) << "g"; } player->ModifyMoney(-int32(loss)); diff --git a/modules/mod-mxwow-levelup/src/mxwow_levelup.cpp b/modules/mod-mxwow-levelup/src/mxwow_levelup.cpp index 7b2cb17..e61adf1 100644 --- a/modules/mod-mxwow-levelup/src/mxwow_levelup.cpp +++ b/modules/mod-mxwow-levelup/src/mxwow_levelup.cpp @@ -18,7 +18,7 @@ public: void OnPlayerLogin(Player* player) override { - ChatHandler(player->GetSession()).SendSysMessage("This server is running the |cff4CFF00MxW Level Up |rmodule."); + } void OnPlayerLevelChanged(Player * player, uint8 oldLevel) diff --git a/modules/mod-mxwow-portalmaster/src/mxwow_portalmaster.cpp b/modules/mod-mxwow-portalmaster/src/mxwow_portalmaster.cpp index 173bbbc..086ae6a 100644 --- a/modules/mod-mxwow-portalmaster/src/mxwow_portalmaster.cpp +++ b/modules/mod-mxwow-portalmaster/src/mxwow_portalmaster.cpp @@ -20,8 +20,7 @@ class mxwow_portalmasterAnnounce : public PlayerScript mxwow_portalmasterAnnounce() : PlayerScript("mxwow_portalmasterAnnounce") {} void OnPlayerLogin(Player* player) override { - // Announce Module - ChatHandler(player->GetSession()).SendSysMessage("This server is running the |cff4CFF00MxW Portal Master|r module."); + } }; diff --git a/modules/mod-mxwow-servant/src/mxwow_servant.cpp b/modules/mod-mxwow-servant/src/mxwow_servant.cpp index e880569..245ca13 100644 --- a/modules/mod-mxwow-servant/src/mxwow_servant.cpp +++ b/modules/mod-mxwow-servant/src/mxwow_servant.cpp @@ -30,7 +30,7 @@ public: void OnPlayerLogin(Player* player) override { - ChatHandler(player->GetSession()).SendSysMessage("This server is running the |cff4CFF00MxW Servant |rmodule."); + } }; @@ -47,14 +47,15 @@ public: AddGossipItemFor(player, 10, "|TInterface/Icons/inv_hammer_08:30:30:-18|t Réparer (Tout)", GOSSIP_SENDER_MAIN, 1); AddGossipItemFor(player, 10, "|TInterface/Icons/inv_misc_enggizmos_18:30:30:-18|t Banque", GOSSIP_SENDER_MAIN, 2); - AddGossipItemFor(player, 10, "|TInterface/Icons/INV_Letter_11:30:30:-18|t Courrier", GOSSIP_SENDER_MAIN, 3); - AddGossipItemFor(player, 10, "|TInterface/Icons/spell_shadow_teleport:30:30:-18|t Buff", GOSSIP_SENDER_MAIN, 4); - AddGossipItemFor(player, 10, "|TInterface/Icons/Achievement_BG_AB_defendflags:30:30:-18|t Mettre fin au combat", GOSSIP_SENDER_MAIN, 5); - AddGossipItemFor(player, 10, "|TInterface/Icons/inv_misc_bag_11:30:30:-18|t Marchant", GOSSIP_SENDER_MAIN, 6); - AddGossipItemFor(player, 10, "|TInterface/Icons/inv_misc_coin_17:30:30:-18|t Enchère", GOSSIP_SENDER_MAIN, 7); - AddGossipItemFor(player, 10, "|TInterface/Icons/Ability_paladin_beaconoflight:30:30:-18|t Transmo", GOSSIP_SENDER_MAIN, 8); - AddGossipItemFor(player, 10, "|TInterface/Icons/inv_crate_01:30:30:-18|t Matériaux", GOSSIP_SENDER_MAIN, 9); - AddGossipItemFor(player, 10, "|TInterface/Icons/spell_nature_polymorph:30:30:-18|t Étable", GOSSIP_SENDER_MAIN, 10); + AddGossipItemFor(player, 10, "|TInterface/Icons/inv_misc_enggizmos_18:30:30:-18|t Banque de Guilde", GOSSIP_SENDER_MAIN, 3); + AddGossipItemFor(player, 10, "|TInterface/Icons/INV_Letter_11:30:30:-18|t Courrier", GOSSIP_SENDER_MAIN, 4); + AddGossipItemFor(player, 10, "|TInterface/Icons/spell_shadow_teleport:30:30:-18|t Buff", GOSSIP_SENDER_MAIN, 5); + AddGossipItemFor(player, 10, "|TInterface/Icons/Achievement_BG_AB_defendflags:30:30:-18|t Mettre fin au combat", GOSSIP_SENDER_MAIN, 6); + AddGossipItemFor(player, 10, "|TInterface/Icons/inv_misc_bag_11:30:30:-18|t Marchant", GOSSIP_SENDER_MAIN, 7); + AddGossipItemFor(player, 10, "|TInterface/Icons/inv_misc_coin_17:30:30:-18|t Enchère", GOSSIP_SENDER_MAIN, 8); + AddGossipItemFor(player, 10, "|TInterface/Icons/Ability_paladin_beaconoflight:30:30:-18|t Transmo", GOSSIP_SENDER_MAIN, 9); + AddGossipItemFor(player, 10, "|TInterface/Icons/inv_crate_01:30:30:-18|t Matériaux", GOSSIP_SENDER_MAIN, 10); + AddGossipItemFor(player, 10, "|TInterface/Icons/spell_nature_polymorph:30:30:-18|t Étable", GOSSIP_SENDER_MAIN, 11); SendGossipMenuFor(player, 20000000, creature->GetGUID()); @@ -80,9 +81,14 @@ public: break; case 3: CloseGossipMenuFor(player); - player->GetSession()->SendShowMailBox(player->GetGUID()); + SummonTempNPC(player, 9000003, 300000); + player->CastSpell(player, 31726); break; case 4: + CloseGossipMenuFor(player); + player->GetSession()->SendShowMailBox(player->GetGUID()); + break; + case 5: CloseGossipMenuFor(player); if (player->GetMap()->IsDungeon() || player->GetMap()->IsRaid()) { if (pLevel < 10) @@ -173,33 +179,33 @@ public: } player->CastSpell(player, 31726); break; - case 5: + case 6: CloseGossipMenuFor(player); player->CombatStop(); player->CastSpell(player, 31726); break; - case 6: + case 7: CloseGossipMenuFor(player); player->GetSession()->SendListInventory(creature->GetGUID()); player->CastSpell(player, 31726); break; - case 7: + case 8: CloseGossipMenuFor(player); //SummonTempNPC(player, 8670, 300000); player->GetSession()->SendAuctionHello(creature->GetGUID(), creature); player->CastSpell(player, 31726); break; - case 8: + case 9: CloseGossipMenuFor(player); SummonTempNPC(player, 190010, 300000); player->CastSpell(player, 31726); break; - case 9: + case 10: CloseGossipMenuFor(player); - SummonTempNPC(player, 190011, 300000); + SummonTempNPC(player, 290011, 300000); player->CastSpell(player, 31726); break; - case 10: + case 11: CloseGossipMenuFor(player); player->GetSession()->SendStablePet(creature->GetGUID()); player->CastSpell(player, 31726); diff --git a/modules/mod-mxwow-toonmaster/src/mxwow-toonmaster.cpp b/modules/mod-mxwow-toonmaster/src/mxwow-toonmaster.cpp index e394916..6a181a2 100644 --- a/modules/mod-mxwow-toonmaster/src/mxwow-toonmaster.cpp +++ b/modules/mod-mxwow-toonmaster/src/mxwow-toonmaster.cpp @@ -19,13 +19,7 @@ class mxwow_toonmasterAnnounce : public PlayerScript mxwow_toonmasterAnnounce() : PlayerScript("mxwow_toonmasterAnnounce") {} void OnPlayerLogin(Player* player) override { - // Announce Module - if (sConfigMgr->GetOption("MxWoW_ToonMaster.Enabled", true)) - { - std::ostringstream ss; - ss << "This server is running the |cff4CFF00MxW Toon Master|r module."; - ChatHandler(player->GetSession()).SendSysMessage(ss.str().c_str()); - } + } }; @@ -82,8 +76,8 @@ public: amount = amount * expMulti; if (sConfigMgr->GetOption("MxWoW_ToonMaster.Verbose", true) && plevel < 80) { - ss << "|cffabeeff[MxW][ToonMaster][%ixNiv.80] Bonus: EXPx%i"; - ChatHandler(player->GetSession()).PSendSysMessage(ss.str().c_str(), cMPL, expMulti); + ss << "|cffabeeff[MxW][ToonMaster]["<< cMPL <<"xNiv.80] Bonus: EXPx"<GetSession()).PSendSysMessage(ss.str().c_str()); } } } diff --git a/modules/mod-mxwow-webhelper/.editorconfig b/modules/mod-mxwow-webhelper/.editorconfig new file mode 100644 index 0000000..eb64e2f --- /dev/null +++ b/modules/mod-mxwow-webhelper/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +tab_width = 4 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 80 diff --git a/modules/mod-mxwow-webhelper/.gitattributes b/modules/mod-mxwow-webhelper/.gitattributes new file mode 100644 index 0000000..7ef9001 --- /dev/null +++ b/modules/mod-mxwow-webhelper/.gitattributes @@ -0,0 +1,105 @@ +## AUTO-DETECT +## Handle line endings automatically for files detected as +## text and leave all files detected as binary untouched. +## This will handle all files NOT defined below. +* text=auto eol=lf + +# Text +*.conf text +*.conf.dist text +*.cmake text + +## Scripts +*.sh text +*.fish text +*.lua text + +## SQL +*.sql text + +## C++ +*.c text +*.cc text +*.cxx text +*.cpp text +*.c++ text +*.hpp text +*.h text +*.h++ text +*.hh text + + +## For documentation + +# Documents +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +## DOCUMENTATION +*.markdown text +*.md text +*.mdwn text +*.mdown text +*.mkd text +*.mkdn text +*.mdtxt text +*.mdtext text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +## GRAPHICS +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +*.svg text +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + + +## ARCHIVES +*.7z binary +*.gz binary +*.jar binary +*.rar binary +*.tar binary +*.zip binary + +## EXECUTABLES +*.exe binary +*.pyc binary diff --git a/modules/mod-mxwow-webhelper/.github/workflows/core-build.yml b/modules/mod-mxwow-webhelper/.github/workflows/core-build.yml new file mode 100644 index 0000000..921c9eb --- /dev/null +++ b/modules/mod-mxwow-webhelper/.github/workflows/core-build.yml @@ -0,0 +1,12 @@ +name: core-build +on: + push: + branches: + - 'master' + pull_request: + +jobs: + build: + uses: azerothcore/reusable-workflows/.github/workflows/core_build_modules.yml@main + with: + module_repo: ${{ github.event.repository.name }} diff --git a/modules/mod-mxwow-webhelper/.gitignore b/modules/mod-mxwow-webhelper/.gitignore new file mode 100644 index 0000000..c6e1299 --- /dev/null +++ b/modules/mod-mxwow-webhelper/.gitignore @@ -0,0 +1,48 @@ +!.gitignore + +# +#Generic +# + +.directory +.mailmap +*.orig +*.rej +*.*~ +.hg/ +*.kdev* +.DS_Store +CMakeLists.txt.user +*.bak +*.patch +*.diff +*.REMOTE.* +*.BACKUP.* +*.BASE.* +*.LOCAL.* + +# +# IDE & other softwares +# +/.settings/ +/.externalToolBuilders/* +# exclude in all levels +nbproject/ +.sync.ffs_db +*.kate-swp + +# +# Eclipse +# +*.pydevproject +.metadata +.gradle +tmp/ +*.tmp +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.project +.cproject diff --git a/modules/mod-mxwow-webhelper/LICENSE b/modules/mod-mxwow-webhelper/LICENSE new file mode 100644 index 0000000..dbbe355 --- /dev/null +++ b/modules/mod-mxwow-webhelper/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/modules/mod-mxwow-webhelper/README.md b/modules/mod-mxwow-webhelper/README.md new file mode 100644 index 0000000..df18060 --- /dev/null +++ b/modules/mod-mxwow-webhelper/README.md @@ -0,0 +1,11 @@ +# ![logo](https://raw.githubusercontent.com/azerothcore/azerothcore.github.io/master/images/logo-github.png) AzerothCore +## MxWoW - Portal Master +(https://mxgit.ovh/MxWoW/mod_mxwow_portalmaster) + + +This is a module for [AzerothCore](http://www.azerothcore.org) + +## Dev(s) +- mikx +## Features +- Cast portals to different key location. \ No newline at end of file diff --git a/modules/mod-mxwow-webhelper/include.sh b/modules/mod-mxwow-webhelper/include.sh new file mode 100644 index 0000000..e69de29 diff --git a/modules/mod-mxwow-webhelper/sql/world/world.sql b/modules/mod-mxwow-webhelper/sql/world/world.sql new file mode 100644 index 0000000..1d37b60 --- /dev/null +++ b/modules/mod-mxwow-webhelper/sql/world/world.sql @@ -0,0 +1,3 @@ +DELETE FROM `item_template` WHERE (`entry` = 701001); +INSERT INTO `item_template` (`entry`, `class`, `subclass`, `SoundOverrideSubclass`, `name`, `displayid`, `Quality`, `Flags`, `FlagsExtra`, `BuyCount`, `BuyPrice`, `SellPrice`, `InventoryType`, `AllowableClass`, `AllowableRace`, `ItemLevel`, `RequiredLevel`, `RequiredSkill`, `RequiredSkillRank`, `requiredspell`, `requiredhonorrank`, `RequiredCityRank`, `RequiredReputationFaction`, `RequiredReputationRank`, `maxcount`, `stackable`, `ContainerSlots`, `StatsCount`, `stat_type1`, `stat_value1`, `stat_type2`, `stat_value2`, `stat_type3`, `stat_value3`, `stat_type4`, `stat_value4`, `stat_type5`, `stat_value5`, `stat_type6`, `stat_value6`, `stat_type7`, `stat_value7`, `stat_type8`, `stat_value8`, `stat_type9`, `stat_value9`, `stat_type10`, `stat_value10`, `ScalingStatDistribution`, `ScalingStatValue`, `dmg_min1`, `dmg_max1`, `dmg_type1`, `dmg_min2`, `dmg_max2`, `dmg_type2`, `armor`, `holy_res`, `fire_res`, `nature_res`, `frost_res`, `shadow_res`, `arcane_res`, `delay`, `ammo_type`, `RangedModRange`, `spellid_1`, `spelltrigger_1`, `spellcharges_1`, `spellppmRate_1`, `spellcooldown_1`, `spellcategory_1`, `spellcategorycooldown_1`, `spellid_2`, `spelltrigger_2`, `spellcharges_2`, `spellppmRate_2`, `spellcooldown_2`, `spellcategory_2`, `spellcategorycooldown_2`, `spellid_3`, `spelltrigger_3`, `spellcharges_3`, `spellppmRate_3`, `spellcooldown_3`, `spellcategory_3`, `spellcategorycooldown_3`, `spellid_4`, `spelltrigger_4`, `spellcharges_4`, `spellppmRate_4`, `spellcooldown_4`, `spellcategory_4`, `spellcategorycooldown_4`, `spellid_5`, `spelltrigger_5`, `spellcharges_5`, `spellppmRate_5`, `spellcooldown_5`, `spellcategory_5`, `spellcategorycooldown_5`, `bonding`, `description`, `PageText`, `LanguageID`, `PageMaterial`, `startquest`, `lockid`, `Material`, `sheath`, `RandomProperty`, `RandomSuffix`, `block`, `itemset`, `MaxDurability`, `area`, `Map`, `BagFamily`, `TotemCategory`, `socketColor_1`, `socketContent_1`, `socketColor_2`, `socketContent_2`, `socketColor_3`, `socketContent_3`, `socketBonus`, `GemProperties`, `RequiredDisenchantSkill`, `ArmorDamageModifier`, `duration`, `ItemLimitCategory`, `HolidayId`, `ScriptName`, `DisenchantID`, `FoodType`, `minMoneyLoot`, `maxMoneyLoot`, `flagsCustom`, `VerifiedBuild`) VALUES +(701001, 15, 4, -1, 'MxWoW Portal Master', 28862, 4, 64, 0, 1, 50000000, 10000000, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1000, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, -1, 1, 'Used to summon portals.', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 'mxwow_portalmaster', 0, 0, 0, 0, 0, 0); diff --git a/modules/mod-mxwow-webhelper/src/mxwow-webhelper.cpp b/modules/mod-mxwow-webhelper/src/mxwow-webhelper.cpp new file mode 100644 index 0000000..45b96de --- /dev/null +++ b/modules/mod-mxwow-webhelper/src/mxwow-webhelper.cpp @@ -0,0 +1,57 @@ +//// MxWoW Official Module +//// Web Helper +//// Dev: mikx +//// Git: https://mxgit.ovh/MxWoW/mod-mxwow-webhelper + +#include "Define.h" +#include "GossipDef.h" +#include "Item.h" +#include "Player.h" +#include "ScriptedGossip.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "Configuration/Config.h" +#include "Chat.h" + +class mxwow_webhelper : public PlayerScript +{ +public: + mxwow_webhelper() : PlayerScript("mxwow_webhelper") {} + + void OnPlayerLogin(Player* player) override + { + QueryResult queryAccount = LoginDatabase.Query("SELECT * FROM account WHERE id = {}", player->GetSession()->GetAccountId()); + if(queryAccount){ + std::string userName = (*queryAccount)[1].Get(); + if(userName.find("RNDBOT") != std::string::npos){ + // Is a bot + LoginDatabase.Execute("INSERT INTO mxw_web_online (aid, cid, isBot) VALUES ({}, {}, {})", player->GetSession()->GetAccountId(), player->GetGUID().GetRawValue(), 1); + } else { + // Is a real player + LoginDatabase.Execute("INSERT INTO mxw_web_online (aid, cid, isBot) VALUES ({}, {}, {})", player->GetSession()->GetAccountId(), player->GetGUID().GetRawValue(), 0); + } + } + } + + void OnPlayerLogout(Player* player) override + { + LoginDatabase.Execute("DELETE FROM mxw_web_online WHERE aid = {} AND cid = {}", player->GetSession()->GetAccountId(), player->GetGUID().GetRawValue()); + } +}; + +class mxwow_webhelperWorld : public WorldScript +{ +public: + mxwow_webhelperWorld() : WorldScript("mxwow_webhelperWorld") {} + + void OnShutdown() override + { + LoginDatabase.Execute("DELETE FROM mxw_web_online"); + } +}; + +void AddMxWoWWebHelperScripts() +{ + new mxwow_webhelper(); + new mxwow_webhelperWorld(); +} diff --git a/modules/mod-mxwow-webhelper/src/mxwow-webhelper_loader.cpp b/modules/mod-mxwow-webhelper/src/mxwow-webhelper_loader.cpp new file mode 100644 index 0000000..f2bb255 --- /dev/null +++ b/modules/mod-mxwow-webhelper/src/mxwow-webhelper_loader.cpp @@ -0,0 +1,12 @@ +//// MxWoW Official Module +//// QoL Master +//// Dev: mikx +//// Git: https://mxgit.ovh/MxWoW/mod_mxwow_portalmaster +//// Credits: Based on https://github.com/azerothcore/mod-character-tools + +void AddMxWoWWebHelperScripts(); + +void Addmod_mxwow_webhelperScripts() +{ + AddMxWoWWebHelperScripts(); +} diff --git a/modules/mod-no-hearthstone-cooldown/.editorconfig b/modules/mod-no-hearthstone-cooldown/.editorconfig new file mode 100644 index 0000000..eb64e2f --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +tab_width = 4 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 80 diff --git a/modules/mod-no-hearthstone-cooldown/.git_commit_template.txt b/modules/mod-no-hearthstone-cooldown/.git_commit_template.txt new file mode 100644 index 0000000..5c5d933 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/.git_commit_template.txt @@ -0,0 +1,45 @@ +### TITLE +## Type(Scope/Subscope): Commit ultra short explanation +## |---- Write below the examples with a maximum of 50 characters ----| +## Example 1: fix(DB/SAI): Missing spell to NPC Hogger +## Example 2: fix(CORE/Raid): Phase 2 of Ragnaros +## Example 3: feat(CORE/Commands): New GM command to do something + +### DESCRIPTION +## Explain why this change is being made, what does it fix etc... +## |---- Write below the examples with a maximum of 72 characters per lines ----| +## Example: Hogger (id: 492) was not charging player when being engaged. + +## Provide links to any issue, commit, pull request or other resource +## Example 1: Closes issue #23 +## Example 2: Ported from other project's commit (link) +## Example 3: References taken from wowpedia / wowhead / wowwiki / https://wowgaming.altervista.org/aowow/ + +## ======================================================= +## EXTRA INFOS +## ======================================================= +## "Type" can be: +## feat (new feature) +## fix (bug fix) +## refactor (refactoring production code) +## style (formatting, missing semi colons, etc; no code change) +## docs (changes to documentation) +## test (adding or refactoring tests; no production code change) +## chore (updating bash scripts, git files etc; no production code change) +## -------------------- +## Remember to +## Capitalize the subject line +## Use the imperative mood in the subject line +## Do not end the subject line with a period +## Separate subject from body with a blank line +## Use the body to explain what and why rather than how +## Can use multiple lines with "-" for bullet points in body +## -------------------- +## More info here https://www.conventionalcommits.org/en/v1.0.0-beta.2/ +## ======================================================= +## "Scope" can be: +## CORE (core related, c++) +## DB (database related, sql) +## ======================================================= +## "Subscope" is optional and depends on the nature of the commit. +## ======================================================= diff --git a/modules/mod-no-hearthstone-cooldown/.gitattributes b/modules/mod-no-hearthstone-cooldown/.gitattributes new file mode 100644 index 0000000..7ef9001 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/.gitattributes @@ -0,0 +1,105 @@ +## AUTO-DETECT +## Handle line endings automatically for files detected as +## text and leave all files detected as binary untouched. +## This will handle all files NOT defined below. +* text=auto eol=lf + +# Text +*.conf text +*.conf.dist text +*.cmake text + +## Scripts +*.sh text +*.fish text +*.lua text + +## SQL +*.sql text + +## C++ +*.c text +*.cc text +*.cxx text +*.cpp text +*.c++ text +*.hpp text +*.h text +*.h++ text +*.hh text + + +## For documentation + +# Documents +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +## DOCUMENTATION +*.markdown text +*.md text +*.mdwn text +*.mdown text +*.mkd text +*.mkdn text +*.mdtxt text +*.mdtext text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +## GRAPHICS +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +*.svg text +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + + +## ARCHIVES +*.7z binary +*.gz binary +*.jar binary +*.rar binary +*.tar binary +*.zip binary + +## EXECUTABLES +*.exe binary +*.pyc binary diff --git a/modules/mod-no-hearthstone-cooldown/.github/workflows/core-build.yml b/modules/mod-no-hearthstone-cooldown/.github/workflows/core-build.yml new file mode 100644 index 0000000..1eec897 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/.github/workflows/core-build.yml @@ -0,0 +1,10 @@ +name: core-build +on: + push: + pull_request: + +jobs: + build: + uses: azerothcore/reusable-workflows/.github/workflows/core_build_modules.yml@main + with: + module_repo: ${{ github.event.repository.name }} diff --git a/modules/mod-no-hearthstone-cooldown/.gitignore b/modules/mod-no-hearthstone-cooldown/.gitignore new file mode 100644 index 0000000..c6e1299 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/.gitignore @@ -0,0 +1,48 @@ +!.gitignore + +# +#Generic +# + +.directory +.mailmap +*.orig +*.rej +*.*~ +.hg/ +*.kdev* +.DS_Store +CMakeLists.txt.user +*.bak +*.patch +*.diff +*.REMOTE.* +*.BACKUP.* +*.BASE.* +*.LOCAL.* + +# +# IDE & other softwares +# +/.settings/ +/.externalToolBuilders/* +# exclude in all levels +nbproject/ +.sync.ffs_db +*.kate-swp + +# +# Eclipse +# +*.pydevproject +.metadata +.gradle +tmp/ +*.tmp +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.project +.cproject diff --git a/modules/mod-no-hearthstone-cooldown/LICENSE b/modules/mod-no-hearthstone-cooldown/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/modules/mod-no-hearthstone-cooldown/README.md b/modules/mod-no-hearthstone-cooldown/README.md new file mode 100644 index 0000000..1afb3b7 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/README.md @@ -0,0 +1,44 @@ +# ![logo](https://raw.githubusercontent.com/azerothcore/azerothcore.github.io/master/images/logo-github.png) AzerothCore +## mod-no-hearthstone-cooldown +### This is a module for [AzerothCore](http://www.azerothcore.org) + +# Module info + +- Name: No Hearthstone cooldown +- Author: BytesGalore +- Module: + + Repository: https://github.com/BytesGalore/mod-no-hearthstone-cooldown + + Download: https://github.com/BytesGalore/mod-no-hearthstone-cooldown/archive/refs/heads/main.zip +- License: AGPL + +# Module integration + +- **AzerothCore hash/commit compliance:** [5af98783](https://github.com/azerothcore/azerothcore-wotlk/commit/5af98783c9f61f059914b3304bb26785502a6924) +- Includes configuration (.conf)?: Yes, copied by CMake +- Includes SQL patches?: No +- Core hooks used: + + PlayerScript: OnLogin + + PlayerScript: OnBeforeTeleport + + WorldScript: OnAfterConfigLoad + +# Description +A module that simply skips the cooldown of the Hearthstone +#### Features: +- It resets the Hearthstone cooldown immediately + +### This module currently requires: +- AzerothCore v4.0.0+ + +### How to install +1. Simply place the module under the `modules` folder of your AzerothCore source folder. +2. Re-run cmake and launch a clean build of AzerothCore +3. that's all + +### (Optional) Edit module configuration +- You can turn the module **on** and **off**, default it is **on** +- You can also turn the announcement of the module on player-login **on** and **off**, default it is **on** +- If you want change the settings, go to your server configuration folder (e.g. **etc**), copy `mod_no_hearthstone_cooldown.conf.dist` to `mod_no_hearthstone_cooldown.conf` and edit the settings + +## Credits +* [BytesGalore](https://github.com/BytesGalore) (author of the module) +* AzerothCore: [repository](https://github.com/azerothcore) - [website](http://azerothcore.org/) - [discord chat community](https://discord.gg/PaqQRkd) diff --git a/modules/mod-no-hearthstone-cooldown/conf/conf.sh.dist b/modules/mod-no-hearthstone-cooldown/conf/conf.sh.dist new file mode 100644 index 0000000..06a5f89 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/conf/conf.sh.dist @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +## CUSTOM SQL - Important file used by the db_assembler.sh +## Keep only the required variables (base sql files or updates, depending on the DB) + +## BASE SQL + +DB_AUTH_CUSTOM_PATHS+=( + $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/sql/auth/base/" +) + +DB_CHARACTERS_CUSTOM_PATHS+=( + $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/sql/characters/base/" +) + +DB_WORLD_CUSTOM_PATHS+=( + $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/sql/world/base/" +) + +## UPDATES + +DB_AUTH_UPDATES_PATHS+=( + $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/sql/auth/updates/" +) + +DB_CHARACTERS_UPDATES_PATHS+=( + $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/sql/characters/updates/" +) + +DB_WORLD_UPDATES_PATHS+=( + $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/sql/world/updates/" +) diff --git a/modules/mod-no-hearthstone-cooldown/conf/mod_no_hearthstone_cooldown.conf.dist b/modules/mod-no-hearthstone-cooldown/conf/mod_no_hearthstone_cooldown.conf.dist new file mode 100644 index 0000000..137dfa6 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/conf/mod_no_hearthstone_cooldown.conf.dist @@ -0,0 +1,24 @@ +# +# Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3 +# + +[worldserver] + +##################################################### +# No Hearthstone cooldown module configuration +##################################################### +# +# NoHearthstoneCooldownConfig.Enable +# Description: Enable to skip the Hearthstone cooldown +# Default: 1 - Enabled +# 0 - Disabled +# +# NoHearthstoneCooldownConfig.Announce +# Description: Inform the player about the loaded module +# Default: 1 - Enabled +# 0 - Disabled +# + +NoHearthstoneCooldown.Enable = 1 + +NoHearthstoneCooldown.Announce = 1 diff --git a/modules/mod-no-hearthstone-cooldown/include.sh b/modules/mod-no-hearthstone-cooldown/include.sh new file mode 100644 index 0000000..d2fdbc6 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/include.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +## GETS THE CURRENT MODULE ROOT DIRECTORY +MOD_NO_HEARTHSTONE_COOLDOWN_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/" && pwd )" + +source $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/conf/conf.sh.dist" + +if [ -f $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/conf/conf.sh" ]; then + source $MOD_NO_HEARTHSTONE_COOLDOWN_ROOT"/conf/conf.sh" +fi diff --git a/modules/mod-no-hearthstone-cooldown/setup_git_commit_template.sh b/modules/mod-no-hearthstone-cooldown/setup_git_commit_template.sh new file mode 100644 index 0000000..7b52062 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/setup_git_commit_template.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +## Set a local git commit template +git config --local commit.template ".git_commit_template.txt" ; diff --git a/modules/mod-no-hearthstone-cooldown/src/NHC_loader.cpp b/modules/mod-no-hearthstone-cooldown/src/NHC_loader.cpp new file mode 100644 index 0000000..178c3d8 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/src/NHC_loader.cpp @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3 + */ + +// From SC +void AddNoHearthstoneCooldownScripts(); + +// Add all +void Addmod_no_hearthstone_cooldownScripts() +{ + AddNoHearthstoneCooldownScripts(); +} diff --git a/modules/mod-no-hearthstone-cooldown/src/NoHearthstoneCooldown.cpp b/modules/mod-no-hearthstone-cooldown/src/NoHearthstoneCooldown.cpp new file mode 100644 index 0000000..6ea04f0 --- /dev/null +++ b/modules/mod-no-hearthstone-cooldown/src/NoHearthstoneCooldown.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3 + */ + +#include "ScriptMgr.h" +#include "Player.h" +#include "Config.h" +#include "Chat.h" + +// Add player scripts +class NoHearthstoneCooldown : public PlayerScript, public WorldScript +{ +public: + NoHearthstoneCooldown(): PlayerScript("NoHearthstoneCooldown"), WorldScript("NoHearthstoneCooldown") {} + + bool OnPlayerBeforeTeleport(Player* player, uint32 /*mapid*/, float /*x*/, float /*y*/, float /*z*/, float /*orientation*/, uint32 /*options*/, Unit* /*target*/) override + { + ClearHearthstoneCooldown(player); + return true; + } + + void OnPlayerLogin(Player* player) override + { + if (sConfigMgr->GetOption("NoHearthstoneCooldown.Announce", true)) + { + ChatHandler(player->GetSession()).SendSysMessage("This server is running the |cff4CFF00No Hearthstone cooldown|r module."); + } + ClearHearthstoneCooldown(player); + } + + void OnAfterConfigLoad(bool /*reload*/) override + { + m_bNHCModuleEnabled = sConfigMgr->GetOption("NoHearthstoneCooldown.Enable", true); + } + +private: + void ClearHearthstoneCooldown(Player* player) + { + if (m_bNHCModuleEnabled) + { + // only clear the cooldown if the player actually casted the Hearthstone spell + if (player->FindCurrentSpellBySpellId(m_nSpellIDHearthstone)) + { + // we remove the cooldown from the list of the player current cooldowns + player->RemoveSpellCooldown(m_nSpellIDHearthstone, true); + player->RemoveSpellCooldown(m_nSpellIDNoPlaceLikeHome, true); + // then we clear the cooldown and send an update to the player client + player->SendClearCooldown(m_nSpellIDHearthstone, player); + player->SendClearCooldown(m_nSpellIDNoPlaceLikeHome, player); + } + } + } + + bool m_bNHCModuleEnabled = false; + // https://www.wowhead.com/spell=8690/hearthstone + const uint32 m_nSpellIDHearthstone = 8690; + // https://tbc.wowhead.com/spell=39937/theres-no-place-like-home + const uint32 m_nSpellIDNoPlaceLikeHome = 39937; +}; + +// Add all scripts in one +void AddNoHearthstoneCooldownScripts() +{ + new NoHearthstoneCooldown(); +} diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/db_playerbots/2025_03_03_00.sql b/modules/mod-playerbots/data/sql/playerbots/updates/db_playerbots/2025_03_03_00.sql index cc1f4f0..29d8afb 100644 --- a/modules/mod-playerbots/data/sql/playerbots/updates/db_playerbots/2025_03_03_00.sql +++ b/modules/mod-playerbots/data/sql/playerbots/updates/db_playerbots/2025_03_03_00.sql @@ -1,12 +1,22 @@ -INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) VALUES -(3780, 'Highlord Mograine', 533, 2524.32, -2951.28, 245.633, 1); +-- Delete existing entries from playerbots_travelnode, playerbots_travelnode_path, and playerbots_travelnode_link tables +DELETE FROM `playerbots_travelnode_link` WHERE `node_id` = 3780; +DELETE FROM `playerbots_travelnode_path` WHERE `node_id` = 3780; +DELETE FROM `playerbots_travelnode` WHERE `id` = 3780; -INSERT INTO `playerbots_travelnode_path` (`node_id`, `to_node_id`, `nr`, `map_id`, `x`, `y`, `z`) VALUES +-- Insert new entries into playerbots_travelnode +INSERT INTO `playerbots_travelnode` (`id`, `name`, `map_id`, `x`, `y`, `z`, `linked`) +VALUES (3780, 'Highlord Mograine', 533, 2524.32, -2951.28, 245.633, 1); + +-- Insert new entries into playerbots_travelnode_path +INSERT INTO `playerbots_travelnode_path` (`node_id`, `to_node_id`, `nr`, `map_id`, `x`, `y`, `z`) +VALUES (3780, 472, 0, 533, 2524.32, -2951.28, 245.633), (3780, 472, 1, 533, 2528.79, -2948.58, 245.633), (3780, 757, 0, 533, 2524.32, -2951.28, 245.633), (3780, 757, 1, 533, 2517.62, -2959.38, 245.636); -INSERT INTO `playerbots_travelnode_link` (`node_id`, `to_node_id`, `type`, `object`, `distance`, `swim_distance`, `extra_cost`, `calculated`, `max_creature_0`, `max_creature_1`, `max_creature_2`) VALUES +-- Insert new entries into playerbots_travelnode_link +INSERT INTO `playerbots_travelnode_link` (`node_id`, `to_node_id`, `type`, `object`, `distance`, `swim_distance`, `extra_cost`, `calculated`, `max_creature_0`, `max_creature_1`, `max_creature_2`) +VALUES (3780, 472, 1, 0, 5.3221, 0, 0, 1, 83, 0, 0), (3780, 757, 1, 0, 10.6118, 0, 0, 1, 83, 0, 0); diff --git a/modules/mod-playerbots/src/LootObjectStack.cpp b/modules/mod-playerbots/src/LootObjectStack.cpp index 196e986..3d34987 100644 --- a/modules/mod-playerbots/src/LootObjectStack.cpp +++ b/modules/mod-playerbots/src/LootObjectStack.cpp @@ -9,7 +9,7 @@ #include "Playerbots.h" #include "Unit.h" -#define MAX_LOOT_OBJECT_COUNT 10 +#define MAX_LOOT_OBJECT_COUNT 200 LootTarget::LootTarget(ObjectGuid guid) : guid(guid), asOfTime(time(nullptr)) {} diff --git a/modules/mod-playerbots/src/PlayerbotAI.cpp b/modules/mod-playerbots/src/PlayerbotAI.cpp index 87ef92a..8c154b1 100644 --- a/modules/mod-playerbots/src/PlayerbotAI.cpp +++ b/modules/mod-playerbots/src/PlayerbotAI.cpp @@ -14,10 +14,13 @@ #include "BudgetValues.h" #include "ChannelMgr.h" #include "CharacterPackets.h" +#include "Common.h" #include "CreatureAIImpl.h" +#include "CreatureData.h" #include "EmoteAction.h" #include "Engine.h" #include "ExternalEventHelper.h" +#include "GameObjectData.h" #include "GameTime.h" #include "GuildMgr.h" #include "GuildTaskMgr.h" @@ -32,6 +35,7 @@ #include "MoveSplineInit.h" #include "NewRpgStrategy.h" #include "ObjectGuid.h" +#include "ObjectMgr.h" #include "PerformanceMonitor.h" #include "Player.h" #include "PlayerbotAIConfig.h" @@ -174,6 +178,7 @@ PlayerbotAI::PlayerbotAI(Player* bot) botOutgoingPacketHandlers.AddHandler(SMSG_RESURRECT_REQUEST, "resurrect request"); botOutgoingPacketHandlers.AddHandler(SMSG_INVENTORY_CHANGE_FAILURE, "cannot equip"); botOutgoingPacketHandlers.AddHandler(SMSG_TRADE_STATUS, "trade status"); + botOutgoingPacketHandlers.AddHandler(SMSG_TRADE_STATUS_EXTENDED, "trade status extended"); botOutgoingPacketHandlers.AddHandler(SMSG_LOOT_RESPONSE, "loot response"); botOutgoingPacketHandlers.AddHandler(SMSG_ITEM_PUSH_RESULT, "item push result"); botOutgoingPacketHandlers.AddHandler(SMSG_PARTY_COMMAND_RESULT, "party command"); @@ -204,7 +209,7 @@ PlayerbotAI::PlayerbotAI(Player* bot) masterIncomingPacketHandlers.AddHandler(CMSG_PUSHQUESTTOPARTY, "quest share"); botOutgoingPacketHandlers.AddHandler(SMSG_QUESTUPDATE_COMPLETE, "quest update complete"); botOutgoingPacketHandlers.AddHandler(SMSG_QUESTUPDATE_ADD_KILL, "quest update add kill"); - botOutgoingPacketHandlers.AddHandler(SMSG_QUESTUPDATE_ADD_ITEM, "quest update add item"); + // botOutgoingPacketHandlers.AddHandler(SMSG_QUESTUPDATE_ADD_ITEM, "quest update add item"); // SMSG_QUESTUPDATE_ADD_ITEM no longer used botOutgoingPacketHandlers.AddHandler(SMSG_QUEST_CONFIRM_ACCEPT, "confirm quest"); } @@ -1100,9 +1105,9 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) } } - QueueChatResponse( - ChatQueuedReply{msgtype, guid1.GetCounter(), guid2.GetCounter(), message, chanName, - name, time(nullptr) + urand(inCombat ? 10 : 5, inCombat ? 25 : 15)}); + QueueChatResponse(ChatQueuedReply{msgtype, guid1.GetCounter(), guid2.GetCounter(), message, + chanName, name, + time(nullptr) + urand(inCombat ? 10 : 5, inCombat ? 25 : 15)}); GetAiObjectContext()->GetValue("last said", "chat")->Set(time(0) + urand(5, 25)); return; } @@ -1239,10 +1244,10 @@ void PlayerbotAI::ChangeEngine(BotState type) switch (type) { case BOT_STATE_COMBAT: - // LOG_DEBUG("playerbots", "=== {} COMBAT ===", bot->GetName().c_str()); + ChangeEngineOnCombat(); break; case BOT_STATE_NON_COMBAT: - // LOG_DEBUG("playerbots", "=== {} NON-COMBAT ===", bot->GetName().c_str()); + ChangeEngineOnNonCombat(); break; case BOT_STATE_DEAD: // LOG_DEBUG("playerbots", "=== {} DEAD ===", bot->GetName().c_str()); @@ -1253,6 +1258,23 @@ void PlayerbotAI::ChangeEngine(BotState type) } } +void PlayerbotAI::ChangeEngineOnCombat() +{ + if (HasStrategy("stay", BOT_STATE_COMBAT)) + { + aiObjectContext->GetValue("pos", "stay") + ->Set(PositionInfo(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId())); + } +} + +void PlayerbotAI::ChangeEngineOnNonCombat() +{ + if (HasStrategy("stay", BOT_STATE_NON_COMBAT)) + { + aiObjectContext->GetValue("pos", "stay")->Reset(); + } +} + void PlayerbotAI::DoNextAction(bool min) { if (!bot->IsInWorld() || bot->IsBeingTeleported() || (GetMaster() && GetMaster()->IsBeingTeleported())) @@ -2287,10 +2309,46 @@ const AreaTableEntry* PlayerbotAI::GetCurrentZone() std::string PlayerbotAI::GetLocalizedAreaName(const AreaTableEntry* entry) { + std::string name; if (entry) - return entry->area_name[sWorld->GetDefaultDbcLocale()]; + { + name = entry->area_name[sWorld->GetDefaultDbcLocale()]; + if (name.empty()) + name = entry->area_name[LOCALE_enUS]; + } - return ""; + return name; +} + +std::string PlayerbotAI::GetLocalizedCreatureName(uint32 entry) +{ + std::string name; + const CreatureLocale* cl = sObjectMgr->GetCreatureLocale(entry); + if (cl) + ObjectMgr::GetLocaleString(cl->Name, sWorld->GetDefaultDbcLocale(), name); + if (name.empty()) + { + CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(entry); + if (ct) + name = ct->Name; + } + return name; +} + + +std::string PlayerbotAI::GetLocalizedGameObjectName(uint32 entry) +{ + std::string name; + const GameObjectLocale* gl = sObjectMgr->GetGameObjectLocale(entry); + if (gl) + ObjectMgr::GetLocaleString(gl->Name, sWorld->GetDefaultDbcLocale(), name); + if (name.empty()) + { + GameObjectTemplate const* gt = sObjectMgr->GetGameObjectTemplate(entry); + if (gt) + name = gt->name; + } + return name; } std::vector PlayerbotAI::GetPlayersInGroup() @@ -2807,7 +2865,6 @@ bool PlayerbotAI::CanCastSpell(uint32 spellid, Unit* target, bool checkHasSpell, if (!target) target = bot; - if (Pet* pet = bot->GetPet()) if (pet->HasSpell(spellid)) return true; @@ -3000,8 +3057,7 @@ bool PlayerbotAI::CanCastSpell(uint32 spellid, GameObject* goTarget, bool checkH return false; } -bool PlayerbotAI::CanCastSpell(uint32 spellid, float x, float y, float z, bool checkHasSpell, - Item* itemTarget) +bool PlayerbotAI::CanCastSpell(uint32 spellid, float x, float y, float z, bool checkHasSpell, Item* itemTarget) { if (!spellid) return false; @@ -3029,7 +3085,7 @@ bool PlayerbotAI::CanCastSpell(uint32 spellid, float x, float y, float z, bool c Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); spell->m_targets.SetDst(x, y, z, 0.f); - + Item* item = itemTarget ? itemTarget : aiObjectContext->GetValue("item for spell", spellid)->Get(); spell->m_targets.SetItemTarget(item); @@ -3133,7 +3189,8 @@ bool PlayerbotAI::CastSpell(uint32 spellId, Unit* target, Item* itemTarget) Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); SpellCastTargets targets; - if (spellInfo->Targets & TARGET_FLAG_ITEM) + if (spellInfo->Effects[0].Effect != SPELL_EFFECT_OPEN_LOCK && + (spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM)) { Item* item = itemTarget ? itemTarget : aiObjectContext->GetValue("item for spell", spellId)->Get(); targets.SetItemTarget(item); @@ -3176,6 +3233,20 @@ bool PlayerbotAI::CastSpell(uint32 spellId, Unit* target, Item* itemTarget) targets.SetGOTarget(go); faceTo = go; } + else if (itemTarget) + { + Player* trader = bot->GetTrader(); + if (trader) + { + targets.SetTradeItemTarget(bot); + targets.SetUnitTarget(bot); + faceTo = trader; + } + else + { + targets.SetItemTarget(itemTarget); + } + } else { if (Unit* creature = GetUnit(loot.guid)) @@ -3212,6 +3283,58 @@ bool PlayerbotAI::CastSpell(uint32 spellId, Unit* target, Item* itemTarget) // LOG_DEBUG("playerbots", "Spell cast failed. - target name: {}, spellid: {}, bot name: {}, result: {}", // target->GetName(), spellId, bot->GetName(), result); // } + if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) + { + std::ostringstream out; + out << "Spell cast failed - "; + out << "Spell ID: " << spellId << " (" << ChatHelper::FormatSpell(spellInfo) << "), "; + out << "Error Code: " << static_cast(result) << " (0x" << std::hex << static_cast(result) << std::dec << "), "; + out << "Bot: " << bot->GetName() << ", "; + + // Check spell target type + if (targets.GetUnitTarget()) + { + out << "Target: Unit (" << targets.GetUnitTarget()->GetName() + << ", Low GUID: " << targets.GetUnitTarget()->GetGUID().GetCounter() + << ", High GUID: " << static_cast(targets.GetUnitTarget()->GetGUID().GetHigh()) << "), "; + } + + if (targets.GetGOTarget()) + { + out << "Target: GameObject (Low GUID: " << targets.GetGOTarget()->GetGUID().GetCounter() + << ", High GUID: " << static_cast(targets.GetGOTarget()->GetGUID().GetHigh()) << "), "; + } + + if (targets.GetItemTarget()) + { + out << "Target: Item (Low GUID: " << targets.GetItemTarget()->GetGUID().GetCounter() + << ", High GUID: " << static_cast(targets.GetItemTarget()->GetGUID().GetHigh()) << "), "; + } + + // Check if bot is in trade mode + if (bot->GetTradeData()) + { + out << "Trade Mode: Active, "; + Item* tradeItem = bot->GetTradeData()->GetTraderData()->GetItem(TRADE_SLOT_NONTRADED); + if (tradeItem) + { + out << "Trade Item: " << tradeItem->GetEntry() + << " (Low GUID: " << tradeItem->GetGUID().GetCounter() + << ", High GUID: " << static_cast(tradeItem->GetGUID().GetHigh()) << "), "; + } + else + { + out << "Trade Item: None, "; + } + } + else + { + out << "Trade Mode: Inactive, "; + } + + TellMasterNoFacing(out); + } + return false; } // if (spellInfo->Effects[0].Effect == SPELL_EFFECT_OPEN_LOCK || spellInfo->Effects[0].Effect == @@ -3308,7 +3431,7 @@ bool PlayerbotAI::CastSpell(uint32 spellId, float x, float y, float z, Item* ite Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); SpellCastTargets targets; - if (spellInfo->Targets & TARGET_FLAG_ITEM) + if (spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM) { Item* item = itemTarget ? itemTarget : aiObjectContext->GetValue("item for spell", spellId)->Get(); targets.SetItemTarget(item); @@ -4188,8 +4311,8 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) mod = AutoScaleActivity(mod); } - uint32 ActivityNumber = GetFixedBotNumer(100, - sPlayerbotAIConfig->botActiveAlone * static_cast(mod) / 100 * 0.01f); + uint32 ActivityNumber = + GetFixedBotNumer(100, sPlayerbotAIConfig->botActiveAlone * static_cast(mod) / 100 * 0.01f); return ActivityNumber <= (sPlayerbotAIConfig->botActiveAlone * mod) / @@ -4248,7 +4371,8 @@ void PlayerbotAI::RemoveShapeshift() RemoveAura("moonkin form"); RemoveAura("travel form"); RemoveAura("cat form"); - RemoveAura("flight form"); bot->RemoveAura(33943); // The latter added for now as RemoveAura("flight form") currently does not work. + RemoveAura("flight form"); + bot->RemoveAura(33943); // The latter added for now as RemoveAura("flight form") currently does not work. RemoveAura("swift flight form"); RemoveAura("aquatic form"); RemoveAura("ghost wolf"); @@ -4835,11 +4959,9 @@ Item* PlayerbotAI::FindAmmo() const } // Search inventory for the correct ammo type - return FindItemInInventory([requiredAmmoType](ItemTemplate const* pItemProto) -> bool - { - return pItemProto->Class == ITEM_CLASS_PROJECTILE && - pItemProto->SubClass == requiredAmmoType; - }); + return FindItemInInventory( + [requiredAmmoType](ItemTemplate const* pItemProto) -> bool + { return pItemProto->Class == ITEM_CLASS_PROJECTILE && pItemProto->SubClass == requiredAmmoType; }); } return nullptr; // No ranged weapon equipped @@ -4864,6 +4986,52 @@ Item* PlayerbotAI::FindBandage() const { return pItemProto->Class == ITEM_CLASS_CONSUMABLE && pItemProto->SubClass == ITEM_SUBCLASS_BANDAGE; }); } +Item* PlayerbotAI::FindOpenableItem() const +{ + return FindItemInInventory([this](ItemTemplate const* itemTemplate) -> bool + { + return (itemTemplate->Flags & ITEM_FLAG_HAS_LOOT) && + (itemTemplate->LockID == 0 || !this->bot->GetItemByEntry(itemTemplate->ItemId)->IsLocked()); + }); +} + +Item* PlayerbotAI::FindLockedItem() const +{ + return FindItemInInventory([this](ItemTemplate const* itemTemplate) -> bool + { + if (!this->bot->HasSkill(SKILL_LOCKPICKING)) // Ensure bot has Lockpicking skill + return false; + + if (itemTemplate->LockID == 0) // Ensure the item is actually locked + return false; + + Item* item = this->bot->GetItemByEntry(itemTemplate->ItemId); + if (!item || !item->IsLocked()) // Ensure item instance is locked + return false; + + // Check if bot has enough Lockpicking skill + LockEntry const* lockInfo = sLockStore.LookupEntry(itemTemplate->LockID); + if (!lockInfo) + return false; + + for (uint8 j = 0; j < 8; ++j) + { + if (lockInfo->Type[j] == LOCK_KEY_SKILL) + { + uint32 skillId = SkillByLockType(LockType(lockInfo->Index[j])); + if (skillId == SKILL_LOCKPICKING) + { + uint32 requiredSkill = lockInfo->Skill[j]; + uint32 botSkill = this->bot->GetSkillValue(SKILL_LOCKPICKING); + return botSkill >= requiredSkill; + } + } + } + + return false; + }); +} + static const uint32 uPriorizedSharpStoneIds[8] = {ADAMANTITE_SHARPENING_DISPLAYID, FEL_SHARPENING_DISPLAYID, ELEMENTAL_SHARPENING_DISPLAYID, DENSE_SHARPENING_DISPLAYID, SOLID_SHARPENING_DISPLAYID, HEAVY_SHARPENING_DISPLAYID, @@ -6024,9 +6192,10 @@ bool PlayerbotAI::IsHealingSpell(uint32 spellFamilyName, flag96 spellFalimyFlags return spellFalimyFlags & healingFlags; } - -SpellFamilyNames PlayerbotAI::Class2SpellFamilyName(uint8 cls) { - switch (cls) { +SpellFamilyNames PlayerbotAI::Class2SpellFamilyName(uint8 cls) +{ + switch (cls) + { case CLASS_WARRIOR: return SPELLFAMILY_WARRIOR; case CLASS_PALADIN: diff --git a/modules/mod-playerbots/src/PlayerbotAI.h b/modules/mod-playerbots/src/PlayerbotAI.h index b6a71c7..4e7c5a5 100644 --- a/modules/mod-playerbots/src/PlayerbotAI.h +++ b/modules/mod-playerbots/src/PlayerbotAI.h @@ -13,8 +13,10 @@ #include "ChatFilter.h" #include "ChatHelper.h" #include "Common.h" +#include "CreatureData.h" #include "Event.h" #include "Item.h" +#include "NewRpgInfo.h" #include "NewRpgStrategy.h" #include "PlayerbotAIBase.h" #include "PlayerbotAIConfig.h" @@ -390,6 +392,8 @@ public: void HandleMasterOutgoingPacket(WorldPacket const& packet); void HandleTeleportAck(); void ChangeEngine(BotState type); + void ChangeEngineOnCombat(); + void ChangeEngineOnNonCombat(); void DoNextAction(bool minimal = false); virtual bool DoSpecificAction(std::string const name, Event event = Event(), bool silent = false, std::string const qualifier = ""); @@ -435,7 +439,8 @@ public: const AreaTableEntry* GetCurrentArea(); const AreaTableEntry* GetCurrentZone(); static std::string GetLocalizedAreaName(const AreaTableEntry* entry); - + static std::string GetLocalizedCreatureName(uint32 entry); + static std::string GetLocalizedGameObjectName(uint32 entry); bool TellMaster(std::ostringstream& stream, PlayerbotSecurityLevel securityLevel = PLAYERBOT_SECURITY_ALLOW_ALL); bool TellMaster(std::string const text, PlayerbotSecurityLevel securityLevel = PLAYERBOT_SECURITY_ALLOW_ALL); bool TellMasterNoFacing(std::ostringstream& stream, @@ -464,6 +469,8 @@ public: Item* FindPoison() const; Item* FindAmmo() const; Item* FindBandage() const; + Item* FindOpenableItem() const; + Item* FindLockedItem() const; Item* FindConsumable(uint32 displayId) const; Item* FindStoneFor(Item* weapon) const; Item* FindOilFor(Item* weapon) const; @@ -579,6 +586,8 @@ public: static bool IsHealingSpell(uint32 spellFamilyName, flag96 spelFalimyFlags); static SpellFamilyNames Class2SpellFamilyName(uint8 cls); NewRpgInfo rpgInfo; + NewRpgStatistic rpgStatistic; + std::unordered_set lowPriorityQuest; private: static void _fillGearScoreData(Player* player, Item* item, std::vector* gearScore, uint32& twoHandScore, diff --git a/modules/mod-playerbots/src/PlayerbotMgr.cpp b/modules/mod-playerbots/src/PlayerbotMgr.cpp index a4faaea..ba3acea 100644 --- a/modules/mod-playerbots/src/PlayerbotMgr.cpp +++ b/modules/mod-playerbots/src/PlayerbotMgr.cpp @@ -91,7 +91,7 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId LOG_DEBUG("playerbots", "PlayerbotMgr not found for master player with GUID: {}", masterPlayer->GetGUID().GetRawValue()); return; } - uint32 count = mgr->GetPlayerbotsCount(); + uint32 count = mgr->GetPlayerbotsCount() + botLoading.size(); if (count >= sPlayerbotAIConfig->maxAddedBots) { allowed = false; @@ -638,11 +638,22 @@ std::string const PlayerbotHolder::ProcessBotCommand(std::string const cmd, Obje bool isRandomAccount = sPlayerbotAIConfig->IsInRandomAccountList(botAccount); bool isMasterAccount = (masterAccountId == botAccount); - if (cmd == "add" || cmd == "login") + if (cmd == "add" || cmd == "addaccount" || cmd == "login") { if (ObjectAccessor::FindPlayer(guid)) return "player already logged in"; + // For addaccount command, verify it's an account name + if (cmd == "addaccount") + { + uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(guid); + if (!accountId) + return "character not found"; + + if (!sPlayerbotAIConfig->allowAccountBots && accountId != masterAccountId) + return "you can only add bots from your own account"; + } + AddPlayerBot(guid, masterAccountId); return "ok"; } @@ -816,7 +827,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg if (!*args) { - messages.push_back("usage: list/reload/tweak/self or add/init/remove PLAYERNAME\n"); + messages.push_back("usage: list/reload/tweak/self or add/addaccount/init/remove PLAYERNAME\n"); messages.push_back("usage: addclass CLASSNAME"); return messages; } @@ -1130,22 +1141,48 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg { std::string const s = *i; - uint32 accountId = GetAccountId(s); - if (!accountId) + if (!strcmp(cmd, "addaccount")) { - bots.insert(s); - continue; - } - - QueryResult results = CharacterDatabase.Query("SELECT name FROM characters WHERE account = {}", accountId); - if (results) - { - do + // When using addaccount, first try to get account ID directly + uint32 accountId = GetAccountId(s); + if (!accountId) { - Field* fields = results->Fetch(); - std::string const charName = fields[0].Get(); - bots.insert(charName); - } while (results->NextRow()); + // If not found, try to get account ID from character name + ObjectGuid charGuid = sCharacterCache->GetCharacterGuidByName(s); + if (!charGuid) + { + messages.push_back("Neither account nor character '" + s + "' found"); + continue; + } + accountId = sCharacterCache->GetCharacterAccountIdByGuid(charGuid); + if (!accountId) + { + messages.push_back("Could not find account for character '" + s + "'"); + continue; + } + } + + QueryResult results = CharacterDatabase.Query("SELECT name FROM characters WHERE account = {}", accountId); + if (results) + { + do + { + Field* fields = results->Fetch(); + std::string const charName = fields[0].Get(); + bots.insert(charName); + } while (results->NextRow()); + } + } + else + { + // For regular add command, only add the specific character + ObjectGuid charGuid = sCharacterCache->GetCharacterGuidByName(s); + if (!charGuid) + { + messages.push_back("Character '" + s + "' not found"); + continue; + } + bots.insert(s); } } diff --git a/modules/mod-playerbots/src/RandomPlayerbotMgr.cpp b/modules/mod-playerbots/src/RandomPlayerbotMgr.cpp index fd8e3fa..ca160a6 100644 --- a/modules/mod-playerbots/src/RandomPlayerbotMgr.cpp +++ b/modules/mod-playerbots/src/RandomPlayerbotMgr.cpp @@ -31,6 +31,7 @@ #include "GuildTaskMgr.h" #include "LFGMgr.h" #include "MapMgr.h" +#include "NewRpgInfo.h" #include "NewRpgStrategy.h" #include "PerformanceMonitor.h" #include "Player.h" @@ -49,6 +50,12 @@ #include "World.h" #include "RandomPlayerbotFactory.h" +struct GuidClassRaceInfo { + ObjectGuid::LowType guid; + uint32 rClass; + uint32 rRace; +}; + void PrintStatsThread() { sRandomPlayerbotMgr->PrintStats(); } void activatePrintStatsThread() @@ -371,7 +378,7 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) sRandomPlayerbotMgr->CheckLfgQueue(); } - if (time(nullptr) > (printStatsTimer + 300)) + if (sPlayerbotAIConfig->randomBotAutologin && time(nullptr) > (printStatsTimer + 300)) { if (!printStatsTimer) { @@ -464,9 +471,17 @@ uint32 RandomPlayerbotMgr::AddRandomBots() { maxAllowedBotCount -= currentBots.size(); maxAllowedBotCount = std::min(sPlayerbotAIConfig->randomBotsPerInterval, maxAllowedBotCount); + + uint32 totalRatio = sPlayerbotAIConfig->randomBotAllianceRatio + sPlayerbotAIConfig->randomBotHordeRatio; + uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) / totalRatio; + + uint32 remainder = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) % totalRatio; + + // Fix #1082: Randomly add one based on reminder + if (remainder && urand(1, totalRatio) <= remainder) { + allowedAllianceCount++; + } - uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) / - (sPlayerbotAIConfig->randomBotAllianceRatio + sPlayerbotAIConfig->randomBotHordeRatio); uint32 allowedHordeCount = maxAllowedBotCount - allowedAllianceCount; for (std::vector::iterator i = sPlayerbotAIConfig->randomBotAccounts.begin(); @@ -492,11 +507,28 @@ uint32 RandomPlayerbotMgr::AddRandomBots() PreparedQueryResult result = CharacterDatabase.Query(stmt); if (!result) continue; - std::vector guids; - do - { + + std::vector allGuidInfos; + + do { Field* fields = result->Fetch(); - ObjectGuid::LowType guid = fields[0].Get(); + GuidClassRaceInfo info; + info.guid = fields[0].Get(); + info.rClass = fields[1].Get(); + info.rRace = fields[2].Get(); + allGuidInfos.push_back(info); + } while (result->NextRow()); + + // random shuffle for class balance + std::mt19937 rnd(time(0)); + std::shuffle(allGuidInfos.begin(), allGuidInfos.end(), rnd); + + std::vector guids; + for (const auto& info : allGuidInfos) { + ObjectGuid::LowType guid = info.guid; + uint32 rClass = info.rClass; + uint32 rRace = info.rRace; + if (GetEventValue(guid, "add")) continue; @@ -509,40 +541,24 @@ uint32 RandomPlayerbotMgr::AddRandomBots() if (std::find(currentBots.begin(), currentBots.end(), guid) != currentBots.end()) continue; - if (sPlayerbotAIConfig->disableDeathKnightLogin) - { - uint32 rClass = fields[1].Get(); - if (rClass == CLASS_DEATH_KNIGHT) - { + if (sPlayerbotAIConfig->disableDeathKnightLogin) { + if (rClass == CLASS_DEATH_KNIGHT) { continue; } } - uint32 rRace = fields[2].Get(); + uint32 isAlliance = IsAlliance(rRace); - if (!allowedAllianceCount && isAlliance) - { + bool factionNotAllowed = (!allowedAllianceCount && isAlliance) || (!allowedHordeCount && !isAlliance); + + if (factionNotAllowed) continue; - } - if (!allowedHordeCount && !isAlliance) - { - continue; - } - if (isAlliance) - { + + if (isAlliance) { allowedAllianceCount--; - } - else - { + } else { allowedHordeCount--; } - guids.push_back(guid); - } while (result->NextRow()); - std::mt19937 rnd(time(0)); - std::shuffle(guids.begin(), guids.end(), rnd); - - for (uint32& guid : guids) - { uint32 add_time = sPlayerbotAIConfig->enablePeriodicOnlineOffline ? urand(sPlayerbotAIConfig->minRandomBotInWorldTime, sPlayerbotAIConfig->maxRandomBotInWorldTime) @@ -1466,6 +1482,82 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& tlocs.size()); } +void RandomPlayerbotMgr::PrepareZone2LevelBracket() +{ + // Classic WoW - Low - level zones + zone2LevelBracket[1] = {5, 12}; // Dun Morogh + zone2LevelBracket[12] = {5, 12}; // Elwynn Forest + zone2LevelBracket[14] = {5, 12}; // Durotar + zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades + zone2LevelBracket[141] = {5, 12}; // Teldrassil + zone2LevelBracket[215] = {5, 12}; // Mulgore + zone2LevelBracket[3430] = {5, 12}; // Eversong Woods + zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle + + // Classic WoW - Mid - level zones + zone2LevelBracket[17] = {10, 25}; // Barrens + zone2LevelBracket[38] = {10, 20}; // Loch Modan + zone2LevelBracket[40] = {10, 21}; // Westfall + zone2LevelBracket[130] = {10, 23}; // Silverpine Forest + zone2LevelBracket[148] = {10, 21}; // Darkshore + zone2LevelBracket[3433] = {10, 22}; // Ghostlands + zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle + + // Classic WoW - High - level zones + zone2LevelBracket[10] = {19, 33}; // Deadwind Pass + zone2LevelBracket[11] = {21, 30}; // Wetlands + zone2LevelBracket[44] = {16, 28}; // Redridge Mountains + zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills + zone2LevelBracket[331] = {18, 33}; // Ashenvale + zone2LevelBracket[400] = {24, 36}; // Thousand Needles + zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains + + // Classic WoW - Higher - level zones + zone2LevelBracket[3] = {36, 46}; // Badlands + zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows + zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh + zone2LevelBracket[16] = {45, 52}; // Azshara + zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale + zone2LevelBracket[45] = {30, 42}; // Arathi Highlands + zone2LevelBracket[47] = {42, 51}; // Hinterlands + zone2LevelBracket[51] = {45, 51}; // Searing Gorge + zone2LevelBracket[357] = {40, 52}; // Feralas + zone2LevelBracket[405] = {30, 41}; // Desolace + zone2LevelBracket[440] = {41, 52}; // Tanaris + + // Classic WoW - Top - level zones + zone2LevelBracket[4] = {52, 57}; // Blasted Lands + zone2LevelBracket[28] = {50, 60}; // Western Plaguelands + zone2LevelBracket[46] = {51, 60}; // Burning Steppes + zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands + zone2LevelBracket[361] = {47, 57}; // Felwood + zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater + zone2LevelBracket[618] = {54, 61}; // Winterspring + zone2LevelBracket[1377] = {54, 63}; // Silithus + + // The Burning Crusade - Zones + zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula + zone2LevelBracket[3518] = {64, 70}; // Nagrand + zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest + zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley + zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh + zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains + zone2LevelBracket[3523] = {67, 73}; // Netherstorm + zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas + + // Wrath of the Lich King - Zones + zone2LevelBracket[65] = {71, 77}; // Dragonblight + zone2LevelBracket[66] = {74, 80}; // Zul'Drak + zone2LevelBracket[67] = {77, 80}; // Storm Peaks + zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier + zone2LevelBracket[394] = {72, 78}; // Grizzly Hills + zone2LevelBracket[495] = {68, 74}; // Howling Fjord + zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest + zone2LevelBracket[3537] = {68, 75}; // Borean Tundra + zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin + zone2LevelBracket[4197] = {79, 80}; // Wintergrasp +} + void RandomPlayerbotMgr::PrepareTeleportCache() { uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); @@ -1542,6 +1634,7 @@ void RandomPlayerbotMgr::PrepareTeleportCache() if (sPlayerbotAIConfig->enableNewRpgStrategy) { + PrepareZone2LevelBracket(); LOG_INFO("playerbots", "Preparing innkeepers locations for level..."); results = WorldDatabase.Query( "SELECT " @@ -1550,8 +1643,7 @@ void RandomPlayerbotMgr::PrepareTeleportCache() "position_y, " "position_z, " "orientation, " - "t.faction, " - "t.entry " + "t.faction " "FROM " "creature c " "INNER JOIN creature_template t on c.id1 = t.entry " @@ -1573,7 +1665,6 @@ void RandomPlayerbotMgr::PrepareTeleportCache() float z = fields[3].Get(); float orient = fields[4].Get(); uint32 faction = fields[5].Get(); - uint32 c_entry = fields[6].Get(); const FactionTemplateEntry* entry = sFactionTemplateStore.LookupEntry(faction); WorldLocation loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); @@ -1581,35 +1672,13 @@ void RandomPlayerbotMgr::PrepareTeleportCache() Map* map = sMapMgr->FindMap(loc.GetMapId(), 0); if (!map) continue; - const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(1, x, y, z)); + const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); uint32 zoneId = area->zone ? area->zone : area->ID; - uint32 level = area->area_level; - for (int i = 5; i <= maxLevel; i++) + if (zone2LevelBracket.find(zoneId) == zone2LevelBracket.end()) + continue; + LevelBracket bracket = zone2LevelBracket[zoneId]; + for (int i = bracket.low; i <= bracket.high; i++) { - std::vector& locs = locsPerLevelCache[i]; - int counter = 0; - WorldLocation levelLoc; - for (auto& checkLoc : locs) - { - if (loc.GetMapId() != checkLoc.GetMapId()) - continue; - - if (loc.GetExactDist(checkLoc) > 1500.0f) - continue; - - if (zoneId != - map->GetZoneId(1, checkLoc.GetPositionX(), checkLoc.GetPositionY(), checkLoc.GetPositionZ())) - continue; - - counter++; - levelLoc = checkLoc; - if (counter >= 15) - break; - } - - if (counter < 15) - continue; - if (!(entry->hostileMask & 4)) { hordeStarterPerLevelCache[i].push_back(loc); @@ -1618,12 +1687,10 @@ void RandomPlayerbotMgr::PrepareTeleportCache() { allianceStarterPerLevelCache[i].push_back(loc); } - LOG_DEBUG("playerbots", "Area: {} Level: {} creature_entry: {} add to: {} {}({},{},{},{})", area->ID, - level, c_entry, i, counter, levelLoc.GetPositionX(), levelLoc.GetPositionY(), - levelLoc.GetPositionZ(), levelLoc.GetMapId()); } } while (results->NextRow()); } + // add all initial position for (uint32 i = 1; i < MAX_RACES; i++) { @@ -2629,9 +2696,8 @@ void RandomPlayerbotMgr::PrintStats() uint32 engine_noncombat = 0; uint32 engine_combat = 0; uint32 engine_dead = 0; - uint32 stateCount[MAX_TRAVEL_STATE + 1] = {0}; - std::vector> questCount; std::unordered_map rpgStatusCount; + NewRpgStatistic rpgStasticTotal; std::unordered_map zoneCount; uint8 maxBotLevel = 0; for (PlayerBotMap::iterator i = playerBots.begin(); i != playerBots.end(); ++i) @@ -2706,41 +2772,19 @@ void RandomPlayerbotMgr::PrintStats() zoneCount[bot->GetZoneId()]++; if (sPlayerbotAIConfig->enableNewRpgStrategy) - rpgStatusCount[botAI->rpgInfo.status]++; - - if (TravelTarget* target = botAI->GetAiObjectContext()->GetValue("travel target")->Get()) { - TravelState state = target->getTravelState(); - stateCount[state]++; - - Quest const* quest; - if (target->getDestination()) - quest = target->getDestination()->GetQuestTemplate(); - - if (quest) - { - bool found = false; - - for (auto& q : questCount) - { - if (q.first != quest) - continue; - - q.second++; - found = true; - } - - if (!found) - questCount.push_back(std::make_pair(quest, 1)); - } + rpgStatusCount[botAI->rpgInfo.status]++; + rpgStasticTotal += botAI->rpgStatistic; } } + LOG_INFO("playerbots", "Bots level:"); // uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - uint32 currentAlliance = 0, currentHorde = 0; - uint32 step = std::max(1, (maxBotLevel + 4) / 8); - uint32 from = 1; + uint32_t currentAlliance = 0, currentHorde = 0; + uint32_t step = std::max(1, static_cast((maxBotLevel + 4) / 8)); + uint32_t from = 1; + for (uint8 i = 1; i <= maxBotLevel; ++i) { currentAlliance += alliance[i]; @@ -2800,19 +2844,18 @@ void RandomPlayerbotMgr::PrintStats() if (sPlayerbotAIConfig->enableNewRpgStrategy) { - LOG_INFO("playerbots", "Bots rpg status:", dead); - LOG_INFO("playerbots", " IDLE: {}", rpgStatusCount[NewRpgStatus::IDLE]); - LOG_INFO("playerbots", " REST: {}", rpgStatusCount[NewRpgStatus::REST]); - LOG_INFO("playerbots", " GO_GRIND: {}", rpgStatusCount[NewRpgStatus::GO_GRIND]); - LOG_INFO("playerbots", " GO_INNKEEPER: {}", rpgStatusCount[NewRpgStatus::GO_INNKEEPER]); - LOG_INFO("playerbots", " NEAR_RANDOM: {}", rpgStatusCount[NewRpgStatus::NEAR_RANDOM]); - LOG_INFO("playerbots", " NEAR_NPC: {}", rpgStatusCount[NewRpgStatus::NEAR_NPC]); + LOG_INFO("playerbots", "Bots rpg status:"); + LOG_INFO("playerbots", " Idle: {}, Rest: {}, GoGrind: {}, GoInnkeeper: {}, MoveRandom: {}, MoveNpc: {}, DoQuest: {}", + rpgStatusCount[RPG_IDLE], rpgStatusCount[RPG_REST], rpgStatusCount[RPG_GO_GRIND], rpgStatusCount[RPG_GO_INNKEEPER], + rpgStatusCount[RPG_NEAR_RANDOM], rpgStatusCount[RPG_NEAR_NPC], rpgStatusCount[RPG_DO_QUEST]); + + LOG_INFO("playerbots", "Bots total quests:"); + LOG_INFO("playerbots", " Accepted: {}, Rewarded: {}, Dropped: {}", + rpgStasticTotal.questAccepted, rpgStasticTotal.questRewarded, rpgStasticTotal.questDropped); } LOG_INFO("playerbots", "Bots engine:", dead); - LOG_INFO("playerbots", " Non-combat: {}", engine_noncombat); - LOG_INFO("playerbots", " Combat: {}", engine_combat); - LOG_INFO("playerbots", " Dead: {}", engine_dead); + LOG_INFO("playerbots", " Non-combat: {}, Combat: {}, Dead: {}", engine_noncombat, engine_combat, engine_dead); // LOG_INFO("playerbots", "Bots questing:"); // LOG_INFO("playerbots", " Picking quests: {}", diff --git a/modules/mod-playerbots/src/RandomPlayerbotMgr.h b/modules/mod-playerbots/src/RandomPlayerbotMgr.h index 4206a97..fc9ab95 100644 --- a/modules/mod-playerbots/src/RandomPlayerbotMgr.h +++ b/modules/mod-playerbots/src/RandomPlayerbotMgr.h @@ -171,12 +171,19 @@ public: static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; } void PrepareAddclassCache(); + void PrepareZone2LevelBracket(); void PrepareTeleportCache(); void Init(); std::map> addclassCache; std::map> locsPerLevelCache; std::map> allianceStarterPerLevelCache; std::map> hordeStarterPerLevelCache; + struct LevelBracket { + uint32 low; + uint32 high; + bool InsideBracket(uint32 val) { return val >= low && val <= high; } + }; + std::map zone2LevelBracket; std::map> bankerLocsPerLevelCache; protected: void OnBotLoginInternal(Player* const bot) override; diff --git a/modules/mod-playerbots/src/factory/PlayerbotFactory.cpp b/modules/mod-playerbots/src/factory/PlayerbotFactory.cpp index c98bf5c..f45b525 100644 --- a/modules/mod-playerbots/src/factory/PlayerbotFactory.cpp +++ b/modules/mod-playerbots/src/factory/PlayerbotFactory.cpp @@ -31,6 +31,7 @@ #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" #include "Playerbots.h" +#include "QuestDef.h" #include "RandomItemMgr.h" #include "RandomPlayerbotFactory.h" #include "ReputationMgr.h" @@ -979,30 +980,23 @@ void PlayerbotFactory::ClearSpells() void PlayerbotFactory::ResetQuests() { + for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) + { + bot->SetQuestSlot(slot, 0); + } ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates(); for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i) { Quest const* quest = i->second; uint32 entry = quest->GetQuestId(); + if (bot->GetQuestStatus(entry) == QUEST_STATUS_NONE) + continue; + + bot->RemoveRewardedQuest(entry); + bot->RemoveActiveQuest(entry, false); - // remove all quest entries for 'entry' from quest log - for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) - { - uint32 quest = bot->GetQuestSlotQuestId(slot); - if (quest == entry) - { - bot->SetQuestSlot(slot, 0); - } - } - - // reset rewarded for restart repeatable quest - bot->getQuestStatusMap().erase(entry); - // bot->getQuestStatusMap()[entry].m_rewarded = false; - // bot->getQuestStatusMap()[entry].m_status = QUEST_STATUS_NONE; } - // bot->UpdateForQuestWorldObjects(); - CharacterDatabase.Execute("DELETE FROM character_queststatus WHERE guid = {}", bot->GetGUID().GetCounter()); } void PlayerbotFactory::InitSpells() { InitAvailableSpells(); } @@ -1618,6 +1612,11 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (level < 20 && (slot == EQUIPMENT_SLOT_FINGER1 || slot == EQUIPMENT_SLOT_FINGER2)) continue; + if (level < 5 && (slot != EQUIPMENT_SLOT_MAINHAND) && (slot != EQUIPMENT_SLOT_OFFHAND) && + (slot != EQUIPMENT_SLOT_FEET) && (slot != EQUIPMENT_SLOT_LEGS) && (slot != EQUIPMENT_SLOT_CHEST) && + (slot != EQUIPMENT_SLOT_RANGED)) + continue; + Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); if (second_chance && oldItem) @@ -1782,6 +1781,10 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (level < 20 && (slot == EQUIPMENT_SLOT_FINGER1 || slot == EQUIPMENT_SLOT_FINGER2)) continue; + + if (level < 5 && (slot != EQUIPMENT_SLOT_MAINHAND) && (slot != EQUIPMENT_SLOT_OFFHAND) && + (slot != EQUIPMENT_SLOT_FEET) && (slot != EQUIPMENT_SLOT_LEGS) && (slot != EQUIPMENT_SLOT_CHEST)) + continue; if (Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); diff --git a/modules/mod-playerbots/src/strategy/actions/AcceptQuestAction.cpp b/modules/mod-playerbots/src/strategy/actions/AcceptQuestAction.cpp index 7408503..82b63c6 100644 --- a/modules/mod-playerbots/src/strategy/actions/AcceptQuestAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/AcceptQuestAction.cpp @@ -87,7 +87,7 @@ bool AcceptQuestAction::Execute(Event event) { std::stringstream ss; ss << "AcceptQuestAction [" << qInfo->GetTitle() << "] - [" << std::to_string(qInfo->GetQuestId()) << "]"; - LOG_INFO("playerbots", "{}", ss.str().c_str()); + LOG_DEBUG("playerbots", "{}", ss.str().c_str()); // botAI->TellMaster(ss.str()); } diff --git a/modules/mod-playerbots/src/strategy/actions/ActionContext.h b/modules/mod-playerbots/src/strategy/actions/ActionContext.h index 0ffe08e..a56e711 100644 --- a/modules/mod-playerbots/src/strategy/actions/ActionContext.h +++ b/modules/mod-playerbots/src/strategy/actions/ActionContext.h @@ -135,6 +135,7 @@ public: creators["move to loot"] = &ActionContext::move_to_loot; creators["open loot"] = &ActionContext::open_loot; creators["guard"] = &ActionContext::guard; + creators["return to stay position"] = &ActionContext::return_to_stay_position; creators["move out of enemy contact"] = &ActionContext::move_out_of_enemy_contact; creators["set facing"] = &ActionContext::set_facing; creators["set behind"] = &ActionContext::set_behind; @@ -247,6 +248,7 @@ public: creators["new rpg go innkeeper"] = &ActionContext::new_rpg_go_innkeeper; creators["new rpg move random"] = &ActionContext::new_rpg_move_random; creators["new rpg move npc"] = &ActionContext::new_rpg_move_npc; + creators["new rpg do quest"] = &ActionContext::new_rpg_do_quest; } private: @@ -270,6 +272,7 @@ private: static Action* drop_target(PlayerbotAI* botAI) { return new DropTargetAction(botAI); } static Action* attack_duel_opponent(PlayerbotAI* botAI) { return new AttackDuelOpponentAction(botAI); } static Action* guard(PlayerbotAI* botAI) { return new GuardAction(botAI); } + static Action* return_to_stay_position(PlayerbotAI* botAI) { return new ReturnToStayPositionAction(botAI); } static Action* open_loot(PlayerbotAI* botAI) { return new OpenLootAction(botAI); } static Action* move_to_loot(PlayerbotAI* botAI) { return new MoveToLootAction(botAI); } static Action* _return(PlayerbotAI* botAI) { return new ReturnAction(botAI); } @@ -428,6 +431,7 @@ private: static Action* new_rpg_go_innkeeper(PlayerbotAI* ai) { return new NewRpgGoInnKeeperAction(ai); } static Action* new_rpg_move_random(PlayerbotAI* ai) { return new NewRpgMoveRandomAction(ai); } static Action* new_rpg_move_npc(PlayerbotAI* ai) { return new NewRpgMoveNpcAction(ai); } + static Action* new_rpg_do_quest(PlayerbotAI* ai) { return new NewRpgDoQuestAction(ai); } }; #endif diff --git a/modules/mod-playerbots/src/strategy/actions/ChatActionContext.h b/modules/mod-playerbots/src/strategy/actions/ChatActionContext.h index f4cd9cb..7b7e10b 100644 --- a/modules/mod-playerbots/src/strategy/actions/ChatActionContext.h +++ b/modules/mod-playerbots/src/strategy/actions/ChatActionContext.h @@ -75,6 +75,8 @@ #include "WhoAction.h" #include "WtsAction.h" #include "OpenItemAction.h" +#include "UnlockItemAction.h" +#include "UnlockTradedItemAction.h" class ChatActionContext : public NamedObjectContext { @@ -82,6 +84,8 @@ public: ChatActionContext() { creators["open items"] = &ChatActionContext::open_items; + creators["unlock items"] = &ChatActionContext::unlock_items; + creators["unlock traded item"] = &ChatActionContext::unlock_traded_item; creators["range"] = &ChatActionContext::range; creators["stats"] = &ChatActionContext::stats; creators["quests"] = &ChatActionContext::quests; @@ -90,6 +94,7 @@ public: creators["log"] = &ChatActionContext::log; creators["los"] = &ChatActionContext::los; creators["rpg status"] = &ChatActionContext::rpg_status; + creators["rpg do quest"] = &ChatActionContext::rpg_do_quest; creators["aura"] = &ChatActionContext::aura; creators["drop"] = &ChatActionContext::drop; creators["clean quest log"] = &ChatActionContext::clean_quest_log; @@ -183,6 +188,8 @@ public: private: static Action* open_items(PlayerbotAI* botAI) { return new OpenItemAction(botAI); } + static Action* unlock_items(PlayerbotAI* botAI) { return new UnlockItemAction(botAI); } + static Action* unlock_traded_item(PlayerbotAI* botAI) { return new UnlockTradedItemAction(botAI); } static Action* range(PlayerbotAI* botAI) { return new RangeAction(botAI); } static Action* flag(PlayerbotAI* botAI) { return new FlagAction(botAI); } static Action* craft(PlayerbotAI* botAI) { return new SetCraftAction(botAI); } @@ -261,6 +268,7 @@ private: static Action* log(PlayerbotAI* botAI) { return new LogLevelAction(botAI); } static Action* los(PlayerbotAI* botAI) { return new TellLosAction(botAI); } static Action* rpg_status(PlayerbotAI* botAI) { return new TellRpgStatusAction(botAI); } + static Action* rpg_do_quest(PlayerbotAI* botAI) { return new StartRpgDoQuestAction(botAI); } static Action* aura(PlayerbotAI* ai) { return new TellAuraAction(ai); } static Action* ll(PlayerbotAI* botAI) { return new LootStrategyAction(botAI); } static Action* ss(PlayerbotAI* botAI) { return new SkipSpellsListAction(botAI); } diff --git a/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.cpp b/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.cpp index 30d4c2f..f7e0216 100644 --- a/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.cpp +++ b/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.cpp @@ -10,7 +10,7 @@ #include "Playerbots.h" #include "PositionValue.h" -void ReturnPositionResetAction::ResetReturnPosition() +void PositionsResetAction::ResetReturnPosition() { PositionMap& posMap = context->GetValue("position")->Get(); PositionInfo pos = posMap["return"]; @@ -18,7 +18,7 @@ void ReturnPositionResetAction::ResetReturnPosition() posMap["return"] = pos; } -void ReturnPositionResetAction::SetReturnPosition(float x, float y, float z) +void PositionsResetAction::SetReturnPosition(float x, float y, float z) { PositionMap& posMap = context->GetValue("position")->Get(); PositionInfo pos = posMap["return"]; @@ -26,6 +26,22 @@ void ReturnPositionResetAction::SetReturnPosition(float x, float y, float z) posMap["return"] = pos; } +void PositionsResetAction::ResetStayPosition() +{ + PositionMap& posMap = context->GetValue("position")->Get(); + PositionInfo pos = posMap["stay"]; + pos.Reset(); + posMap["stay"] = pos; +} + +void PositionsResetAction::SetStayPosition(float x, float y, float z) +{ + PositionMap& posMap = context->GetValue("position")->Get(); + PositionInfo pos = posMap["stay"]; + pos.Set(x, y, z, botAI->GetBot()->GetMapId()); + posMap["stay"] = pos; +} + bool FollowChatShortcutAction::Execute(Event event) { Player* master = GetMaster(); @@ -34,7 +50,7 @@ bool FollowChatShortcutAction::Execute(Event event) // botAI->Reset(); botAI->ChangeStrategy("+follow,-passive,-grind,-move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("-follow,-passive,-grind,-move from group", BOT_STATE_COMBAT); + botAI->ChangeStrategy("-stay,-follow,-passive,-grind,-move from group", BOT_STATE_COMBAT); botAI->GetAiObjectContext()->GetValue("prioritized targets")->Reset(); PositionMap& posMap = context->GetValue("position")->Get(); @@ -42,6 +58,10 @@ bool FollowChatShortcutAction::Execute(Event event) pos.Reset(); posMap["return"] = pos; + pos = posMap["stay"]; + pos.Reset(); + posMap["stay"] = pos; + if (bot->IsInCombat()) { Formation* formation = AI_VALUE(Formation*, "formation"); @@ -103,9 +123,10 @@ bool StayChatShortcutAction::Execute(Event event) botAI->Reset(); botAI->ChangeStrategy("+stay,-passive,-move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("-follow,-passive,-move from group", BOT_STATE_COMBAT); + botAI->ChangeStrategy("+stay,-follow,-passive,-move from group", BOT_STATE_COMBAT); SetReturnPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + SetStayPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); botAI->TellMaster("Staying"); return true; @@ -133,10 +154,11 @@ bool FleeChatShortcutAction::Execute(Event event) return false; botAI->Reset(); - botAI->ChangeStrategy("+follow,+passive", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("+follow,+passive", BOT_STATE_COMBAT); + botAI->ChangeStrategy("+follow,-stay,+passive", BOT_STATE_NON_COMBAT); + botAI->ChangeStrategy("+follow,-stay,+passive", BOT_STATE_COMBAT); ResetReturnPosition(); + ResetStayPosition(); if (bot->GetMapId() != master->GetMapId() || bot->GetDistance(master) > sPlayerbotAIConfig->sightDistance) { @@ -155,10 +177,11 @@ bool GoawayChatShortcutAction::Execute(Event event) return false; botAI->Reset(); - botAI->ChangeStrategy("+runaway", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("+runaway", BOT_STATE_COMBAT); + botAI->ChangeStrategy("+runaway,-stay", BOT_STATE_NON_COMBAT); + botAI->ChangeStrategy("+runaway,-stay", BOT_STATE_COMBAT); ResetReturnPosition(); + ResetStayPosition(); botAI->TellMaster("Running away"); return true; @@ -171,9 +194,10 @@ bool GrindChatShortcutAction::Execute(Event event) return false; botAI->Reset(); - botAI->ChangeStrategy("+grind,-passive", BOT_STATE_NON_COMBAT); + botAI->ChangeStrategy("+grind,-passive,-stay", BOT_STATE_NON_COMBAT); ResetReturnPosition(); + ResetStayPosition(); botAI->TellMaster("Grinding"); return true; @@ -193,6 +217,7 @@ bool TankAttackChatShortcutAction::Execute(Event event) botAI->ChangeStrategy("-passive", BOT_STATE_COMBAT); ResetReturnPosition(); + ResetStayPosition(); botAI->TellMaster("Attacking"); return true; diff --git a/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.h b/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.h index 3ee0240..9089543 100644 --- a/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.h +++ b/modules/mod-playerbots/src/strategy/actions/ChatShortcutActions.h @@ -10,13 +10,15 @@ class PlayerbotAI; -class ReturnPositionResetAction : public Action +class PositionsResetAction : public Action { public: - ReturnPositionResetAction(PlayerbotAI* botAI, std::string const name) : Action(botAI, name) {} + PositionsResetAction(PlayerbotAI* botAI, std::string const name) : Action(botAI, name) {} void ResetReturnPosition(); void SetReturnPosition(float x, float y, float z); + void ResetStayPosition(); + void SetStayPosition(float x, float y, float z); }; class FollowChatShortcutAction : public MovementAction @@ -27,10 +29,10 @@ public: bool Execute(Event event) override; }; -class StayChatShortcutAction : public ReturnPositionResetAction +class StayChatShortcutAction : public PositionsResetAction { public: - StayChatShortcutAction(PlayerbotAI* botAI) : ReturnPositionResetAction(botAI, "stay chat shortcut") {} + StayChatShortcutAction(PlayerbotAI* botAI) : PositionsResetAction(botAI, "stay chat shortcut") {} bool Execute(Event event) override; }; @@ -43,34 +45,34 @@ public: bool Execute(Event event) override; }; -class FleeChatShortcutAction : public ReturnPositionResetAction +class FleeChatShortcutAction : public PositionsResetAction { public: - FleeChatShortcutAction(PlayerbotAI* botAI) : ReturnPositionResetAction(botAI, "flee chat shortcut") {} + FleeChatShortcutAction(PlayerbotAI* botAI) : PositionsResetAction(botAI, "flee chat shortcut") {} bool Execute(Event event) override; }; -class GoawayChatShortcutAction : public ReturnPositionResetAction +class GoawayChatShortcutAction : public PositionsResetAction { public: - GoawayChatShortcutAction(PlayerbotAI* botAI) : ReturnPositionResetAction(botAI, "runaway chat shortcut") {} + GoawayChatShortcutAction(PlayerbotAI* botAI) : PositionsResetAction(botAI, "runaway chat shortcut") {} bool Execute(Event event) override; }; -class GrindChatShortcutAction : public ReturnPositionResetAction +class GrindChatShortcutAction : public PositionsResetAction { public: - GrindChatShortcutAction(PlayerbotAI* botAI) : ReturnPositionResetAction(botAI, "grind chat shortcut") {} + GrindChatShortcutAction(PlayerbotAI* botAI) : PositionsResetAction(botAI, "grind chat shortcut") {} bool Execute(Event event) override; }; -class TankAttackChatShortcutAction : public ReturnPositionResetAction +class TankAttackChatShortcutAction : public PositionsResetAction { public: - TankAttackChatShortcutAction(PlayerbotAI* botAI) : ReturnPositionResetAction(botAI, "tank attack chat shortcut") {} + TankAttackChatShortcutAction(PlayerbotAI* botAI) : PositionsResetAction(botAI, "tank attack chat shortcut") {} bool Execute(Event event) override; }; diff --git a/modules/mod-playerbots/src/strategy/actions/ChooseTargetActions.cpp b/modules/mod-playerbots/src/strategy/actions/ChooseTargetActions.cpp index 5481376..e4df1e0 100644 --- a/modules/mod-playerbots/src/strategy/actions/ChooseTargetActions.cpp +++ b/modules/mod-playerbots/src/strategy/actions/ChooseTargetActions.cpp @@ -34,28 +34,17 @@ bool AttackAnythingAction::isUseful() if (!botAI->AllowActivity(GRIND_ACTIVITY)) // Bot not allowed to be active return false; - if (!AI_VALUE(bool, "can move around")) + if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT)) + return false; + + if (bot->IsInCombat()) return false; - - - // if (context->GetValue("travel target")->Get()->isTraveling() && - // ChooseRpgTargetAction::isFollowValid( - // bot, *context->GetValue("travel target")->Get()->getPosition())) // Bot is traveling - // return false; Unit* target = GetTarget(); if (!target) return false; - bool inactiveGrindStatus = botAI->rpgInfo.status == NewRpgStatus::GO_GRIND || - botAI->rpgInfo.status == NewRpgStatus::NEAR_NPC || - botAI->rpgInfo.status == NewRpgStatus::REST || - botAI->rpgInfo.status == NewRpgStatus::GO_INNKEEPER; - - if (inactiveGrindStatus && bot->GetDistance(target) > 25.0f) - return false; - std::string const name = std::string(target->GetName()); // Check for invalid targets: Dummy, Charge Target, Melee Target, Ranged Target if (!name.empty() && diff --git a/modules/mod-playerbots/src/strategy/actions/DropQuestAction.cpp b/modules/mod-playerbots/src/strategy/actions/DropQuestAction.cpp index eea9337..672358c 100644 --- a/modules/mod-playerbots/src/strategy/actions/DropQuestAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/DropQuestAction.cpp @@ -132,6 +132,7 @@ bool CleanQuestLogAction::Execute(Event event) } // Remove quest + botAI->rpgStatistic.questDropped++; bot->SetQuestSlot(slot, 0); bot->TakeQuestSourceItem(questId, false); bot->SetQuestStatus(questId, QUEST_STATUS_NONE); diff --git a/modules/mod-playerbots/src/strategy/actions/OpenItemAction.cpp b/modules/mod-playerbots/src/strategy/actions/OpenItemAction.cpp index 2076b84..ff8407c 100644 --- a/modules/mod-playerbots/src/strategy/actions/OpenItemAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/OpenItemAction.cpp @@ -4,64 +4,23 @@ #include "WorldPacket.h" #include "Player.h" #include "ObjectMgr.h" - bool OpenItemAction::Execute(Event event) { bool foundOpenable = false; - // Check main inventory slots - for (uint8 slot = EQUIPMENT_SLOT_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + Item* item = botAI->FindOpenableItem(); + if (item) { - Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + uint8 bag = item->GetBagSlot(); // Retrieves the bag slot (0 for main inventory) + uint8 slot = item->GetSlot(); // Retrieves the actual slot inside the bag - if (item && CanOpenItem(item)) - { - OpenItem(item, INVENTORY_SLOT_BAG_0, slot); - foundOpenable = true; - } - } - - // Check items in the bags - for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) - { - Bag* bagItem = bot->GetBagByPos(bag); - if (!bagItem) - continue; - - for (uint32 slot = 0; slot < bagItem->GetBagSize(); ++slot) - { - Item* item = bot->GetItemByPos(bag, slot); - - if (item && CanOpenItem(item)) - { - OpenItem(item, bag, slot); - foundOpenable = true; - } - } - } - - // If no openable items found - if (!foundOpenable) - { - botAI->TellError("No openable items in inventory."); + OpenItem(item, bag, slot); + foundOpenable = true; } return foundOpenable; } -bool OpenItemAction::CanOpenItem(Item* item) -{ - if (!item) - return false; - - ItemTemplate const* itemTemplate = item->GetTemplate(); - if (!itemTemplate) - return false; - - // Check if the item has the openable flag - return itemTemplate->Flags & ITEM_FLAG_HAS_LOOT; -} - void OpenItemAction::OpenItem(Item* item, uint8 bag, uint8 slot) { WorldPacket packet(CMSG_OPEN_ITEM); diff --git a/modules/mod-playerbots/src/strategy/actions/OpenItemAction.h b/modules/mod-playerbots/src/strategy/actions/OpenItemAction.h index e60dd33..6ab73b7 100644 --- a/modules/mod-playerbots/src/strategy/actions/OpenItemAction.h +++ b/modules/mod-playerbots/src/strategy/actions/OpenItemAction.h @@ -21,9 +21,6 @@ public: bool Execute(Event event) override; private: - // Checks if the given item can be opened (i.e., has the openable flag) - bool CanOpenItem(Item* item); - // Performs the action of opening the item void OpenItem(Item* item, uint8 bag, uint8 slot); }; diff --git a/modules/mod-playerbots/src/strategy/actions/PositionAction.cpp b/modules/mod-playerbots/src/strategy/actions/PositionAction.cpp index df5fd50..3888f28 100644 --- a/modules/mod-playerbots/src/strategy/actions/PositionAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/PositionAction.cpp @@ -159,3 +159,25 @@ bool ReturnAction::isUseful() PositionInfo pos = context->GetValue("position")->Get()[qualifier]; return pos.isSet() && AI_VALUE2(float, "distance", "position_random") > sPlayerbotAIConfig->followDistance; } + +bool ReturnToStayPositionAction::isPossible() +{ + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo stayPosition = posMap["stay"]; + if (stayPosition.isSet()) + { + const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z); + if (distance > sPlayerbotAIConfig->reactDistance) + { + botAI->TellMaster("The stay position is too far to return. I am going to stay where I am now"); + + // Set the stay position to current position + stayPosition.Set(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId()); + posMap["stay"] = stayPosition; + } + + return true; + } + + return false; +} diff --git a/modules/mod-playerbots/src/strategy/actions/PositionAction.h b/modules/mod-playerbots/src/strategy/actions/PositionAction.h index b53ba1b..16a671d 100644 --- a/modules/mod-playerbots/src/strategy/actions/PositionAction.h +++ b/modules/mod-playerbots/src/strategy/actions/PositionAction.h @@ -40,6 +40,13 @@ public: GuardAction(PlayerbotAI* botAI) : MoveToPositionAction(botAI, "move to position", "guard") {} }; +class ReturnToStayPositionAction : public MoveToPositionAction +{ +public: + ReturnToStayPositionAction(PlayerbotAI* ai) : MoveToPositionAction(ai, "move to position", "stay") {} + virtual bool isPossible(); +}; + class SetReturnPositionAction : public Action { public: diff --git a/modules/mod-playerbots/src/strategy/actions/QuestAction.cpp b/modules/mod-playerbots/src/strategy/actions/QuestAction.cpp index 4ecb4ff..2920194 100644 --- a/modules/mod-playerbots/src/strategy/actions/QuestAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/QuestAction.cpp @@ -4,9 +4,14 @@ */ #include "QuestAction.h" +#include +#include "Chat.h" #include "ChatHelper.h" #include "Event.h" +#include "ItemTemplate.h" +#include "ObjectGuid.h" +#include "ObjectMgr.h" #include "Playerbots.h" #include "ReputationMgr.h" #include "ServerFacade.h" @@ -258,6 +263,7 @@ bool QuestAction::AcceptQuest(Quest const* quest, ObjectGuid questGiver) bool QuestUpdateCompleteAction::Execute(Event event) { + // the action can hardly be triggered WorldPacket p(event.getPacket()); p.rpos(0); @@ -265,22 +271,26 @@ bool QuestUpdateCompleteAction::Execute(Event event) p >> questId; p.print_storage(); - LOG_INFO("playerbots", "Packet: empty{} questId{}", p.empty(), questId); + // LOG_INFO("playerbots", "Packet: empty{} questId{}", p.empty(), questId); Quest const* qInfo = sObjectMgr->GetQuestTemplate(questId); if (qInfo) { - std::map placeholders; + // std::map placeholders; + // placeholders["%quest_link"] = format; + + // if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) + // { + // LOG_INFO("playerbots", "{} => Quest [ {} ] completed", bot->GetName(), qInfo->GetTitle()); + // bot->Say("Quest [ " + format + " ] completed", LANG_UNIVERSAL); + // } const auto format = ChatHelper::FormatQuest(qInfo); - placeholders["%quest_link"] = format; - - if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { - LOG_INFO("playerbots", "{} => Quest [ {} ] completed", bot->GetName(), qInfo->GetTitle()); - bot->Say("Quest [ " + format + " ] completed", LANG_UNIVERSAL); - } - botAI->TellMasterNoFacing("Quest completed " + format); + if (botAI->GetMaster()) + botAI->TellMasterNoFacing("Quest completed " + format); BroadcastHelper::BroadcastQuestUpdateComplete(botAI, bot, qInfo); + botAI->rpgStatistic.questCompleted++; + // LOG_DEBUG("playerbots", "[New rpg] {} complete quest {}", bot->GetName(), qInfo->GetQuestId()); + // botAI->rpgStatistic.questCompleted++; } return true; @@ -296,39 +306,39 @@ bool QuestUpdateAddKillAction::Execute(Event event) uint32 entry, questId, available, required; p >> questId >> entry >> available >> required; - + // LOG_INFO("playerbots", "[New rpg] Quest {} -> Creature {} ({}/{})", questId, entry, available, required); const Quest* qInfo = sObjectMgr->GetQuestTemplate(questId); if (qInfo && (entry & 0x80000000)) { entry &= 0x7FFFFFFF; const GameObjectTemplate* info = sObjectMgr->GetGameObjectTemplate(entry); if (info) - BroadcastHelper::BroadcastQuestUpdateAddKill(botAI, bot, qInfo, available, required, info->name); + { + std::string infoName = botAI->GetLocalizedGameObjectName(entry); + BroadcastHelper::BroadcastQuestUpdateAddKill(botAI, bot, qInfo, available, required, infoName); + if (botAI->GetMaster()) + { + std::ostringstream out; + out << infoName << " " << available << "/" << required << " " << ChatHelper::FormatQuest(qInfo); + botAI->TellMasterNoFacing(out.str()); + } + } } else if (qInfo) { CreatureTemplate const* info = sObjectMgr->GetCreatureTemplate(entry); if (info) { - BroadcastHelper::BroadcastQuestUpdateAddKill(botAI, bot, qInfo, available, required, info->Name); + std::string infoName = botAI->GetLocalizedCreatureName(entry); + BroadcastHelper::BroadcastQuestUpdateAddKill(botAI, bot, qInfo, available, required, infoName); + if (botAI->GetMaster()) + { + std::ostringstream out; + out << infoName << " " << available << "/" << required << " " << ChatHelper::FormatQuest(qInfo); + botAI->TellMasterNoFacing(out.str()); + } } - } - else - { - std::map placeholders; - placeholders["%quest_id"] = questId; - placeholders["%available"] = available; - placeholders["%required"] = required; - - if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_COMBAT) || botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT)) - { - LOG_INFO("playerbots", "{} => {}", bot->GetName(), BOT_TEXT2("%available/%required for questId: %quest_id", placeholders)); - botAI->Say(BOT_TEXT2("%available/%required for questId: %quest_id", placeholders)); - } - - botAI->TellMasterNoFacing(BOT_TEXT2("%available/%required for questId: %quest_id", placeholders)); - } - + } return false; } @@ -363,8 +373,62 @@ bool QuestUpdateAddItemAction::Execute(Event event) BroadcastHelper::BroadcastQuestUpdateAddItem(botAI, bot, pair.first, availableItemsCount, requiredItemsCount, itemPrototype); } - } + } + return false; +} + +bool QuestItemPushResultAction::Execute(Event event) +{ + WorldPacket packet = event.getPacket(); + ObjectGuid guid; + uint32 received, created, sendChatMessage, itemSlot, itemEntry, itemSuffixFactory, count, itemCount; + uint8 bagSlot; + int32 itemRandomPropertyId; + packet >> guid >> received >> created >> sendChatMessage; + packet >> bagSlot >> itemSlot >> itemEntry >> itemSuffixFactory >> itemRandomPropertyId; + packet >> count >> itemCount; + + if (guid != bot->GetGUID()) + return false; + const ItemTemplate* proto = sObjectMgr->GetItemTemplate(itemEntry); + if (!proto) + return false; + + for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questId = bot->GetQuestSlotQuestId(i); + if (!questId) + continue; + + const Quest* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + return false; + + const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId); + + for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++) + { + uint32 itemId = quest->RequiredItemId[i]; + if (!itemId) + continue; + + int32 previousCount = itemCount - count; + if (itemId == itemEntry && previousCount < quest->RequiredItemCount[i]) + { + if (botAI->GetMaster()) + { + std::string itemLink = ChatHelper::FormatItem(proto); + std::ostringstream out; + int32 required = quest->RequiredItemCount[i]; + int32 available = std::min((int32)itemCount, required); + out << itemLink << " " << available << "/" << required << " " << ChatHelper::FormatQuest(quest); + botAI->TellMasterNoFacing(out.str()); + } + } + } + } + return false; } diff --git a/modules/mod-playerbots/src/strategy/actions/QuestAction.h b/modules/mod-playerbots/src/strategy/actions/QuestAction.h index 1d2497e..680154c 100644 --- a/modules/mod-playerbots/src/strategy/actions/QuestAction.h +++ b/modules/mod-playerbots/src/strategy/actions/QuestAction.h @@ -66,4 +66,11 @@ public: bool Execute(Event event) override; }; +class QuestItemPushResultAction : public Action +{ +public: + QuestItemPushResultAction(PlayerbotAI* ai) : Action(ai, "quest item push result") {} + bool Execute(Event event) override;; +}; + #endif diff --git a/modules/mod-playerbots/src/strategy/actions/ReachTargetActions.cpp b/modules/mod-playerbots/src/strategy/actions/ReachTargetActions.cpp index 9184336..f5e5101 100644 --- a/modules/mod-playerbots/src/strategy/actions/ReachTargetActions.cpp +++ b/modules/mod-playerbots/src/strategy/actions/ReachTargetActions.cpp @@ -14,6 +14,12 @@ bool ReachTargetAction::Execute(Event event) { return ReachCombatTo(AI_VALUE(Uni bool ReachTargetAction::isUseful() { + // do not move while staying + if (botAI->HasStrategy("stay", botAI->GetState())) + { + return false; + } + // do not move while casting if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr) { @@ -30,6 +36,12 @@ std::string const ReachTargetAction::GetTargetName() { return "current target"; bool CastReachTargetSpellAction::isUseful() { + // do not move while staying + if (botAI->HasStrategy("stay", botAI->GetState())) + { + return false; + } + return sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "current target"), (distance + sPlayerbotAIConfig->contactDistance)); } diff --git a/modules/mod-playerbots/src/strategy/actions/SeeSpellAction.cpp b/modules/mod-playerbots/src/strategy/actions/SeeSpellAction.cpp index dcce695..0fd90bc 100644 --- a/modules/mod-playerbots/src/strategy/actions/SeeSpellAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/SeeSpellAction.cpp @@ -11,6 +11,7 @@ #include "Playerbots.h" #include "RTSCValues.h" #include "RtscAction.h" +#include "PositionValue.h" Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* lastWp, bool important) @@ -123,6 +124,15 @@ bool SeeSpellAction::MoveToSpell(WorldPosition& spellPosition, bool inFormation) if (inFormation) SetFormationOffset(spellPosition); + if (botAI->HasStrategy("stay", botAI->GetState())) + { + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo stayPosition = posMap["stay"]; + + stayPosition.Set(spellPosition.getX(), spellPosition.getY(), spellPosition.getZ(), spellPosition.getMapId()); + posMap["stay"] = stayPosition; + } + if (bot->IsWithinLOS(spellPosition.getX(), spellPosition.getY(), spellPosition.getZ())) return MoveNear(spellPosition.getMapId(), spellPosition.getX(), spellPosition.getY(), spellPosition.getZ(), 0); diff --git a/modules/mod-playerbots/src/strategy/actions/StayActions.cpp b/modules/mod-playerbots/src/strategy/actions/StayActions.cpp index b041f4e..cca32c1 100644 --- a/modules/mod-playerbots/src/strategy/actions/StayActions.cpp +++ b/modules/mod-playerbots/src/strategy/actions/StayActions.cpp @@ -8,6 +8,7 @@ #include "Event.h" #include "LastMovementValue.h" #include "Playerbots.h" +#include "PositionValue.h" bool StayActionBase::Stay() { @@ -42,6 +43,17 @@ bool StayAction::Execute(Event event) { return Stay(); } bool StayAction::isUseful() { + // Check if the bots is in stay position + PositionInfo stayPosition = AI_VALUE(PositionMap&, "position")["stay"]; + if (stayPosition.isSet()) + { + const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z); + if (sPlayerbotAIConfig->followDistance) + { + return false; + } + } + // move from group takes priority over stay as it's added and removed automatically // (without removing/adding stay) if (botAI->HasStrategy("move from group", BOT_STATE_COMBAT) || diff --git a/modules/mod-playerbots/src/strategy/actions/TalkToQuestGiverAction.cpp b/modules/mod-playerbots/src/strategy/actions/TalkToQuestGiverAction.cpp index 772d3ff..93f85d9 100644 --- a/modules/mod-playerbots/src/strategy/actions/TalkToQuestGiverAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/TalkToQuestGiverAction.cpp @@ -173,7 +173,6 @@ void TalkToQuestGiverAction::RewardMultipleItem(Quest const* quest, Object* ques std::ostringstream outid; if (!botAI->IsAlt() || sPlayerbotAIConfig->autoPickReward == "yes") { - // Pick the first item of the best rewards. bestIds = BestRewards(quest); if (!bestIds.empty()) { diff --git a/modules/mod-playerbots/src/strategy/actions/TradeStatusAction.cpp b/modules/mod-playerbots/src/strategy/actions/TradeStatusAction.cpp index 577d46d..ad42136 100644 --- a/modules/mod-playerbots/src/strategy/actions/TradeStatusAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/TradeStatusAction.cpp @@ -24,12 +24,17 @@ bool TradeStatusAction::Execute(Event event) return false; PlayerbotAI* traderBotAI = GET_PLAYERBOT_AI(trader); - if (trader != master && !traderBotAI) + + // Allow the master and group members to trade + if (trader != master && !traderBotAI && (!bot->GetGroup() || !bot->GetGroup()->IsMember(trader->GetGUID()))) { bot->Whisper("I'm kind of busy now", LANG_UNIVERSAL, trader); + return false; } - if ((trader != master || !botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_ALLOW_ALL, true, master)) && + // Allow trades from group members or bots + if ((!bot->GetGroup() || !bot->GetGroup()->IsMember(trader->GetGUID())) && + (trader != master || !botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_ALLOW_ALL, true, master)) && !traderBotAI) { WorldPacket p; @@ -109,9 +114,9 @@ bool TradeStatusAction::Execute(Event event) bot->SetFacingToObject(trader); BeginTrade(); + return true; } - return false; } diff --git a/modules/mod-playerbots/src/strategy/actions/TradeStatusExtendedAction.cpp b/modules/mod-playerbots/src/strategy/actions/TradeStatusExtendedAction.cpp new file mode 100644 index 0000000..ada779e --- /dev/null +++ b/modules/mod-playerbots/src/strategy/actions/TradeStatusExtendedAction.cpp @@ -0,0 +1,84 @@ +#include "TradeStatusExtendedAction.h" +#include "Event.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "WorldPacket.h" +#include "TradeData.h" + +bool TradeStatusExtendedAction::Execute(Event event) +{ + Player* trader = bot->GetTrader(); + if (!trader) + return false; + + TradeData* tradeData = trader->GetTradeData(); + if (!tradeData) + return false; + + WorldPacket p(event.getPacket()); + p.rpos(0); + + uint8 isTraderData; + uint32 unknown1, slotCount1, slotCount2, tradeGold, spellCast; + p >> isTraderData; + p >> unknown1; + p >> slotCount1; + p >> slotCount2; + p >> tradeGold; + p >> spellCast; + + for (uint8 i = 0; i < TRADE_SLOT_COUNT; ++i) + { + uint8 tradeSlot; + p >> tradeSlot; + + if (tradeSlot >= TRADE_SLOT_COUNT) + break; // End of packet + + uint32 itemId, displayId, count, wrapped, lockId; + uint64 giftCreator, creator; + uint32 permEnchant, gem1, gem2, gem3; + uint32 spellCharges, suffixFactor, randomProp, maxDurability, durability; + + p >> itemId; + p >> displayId; + p >> count; + p >> wrapped; + p >> giftCreator; + p >> permEnchant; + p >> gem1; + p >> gem2; + p >> gem3; + p >> creator; + p >> spellCharges; + p >> suffixFactor; + p >> randomProp; + p >> lockId; + p >> maxDurability; + p >> durability; + + // Check for locked items in "Do Not Trade" slot + if (tradeSlot == TRADE_SLOT_NONTRADED && lockId > 0) + { + // Get the actual item reference from TradeData + Item* lockbox = tradeData->GetItem(TRADE_SLOT_NONTRADED); + if (!lockbox) + { + return false; + } + + if (bot->getClass() == CLASS_ROGUE && bot->HasSpell(1804) && lockbox->IsLocked()) // Pick Lock spell + { + // botAI->CastSpell(1804, bot, lockbox); // Attempt to cast Pick Lock on the lockbox + botAI->DoSpecificAction("unlock traded item"); + botAI->SetNextCheckDelay(4000); // Delay before accepting trade + } + else + { + botAI->TellMaster("I can't unlock this item."); + } + } + } + + return true; +} diff --git a/modules/mod-playerbots/src/strategy/actions/TradeStatusExtendedAction.h b/modules/mod-playerbots/src/strategy/actions/TradeStatusExtendedAction.h new file mode 100644 index 0000000..8797d8d --- /dev/null +++ b/modules/mod-playerbots/src/strategy/actions/TradeStatusExtendedAction.h @@ -0,0 +1,17 @@ +#ifndef _PLAYERBOT_TRADESTATUSEXTENDEDACTION_H +#define _PLAYERBOT_TRADESTATUSEXTENDEDACTION_H + +#include "QueryItemUsageAction.h" + +class Player; +class PlayerbotAI; + +class TradeStatusExtendedAction : public QueryItemUsageAction +{ +public: + TradeStatusExtendedAction(PlayerbotAI* botAI) : QueryItemUsageAction(botAI, "trade status extended") {} + + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/strategy/actions/UnlockItemAction.cpp b/modules/mod-playerbots/src/strategy/actions/UnlockItemAction.cpp new file mode 100644 index 0000000..367a4fc --- /dev/null +++ b/modules/mod-playerbots/src/strategy/actions/UnlockItemAction.cpp @@ -0,0 +1,38 @@ +#include "UnlockItemAction.h" +#include "PlayerbotAI.h" +#include "ItemTemplate.h" +#include "WorldPacket.h" +#include "Player.h" +#include "ObjectMgr.h" +#include "SpellInfo.h" + +#define PICK_LOCK_SPELL_ID 1804 + +bool UnlockItemAction::Execute(Event event) +{ + bool foundLockedItem = false; + + Item* item = botAI->FindLockedItem(); + if (item) + { + UnlockItem(item); + foundLockedItem = true; + } + + return foundLockedItem; +} + +void UnlockItemAction::UnlockItem(Item* item) +{ + // Use CastSpell to unlock the item + if (botAI->CastSpell(PICK_LOCK_SPELL_ID, bot, item)) + { + std::ostringstream out; + out << "Used Pick Lock on: " << item->GetTemplate()->Name1; + botAI->TellMaster(out.str()); + } + else + { + botAI->TellError("Failed to cast Pick Lock."); + } +} diff --git a/modules/mod-playerbots/src/strategy/actions/UnlockItemAction.h b/modules/mod-playerbots/src/strategy/actions/UnlockItemAction.h new file mode 100644 index 0000000..a4738da --- /dev/null +++ b/modules/mod-playerbots/src/strategy/actions/UnlockItemAction.h @@ -0,0 +1,19 @@ +#ifndef _PLAYERBOT_UNLOCKITEMACTION_H +#define _PLAYERBOT_UNLOCKITEMACTION_H + +#include "Action.h" + +class PlayerbotAI; + +class UnlockItemAction : public Action +{ +public: + UnlockItemAction(PlayerbotAI* botAI) : Action(botAI, "unlock item") { } + + bool Execute(Event event) override; + +private: + void UnlockItem(Item* item); +}; + +#endif diff --git a/modules/mod-playerbots/src/strategy/actions/UnlockTradedItemAction.cpp b/modules/mod-playerbots/src/strategy/actions/UnlockTradedItemAction.cpp new file mode 100644 index 0000000..22d9c14 --- /dev/null +++ b/modules/mod-playerbots/src/strategy/actions/UnlockTradedItemAction.cpp @@ -0,0 +1,98 @@ +#include "UnlockTradedItemAction.h" +#include "Playerbots.h" +#include "TradeData.h" +#include "SpellInfo.h" + +#define PICK_LOCK_SPELL_ID 1804 + +bool UnlockTradedItemAction::Execute(Event event) +{ + Player* trader = bot->GetTrader(); + if (!trader) + return false; // No active trade session + + TradeData* tradeData = trader->GetTradeData(); + if (!tradeData) + return false; // No trade data available + + Item* lockbox = tradeData->GetItem(TRADE_SLOT_NONTRADED); + if (!lockbox) + { + botAI->TellError("No item in the Do Not Trade slot."); + return false; + } + + if (!CanUnlockItem(lockbox)) + { + botAI->TellError("Cannot unlock this item."); + return false; + } + + UnlockItem(lockbox); + return true; +} + +bool UnlockTradedItemAction::CanUnlockItem(Item* item) +{ + if (!item) + return false; + + ItemTemplate const* itemTemplate = item->GetTemplate(); + if (!itemTemplate) + return false; + + // Ensure the bot is a rogue and has Lockpicking skill + if (bot->getClass() != CLASS_ROGUE || !botAI->HasSkill(SKILL_LOCKPICKING)) + return false; + + // Ensure the item is actually locked + if (itemTemplate->LockID == 0 || !item->IsLocked()) + return false; + + // Check if the bot's Lockpicking skill is high enough + uint32 lockId = itemTemplate->LockID; + LockEntry const* lockInfo = sLockStore.LookupEntry(lockId); + if (!lockInfo) + return false; + + uint32 botSkill = bot->GetSkillValue(SKILL_LOCKPICKING); + for (uint8 j = 0; j < 8; ++j) + { + if (lockInfo->Type[j] == LOCK_KEY_SKILL && SkillByLockType(LockType(lockInfo->Index[j])) == SKILL_LOCKPICKING) + { + uint32 requiredSkill = lockInfo->Skill[j]; + if (botSkill >= requiredSkill) + return true; + else + { + std::ostringstream out; + out << "Lockpicking skill too low (" << botSkill << "/" << requiredSkill << ") to unlock: " + << item->GetTemplate()->Name1; + botAI->TellMaster(out.str()); + } + } + } + + return false; +} + +void UnlockTradedItemAction::UnlockItem(Item* item) +{ + if (!bot->HasSpell(PICK_LOCK_SPELL_ID)) + { + botAI->TellError("Cannot unlock, Pick Lock spell is missing."); + return; + } + + // Use CastSpell to unlock the item + if (botAI->CastSpell(PICK_LOCK_SPELL_ID, bot->GetTrader(), item)) // Unit target is trader + { + std::ostringstream out; + out << "Picking Lock on traded item: " << item->GetTemplate()->Name1; + botAI->TellMaster(out.str()); + } + else + { + botAI->TellError("Failed to cast Pick Lock."); + } +} diff --git a/modules/mod-playerbots/src/strategy/actions/UnlockTradedItemAction.h b/modules/mod-playerbots/src/strategy/actions/UnlockTradedItemAction.h new file mode 100644 index 0000000..6349ab5 --- /dev/null +++ b/modules/mod-playerbots/src/strategy/actions/UnlockTradedItemAction.h @@ -0,0 +1,20 @@ +#ifndef _PLAYERBOT_UNLOCKTRADEDITEMACTION_H +#define _PLAYERBOT_UNLOCKTRADEDITEMACTION_H + +#include "Action.h" + +class PlayerbotAI; + +class UnlockTradedItemAction : public Action +{ +public: + UnlockTradedItemAction(PlayerbotAI* botAI) : Action(botAI, "unlock traded item") {} + + bool Execute(Event event) override; + +private: + bool CanUnlockItem(Item* item); + void UnlockItem(Item* item); +}; + +#endif diff --git a/modules/mod-playerbots/src/strategy/actions/UseMeetingStoneAction.cpp b/modules/mod-playerbots/src/strategy/actions/UseMeetingStoneAction.cpp index 071b5b4..82ab1f7 100644 --- a/modules/mod-playerbots/src/strategy/actions/UseMeetingStoneAction.cpp +++ b/modules/mod-playerbots/src/strategy/actions/UseMeetingStoneAction.cpp @@ -11,6 +11,7 @@ #include "GridNotifiersImpl.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" +#include "PositionValue.h" bool UseMeetingStoneAction::Execute(Event event) { @@ -224,6 +225,16 @@ bool SummonAction::Teleport(Player* summoner, Player* player) player->GetMotionMaster()->Clear(); AI_VALUE(LastMovement&, "last movement").clear(); player->TeleportTo(mapId, x, y, z, 0); + + if (botAI->HasStrategy("stay", botAI->GetState())) + { + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo stayPosition = posMap["stay"]; + + stayPosition.Set(x,y, z, mapId); + posMap["stay"] = stayPosition; + } + return true; } } diff --git a/modules/mod-playerbots/src/strategy/actions/WorldPacketActionContext.h b/modules/mod-playerbots/src/strategy/actions/WorldPacketActionContext.h index 947b482..2c32d11 100644 --- a/modules/mod-playerbots/src/strategy/actions/WorldPacketActionContext.h +++ b/modules/mod-playerbots/src/strategy/actions/WorldPacketActionContext.h @@ -37,6 +37,7 @@ #include "TellCastFailedAction.h" #include "TellMasterAction.h" #include "TradeStatusAction.h" +#include "TradeStatusExtendedAction.h" #include "UseMeetingStoneAction.h" #include "NamedObjectContext.h" @@ -65,6 +66,7 @@ public: creators["check mount state"] = &WorldPacketActionContext::check_mount_state; creators["remember taxi"] = &WorldPacketActionContext::remember_taxi; creators["accept trade"] = &WorldPacketActionContext::accept_trade; + creators["trade status extended"] = &WorldPacketActionContext::trade_status_extended; creators["store loot"] = &WorldPacketActionContext::store_loot; // quest @@ -79,7 +81,8 @@ public: creators["quest update failed timer"] = &WorldPacketActionContext::quest_update_failed_timer; creators["quest update complete"] = &WorldPacketActionContext::quest_update_complete; creators["turn in query quest"] = &WorldPacketActionContext::turn_in_query_quest; - + creators["quest item push result"] = &WorldPacketActionContext::quest_item_push_result; + creators["party command"] = &WorldPacketActionContext::party_command; creators["tell cast failed"] = &WorldPacketActionContext::tell_cast_failed; creators["accept duel"] = &WorldPacketActionContext::accept_duel; @@ -117,6 +120,7 @@ private: static Action* party_command(PlayerbotAI* botAI) { return new PartyCommandAction(botAI); } static Action* store_loot(PlayerbotAI* botAI) { return new StoreLootAction(botAI); } static Action* accept_trade(PlayerbotAI* botAI) { return new TradeStatusAction(botAI); } + static Action* trade_status_extended(PlayerbotAI* botAI) { return new TradeStatusExtendedAction(botAI); } static Action* remember_taxi(PlayerbotAI* botAI) { return new RememberTaxiAction(botAI); } static Action* check_mount_state(PlayerbotAI* botAI) { return new CheckMountStateAction(botAI); } static Action* area_trigger(PlayerbotAI* botAI) { return new AreaTriggerAction(botAI); } @@ -139,6 +143,7 @@ private: static Action* quest_update_failed(PlayerbotAI* ai) { return new QuestUpdateFailedAction(ai); } static Action* quest_update_failed_timer(PlayerbotAI* ai) { return new QuestUpdateFailedTimerAction(ai); } static Action* quest_update_complete(PlayerbotAI* botAI) { return new QuestUpdateCompleteAction(botAI); } + static Action* quest_item_push_result(PlayerbotAI* ai) { return new QuestItemPushResultAction(ai); } static Action* turn_in_quest(PlayerbotAI* botAI) { return new TalkToQuestGiverAction(botAI); } static Action* accept_quest(PlayerbotAI* botAI) { return new AcceptQuestAction(botAI); } diff --git a/modules/mod-playerbots/src/strategy/generic/ChatCommandHandlerStrategy.cpp b/modules/mod-playerbots/src/strategy/generic/ChatCommandHandlerStrategy.cpp index 072814a..27f46f9 100644 --- a/modules/mod-playerbots/src/strategy/generic/ChatCommandHandlerStrategy.cpp +++ b/modules/mod-playerbots/src/strategy/generic/ChatCommandHandlerStrategy.cpp @@ -96,6 +96,10 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger new TriggerNode("open items", NextAction::array(0, new NextAction("open items", relevance), nullptr))); triggers.push_back( new TriggerNode("qi", NextAction::array(0, new NextAction("query item usage", relevance), nullptr))); + triggers.push_back( + new TriggerNode("unlock items", NextAction::array(0, new NextAction("unlock items", relevance), nullptr))); + triggers.push_back( + new TriggerNode("unlock traded item", NextAction::array(0, new NextAction("unlock traded item", relevance), nullptr))); } ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) @@ -109,6 +113,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("log"); supported.push_back("los"); supported.push_back("rpg status"); + supported.push_back("rpg do quest"); supported.push_back("aura"); supported.push_back("drop"); supported.push_back("share"); @@ -171,4 +176,6 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("calc"); supported.push_back("open items"); supported.push_back("qi"); + supported.push_back("unlock items"); + supported.push_back("unlock traded item"); } diff --git a/modules/mod-playerbots/src/strategy/generic/CombatStrategy.cpp b/modules/mod-playerbots/src/strategy/generic/CombatStrategy.cpp index 6bbf09c..5026d3b 100644 --- a/modules/mod-playerbots/src/strategy/generic/CombatStrategy.cpp +++ b/modules/mod-playerbots/src/strategy/generic/CombatStrategy.cpp @@ -12,8 +12,9 @@ void CombatStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("enemy out of spell", NextAction::array(0, new NextAction("reach spell", ACTION_HIGH), nullptr))); + // drop target relevance 99 (lower than Worldpacket triggers) triggers.push_back( - new TriggerNode("invalid target", NextAction::array(0, new NextAction("drop target", 100), nullptr))); + new TriggerNode("invalid target", NextAction::array(0, new NextAction("drop target", 99), nullptr))); triggers.push_back( new TriggerNode("mounted", NextAction::array(0, new NextAction("check mount state", 54), nullptr))); // triggers.push_back(new TriggerNode("out of react range", NextAction::array(0, new NextAction("flee to master", diff --git a/modules/mod-playerbots/src/strategy/generic/GrindingStrategy.cpp b/modules/mod-playerbots/src/strategy/generic/GrindingStrategy.cpp index 8797063..e20b7fb 100644 --- a/modules/mod-playerbots/src/strategy/generic/GrindingStrategy.cpp +++ b/modules/mod-playerbots/src/strategy/generic/GrindingStrategy.cpp @@ -7,14 +7,19 @@ #include "Playerbots.h" -NextAction** GrindingStrategy::getDefaultActions() { return nullptr; } +NextAction** GrindingStrategy::getDefaultActions() +{ + return NextAction::array(0, + new NextAction("drink", 4.2f), + new NextAction("food", 4.1f), + nullptr); +} void GrindingStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("timer", NextAction::array(0, new NextAction("drink", 10.2f), nullptr))); - triggers.push_back(new TriggerNode("timer", NextAction::array(0, new NextAction("food", 10.1f), nullptr))); + // reduce lower than loot triggers.push_back( - new TriggerNode("no target", NextAction::array(0, new NextAction("attack anything", 10.0f), nullptr))); + new TriggerNode("no target", NextAction::array(0, new NextAction("attack anything", 4.0f), nullptr))); } void MoveRandomStrategy::InitTriggers(std::vector& triggers) diff --git a/modules/mod-playerbots/src/strategy/generic/LootNonCombatStrategy.cpp b/modules/mod-playerbots/src/strategy/generic/LootNonCombatStrategy.cpp index 57cc296..89fb6b5 100644 --- a/modules/mod-playerbots/src/strategy/generic/LootNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/strategy/generic/LootNonCombatStrategy.cpp @@ -13,13 +13,13 @@ void LootNonCombatStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("far from loot target", NextAction::array(0, new NextAction("move to loot", 7.0f), nullptr))); triggers.push_back(new TriggerNode("can loot", NextAction::array(0, new NextAction("open loot", 8.0f), nullptr))); - triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("add all loot", 1.0f), nullptr))); + triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("add all loot", 5.0f), nullptr))); } void GatherStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( - new TriggerNode("timer", NextAction::array(0, new NextAction("add gathering loot", 2.0f), nullptr))); + new TriggerNode("timer", NextAction::array(0, new NextAction("add gathering loot", 5.0f), nullptr))); } void RevealStrategy::InitTriggers(std::vector& triggers) diff --git a/modules/mod-playerbots/src/strategy/generic/StayStrategy.cpp b/modules/mod-playerbots/src/strategy/generic/StayStrategy.cpp index 8809971..b980275 100644 --- a/modules/mod-playerbots/src/strategy/generic/StayStrategy.cpp +++ b/modules/mod-playerbots/src/strategy/generic/StayStrategy.cpp @@ -7,6 +7,13 @@ #include "Playerbots.h" +void StayStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode( + "return to stay position", + NextAction::array(0, new NextAction("return to stay position", ACTION_MOVE), nullptr))); +} + NextAction** StayStrategy::getDefaultActions() { return NextAction::array(0, new NextAction("stay", 1.0f), nullptr); } void SitStrategy::InitTriggers(std::vector& triggers) diff --git a/modules/mod-playerbots/src/strategy/generic/StayStrategy.h b/modules/mod-playerbots/src/strategy/generic/StayStrategy.h index 0d9d638..7667485 100644 --- a/modules/mod-playerbots/src/strategy/generic/StayStrategy.h +++ b/modules/mod-playerbots/src/strategy/generic/StayStrategy.h @@ -10,12 +10,13 @@ class PlayerbotAI; -class StayStrategy : public NonCombatStrategy +class StayStrategy : public Strategy { public: - StayStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + StayStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} std::string const getName() override { return "stay"; } + void InitTriggers(std::vector& triggers) override; NextAction** getDefaultActions() override; }; diff --git a/modules/mod-playerbots/src/strategy/generic/WorldPacketHandlerStrategy.cpp b/modules/mod-playerbots/src/strategy/generic/WorldPacketHandlerStrategy.cpp index d68cea4..dacac30 100644 --- a/modules/mod-playerbots/src/strategy/generic/WorldPacketHandlerStrategy.cpp +++ b/modules/mod-playerbots/src/strategy/generic/WorldPacketHandlerStrategy.cpp @@ -35,10 +35,15 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger new NextAction("taxi", relevance), nullptr))); triggers.push_back(new TriggerNode("taxi done", NextAction::array(0, new NextAction("taxi", relevance), nullptr))); triggers.push_back(new TriggerNode("trade status", NextAction::array(0, new NextAction("accept trade", relevance), new NextAction("equip upgrades", relevance), nullptr))); + triggers.push_back(new TriggerNode("trade status extended", NextAction::array(0, new NextAction("trade status extended", relevance), nullptr))); triggers.push_back(new TriggerNode("area trigger", NextAction::array(0, new NextAction("reach area trigger", relevance), nullptr))); triggers.push_back(new TriggerNode("within area trigger", NextAction::array(0, new NextAction("area trigger", relevance), nullptr))); triggers.push_back(new TriggerNode("loot response", NextAction::array(0, new NextAction("store loot", relevance), nullptr))); - triggers.push_back(new TriggerNode("item push result", NextAction::array(0, new NextAction("query item usage", relevance), new NextAction("equip upgrades", relevance), nullptr))); + triggers.push_back(new TriggerNode("item push result", NextAction::array(0, new NextAction("unlock items", relevance), + new NextAction("open items", relevance), + new NextAction("query item usage", relevance), + new NextAction("equip upgrades", relevance), nullptr))); + triggers.push_back(new TriggerNode("item push result", NextAction::array(0, new NextAction("quest item push result", relevance), nullptr))); triggers.push_back(new TriggerNode("ready check finished", NextAction::array(0, new NextAction("finish ready check", relevance), nullptr))); // triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("security check", relevance), new NextAction("check mail", relevance), nullptr))); triggers.push_back(new TriggerNode("guild invite", NextAction::array(0, new NextAction("guild accept", relevance), nullptr))); @@ -79,7 +84,7 @@ WorldPacketHandlerStrategy::WorldPacketHandlerStrategy(PlayerbotAI* botAI) : Pas // quests supported.push_back("quest update add kill"); - supported.push_back("quest update add item"); + // supported.push_back("quest update add item"); supported.push_back("quest update failed"); supported.push_back("quest update failed timer"); supported.push_back("quest update complete"); diff --git a/modules/mod-playerbots/src/strategy/rogue/RogueActions.cpp b/modules/mod-playerbots/src/strategy/rogue/RogueActions.cpp index d037347..89d744c 100644 --- a/modules/mod-playerbots/src/strategy/rogue/RogueActions.cpp +++ b/modules/mod-playerbots/src/strategy/rogue/RogueActions.cpp @@ -8,8 +8,18 @@ #include "Event.h" #include "ObjectGuid.h" #include "Player.h" +#include "PlayerbotAIConfig.h" #include "Playerbots.h" +bool CastStealthAction::isUseful() +{ + Unit* target = AI_VALUE(Unit*, "current target"); + if (target && bot->GetDistance(target) >= sPlayerbotAIConfig->spellDistance) + return false; + return true; +} + + bool CastStealthAction::isPossible() { // do not use with WSG flag or EYE flag diff --git a/modules/mod-playerbots/src/strategy/rogue/RogueActions.h b/modules/mod-playerbots/src/strategy/rogue/RogueActions.h index c6fc4b5..9b276cc 100644 --- a/modules/mod-playerbots/src/strategy/rogue/RogueActions.h +++ b/modules/mod-playerbots/src/strategy/rogue/RogueActions.h @@ -44,7 +44,7 @@ public: CastStealthAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "stealth") {} std::string const GetTargetName() override { return "self target"; } - + bool isUseful() override; bool isPossible() override; }; diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.cpp b/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.cpp index 0f6697d..ecf795f 100644 --- a/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.cpp +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.cpp @@ -2,18 +2,32 @@ #include #include +#include +#include "ChatHelper.h" +#include "G3D/Vector2.h" +#include "GossipDef.h" +#include "IVMapMgr.h" +#include "NewRpgInfo.h" #include "NewRpgStrategy.h" +#include "Object.h" +#include "ObjectAccessor.h" #include "ObjectDefines.h" #include "ObjectGuid.h" +#include "ObjectMgr.h" #include "PathGenerator.h" #include "Player.h" #include "PlayerbotAI.h" #include "Playerbots.h" +#include "Position.h" +#include "QuestDef.h" #include "Random.h" #include "RandomPlayerbotMgr.h" +#include "SharedDefines.h" +#include "StatsWeightCalculator.h" #include "Timer.h" #include "TravelMgr.h" +#include "BroadcastHelper.h" #include "World.h" bool TellRpgStatusAction::Execute(Event event) @@ -26,118 +40,156 @@ bool TellRpgStatusAction::Execute(Event event) return true; } +bool StartRpgDoQuestAction::Execute(Event event) +{ + Player* owner = event.getOwner(); + if (!owner) + return false; + + std::string const text = event.getParam(); + PlayerbotChatHandler ch(owner); + uint32 questId = ch.extractQuestId(text); + const Quest* quest = sObjectMgr->GetQuestTemplate(questId); + if (quest) + { + botAI->rpgInfo.ChangeToDoQuest(questId, quest); + bot->Whisper("Start to do quest " + std::to_string(questId), LANG_UNIVERSAL, owner); + return true; + } + bot->Whisper("Invalid quest " + text, LANG_UNIVERSAL, owner); + return false; +} + bool NewRpgStatusUpdateAction::Execute(Event event) { NewRpgInfo& info = botAI->rpgInfo; + /// @TODO: Refactor by transition probability switch (info.status) { - case NewRpgStatus::IDLE: + case RPG_IDLE: { uint32 roll = urand(1, 100); // IDLE -> NEAR_NPC - // if ((!info.lastNearNpc || info.lastNearNpc + setNpcInterval < getMSTime()) && roll <= 30) if (roll <= 30) { - GuidVector possibleTargets = AI_VALUE(GuidVector, "possible rpg targets"); - if (!possibleTargets.empty()) + GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets"); + if (possibleTargets.size() >= 3) { - info.Reset(); - info.lastNearNpc = getMSTime(); - info.status = NewRpgStatus::NEAR_NPC; + info.ChangeToNearNpc(); return true; } } // IDLE -> GO_INNKEEPER else if (roll <= 45) { - WorldPosition pos = SelectRandomInnKeeperPos(); + WorldPosition pos = SelectRandomInnKeeperPos(bot); if (pos != WorldPosition() && bot->GetExactDist(pos) > 50.0f) { - info.Reset(); - info.lastGoInnKeeper = getMSTime(); - info.status = NewRpgStatus::GO_INNKEEPER; - info.innKeeperPos = pos; + info.ChangeToGoInnkeeper(pos); return true; } } // IDLE -> GO_GRIND - else if (roll <= 90) + else if (roll <= 100) { - WorldPosition pos = SelectRandomGrindPos(); + if (roll >= 60) + { + std::vector availableQuests; + for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) + { + uint32 questId = bot->GetQuestSlotQuestId(slot); + if (botAI->lowPriorityQuest.find(questId) != botAI->lowPriorityQuest.end()) + continue; + + std::vector poiInfo; + if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo, true)) + { + availableQuests.push_back(questId); + } + } + if (availableQuests.size()) + { + uint32 questId = availableQuests[urand(0, availableQuests.size() - 1)]; + const Quest* quest = sObjectMgr->GetQuestTemplate(questId); + if (quest) + { + // IDLE -> DO_QUEST + info.ChangeToDoQuest(questId, quest); + return true; + } + } + } + WorldPosition pos = SelectRandomGrindPos(bot); if (pos != WorldPosition()) { - info.Reset(); - info.lastGoGrind = getMSTime(); - info.status = NewRpgStatus::GO_GRIND; - info.grindPos = pos; + info.ChangeToGoGrind(pos); return true; } } // IDLE -> REST - info.Reset(); - info.status = NewRpgStatus::REST; - info.lastRest = getMSTime(); + info.ChangeToRest(); bot->SetStandState(UNIT_STAND_STATE_SIT); return true; } - case NewRpgStatus::GO_GRIND: + case RPG_GO_GRIND: { - WorldPosition& originalPos = info.grindPos; - assert(info.grindPos != WorldPosition()); + WorldPosition& originalPos = info.go_grind.pos; + assert(info.go_grind.pos != WorldPosition()); // GO_GRIND -> NEAR_RANDOM if (bot->GetExactDist(originalPos) < 10.0f) { - info.Reset(); - info.status = NewRpgStatus::NEAR_RANDOM; - info.lastNearRandom = getMSTime(); - info.grindPos = WorldPosition(); + info.ChangeToNearRandom(); return true; } break; } - case NewRpgStatus::GO_INNKEEPER: + case RPG_GO_INNKEEPER: { - WorldPosition& originalPos = info.innKeeperPos; - assert(info.innKeeperPos != WorldPosition()); + WorldPosition& originalPos = info.go_innkeeper.pos; + assert(info.go_innkeeper.pos != WorldPosition()); // GO_INNKEEPER -> NEAR_NPC if (bot->GetExactDist(originalPos) < 10.0f) { - info.Reset(); - info.lastNearNpc = getMSTime(); - info.status = NewRpgStatus::NEAR_NPC; - info.innKeeperPos = WorldPosition(); + info.ChangeToNearNpc(); return true; } break; } - case NewRpgStatus::NEAR_RANDOM: + case RPG_NEAR_RANDOM: { // NEAR_RANDOM -> IDLE - if (info.lastNearRandom + statusNearRandomDuration < getMSTime()) + if (info.HasStatusPersisted(statusNearRandomDuration)) { - info.Reset(); - info.status = NewRpgStatus::IDLE; + info.ChangeToIdle(); return true; } break; } - case NewRpgStatus::NEAR_NPC: + case RPG_DO_QUEST: { - if (info.lastNearNpc + statusNearNpcDuration < getMSTime()) + // DO_QUEST -> IDLE + if (info.HasStatusPersisted(statusDoQuestDuration)) { - info.Reset(); - info.status = NewRpgStatus::IDLE; + info.ChangeToIdle(); return true; } break; } - case NewRpgStatus::REST: + case RPG_NEAR_NPC: + { + if (info.HasStatusPersisted(statusNearNpcDuration)) + { + info.ChangeToIdle(); + return true; + } + break; + } + case RPG_REST: { // REST -> IDLE - if (info.lastRest + statusRestDuration < getMSTime()) + if (info.HasStatusPersisted(statusRestDuration)) { - info.Reset(); - info.status = NewRpgStatus::IDLE; + info.ChangeToIdle(); return true; } break; @@ -148,258 +200,260 @@ bool NewRpgStatusUpdateAction::Execute(Event event) return false; } -WorldPosition NewRpgStatusUpdateAction::SelectRandomGrindPos() +bool NewRpgGoGrindAction::Execute(Event event) { - const std::vector& locs = sRandomPlayerbotMgr->locsPerLevelCache[bot->GetLevel()]; - std::vector lo_prepared_locs, hi_prepared_locs; - for (auto& loc : locs) - { - if (bot->GetMapId() != loc.GetMapId()) - continue; + if (SearchQuestGiverAndAcceptOrReward()) + return true; - if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) != - bot->GetZoneId()) - continue; - - if (bot->GetExactDist(loc) < 500.0f) - { - hi_prepared_locs.push_back(loc); - } - - if (bot->GetExactDist(loc) < 2500.0f) - { - lo_prepared_locs.push_back(loc); - } - } - WorldPosition dest{}; - if (urand(1, 100) <= 50 && !hi_prepared_locs.empty()) - { - uint32 idx = urand(0, hi_prepared_locs.size() - 1); - dest = hi_prepared_locs[idx]; - } - else if (!lo_prepared_locs.empty()) - { - uint32 idx = urand(0, lo_prepared_locs.size() - 1); - dest = lo_prepared_locs[idx]; - } - LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})", - bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), - hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size()); - return dest; + return MoveFarTo(botAI->rpgInfo.go_grind.pos); } -WorldPosition NewRpgStatusUpdateAction::SelectRandomInnKeeperPos() +bool NewRpgGoInnKeeperAction::Execute(Event event) { - const std::vector& locs = IsAlliance(bot->getRace()) - ? sRandomPlayerbotMgr->allianceStarterPerLevelCache[bot->GetLevel()] - : sRandomPlayerbotMgr->hordeStarterPerLevelCache[bot->GetLevel()]; - std::vector prepared_locs; - for (auto& loc : locs) - { - if (bot->GetMapId() != loc.GetMapId()) - continue; - - if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) != - bot->GetZoneId()) - continue; - - float range = bot->GetLevel() <= 5 ? 500.0f : 2500.0f; - if (bot->GetExactDist(loc) < range) - { - prepared_locs.push_back(loc); - } - } - WorldPosition dest{}; - if (!prepared_locs.empty()) - { - uint32 idx = urand(0, prepared_locs.size() - 1); - dest = prepared_locs[idx]; - } - LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})", - bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), - prepared_locs.size(), locs.size()); - return dest; + if (SearchQuestGiverAndAcceptOrReward()) + return true; + + return MoveFarTo(botAI->rpgInfo.go_innkeeper.pos); } -bool NewRpgGoFarAwayPosAction::MoveFarTo(WorldPosition dest) -{ - if (dest == WorldPosition()) - return false; - - float dis = bot->GetExactDist(dest); - if (dis < pathFinderDis) - { - return MoveTo(dest.getMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false, - false, true); - } - - // performance optimization - if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) - { - return false; - } - - // stuck check - float disToDest = bot->GetDistance(dest); - if (disToDest + 1.0f < botAI->rpgInfo.nearestMoveFarDis) - { - botAI->rpgInfo.nearestMoveFarDis = disToDest; - botAI->rpgInfo.stuckTs = getMSTime(); - botAI->rpgInfo.stuckAttempts = 0; - } - else if (++botAI->rpgInfo.stuckAttempts >= 10 && botAI->rpgInfo.stuckTs + stuckTime < getMSTime()) - { - // Unfortunately we've been stuck here for over 5 mins, fallback to teleporting directly to the destination - botAI->rpgInfo.stuckTs = getMSTime(); - botAI->rpgInfo.stuckAttempts = 0; - const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId()); - std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry); - LOG_DEBUG("playerbots", "[New Rpg] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})", bot->GetName(), - bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(), - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.getMapId(), bot->GetZoneId(), zone_name); - return bot->TeleportTo(dest); - } - - float minDelta = M_PI; - const float x = bot->GetPositionX(); - const float y = bot->GetPositionY(); - const float z = bot->GetPositionZ(); - float rx, ry, rz; - bool found = false; - int attempt = 3; - while (--attempt) - { - float angle = bot->GetAngle(&dest); - float delta = urand(1, 100) <= 75 ? (rand_norm() - 0.5) * M_PI * 0.5 : (rand_norm() - 0.5) * M_PI * 2; - angle += delta; - float dis = rand_norm() * pathFinderDis; - float dx = x + cos(angle) * dis; - float dy = y + sin(angle) * dis; - float dz = z + 0.5f; - PathGenerator path(bot); - path.CalculatePath(dx, dy, dz); - PathType type = path.GetPathType(); - uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; - bool canReach = !(type & (~typeOk)); - - if (canReach && fabs(delta) <= minDelta) - { - found = true; - const G3D::Vector3& endPos = path.GetActualEndPosition(); - rx = endPos.x; - ry = endPos.y; - rz = endPos.z; - minDelta = fabs(delta); - } - } - if (found) - { - return MoveTo(bot->GetMapId(), rx, ry, rz, false, false, false, true); - } - return false; -} - -bool NewRpgGoGrindAction::Execute(Event event) { return MoveFarTo(botAI->rpgInfo.grindPos); } - -bool NewRpgGoInnKeeperAction::Execute(Event event) { return MoveFarTo(botAI->rpgInfo.innKeeperPos); } - bool NewRpgMoveRandomAction::Execute(Event event) { - float distance = rand_norm() * moveStep; - Map* map = bot->GetMap(); - const float x = bot->GetPositionX(); - const float y = bot->GetPositionY(); - const float z = bot->GetPositionZ(); - int attempts = 5; - while (--attempts) - { - float angle = (float)rand_norm() * 2 * static_cast(M_PI); - float dx = x + distance * cos(angle); - float dy = y + distance * sin(angle); - float dz = z; - if (!map->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), - dx, dy, dz)) - continue; - - if (map->IsInWater(bot->GetPhaseMask(), dx, dy, dz, bot->GetCollisionHeight())) - continue; - - bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true); - if (moved) - return true; - } - - return false; + if (SearchQuestGiverAndAcceptOrReward()) + return true; + + return MoveRandomNear(); } bool NewRpgMoveNpcAction::Execute(Event event) { NewRpgInfo& info = botAI->rpgInfo; - if (!info.npcPos) + if (!info.near_npc.npcOrGo) { - GuidVector possibleTargets = AI_VALUE(GuidVector, "possible rpg targets"); - if (possibleTargets.empty()) - return false; - int idx = urand(0, possibleTargets.size() - 1); - ObjectGuid guid = possibleTargets[idx]; - Unit* unit = botAI->GetUnit(guid); - if (unit) + // No npc can be found, switch to IDLE + ObjectGuid npcOrGo = ChooseNpcOrGameObjectToInteract(); + if (npcOrGo.IsEmpty()) { - info.npcPos = GuidPosition(unit); - info.lastReachNpc = 0; + info.ChangeToIdle(); + return true; } - else - return false; + info.near_npc.npcOrGo = npcOrGo; + info.near_npc.lastReach = 0; + return true; } - if (bot->GetDistance(info.npcPos) <= INTERACTION_DISTANCE) + WorldObject* object = ObjectAccessor::GetWorldObject(*bot, info.near_npc.npcOrGo); + if (object && bot->GetDistance(object) <= INTERACTION_DISTANCE) { - if (!info.lastReachNpc) + if (!info.near_npc.lastReach) { - info.lastReachNpc = getMSTime(); + info.near_npc.lastReach = getMSTime(); + if (bot->CanInteractWithQuestGiver(object)) + InteractWithNpcOrGameObjectForQuest(info.near_npc.npcOrGo); return true; } - if (info.lastReachNpc && info.lastReachNpc + stayTime > getMSTime()) + if (info.near_npc.lastReach && GetMSTimeDiffToNow(info.near_npc.lastReach) < npcStayTime) return false; - info.npcPos = GuidPosition(); - info.lastReachNpc = 0; + // has reached the npc for more than `npcStayTime`, select the next target + info.near_npc.npcOrGo = ObjectGuid(); + info.near_npc.lastReach = 0; } else { - assert(info.npcPos); - Unit* unit = botAI->GetUnit(info.npcPos); - if (!unit) - return false; - float x = unit->GetPositionX(); - float y = unit->GetPositionY(); - float z = unit->GetPositionZ(); - float mapId = unit->GetMapId(); - float angle = 0.f; - if (bot->IsWithinLOS(x, y, z)) - { - if (!unit->isMoving()) - angle = unit->GetAngle(bot) + (M_PI * irand(-25, 25) / 100.0); // Closest 45 degrees towards the target - else - angle = unit->GetOrientation() + - (M_PI * irand(-25, 25) / 100.0); // 45 degrees infront of target (leading it's movement) - } - else - angle = 2 * M_PI * rand_norm(); // A circle around the target. - float rnd = rand_norm(); - x += cos(angle) * INTERACTION_DISTANCE * rnd; - y += sin(angle) * INTERACTION_DISTANCE * rnd; - // bool exact = true; - if (!unit->GetMap()->CheckCollisionAndGetValidCoords(unit, unit->GetPositionX(), unit->GetPositionY(), - unit->GetPositionZ(), x, y, z)) - { - x = unit->GetPositionX(); - y = unit->GetPositionY(); - z = unit->GetPositionZ(); - // exact = false; - } - return MoveTo(mapId, x, y, z, false, false, false, true); + return MoveWorldObjectTo(info.near_npc.npcOrGo); } return true; -} \ No newline at end of file +} + +bool NewRpgDoQuestAction::Execute(Event event) +{ + if (SearchQuestGiverAndAcceptOrReward()) + return true; + + NewRpgInfo& info = botAI->rpgInfo; + uint32 questId = RPG_INFO(quest, questId); + const Quest* quest = RPG_INFO(quest, quest); + uint8 questStatus = bot->GetQuestStatus(questId); + switch (questStatus) + { + case QUEST_STATUS_INCOMPLETE: + return DoIncompleteQuest(); + case QUEST_STATUS_COMPLETE: + return DoCompletedQuest(); + default: + break; + } + botAI->rpgInfo.ChangeToIdle(); + return true; +} + +bool NewRpgDoQuestAction::DoIncompleteQuest() +{ + uint32 questId = RPG_INFO(do_quest, questId); + if (botAI->rpgInfo.do_quest.pos != WorldPosition()) + { + /// @TODO: extract to a new function + int32 currentObjective = botAI->rpgInfo.do_quest.objectiveIdx; + // check if the objective has completed + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId); + bool completed = true; + if (currentObjective < QUEST_OBJECTIVES_COUNT) + { + if (q_status.CreatureOrGOCount[currentObjective] < quest->RequiredNpcOrGoCount[currentObjective]) + completed = false; + } + else if (currentObjective < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT) + { + if (q_status.ItemCount[currentObjective - QUEST_OBJECTIVES_COUNT] < + quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT]) + completed = false; + } + // the current objective is completed, clear and find a new objective later + if (completed) + { + botAI->rpgInfo.do_quest.lastReachPOI = 0; + botAI->rpgInfo.do_quest.pos = WorldPosition(); + botAI->rpgInfo.do_quest.objectiveIdx = 0; + } + } + if (botAI->rpgInfo.do_quest.pos == WorldPosition()) + { + std::vector poiInfo; + if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo)) + { + // can't find a poi pos to go, stop doing quest for now + botAI->rpgInfo.ChangeToIdle(); + return true; + } + uint32 rndIdx = urand(0, poiInfo.size() - 1); + G3D::Vector2 nearestPoi = poiInfo[rndIdx].pos; + int32 objectiveIdx = poiInfo[rndIdx].objectiveIdx; + + float dx = nearestPoi.x, dy = nearestPoi.y; + + // z = MAX_HEIGHT as we do not know accurate z + float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy)); + + // double check for GetQuestPOIPosAndObjectiveIdx + if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE) + return false; + + WorldPosition pos(bot->GetMapId(), dx, dy, dz); + botAI->rpgInfo.do_quest.lastReachPOI = 0; + botAI->rpgInfo.do_quest.pos = pos; + botAI->rpgInfo.do_quest.objectiveIdx = objectiveIdx; + } + + if (bot->GetDistance(botAI->rpgInfo.do_quest.pos) > 10.0f && !botAI->rpgInfo.do_quest.lastReachPOI) + { + return MoveFarTo(botAI->rpgInfo.do_quest.pos); + } + // Now we are near the quest objective + // kill mobs and looting quest should be done automatically by grind strategy + + if (!botAI->rpgInfo.do_quest.lastReachPOI) + { + botAI->rpgInfo.do_quest.lastReachPOI = getMSTime(); + return true; + } + // stayed at this POI for more than 5 minutes + if (GetMSTimeDiffToNow(botAI->rpgInfo.do_quest.lastReachPOI) >= poiStayTime) + { + bool hasProgression = false; + int32 currentObjective = botAI->rpgInfo.do_quest.objectiveIdx; + // check if the objective has progression + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId); + if (currentObjective < QUEST_OBJECTIVES_COUNT) + { + if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective]) + hasProgression = true; + } + else if (currentObjective < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT) + { + if (q_status.ItemCount[currentObjective - QUEST_OBJECTIVES_COUNT] != 0 && + quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT]) + hasProgression = true; + } + if (!hasProgression) + { + // we has reach the poi for more than 5 mins but no progession + // may not be able to complete this quest, marked as abandoned + /// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db) + botAI->lowPriorityQuest.insert(questId); + botAI->rpgStatistic.questAbandoned++; + LOG_DEBUG("playerbots", "[New rpg] {} marked as abandoned quest {}", bot->GetName(), questId); + botAI->rpgInfo.ChangeToIdle(); + return true; + } + // clear and select another poi later + botAI->rpgInfo.do_quest.lastReachPOI = 0; + botAI->rpgInfo.do_quest.pos = WorldPosition(); + botAI->rpgInfo.do_quest.objectiveIdx = 0; + return true; + } + + return MoveRandomNear(20.0f); +} + +bool NewRpgDoQuestAction::DoCompletedQuest() +{ + uint32 questId = RPG_INFO(quest, questId); + const Quest* quest = RPG_INFO(quest, quest); + + if (RPG_INFO(quest, objectiveIdx) != -1) + { + // if quest is completed, back to poi with -1 idx to reward + BroadcastHelper::BroadcastQuestUpdateComplete(botAI, bot, quest); + botAI->rpgStatistic.questCompleted++; + std::vector poiInfo; + if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo, true)) + { + // can't find a poi pos to reward, stop doing quest for now + botAI->rpgInfo.ChangeToIdle(); + return false; + } + assert(poiInfo.size() > 0); + // now we get the place to get rewarded + float dx = poiInfo[0].pos.x, dy = poiInfo[0].pos.y; + // z = MAX_HEIGHT as we do not know accurate z + float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy)); + + // double check for GetQuestPOIPosAndObjectiveIdx + if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE) + return false; + + WorldPosition pos(bot->GetMapId(), dx, dy, dz); + botAI->rpgInfo.do_quest.lastReachPOI = 0; + botAI->rpgInfo.do_quest.pos = pos; + botAI->rpgInfo.do_quest.objectiveIdx = -1; + } + + if (botAI->rpgInfo.do_quest.pos == WorldPosition()) + return false; + + if (bot->GetDistance(botAI->rpgInfo.do_quest.pos) > 10.0f && !botAI->rpgInfo.do_quest.lastReachPOI) + return MoveFarTo(botAI->rpgInfo.do_quest.pos); + + // Now we are near the qoi of reward + // the quest should be rewarded by SearchQuestGiverAndAcceptOrReward + if (!botAI->rpgInfo.do_quest.lastReachPOI) + { + botAI->rpgInfo.do_quest.lastReachPOI = getMSTime(); + return true; + } + // stayed at this POI for more than 5 minutes + if (GetMSTimeDiffToNow(botAI->rpgInfo.do_quest.lastReachPOI) >= poiStayTime) + { + // e.g. Can not reward quest to gameobjects + /// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db) + botAI->lowPriorityQuest.insert(questId); + botAI->rpgStatistic.questAbandoned++; + LOG_DEBUG("playerbots", "[New rpg] {} marked as abandoned quest {}", bot->GetName(), questId); + botAI->rpgInfo.ChangeToIdle(); + return true; + } + return false; +} diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.h b/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.h index 83cb604..422d577 100644 --- a/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.h +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgAction.h @@ -3,9 +3,15 @@ #include "Duration.h" #include "MovementActions.h" +#include "NewRpgInfo.h" #include "NewRpgStrategy.h" +#include "Object.h" +#include "ObjectDefines.h" +#include "ObjectGuid.h" +#include "QuestDef.h" #include "TravelMgr.h" #include "PlayerbotAI.h" +#include "NewRpgBaseAction.h" class TellRpgStatusAction : public Action { @@ -15,65 +21,78 @@ public: bool Execute(Event event) override; }; -class NewRpgStatusUpdateAction : public Action +class StartRpgDoQuestAction : public Action { public: - NewRpgStatusUpdateAction(PlayerbotAI* botAI) : Action(botAI, "new rpg status update") {} + StartRpgDoQuestAction(PlayerbotAI* botAI) : Action(botAI, "start rpg do quest") {} + + bool Execute(Event event) override; +}; + +class NewRpgStatusUpdateAction : public NewRpgBaseAction +{ +public: + NewRpgStatusUpdateAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg status update") + { + // int statusCount = RPG_STATUS_END - 1; + + // transitionMat.resize(statusCount, std::vector(statusCount, 0)); + + // transitionMat[RPG_IDLE][RPG_GO_GRIND] = 20; + // transitionMat[RPG_IDLE][RPG_GO_INNKEEPER] = 15; + // transitionMat[RPG_IDLE][RPG_NEAR_NPC] = 30; + // transitionMat[RPG_IDLE][RPG_DO_QUEST] = 35; + } bool Execute(Event event) override; protected: - // const int32 setGrindInterval = 5 * 60 * 1000; - // const int32 setNpcInterval = 1 * 60 * 1000; + // static NewRpgStatusTransitionProb transitionMat; const int32 statusNearNpcDuration = 5 * 60 * 1000; const int32 statusNearRandomDuration = 5 * 60 * 1000; const int32 statusRestDuration = 30 * 1000; - WorldPosition SelectRandomGrindPos(); - WorldPosition SelectRandomInnKeeperPos(); + const int32 statusDoQuestDuration = 30 * 60 * 1000; }; -class NewRpgGoFarAwayPosAction : public MovementAction +class NewRpgGoGrindAction : public NewRpgBaseAction { public: - NewRpgGoFarAwayPosAction(PlayerbotAI* botAI, std::string name) : MovementAction(botAI, name) {} - // bool Execute(Event event) override; - bool MoveFarTo(WorldPosition dest); - -protected: - // WorldPosition dest; - const float pathFinderDis = 70.0f; // path finder - const uint32 stuckTime = 5 * 60 * 1000; -}; - -class NewRpgGoGrindAction : public NewRpgGoFarAwayPosAction -{ -public: - NewRpgGoGrindAction(PlayerbotAI* botAI) : NewRpgGoFarAwayPosAction(botAI, "new rpg go grind") {} + NewRpgGoGrindAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg go grind") {} bool Execute(Event event) override; }; -class NewRpgGoInnKeeperAction : public NewRpgGoFarAwayPosAction +class NewRpgGoInnKeeperAction : public NewRpgBaseAction { public: - NewRpgGoInnKeeperAction(PlayerbotAI* botAI) : NewRpgGoFarAwayPosAction(botAI, "new rpg go innkeeper") {} + NewRpgGoInnKeeperAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg go innkeeper") {} bool Execute(Event event) override; }; -class NewRpgMoveRandomAction : public MovementAction +class NewRpgMoveRandomAction : public NewRpgBaseAction { public: - NewRpgMoveRandomAction(PlayerbotAI* botAI) : MovementAction(botAI, "new rpg move random") {} + NewRpgMoveRandomAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg move random") {} + bool Execute(Event event) override; +}; + +class NewRpgMoveNpcAction : public NewRpgBaseAction +{ +public: + NewRpgMoveNpcAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg move npcs") {} + bool Execute(Event event) override; + + const uint32 npcStayTime = 8 * 1000; +}; + +class NewRpgDoQuestAction : public NewRpgBaseAction +{ +public: + NewRpgDoQuestAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg do quest") {} bool Execute(Event event) override; protected: - const float moveStep = 50.0f; -}; - -class NewRpgMoveNpcAction : public MovementAction -{ -public: - NewRpgMoveNpcAction(PlayerbotAI* botAI) : MovementAction(botAI, "new rpg move npcs") {} - bool Execute(Event event) override; -protected: - const uint32 stayTime = 8 * 1000; + bool DoIncompleteQuest(); + bool DoCompletedQuest(); + + const uint32 poiStayTime = 5 * 60 * 1000; }; #endif \ No newline at end of file diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgBaseAction.cpp b/modules/mod-playerbots/src/strategy/rpg/NewRpgBaseAction.cpp new file mode 100644 index 0000000..3d387c1 --- /dev/null +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgBaseAction.cpp @@ -0,0 +1,801 @@ +#include "NewRpgBaseAction.h" +#include "ChatHelper.h" +#include "G3D/Vector2.h" +#include "GameObject.h" +#include "GossipDef.h" +#include "GridTerrainData.h" +#include "IVMapMgr.h" +#include "NewRpgInfo.h" +#include "NewRpgStrategy.h" +#include "Object.h" +#include "ObjectAccessor.h" +#include "ObjectDefines.h" +#include "ObjectGuid.h" +#include "ObjectMgr.h" +#include "PathGenerator.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "Position.h" +#include "QuestDef.h" +#include "Random.h" +#include "RandomPlayerbotMgr.h" +#include "SharedDefines.h" +#include "StatsWeightCalculator.h" +#include "Timer.h" +#include "TravelMgr.h" +#include "BroadcastHelper.h" + +bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) +{ + if (dest == WorldPosition()) + return false; + + if (dest != botAI->rpgInfo.moveFarPos) + { + // clear stuck information if it's a new dest + botAI->rpgInfo.SetMoveFarTo(dest); + } + + float dis = bot->GetExactDist(dest); + if (dis < pathFinderDis) + { + return MoveTo(dest.getMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false, + false, true); + } + + // performance optimization + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) + { + return false; + } + + // stuck check + float disToDest = bot->GetDistance(dest); + if (disToDest + 1.0f < botAI->rpgInfo.nearestMoveFarDis) + { + botAI->rpgInfo.nearestMoveFarDis = disToDest; + botAI->rpgInfo.stuckTs = getMSTime(); + botAI->rpgInfo.stuckAttempts = 0; + } + else if (++botAI->rpgInfo.stuckAttempts >= 10 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime) + { + // Unfortunately we've been stuck here for over 5 mins, fallback to teleporting directly to the destination + botAI->rpgInfo.stuckTs = getMSTime(); + botAI->rpgInfo.stuckAttempts = 0; + const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId()); + std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry); + LOG_DEBUG("playerbots", "[New Rpg] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})", bot->GetName(), + bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(), + dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.getMapId(), bot->GetZoneId(), zone_name); + return bot->TeleportTo(dest); + } + + float minDelta = M_PI; + const float x = bot->GetPositionX(); + const float y = bot->GetPositionY(); + const float z = bot->GetPositionZ(); + float rx, ry, rz; + bool found = false; + int attempt = 3; + while (--attempt) + { + float angle = bot->GetAngle(&dest); + float delta = urand(1, 100) <= 75 ? (rand_norm() - 0.5) * M_PI * 0.5 : (rand_norm() - 0.5) * M_PI * 2; + angle += delta; + float dis = rand_norm() * pathFinderDis; + float dx = x + cos(angle) * dis; + float dy = y + sin(angle) * dis; + float dz = z + 0.5f; + PathGenerator path(bot); + path.CalculatePath(dx, dy, dz); + PathType type = path.GetPathType(); + uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; + bool canReach = !(type & (~typeOk)); + + if (canReach && fabs(delta) <= minDelta) + { + found = true; + const G3D::Vector3& endPos = path.GetActualEndPosition(); + rx = endPos.x; + ry = endPos.y; + rz = endPos.z; + minDelta = fabs(delta); + } + } + if (found) + { + return MoveTo(bot->GetMapId(), rx, ry, rz, false, false, false, true); + } + return false; +} + +bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) +{ + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) + { + return false; + } + + WorldObject* object = botAI->GetWorldObject(guid); + if (!object) + return false; + float x = object->GetPositionX(); + float y = object->GetPositionY(); + float z = object->GetPositionZ(); + float mapId = object->GetMapId(); + float angle = 0.f; + + if (!object->ToUnit() || !object->ToUnit()->isMoving()) + angle = object->GetAngle(bot) + (M_PI * irand(-25, 25) / 100.0); // Closest 45 degrees towards the target + else + angle = object->GetOrientation() + + (M_PI * irand(-25, 25) / 100.0); // 45 degrees infront of target (leading it's movement) + + float rnd = rand_norm(); + x += cos(angle) * distance * rnd; + y += sin(angle) * distance * rnd; + if (!object->GetMap()->CheckCollisionAndGetValidCoords(object, object->GetPositionX(), object->GetPositionY(), + object->GetPositionZ(), x, y, z)) + { + x = object->GetPositionX(); + y = object->GetPositionY(); + z = object->GetPositionZ(); + } + return MoveTo(mapId, x, y, z, false, false, false, true); +} + +bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority) +{ + if (IsWaitingForLastMove(priority)) + { + return false; + } + + float distance = rand_norm() * moveStep; + Map* map = bot->GetMap(); + const float x = bot->GetPositionX(); + const float y = bot->GetPositionY(); + const float z = bot->GetPositionZ(); + int attempts = 5; + while (--attempts) + { + float angle = (float)rand_norm() * 2 * static_cast(M_PI); + float dx = x + distance * cos(angle); + float dy = y + distance * sin(angle); + float dz = z; + + PathGenerator path(bot); + path.CalculatePath(dx, dy, dz); + PathType type = path.GetPathType(); + uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; + bool canReach = !(type & (~typeOk)); + + if (!canReach) + continue; + + if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz)) + continue; + + if (map->IsInWater(bot->GetPhaseMask(), dx, dy, dz, bot->GetCollisionHeight())) + continue; + + bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true, priority); + if (moved) + return true; + } + + return false; +} + +bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority) +{ + AI_VALUE(LastMovement&, "last movement").Set(bot->GetMapId(), + bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetOrientation(), duration, priority); + return true; +} + +/// @TODO: Fix redundant code +/// Quest related method refer to TalkToQuestGiverAction.h +bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid) +{ + WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid); + if (!object || !bot->CanInteractWithQuestGiver(object)) + return false; + + // Creature* creature = bot->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_NONE); + // if (creature) + // { + // WorldPacket packet(CMSG_GOSSIP_HELLO); + // packet << guid; + // bot->GetSession()->HandleGossipHelloOpcode(packet); + // } + + bot->PrepareQuestMenu(guid); + const QuestMenu &menu = bot->PlayerTalkClass->GetQuestMenu(); + if (menu.Empty()) + return true; + + for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++) + { + const QuestMenuItem &item = menu.GetItem(idx); + const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId); + if (!quest) + continue; + + const QuestStatus &status = bot->GetQuestStatus(item.QuestId); + if (status == QUEST_STATUS_NONE && bot->CanTakeQuest(quest, false) && + bot->CanAddQuest(quest, false) && IsQuestWorthDoing(quest) && IsQuestCapableDoing(quest)) + { + AcceptQuest(quest, guid); + if (botAI->GetMaster()) + botAI->TellMasterNoFacing("Quest accepted " + ChatHelper::FormatQuest(quest)); + BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest); + botAI->rpgStatistic.questAccepted++; + LOG_DEBUG("playerbots", "[New rpg] {} accept quest {}", bot->GetName(), quest->GetQuestId()); + } + if (status == QUEST_STATUS_COMPLETE && bot->CanRewardQuest(quest, 0, false)) + { + TurnInQuest(quest, guid); + if (botAI->GetMaster()) + botAI->TellMasterNoFacing("Quest rewarded " + ChatHelper::FormatQuest(quest)); + BroadcastHelper::BroadcastQuestTurnedIn(botAI, bot, quest); + botAI->rpgStatistic.questRewarded++; + LOG_DEBUG("playerbots", "[New rpg] {} turned in quest {}", bot->GetName(), quest->GetQuestId()); + } + } + return true; +} + +bool NewRpgBaseAction::AcceptQuest(Quest const* quest, ObjectGuid guid) +{ + WorldPacket p(CMSG_QUESTGIVER_ACCEPT_QUEST); + uint32 unk1 = 0; + p << guid << quest->GetQuestId() << unk1; + p.rpos(0); + bot->GetSession()->HandleQuestgiverAcceptQuestOpcode(p); + + return true; +} + +bool NewRpgBaseAction::TurnInQuest(Quest const* quest, ObjectGuid guid) +{ + uint32 questID = quest->GetQuestId(); + + if (bot->GetQuestRewardStatus(questID)) + { + return false; + } + + if (!bot->CanRewardQuest(quest, false)) + { + return false; + } + + bot->PlayDistanceSound(621); + + WorldPacket p(CMSG_QUESTGIVER_CHOOSE_REWARD); + p << guid << quest->GetQuestId(); + if (quest->GetRewChoiceItemsCount() <= 1) + { + p << 0; + bot->GetSession()->HandleQuestgiverChooseRewardOpcode(p); + } + else + { + uint32 bestId = BestReward(quest); + p << bestId; + bot->GetSession()->HandleQuestgiverChooseRewardOpcode(p); + } + + return true; +} + +uint32 NewRpgBaseAction::BestReward(Quest const* quest) +{ + ItemIds returnIds; + ItemUsage bestUsage = ITEM_USAGE_NONE; + if (quest->GetRewChoiceItemsCount() <= 1) + return 0; + else + { + for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i) + { + ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", quest->RewardChoiceItemId[i]); + if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE) + bestUsage = ITEM_USAGE_EQUIP; + else if (usage == ITEM_USAGE_BAD_EQUIP && bestUsage != ITEM_USAGE_EQUIP) + bestUsage = usage; + else if (usage != ITEM_USAGE_NONE && bestUsage == ITEM_USAGE_NONE) + bestUsage = usage; + } + StatsWeightCalculator calc(bot); + uint32 best = 0; + float bestScore = 0; + for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i) + { + ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", quest->RewardChoiceItemId[i]); + if (usage == bestUsage || usage == ITEM_USAGE_REPLACE) + { + float score = calc.CalculateItem(quest->RewardChoiceItemId[i]); + if (score > bestScore) + { + bestScore = score; + best = i; + } + } + } + return best; + } +} + +bool NewRpgBaseAction::IsQuestWorthDoing(Quest const* quest) +{ + bool isLowLevelQuest = bot->GetLevel() > (bot->GetQuestLevel(quest) + sWorld->getIntConfig(CONFIG_QUEST_LOW_LEVEL_HIDE_DIFF)); + + if (isLowLevelQuest) + return false; + + if (quest->IsRepeatable()) + return false; + + if (quest->IsSeasonal()) + return false; + + return true; +} + +bool NewRpgBaseAction::IsQuestCapableDoing(Quest const* quest) +{ + bool highLevelQuest = bot->GetLevel() + 3 < bot->GetQuestLevel(quest); + if (highLevelQuest) + return false; + + // Elite quest and dungeon quest etc + if (quest->GetType() != 0) + return false; + + // now we only capable of doing solo quests + if (quest->GetSuggestedPlayers() >= 2) + return false; + + return true; +} + +bool NewRpgBaseAction::OrganizeQuestLog() +{ + int32 freeSlotNum = 0; + + for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questId = bot->GetQuestSlotQuestId(i); + if (!questId) + freeSlotNum++; + } + + // it's ok if we have two more free slots + if (freeSlotNum >= 2) + return false; + + int32 dropped = 0; + // remove quests that not worth doing or not capable of doing + for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questId = bot->GetQuestSlotQuestId(i); + if (!questId) + continue; + + const Quest* quest = sObjectMgr->GetQuestTemplate(questId); + if (!IsQuestWorthDoing(quest) || + !IsQuestCapableDoing(quest) || + bot->GetQuestStatus(questId) == QUEST_STATUS_FAILED) + { + LOG_DEBUG("playerbots", "[New rpg] {} drop quest {}", bot->GetName(), questId); + WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST); + packet << (uint8)i; + bot->GetSession()->HandleQuestLogRemoveQuest(packet); + if (botAI->GetMaster()) + botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest)); + botAI->rpgStatistic.questDropped++; + dropped++; + } + } + + // drop more than 8 quests at once to avoid repeated accept and drop + if (dropped >= 8) + return true; + + // remove festival/class quests and quests in different zone + for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questId = bot->GetQuestSlotQuestId(i); + if (!questId) + continue; + + const Quest* quest = sObjectMgr->GetQuestTemplate(questId); + if (quest->GetZoneOrSort() < 0 || + (quest->GetZoneOrSort() > 0 && quest->GetZoneOrSort() != bot->GetZoneId())) + { + LOG_DEBUG("playerbots", "[New rpg] {} drop quest {}", bot->GetName(), questId); + WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST); + packet << (uint8)i; + bot->GetSession()->HandleQuestLogRemoveQuest(packet); + if (botAI->GetMaster()) + botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest)); + botAI->rpgStatistic.questDropped++; + dropped++; + } + } + + if (dropped >= 8) + return true; + + // clear quests log + for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i) + { + uint32 questId = bot->GetQuestSlotQuestId(i); + if (!questId) + continue; + + const Quest* quest = sObjectMgr->GetQuestTemplate(questId); + LOG_DEBUG("playerbots", "[New rpg] {} drop quest {}", bot->GetName(), questId); + WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST); + packet << (uint8)i; + bot->GetSession()->HandleQuestLogRemoveQuest(packet); + if (botAI->GetMaster()) + botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest)); + botAI->rpgStatistic.questDropped++; + } + + return true; +} + +bool NewRpgBaseAction::SearchQuestGiverAndAcceptOrReward() +{ + OrganizeQuestLog(); + if (ObjectGuid npcOrGo = ChooseNpcOrGameObjectToInteract(true, 80.0f)) + { + WorldObject* object = ObjectAccessor::GetWorldObject(*bot, npcOrGo); + if (bot->CanInteractWithQuestGiver(object)) + { + InteractWithNpcOrGameObjectForQuest(npcOrGo); + ForceToWait(5000); + return true; + } + return MoveWorldObjectTo(npcOrGo); + } + return false; +} + +ObjectGuid NewRpgBaseAction::ChooseNpcOrGameObjectToInteract(bool questgiverOnly, float distanceLimit) +{ + GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets"); + GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects"); + + if (possibleTargets.empty() && possibleGameObjects.empty()) + return ObjectGuid(); + + WorldObject* nearestObject = nullptr; + for (ObjectGuid& guid: possibleTargets) + { + WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid); + + if (!object || !object->IsInWorld()) + continue; + + if (distanceLimit && bot->GetDistance(object) > distanceLimit) + continue; + + if (HasQuestToAcceptOrReward(object)) + { + if (!nearestObject || bot->GetExactDist(nearestObject) > bot->GetExactDist(object)) + nearestObject = object; + break; + } + } + + for (ObjectGuid& guid: possibleGameObjects) + { + WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid); + + if (!object || !object->IsInWorld()) + continue; + + if (distanceLimit && bot->GetDistance(object) > distanceLimit) + continue; + + if (HasQuestToAcceptOrReward(object)) + { + if (!nearestObject || bot->GetExactDist(nearestObject) > bot->GetExactDist(object)) + nearestObject = object; + break; + } + } + + if (nearestObject) + return nearestObject->GetGUID(); + + // No questgiver to accept or reward + if (questgiverOnly) + return ObjectGuid(); + + if (possibleTargets.empty()) + return ObjectGuid(); + + int idx = urand(0, possibleTargets.size() - 1); + ObjectGuid guid = possibleTargets[idx]; + WorldObject* object = ObjectAccessor::GetCreatureOrPetOrVehicle(*bot, guid); + if (!object) + object = ObjectAccessor::GetGameObject(*bot, guid); + + if (object && object->IsInWorld()) + { + return object->GetGUID(); + } + return ObjectGuid(); +} + +bool NewRpgBaseAction::HasQuestToAcceptOrReward(WorldObject* object) +{ + ObjectGuid guid = object->GetGUID(); + bot->PrepareQuestMenu(guid); + const QuestMenu &menu = bot->PlayerTalkClass->GetQuestMenu(); + if (menu.Empty()) + return false; + + for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++) + { + const QuestMenuItem &item = menu.GetItem(idx); + const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId); + if (!quest) + continue; + const QuestStatus &status = bot->GetQuestStatus(item.QuestId); + if (status == QUEST_STATUS_COMPLETE && bot->CanRewardQuest(quest, 0, false)) + { + return true; + } + } + for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++) + { + const QuestMenuItem &item = menu.GetItem(idx); + const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId); + if (!quest) + continue; + + const QuestStatus &status = bot->GetQuestStatus(item.QuestId); + if (status == QUEST_STATUS_NONE && bot->CanTakeQuest(quest, false) && + bot->CanAddQuest(quest, false) && IsQuestWorthDoing(quest) && IsQuestCapableDoing(quest)) + { + return true; + } + } + return false; +} + +static std::vector GenerateRandomWeights(int n) { + std::vector weights(n); + float sum = 0.0; + + for (int i = 0; i < n; ++i) { + weights[i] = rand_norm(); + sum += weights[i]; + } + for (int i = 0; i < n; ++i) { + weights[i] /= sum; + } + return weights; +} + +bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector &poiInfo, bool toComplete) +{ + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + return false; + + const QuestPOIVector* poiVector = sObjectMgr->GetQuestPOIVector(questId); + if (!poiVector) + { + return false; + } + + const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId); + + if (toComplete && q_status.Status == QUEST_STATUS_COMPLETE) + { + for (const QuestPOI &qPoi : *poiVector) + { + if (qPoi.MapId != bot->GetMapId()) + continue; + + // not the poi pos to reward quest + if (qPoi.ObjectiveIndex != -1) + continue; + + if (qPoi.points.size() == 0) + continue; + + float dx = 0, dy = 0; + std::vector weights = GenerateRandomWeights(qPoi.points.size()); + for (size_t i = 0; i < qPoi.points.size(); i++) + { + const QuestPOIPoint &point = qPoi.points[i]; + dx += point.x * weights[i]; + dy += point.y * weights[i]; + } + + if (bot->GetDistance2d(dx, dy) >= 1500.0f) + continue; + + float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy)); + + if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE) + continue; + + if (bot->GetZoneId() != bot->GetMap()->GetZoneId(bot->GetPhaseMask(), dx, dy, dz)) + continue; + + poiInfo.push_back({{dx, dy}, qPoi.ObjectiveIndex}); + } + + if (poiInfo.empty()) + return false; + + return true; + } + + if (q_status.Status != QUEST_STATUS_INCOMPLETE) + return false; + + // Get incomplete quest objective index + std::vector incompleteObjectiveIdx; + for (int i = 0; i < QUEST_OBJECTIVES_COUNT; i++) + { + int32 npcOrGo = quest->RequiredNpcOrGo[i]; + if (!npcOrGo) + continue; + + if (q_status.CreatureOrGOCount[i] < quest->RequiredNpcOrGoCount[i]) + incompleteObjectiveIdx.push_back(i); + } + for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++) + { + uint32 itemId = quest->RequiredItemId[i]; + if (!itemId) + continue; + + if (q_status.ItemCount[i] < quest->RequiredItemCount[i]) + incompleteObjectiveIdx.push_back(QUEST_OBJECTIVES_COUNT + i); + } + + // Get POIs to go + for (const QuestPOI &qPoi : *poiVector) + { + if (qPoi.MapId != bot->GetMapId()) + continue; + + bool inComplete = false; + for (uint32 objective : incompleteObjectiveIdx) + { + if (qPoi.ObjectiveIndex == objective) + { + inComplete = true; + break; + } + } + if (!inComplete) + continue; + if (qPoi.points.size() == 0) + continue; + float dx = 0, dy = 0; + std::vector weights = GenerateRandomWeights(qPoi.points.size()); + for (size_t i = 0; i < qPoi.points.size(); i++) + { + const QuestPOIPoint &point = qPoi.points[i]; + dx += point.x * weights[i]; + dy += point.y * weights[i]; + } + + if (bot->GetDistance2d(dx, dy) >= 1500.0f) + continue; + + float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy)); + + if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE) + continue; + + if (bot->GetZoneId() != bot->GetMap()->GetZoneId(bot->GetPhaseMask(), dx, dy, dz)) + continue; + + poiInfo.push_back({{dx, dy}, qPoi.ObjectiveIndex}); + } + + if (poiInfo.size() == 0) { + // LOG_DEBUG("playerbots", "[New rpg] {}: No available poi can be found for quest {}", bot->GetName(), questId); + return false; + } + + return true; +} + +WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) +{ + const std::vector& locs = sRandomPlayerbotMgr->locsPerLevelCache[bot->GetLevel()]; + float hiRange = 500.0f; + float loRange = 2500.0f; + if (bot->GetLevel() < 5) + { + hiRange /= 10; + loRange /= 10; + } + std::vector lo_prepared_locs, hi_prepared_locs; + for (auto& loc : locs) + { + if (bot->GetMapId() != loc.GetMapId()) + continue; + + if (bot->GetExactDist(loc) > 2500.0f) + continue; + + if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) != + bot->GetZoneId()) + continue; + + if (bot->GetExactDist(loc) < 500.0f) + { + hi_prepared_locs.push_back(loc); + } + + if (bot->GetExactDist(loc) < 2500.0f) + { + lo_prepared_locs.push_back(loc); + } + } + WorldPosition dest{}; + if (urand(1, 100) <= 50 && !hi_prepared_locs.empty()) + { + uint32 idx = urand(0, hi_prepared_locs.size() - 1); + dest = hi_prepared_locs[idx]; + } + else if (!lo_prepared_locs.empty()) + { + uint32 idx = urand(0, lo_prepared_locs.size() - 1); + dest = lo_prepared_locs[idx]; + } + LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})", + bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), + hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size()); + return dest; +} + +WorldPosition NewRpgBaseAction::SelectRandomInnKeeperPos(Player* bot) +{ + const std::vector& locs = IsAlliance(bot->getRace()) + ? sRandomPlayerbotMgr->allianceStarterPerLevelCache[bot->GetLevel()] + : sRandomPlayerbotMgr->hordeStarterPerLevelCache[bot->GetLevel()]; + std::vector prepared_locs; + for (auto& loc : locs) + { + if (bot->GetMapId() != loc.GetMapId()) + continue; + + float range = bot->GetLevel() <= 5 ? 500.0f : 2500.0f; + if (bot->GetExactDist(loc) > range) + continue; + + if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) != + bot->GetZoneId()) + continue; + + prepared_locs.push_back(loc); + } + WorldPosition dest{}; + if (!prepared_locs.empty()) + { + uint32 idx = urand(0, prepared_locs.size() - 1); + dest = prepared_locs[idx]; + } + LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})", + bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), + prepared_locs.size(), locs.size()); + return dest; +} \ No newline at end of file diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgBaseAction.h b/modules/mod-playerbots/src/strategy/rpg/NewRpgBaseAction.h new file mode 100644 index 0000000..62c415a --- /dev/null +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgBaseAction.h @@ -0,0 +1,58 @@ +#ifndef _PLAYERBOT_NEWRPGBASEACTION_H +#define _PLAYERBOT_NEWRPGBASEACTION_H + +#include "Duration.h" +#include "LastMovementValue.h" +#include "MovementActions.h" +#include "NewRpgStrategy.h" +#include "Object.h" +#include "ObjectDefines.h" +#include "ObjectGuid.h" +#include "QuestDef.h" +#include "TravelMgr.h" +#include "PlayerbotAI.h" + +struct POIInfo { + G3D::Vector2 pos; + int32 objectiveIdx; +}; + +/// A base (composition) class for all new rpg actions +/// All functions that may be shared by multiple actions should be declared here +/// And we should make all actions composable instead of inheritable +class NewRpgBaseAction : public MovementAction +{ +public: + NewRpgBaseAction(PlayerbotAI* botAI, std::string name) : MovementAction(botAI, name) {} + +protected: + // MOVEMENT RELATED + bool MoveFarTo(WorldPosition dest); + bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE); + bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); + bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); + + // QUEST RELATED + bool SearchQuestGiverAndAcceptOrReward(); + ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f); + bool HasQuestToAcceptOrReward(WorldObject* object); + bool InteractWithNpcOrGameObjectForQuest(ObjectGuid guid); + bool AcceptQuest(Quest const* quest, ObjectGuid guid); + bool TurnInQuest(Quest const* quest, ObjectGuid guid); + uint32 BestReward(Quest const* quest); + bool IsQuestWorthDoing(Quest const* quest); + bool IsQuestCapableDoing(Quest const* quest); + bool OrganizeQuestLog(); + +protected: + bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector &poiInfo, bool toComplete = false); + static WorldPosition SelectRandomGrindPos(Player* bot); + static WorldPosition SelectRandomInnKeeperPos(Player* bot); + +protected: + // WorldPosition dest; + const float pathFinderDis = 70.0f; // path finder + const uint32 stuckTime = 5 * 60 * 1000; +}; + +#endif \ No newline at end of file diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgInfo.cpp b/modules/mod-playerbots/src/strategy/rpg/NewRpgInfo.cpp new file mode 100644 index 0000000..a9c8264 --- /dev/null +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgInfo.cpp @@ -0,0 +1,119 @@ +#include "NewRpgInfo.h" +#include "Timer.h" + +void NewRpgInfo::ChangeToGoGrind(WorldPosition pos) +{ + Reset(); + status = RPG_GO_GRIND; + go_grind = GoGrind(); + go_grind.pos = pos; +} + +void NewRpgInfo::ChangeToGoInnkeeper(WorldPosition pos) +{ + Reset(); + status = RPG_GO_INNKEEPER; + go_innkeeper = GoInnkeeper(); + go_innkeeper.pos = pos; +} + +void NewRpgInfo::ChangeToNearNpc() +{ + Reset(); + status = RPG_NEAR_NPC; + near_npc = NearNpc(); +} + +void NewRpgInfo::ChangeToNearRandom() +{ + Reset(); + status = RPG_NEAR_RANDOM; + near_random = NearRandom(); +} + +void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) +{ + Reset(); + status = RPG_DO_QUEST; + do_quest = DoQuest(); + do_quest.questId = questId; + do_quest.quest = quest; +} + +void NewRpgInfo::ChangeToRest() +{ + Reset(); + status = RPG_REST; + rest = Rest(); +} + +void NewRpgInfo::ChangeToIdle() +{ + Reset(); + status = RPG_IDLE; +} + +bool NewRpgInfo::CanChangeTo(NewRpgStatus status) +{ + return true; +} + +void NewRpgInfo::Reset() +{ + *this = NewRpgInfo(); + startT = getMSTime(); +} + +void NewRpgInfo::SetMoveFarTo(WorldPosition pos) +{ + nearestMoveFarDis = FLT_MAX; + stuckTs = 0; + stuckAttempts = 0; + moveFarPos = pos; +} + +std::string NewRpgInfo::ToString() +{ + std::stringstream out; + out << "Status: "; + switch (status) + { + case RPG_GO_GRIND: + out << "GO_GRIND"; + out << "\nGrindPos: " << go_grind.pos.GetMapId() << " " << go_grind.pos.GetPositionX() << " " << go_grind.pos.GetPositionY() << " " << go_grind.pos.GetPositionZ(); + out << "\nlastGoGrind: " << startT; + break; + case RPG_GO_INNKEEPER: + out << "GO_INNKEEPER"; + out << "\nInnKeeperPos: " << go_innkeeper.pos.GetMapId() << " " << go_innkeeper.pos.GetPositionX() << " " << go_innkeeper.pos.GetPositionY() << " " << go_innkeeper.pos.GetPositionZ(); + out << "\nlastGoInnKeeper: " << startT; + break; + case RPG_NEAR_NPC: + out << "NEAR_NPC"; + out << "\nnpcOrGoEntry: " << near_npc.npcOrGo.GetCounter(); + out << "\nlastNearNpc: " << startT; + out << "\nlastReachNpcOrGo: " << near_npc.lastReach; + break; + case RPG_NEAR_RANDOM: + out << "NEAR_RANDOM"; + out << "\nlastNearRandom: " << startT; + break; + case RPG_IDLE: + out << "IDLE"; + break; + case RPG_REST: + out << "REST"; + out << "\nlastRest: " << startT; + break; + case RPG_DO_QUEST: + out << "DO_QUEST"; + out << "\nquestId: " << do_quest.questId; + out << "\nobjectiveIdx: " << do_quest.objectiveIdx; + out << "\npoiPos: " << do_quest.pos.GetMapId() << " " << do_quest.pos.GetPositionX() << " " << do_quest.pos.GetPositionY() << " " << do_quest.pos.GetPositionZ(); + out << "\nlastReachPOI: " << do_quest.lastReachPOI; + break; + default: + out << "UNKNOWN"; + } + return out.str(); +} \ No newline at end of file diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgInfo.h b/modules/mod-playerbots/src/strategy/rpg/NewRpgInfo.h new file mode 100644 index 0000000..d4b39b2 --- /dev/null +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgInfo.h @@ -0,0 +1,136 @@ +#ifndef _PLAYERBOT_NEWRPGINFO_H +#define _PLAYERBOT_NEWRPGINFO_H + +#include "Define.h" +#include "ObjectGuid.h" +#include "ObjectMgr.h" +#include "QuestDef.h" +#include "Strategy.h" +#include "Timer.h" +#include "TravelMgr.h" + +enum NewRpgStatus: int +{ + RPG_STATUS_START = 0, + // Going to far away place + RPG_GO_GRIND = 0, + RPG_GO_INNKEEPER = 1, + // Exploring nearby + RPG_NEAR_RANDOM = 2, + RPG_NEAR_NPC = 3, + // Do Quest (based on quest status) + RPG_DO_QUEST = 4, + // Taking a break + RPG_REST = 5, + // Initial status + RPG_IDLE = 6, + RPG_STATUS_END = 7 +}; + +using NewRpgStatusTransitionProb = std::vector>; + +struct NewRpgInfo +{ + NewRpgInfo() {} + + // RPG_GO_GRIND + struct GoGrind { + GoGrind() = default; + WorldPosition pos{}; + }; + // RPG_GO_INNKEEPER + struct GoInnkeeper { + GoInnkeeper() = default; + WorldPosition pos{}; + }; + // RPG_NEAR_NPC + struct NearNpc { + NearNpc() = default; + ObjectGuid npcOrGo{}; + uint32 lastReach{0}; + }; + // RPG_NEAR_RANDOM + struct NearRandom { + NearRandom() = default; + }; + // NewRpgStatus::QUESTING + struct DoQuest { + const Quest* quest{nullptr}; + uint32 questId{0}; + int32 objectiveIdx{0}; + WorldPosition pos{}; + uint32 lastReachPOI{0}; + }; + // RPG_REST + struct Rest { + Rest() = default; + }; + struct Idle { + }; + NewRpgStatus status{RPG_IDLE}; + + uint32 startT{0}; // start timestamp of the current status + + // MOVE_FAR + float nearestMoveFarDis{FLT_MAX}; + uint32 stuckTs{0}; + uint32 stuckAttempts{0}; + WorldPosition moveFarPos; + // END MOVE_FAR + + union { + GoGrind go_grind; + GoInnkeeper go_innkeeper; + NearNpc near_npc; + NearRandom near_random; + DoQuest do_quest; + Rest rest; + DoQuest quest; + }; + + bool HasStatusPersisted(uint32 maxDuration) { return GetMSTimeDiffToNow(startT) > maxDuration; } + void ChangeToGoGrind(WorldPosition pos); + void ChangeToGoInnkeeper(WorldPosition pos); + void ChangeToNearNpc(); + void ChangeToNearRandom(); + void ChangeToDoQuest(uint32 questId, const Quest* quest); + void ChangeToRest(); + void ChangeToIdle(); + bool CanChangeTo(NewRpgStatus status); + void Reset(); + void SetMoveFarTo(WorldPosition pos); + std::string ToString(); +}; + +struct NewRpgStatistic +{ + uint32 questAccepted{0}; + uint32 questCompleted{0}; + uint32 questAbandoned{0}; + uint32 questRewarded{0}; + uint32 questDropped{0}; + NewRpgStatistic operator+(const NewRpgStatistic& other) const + { + NewRpgStatistic result; + result.questAccepted = this->questAccepted + other.questAccepted; + result.questCompleted = this->questCompleted + other.questCompleted; + result.questAbandoned = this->questAbandoned + other.questAbandoned; + result.questRewarded = this->questRewarded + other.questRewarded; + result.questDropped = this->questDropped + other.questDropped; + return result; + } + NewRpgStatistic& operator+=(const NewRpgStatistic& other) + { + this->questAccepted += other.questAccepted; + this->questCompleted += other.questCompleted; + this->questAbandoned += other.questAbandoned; + this->questRewarded += other.questRewarded; + this->questDropped += other.questDropped; + return *this; + } +}; + +// not sure is it necessary but keep it for now +#define RPG_INFO(x, y) botAI->rpgInfo.x.y + +#endif \ No newline at end of file diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.cpp b/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.cpp index 82bd03b..9a078d4 100644 --- a/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.cpp +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.cpp @@ -11,22 +11,28 @@ NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} NextAction** NewRpgStrategy::getDefaultActions() { - return NextAction::array(0, new NextAction("new rpg status update", 5.0f), nullptr); + // the releavance should be greater than grind + return NextAction::array(0, + new NextAction("new rpg status update", 11.0f), + nullptr); } void NewRpgStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( - new TriggerNode("go grind status", NextAction::array(0, new NextAction("new rpg go grind", 1.0f), nullptr))); + new TriggerNode("go grind status", NextAction::array(0, new NextAction("new rpg go grind", 3.0f), nullptr))); triggers.push_back( - new TriggerNode("go innkeeper status", NextAction::array(0, new NextAction("new rpg go innkeeper", 1.0f), nullptr))); + new TriggerNode("go innkeeper status", NextAction::array(0, new NextAction("new rpg go innkeeper", 3.0f), nullptr))); triggers.push_back( - new TriggerNode("near random status", NextAction::array(0, new NextAction("new rpg move random", 1.0f), nullptr))); + new TriggerNode("near random status", NextAction::array(0, new NextAction("new rpg move random", 3.0f), nullptr))); triggers.push_back( - new TriggerNode("near npc status", NextAction::array(0, new NextAction("new rpg move npc", 1.0f), nullptr))); + new TriggerNode("near npc status", NextAction::array(0, new NextAction("new rpg move npc", 3.0f), nullptr))); + + triggers.push_back( + new TriggerNode("do quest status", NextAction::array(0, new NextAction("new rpg do quest", 3.0f), nullptr))); } void NewRpgStrategy::InitMultipliers(std::vector& multipliers) diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.h b/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.h index 1c5a642..59fa61f 100644 --- a/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.h +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgStrategy.h @@ -6,91 +6,12 @@ #ifndef _PLAYERBOT_NEWRPGSTRATEGY_H #define _PLAYERBOT_NEWRPGSTRATEGY_H -#include #include "Strategy.h" #include "TravelMgr.h" +#include "NewRpgInfo.h" class PlayerbotAI; -enum class NewRpgStatus -{ - // Going to far away place - GO_GRIND, - GO_INNKEEPER, - // Exploring nearby - NEAR_RANDOM, - NEAR_NPC, - // Taking a break - REST, - // Initial status - IDLE -}; - -struct NewRpgInfo -{ - NewRpgStatus status{NewRpgStatus::IDLE}; - // NewRpgStatus::GO_GRIND - WorldPosition grindPos{}; - uint32 lastGoGrind{0}; - // NewRpgStatus::GO_INNKEEPER - WorldPosition innKeeperPos{}; - uint32 lastGoInnKeeper{0}; - // NewRpgStatus::NEAR_NPC - GuidPosition npcPos{}; - uint32 lastNearNpc{0}; - uint32 lastReachNpc{0}; - // NewRpgStatus::NEAR_RANDOM - uint32 lastNearRandom{0}; - // NewRpgStatus::REST - uint32 lastRest{0}; - // MOVE_FAR - float nearestMoveFarDis{FLT_MAX}; - uint32 stuckTs{0}; - uint32 stuckAttempts{0}; - std::string ToString() - { - std::stringstream out; - out << "Status: "; - switch (status) - { - case NewRpgStatus::GO_GRIND: - out << "GO_GRIND"; - out << "\nGrindPos: " << grindPos.GetMapId() << " " << grindPos.GetPositionX() << " " << grindPos.GetPositionY() << " " << grindPos.GetPositionZ(); - out << "\nlastGoGrind: " << lastGoGrind; - break; - case NewRpgStatus::GO_INNKEEPER: - out << "GO_INNKEEPER"; - out << "\nInnKeeperPos: " << innKeeperPos.GetMapId() << " " << innKeeperPos.GetPositionX() << " " << innKeeperPos.GetPositionY() << " " << innKeeperPos.GetPositionZ(); - out << "\nlastGoInnKeeper: " << lastGoInnKeeper; - break; - case NewRpgStatus::NEAR_NPC: - out << "NEAR_NPC"; - out << "\nNpcPos: " << npcPos.GetMapId() << " " << npcPos.GetPositionX() << " " << npcPos.GetPositionY() << " " << npcPos.GetPositionZ(); - out << "\nlastNearNpc: " << lastNearNpc; - out << "\nlastReachNpc: " << lastReachNpc; - break; - case NewRpgStatus::NEAR_RANDOM: - out << "NEAR_RANDOM"; - out << "\nlastNearRandom: " << lastNearRandom; - break; - case NewRpgStatus::IDLE: - out << "IDLE"; - break; - case NewRpgStatus::REST: - out << "REST"; - out << "\nlastRest: " << lastRest; - break; - default: - out << "UNKNOWN"; - } - return out.str(); - } - void Reset() - { - *this = NewRpgInfo(); - } -}; - class NewRpgStrategy : public Strategy { public: diff --git a/modules/mod-playerbots/src/strategy/rpg/NewRpgTriggers.h b/modules/mod-playerbots/src/strategy/rpg/NewRpgTriggers.h index 9827d57..fe2efe3 100644 --- a/modules/mod-playerbots/src/strategy/rpg/NewRpgTriggers.h +++ b/modules/mod-playerbots/src/strategy/rpg/NewRpgTriggers.h @@ -7,7 +7,7 @@ class NewRpgStatusTrigger : public Trigger { public: - NewRpgStatusTrigger(PlayerbotAI* botAI, NewRpgStatus status = NewRpgStatus::IDLE) + NewRpgStatusTrigger(PlayerbotAI* botAI, NewRpgStatus status = RPG_IDLE) : Trigger(botAI, "new rpg status"), status(status) { } diff --git a/modules/mod-playerbots/src/strategy/triggers/ChatTriggerContext.h b/modules/mod-playerbots/src/strategy/triggers/ChatTriggerContext.h index 5f7ab84..ebc1121 100644 --- a/modules/mod-playerbots/src/strategy/triggers/ChatTriggerContext.h +++ b/modules/mod-playerbots/src/strategy/triggers/ChatTriggerContext.h @@ -17,6 +17,8 @@ public: ChatTriggerContext() { creators["open items"] = &ChatTriggerContext::open_items; + creators["unlock items"] = &ChatTriggerContext::unlock_items; + creators["unlock traded item"] = &ChatTriggerContext::unlock_traded_item; creators["quests"] = &ChatTriggerContext::quests; creators["stats"] = &ChatTriggerContext::stats; creators["leave"] = &ChatTriggerContext::leave; @@ -25,6 +27,7 @@ public: creators["log"] = &ChatTriggerContext::log; creators["los"] = &ChatTriggerContext::los; creators["rpg status"] = &ChatTriggerContext::rpg_status; + creators["rpg do quest"] = &ChatTriggerContext::rpg_do_quest; creators["aura"] = &ChatTriggerContext::aura; creators["drop"] = &ChatTriggerContext::drop; creators["share"] = &ChatTriggerContext::share; @@ -132,6 +135,8 @@ public: private: static Trigger* open_items(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "open items"); } + static Trigger* unlock_items(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "unlock items"); } + static Trigger* unlock_traded_item(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "unlock traded item"); } static Trigger* ra(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "ra"); } static Trigger* range(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "range"); } static Trigger* flag(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "flag"); } @@ -214,6 +219,7 @@ private: static Trigger* log(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "log"); } static Trigger* los(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "los"); } static Trigger* rpg_status(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "rpg status"); } + static Trigger* rpg_do_quest(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "rpg do quest"); } static Trigger* aura(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "aura"); } static Trigger* loot_all(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "add all loot"); } static Trigger* release(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "release"); } diff --git a/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.cpp b/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.cpp index bbda4c9..4f0038f 100644 --- a/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.cpp +++ b/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.cpp @@ -15,6 +15,7 @@ #include "ObjectGuid.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" +#include "PositionValue.h" #include "SharedDefines.h" #include "TemporarySummon.h" #include "ThreatMgr.h" @@ -507,11 +508,22 @@ bool IsBehindTargetTrigger::IsActive() bool IsNotBehindTargetTrigger::IsActive() { + if (botAI->HasStrategy("stay", botAI->GetState())) + { + return false; + } Unit* target = AI_VALUE(Unit*, "current target"); return target && !AI_VALUE2(bool, "behind", "current target"); } -bool IsNotFacingTargetTrigger::IsActive() { return !AI_VALUE2(bool, "facing", "current target"); } +bool IsNotFacingTargetTrigger::IsActive() +{ + if (botAI->HasStrategy("stay", botAI->GetState())) + { + return false; + } + return !AI_VALUE2(bool, "facing", "current target"); +} bool HasCcTargetTrigger::IsActive() { @@ -599,6 +611,18 @@ bool NewPlayerNearbyTrigger::IsActive() { return AI_VALUE(ObjectGuid, "new playe bool CollisionTrigger::IsActive() { return AI_VALUE2(bool, "collision", "self target"); } +bool ReturnToStayPositionTrigger::IsActive() +{ + PositionInfo stayPosition = AI_VALUE(PositionMap&, "position")["stay"]; + if (stayPosition.isSet()) + { + const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z); + return distance > sPlayerbotAIConfig->followDistance; + } + + return false; +} + bool GiveItemTrigger::IsActive() { return AI_VALUE2(Unit*, "party member without item", item) && AI_VALUE2(uint32, "item count", item); diff --git a/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.h b/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.h index dd4e465..cee6b18 100644 --- a/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.h +++ b/modules/mod-playerbots/src/strategy/triggers/GenericTriggers.h @@ -827,6 +827,14 @@ public: SitTrigger(PlayerbotAI* botAI) : StayTimeTrigger(botAI, sPlayerbotAIConfig->sitDelay, "sit") {} }; +class ReturnToStayPositionTrigger : public Trigger +{ +public: + ReturnToStayPositionTrigger(PlayerbotAI* ai) : Trigger(ai, "return to stay position", 2) {} + + virtual bool IsActive() override; +}; + class ReturnTrigger : public StayTimeTrigger { public: diff --git a/modules/mod-playerbots/src/strategy/triggers/LootTriggers.cpp b/modules/mod-playerbots/src/strategy/triggers/LootTriggers.cpp index 17c1304..eed1a3e 100644 --- a/modules/mod-playerbots/src/strategy/triggers/LootTriggers.cpp +++ b/modules/mod-playerbots/src/strategy/triggers/LootTriggers.cpp @@ -12,10 +12,10 @@ bool LootAvailableTrigger::IsActive() { return AI_VALUE(bool, "has available loot") && + // if loot target if empty, always pass distance check (sServerFacade->IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), INTERACTION_DISTANCE - 2.0f) || - AI_VALUE(GuidVector, "all targets").empty()) && - !AI_VALUE2(bool, "combat", "self target"); + AI_VALUE(GuidVector, "all targets").empty()); } bool FarFromCurrentLootTrigger::IsActive() diff --git a/modules/mod-playerbots/src/strategy/triggers/TriggerContext.h b/modules/mod-playerbots/src/strategy/triggers/TriggerContext.h index d47b75b..1b2c86f 100644 --- a/modules/mod-playerbots/src/strategy/triggers/TriggerContext.h +++ b/modules/mod-playerbots/src/strategy/triggers/TriggerContext.h @@ -30,6 +30,7 @@ public: { creators["return"] = &TriggerContext::_return; creators["sit"] = &TriggerContext::sit; + creators["return to stay position"] = &TriggerContext::return_to_stay_position; creators["collision"] = &TriggerContext::collision; creators["timer"] = &TriggerContext::Timer; @@ -220,6 +221,7 @@ public: creators["go innkeeper status"] = &TriggerContext::go_innkeeper_status; creators["near random status"] = &TriggerContext::near_random_status; creators["near npc status"] = &TriggerContext::near_npc_status; + creators["do quest status"] = &TriggerContext::do_quest_status; } private: @@ -227,6 +229,7 @@ private: static Trigger* give_water(PlayerbotAI* botAI) { return new GiveWaterTrigger(botAI); } static Trigger* no_rti(PlayerbotAI* botAI) { return new NoRtiTrigger(botAI); } static Trigger* _return(PlayerbotAI* botAI) { return new ReturnTrigger(botAI); } + static Trigger* return_to_stay_position(PlayerbotAI* ai) { return new ReturnToStayPositionTrigger(ai); } static Trigger* sit(PlayerbotAI* botAI) { return new SitTrigger(botAI); } static Trigger* far_from_rpg_target(PlayerbotAI* botAI) { return new FarFromRpgTargetTrigger(botAI); } static Trigger* near_rpg_target(PlayerbotAI* botAI) { return new NearRpgTargetTrigger(botAI); } @@ -410,10 +413,11 @@ private: static Trigger* rpg_craft(PlayerbotAI* botAI) { return new RpgCraftTrigger(botAI); } static Trigger* rpg_trade_useful(PlayerbotAI* botAI) { return new RpgTradeUsefulTrigger(botAI); } static Trigger* rpg_duel(PlayerbotAI* botAI) { return new RpgDuelTrigger(botAI); } - static Trigger* go_grind_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, NewRpgStatus::GO_GRIND); } - static Trigger* go_innkeeper_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, NewRpgStatus::GO_INNKEEPER); } - static Trigger* near_random_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, NewRpgStatus::NEAR_RANDOM); } - static Trigger* near_npc_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, NewRpgStatus::NEAR_NPC); } + static Trigger* go_grind_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_GO_GRIND); } + static Trigger* go_innkeeper_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_GO_INNKEEPER); } + static Trigger* near_random_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_NEAR_RANDOM); } + static Trigger* near_npc_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_NEAR_NPC); } + static Trigger* do_quest_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_DO_QUEST); } }; #endif diff --git a/modules/mod-playerbots/src/strategy/triggers/WorldPacketTriggerContext.h b/modules/mod-playerbots/src/strategy/triggers/WorldPacketTriggerContext.h index 72ad3e5..5404a22 100644 --- a/modules/mod-playerbots/src/strategy/triggers/WorldPacketTriggerContext.h +++ b/modules/mod-playerbots/src/strategy/triggers/WorldPacketTriggerContext.h @@ -29,6 +29,7 @@ public: creators["check mount state"] = &WorldPacketTriggerContext::check_mount_state; creators["activate taxi"] = &WorldPacketTriggerContext::taxi; creators["trade status"] = &WorldPacketTriggerContext::trade_status; + creators["trade status extended"] = &WorldPacketTriggerContext::trade_status_extended; creators["loot response"] = &WorldPacketTriggerContext::loot_response; creators["out of react range"] = &WorldPacketTriggerContext::out_of_react_range; @@ -108,6 +109,7 @@ private: static Trigger* out_of_react_range(PlayerbotAI* botAI) { return new OutOfReactRangeTrigger(botAI); } static Trigger* loot_response(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "loot response"); } static Trigger* trade_status(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "trade status"); } + static Trigger* trade_status_extended(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "trade status extended"); } static Trigger* cannot_equip(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "cannot equip"); } static Trigger* check_mount_state(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "check mount state"); } static Trigger* area_trigger(PlayerbotAI* botAI) { return new WorldPacketTrigger(botAI, "area trigger"); } diff --git a/modules/mod-playerbots/src/strategy/values/AvailableLootValue.cpp b/modules/mod-playerbots/src/strategy/values/AvailableLootValue.cpp index 2adb410..a0506c4 100644 --- a/modules/mod-playerbots/src/strategy/values/AvailableLootValue.cpp +++ b/modules/mod-playerbots/src/strategy/values/AvailableLootValue.cpp @@ -26,5 +26,5 @@ bool CanLootValue::Calculate() { LootObject loot = AI_VALUE(LootObject, "loot target"); return !loot.IsEmpty() && loot.GetWorldObject(bot) && loot.IsLootPossible(bot) && - sServerFacade->IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), INTERACTION_DISTANCE); + sServerFacade->IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "loot target"), INTERACTION_DISTANCE - 2); } diff --git a/modules/mod-playerbots/src/strategy/values/GrindTargetValue.cpp b/modules/mod-playerbots/src/strategy/values/GrindTargetValue.cpp index b21f62a..da8fed8 100644 --- a/modules/mod-playerbots/src/strategy/values/GrindTargetValue.cpp +++ b/modules/mod-playerbots/src/strategy/values/GrindTargetValue.cpp @@ -5,6 +5,7 @@ #include "GrindTargetValue.h" +#include "NewRpgInfo.h" #include "Playerbots.h" #include "ReputationMgr.h" #include "SharedDefines.h" @@ -52,7 +53,7 @@ Unit* GrindTargetValue::FindTargetForGrinding(uint32 assistCount) float distance = 0; Unit* result = nullptr; - // std::unordered_map needForQuestMap; + std::unordered_map needForQuestMap; for (ObjectGuid const guid : targets) { @@ -99,19 +100,6 @@ Unit* GrindTargetValue::FindTargetForGrinding(uint32 assistCount) if (!bot->InBattleground() && (int)unit->GetLevel() - (int)bot->GetLevel() > 4 && !unit->GetGUID().IsPlayer()) continue; - // if (needForQuestMap.find(unit->GetEntry()) == needForQuestMap.end()) - // needForQuestMap[unit->GetEntry()] = needForQuest(unit); - - // if (!needForQuestMap[unit->GetEntry()]) - // { - // Creature* creature = dynamic_cast(unit); - // if ((urand(0, 100) < 60 || (context->GetValue("travel target")->Get()->isWorking() && - // context->GetValue("travel target")->Get()->getDestination()->getName() != "GrindTravelDestination"))) - // { - // continue; - // } - // } - if (Creature* creature = unit->ToCreature()) if (CreatureTemplate const* CreatureTemplate = creature->GetCreatureTemplate()) if (CreatureTemplate->rank > CREATURE_ELITE_NORMAL && !AI_VALUE(bool, "can fight elite")) @@ -122,6 +110,26 @@ Unit* GrindTargetValue::FindTargetForGrinding(uint32 assistCount) continue; } + bool inactiveGrindStatus = botAI->rpgInfo.status == RPG_GO_GRIND || + botAI->rpgInfo.status == RPG_NEAR_NPC || + botAI->rpgInfo.status == RPG_REST || + botAI->rpgInfo.status == RPG_GO_INNKEEPER || + botAI->rpgInfo.status == RPG_DO_QUEST; + + bool notHostile = !bot->IsHostileTo(unit); /*|| (unit->ToCreature() && unit->ToCreature()->IsCivilian());*/ + float aggroRange = 30.0f; + if (unit->ToCreature()) + aggroRange = std::min(30.0f, unit->ToCreature()->GetAggroRange(bot) + 10.0f); + bool outOfAggro = unit->ToCreature() && bot->GetDistance(unit) > aggroRange; + if (inactiveGrindStatus && (outOfAggro || notHostile)) + { + if (needForQuestMap.find(unit->GetEntry()) == needForQuestMap.end()) + needForQuestMap[unit->GetEntry()] = needForQuest(unit); + + if (!needForQuestMap[unit->GetEntry()]) + continue; + } + if (group) { Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); @@ -155,8 +163,6 @@ Unit* GrindTargetValue::FindTargetForGrinding(uint32 assistCount) bool GrindTargetValue::needForQuest(Unit* target) { - bool justCheck = (bot->GetGUID() == target->GetGUID()); - QuestStatusMap& questMap = bot->getQuestStatusMap(); for (auto& quest : questMap) { @@ -170,16 +176,9 @@ bool GrindTargetValue::needForQuest(Unit* target) QuestStatus status = bot->GetQuestStatus(questId); - if ((status == QUEST_STATUS_COMPLETE && !bot->GetQuestRewardStatus(questId))) + if (status == QUEST_STATUS_INCOMPLETE) { - if (!justCheck && !target->hasInvolvedQuest(questId)) - continue; - - return true; - } - else if (status == QUEST_STATUS_INCOMPLETE) - { - QuestStatusData* questStatus = sTravelMgr->getQuestStatus(bot, questId); + const QuestStatusData* questStatus = &bot->getQuestStatusMap()[questId]; if (questTemplate->GetQuestLevel() > bot->GetLevel() + 5) continue; @@ -193,33 +192,18 @@ bool GrindTargetValue::needForQuest(Unit* target) int required = questTemplate->RequiredNpcOrGoCount[j]; int available = questStatus->CreatureOrGOCount[j]; - if (required && available < required && (target->GetEntry() == entry || justCheck)) + if (required && available < required && target->GetEntry() == entry) return true; } - - if (justCheck) - { - int32 itemId = questTemplate->RequiredItemId[j]; - - if (itemId && itemId > 0) - { - uint32 required = questTemplate->RequiredItemCount[j]; - uint32 available = questStatus->ItemCount[j]; - - if (required && available < required) - return true; - } - } } - if (!justCheck) + if (CreatureTemplate const* data = sObjectMgr->GetCreatureTemplate(target->GetEntry())) { - if (CreatureTemplate const* data = sObjectMgr->GetCreatureTemplate(target->GetEntry())) + if (uint32 lootId = data->lootid) { - if (uint32 lootId = data->lootid) + if (LootTemplates_Creature.HaveQuestLootForPlayer(lootId, bot)) { - if (LootTemplates_Creature.HaveQuestLootForPlayer(lootId, bot)) - return true; + return true; } } } diff --git a/modules/mod-playerbots/src/strategy/values/ItemForSpellValue.cpp b/modules/mod-playerbots/src/strategy/values/ItemForSpellValue.cpp index 95751b4..366f38a 100644 --- a/modules/mod-playerbots/src/strategy/values/ItemForSpellValue.cpp +++ b/modules/mod-playerbots/src/strategy/values/ItemForSpellValue.cpp @@ -71,6 +71,9 @@ Item* ItemForSpellValue::Calculate() if (!strcmpi(spellInfo->SpellName[0], "disenchant")) return nullptr; + if (!strcmpi(spellInfo->SpellName[0], "pick lock")) + return nullptr; + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++) { itemForSpell = GetItemFitsToSpellRequirements(slot, spellInfo); diff --git a/modules/mod-playerbots/src/strategy/values/LastMovementValue.h b/modules/mod-playerbots/src/strategy/values/LastMovementValue.h index a22debb..f3ecf91 100644 --- a/modules/mod-playerbots/src/strategy/values/LastMovementValue.h +++ b/modules/mod-playerbots/src/strategy/values/LastMovementValue.h @@ -17,6 +17,7 @@ class Unit; enum class MovementPriority { MOVEMENT_IDLE, + MOVEMENT_WANDER, MOVEMENT_NORMAL, MOVEMENT_COMBAT, MOVEMENT_FORCED diff --git a/modules/mod-playerbots/src/strategy/values/NearestGameObjects.cpp b/modules/mod-playerbots/src/strategy/values/NearestGameObjects.cpp index a600bc6..85a3428 100644 --- a/modules/mod-playerbots/src/strategy/values/NearestGameObjects.cpp +++ b/modules/mod-playerbots/src/strategy/values/NearestGameObjects.cpp @@ -12,24 +12,6 @@ #include "SharedDefines.h" #include "SpellMgr.h" -class AnyGameObjectInObjectRangeCheck -{ -public: - AnyGameObjectInObjectRangeCheck(WorldObject const* obj, float range) : i_obj(obj), i_range(range) {} - WorldObject const& GetFocusObject() const { return *i_obj; } - bool operator()(GameObject* u) - { - if (u && i_obj->IsWithinDistInMap(u, i_range) && u->isSpawned() && u->GetGOInfo()) - return true; - - return false; - } - -private: - WorldObject const* i_obj; - float i_range; -}; - GuidVector NearestGameObjects::Calculate() { std::list targets; diff --git a/modules/mod-playerbots/src/strategy/values/NearestGameObjects.h b/modules/mod-playerbots/src/strategy/values/NearestGameObjects.h index 04d1648..e29467d 100644 --- a/modules/mod-playerbots/src/strategy/values/NearestGameObjects.h +++ b/modules/mod-playerbots/src/strategy/values/NearestGameObjects.h @@ -8,15 +8,34 @@ #include "PlayerbotAIConfig.h" #include "Value.h" +#include "GameObject.h" class PlayerbotAI; +class AnyGameObjectInObjectRangeCheck +{ +public: + AnyGameObjectInObjectRangeCheck(WorldObject const* obj, float range) : i_obj(obj), i_range(range) {} + WorldObject const& GetFocusObject() const { return *i_obj; } + bool operator()(GameObject* u) + { + if (u && i_obj->IsWithinDistInMap(u, i_range) && u->isSpawned() && u->GetGOInfo()) + return true; + + return false; + } + +private: + WorldObject const* i_obj; + float i_range; +}; + class NearestGameObjects : public ObjectGuidListCalculatedValue { public: NearestGameObjects(PlayerbotAI* botAI, float range = sPlayerbotAIConfig->sightDistance, bool ignoreLos = false, std::string const name = "nearest game objects") - : ObjectGuidListCalculatedValue(botAI, name, 2 * 1000), range(range), ignoreLos(ignoreLos) + : ObjectGuidListCalculatedValue(botAI, name, 1 * 1000), range(range), ignoreLos(ignoreLos) { } diff --git a/modules/mod-playerbots/src/strategy/values/PositionValue.cpp b/modules/mod-playerbots/src/strategy/values/PositionValue.cpp index a914ef3..e299c98 100644 --- a/modules/mod-playerbots/src/strategy/values/PositionValue.cpp +++ b/modules/mod-playerbots/src/strategy/values/PositionValue.cpp @@ -63,3 +63,25 @@ bool PositionValue::Load(std::string const text) } WorldPosition CurrentPositionValue::Calculate() { return WorldPosition(bot); } + +PositionInfo SinglePositionValue::Calculate() +{ + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + return posMap[getQualifier()]; +} + +void SinglePositionValue::Set(PositionInfo value) +{ + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo pos = posMap[getQualifier()]; + pos = value; + posMap[getQualifier()] = pos; +} + +void SinglePositionValue::Reset() +{ + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo pos = posMap[getQualifier()]; + pos.Reset(); + posMap[getQualifier()] = pos; +} diff --git a/modules/mod-playerbots/src/strategy/values/PositionValue.h b/modules/mod-playerbots/src/strategy/values/PositionValue.h index 42b0aaf..8919324 100644 --- a/modules/mod-playerbots/src/strategy/values/PositionValue.h +++ b/modules/mod-playerbots/src/strategy/values/PositionValue.h @@ -6,6 +6,7 @@ #ifndef _PLAYERBOT_POSITIONVALUE_H #define _PLAYERBOT_POSITIONVALUE_H +#include "NamedObjectContext.h" #include "TravelMgr.h" #include "Value.h" @@ -15,6 +16,10 @@ class PositionInfo { public: PositionInfo() : valueSet(false), x(0), y(0), z(0), mapId(0) {} + PositionInfo(float x, float y, float z, uint32 mapId, bool valueSet = true) + : valueSet(valueSet), x(x), y(y), z(z), mapId(mapId) + { + } PositionInfo(PositionInfo const& other) : valueSet(other.valueSet), x(other.x), y(other.y), z(other.z), mapId(other.mapId) { @@ -72,4 +77,13 @@ public: WorldPosition Calculate() override; }; +class SinglePositionValue : public CalculatedValue, public Qualified +{ +public: + SinglePositionValue(PlayerbotAI* ai, std::string name = "pos") : CalculatedValue(ai, name), Qualified() {}; + virtual PositionInfo Calculate() override; + virtual void Set(PositionInfo value) override; + virtual void Reset() override; +}; + #endif diff --git a/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.cpp b/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.cpp index 55ee94b..cee20d9 100644 --- a/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.cpp +++ b/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.cpp @@ -8,8 +8,11 @@ #include "CellImpl.h" #include "GridNotifiers.h" #include "GridNotifiersImpl.h" +#include "ObjectGuid.h" #include "Playerbots.h" #include "ServerFacade.h" +#include "SharedDefines.h" +#include "NearestGameObjects.h" std::vector PossibleRpgTargetsValue::allowedNpcFlags; @@ -78,3 +81,125 @@ bool PossibleRpgTargetsValue::AcceptUnit(Unit* unit) return false; } + + +std::vector PossibleNewRpgTargetsValue::allowedNpcFlags; + +PossibleNewRpgTargetsValue::PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float range) + : NearestUnitsValue(botAI, "possible new rpg targets", range, true) +{ + if (allowedNpcFlags.empty()) + { + allowedNpcFlags.push_back(UNIT_NPC_FLAG_INNKEEPER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_GOSSIP); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_QUESTGIVER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_FLIGHTMASTER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_BANKER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_GUILD_BANKER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_TRAINER_CLASS); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_TRAINER_PROFESSION); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_VENDOR_AMMO); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_VENDOR_FOOD); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_VENDOR_POISON); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_VENDOR_REAGENT); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_AUCTIONEER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_STABLEMASTER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_PETITIONER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_TABARDDESIGNER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_BATTLEMASTER); + + allowedNpcFlags.push_back(UNIT_NPC_FLAG_TRAINER); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_VENDOR); + allowedNpcFlags.push_back(UNIT_NPC_FLAG_REPAIR); + } +} + +GuidVector PossibleNewRpgTargetsValue::Calculate() +{ + std::list targets; + FindUnits(targets); + + GuidVector results; + std::vector> guidDistancePairs; + for (Unit* unit : targets) + { + if (AcceptUnit(unit) && (ignoreLos || bot->IsWithinLOSInMap(unit))) + guidDistancePairs.push_back({unit->GetGUID(), bot->GetExactDist(unit)}); + } + // Override to sort by distance + std::sort(guidDistancePairs.begin(), guidDistancePairs.end(), [](const auto& a, const auto& b) { + return a.second < b.second; + }); + + for (const auto& pair : guidDistancePairs) { + results.push_back(pair.first); + } + return results; +} + +void PossibleNewRpgTargetsValue::FindUnits(std::list& targets) +{ + Acore::AnyUnitInObjectRangeCheck u_check(bot, range); + Acore::UnitListSearcher searcher(bot, targets, u_check); + Cell::VisitAllObjects(bot, searcher, range); +} + +bool PossibleNewRpgTargetsValue::AcceptUnit(Unit* unit) +{ + if (unit->IsHostileTo(bot) || unit->GetTypeId() == TYPEID_PLAYER) + return false; + + if (unit->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_SPIRITHEALER)) + return false; + + for (uint32 npcFlag : allowedNpcFlags) + { + if (unit->HasFlag(UNIT_NPC_FLAGS, npcFlag)) + return true; + } + + return false; +} + +std::vector PossibleNewRpgGameObjectsValue::allowedGOFlags; + +GuidVector PossibleNewRpgGameObjectsValue::Calculate() +{ + std::list targets; + AnyGameObjectInObjectRangeCheck u_check(bot, range); + Acore::GameObjectListSearcher searcher(bot, targets, u_check); + Cell::VisitAllObjects(bot, searcher, range); + + + std::vector> guidDistancePairs; + for (GameObject* go : targets) + { + bool flagCheck = false; + for (uint32 goFlag : allowedGOFlags) + { + if (go->GetGoType() == goFlag) + { + flagCheck = true; + break; + } + } + if (!flagCheck) + continue; + + if (!ignoreLos && !bot->IsWithinLOSInMap(go)) + continue; + + guidDistancePairs.push_back({go->GetGUID(), bot->GetExactDist(go)}); + } + GuidVector results; + + // Sort by distance + std::sort(guidDistancePairs.begin(), guidDistancePairs.end(), [](const auto& a, const auto& b) { + return a.second < b.second; + }); + + for (const auto& pair : guidDistancePairs) { + results.push_back(pair.first); + } + return results; +} diff --git a/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.h b/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.h index 287f5db..2312480 100644 --- a/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.h +++ b/modules/mod-playerbots/src/strategy/values/PossibleRpgTargetsValue.h @@ -6,8 +6,10 @@ #ifndef _PLAYERBOT_POSSIBLERPGTARGETSVALUE_H #define _PLAYERBOT_POSSIBLERPGTARGETSVALUE_H +#include "NearestGameObjects.h" #include "NearestUnitsValue.h" #include "PlayerbotAIConfig.h" +#include "SharedDefines.h" class PlayerbotAI; @@ -23,4 +25,36 @@ protected: bool AcceptUnit(Unit* unit) override; }; +class PossibleNewRpgTargetsValue : public NearestUnitsValue +{ +public: + PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float range = 150.0f); + + static std::vector allowedNpcFlags; + GuidVector Calculate() override; +protected: + void FindUnits(std::list& targets) override; + bool AcceptUnit(Unit* unit) override; +}; + +class PossibleNewRpgGameObjectsValue : public ObjectGuidListCalculatedValue +{ +public: + PossibleNewRpgGameObjectsValue(PlayerbotAI* botAI, float range = 150.0f, bool ignoreLos = true) + : ObjectGuidListCalculatedValue(botAI, "possible new rpg game objects"), range(range), ignoreLos(ignoreLos) + { + if (allowedGOFlags.empty()) + { + allowedGOFlags.push_back(GAMEOBJECT_TYPE_QUESTGIVER); + } + } + + static std::vector allowedGOFlags; + GuidVector Calculate() override; + +private: + float range; + bool ignoreLos; +}; + #endif diff --git a/modules/mod-playerbots/src/strategy/values/ValueContext.h b/modules/mod-playerbots/src/strategy/values/ValueContext.h index f57c808..c9dcdb7 100644 --- a/modules/mod-playerbots/src/strategy/values/ValueContext.h +++ b/modules/mod-playerbots/src/strategy/values/ValueContext.h @@ -118,6 +118,8 @@ public: creators["prioritized targets"] = &ValueContext::prioritized_targets; creators["all targets"] = &ValueContext::all_targets; creators["possible rpg targets"] = &ValueContext::possible_rpg_targets; + creators["possible new rpg targets"] = &ValueContext::possible_new_rpg_targets; + creators["possible new rpg game objects"] = &ValueContext::possible_new_rpg_game_objects; creators["nearest adds"] = &ValueContext::nearest_adds; creators["nearest corpses"] = &ValueContext::nearest_corpses; creators["log level"] = &ValueContext::log_level; @@ -192,6 +194,7 @@ public: creators["rti cc"] = &ValueContext::rti_cc; creators["rti"] = &ValueContext::rti; creators["position"] = &ValueContext::position; + creators["pos"] = &ValueContext::pos; creators["current position"] = &ValueContext::current_position; creators["threat"] = &ValueContext::threat; @@ -340,6 +343,7 @@ private: static UntypedValue* attackers(PlayerbotAI* botAI) { return new AttackersValue(botAI); } static UntypedValue* position(PlayerbotAI* botAI) { return new PositionValue(botAI); } + static UntypedValue* pos(PlayerbotAI* ai) { return new SinglePositionValue(ai); } static UntypedValue* current_position(PlayerbotAI* botAI) { return new CurrentPositionValue(botAI); } static UntypedValue* rti(PlayerbotAI* botAI) { return new RtiValue(botAI); } static UntypedValue* rti_cc(PlayerbotAI* botAI) { return new RtiCcValue(botAI); } @@ -406,6 +410,8 @@ private: static UntypedValue* nearest_enemy_players(PlayerbotAI* botAI) { return new NearestEnemyPlayersValue(botAI); } static UntypedValue* nearest_corpses(PlayerbotAI* botAI) { return new NearestCorpsesValue(botAI); } static UntypedValue* possible_rpg_targets(PlayerbotAI* botAI) { return new PossibleRpgTargetsValue(botAI); } + static UntypedValue* possible_new_rpg_targets(PlayerbotAI* botAI) { return new PossibleNewRpgTargetsValue(botAI); } + static UntypedValue* possible_new_rpg_game_objects(PlayerbotAI* botAI) { return new PossibleNewRpgGameObjectsValue(botAI); } static UntypedValue* possible_targets(PlayerbotAI* botAI) { return new PossibleTargetsValue(botAI); } static UntypedValue* possible_triggers(PlayerbotAI* botAI) { return new PossibleTriggersValue(botAI); } static UntypedValue* possible_targets_no_los(PlayerbotAI* botAI)