diff --git a/config.lua b/config.lua index c692d20..2cf2d9c 100644 --- a/config.lua +++ b/config.lua @@ -37,7 +37,7 @@ replaceKickOnLogin = true maxPacketsPerSecond = -1 autoStackCumulatives = false moneyRate = 1 -clientVersion = 780 +clientVersion = 792 -- Deaths -- NOTE: Leave deathLosePercent as -1 if you want to use the default diff --git a/data/XML/quests.xml b/data/XML/quests.xml new file mode 100644 index 0000000..fed047e --- /dev/null +++ b/data/XML/quests.xml @@ -0,0 +1,1167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/talkactions/scripts/reload.lua b/data/talkactions/scripts/reload.lua index 1dd9c14..57e8f90 100644 --- a/data/talkactions/scripts/reload.lua +++ b/data/talkactions/scripts/reload.lua @@ -32,7 +32,10 @@ local reloadTypes = { ["npc"] = { targetType = RELOAD_TYPE_NPCS, name = "npcs" }, ["npcs"] = { targetType = RELOAD_TYPE_NPCS, name = "npcs" }, - + + ["quest"] = { targetType = RELOAD_TYPE_QUESTS, name = "quests" }, + ["quests"] = { targetType = RELOAD_TYPE_QUESTS, name = "quests" }, + ["raid"] = { targetType = RELOAD_TYPE_RAIDS, name = "raids" }, ["raids"] = { targetType = RELOAD_TYPE_RAIDS, name = "raids" }, diff --git a/src/game.cpp b/src/game.cpp index c35f83a..028e1a4 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -104,6 +104,8 @@ void Game::setGameState(GameState_t newState) raids.loadFromXml(); raids.startup(); + quests.loadFromXml(); + loadMotdNum(); loadPlayersRecord(); @@ -2914,6 +2916,31 @@ void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit) } } +void Game::playerShowQuestLog(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->sendQuestLog(); +} + +void Game::playerShowQuestLine(uint32_t playerId, uint16_t questId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Quest* quest = quests.getQuestByID(questId); + if (!quest) { + return; + } + + player->sendQuestLine(quest); +} + void Game::playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, const std::string& receiver, const std::string& text) { @@ -4599,6 +4626,8 @@ bool Game::reload(ReloadTypes_t reloadType) Npcs::reload(); return true; } + + case RELOAD_TYPE_QUESTS: return quests.reload(); case RELOAD_TYPE_RAIDS: return raids.reload() && raids.startup(); case RELOAD_TYPE_SPELLS: { @@ -4636,6 +4665,7 @@ bool Game::reload(ReloadTypes_t reloadType) raids.reload() && raids.startup(); g_talkActions->reload(); Item::items.reload(); + quests.reload(); g_globalEvents->reload(); g_events->load(); g_chat->load(); diff --git a/src/game.h b/src/game.h index e7132c4..2182dca 100644 --- a/src/game.h +++ b/src/game.h @@ -31,6 +31,7 @@ #include "raids.h" #include "npc.h" #include "wildcardtree.h" +#include "quests.h" class ServiceManager; class Creature; @@ -387,6 +388,8 @@ class Game void playerRequestRemoveVip(uint32_t playerId, uint32_t guid); void playerTurn(uint32_t playerId, Direction dir); void playerRequestOutfit(uint32_t playerId); + void playerShowQuestLog(uint32_t playerId); + void playerShowQuestLine(uint32_t playerId, uint16_t questId); void playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, const std::string& receiver, const std::string& text); void playerChangeOutfit(uint32_t playerId, Outfit_t outfit); @@ -493,6 +496,7 @@ class Game Groups groups; Map map; Raids raids; + Quests quests; protected: bool playerSaySpell(Player* player, SpeakClasses type, const std::string& text); diff --git a/src/iologindata.cpp b/src/iologindata.cpp index 61e49ce..8501bd8 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -495,7 +495,7 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) query << "SELECT `key`, `value` FROM `player_storage` WHERE `player_id` = " << player->getGUID(); if ((result = db->storeQuery(query.str()))) { do { - player->addStorageValue(result->getNumber("key"), result->getNumber("value")); + player->addStorageValue(result->getNumber("key"), result->getNumber("value"), true); } while (result->next()); } diff --git a/src/player.cpp b/src/player.cpp index 7335037..90a303b 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -507,7 +507,7 @@ uint16_t Player::getLookCorpse() const } } -void Player::addStorageValue(const uint32_t key, const int32_t value) +void Player::addStorageValue(const uint32_t key, const int32_t value, const bool isLogin/* = false*/) { if (IS_IN_KEYRANGE(key, RESERVED_RANGE)) { if (IS_IN_KEYRANGE(key, OUTFITS_RANGE)) { @@ -524,8 +524,20 @@ void Player::addStorageValue(const uint32_t key, const int32_t value) } if (value != -1) { + int32_t oldValue; + getStorageValue(key, oldValue); + storageMap[key] = value; - } else { + + if (!isLogin && g_game.getClientVersion() >= CLIENT_VERSION_790) { + auto currentFrameTime = g_dispatcher.getDispatcherCycle(); + if (lastQuestlogUpdate != currentFrameTime && g_game.quests.isQuestStorage(key, value, oldValue)) { + lastQuestlogUpdate = currentFrameTime; + sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your questlog has been updated."); + } + } + } + else { storageMap.erase(key); } } diff --git a/src/player.h b/src/player.h index 7a47cf3..15703a4 100644 --- a/src/player.h +++ b/src/player.h @@ -273,7 +273,7 @@ class Player final : public Creature, public Cylinder bool canOpenCorpse(uint32_t ownerId) const; - void addStorageValue(const uint32_t key, const int32_t value); + void addStorageValue(const uint32_t key, const int32_t value, const bool isLogin = false); bool getStorageValue(const uint32_t key, int32_t& value) const; void genReservedStorageRange(); @@ -854,6 +854,16 @@ class Player final : public Creature, public Cylinder client->sendOpenPrivateChannel(receiver); } } + void sendQuestLog() { + if (client) { + client->sendQuestLog(); + } + } + void sendQuestLine(const Quest* quest) { + if (client) { + client->sendQuestLine(quest); + } + } void sendOutfitWindow() { if (client) { client->sendOutfitWindow(); @@ -1012,6 +1022,7 @@ class Player final : public Creature, public Cylinder uint64_t experience = 0; uint64_t manaSpent = 0; uint64_t bankBalance = 0; + uint64_t lastQuestlogUpdate = 0; int64_t lastAttack = 0; int64_t lastFailedFollow = 0; int64_t lastPing; diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp index b93956e..7d1d50e 100644 --- a/src/protocolgame.cpp +++ b/src/protocolgame.cpp @@ -429,6 +429,8 @@ void ProtocolGame::parsePacket(NetworkMessage& msg) case 0xE6: parseBugReport(msg); break; case 0xE7: /* violation window */ break; case 0xE8: parseDebugAssert(msg); break; + case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break; + case 0xF1: parseQuestLine(msg); break; default: std::cout << "Player: " << player->getName() << " sent an unknown packet header: 0x" << std::hex << static_cast(recvbyte) << std::dec << "!" << std::endl; break; @@ -963,6 +965,12 @@ void ProtocolGame::parsePassPartyLeadership(NetworkMessage& msg) addGameTask(&Game::playerPassPartyLeadership, player->getID(), targetId); } +void ProtocolGame::parseQuestLine(NetworkMessage& msg) +{ + uint16_t questId = msg.get(); + addGameTask(&Game::playerShowQuestLine, player->getID(), questId); +} + void ProtocolGame::parseSeekInContainer(NetworkMessage& msg) { uint8_t containerId = msg.getByte(); @@ -1205,7 +1213,39 @@ void ProtocolGame::sendContainer(uint8_t cid, const Container* container, bool h writeToOutputBuffer(msg); } +void ProtocolGame::sendQuestLog() +{ + NetworkMessage msg; + msg.addByte(0xF0); + msg.add(g_game.quests.getQuestsCount(player)); + for (const Quest& quest : g_game.quests.getQuests()) { + if (quest.isStarted(player)) { + msg.add(quest.getID()); + msg.addString(quest.getName()); + msg.addByte(quest.isCompleted(player)); + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendQuestLine(const Quest* quest) +{ + NetworkMessage msg; + msg.addByte(0xF1); + msg.add(quest->getID()); + msg.addByte(quest->getMissionsCount(player)); + + for (const Mission& mission : quest->getMissions()) { + if (mission.isStarted(player)) { + msg.addString(mission.getName(player)); + msg.addString(mission.getDescription(player)); + } + } + + writeToOutputBuffer(msg); +} void ProtocolGame::sendTradeItemRequest(const std::string& traderName, const Item* item, bool ack) { diff --git a/src/protocolgame.h b/src/protocolgame.h index 773c633..c581a5e 100644 --- a/src/protocolgame.h +++ b/src/protocolgame.h @@ -113,6 +113,8 @@ class ProtocolGame final : public Protocol void parseTextWindow(NetworkMessage& msg); void parseHouseWindow(NetworkMessage& msg); + void parseQuestLine(NetworkMessage& msg); + void parseInviteToParty(NetworkMessage& msg); void parseJoinParty(NetworkMessage& msg); void parseRevokePartyInvite(NetworkMessage& msg); @@ -156,6 +158,9 @@ class ProtocolGame final : public Protocol void sendCreatureTurn(const Creature* creature, uint32_t stackpos); void sendCreatureSay(const Creature* creature, SpeakClasses type, const std::string& text, const Position* pos = nullptr); + void sendQuestLog(); + void sendQuestLine(const Quest* quest); + void sendCancelWalk(); void sendChangeSpeed(const Creature* creature, uint32_t speed); void sendCancelTarget(); diff --git a/src/quests.cpp b/src/quests.cpp new file mode 100644 index 0000000..d28d9e7 --- /dev/null +++ b/src/quests.cpp @@ -0,0 +1,230 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * 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 + * (at your option) 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "quests.h" + +#include "pugicast.h" + +std::string Mission::getDescription(Player* player) const +{ + int32_t value; + player->getStorageValue(storageID, value); + + if (!mainDescription.empty()) { + std::string desc = mainDescription; + replaceString(desc, "|STATE|", std::to_string(value)); + replaceString(desc, "\\n", "\n"); + return desc; + } + + if (ignoreEndValue) { + for (int32_t current = endValue; current >= startValue; current--) { + if (value >= current) { + auto sit = descriptions.find(current); + if (sit != descriptions.end()) { + return sit->second; + } + } + } + } + else { + for (int32_t current = endValue; current >= startValue; current--) { + if (value == current) { + auto sit = descriptions.find(current); + if (sit != descriptions.end()) { + return sit->second; + } + } + } + } + return "An error has occurred, please contact a gamemaster."; +} + +bool Mission::isStarted(Player* player) const +{ + if (!player) { + return false; + } + + int32_t value; + if (!player->getStorageValue(storageID, value)) { + return false; + } + + if (value < startValue) { + return false; + } + + if (!ignoreEndValue && value > endValue) { + return false; + } + + return true; +} + +bool Mission::isCompleted(Player* player) const +{ + if (!player) { + return false; + } + + int32_t value; + if (!player->getStorageValue(storageID, value)) { + return false; + } + + if (ignoreEndValue) { + return value >= endValue; + } + + return value == endValue; +} + +std::string Mission::getName(Player* player) const +{ + if (isCompleted(player)) { + return name + " (completed)"; + } + return name; +} + +uint16_t Quest::getMissionsCount(Player* player) const +{ + uint16_t count = 0; + for (const Mission& mission : missions) { + if (mission.isStarted(player)) { + count++; + } + } + return count; +} + +bool Quest::isCompleted(Player* player) const +{ + for (const Mission& mission : missions) { + if (!mission.isCompleted(player)) { + return false; + } + } + return true; +} + +bool Quest::isStarted(Player* player) const +{ + if (!player) { + return false; + } + + int32_t value; + if (!player->getStorageValue(startStorageID, value) || value < startStorageValue) { + return false; + } + + return true; +} + +bool Quests::reload() +{ + quests.clear(); + return loadFromXml(); +} + +bool Quests::loadFromXml() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/quests.xml"); + if (!result) { + printXMLError("Error - Quests::loadFromXml", "data/XML/quests.xml", result); + return false; + } + + uint16_t id = 0; + for (auto questNode : doc.child("quests").children()) { + quests.emplace_back( + questNode.attribute("name").as_string(), + ++id, + pugi::cast(questNode.attribute("startstorageid").value()), + pugi::cast(questNode.attribute("startstoragevalue").value()) + ); + Quest& quest = quests.back(); + + for (auto missionNode : questNode.children()) { + std::string mainDescription = missionNode.attribute("description").as_string(); + + quest.missions.emplace_back( + missionNode.attribute("name").as_string(), + pugi::cast(missionNode.attribute("storageid").value()), + pugi::cast(missionNode.attribute("startvalue").value()), + pugi::cast(missionNode.attribute("endvalue").value()), + missionNode.attribute("ignoreendvalue").as_bool() + ); + Mission& mission = quest.missions.back(); + + if (mainDescription.empty()) { + for (auto missionStateNode : missionNode.children()) { + int32_t missionId = pugi::cast(missionStateNode.attribute("id").value()); + mission.descriptions.emplace(missionId, missionStateNode.attribute("description").as_string()); + } + } + else { + mission.mainDescription = mainDescription; + } + } + } + return true; +} + +Quest* Quests::getQuestByID(uint16_t id) +{ + for (Quest& quest : quests) { + if (quest.id == id) { + return ? + } + } + return nullptr; +} + +uint16_t Quests::getQuestsCount(Player* player) const +{ + uint16_t count = 0; + for (const Quest& quest : quests) { + if (quest.isStarted(player)) { + count++; + } + } + return count; +} + +bool Quests::isQuestStorage(const uint32_t key, const int32_t value, const int32_t oldValue) const +{ + for (const Quest& quest : quests) { + if (quest.getStartStorageId() == key && quest.getStartStorageValue() == value) { + return true; + } + + for (const Mission& mission : quest.getMissions()) { + if (mission.getStorageId() == key && value >= mission.getStartStorageValue() && value <= mission.getEndStorageValue()) { + return mission.mainDescription.empty() || oldValue < mission.getStartStorageValue() || oldValue > mission.getEndStorageValue(); + } + } + } + return false; +} diff --git a/src/quests.h b/src/quests.h new file mode 100644 index 0000000..45cb002 --- /dev/null +++ b/src/quests.h @@ -0,0 +1,119 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * 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 + * (at your option) 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_QUESTS_H_16E44051F23547BE8097F8EA9FCAACA0 +#define FS_QUESTS_H_16E44051F23547BE8097F8EA9FCAACA0 + +#include "player.h" +#include "networkmessage.h" + +class Mission; +class Quest; + +using MissionsList = std::list; +using QuestsList = std::list; + +class Mission +{ +public: + Mission(std::string name, int32_t storageID, int32_t startValue, int32_t endValue, bool ignoreEndValue) : + name(std::move(name)), storageID(storageID), startValue(startValue), endValue(endValue), ignoreEndValue(ignoreEndValue) {} + + bool isCompleted(Player* player) const; + bool isStarted(Player* player) const; + std::string getName(Player* player) const; + std::string getDescription(Player* player) const; + + uint32_t getStorageId() const { + return storageID; + } + int32_t getStartStorageValue() const { + return startValue; + } + int32_t getEndStorageValue() const { + return endValue; + } + + std::map descriptions; + std::string mainDescription; + +private: + std::string name; + uint32_t storageID; + int32_t startValue, endValue; + bool ignoreEndValue; +}; + +class Quest +{ +public: + Quest(std::string name, uint16_t id, int32_t startStorageID, int32_t startStorageValue) : + name(std::move(name)), startStorageID(startStorageID), startStorageValue(startStorageValue), id(id) {} + + bool isCompleted(Player* player) const; + bool isStarted(Player* player) const; + uint16_t getID() const { + return id; + } + std::string getName() const { + return name; + } + uint16_t getMissionsCount(Player* player) const; + + uint32_t getStartStorageId() const { + return startStorageID; + } + int32_t getStartStorageValue() const { + return startStorageValue; + } + + const MissionsList& getMissions() const { + return missions; + } + +private: + std::string name; + + uint32_t startStorageID; + int32_t startStorageValue; + uint16_t id; + + MissionsList missions; + + friend class Quests; +}; + +class Quests +{ +public: + const QuestsList& getQuests() const { + return quests; + } + + bool loadFromXml(); + Quest* getQuestByID(uint16_t id); + bool isQuestStorage(const uint32_t key, const int32_t value, const int32_t oldValue) const; + uint16_t getQuestsCount(Player* player) const; + bool reload(); + +private: + QuestsList quests; +}; + +#endif diff --git a/vc14/theforgottenserver.vcxproj b/vc14/theforgottenserver.vcxproj index 54a98a8..b1c4e61 100644 --- a/vc14/theforgottenserver.vcxproj +++ b/vc14/theforgottenserver.vcxproj @@ -217,6 +217,7 @@ + @@ -295,6 +296,7 @@ +