summaryrefslogtreecommitdiff
path: root/src/resources/questdb.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/resources/questdb.cpp')
-rw-r--r--src/resources/questdb.cpp232
1 files changed, 232 insertions, 0 deletions
diff --git a/src/resources/questdb.cpp b/src/resources/questdb.cpp
new file mode 100644
index 00000000..1424c20e
--- /dev/null
+++ b/src/resources/questdb.cpp
@@ -0,0 +1,232 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "resources/questdb.h"
+#include "log.h"
+
+#include <algorithm>
+#include <unordered_map>
+#include <utility>
+
+namespace QuestDB {
+
+// The quests are stored in a map using their variable ID as the key
+static std::unordered_map<int, Quest> quests;
+
+// Helper function to check if a container contains a value
+template<typename Container, typename Value>
+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<QuestEntry> getQuestsEntries(const QuestVars &questVars,
+ bool skipCompleted)
+{
+ std::vector<QuestEntry> 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<int, int> 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