/*
* The Mana Client
* Copyright (C) 2025 The Mana Developers
*
* This file is part of The Mana Client.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "resources/questdb.h"
#include "log.h"
#include
#include
#include
namespace QuestDB {
// The quests are stored in a map using their variable ID as the key
static std::unordered_map quests;
// Helper function to check if a container contains a value
template
static bool contains(const Container &container, const Value &value)
{
return std::find(container.begin(), container.end(), value) != container.end();
}
void init()
{
unload();
}
void readQuestVarNode(XML::Node node, const std::string &filename)
{
int varId = 0;
if (!node.attribute("id", varId))
return;
Quest &quest = quests[varId];
for (auto child : node.children())
{
if (child.name() == "effect")
{
QuestEffect &effect = quest.effects.emplace_back();
child.attribute("map", effect.map);
child.attribute("npc", effect.npcId);
child.attribute("effect", effect.statusEffectId);
child.attribute("value", effect.values);
if (effect.map.empty() || effect.npcId == 0 || effect.statusEffectId == 0 || effect.values.empty())
{
Log::warn("effect node for var %d is missing required attributes", varId);
}
}
else if (child.name() == "quest")
{
QuestState &state = quest.states.emplace_back();
child.attribute("name", state.name);
child.attribute("group", state.group);
child.attribute("incomplete", state.incomplete);
child.attribute("complete", state.complete);
if (state.incomplete.empty() && state.complete.empty())
{
Log::warn("quest node for var %d ('%s') has neither 'complete' nor 'incomplete' values",
varId, state.name.c_str());
continue;
}
for (auto questChild : child.children())
{
QuestRowType rowType;
std::string_view tag = questChild.name();
if (tag == "text")
rowType = QuestRowType::Text;
else if (tag == "name")
rowType = QuestRowType::Name;
else if (tag == "reward")
rowType = QuestRowType::Reward;
else if (tag == "questgiver" || tag == "giver")
rowType = QuestRowType::Giver;
else if (tag == "coordinates")
rowType = QuestRowType::Coordinates;
else if (tag == "npc")
rowType = QuestRowType::NPC;
else
{
Log::warn("unknown quest row type '%s' for var %d ('%s')",
tag.data(), varId, state.name.c_str());
continue;
}
QuestRow &row = state.rows.emplace_back(rowType);
row.text = questChild.textContent();
if (rowType == QuestRowType::Coordinates)
{
questChild.attribute("x", row.x);
questChild.attribute("y", row.y);
}
}
}
}
}
void unload()
{
quests.clear();
}
bool hasQuests()
{
return !quests.empty();
}
// In quests, the map name may include the file extension. This is discouraged
// but supported for compatibility.
static std::string_view baseName(const std::string &fileName)
{
auto pos = fileName.find_last_of('.');
return pos == std::string::npos ? fileName : std::string_view(fileName.data(), pos);
}
QuestEffectMap getActiveEffects(const QuestVars &questVars,
const std::string &mapName)
{
QuestEffectMap activeEffects;
for (auto &[var, quest] : std::as_const(quests))
{
auto value = questVars.get(var);
for (auto &effect : quest.effects)
{
if (baseName(effect.map) != mapName)
continue;
if (!contains(effect.values, value))
continue;
activeEffects.set(effect.npcId, effect.statusEffectId);
}
}
return activeEffects;
}
std::vector getQuestsEntries(const QuestVars &questVars,
bool skipCompleted)
{
std::vector activeQuests;
for (auto &[varId, quest] : std::as_const(quests))
{
auto value = questVars.get(varId);
for (auto &state : quest.states)
{
bool matchesIncomplete = contains(state.incomplete, value);
bool matchesComplete = contains(state.complete, value);
if (skipCompleted && matchesComplete)
continue;
if (matchesIncomplete || matchesComplete)
{
QuestEntry &entry = activeQuests.emplace_back();
entry.varId = varId;
entry.completed = matchesComplete;
entry.state = &state;
}
}
}
return activeQuests;
}
static std::pair countQuestEntries(const Quest &quest, int value)
{
int totalEntries = 0;
int completedEntries = 0;
for (const auto &state : quest.states)
{
bool matchesIncomplete = contains(state.incomplete, value);
bool matchesComplete = contains(state.complete, value);
if (matchesIncomplete || matchesComplete)
{
totalEntries++;
if (matchesComplete)
completedEntries++;
}
}
return { totalEntries, completedEntries };
}
QuestChange questChange(int varId, int oldValue, int newValue)
{
if (newValue == oldValue)
return QuestChange::None;
auto questIt = quests.find(varId);
if (questIt == quests.end())
return QuestChange::None;
const Quest &quest = questIt->second;
auto [oldQuestEntries, oldCompletedEntries] = countQuestEntries(quest, oldValue);
auto [newQuestEntries, newCompletedEntries] = countQuestEntries(quest, newValue);
if (newCompletedEntries > oldCompletedEntries)
return QuestChange::Completed;
if (newQuestEntries > oldQuestEntries)
return QuestChange::New;
return QuestChange::None;
}
} // namespace QuestDB