Files
mod-playerbots/src/Ai/Base/Value/QuestValues.cpp
privatecore 610fdc16d7 Fix bug with GetCreature + GetGameObject = use ObjectAccessor's methods instead (#2105)
# Pull Request

https://en.cppreference.com/w/cpp/algorithm/equal_range.html
> second is an iterator to the first element of the range [first, last)
ordered after value (or last if no such element is found).

The original code uses `return bounds.second->second`, which causes the
wrong creature/gameobject to be returned. Instead, both methods
(`GetCreature` and `GetGameObject`) now utilize ObjectAccessor's methods
to retrieve the correct entities. These built-in methods offer a safer
way to access objects. Additionally, `GetUnit` no longer includes
redundant creature processing before checks and now has the same logic
as the `ObjectAccessor::GetUnit` method.

Furthermore, `GuidPosition::isDead` method has been renamed to
`GuidPosition::IsCreatureOrGOAccessible` and updated, as it is used only
for creatures (NOT units) and gameobjects.

---

## 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.

---

## How to Test the Changes

The behavior has not changed after all.

## Complexity & Impact

- Does this change add new decision branches?
    - [x] No
    - [ ] 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?
    - [x] No
    - [ ] Yes (**explain why**)

If this introduces more advanced or AI-heavy logic:

- [ ] Lightweight mode remains the default
- [ ] 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**)

If yes, please specify:

- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.)
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation)
- Which parts of the change were influenced or generated
- Whether the result was manually reviewed and adapted

AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
Any AI-influenced changes must be verified against existing CORE and PB
logic. We expect contributors to be honest
about what they do and do not understand.

---

## 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.

---------

Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com>
2026-02-08 09:36:56 -08:00

469 lines
14 KiB
C++

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "QuestValues.h"
#include "MapMgr.h"
#include "Playerbots.h"
#include "SharedValueContext.h"
// What kind of a relation does this entry have with this quest.
entryQuestRelationMap EntryQuestRelationMapValue::Calculate()
{
entryQuestRelationMap rMap;
for (auto relation : *sObjectMgr->GetCreatureQuestRelationMap())
rMap[relation.first][relation.second] |= (int)QuestRelationFlag::questGiver;
for (auto relation : *sObjectMgr->GetCreatureQuestInvolvedRelationMap())
rMap[relation.first][relation.second] |= (int)QuestRelationFlag::questTaker;
for (auto relation : *sObjectMgr->GetGOQuestRelationMap())
rMap[-(int32)relation.first][relation.second] |= (int)QuestRelationFlag::questGiver;
for (auto relation : *sObjectMgr->GetGOQuestInvolvedRelationMap())
rMap[-(int32)relation.first][relation.second] |= (int)QuestRelationFlag::questGiver;
// Quest objectives
ObjectMgr::QuestMap const& questMap = sObjectMgr->GetQuestTemplates();
for (auto& questItr : questMap)
{
uint32 questId = questItr.first;
Quest* quest = questItr.second;
for (uint32 objective = 0; objective < QUEST_OBJECTIVES_COUNT; objective++)
{
uint32 relationFlag = 1 << objective;
// Kill objective
if (quest->RequiredNpcOrGo[objective])
rMap[quest->RequiredNpcOrGo[objective]][questId] |= relationFlag;
// Loot objective
if (quest->RequiredItemId[objective])
{
for (auto& entry : GAI_VALUE2(std::vector<int32>, "item drop list", quest->RequiredItemId[objective]))
rMap[entry][questId] |= relationFlag;
}
}
}
return rMap;
}
// Get all the objective entries for a specific quest.
void FindQuestObjectData::GetObjectiveEntries()
{
relationMap = GAI_VALUE(entryQuestRelationMap, "entry quest relation");
}
// Data worker. Checks for a specific creature what quest they are needed for and puts them in the proper place in the
// quest map.
void FindQuestObjectData::operator()(CreatureData const& creData)
{
uint32 entry = creData.id1;
for (auto& relation : relationMap[entry])
{
uint32 questId = relation.first;
uint32 flag = relation.second;
data[questId][flag][entry].push_back(GuidPosition(creData));
}
}
// GameObject data worker. Checks for a specific gameObject what quest they are needed for and puts them in the proper
// place in the quest map.
void FindQuestObjectData::operator()(GameObjectData const& goData)
{
int32 entry = goData.id * -1;
for (auto& relation : relationMap[entry])
{
uint32 questId = relation.first;
uint32 flag = relation.second;
data[questId][flag][entry].push_back(GuidPosition(goData));
}
}
// Goes past all creatures and gameobjects and creatures the full quest guid map.
questGuidpMap QuestGuidpMapValue::Calculate()
{
FindQuestObjectData worker;
for (auto const& itr : sObjectMgr->GetAllCreatureData())
worker(itr.second);
for (auto const& itr : sObjectMgr->GetAllGOData())
worker(itr.second);
return worker.GetResult();
}
// Selects all questgivers for a specific level (range).
questGiverMap QuestGiversValue::Calculate()
{
uint32 level = 0;
std::string const q = getQualifier();
bool hasQualifier = !q.empty();
if (hasQualifier)
level = stoi(q);
questGuidpMap questMap = GAI_VALUE(questGuidpMap, "quest guidp map");
questGiverMap guidps;
for (auto& qPair : questMap)
{
for (auto& entry : qPair.second[(int)QuestRelationFlag::questGiver])
{
for (auto& guidp : entry.second)
{
uint32 questId = qPair.first;
if (hasQualifier)
{
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (quest && (level < quest->GetMinLevel() || (int)level > quest->GetQuestLevel() + 10))
continue;
}
guidps[questId].push_back(guidp);
}
}
}
return guidps;
}
std::vector<GuidPosition> ActiveQuestGiversValue::Calculate()
{
questGiverMap qGivers = GAI_VALUE2(questGiverMap, "quest givers", bot->GetLevel());
std::vector<GuidPosition> retQuestGivers;
for (auto& qGiver : qGivers)
{
uint32 questId = qGiver.first;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
{
continue;
}
if (!bot->CanTakeQuest(quest, false))
continue;
QuestStatus status = bot->GetQuestStatus(questId);
if (status != QUEST_STATUS_NONE)
continue;
for (auto& guidp : qGiver.second)
{
CreatureTemplate const* creatureTemplate = guidp.GetCreatureTemplate();
if (creatureTemplate)
{
if (bot->GetFactionReactionTo(bot->GetFactionTemplateEntry(),
sFactionTemplateStore.LookupEntry(creatureTemplate->faction)) <
REP_FRIENDLY)
continue;
}
if (!guidp.IsCreatureOrGOAccessible())
continue;
retQuestGivers.push_back(guidp);
}
}
return retQuestGivers;
}
std::vector<GuidPosition> ActiveQuestTakersValue::Calculate()
{
questGuidpMap questMap = GAI_VALUE(questGuidpMap, "quest guidp map");
std::vector<GuidPosition> retQuestTakers;
QuestStatusMap& questStatusMap = bot->getQuestStatusMap();
for (auto& questStatus : questStatusMap)
{
uint32 questId = questStatus.first;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
{
continue;
}
QuestStatus status = questStatus.second.Status;
if ((status != QUEST_STATUS_COMPLETE || bot->GetQuestRewardStatus(questId)) &&
(!quest->IsAutoComplete() || !bot->CanTakeQuest(quest, false)))
continue;
auto q = questMap.find(questId);
if (q == questMap.end())
continue;
auto qt = q->second.find((int)QuestRelationFlag::questTaker);
if (qt == q->second.end())
continue;
for (auto& entry : qt->second)
{
if (entry.first > 0)
{
if (CreatureTemplate const* info = sObjectMgr->GetCreatureTemplate(entry.first))
{
if (bot->GetFactionReactionTo(bot->GetFactionTemplateEntry(),
sFactionTemplateStore.LookupEntry(info->faction)) < REP_FRIENDLY)
continue;
}
}
for (auto& guidp : entry.second)
{
if (!guidp.IsCreatureOrGOAccessible())
continue;
retQuestTakers.push_back(guidp);
}
}
}
return retQuestTakers;
}
std::vector<GuidPosition> ActiveQuestObjectivesValue::Calculate()
{
questGuidpMap questMap = GAI_VALUE(questGuidpMap, "quest guidp map");
std::vector<GuidPosition> retQuestObjectives;
QuestStatusMap& questStatusMap = bot->getQuestStatusMap();
for (auto& questStatus : questStatusMap)
{
uint32 questId = questStatus.first;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
{
continue;
}
QuestStatusData statusData = questStatus.second;
if (statusData.Status != QUEST_STATUS_INCOMPLETE)
continue;
for (uint32 objective = 0; objective < QUEST_OBJECTIVES_COUNT; objective++)
{
if (quest->RequiredItemCount[objective])
{
uint32 reqCount = quest->RequiredItemCount[objective];
uint32 hasCount = statusData.ItemCount[objective];
if (!reqCount || hasCount >= reqCount)
continue;
}
if (quest->RequiredNpcOrGoCount[objective])
{
uint32 reqCount = quest->RequiredItemCount[objective];
uint32 hasCount = statusData.CreatureOrGOCount[objective];
if (!reqCount || hasCount >= reqCount)
continue;
}
auto q = questMap.find(questId);
if (q == questMap.end())
continue;
auto qt = q->second.find((int)QuestRelationFlag(1 << objective));
if (qt == q->second.end())
continue;
for (auto& entry : qt->second)
{
for (auto& guidp : entry.second)
{
if (!guidp.IsCreatureOrGOAccessible())
continue;
retQuestObjectives.push_back(guidp);
}
}
}
}
return retQuestObjectives;
}
uint8 FreeQuestLogSlotValue::Calculate()
{
uint8 numQuest = 0;
for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (!questId)
continue;
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
continue;
numQuest++;
}
return MAX_QUEST_LOG_SIZE - numQuest;
}
uint32 DialogStatusValue::getDialogStatus(Player* bot, int32 questgiver, uint32 questId)
{
uint32 dialogStatus = DIALOG_STATUS_NONE;
QuestRelationBounds rbounds; // QuestRelations (quest-giver)
QuestRelationBounds irbounds; // InvolvedRelations (quest-finisher)
if (questgiver > 0)
{
rbounds = sObjectMgr->GetCreatureQuestRelationBounds(questgiver);
irbounds = sObjectMgr->GetCreatureQuestInvolvedRelationBounds(questgiver);
}
else
{
rbounds = sObjectMgr->GetGOQuestRelationBounds(questgiver * -1);
irbounds = sObjectMgr->GetGOQuestInvolvedRelationBounds(questgiver * -1);
}
// Check markings for quest-finisher
for (QuestRelations::const_iterator itr = irbounds.first; itr != irbounds.second; ++itr)
{
if (questId && itr->second != questId)
continue;
Quest const* pQuest = sObjectMgr->GetQuestTemplate(itr->second);
if (!pQuest)
{
continue;
}
uint32 dialogStatusNew = DIALOG_STATUS_NONE;
QuestStatus status = bot->GetQuestStatus(itr->second);
if ((status == QUEST_STATUS_COMPLETE && !bot->GetQuestRewardStatus(itr->second)) ||
(pQuest->IsAutoComplete() && bot->CanTakeQuest(pQuest, false)))
{
if (pQuest->IsAutoComplete() && pQuest->IsRepeatable())
{
dialogStatusNew = DIALOG_STATUS_REWARD_REP;
}
else
{
dialogStatusNew = DIALOG_STATUS_REWARD2;
}
}
else if (status == QUEST_STATUS_INCOMPLETE)
{
dialogStatusNew = DIALOG_STATUS_INCOMPLETE;
}
if (dialogStatusNew > dialogStatus)
{
dialogStatus = dialogStatusNew;
}
}
// check markings for quest-giver
for (QuestRelations::const_iterator itr = rbounds.first; itr != rbounds.second; ++itr)
{
if (questId && itr->second != questId)
continue;
Quest const* pQuest = sObjectMgr->GetQuestTemplate(itr->second);
if (!pQuest)
{
continue;
}
uint32 dialogStatusNew = DIALOG_STATUS_NONE;
QuestStatus status = bot->GetQuestStatus(itr->second);
if (status == QUEST_STATUS_NONE) // For all other cases the mark is handled either at some place else, or with
// involved-relations already
{
if (bot->CanSeeStartQuest(pQuest))
{
if (bot->SatisfyQuestLevel(pQuest, false))
{
int32 lowLevelDiff = sWorld->getIntConfig(CONFIG_QUEST_LOW_LEVEL_HIDE_DIFF);
if (pQuest->IsAutoComplete() ||
(pQuest->IsRepeatable() &&
bot->getQuestStatusMap()[itr->second].Status == QUEST_STATUS_REWARDED))
{
dialogStatusNew = DIALOG_STATUS_REWARD_REP;
}
else if (lowLevelDiff < 0 || bot->GetLevel() <= bot->GetQuestLevel(pQuest) + uint32(lowLevelDiff))
{
dialogStatusNew = DIALOG_STATUS_AVAILABLE;
}
else
{
dialogStatusNew = DIALOG_STATUS_LOW_LEVEL_AVAILABLE;
}
}
else
{
dialogStatusNew = DIALOG_STATUS_UNAVAILABLE;
}
}
}
if (dialogStatusNew > dialogStatus)
{
dialogStatus = dialogStatusNew;
}
}
return dialogStatus;
}
uint32 DialogStatusValue::Calculate() { return getDialogStatus(bot, stoi(getQualifier())); }
uint32 DialogStatusQuestValue::Calculate()
{
return getDialogStatus(bot, getMultiQualifier(getQualifier(), 0), getMultiQualifier(getQualifier(), 1));
}
bool CanAcceptQuestValue::Calculate()
{
return AI_VALUE2(uint32, "dialog status", getQualifier()) == DIALOG_STATUS_AVAILABLE;
};
bool CanAcceptQuestLowLevelValue::Calculate()
{
uint32 dialogStatus = AI_VALUE2(uint32, "dialog status", getQualifier());
return dialogStatus == DIALOG_STATUS_LOW_LEVEL_AVAILABLE;
};
bool CanTurnInQuestValue::Calculate()
{
uint32 dialogStatus = AI_VALUE2(uint32, "dialog status", getQualifier());
return dialogStatus == DIALOG_STATUS_REWARD2 || dialogStatus == DIALOG_STATUS_REWARD ||
dialogStatus == DIALOG_STATUS_REWARD_REP;
};