From 8aa4a68f80b86cc038ebd0670ba9f13341deeee3 Mon Sep 17 00:00:00 2001 From: zeb <37308742+zeb139@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:32:26 -0400 Subject: [PATCH 1/3] added feature: use DB drop rates for weapons/armor/recipes probability in AH --- conf/mod_ahbot.conf.dist | 6 + src/AuctionHouseBot.cpp | 368 ++++++++++++++++++++++++++++++++-- src/AuctionHouseBot.h | 10 + src/AuctionHouseBotScript.cpp | 6 +- 4 files changed, 371 insertions(+), 19 deletions(-) diff --git a/conf/mod_ahbot.conf.dist b/conf/mod_ahbot.conf.dist index 0c3a0fa..cba14ba 100644 --- a/conf/mod_ahbot.conf.dist +++ b/conf/mod_ahbot.conf.dist @@ -46,6 +46,11 @@ # acore_characters.item_instance database table to grow over time. # Default: false (disabled) # +# AuctionHouseBot.Seller.UseDBDropRates.Enabled +# Enable/Disable the Seller using items' in-game drop rates from enemies +# and gameobjects to determine probability of listing the items on the AH. +# Default: false (disabled) +# # AuctionHouseBot.GUIDs # These are the character GUIDS (from characters->characters table) that # will be used to create auctions and otherwise interact with auctions. @@ -76,6 +81,7 @@ AuctionHouseBot.MinutesBetweenBuyCycle = 1 AuctionHouseBot.MinutesBetweenSellCycle = 1 AuctionHouseBot.EnableSeller = false AuctionHouseBot.ReturnExpiredAuctionItemsToBot = false +AuctionHouseBot.Seller.UseDBDropRates.Enabled = false AuctionHouseBot.GUIDs = 0 AuctionHouseBot.ItemsPerCycle = 150 AuctionHouseBot.ListingExpireTimeInSecondsMin = 900 diff --git a/src/AuctionHouseBot.cpp b/src/AuctionHouseBot.cpp index d374936..26f597b 100644 --- a/src/AuctionHouseBot.cpp +++ b/src/AuctionHouseBot.cpp @@ -25,7 +25,6 @@ #include "Config.h" #include "Player.h" #include "WorldSession.h" -#include "GameTime.h" #include "DatabaseEnv.h" #include "ItemTemplate.h" #include "SharedDefines.h" @@ -33,6 +32,7 @@ #include #include +#include using namespace std; @@ -157,6 +157,7 @@ AuctionHouseBot::AuctionHouseBot() : ListedItemIDRestrictedEnabled(false), ListedItemIDMin(0), ListedItemIDMax(200000), + SellerUseDBDropRates(false), LastBuyCycleCount(0), LastSellCycleCount(0), ActiveListMultipleItemID(0), @@ -971,6 +972,7 @@ void AuctionHouseBot::AddNewAuctions(std::vector AHBPlayers, FactionSpe for (uint32 cnt = 1; cnt <= newItemsToListCount; cnt++) { auto trans = CharacterDatabase.BeginTransaction(); + ItemTemplate const* prototype = nullptr; uint32 batchCount = 0; while (batchCount < 500 && itemsGenerated < newItemsToListCount) @@ -980,6 +982,15 @@ void AuctionHouseBot::AddNewAuctions(std::vector AHBPlayers, FactionSpe if (ActiveListMultipleItemID != 0) { itemID = ActiveListMultipleItemID; + + prototype = sObjectMgr->GetItemTemplate(itemID); + if (!prototype) + { + if (debug_Out) + LOG_ERROR("module", "AHSeller: prototype == NULL"); + continue; + } + RemainingListMultipleCount--; if (RemainingListMultipleCount <= 0) ActiveListMultipleItemID = 0; @@ -987,7 +998,59 @@ void AuctionHouseBot::AddNewAuctions(std::vector AHBPlayers, FactionSpe else { itemID = GetRandomItemIDForListing(); - if (itemID != 0 && ItemListProportionMultipliedItemIDs.find(itemID) != ItemListProportionMultipliedItemIDs.end() && + if (itemID == 0) + { + if (debug_Out) + LOG_ERROR("module", "AHSeller: Item::CreateItem() failed as the ItemID is 0"); + continue; + } + + prototype = sObjectMgr->GetItemTemplate(itemID); + if (!prototype) + { + if (debug_Out) + LOG_ERROR("module", "AHSeller: prototype == NULL"); + continue; + } + + // If current item is crafted, ineligible, or quest reward, ignore drop rates and continue to list it + if (SellerUseDBDropRates && + IsItemEligibleForDBDropRates(prototype) && + !IsItemCrafted(itemID) && + !IsItemQuestReward(itemID)) + { + // The AHBot has chosen a rare/epic armor/weapon/recipe, so select another item + // of that type based on drop rates. This way ListProportions are respected. + + // Roll for rarity tier + double r = 100.0 * (urand(0, INT32_MAX) / static_cast(INT32_MAX)); + int tier = GetItemDropChanceTier(r); + + // If chosen tier is empty, search rarer tiers until not empty + auto& tierBuckets = ItemTiersByClassAndQuality[prototype->Class][prototype->Quality]; + while (tierBuckets[tier].empty() && tier < 10) { + if (debug_Out) + LOG_INFO("module", "Bucket is empty for class {} quality {} tier {}", prototype->Class, prototype->Quality, tier); + tier++; + } + + // Pull a random item from selected rarity tier + auto& bucket = tierBuckets[tier]; + if (!bucket.empty()) + { + itemID = bucket[rand() % bucket.size()]; + prototype = sObjectMgr->GetItemTemplate(itemID); + if (!prototype) + { + if (debug_Out) + LOG_ERROR("module", "AHSeller: prototype == NULL"); + continue; + } + } + else continue; + } + + if (ItemListProportionMultipliedItemIDs.find(itemID) != ItemListProportionMultipliedItemIDs.end() && ItemListProportionMultipliedItemIDs[itemID] > 1) { ActiveListMultipleItemID = itemID; @@ -997,22 +1060,6 @@ void AuctionHouseBot::AddNewAuctions(std::vector AHBPlayers, FactionSpe } } - // Prevent invalid IDs - if (itemID == 0) - { - if (debug_Out) - LOG_ERROR("module", "AHSeller: Item::CreateItem() failed as the ItemID is 0"); - continue; - } - - ItemTemplate const* prototype = sObjectMgr->GetItemTemplate(itemID); - if (prototype == NULL) - { - if (debug_Out) - LOG_ERROR("module", "AHSeller: prototype == NULL"); - continue; - } - Player* AHBplayer = AHBPlayers[urand(0, AHBPlayers.size() - 1)]; Item* item = Item::CreateItem(itemID, 1, AHBplayer); @@ -1070,6 +1117,272 @@ void AuctionHouseBot::AddNewAuctions(std::vector AHBPlayers, FactionSpe LOG_INFO("module", "AHSeller: Added {} items", itemsGenerated); } +void AuctionHouseBot::PopulateItemDropChances() +{ + // Search creature loot templates, referenced loot_loot_template, group_loot tables, and object_loot tables for items' drop rates + std::string directDropString = R"SQL( + SELECT it.entry AS itemID, + clt.Chance AS direct_chance, + 0 AS reference_chance + FROM creature_template ct + JOIN creature_loot_template clt ON clt.Entry = ct.lootid + JOIN item_template it ON it.entry = clt.Item + WHERE clt.Reference = 0 AND clt.GroupId = 0 AND it.class IN (2,4,9) AND it.quality > 2 + )SQL"; + + std::string referenceDropString = R"SQL( + WITH reference_group_counts AS ( + SELECT entry AS referenceID, COUNT(*) AS groupCount + FROM reference_loot_template + GROUP BY entry + ), + all_references AS ( + SELECT + rlt.Entry AS referenceID, + rlt.Item AS itemID, + rlt.Chance AS referenceChance, + rgc.groupCount, + MIN(clt.Chance) AS creatureChance, + CASE WHEN COUNT(clt.Entry) > 0 THEN 1 ELSE 0 END AS hasCreature + FROM reference_loot_template rlt + JOIN reference_group_counts rgc ON rlt.Entry = rgc.referenceID + LEFT JOIN creature_loot_template clt ON clt.Reference = rlt.Entry + WHERE clt.`Comment` NOT LIKE '%Placeholder%' + GROUP BY rlt.Entry, rlt.Item, rlt.Chance, rgc.groupCount + ) + SELECT + ar.itemID AS itemID, + 0 AS direct_chance, + CASE + WHEN ar.hasCreature = 1 + THEN (1.0 / case when ar.groupCount < 6 then 6 ELSE ar.groupCount end) * + COALESCE(NULLIF(ar.referenceChance,0),1) * + COALESCE(NULLIF(ar.creatureChance,0),1) + ELSE + (1.0 / ar.groupCount) * + COALESCE(NULLIF(ar.referenceChance,0),1) + END AS reference_chance + FROM all_references ar + JOIN item_template it ON it.entry = ar.itemID + WHERE it.class IN (2,4,9) AND it.quality > 2 + )SQL"; + + // This will lookup items in referenced_loot_template whose Reference entry is not associated with a creature_loot_template + std::string danglingReferenceDropString = R"SQL( + WITH reference_group_counts AS ( + SELECT entry AS referenceID, COUNT(*) AS groupCount + FROM reference_loot_template + GROUP BY entry + ), + creature_references AS ( + SELECT DISTINCT Reference AS referenceID FROM creature_loot_template WHERE REFERENCE != 0 + ), + reference_data AS ( + SELECT + rlt.Entry AS referenceID, + rlt.Item AS itemID, + rlt.Chance AS referenceChance, + rgc.groupCount + FROM reference_loot_template rlt + JOIN reference_group_counts rgc ON rlt.Entry = rgc.referenceID + ) + SELECT + rd.itemID AS itemID, + 0 AS direct_chance, + (1.0 / rd.groupCount) * COALESCE(NULLIF(rd.referenceChance, 0), 1) AS reference_chance + FROM reference_data rd + JOIN item_template it ON it.entry = rd.itemID + WHERE it.class IN (2, 4, 9) + AND it.quality > 2 + )SQL"; + + std::string groupDropString = R"SQL( + WITH group_tables AS ( + SELECT clt.Entry AS loot_entry, clt.GroupId AS group_id, clt.Chance AS chance, clt.Item AS item_id, it.`name` AS itemName, it.class AS itemClass, it.Quality AS itemQuality + FROM creature_loot_template clt + JOIN item_template it ON clt.Item = it.entry + WHERE clt.groupid != 0 AND clt.REFERENCE = 0 + ) + SELECT item_id, + 0 AS direct_chance, + CASE + WHEN chance = 0 THEN (1.0 / item_count) * (1 - POWER(1 - (1.0 / item_count), item_count)) * 100 + WHEN chance != 0 THEN ((1.0 / item_count) * (1 - POWER(1 - (1.0 / item_count), item_count)) * 100) * chance/100 + END AS reference_chance + FROM ( + SELECT group_tables.*, COUNT(*) OVER (PARTITION BY loot_entry, group_id) AS item_count + FROM group_tables + ) compute_item_count + WHERE itemClass IN (2,4,9) AND itemQuality > 2 + )SQL"; + + std::string objectsDropString = R"SQL( + SELECT it.entry AS itemID, + ilt.Chance AS direct_chance, + 0 AS reference_chance + FROM item_loot_template ilt + JOIN item_template it ON it.entry = ilt.Item + WHERE it.class IN (2,4,9) AND it.quality > 2 AND chance != 0 + UNION ALL + SELECT it.entry AS itemID, + golt.Chance AS direct_chance, + 0 AS reference_chance + FROM gameobject_loot_template golt + JOIN item_template it ON it.entry = golt.Item + WHERE it.class IN (2,4,9) AND it.quality > 2 AND chance != 0 + )SQL"; + + QueryResult directResult = WorldDatabase.Query(directDropString); + QueryResult referenceResult = WorldDatabase.Query(referenceDropString); + QueryResult danglingReferenceResult = WorldDatabase.Query(danglingReferenceDropString); + QueryResult groupResult = WorldDatabase.Query(groupDropString); + QueryResult objectsDropResult = WorldDatabase.Query(objectsDropString); + if (!directResult || !referenceResult || !danglingReferenceResult || !groupResult || !objectsDropResult) + { + LOG_ERROR("module", "AuctionHouseBot: PopulateItemDropChances() failed!"); + return; + } + + // Add drop rate of all results to CachedItemDropRates + auto parseResults = [this](QueryResult result) + { + do { + Field* fields = result->Fetch(); + double directDropChance = 0.0; + double referenceDropChance = 0.0; + uint32 itemID = fields[0].Get(); + + // Ignore quest rewards and crafted items, they have "100%" drop rate + if (IsItemQuestReward(itemID) || IsItemCrafted(itemID)) + continue; + + if (!fields[1].IsNull()) + directDropChance = fields[1].Get(); + if (!fields[2].IsNull()) + referenceDropChance = fields[2].Get(); + + double higherDropChance = (directDropChance > referenceDropChance) ? directDropChance : referenceDropChance; + + if (CachedItemDropRates[itemID] < higherDropChance) + CachedItemDropRates[itemID] = higherDropChance; + + } while (result->NextRow()); + }; + + parseResults(directResult); + parseResults(referenceResult); + parseResults(danglingReferenceResult); + parseResults(groupResult); + parseResults(objectsDropResult); + + // Process item candidates: filter invalid entries and group by drop rate tier + for (auto& [classID, qualityGroups] : ItemCandidatesByItemClassAndQuality) + { + for (auto& [qualityID, candidates] : qualityGroups) + { + // Erase items that are not crafted, not quest rewards, and missing DB drop rate + candidates.erase( + std::remove_if( + candidates.begin(), + candidates.end(), + [&](uint32 id) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(id); + if (!IsItemEligibleForDBDropRates(proto)) + return false; + + bool shouldErase = !IsItemCrafted(id) + && !IsItemQuestReward(id) + && !CachedItemDropRates.contains(id); + + return shouldErase; + }), + candidates.end() + ); + + // Now process the remaining valid items + for (uint32 id : candidates) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(id); + if (!IsItemEligibleForDBDropRates(proto)) + continue; + + double rate = CachedItemDropRates[id]; + int tier = GetItemDropChanceTier(rate); + ItemTiersByClassAndQuality[proto->Class][proto->Quality][tier].push_back(id); + } + } + } + + if (debug_Out) + { + // Show number of items in each tier + for (int i = 0; i < 17; i++) + for (int j = 0; j < 7; j++) + for (int k = 0; k < 10; k++) + { + if (i == 2) + LOG_INFO("module", "Armor Count: Rarity {} Tier {} has {} items", j, k, ItemTiersByClassAndQuality[i][j][k].size()); + if (i == 4) + LOG_INFO("module", "Weapon Count: Rarity {} Tier {} has {} items", j, k, ItemTiersByClassAndQuality[i][j][k].size()); + if (i == 9) + LOG_INFO("module", "Recipe Count: Rarity {} Tier {} has {} items", j, k, ItemTiersByClassAndQuality[i][j][k].size()); + } + } +} + +void AuctionHouseBot::PopulateQuestRewardItemIDs() +{ + string questRewardsString = R"SQL( + SELECT DISTINCT item_id + FROM ( + SELECT RewardItem1 AS item_id FROM quest_template UNION ALL + SELECT RewardItem2 AS item_id FROM quest_template UNION ALL + SELECT RewardItem3 AS item_id FROM quest_template UNION ALL + SELECT RewardItem4 AS item_id FROM quest_template UNION ALL + SELECT ItemDrop1 AS item_id FROM quest_template UNION ALL + SELECT ItemDrop2 AS item_id FROM quest_template UNION ALL + SELECT ItemDrop3 AS item_id FROM quest_template UNION ALL + SELECT ItemDrop4 AS item_id FROM quest_template UNION ALL + SELECT RewardChoiceItemID1 AS item_id FROM quest_template UNION ALL + SELECT RewardChoiceItemID2 AS item_id FROM quest_template UNION ALL + SELECT RewardChoiceItemID3 AS item_id FROM quest_template UNION ALL + SELECT RewardChoiceItemID4 AS item_id FROM quest_template UNION ALL + SELECT RewardChoiceItemID5 AS item_id FROM quest_template UNION ALL + SELECT RewardChoiceItemID6 AS item_id FROM quest_template + ) AS quest_rewards + WHERE item_id != 0 + )SQL"; + + QueryResult questRewardsResult = WorldDatabase.Query(questRewardsString); + if (!questRewardsResult) + { + LOG_ERROR("module", "AuctionHouseBot: Quest Rewards lookup failed."); + return; + } + + do + { + uint32 id = questRewardsResult->Fetch()->Get(); + QuestRewardItemIDs.insert(id); + } while (questRewardsResult->NextRow()); +} + +int AuctionHouseBot::GetItemDropChanceTier(double dropRate) +{ + if (dropRate > 10) return 0; + else if (dropRate > 5) return 1; + else if (dropRate > 2) return 2; + else if (dropRate > 1) return 3; + else if (dropRate > 0.5) return 4; + else if (dropRate > 0.2) return 5; + else if (dropRate > 0.1) return 6; + else if (dropRate > 0.05) return 7; + else if (dropRate > 0.02) return 8; + else if (dropRate > 0.01) return 9; + else return 10; +} + void AuctionHouseBot::AddNewAuctionBuyerBotBid(std::vector AHBPlayers, FactionSpecificAuctionHouseConfig *config) { if (!BuyingBotEnabled) @@ -1375,6 +1688,7 @@ void AuctionHouseBot::InitializeConfiguration() SetCyclesBetweenBuyOrSell(); ReturnExpiredAuctionItemsToBot = sConfigMgr->GetOption("AuctionHouseBot.ReturnExpiredAuctionItemsToBot", false); ItemsPerCycle = sConfigMgr->GetOption("AuctionHouseBot.ItemsPerCycle", 75); + SellerUseDBDropRates = sConfigMgr->GetOption("AuctionHouseBot.Seller.UseDBDropRates.Enabled", false); MaxBuyoutPriceInCopper = sConfigMgr->GetOption("AuctionHouseBot.MaxBuyoutPriceInCopper", 1000000000); BuyoutVariationReducePercent = sConfigMgr->GetOption("AuctionHouseBot.BuyoutVariationReducePercent", 0.15f); BuyoutVariationAddPercent = sConfigMgr->GetOption("AuctionHouseBot.BuyoutVariationAddPercent", 0.25f); @@ -1984,3 +2298,21 @@ void AuctionHouseBot::CleanupExpiredAuctionItems() CharacterDatabase.CommitTransaction(trans); } + +bool AuctionHouseBot::IsItemQuestReward(uint32 itemID) +{ + return (QuestRewardItemIDs.find(itemID) != QuestRewardItemIDs.end()); +} + +bool AuctionHouseBot::IsItemCrafted(uint32 itemID) +{ + return (ItemIDsProducedByRecipes.find(itemID) != ItemIDsProducedByRecipes.end()); +} + +bool AuctionHouseBot::IsItemEligibleForDBDropRates(ItemTemplate const* proto) +{ + return (proto->Quality >= ITEM_QUALITY_RARE && + (proto->Class == ITEM_CLASS_WEAPON || + proto->Class == ITEM_CLASS_ARMOR || + proto->Class == ITEM_CLASS_RECIPE)); +} diff --git a/src/AuctionHouseBot.h b/src/AuctionHouseBot.h index 0a25d97..228a63a 100644 --- a/src/AuctionHouseBot.h +++ b/src/AuctionHouseBot.h @@ -278,6 +278,10 @@ private: uint32 ListedItemIDMax; std::set ListedItemIDExceptionItems; bool PreventOverpayingForVendorItems; + std::unordered_map CachedItemDropRates; + std::vector ItemTiersByClassAndQuality[17][7][11]; // [Classes][Qualities][Tiers] + bool SellerUseDBDropRates; + std::unordered_set QuestRewardItemIDs; FactionSpecificAuctionHouseConfig AllianceConfig; FactionSpecificAuctionHouseConfig HordeConfig; @@ -314,6 +318,12 @@ public: const char* GetCategoryName(ItemClass category); uint32 GetStackSizeForItem(ItemTemplate const* itemProto) const; void CalculateItemValue(ItemTemplate const* itemProto, uint64& outBidPrice, uint64& outBuyoutPrice); + void PopulateItemDropChances(); + void PopulateQuestRewardItemIDs(); + bool IsItemQuestReward(uint32 itemID); + bool IsItemCrafted(uint32 itemID); + bool IsItemEligibleForDBDropRates(ItemTemplate const* proto); + int GetItemDropChanceTier(double dropRate); float GetAdvancedPricingMultiplier(ItemTemplate const* itemProto); ItemTemplate const* GetProducedItemFromRecipe(ItemTemplate const* recipeItemTemplate); std::unordered_set GetItemIDsProducedByRecipes(); diff --git a/src/AuctionHouseBotScript.cpp b/src/AuctionHouseBotScript.cpp index 2cf7c3a..2c5b48c 100644 --- a/src/AuctionHouseBotScript.cpp +++ b/src/AuctionHouseBotScript.cpp @@ -24,7 +24,6 @@ public: if (HasPerformedStartup == true) { LOG_INFO("server.loading", "AuctionHouseBot: (Re)populating item candidate lists ..."); - auctionbot->PopulateItemCandidatesAndProportions(); } } @@ -32,6 +31,11 @@ public: { LOG_INFO("server.loading", "AuctionHouseBot: (Re)populating item candidate lists ..."); auctionbot->PopulateItemCandidatesAndProportions(); + if (sConfigMgr->GetOption("AuctionHouseBot.Seller.UseDBDropRates.Enabled", true)) + { + auctionbot->PopulateQuestRewardItemIDs(); + auctionbot->PopulateItemDropChances(); + } HasPerformedStartup = true; } }; From d7b742f47eee85a054751815eaa4347fe8f7d069 Mon Sep 17 00:00:00 2001 From: zeb <37308742+zeb139@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:52:55 -0400 Subject: [PATCH 2/3] code review: fixed startup script behavior --- src/AuctionHouseBotScript.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/AuctionHouseBotScript.cpp b/src/AuctionHouseBotScript.cpp index 2c5b48c..71dd96e 100644 --- a/src/AuctionHouseBotScript.cpp +++ b/src/AuctionHouseBotScript.cpp @@ -24,6 +24,13 @@ public: if (HasPerformedStartup == true) { LOG_INFO("server.loading", "AuctionHouseBot: (Re)populating item candidate lists ..."); + auctionbot->PopulateItemCandidatesAndProportions(); + + if (sConfigMgr->GetOption("AuctionHouseBot.Seller.UseDBDropRates.Enabled", true)) + { + auctionbot->PopulateQuestRewardItemIDs(); + auctionbot->PopulateItemDropChances(); + } } } From a9973cf8cbb7dbb806c29047967471919be779a2 Mon Sep 17 00:00:00 2001 From: zeb <37308742+zeb139@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:54:59 -0400 Subject: [PATCH 3/3] Exposed additional config, added .conf.dist readme --- conf/mod_ahbot.conf.dist | 37 +++++++++++--- src/AuctionHouseBot.cpp | 91 +++++++++++++++++++++++++++-------- src/AuctionHouseBot.h | 7 ++- src/AuctionHouseBotScript.cpp | 4 +- 4 files changed, 110 insertions(+), 29 deletions(-) diff --git a/conf/mod_ahbot.conf.dist b/conf/mod_ahbot.conf.dist index cba14ba..79d447a 100644 --- a/conf/mod_ahbot.conf.dist +++ b/conf/mod_ahbot.conf.dist @@ -46,11 +46,6 @@ # acore_characters.item_instance database table to grow over time. # Default: false (disabled) # -# AuctionHouseBot.Seller.UseDBDropRates.Enabled -# Enable/Disable the Seller using items' in-game drop rates from enemies -# and gameobjects to determine probability of listing the items on the AH. -# Default: false (disabled) -# # AuctionHouseBot.GUIDs # These are the character GUIDS (from characters->characters table) that # will be used to create auctions and otherwise interact with auctions. @@ -81,12 +76,42 @@ AuctionHouseBot.MinutesBetweenBuyCycle = 1 AuctionHouseBot.MinutesBetweenSellCycle = 1 AuctionHouseBot.EnableSeller = false AuctionHouseBot.ReturnExpiredAuctionItemsToBot = false -AuctionHouseBot.Seller.UseDBDropRates.Enabled = false AuctionHouseBot.GUIDs = 0 AuctionHouseBot.ItemsPerCycle = 150 AuctionHouseBot.ListingExpireTimeInSecondsMin = 900 AuctionHouseBot.ListingExpireTimeInSecondsMax = 86400 +############################################################################### +# AuctionHouseBot.AdvancedListingRules.UseDropRates.Enabled +# Enable/Disable the Seller using items' in-game drop rates from Enemies +# and GameObjects to determine the probability of listing them on the AH. +# Attempts to simulate "live" AHs by making very powerful items appear less often. +# This setting respects ListProportion config. It can also result in more duplicate +# items appearing on the AH due to higher-drop-rate items being selected more often. +# Crafted items may also appear more often since their drop rate is effectively 100%. +# Default: false (disabled) +# +# AuctionHouseBot.AdvancedListingRules.UseDropRates.MinQuality +# The minimum quality that should be included by AdvancedListingRules.UseDropRates. +# Value should be the integer corresponding to the desired minimum rarity. +# Examples: Setting to 3 will apply to Rare, Epic, Legendary, and Heirloom. +# Setting to 1 will apply to Common, Uncommon, Rare, Epic, Legendary, and Heirloom. +# (0) Poor, (1) Common, (2) Uncommon, (3) Rare, (4) Epic, (5) Legendary, (6) Heirloom +# Default: 3 +# +# AuctionHouseBot.Seller.AdvancedListingRules.UseDropRates. +# Toggle AdvancedListingRules.UseDropRates behavior for individual category. +# Only applied if AdvancedListingRules.UseDropRates.Enabled is true. +# Default: true (enabled) +############################################################################### + +AuctionHouseBot.AdvancedListingRules.UseDropRates.Enabled = false +AuctionHouseBot.AdvancedListingRules.UseDropRates.MinQuality = 2 + +AuctionHouseBot.AdvancedListingRules.UseDropRates.Weapon = true +AuctionHouseBot.AdvancedListingRules.UseDropRates.Armor = true +AuctionHouseBot.AdvancedListingRules.UseDropRates.Recipe = true + ############################################################################### # AuctionHouseBot.MaxBuyoutPriceInCopper # Maximum amount that a buyout on a listing can be in copper. Prevents diff --git a/src/AuctionHouseBot.cpp b/src/AuctionHouseBot.cpp index 26f597b..20a3441 100644 --- a/src/AuctionHouseBot.cpp +++ b/src/AuctionHouseBot.cpp @@ -157,7 +157,10 @@ AuctionHouseBot::AuctionHouseBot() : ListedItemIDRestrictedEnabled(false), ListedItemIDMin(0), ListedItemIDMax(200000), - SellerUseDBDropRates(false), + AdvancedListingRuleUseDropRatesEnabled(false), + AdvancedListingRuleUseDropRatesWeaponEnabled(true), + AdvancedListingRuleUseDropRatesArmorEnabled(true), + AdvancedListingRuleUseDropRatesRecipeEnabled(true), LastBuyCycleCount(0), LastSellCycleCount(0), ActiveListMultipleItemID(0), @@ -1014,7 +1017,7 @@ void AuctionHouseBot::AddNewAuctions(std::vector AHBPlayers, FactionSpe } // If current item is crafted, ineligible, or quest reward, ignore drop rates and continue to list it - if (SellerUseDBDropRates && + if (AdvancedListingRuleUseDropRatesEnabled && IsItemEligibleForDBDropRates(prototype) && !IsItemCrafted(itemID) && !IsItemQuestReward(itemID)) @@ -1117,6 +1120,28 @@ void AuctionHouseBot::AddNewAuctions(std::vector AHBPlayers, FactionSpe LOG_INFO("module", "AHSeller: Added {} items", itemsGenerated); } +std::string AuctionHouseBot::GetAdvancedListingRuleUseDropRatesEnabledCategoriesString() +{ + std::vector enabledCategories; + + if (AdvancedListingRuleUseDropRatesWeaponEnabled) + enabledCategories.push_back(ITEM_CLASS_WEAPON); + if (AdvancedListingRuleUseDropRatesArmorEnabled) + enabledCategories.push_back(ITEM_CLASS_ARMOR); + if (AdvancedListingRuleUseDropRatesRecipeEnabled) + enabledCategories.push_back(ITEM_CLASS_RECIPE); + + std::ostringstream oss; + for (size_t i = 0; i < enabledCategories.size(); ++i) + { + if (i > 0) + oss << ","; + oss << enabledCategories[i]; + } + + return oss.str(); +} + void AuctionHouseBot::PopulateItemDropChances() { // Search creature loot templates, referenced loot_loot_template, group_loot tables, and object_loot tables for items' drop rates @@ -1127,7 +1152,7 @@ void AuctionHouseBot::PopulateItemDropChances() FROM creature_template ct JOIN creature_loot_template clt ON clt.Entry = ct.lootid JOIN item_template it ON it.entry = clt.Item - WHERE clt.Reference = 0 AND clt.GroupId = 0 AND it.class IN (2,4,9) AND it.quality > 2 + WHERE clt.Reference = 0 AND clt.GroupId = 0 AND it.class IN ({}) AND it.quality >= {} )SQL"; std::string referenceDropString = R"SQL( @@ -1164,7 +1189,7 @@ void AuctionHouseBot::PopulateItemDropChances() END AS reference_chance FROM all_references ar JOIN item_template it ON it.entry = ar.itemID - WHERE it.class IN (2,4,9) AND it.quality > 2 + WHERE it.class IN ({}) AND it.quality >= {} )SQL"; // This will lookup items in referenced_loot_template whose Reference entry is not associated with a creature_loot_template @@ -1192,8 +1217,8 @@ void AuctionHouseBot::PopulateItemDropChances() (1.0 / rd.groupCount) * COALESCE(NULLIF(rd.referenceChance, 0), 1) AS reference_chance FROM reference_data rd JOIN item_template it ON it.entry = rd.itemID - WHERE it.class IN (2, 4, 9) - AND it.quality > 2 + WHERE it.class IN ({}) + AND it.quality >= {} )SQL"; std::string groupDropString = R"SQL( @@ -1213,7 +1238,7 @@ void AuctionHouseBot::PopulateItemDropChances() SELECT group_tables.*, COUNT(*) OVER (PARTITION BY loot_entry, group_id) AS item_count FROM group_tables ) compute_item_count - WHERE itemClass IN (2,4,9) AND itemQuality > 2 + WHERE itemClass IN ({}) AND itemQuality >= {} )SQL"; std::string objectsDropString = R"SQL( @@ -1222,24 +1247,33 @@ void AuctionHouseBot::PopulateItemDropChances() 0 AS reference_chance FROM item_loot_template ilt JOIN item_template it ON it.entry = ilt.Item - WHERE it.class IN (2,4,9) AND it.quality > 2 AND chance != 0 + WHERE it.class IN ({}) AND it.quality >= {} AND chance != 0 UNION ALL SELECT it.entry AS itemID, golt.Chance AS direct_chance, 0 AS reference_chance FROM gameobject_loot_template golt JOIN item_template it ON it.entry = golt.Item - WHERE it.class IN (2,4,9) AND it.quality > 2 AND chance != 0 + WHERE it.class IN ({}) AND it.quality >= {} AND chance != 0 )SQL"; - QueryResult directResult = WorldDatabase.Query(directDropString); - QueryResult referenceResult = WorldDatabase.Query(referenceDropString); - QueryResult danglingReferenceResult = WorldDatabase.Query(danglingReferenceDropString); - QueryResult groupResult = WorldDatabase.Query(groupDropString); - QueryResult objectsDropResult = WorldDatabase.Query(objectsDropString); + std::string enabledCategories = GetAdvancedListingRuleUseDropRatesEnabledCategoriesString(); + if (enabledCategories.empty()) + { + LOG_ERROR("module", "AuctionHouseBot: No categories are enabled for AuctionHouseBot.Seller.AdvancedListingRules.UseDropRates"); + return; + } + + QueryResult directResult = WorldDatabase.Query(directDropString, enabledCategories, AdvancedListingRuleUseDropRatesMinQuality); + QueryResult referenceResult = WorldDatabase.Query(referenceDropString, enabledCategories, AdvancedListingRuleUseDropRatesMinQuality); + QueryResult danglingReferenceResult = WorldDatabase.Query(danglingReferenceDropString, enabledCategories, AdvancedListingRuleUseDropRatesMinQuality); + QueryResult groupResult = WorldDatabase.Query(groupDropString, enabledCategories, AdvancedListingRuleUseDropRatesMinQuality); + QueryResult objectsDropResult = WorldDatabase.Query(objectsDropString, + enabledCategories, AdvancedListingRuleUseDropRatesMinQuality, + enabledCategories, AdvancedListingRuleUseDropRatesMinQuality); if (!directResult || !referenceResult || !danglingReferenceResult || !groupResult || !objectsDropResult) { - LOG_ERROR("module", "AuctionHouseBot: PopulateItemDropChances() failed!"); + LOG_ERROR("module", "AuctionHouseBot: PopulateItemDropChances() failed to query items' drop rates."); return; } @@ -1688,7 +1722,11 @@ void AuctionHouseBot::InitializeConfiguration() SetCyclesBetweenBuyOrSell(); ReturnExpiredAuctionItemsToBot = sConfigMgr->GetOption("AuctionHouseBot.ReturnExpiredAuctionItemsToBot", false); ItemsPerCycle = sConfigMgr->GetOption("AuctionHouseBot.ItemsPerCycle", 75); - SellerUseDBDropRates = sConfigMgr->GetOption("AuctionHouseBot.Seller.UseDBDropRates.Enabled", false); + AdvancedListingRuleUseDropRatesEnabled = sConfigMgr->GetOption("AuctionHouseBot.AdvancedListingRules.UseDropRates.Enabled", false); + AdvancedListingRuleUseDropRatesWeaponEnabled = sConfigMgr->GetOption("AuctionHouseBot.AdvancedListingRules.UseDropRates.Weapon", true); + AdvancedListingRuleUseDropRatesArmorEnabled = sConfigMgr->GetOption("AuctionHouseBot.AdvancedListingRules.UseDropRates.Armor", true); + AdvancedListingRuleUseDropRatesRecipeEnabled = sConfigMgr->GetOption("AuctionHouseBot.AdvancedListingRules.UseDropRates.Recipe", true); + AdvancedListingRuleUseDropRatesMinQuality = sConfigMgr->GetOption("AuctionHouseBot.AdvancedListingRules.UseDropRates.MinQuality", 3); MaxBuyoutPriceInCopper = sConfigMgr->GetOption("AuctionHouseBot.MaxBuyoutPriceInCopper", 1000000000); BuyoutVariationReducePercent = sConfigMgr->GetOption("AuctionHouseBot.BuyoutVariationReducePercent", 0.15f); BuyoutVariationAddPercent = sConfigMgr->GetOption("AuctionHouseBot.BuyoutVariationAddPercent", 0.25f); @@ -2311,8 +2349,21 @@ bool AuctionHouseBot::IsItemCrafted(uint32 itemID) bool AuctionHouseBot::IsItemEligibleForDBDropRates(ItemTemplate const* proto) { - return (proto->Quality >= ITEM_QUALITY_RARE && - (proto->Class == ITEM_CLASS_WEAPON || - proto->Class == ITEM_CLASS_ARMOR || - proto->Class == ITEM_CLASS_RECIPE)); + if (!AdvancedListingRuleUseDropRatesEnabled) + return false; + + if (!proto || proto->Quality < AdvancedListingRuleUseDropRatesMinQuality) + return false; + + switch (proto->Class) + { + case ITEM_CLASS_WEAPON: + return AdvancedListingRuleUseDropRatesWeaponEnabled; + case ITEM_CLASS_ARMOR: + return AdvancedListingRuleUseDropRatesArmorEnabled; + case ITEM_CLASS_RECIPE: + return AdvancedListingRuleUseDropRatesRecipeEnabled; + default: + return false; + } } diff --git a/src/AuctionHouseBot.h b/src/AuctionHouseBot.h index 228a63a..d72ebf9 100644 --- a/src/AuctionHouseBot.h +++ b/src/AuctionHouseBot.h @@ -280,7 +280,11 @@ private: bool PreventOverpayingForVendorItems; std::unordered_map CachedItemDropRates; std::vector ItemTiersByClassAndQuality[17][7][11]; // [Classes][Qualities][Tiers] - bool SellerUseDBDropRates; + bool AdvancedListingRuleUseDropRatesEnabled; + bool AdvancedListingRuleUseDropRatesWeaponEnabled; + bool AdvancedListingRuleUseDropRatesArmorEnabled; + bool AdvancedListingRuleUseDropRatesRecipeEnabled; + int AdvancedListingRuleUseDropRatesMinQuality; std::unordered_set QuestRewardItemIDs; FactionSpecificAuctionHouseConfig AllianceConfig; @@ -319,6 +323,7 @@ public: uint32 GetStackSizeForItem(ItemTemplate const* itemProto) const; void CalculateItemValue(ItemTemplate const* itemProto, uint64& outBidPrice, uint64& outBuyoutPrice); void PopulateItemDropChances(); + std::string GetAdvancedListingRuleUseDropRatesEnabledCategoriesString(); void PopulateQuestRewardItemIDs(); bool IsItemQuestReward(uint32 itemID); bool IsItemCrafted(uint32 itemID); diff --git a/src/AuctionHouseBotScript.cpp b/src/AuctionHouseBotScript.cpp index 71dd96e..fcfbb97 100644 --- a/src/AuctionHouseBotScript.cpp +++ b/src/AuctionHouseBotScript.cpp @@ -26,7 +26,7 @@ public: LOG_INFO("server.loading", "AuctionHouseBot: (Re)populating item candidate lists ..."); auctionbot->PopulateItemCandidatesAndProportions(); - if (sConfigMgr->GetOption("AuctionHouseBot.Seller.UseDBDropRates.Enabled", true)) + if (sConfigMgr->GetOption("AuctionHouseBot.AdvancedListingRules.UseDropRates.Enabled", true)) { auctionbot->PopulateQuestRewardItemIDs(); auctionbot->PopulateItemDropChances(); @@ -38,7 +38,7 @@ public: { LOG_INFO("server.loading", "AuctionHouseBot: (Re)populating item candidate lists ..."); auctionbot->PopulateItemCandidatesAndProportions(); - if (sConfigMgr->GetOption("AuctionHouseBot.Seller.UseDBDropRates.Enabled", true)) + if (sConfigMgr->GetOption("AuctionHouseBot.AdvancedListingRules.UseDropRates.Enabled", true)) { auctionbot->PopulateQuestRewardItemIDs(); auctionbot->PopulateItemDropChances();