mirror of
https://github.com/mod-playerbots/mod-playerbots.git
synced 2026-02-28 06:15:55 +00:00
# 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>
469 lines
14 KiB
C++
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;
|
|
};
|