summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThorbjørn Lindeijer <bjorn@lindeijer.nl>2025-06-17 15:35:55 +0200
committerThorbjørn Lindeijer <bjorn@lindeijer.nl>2025-06-23 16:57:49 +0200
commite2042f2b6684e21b39214ce422212720b4374e1b (patch)
treeca10aa99e61891a906c6e07c6561c74bbca3095c
parentf79ea07b18b0a83594b86df0714caccfbd9b20a0 (diff)
downloadmana-e2042f2b6684e21b39214ce422212720b4374e1b.tar.gz
mana-e2042f2b6684e21b39214ce422212720b4374e1b.tar.bz2
mana-e2042f2b6684e21b39214ce422212720b4374e1b.tar.xz
mana-e2042f2b6684e21b39214ce422212720b4374e1b.zip
Apply quest effects to NPCs
The `TmwAthena::PlayerHandler` now handles the quest variables and applies the active quest effects to NPCs. They are updated when variables change or the map changes.
-rw-r--r--src/net/tmwa/beinghandler.cpp8
-rw-r--r--src/net/tmwa/playerhandler.cpp80
-rw-r--r--src/net/tmwa/playerhandler.h16
-rw-r--r--src/resources/questdb.cpp39
-rw-r--r--src/resources/questdb.h41
5 files changed, 177 insertions, 7 deletions
diff --git a/src/net/tmwa/beinghandler.cpp b/src/net/tmwa/beinghandler.cpp
index c5979e9f..c7debd5e 100644
--- a/src/net/tmwa/beinghandler.cpp
+++ b/src/net/tmwa/beinghandler.cpp
@@ -33,9 +33,9 @@
#include "playerrelations.h"
#include "net/net.h"
-#include "net/playerhandler.h"
#include "net/tmwa/messagein.h"
#include "net/tmwa/messageout.h"
+#include "net/tmwa/playerhandler.h"
#include "net/tmwa/protocol.h"
#include "resources/emotedb.h"
@@ -107,6 +107,12 @@ static Being *createBeing(int id, short job)
outMsg.writeInt32(id);
}
+ if (type == ActorSprite::NPC)
+ {
+ auto playerHandler = static_cast<TmwAthena::PlayerHandler*>(Net::getPlayerHandler());
+ playerHandler->applyQuestStatusEffects(being);
+ }
+
return being;
}
diff --git a/src/net/tmwa/playerhandler.cpp b/src/net/tmwa/playerhandler.cpp
index f6f6ef41..14477e7b 100644
--- a/src/net/tmwa/playerhandler.cpp
+++ b/src/net/tmwa/playerhandler.cpp
@@ -21,6 +21,8 @@
#include "net/tmwa/playerhandler.h"
+#include "actorspritemanager.h"
+#include "being.h"
#include "client.h"
#include "configuration.h"
#include "game.h"
@@ -152,10 +154,14 @@ PlayerHandler::PlayerHandler()
SMSG_PLAYER_STAT_UPDATE_6,
SMSG_PLAYER_ARROW_MESSAGE,
SMSG_MAP_MASK,
+ SMSG_QUEST_SET_VAR,
+ SMSG_QUEST_PLAYER_VARS,
0
};
handledMessages = _messages;
playerHandler = this;
+
+ listen(Event::GameChannel);
}
void PlayerHandler::handleMessage(MessageIn &msg)
@@ -514,6 +520,30 @@ void PlayerHandler::handleMessage(MessageIn &msg)
map->setMask(mask);
}
break;
+
+ case SMSG_QUEST_SET_VAR:
+ {
+ int variable = msg.readInt16();
+ int value = msg.readInt32();
+ mQuestVars.set(variable, value);
+ updateQuestStatusEffects();
+ break;
+ }
+
+ case SMSG_QUEST_PLAYER_VARS:
+ {
+ msg.readInt16(); // length
+ mQuestVars.clear();
+ unsigned int count = (msg.getLength() - 4) / 6;
+ for (unsigned int i = 0; i < count; ++i)
+ {
+ int variable = msg.readInt16();
+ int value = msg.readInt32();
+ mQuestVars.set(variable, value);
+ }
+ updateQuestStatusEffects();
+ break;
+ }
}
}
@@ -664,4 +694,54 @@ Vector PlayerHandler::getPixelsPerSecondMoveSpeed(const Vector &speed, Map *map)
return pixelsPerSecond;
}
+void PlayerHandler::event(Event::Channel channel, const Event &event)
+{
+ if (channel == Event::GameChannel)
+ {
+ if (event.getType() == Event::MapLoaded)
+ {
+ updateQuestStatusEffects();
+ }
+ }
+}
+
+void PlayerHandler::applyQuestStatusEffects(Being *npc)
+{
+ const auto npcId = npc->getSubType();
+ const auto effect = mActiveQuestEffects.get(npcId);
+ if (effect != 0)
+ npc->setStatusEffect(effect, true);
+}
+
+void PlayerHandler::updateQuestStatusEffects()
+{
+ auto game = Game::instance();
+ if (!game)
+ return;
+
+ const auto &currentMapName = game->getCurrentMapName();
+ auto updatedQuestEffects = QuestDB::getActiveEffects(mQuestVars, currentMapName);
+
+ // Loop over all NPCs, disabling no longer active effects and enabling new ones
+ for (auto actor : actorSpriteManager->getAll()) {
+ if (actor->getType() != ActorSprite::NPC)
+ continue;
+
+ auto *npc = static_cast<Being *>(actor);
+ const auto npcId = npc->getSubType();
+ const auto oldEffect = mActiveQuestEffects.get(npcId);
+ const auto newEffect = updatedQuestEffects.get(npcId);
+
+ if (oldEffect != newEffect)
+ {
+ if (oldEffect != 0)
+ npc->setStatusEffect(oldEffect, false);
+ if (newEffect != 0)
+ npc->setStatusEffect(newEffect, true);
+ }
+ }
+
+ std::swap(mActiveQuestEffects, updatedQuestEffects);
+}
+
} // namespace TmwAthena
diff --git a/src/net/tmwa/playerhandler.h b/src/net/tmwa/playerhandler.h
index f1a67e94..6d2da1af 100644
--- a/src/net/tmwa/playerhandler.h
+++ b/src/net/tmwa/playerhandler.h
@@ -26,9 +26,12 @@
#include "net/tmwa/messagehandler.h"
+#include "resources/questdb.h"
+
namespace TmwAthena {
-class PlayerHandler final : public MessageHandler, public Net::PlayerHandler
+class PlayerHandler final : public MessageHandler, public Net::PlayerHandler,
+ public EventListener
{
public:
PlayerHandler();
@@ -63,6 +66,17 @@ class PlayerHandler final : public MessageHandler, public Net::PlayerHandler
bool usePixelPrecision() override
{ return false; }
+
+ // EventListener
+ void event(Event::Channel channel, const Event &event) override;
+
+ void applyQuestStatusEffects(Being *npc);
+
+ private:
+ void updateQuestStatusEffects();
+
+ QuestVars mQuestVars;
+ QuestEffectMap mActiveQuestEffects;
};
} // namespace TmwAthena
diff --git a/src/resources/questdb.cpp b/src/resources/questdb.cpp
index 207389e7..13bc2112 100644
--- a/src/resources/questdb.cpp
+++ b/src/resources/questdb.cpp
@@ -21,7 +21,9 @@
#include "resources/questdb.h"
#include "log.h"
+#include <algorithm>
#include <unordered_map>
+#include <utility>
namespace QuestDB {
@@ -42,11 +44,11 @@ void readQuestVarNode(XML::Node node, const std::string &filename)
{
QuestEffect &effect = quest.effects.emplace_back();
child.attribute("map", effect.map);
- child.attribute("npc", effect.npc);
- child.attribute("effect", effect.effect);
+ child.attribute("npc", effect.npcId);
+ child.attribute("effect", effect.statusEffectId);
child.attribute("value", effect.values);
- if (effect.map.empty() || effect.npc == 0 || effect.effect == 0 || effect.values.empty())
+ if (effect.map.empty() || effect.npcId == 0 || effect.statusEffectId == 0 || effect.values.empty())
{
logger->log("Warning: effect node for var %d is missing required attributes", varId);
}
@@ -108,4 +110,35 @@ const Quest &get(int var)
return it == quests.end() ? emptyQuest : it->second;
}
+// 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] : quests)
+ {
+ auto value = questVars.get(var);
+
+ for (auto &effect : quest.effects)
+ {
+ if (baseName(effect.map) != mapName)
+ continue;
+ if (std::find(effect.values.begin(), effect.values.end(), value) == effect.values.end())
+ continue;
+
+ activeEffects.set(effect.npcId, effect.statusEffectId);
+ }
+ }
+
+ return activeEffects;
+}
+
} // namespace QuestDB
diff --git a/src/resources/questdb.h b/src/resources/questdb.h
index 5c03a255..23817b81 100644
--- a/src/resources/questdb.h
+++ b/src/resources/questdb.h
@@ -22,17 +22,51 @@
#include "utils/xml.h"
+#include <map>
#include <string>
#include <vector>
+/**
+ * A map that returns a default value for non-existent keys.
+ */
+template<typename Key, typename Value, Value def = Value()>
+class MapWithDefault
+{
+public:
+ void set(Key key, Value value)
+ {
+ mVars[key] = value;
+ }
+
+ Value get(Key key) const
+ {
+ auto it = mVars.find(key);
+ return it != mVars.end() ? it->second : def;
+ }
+
+ void clear()
+ {
+ mVars.clear();
+ }
+
+private:
+ std::map<Key, Value> mVars;
+};
+
struct QuestEffect
{
std::vector<int> values; // Quest variable values to which the effect applies
std::string map; // Map name the NPC is located on
- int npc = 0; // NPC ID
- int effect = 0; // Effect ID
+ int npcId = 0;
+ int statusEffectId = 0;
};
+// Map of quest variables, from variable ID to value
+using QuestVars = MapWithDefault<int, int>;
+
+// Map of quest effects, from NPC ID to status effect ID
+using QuestEffectMap = MapWithDefault<int, int>;
+
enum class QuestRowType
{
Text,
@@ -74,4 +108,7 @@ namespace QuestDB
void unload();
const Quest &get(int var);
+
+ QuestEffectMap getActiveEffects(const QuestVars &questVars,
+ const std::string &mapName);
};