Correct Loot rolling behavior (#2190)

# Pull Request

This fixes the loot rolling behavior issue created by #2068 . 
Introduce the ability for enchanter bots to disenchant items they dont
need, and roll need on recipes they also need.
Make it so ITEM_USAGE_AH ensures the item is not BOP.
Try to reduce the call for item_usage in CalculateRollVote by passing
usage if available.

---

## Design Philosophy

We prioritize **stability, performance, and predictability** over
behavioral realism.
Complex player-mimicking logic is intentionally limited due to its
negative impact on scalability, maintainability, and
long-term robustness.

Excessive processing overhead can lead to server hiccups, increased CPU
usage, and degraded performance for all
participants. Because every action and
decision tree is executed **per bot and per trigger**, even small
increases in logic complexity can scale poorly and
negatively affect both players and
world (random) bots. Bots are not expected to behave perfectly, and
perfect simulation of human decision-making is not a
project goal. Increased behavioral
realism often introduces disproportionate cost, reduced predictability,
and significantly higher maintenance overhead.

Every additional branch of logic increases long-term responsibility. All
decision paths must be tested, validated, and
maintained continuously as the system evolves.
If advanced or AI-intensive behavior is introduced, the **default
configuration must remain the lightweight decision
model**. More complex behavior should only be
available as an **explicit opt-in option**, clearly documented as having
a measurable performance cost.

Principles:

- **Stability before intelligence**  
  A stable system is always preferred over a smarter one.

- **Performance is a shared resource**  
  Any increase in bot cost affects all players and all bots.

- **Simple logic scales better than smart logic**  
Predictable behavior under load is more valuable than perfect decisions.

- **Complexity must justify itself**  
  If a feature cannot clearly explain its cost, it should not exist.

- **Defaults must be cheap**  
  Expensive behavior must always be optional and clearly communicated.

- **Bots should look reasonable, not perfect**  
  The goal is believable behavior, not human simulation.

Before submitting, confirm that this change aligns with those
principles.

---

## Feature Evaluation

Please answer the following:

- Describe the **minimum logic** required to achieve the intended
behavior?
-- Add a new check that downgrades greed rolls to desired levels, or
bools for the other two options.
- Describe the **cheapest implementation** that produces an acceptable
result?
-- As implemented.
- Describe the **runtime cost** when this logic executes across many
bots?
-- Same as before. Item usage is the heaviest part, and that hasnt
changed to accommodate this.

---

## How to Test the Changes

- multiple bots in a group with group loot on, do a dungeon or
something. One bot should be an enchanter.

## Complexity & Impact

Does this change add new decision branches?
- - [ ] No
- - [x] Yes (**explain below**)

Does this change increase per-bot or per-tick processing?
- - [X] No
- - [ ] Yes (**describe and justify impact**)

Could this logic scale poorly under load?
- - [X] No
- - [ ] Yes (**explain why**)
---

## Defaults & Configuration

Does this change modify default bot behavior?
- - [ ] No
- - [X] Yes (**explain why**)
- - - Corrects the looting behavior to original design. 

If this introduces more advanced or AI-heavy logic:
- - [ ] Lightweight mode remains the default
- - [X] More complex behavior is optional and thereby configurable
---

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [x] No
- - [ ] Yes (**explain below**)

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.
This commit is contained in:
Keleborn 2026-03-20 12:37:02 -07:00 committed by GitHub
parent a473432b8f
commit 2ce8993986
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 60 additions and 52 deletions

View File

@ -298,9 +298,24 @@ AiPlayerbot.TwoRoundsGearInit = 0
# Default: 0 (disabled)
AiPlayerbot.FreeMethodLoot = 0
# Bots' loot roll level (0 = pass, 1 = greed, 2 = need)
# Bots' Roll level bots will use for items they Need (0 = pass, 1 = greed, 2 = need)
# Default: 1 (greed)
AiPlayerbot.LootRollLevel = 1
AiPlayerbot.LootNeedRollLevel = 1
# Enable bots to roll GREED on items (global toggle)
# If disabled, bots will PASS instead of GREED on all items
# Default: 0 (disabled - bots only NEED or PASS)
AiPlayerbot.LootGreedRollLevel = 0
# Enable bots to roll on recipes. Will NEED on learnable profession recipes they don't already know
# Bots will roll GREED on BoE recipes they can't learn if LootRollGreed is enabled.
# Default: 0 (disabled)
AiPlayerbot.LootRollRecipe = 0
# Bots with enchanting will roll DISENCHANT instead of GREED on disenchantable items
# If disabled, bots will GREED on disenchantable items instead
# Default: 0 (disabled)
AiPlayerbot.LootRollDisenchant = 0
#
#

View File

@ -22,10 +22,10 @@ bool LootRollAction::Execute(Event /*event*/)
std::vector<Roll*> rolls = group->GetRolls();
for (Roll*& roll : rolls)
{
if (roll->playerVote.find(bot->GetGUID())->second != NOT_EMITED_YET)
{
auto voteItr = roll->playerVote.find(bot->GetGUID());
if (voteItr == roll->playerVote.end() || voteItr->second != NOT_EMITED_YET)
continue;
}
ObjectGuid guid = roll->itemGUID;
uint32 itemId = roll->itemid;
int32 randomProperty = 0;
@ -41,27 +41,22 @@ bool LootRollAction::Execute(Event /*event*/)
std::string itemUsageParam;
if (randomProperty != 0)
{
itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty);
}
else
{
itemUsageParam = std::to_string(itemId);
}
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam);
// Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality.
if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC)
{
if (CanBotUseToken(proto, bot))
{
vote = NEED; // Eligible for "Need"
}
else
{
vote = GREED; // Not eligible, so "Greed"
}
}
else if (usage == ITEM_USAGE_DISENCHANT)
vote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
else
{
switch (proto->Class)
@ -69,40 +64,34 @@ bool LootRollAction::Execute(Event /*event*/)
case ITEM_CLASS_WEAPON:
case ITEM_CLASS_ARMOR:
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP)
{
vote = NEED;
}
else if (usage != ITEM_USAGE_NONE)
{
vote = GREED;
}
break;
case ITEM_CLASS_RECIPE:
if (!sPlayerbotAIConfig.lootRollRecipe)
vote = PASS;
else if (usage == ITEM_USAGE_SKILL)
vote = NEED; // Bot can learn this recipe
else if (proto->Bonding != BIND_WHEN_PICKED_UP)
vote = GREED; // BoE recipe bot can't learn - GREED for AH/trade
break;
default:
if (StoreLootAction::IsLootAllowed(itemId, botAI))
vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior
vote = CalculateRollVote(proto, usage);
break;
}
}
if (sPlayerbotAIConfig.lootRollLevel == 0)
if (vote == NEED)
{
if (sPlayerbotAIConfig.lootNeedRollLevel == 0 || RollUniqueCheck(proto, bot))
vote = PASS;
else if (sPlayerbotAIConfig.lootNeedRollLevel == 1)
vote = GREED;
}
else if (vote == GREED && !sPlayerbotAIConfig.lootGreedRollLevel)
vote = PASS;
}
else if (sPlayerbotAIConfig.lootRollLevel == 1)
{
// Level 1 = "greed" mode: bots greed on useful items but never need
// Only downgrade NEED to GREED, preserve GREED votes as-is
if (vote == NEED)
{
if (RollUniqueCheck(proto, bot))
{
vote = PASS;
}
else
{
vote = GREED;
}
}
}
switch (group->GetLootMethod())
{
case MASTER_LOOT:
@ -120,11 +109,14 @@ bool LootRollAction::Execute(Event /*event*/)
return false;
}
RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto)
RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto, ItemUsage usage)
{
std::ostringstream out;
out << proto->ItemId;
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str());
if (usage == ITEM_USAGE_NONE)
{
std::ostringstream out;
out << proto->ItemId;
usage = AI_VALUE2(ItemUsage, "item usage", out.str());
}
RollVote needVote = PASS;
switch (usage)
@ -137,11 +129,13 @@ RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto)
break;
case ITEM_USAGE_SKILL:
case ITEM_USAGE_USE:
case ITEM_USAGE_DISENCHANT:
case ITEM_USAGE_AH:
case ITEM_USAGE_VENDOR:
needVote = GREED;
break;
case ITEM_USAGE_DISENCHANT:
needVote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
break;
default:
break;
}
@ -195,9 +189,7 @@ bool CanBotUseToken(ItemTemplate const* proto, Player* bot)
// Check if the bot's class is allowed to use the token
if (proto->AllowableClass & botClassMask)
{
return true; // Bot's class is eligible to use this token
}
return false; // Bot's class cannot use this token
}
@ -213,13 +205,9 @@ bool RollUniqueCheck(ItemTemplate const* proto, Player* bot)
// Determine if the unique item is already equipped
bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
{
return true; // Unique Item is already equipped
}
else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1))
{
return true; // Unique item already in bag, don't roll for it
}
return false; // Item is not equipped or in bags, roll for it
}

View File

@ -22,7 +22,7 @@ public:
bool Execute(Event event) override;
protected:
RollVote CalculateRollVote(ItemTemplate const* proto);
RollVote CalculateRollVote(ItemTemplate const* proto, ItemUsage usage = ITEM_USAGE_NONE);
};
bool CanBotUseToken(ItemTemplate const* proto, Player* bot);

View File

@ -153,9 +153,8 @@ ItemUsage ItemUsageValue::Calculate()
// Need to add something like free bagspace or item value.
if (proto->SellPrice > 0)
{
if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound)
if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound && proto->Bonding != BIND_WHEN_PICKED_UP)
return ITEM_USAGE_AH;
else
return ITEM_USAGE_VENDOR;
}

View File

@ -620,7 +620,10 @@ bool PlayerbotAIConfig::Initialize()
// SPP automation
freeMethodLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.FreeMethodLoot", false);
lootRollLevel = sConfigMgr->GetOption<int32>("AiPlayerbot.LootRollLevel", 1);
lootNeedRollLevel = sConfigMgr->GetOption<int32>("AiPlayerbot.LootNeedRollLevel", 1);
lootRollRecipe = sConfigMgr->GetOption<bool>("AiPlayerbot.LootRollRecipe", false);
lootRollDisenchant = sConfigMgr->GetOption<bool>("AiPlayerbot.LootRollDisenchant", false);
lootGreedRollLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.LootGreedRollLevel", false);
autoPickReward = sConfigMgr->GetOption<std::string>("AiPlayerbot.AutoPickReward", "yes");
autoEquipUpgradeLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoEquipUpgradeLoot", true);
equipUpgradeThreshold = sConfigMgr->GetOption<float>("AiPlayerbot.EquipUpgradeThreshold", 1.1f);

View File

@ -346,7 +346,10 @@ public:
uint32 botActiveAloneSmartScaleWhenMaxLevel;
bool freeMethodLoot;
int32 lootRollLevel;
int32 lootNeedRollLevel;
bool lootGreedRollLevel;
bool lootRollRecipe;
bool lootRollDisenchant;
std::string autoPickReward;
bool autoEquipUpgradeLoot;
float equipUpgradeThreshold;