Skip to content

Commit

Permalink
NPCBots: Implement bot item sets similar to player equiment manager f…
Browse files Browse the repository at this point in the history
…eature item sets. New config: `NpcBot.GearBank.EquipmentSets` to set maximum item sets for bot owners to save

(cherry picked from commit 5bc8bd1cd84f37b9bc115950a8cafb44de441eaf)

# Conflicts:
#	data/sql/custom/db_characters/2024_11_13_00_characters_npcbot_gear_set.sql
#	data/sql/custom/db_characters/2024_11_13_01_characters_npcbot_gear_set_item.sql
#	data/sql/custom/db_world/2024_11_13_00_npc_text.sql
#	src/server/game/AI/NpcBots/bot_ai.cpp
#	src/server/game/Entities/Player/Player.cpp
#	src/server/worldserver/worldserver.conf.dist
  • Loading branch information
trickerer committed Nov 13, 2024
1 parent ebd72a4 commit 6985a72
Show file tree
Hide file tree
Showing 14 changed files with 637 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--
DROP TABLE IF EXISTS `characters_npcbot_gear_set`;
CREATE TABLE `characters_npcbot_gear_set` (
`owner` int unsigned NOT NULL DEFAULT '0',
`set_id` tinyint unsigned NOT NULL DEFAULT '0',
`set_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`owner`,`set_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Bot equipment sets system';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--
DROP TABLE IF EXISTS `characters_npcbot_gear_set_item`;
CREATE TABLE `characters_npcbot_gear_set_item` (
`owner` int unsigned NOT NULL DEFAULT '0',
`set_id` tinyint unsigned NOT NULL DEFAULT '0',
`slot` tinyint unsigned NOT NULL DEFAULT '0',
`item_id` mediumint unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`owner`,`set_id`,`slot`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Bot equipment sets system';
11 changes: 11 additions & 0 deletions data/sql/custom/db_world/2024_11_13_00_npc_text.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--
SET @LOCALIZED_STRINGS_START = 70689;
SET @LOCALIZED_STRINGS_END = 70693;

DELETE FROM `npc_text` WHERE ID BETWEEN @LOCALIZED_STRINGS_START and @LOCALIZED_STRINGS_END;
INSERT INTO `npc_text` (`ID`,`text0_0`,`VerifiedBuild`) VALUES
(@LOCALIZED_STRINGS_START+0,'Equipment sets','-1'),
(@LOCALIZED_STRINGS_START+1,'Create','-1'),
(@LOCALIZED_STRINGS_START+2,'Delete','-1'),
(@LOCALIZED_STRINGS_START+3,'Equip','-1'),
(@LOCALIZED_STRINGS_START+4,'missing','-1');
12 changes: 11 additions & 1 deletion src/server/apps/worldserver/worldserver.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4806,10 +4806,20 @@ NpcBot.GearBank.Enable = 1
# Description: Maximum number of items player can keep in bot gear storage.
# Note: This parameter only affects player's ability to *deposit* items.
# Default: 40 - (Maximum 40 items)
# 0 - (Unlimited capacity)
# 0 - (Unlimited capacity)

NpcBot.GearBank.Capacity = 40

#
# NpcBot.GearBank.EquipmentSets
# Description: Enable players to create bot equipment sets from items currently equipped
# on a bot, allowing to quickly swap bot equipment using gear bank as storage.
# Default: 0 - (Disabled)
# 10 - (Enabled, allow to keep up to 10 equipment sets per player)
# 30 - (Absolute maximum to fit on a single page, this is a hard limit)

NpcBot.GearBank.EquipmentSets = 0

#
# NpcBot.Classes.<Class>.Enable
# Description: Allow players to hire bots of certain classes.
Expand Down
379 changes: 297 additions & 82 deletions src/server/game/AI/NpcBots/bot_ai.cpp

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions src/server/game/AI/NpcBots/bot_ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ class bot_ai : public CreatureAI
Item* GetEquipsByGuid(ObjectGuid itemGuid) const;
uint32 GetEquipDisplayId(uint8 slot) const;
[[nodiscard]] BotEquipResult UnEquipAll(ObjectGuid receiver, bool store_to_bank);
bool HasRealEquipment() const;
uint8 GetRealEquippedItemsCount() const;
bool HasRealEquipment() const { return !!GetRealEquippedItemsCount(); }
float GetAverageItemLevel() const;
std::pair<float, float> GetBotGearScores() const;

Expand Down Expand Up @@ -638,13 +639,14 @@ class bot_ai : public CreatureAI
void _autoLootCreatureItems(Player* receiver, Creature* creature, uint32 lootQualityMask, uint32 lootThreshold) const;
void _autoLootCreature(Creature* creature);

bool _canUseOffHand() const;
bool _canUseOffHand(ItemTemplate const* with = nullptr) const;
bool _canUseRanged() const;
bool _canUseRelic() const;
bool _canCombineWeapons(ItemTemplate const* mh, ItemTemplate const* oh) const;
bool _canEquip(ItemTemplate const* newProto, uint8 slot, bool ignoreItemLevel, Item const* newItem = nullptr) const;
void _removeEquipment(uint8 slot);
[[nodiscard]] BotEquipResult _unequip(uint8 slot, ObjectGuid receiver, bool store_to_bank);
[[nodiscard]] BotEquipResult _equip(uint8 slot, Item* newItem, ObjectGuid receiver, bool store_to_bank);
[[nodiscard]] BotEquipResult _unequip(uint8 slot, ObjectGuid receiver, bool store_to_bank, bool on_equip_from_bank = false);
[[nodiscard]] BotEquipResult _equip(uint8 slot, Item* newItem, ObjectGuid receiver, bool store_to_bank, bool from_bank = false);
[[nodiscard]] BotEquipResult _resetEquipment(uint8 slot, ObjectGuid receiver, bool store_to_bank);

void _castBotItemUseSpell(Item const* item, SpellCastTargets const& targets/*, uint8 cast_count = 0, uint32 glyphIndex = 0*/);
Expand Down
7 changes: 6 additions & 1 deletion src/server/game/AI/NpcBots/botcommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Original patch from: LordPsyan https://bitbucket.org/lordpsyan/trinitycore-patch

constexpr std::size_t MAX_BOT_LOG_PARAMS = 5;
constexpr std::size_t MAX_BOT_LOG_PARAM_LENGTH = 50;
constexpr std::size_t MAX_BOT_ITEM_SET_NAME_LENGTH = 30;
constexpr uint8 BOT_GOSSIP_MAX_ITEMS = 32; // Client limitation 3.3.5 code confirmed

struct Position;

Expand Down Expand Up @@ -455,6 +457,7 @@ enum BotEquipSlot : uint8
};

constexpr uint8 BOT_TRANSMOG_INVENTORY_SIZE = 13; // BOT_SLOT_BODY + 1
constexpr uint8 MAX_BOT_EQUIPMENT_SETS = BOT_GOSSIP_MAX_ITEMS - 2;

enum class BotEquipResult : uint8
{
Expand All @@ -468,7 +471,9 @@ enum class BotEquipResult : uint8
BOT_EQUIP_RESULT_FAIL_SAME_ID = 6,
BOT_EQUIP_RESULT_FAIL_WANDERER = 7,
BOT_EQUIP_RESULT_FAIL_LINKED_UNEQUIP_FAILED = 8,
BOT_EQUIP_RESULT_FAIL_LINKED_RESET_FAILED = 9
BOT_EQUIP_RESULT_FAIL_LINKED_RESET_FAILED = 9,
BOT_EQUIP_RESULT_FAIL_CANT_EQUIP = 10,
BOT_EQUIP_RESULT_FAIL_ITEM_CONFLICT = 11,
};

enum BotStatMods: uint8
Expand Down
233 changes: 228 additions & 5 deletions src/server/game/AI/NpcBots/botdatamgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ ItemPerBotClassMap _botsWanderCreaturesSortedGear;

typedef std::unordered_map<ObjectGuid /*playerGuid*/, BotBankItemContainer> BotGearStorageMap;
BotGearStorageMap _botStoredGearMap;
typedef std::unordered_map<ObjectGuid /*playerGuid*/, BotItemSetsArray> BotGearSetStorageMap;
BotGearSetStorageMap _botStoredGearSetMap;

static bool allBotsLoaded = false;

Expand Down Expand Up @@ -1022,6 +1024,143 @@ void BotDataMgr::LoadNpcBotGearStorage()
BOT_LOG_INFO("server.loading", ">> Loaded {} NPCBot stored items for {} bot owners in {} ms", count, uint32(player_guids.size()), GetMSTimeDiffToNow(oldMSTime));
}

void BotDataMgr::LoadNpcBotGearSets()
{
BOT_LOG_INFO("server.loading", "Loading NPCBot item sets...");

uint32 oldMSTime = getMSTime();

static auto MAKE_PAIR64 = [](uint32 l, uint32 h) { return uint64(l | (uint64(h) << 32)); };
auto make_set_guid = [](uint32 plow, uint8 set_id) { return MAKE_PAIR64(plow, set_id); };
auto unpack_set_guid = [](uint64 set_guid) { return std::tuple(PAIR64_LOPART(set_guid), (uint8)PAIR64_HIPART(set_guid)); };

// 0 1 2
QueryResult result = CharacterDatabase.Query("SELECT owner, set_id, set_name FROM characters_npcbot_gear_set");
if (!result)
{
BOT_LOG_INFO("server.loading", ">> Loaded 0 NPCBot item sets. DB table `characters_npcbot_gear_set` is empty!");
return;
}

std::set<uint32> player_guids;
std::set<uint64> set_guids;
do
{
Field* fields = result->Fetch();

uint32 player_guidlow = fields[0].Get<uint32>();
uint8 set_id = fields[1].Get<uint8>();
std::string set_name = fields[2].Get<std::string>();

ObjectGuid player_guid = ObjectGuid::Create<HighGuid::Player>(player_guidlow);

UpdateBotItemSet(player_guid, set_id, set_name);

player_guids.insert(player_guidlow);
set_guids.insert(make_set_guid(player_guidlow, set_id));

} while (result->NextRow());

// 0 1 2 3
result = CharacterDatabase.Query("SELECT owner, set_id, slot, item_id FROM characters_npcbot_gear_set_item ORDER BY owner,set_id,slot");

std::set<uint64> invalid_sets;
if (!result)
invalid_sets = set_guids;
else
{
do
{
Field* fields = result->Fetch();

uint32 player_guidlow = fields[0].Get<uint32>();
uint8 set_id = fields[1].Get<uint8>();
uint8 slot = fields[2].Get<uint8>();
uint32 item_id = fields[3].Get<uint32>();

uint64 set_guid = make_set_guid(player_guidlow, set_id);

if (!player_guids.contains(player_guidlow))
{
BOT_LOG_ERROR("server.loading", "Table `characters_npcbot_gear_set_item` contains values '{} {} {}' for non-existent player {}. Removing!",
uint32(set_id), uint32(slot), item_id, player_guidlow);
invalid_sets.insert(set_guid);
continue;
}

if (!set_guids.contains(set_guid))
{
BOT_LOG_ERROR("server.loading", "Table `characters_npcbot_gear_set_item` contains values '{} {}' for non-existent item set {} (player {}). Removing!",
uint32(slot), item_id, uint32(set_id), player_guidlow);
invalid_sets.insert(set_guid);
continue;
}

if (set_id >= MAX_BOT_EQUIPMENT_SETS)
{
BOT_LOG_ERROR("server.loading", "Table `characters_npcbot_gear_set_item` contains invalid set id {} (player {}). Removing!",
uint32(set_id), player_guidlow);
invalid_sets.insert(set_guid);
continue;
}

if (slot >= BOT_INVENTORY_SIZE)
{
BOT_LOG_ERROR("server.loading", "Table `characters_npcbot_gear_set_item` contains invalid slot {} for item set {} (player {}). Removing!",
uint32(slot), uint32(set_id), player_guidlow);
invalid_sets.insert(set_guid);
continue;
}

if (!sObjectMgr->GetItemTemplate(item_id))
{
BOT_LOG_ERROR("server.loading", "Table `characters_npcbot_gear_set_item` contains invalid item id {} in slot {} for item set {} (player {}). Removing!",
item_id, uint32(slot), uint32(set_id), player_guidlow);
invalid_sets.insert(set_guid);
continue;
}

ObjectGuid player_guid = ObjectGuid::Create<HighGuid::Player>(player_guidlow);

UpdateBotItemSet(player_guid, set_id, slot, item_id);

} while (result->NextRow());
}

if (!invalid_sets.empty())
{
CharacterDatabaseTransaction ctrans = CharacterDatabase.BeginTransaction();
for (uint64 set_guid : invalid_sets)
{
set_guids.erase(set_guid);
auto [player_guidlow, set_id] = unpack_set_guid(set_guid);
ObjectGuid player_guid = ObjectGuid::Create<HighGuid::Player>(player_guidlow);
_botStoredGearSetMap[player_guid][set_id].clear();
ctrans->Append("DELETE FROM characters_npcbot_gear_set_item WHERE owner = {} and set_id = {}", player_guidlow, uint32(set_id));
}

std::set<uint32> invalid_players;
for (auto const& p : _botStoredGearSetMap)
{
if (std::ranges::all_of(p.second, [](NpcBotItemSet const& arr) { return arr.empty(); }))
{
invalid_players.insert(p.first.GetCounter());
ctrans->Append("DELETE FROM characters_npcbot_gear_set WHERE owner = {}", p.first.GetCounter());
ctrans->Append("DELETE FROM characters_npcbot_gear_set_item WHERE owner = {}", p.first.GetCounter());
}
}
CharacterDatabase.CommitTransaction(ctrans);

for (uint32 player_guidlow : invalid_players)
{
player_guids.erase(player_guidlow);
_botStoredGearSetMap.erase(ObjectGuid::Create<HighGuid::Player>(player_guidlow));
}
}

BOT_LOG_INFO("server.loading", ">> Loaded {} NPCBot item sets for {} bow owners in {} ms", uint32(set_guids.size()), uint32(player_guids.size()), GetMSTimeDiffToNow(oldMSTime));
}

void BotDataMgr::LoadNpcBotMgrData()
{
BOT_LOG_INFO("server.loading", "Loading NPCBot managers data...");
Expand Down Expand Up @@ -1345,7 +1484,7 @@ void BotDataMgr::LoadWanderMap(bool reload, bool force_all_maps)
(k == 1 && !wp->HasFlag(BotWPFlags::BOTWP_FLAG_ALLIANCE_ONLY)) ||
(k == 2 && !wp->HasFlag(BotWPFlags::BOTWP_FLAG_ALLIANCE_OR_HORDE_ONLY)))
{
for (uint8 i = minLevel; i <= maxLevel; ++i)
for (size_t i = minLevel; i <= maxLevel; ++i)
spawn_node_levels[k][i - 1] = true;
}
}
Expand All @@ -1354,7 +1493,7 @@ void BotDataMgr::LoadWanderMap(bool reload, bool force_all_maps)
for (uint8 k = 0; k < TEAMS_COUNT; ++k)
{
auto const& vec = spawn_node_levels[k];
for (uint32 i = min_spawn_level; i <= max_spawn_level; ++i)
for (size_t i = min_spawn_level; i <= max_spawn_level; ++i)
{
if (vec[i - 1] == false)
BOT_LOG_ERROR("server.loading", "No {} spawn node found for level {}! Wandering bots may cause a crash!", team_strs[k], i);
Expand Down Expand Up @@ -3032,20 +3171,36 @@ uint32 BotDataMgr::GetTeamForFaction(uint32 factionTemplateId)
}
}

bool BotDataMgr::CanDepositBotBankItemsCount(ObjectGuid playerGuid, uint32 items_count)
{
if (uint32 capacity = BotMgr::GetGearBankCapacity())
{
uint32 stored_count = GetBotBankItemsCount(playerGuid);
if (stored_count + items_count > capacity)
return false;
}
return true;
}

BotBankItemContainer const* BotDataMgr::GetBotBankItems(ObjectGuid playerGuid)
{
decltype(_botStoredGearMap)::iterator mci = _botStoredGearMap.find(playerGuid);
return mci == _botStoredGearMap.cend() ? nullptr : &mci->second;
}

uint32 BotDataMgr::GetBotBankItemsCount(ObjectGuid playerGuid)
{
if (BotBankItemContainer const* botBankItems = GetBotBankItems(playerGuid))
return static_cast<uint32>(botBankItems->size());
return 0;
}

Item* BotDataMgr::WithdrawBotBankItem(ObjectGuid playerGuid, ObjectGuid::LowType itemGuidLow)
{
decltype(_botStoredGearMap)::iterator mci = _botStoredGearMap.find(playerGuid);
if (mci != _botStoredGearMap.cend())
{
auto ici = std::find_if(std::cbegin(mci->second), std::cend(mci->second), [guidLow = itemGuidLow](Item const* item) {
return item->GetGUID().GetCounter() == guidLow;
});
auto ici = std::ranges::find_if(mci->second, [=](Item const* item) { return item->GetGUID().GetCounter() == itemGuidLow; });
if (ici != mci->second.cend())
{
Item* item = *ici;
Expand Down Expand Up @@ -3080,6 +3235,74 @@ void BotDataMgr::SaveNpcBotStoredGear(ObjectGuid playerGuid, CharacterDatabaseTr
}
}

BotItemSetsArray const* BotDataMgr::GetBotItemSets(ObjectGuid playerGuid)
{
decltype(_botStoredGearSetMap)::const_iterator sci = _botStoredGearSetMap.find(playerGuid);
return sci == _botStoredGearSetMap.cend() ? nullptr : &sci->second;
}

NpcBotItemSet const* BotDataMgr::GetBotItemSet(ObjectGuid playerGuid, uint8 set_id)
{
if (BotItemSetsArray const* item_sets = GetBotItemSets(playerGuid))
return &item_sets->at(set_id);
return nullptr;
}

NpcBotItemSet& BotDataMgr::CreateNewBotItemSet(ObjectGuid playerGuid)
{
for (uint8 i : NPCBots::index_array<uint8, MAX_BOT_EQUIPMENT_SETS>)
{
if (!_botStoredGearSetMap[playerGuid][i])
return _botStoredGearSetMap[playerGuid][i];
}

//should not happen
size_t max_offset = size_t(MAX_BOT_EQUIPMENT_SETS) - 1;
_botStoredGearSetMap[playerGuid][max_offset].clear();
return _botStoredGearSetMap[playerGuid][max_offset];
}

void BotDataMgr::UpdateBotItemSet(ObjectGuid playerGuid, uint8 set_id, std::string const& set_name)
{
_botStoredGearSetMap[playerGuid][set_id].name = set_name;
}

void BotDataMgr::UpdateBotItemSet(ObjectGuid playerGuid, uint8 set_id, uint8 slot, uint32 item_id)
{
_botStoredGearSetMap[playerGuid][set_id].items[slot] = item_id;
}

void BotDataMgr::DeleteBotItemSet(ObjectGuid playerGuid, uint8 set_id)
{
_botStoredGearSetMap[playerGuid][set_id].clear();
}

void BotDataMgr::SaveNpcBotItemSets(ObjectGuid playerGuid, CharacterDatabaseTransaction trans)
{
decltype(_botStoredGearSetMap)::const_iterator sci = _botStoredGearSetMap.find(playerGuid);
if (sci == _botStoredGearSetMap.cend())
return;

trans->Append("DELETE FROM characters_npcbot_gear_set WHERE owner = {}", sci->first.GetCounter());
trans->Append("DELETE FROM characters_npcbot_gear_set_item WHERE owner = {}", sci->first.GetCounter());
for (uint8 i : NPCBots::index_array<uint8, MAX_BOT_EQUIPMENT_SETS>)
{
NpcBotItemSet const& item_set = sci->second[i];
if (!!item_set)
{
trans->Append("INSERT INTO characters_npcbot_gear_set (owner, set_id, set_name) VALUES ({}, {}, '{}')", sci->first.GetCounter(), uint32(i), item_set.name);
for (uint8 j : NPCBots::index_array<uint8, BOT_INVENTORY_SIZE>)
{
if (item_set.items[j])
{
trans->Append("INSERT INTO characters_npcbot_gear_set_item (owner, set_id, slot, item_id) VALUES ({}, {}, {}, {})",
sci->first.GetCounter(), uint32(i), uint32(j), item_set.items[j]);
}
}
}
}
}

NpcBotMgrData* BotDataMgr::SelectOrCreateNpcBotMgrData(ObjectGuid playerGuid)
{
std::unique_lock<std::shared_mutex> lock(*GetLock());
Expand Down
Loading

0 comments on commit 6985a72

Please sign in to comment.