From f219099d65746d3b0047d10692d06000524363d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Lu=C3=ADs=20Lucarelo=20Lamonato?= Date: Thu, 19 Oct 2023 01:45:55 -0300 Subject: [PATCH 01/25] fix: stack overflow in bug report (#1704) fix the FS.mkdir_p(path) function that was causing stackoverflow exception I made a change so that the code works as follows: The path is divided into parts, then the function goes through the components, creating the directory if it does not exist. The process repeats until all components of the path have been successfully completed, avoiding stackoverflow exception. At the end, the function returns true to indicate that the operation was successful. --- data/libs/functions/fs.lua | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/data/libs/functions/fs.lua b/data/libs/functions/fs.lua index 46e81a7352f..f1a98d66f06 100644 --- a/data/libs/functions/fs.lua +++ b/data/libs/functions/fs.lua @@ -24,9 +24,27 @@ function FS.mkdir_p(path) if path == "" then return true end - if FS.exists(path) then - return true + + local components = {} + for component in path:gmatch("[^/\\]+") do + table.insert(components, component) end - FS.mkdir_p(path:match("(.*[/\\])")) - return FS.mkdir(path) + + local currentPath = "" + for i, component in ipairs(components) do + currentPath = currentPath .. component + + if not FS.exists(currentPath) then + local success, err = FS.mkdir(currentPath) + if not success then + return false, err + end + end + + if i < #components then + currentPath = currentPath .. "/" + end + end + + return true end From 7f5fce13ef08a02ad3af409f7ca96c5f8942984c Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Thu, 19 Oct 2023 11:23:55 -0300 Subject: [PATCH 02/25] feat: vector-sort (#1690) vector_sort is a container that contains sorted objects. --- src/pch.hpp | 1 + src/utils/vectorsort.hpp | 178 +++++++++++++++++++++++++++++++++++++++ vcproj/canary.vcxproj | 1 + 3 files changed, 180 insertions(+) create mode 100644 src/utils/vectorsort.hpp diff --git a/src/pch.hpp b/src/pch.hpp index c2308989b8f..84d07897ba1 100644 --- a/src/pch.hpp +++ b/src/pch.hpp @@ -18,6 +18,7 @@ #include "utils/definitions.hpp" #include "utils/simd.hpp" #include "utils/vectorset.hpp" +#include "utils/vectorsort.hpp" // -------------------- // STL Includes diff --git a/src/utils/vectorsort.hpp b/src/utils/vectorsort.hpp new file mode 100644 index 00000000000..7069e5463c8 --- /dev/null +++ b/src/utils/vectorsort.hpp @@ -0,0 +1,178 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +#include +#include + +// # Mehah +// vector_sort is a container that contains sorted objects. + +namespace stdext { + template + class vector_sort { + public: + bool contains(const T &v) { + update(); + return std::ranges::binary_search(container, v); + } + + bool erase(const T &v) { + update(); + + const auto &it = std::ranges::lower_bound(container, v); + if (it == container.end()) { + return false; + } + container.erase(it); + + return true; + } + + bool erase(const size_t begin, const size_t end) { + update(); + + if (begin > size() || end > size()) { + return false; + } + + container.erase(container.begin() + begin, container.begin() + end); + return true; + } + + template + bool erase_if(F fnc) { + update(); + return std::erase_if(container, std::move(fnc)) > 0; + } + + auto &front() { + update(); + return container.front(); + } + + void pop_back() { + update(); + container.pop_back(); + } + + auto &back() { + update(); + return container.back(); + } + + void push_back(const T &v) { + needUpdate = true; + container.push_back(v); + } + + void push_back(T &&_Val) { + needUpdate = true; + container.push_back(std::move(_Val)); + } + + // Copy all content list to this + auto insert_all(const vector_sort &list) { + needUpdate = true; + return container.insert(container.end(), list.begin(), list.end()); + } + + // Copy all content list to this + auto insert_all(const std::vector &list) { + needUpdate = true; + return container.insert(container.end(), list.begin(), list.end()); + } + + // Move all content list to this + auto join(vector_sort &list) { + needUpdate = true; + auto res = container.insert(container.end(), make_move_iterator(list.begin()), make_move_iterator(list.end())); + list.clear(); + return res; + } + + // Move all content list to this + auto join(std::vector &list) { + needUpdate = true; + auto res = container.insert(container.end(), make_move_iterator(list.begin()), make_move_iterator(list.end())); + list.clear(); + return res; + } + + template + decltype(auto) emplace_back(_Valty &&... v) { + needUpdate = true; + return container.emplace_back(std::forward<_Valty>(v)...); + } + + void partial_sort(size_t begin, size_t end = 0) { + partial_begin = begin; + if (end > begin) { + partial_end = size() - end; + } + } + + void notify_sort() { + needUpdate = true; + } + + bool empty() const noexcept { + return container.empty(); + } + + size_t size() const noexcept { + return container.size(); + } + + auto begin() noexcept { + update(); + return container.begin(); + } + + auto end() noexcept { + return container.end(); + } + + void clear() noexcept { + partial_begin = partial_end = 0; + return container.clear(); + } + + void reserve(size_t newCap) noexcept { + container.reserve(newCap); + } + + const auto &data() noexcept { + update(); + return container.data(); + } + + T &operator[](const size_t i) { + update(); + return container[i]; + } + + private: + inline void update() noexcept { + if (!needUpdate) { + return; + } + + needUpdate = false; + std::ranges::sort(container.begin() + partial_begin, container.end() - partial_end, std::less()); + } + + std::vector container; + + bool needUpdate = false; + size_t partial_begin { 0 }; + size_t partial_end { 0 }; + }; +} diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index e7fa84338e0..67268dff0f7 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -220,6 +220,7 @@ + From 51eb0b29017ebebfec2c2ede2b27083d806f77c4 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Thu, 19 Oct 2023 11:14:12 -0700 Subject: [PATCH 03/25] feat: fully async saves (#1560) Credits: @beats-dh. This is highly inspired and done with Beats' help. Save intervals are pretty frustrating, they lock the server up and no one is excited to see them come. This makes that whole process asynchronous and speeds everything up. Only saves that happen during the interval will be async, everything else stays the same. This means that logging out, using the market, etc, will still directly save the player. When that happens, the player is then automatically removed from the save queue. --- src/creatures/creature.cpp | 4 +- src/creatures/creature.hpp | 1 + src/creatures/monsters/monster.cpp | 5 +- src/creatures/players/player.cpp | 22 +-- src/creatures/players/player.hpp | 21 +++ src/creatures/players/wheel/player_wheel.cpp | 2 +- src/game/CMakeLists.txt | 1 + src/game/bank/bank.cpp | 5 +- src/game/game.cpp | 49 ++---- src/game/game.hpp | 11 +- src/game/scheduling/save_manager.cpp | 144 ++++++++++++++++++ src/game/scheduling/save_manager.hpp | 46 ++++++ src/io/iologindata.cpp | 8 +- src/io/iomap.cpp | 2 +- src/io/iomapserialize.cpp | 5 +- src/io/iomarket.cpp | 3 +- src/items/bed.cpp | 3 +- src/items/containers/container.cpp | 2 +- src/items/containers/mailbox/mailbox.cpp | 3 +- .../functions/core/game/global_functions.cpp | 3 +- .../creatures/player/player_functions.cpp | 11 +- src/lua/functions/items/item_functions.cpp | 1 + src/map/house/house.cpp | 5 +- src/server/signals.cpp | 3 +- src/utils/benchmark.hpp | 2 +- vcproj/canary.vcxproj | 4 +- 26 files changed, 288 insertions(+), 78 deletions(-) create mode 100644 src/game/scheduling/save_manager.cpp create mode 100644 src/game/scheduling/save_manager.hpp diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 9601af20e18..9825f50076a 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -738,8 +738,8 @@ bool Creature::dropCorpse(std::shared_ptr lastHitCreature, std::shared dropLoot(corpse->getContainer(), lastHitCreature); corpse->startDecaying(); bool corpses = corpse->isRewardCorpse() || (corpse->getID() == ITEM_MALE_CORPSE || corpse->getID() == ITEM_FEMALE_CORPSE); - if (corpse->getContainer() && mostDamageCreature && mostDamageCreature->getPlayer() && !corpses) { - const auto player = mostDamageCreature->getPlayer(); + const auto player = mostDamageCreature ? mostDamageCreature->getPlayer() : nullptr; + if (corpse->getContainer() && player && !corpses) { auto monster = getMonster(); if (monster && !monster->isRewardBoss()) { std::ostringstream lootMessage; diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index b6a8df61a8f..9dd5745c353 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -493,6 +493,7 @@ class Creature : virtual public Thing, public SharedObject { std::shared_ptr getParent() override final { return getTile(); } + void setParent(std::weak_ptr cylinder) override final { auto lockedCylinder = cylinder.lock(); if (lockedCylinder) { diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 013c5412137..9bef75a5ecb 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -225,6 +225,9 @@ void Monster::onCreatureMove(std::shared_ptr creature, std::shared_ptr } updateIdleStatus(); + if (!m_attackedCreature.expired()) { + return; + } if (!isSummon()) { auto followCreature = getFollowCreature(); @@ -792,7 +795,7 @@ void Monster::onThink(uint32_t interval) { // This happens just after a master orders an attack, so lets follow it aswell. setFollowCreature(attackedCreature); } - } else if (!targetIDList.empty()) { + } else if (!attackedCreature && !targetIDList.empty()) { if (!followCreature || !hasFollowPath) { searchTarget(TARGETSEARCH_NEAREST); } else if (isFleeing()) { diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index b596e65e529..996c8de5d41 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -19,6 +19,7 @@ #include "game/game.hpp" #include "game/scheduling/dispatcher.hpp" #include "game/scheduling/task.hpp" +#include "game/scheduling/save_manager.hpp" #include "grouping/familiars.hpp" #include "lua/creature/creatureevent.hpp" #include "lua/creature/events.hpp" @@ -1715,17 +1716,18 @@ void Player::onAttackedCreatureChangeZone(ZoneType_t zone) { void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) { Creature::onRemoveCreature(creature, isLogout); + auto player = getPlayer(); - if (creature == getPlayer()) { + if (creature == player) { if (isLogout) { if (party) { - party->leaveParty(static_self_cast()); + party->leaveParty(player); } if (guild) { - guild->removeMember(static_self_cast()); + guild->removeMember(player); } - g_game().removePlayerUniqueLogin(static_self_cast()); + g_game().removePlayerUniqueLogin(player); loginPosition = getPosition(); lastLogout = time(nullptr); g_logger().info("{} has logged out", getName()); @@ -1739,16 +1741,12 @@ void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) } if (tradePartner) { - g_game().internalCloseTrade(static_self_cast()); + g_game().internalCloseTrade(player); } closeShopWindow(); - for (uint32_t tries = 0; tries < 3; ++tries) { - if (IOLoginData::savePlayer(static_self_cast())) { - break; - } - } + g_saveManager().savePlayer(player); } if (creature == shopOwner) { @@ -4048,7 +4046,9 @@ void Player::postRemoveNotification(std::shared_ptr thing, std::shared_pt assert(i ? i->getContainer() != nullptr : true); if (i) { - requireListUpdate = i->getContainer()->getHoldingPlayer() != getPlayer(); + if (auto container = i->getContainer()) { + requireListUpdate = container->getHoldingPlayer() != getPlayer(); + } } else { requireListUpdate = newParent != getPlayer(); } diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 19614aa3d7e..306fed3f2b3 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -94,6 +94,23 @@ static constexpr int32_t PLAYER_SOUND_HEALTH_CHANGE = 10; class Player final : public Creature, public Cylinder, public Bankable { public: + class PlayerLock { + public: + explicit PlayerLock(const std::shared_ptr &p) : + player(p) { + player->mutex.lock(); + } + + PlayerLock(const PlayerLock &) = delete; + + ~PlayerLock() { + player->mutex.unlock(); + } + + private: + const std::shared_ptr &player; + }; + explicit Player(ProtocolGame_ptr p); ~Player(); @@ -2504,6 +2521,9 @@ class Player final : public Creature, public Cylinder, public Bankable { std::shared_ptr getLootPouch(); private: + friend class PlayerLock; + std::mutex mutex; + static uint32_t playerFirstID; static uint32_t playerLastID; @@ -2862,6 +2882,7 @@ class Player final : public Creature, public Cylinder, public Bankable { void clearCooldowns(); friend class Game; + friend class SaveManager; friend class Npc; friend class PlayerFunctions; friend class NetworkMessageFunctions; diff --git a/src/creatures/players/wheel/player_wheel.cpp b/src/creatures/players/wheel/player_wheel.cpp index dc7d0ee77f6..e6b02777828 100644 --- a/src/creatures/players/wheel/player_wheel.cpp +++ b/src/creatures/players/wheel/player_wheel.cpp @@ -849,7 +849,7 @@ void PlayerWheel::saveSlotPointsOnPressSaveButton(NetworkMessage &msg) { initializePlayerData(); registerPlayerBonusData(); - g_logger().debug("Player: {} is saved the all slots info in: {} seconds", m_player.getName(), bm_saveSlot.duration()); + g_logger().debug("Player: {} is saved the all slots info in: {} milliseconds", m_player.getName(), bm_saveSlot.duration()); } /* diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt index fdf967662a7..dc7758f996d 100644 --- a/src/game/CMakeLists.txt +++ b/src/game/CMakeLists.txt @@ -7,5 +7,6 @@ target_sources(${PROJECT_NAME}_lib PRIVATE scheduling/events_scheduler.cpp scheduling/dispatcher.cpp scheduling/task.cpp + scheduling/save_manager.cpp zones/zone.cpp ) diff --git a/src/game/bank/bank.cpp b/src/game/bank/bank.cpp index bab052b8a38..63952e917b8 100644 --- a/src/game/bank/bank.cpp +++ b/src/game/bank/bank.cpp @@ -13,6 +13,7 @@ #include "game/game.hpp" #include "creatures/players/player.hpp" #include "io/iologindata.hpp" +#include "game/scheduling/save_manager.hpp" Bank::Bank(const std::shared_ptr bankable) : m_bankable(bankable) { @@ -25,14 +26,14 @@ Bank::~Bank() { } std::shared_ptr player = bankable->getPlayer(); if (player && !player->isOnline()) { - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); return; } if (bankable->isGuild()) { const auto guild = static_self_cast(bankable); if (guild && !guild->isOnline()) { - IOGuild::saveGuild(guild); + g_saveManager().saveGuild(guild); } } } diff --git a/src/game/game.cpp b/src/game/game.cpp index 474e73dee3c..cec2e9ab3e2 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -27,6 +27,7 @@ #include "creatures/monsters/monster.hpp" #include "lua/creature/movement.hpp" #include "game/scheduling/dispatcher.hpp" +#include "game/scheduling/save_manager.hpp" #include "server/server.hpp" #include "creatures/combat/spells.hpp" #include "lua/creature/talkaction.hpp" @@ -358,7 +359,7 @@ void Game::setGameState(GameState_t newState) { } saveMotdNum(); - saveGameState(); + g_saveManager().saveAll(); g_dispatcher().addEvent(std::bind(&Game::shutdown, this), "Game::shutdown"); @@ -377,7 +378,7 @@ void Game::setGameState(GameState_t newState) { } } - saveGameState(); + g_saveManager().saveAll(); break; } @@ -386,31 +387,6 @@ void Game::setGameState(GameState_t newState) { } } -void Game::saveGameState() { - if (gameState == GAME_STATE_NORMAL) { - setGameState(GAME_STATE_MAINTAIN); - } - - g_logger().info("Saving server..."); - - for (const auto &it : players) { - it.second->loginPosition = it.second->getPosition(); - IOLoginData::savePlayer(it.second); - } - - for (const auto &it : guilds) { - IOGuild::saveGuild(it.second); - } - - Map::save(); - - g_kv().saveAll(); - - if (gameState == GAME_STATE_MAINTAIN) { - setGameState(GAME_STATE_NORMAL); - } -} - bool Game::loadItemsPrice() { itemsSaleCount = 0; std::ostringstream query, marketQuery; @@ -3839,9 +3815,10 @@ std::shared_ptr Game::wrapItem(std::shared_ptr item, std::shared_ptr house->removeBed(item->getBed()); } uint16_t oldItemID = item->getID(); + auto itemName = item->getName(); std::shared_ptr newItem = transformItem(item, ITEM_DECORATION_KIT); newItem->setCustomAttribute("unWrapId", static_cast(oldItemID)); - item->setAttribute(ItemAttribute_t::DESCRIPTION, "Unwrap it in your own house to create a <" + item->getName() + ">."); + item->setAttribute(ItemAttribute_t::DESCRIPTION, "Unwrap it in your own house to create a <" + itemName + ">."); if (hiddenCharges > 0) { newItem->setAttribute(DATE, hiddenCharges); } @@ -4764,7 +4741,7 @@ void Game::playerQuickLoot(uint32_t playerId, const Position &pos, uint16_t item return; } - std::lock_guard lock(player->quickLootMutex); + Player::PlayerLock lock(player); if (!autoLoot) { player->setNextActionTask(nullptr); } @@ -8333,7 +8310,7 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite // Exhausted for create offert in the market player->updateUIExhausted(); - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); } void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter) { @@ -8414,7 +8391,7 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 player->sendMarketEnter(player->getLastDepotId()); // Exhausted for cancel offer in the market player->updateUIExhausted(); - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); } void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter, uint16_t amount) { @@ -8564,7 +8541,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } if (buyerPlayer->isOffline()) { - IOLoginData::savePlayer(buyerPlayer); + g_saveManager().savePlayer(buyerPlayer); } } else if (offer.type == MARKETACTION_SELL) { std::shared_ptr sellerPlayer = getPlayerByGUID(offer.playerId); @@ -8658,7 +8635,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } if (sellerPlayer->isOffline()) { - IOLoginData::savePlayer(sellerPlayer); + g_saveManager().savePlayer(sellerPlayer); } } @@ -8689,7 +8666,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 player->sendMarketAcceptOffer(offer); // Exhausted for accept offer in the market player->updateUIExhausted(); - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); } void Game::parsePlayerExtendedOpcode(uint32_t playerId, uint8_t opcode, const std::string &buffer) { @@ -9172,7 +9149,7 @@ void Game::addGuild(const std::shared_ptr guild) { void Game::removeGuild(uint32_t guildId) { auto it = guilds.find(guildId); if (it != guilds.end()) { - IOGuild::saveGuild(it->second); + g_saveManager().saveGuild(it->second); } guilds.erase(guildId); } @@ -9729,7 +9706,7 @@ void Game::playerRewardChestCollect(uint32_t playerId, const Position &pos, uint // Updates the parent of the reward chest and reward containers to avoid memory usage after cleaning auto playerRewardChest = player->getRewardChest(); - if (playerRewardChest->empty()) { + if (playerRewardChest && playerRewardChest->empty()) { player->sendCancelMessage(RETURNVALUE_REWARDCHESTISEMPTY); return; } diff --git a/src/game/game.hpp b/src/game/game.hpp index 8f73ff17977..f94e456819c 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -427,7 +427,6 @@ class Game { GameState_t getGameState() const; void setGameState(GameState_t newState); - void saveGameState(); // Events void checkCreatureWalk(uint32_t creatureId); @@ -497,7 +496,10 @@ class Game { const std::map> &getItemsPrice() const { return itemsPriceMap; } - const phmap::flat_hash_map> &getPlayers() const { + const phmap::parallel_flat_hash_map> &getGuilds() const { + return guilds; + } + const phmap::parallel_flat_hash_map> &getPlayers() const { return players; } const std::map> &getMonsters() const { @@ -770,10 +772,11 @@ class Game { phmap::flat_hash_map highscoreCache; phmap::flat_hash_map> m_uniqueLoginPlayerNames; - phmap::flat_hash_map> players; + phmap::parallel_flat_hash_map> players; phmap::flat_hash_map> mappedPlayerNames; - phmap::flat_hash_map> guilds; + phmap::parallel_flat_hash_map> guilds; phmap::flat_hash_map> uniqueItems; + phmap::parallel_flat_hash_map m_playerNameCache; std::map stages; /* Items stored from the lua scripts positions diff --git a/src/game/scheduling/save_manager.cpp b/src/game/scheduling/save_manager.cpp new file mode 100644 index 00000000000..9a5d0114544 --- /dev/null +++ b/src/game/scheduling/save_manager.cpp @@ -0,0 +1,144 @@ +#include "pch.hpp" + +#include "game/game.hpp" +#include "game/scheduling/save_manager.hpp" +#include "io/iologindata.hpp" + +SaveManager::SaveManager(ThreadPool &threadPool, KVStore &kvStore, Logger &logger, Game &game) : + threadPool(threadPool), kv(kvStore), logger(logger), game(game) { } + +SaveManager &SaveManager::getInstance() { + return inject(); +} + +void SaveManager::saveAll() { + Benchmark bm_saveAll; + logger.info("Saving server..."); + const auto players = game.getPlayers(); + + for (const auto &[_, player] : players) { + player->loginPosition = player->getPosition(); + doSavePlayer(player); + } + + auto guilds = game.getGuilds(); + for (const auto &[_, guild] : guilds) { + saveGuild(guild); + } + + saveMap(); + saveKV(); + logger.info("Server saved in {} milliseconds.", bm_saveAll.duration()); +} + +void SaveManager::scheduleAll() { + auto scheduledAt = std::chrono::steady_clock::now(); + m_scheduledAt = scheduledAt; + + threadPool.addLoad([this, scheduledAt]() { + if (m_scheduledAt.load() != scheduledAt) { + logger.warn("Skipping save for server because another save has been scheduled."); + return; + } + saveAll(); + }); +} + +void SaveManager::schedulePlayer(std::weak_ptr playerPtr) { + auto playerToSave = playerPtr.lock(); + if (!playerToSave) { + logger.debug("Skipping save for player because player is no longer online."); + return; + } + logger.debug("Scheduling player {} for saving.", playerToSave->getName()); + auto scheduledAt = std::chrono::steady_clock::now(); + m_playerMap[playerToSave->getGUID()] = scheduledAt; + threadPool.addLoad([this, playerPtr, scheduledAt]() { + auto player = playerPtr.lock(); + if (!player) { + logger.debug("Skipping save for player because player is no longer online."); + return; + } + if (m_playerMap[player->getGUID()] != scheduledAt) { + logger.warn("Skipping save for player because another save has been scheduled."); + return; + } + doSavePlayer(player); + }); +} + +bool SaveManager::doSavePlayer(std::shared_ptr player) { + if (!player) { + logger.debug("Failed to save player because player is null."); + return false; + } + Benchmark bm_savePlayer; + Player::PlayerLock lock(player); + m_playerMap.erase(player->getGUID()); + logger.debug("Saving player {}...", player->getName()); + bool saveSuccess = IOLoginData::savePlayer(player); + if (!saveSuccess) { + logger.error("Failed to save player {}.", player->getName()); + } + auto duration = bm_savePlayer.duration(); + if (duration > 100) { + logger.warn("Saving player {} took {} milliseconds.", player->getName(), duration); + } else { + logger.debug("Saving player {} took {} milliseconds.", player->getName(), duration); + } + return saveSuccess; +} + +bool SaveManager::savePlayer(std::shared_ptr player) { + if (player->isOnline()) { + schedulePlayer(player); + return true; + } + return doSavePlayer(player); +} + +void SaveManager::saveGuild(std::shared_ptr guild) { + if (!guild) { + logger.debug("Failed to save guild because guild is null."); + return; + } + Benchmark bm_saveGuild; + logger.debug("Saving guild {}...", guild->getName()); + IOGuild::saveGuild(guild); + auto duration = bm_saveGuild.duration(); + if (duration > 100) { + logger.warn("Saving guild {} took {} milliseconds.", guild->getName(), duration); + } else { + logger.debug("Saving guild {} took {} milliseconds.", guild->getName(), duration); + } +} + +void SaveManager::saveMap() { + Benchmark bm_saveMap; + logger.debug("Saving map..."); + bool saveSuccess = Map::save(); + if (!saveSuccess) { + logger.error("Failed to save map."); + } + auto duration = bm_saveMap.duration(); + if (duration > 100) { + logger.warn("Map saved in {} milliseconds.", bm_saveMap.duration()); + } else { + logger.debug("Map saved in {} milliseconds.", bm_saveMap.duration()); + } +} + +void SaveManager::saveKV() { + Benchmark bm_saveKV; + logger.debug("Saving key-value store..."); + bool saveSuccess = kv.saveAll(); + if (!saveSuccess) { + logger.error("Failed to save key-value store."); + } + auto duration = bm_saveKV.duration(); + if (duration > 100) { + logger.warn("Key-value store saved in {} milliseconds.", bm_saveKV.duration()); + } else { + logger.debug("Key-value store saved in {} milliseconds.", bm_saveKV.duration()); + } +} diff --git a/src/game/scheduling/save_manager.hpp b/src/game/scheduling/save_manager.hpp new file mode 100644 index 00000000000..a809dd73f6a --- /dev/null +++ b/src/game/scheduling/save_manager.hpp @@ -0,0 +1,46 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +#include "lib/thread/thread_pool.hpp" +#include "kv/kv.hpp" + +class SaveManager { +public: + explicit SaveManager(ThreadPool &threadPool, KVStore &kvStore, Logger &logger, Game &game); + + SaveManager(const SaveManager &) = delete; + void operator=(const SaveManager &) = delete; + + static SaveManager &getInstance(); + + void saveAll(); + void scheduleAll(); + + bool savePlayer(std::shared_ptr player); + void saveGuild(std::shared_ptr guild); + +private: + void saveMap(); + void saveKV(); + + void schedulePlayer(std::weak_ptr player); + bool doSavePlayer(std::shared_ptr player); + + std::atomic m_scheduledAt; + phmap::parallel_flat_hash_map m_playerMap; + + ThreadPool &threadPool; + KVStore &kv; + Logger &logger; + Game &game; +}; + +constexpr auto g_saveManager = SaveManager::getInstance; diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index f32b90568a4..d454cd32bdb 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -362,7 +362,9 @@ void IOLoginData::addVIPEntry(uint32_t accountId, uint32_t guid, const std::stri std::ostringstream query; query << "INSERT INTO `account_viplist` (`account_id`, `player_id`, `description`, `icon`, `notify`) VALUES (" << accountId << ',' << guid << ',' << db.escapeString(description) << ',' << icon << ',' << notify << ')'; - db.executeQuery(query.str()); + if (!db.executeQuery(query.str())) { + g_logger().error("Failed to add VIP entry for account %u. QUERY: %s", accountId, query.str().c_str()); + } } void IOLoginData::editVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify) { @@ -370,7 +372,9 @@ void IOLoginData::editVIPEntry(uint32_t accountId, uint32_t guid, const std::str std::ostringstream query; query << "UPDATE `account_viplist` SET `description` = " << db.escapeString(description) << ", `icon` = " << icon << ", `notify` = " << notify << " WHERE `account_id` = " << accountId << " AND `player_id` = " << guid; - db.executeQuery(query.str()); + if (!db.executeQuery(query.str())) { + g_logger().error("Failed to edit VIP entry for account %u. QUERY: %s", accountId, query.str().c_str()); + } } void IOLoginData::removeVIPEntry(uint32_t accountId, uint32_t guid) { diff --git a/src/io/iomap.cpp b/src/io/iomap.cpp index 72eaed123ae..51306b7f756 100644 --- a/src/io/iomap.cpp +++ b/src/io/iomap.cpp @@ -77,7 +77,7 @@ void IOMap::loadMap(Map* map, const Position &pos) { map->flush(); - g_logger().info("Map Loaded {} ({}x{}) in {} seconds", map->path.filename().string(), map->width, map->height, bm_mapLoad.duration()); + g_logger().info("Map Loaded {} ({}x{}) in {} milliseconds", map->path.filename().string(), map->width, map->height, bm_mapLoad.duration()); } void IOMap::parseMapDataAttributes(FileStream &stream, Map* map) { diff --git a/src/io/iomapserialize.cpp b/src/io/iomapserialize.cpp index 94531a61228..c089fe022db 100644 --- a/src/io/iomapserialize.cpp +++ b/src/io/iomapserialize.cpp @@ -49,7 +49,7 @@ void IOMapSerialize::loadHouseItems(Map* map) { loadItem(propStream, tile, true); } } while (result->next()); - g_logger().info("Loaded house items in {} seconds", bm_context.duration()); + g_logger().info("Loaded house items in {} milliseconds", bm_context.duration()); } bool IOMapSerialize::saveHouseItems() { bool success = DBTransaction::executeWithinTransaction([]() { @@ -64,8 +64,6 @@ bool IOMapSerialize::saveHouseItems() { } bool IOMapSerialize::SaveHouseItemsGuard() { - Benchmark bm_context; - Database &db = Database::getInstance(); std::ostringstream query; @@ -98,7 +96,6 @@ bool IOMapSerialize::SaveHouseItemsGuard() { return false; } - g_logger().info("Saved house items in {} seconds", bm_context.duration()); return true; } diff --git a/src/io/iomarket.cpp b/src/io/iomarket.cpp index d7edca1c51e..d6b6109309c 100644 --- a/src/io/iomarket.cpp +++ b/src/io/iomarket.cpp @@ -14,6 +14,7 @@ #include "io/iologindata.hpp" #include "game/game.hpp" #include "game/scheduling/dispatcher.hpp" +#include "game/scheduling/save_manager.hpp" uint8_t IOMarket::getTierFromDatabaseTable(const std::string &string) { auto tier = static_cast(std::atoi(string.c_str())); @@ -177,7 +178,7 @@ void IOMarket::processExpiredOffers(DBResult_ptr result, bool) { } if (player->isOffline()) { - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); } } else { uint64_t totalPrice = result->getNumber("price") * amount; diff --git a/src/items/bed.cpp b/src/items/bed.cpp index d8a246ec8b7..289ae210a6a 100644 --- a/src/items/bed.cpp +++ b/src/items/bed.cpp @@ -13,6 +13,7 @@ #include "game/game.hpp" #include "io/iologindata.hpp" #include "game/scheduling/dispatcher.hpp" +#include "game/scheduling/save_manager.hpp" BedItem::BedItem(uint16_t id) : Item(id) { @@ -178,7 +179,7 @@ void BedItem::wakeUp(std::shared_ptr player) { auto regenPlayer = std::make_shared(nullptr); if (IOLoginData::loadPlayerById(regenPlayer, sleeperGUID)) { regeneratePlayer(regenPlayer); - IOLoginData::savePlayer(regenPlayer); + g_saveManager().savePlayer(regenPlayer); } } else { regeneratePlayer(player); diff --git a/src/items/containers/container.cpp b/src/items/containers/container.cpp index 67884eb4fcc..0800f032adb 100644 --- a/src/items/containers/container.cpp +++ b/src/items/containers/container.cpp @@ -58,7 +58,7 @@ std::shared_ptr Container::create(std::shared_ptr tile) { Container::~Container() { if (getID() == ITEM_BROWSEFIELD) { for (std::shared_ptr item : itemlist) { - item->setParent(m_parent); + item->setParent(getParent()); } } } diff --git a/src/items/containers/mailbox/mailbox.cpp b/src/items/containers/mailbox/mailbox.cpp index 959ca818bb5..9253bdd7417 100644 --- a/src/items/containers/mailbox/mailbox.cpp +++ b/src/items/containers/mailbox/mailbox.cpp @@ -12,6 +12,7 @@ #include "items/containers/mailbox/mailbox.hpp" #include "game/game.hpp" #include "io/iologindata.hpp" +#include "game/scheduling/save_manager.hpp" #include "map/spectators.hpp" ReturnValue Mailbox::queryAdd(int32_t, const std::shared_ptr &thing, uint32_t, uint32_t, std::shared_ptr) { @@ -107,7 +108,7 @@ bool Mailbox::sendItem(std::shared_ptr item) const { if (player->isOnline()) { player->onReceiveMail(); } else { - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); } return true; } diff --git a/src/lua/functions/core/game/global_functions.cpp b/src/lua/functions/core/game/global_functions.cpp index 1188233dd83..7abf3b2c6e6 100644 --- a/src/lua/functions/core/game/global_functions.cpp +++ b/src/lua/functions/core/game/global_functions.cpp @@ -12,6 +12,7 @@ #include "creatures/interactions/chat.hpp" #include "game/game.hpp" #include "game/scheduling/dispatcher.hpp" +#include "game/scheduling/save_manager.hpp" #include "lua/functions/core/game/global_functions.hpp" #include "lua/scripts/lua_environment.hpp" #include "lua/scripts/script_environment.hpp" @@ -722,7 +723,7 @@ int GlobalFunctions::luaStopEvent(lua_State* L) { } int GlobalFunctions::luaSaveServer(lua_State* L) { - g_game().saveGameState(); + g_saveManager().scheduleAll(); pushBoolean(L, true); return 1; } diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 7145525c3bd..6779101380d 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -19,6 +19,7 @@ #include "io/ioprey.hpp" #include "items/item.hpp" #include "lua/functions/creatures/player/player_functions.hpp" +#include "game/scheduling/save_manager.hpp" #include "map/spectators.hpp" int PlayerFunctions::luaPlayerSendInventory(lua_State* L) { @@ -1379,6 +1380,11 @@ int PlayerFunctions::luaPlayerSetVocation(lua_State* L) { } player->setVocation(vocation->getId()); + player->sendSkills(); + player->sendStats(); + player->sendBasicData(); + player->wheel()->sendGiftOfLifeCooldown(); + g_game().reloadCreature(player); pushBoolean(L, true); return 1; } @@ -2824,10 +2830,7 @@ int PlayerFunctions::luaPlayerSave(lua_State* L) { if (!player->isOffline()) { player->loginPosition = player->getPosition(); } - pushBoolean(L, IOLoginData::savePlayer(player)); - if (player->isOffline()) { - // avoiding memory leak - } + pushBoolean(L, g_saveManager().savePlayer(player)); } else { lua_pushnil(L); } diff --git a/src/lua/functions/items/item_functions.cpp b/src/lua/functions/items/item_functions.cpp index f943c52c8fc..c1f25361253 100644 --- a/src/lua/functions/items/item_functions.cpp +++ b/src/lua/functions/items/item_functions.cpp @@ -14,6 +14,7 @@ #include "game/game.hpp" #include "items/item.hpp" #include "items/decay/decay.hpp" +#include "game/scheduling/save_manager.hpp" class Imbuement; diff --git a/src/map/house/house.cpp b/src/map/house/house.cpp index a623a3e523a..e1225aa9370 100644 --- a/src/map/house/house.cpp +++ b/src/map/house/house.cpp @@ -14,6 +14,7 @@ #include "io/iologindata.hpp" #include "game/game.hpp" #include "items/bed.hpp" +#include "game/scheduling/save_manager.hpp" House::House(uint32_t houseId) : id(houseId) { } @@ -285,7 +286,7 @@ bool House::transferToDepot(std::shared_ptr player) const { g_logger().debug("[{}] moving item '{}' to depot", __FUNCTION__, item->getName()); g_game().internalMoveItem(item->getParent(), player->getInbox(), INDEX_WHEREEVER, item, item->getItemCount(), nullptr, FLAG_NOLIMIT); } - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); return true; } @@ -821,7 +822,7 @@ void Houses::payHouses(RentPeriod_t rentPeriod) const { } } - IOLoginData::savePlayer(player); + g_saveManager().savePlayer(player); } } diff --git a/src/server/signals.cpp b/src/server/signals.cpp index d70f65e06a9..bb40bbcd859 100644 --- a/src/server/signals.cpp +++ b/src/server/signals.cpp @@ -11,6 +11,7 @@ #include "game/game.hpp" #include "game/scheduling/dispatcher.hpp" +#include "game/scheduling/save_manager.hpp" #include "lib/thread/thread_pool.hpp" #include "lua/creature/events.hpp" #include "lua/scripts/lua_environment.hpp" @@ -91,7 +92,7 @@ void Signals::sigtermHandler() { void Signals::sigusr1Handler() { // Dispatcher thread g_logger().info("SIGUSR1 received, saving the game state..."); - g_game().saveGameState(); + g_saveManager().scheduleAll(); } void Signals::sighupHandler() { diff --git a/src/utils/benchmark.hpp b/src/utils/benchmark.hpp index 8d968ca708b..7ec007ceaeb 100644 --- a/src/utils/benchmark.hpp +++ b/src/utils/benchmark.hpp @@ -75,7 +75,7 @@ class Benchmark { private: int64_t time() const noexcept { - return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } int64_t startTime = -1; diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index 67268dff0f7..ed1217302f2 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -61,6 +61,7 @@ + @@ -258,6 +259,7 @@ + @@ -548,4 +550,4 @@ - \ No newline at end of file + From afcae2c369e7893ea38ecbb531c4dda0dac071f2 Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Thu, 19 Oct 2023 20:00:44 -0300 Subject: [PATCH 04/25] feat: Arraylist, fast new container to add front and back (#1677) Arraylist is a very fast container for adding to the front and back, it uses two vectors to do this juggling and doesn't allow you to remove the front, as it is slow, use std::list for this case. **Benchmark** loop: 99999 ![image](https://github.com/opentibiabr/canary/assets/2267386/ebdc1883-0249-413a-8553-8c5dab0b7ac8) loop: 999999 ![image](https://github.com/opentibiabr/canary/assets/2267386/6d0c46b3-ff6e-429a-8ae7-9cf015ed142e)
```c++ constexpr size_t LOOP = 999999; Benchmark bm; for (size_t i = 0; i < 999; i++) { stdext::arraylist list; list.reserve(LOOP); for (size_t d = 0; d < LOOP; d++) { if (d % 2) { list.emplace_front(d); } else { list.emplace_back(d); } } for (const auto &v : list) { } } g_logger().info("stdext::arraylist + reserve: {}ms", bm.duration()); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { stdext::arraylist list; for (size_t d = 0; d < LOOP; d++) { if (d % 2) { list.emplace_front(d); } else { list.emplace_back(d); } } for (const auto &v : list) { } } g_logger().info("stdext::arraylist: {}ms", bm.duration()); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { std::list list; for (size_t d = 0; d < LOOP; d++) { if (d % 2) { list.emplace_front(d); } else { list.emplace_back(d); } } for (const auto &v : list) { } } g_logger().info("std::list: {}ms", bm.duration()); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { std::deque list; for (size_t d = 0; d < LOOP; d++) { if (d % 2) { list.emplace_front(d); } else { list.emplace_back(d); } } for (const auto &v : list) { } } g_logger().info("std::deque: {}ms", bm.duration()); g_logger().info("--- stdext::arraylist vs std::forward_list vs std::list vs std::deque --- (push_front)"); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { stdext::arraylist list; list.reserve(LOOP); for (size_t d = 0; d < LOOP; d++) { list.emplace_front(d); } for (const auto &v : list) { } } g_logger().info("stdext::arraylist + reserve: {}ms", bm.duration()); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { stdext::arraylist arraylist; for (size_t d = 0; d < LOOP; d++) { arraylist.emplace_front(d); } for (const auto &v : arraylist) { } } g_logger().info("stdext::arraylist: {}ms", bm.duration()); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { std::forward_list list; for (size_t d = 0; d < LOOP; d++) { list.emplace_front(d); } for (const auto &v : list) { } } g_logger().info("std::forward_list: {}ms", bm.duration()); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { std::list list; for (size_t d = 0; d < LOOP; d++) { list.emplace_front(d); } for (const auto &v : list) { } } g_logger().info("std::list: {}ms", bm.duration()); bm.reset(); bm.start(); for (size_t i = 0; i < 999; i++) { std::deque list; for (size_t d = 0; d < LOOP; d++) { list.emplace_front(d); } for (const auto &v : list) { } } g_logger().info("std::deque: {}ms", bm.duration()); ```
**Validating Content**
```c++ constexpr size_t LOOP = 999999; stdext::arraylist arraylist; arraylist.reserve(LOOP); for (size_t d = 0; d < LOOP; d++) { if (d % 2) { arraylist.emplace_front(d); } else { arraylist.emplace_back(d); } } std::deque deque; for (size_t d = 0; d < LOOP; d++) { if (d % 2) { deque.emplace_front(d); } else { deque.emplace_back(d); } } for (size_t i = 0; i < LOOP; i++) { if (arraylist[i] != deque[i]) { g_logger().info("NOT EQUAL"); } } g_logger().info("FINISHED"); ```
--- src/pch.hpp | 1 + src/utils/arraylist.hpp | 158 ++++++++++++++++++++++++++++++++++++++++ vcproj/canary.vcxproj | 1 + 3 files changed, 160 insertions(+) create mode 100644 src/utils/arraylist.hpp diff --git a/src/pch.hpp b/src/pch.hpp index 84d07897ba1..ab57f178e84 100644 --- a/src/pch.hpp +++ b/src/pch.hpp @@ -18,6 +18,7 @@ #include "utils/definitions.hpp" #include "utils/simd.hpp" #include "utils/vectorset.hpp" +#include "utils/arraylist.hpp" #include "utils/vectorsort.hpp" // -------------------- diff --git a/src/utils/arraylist.hpp b/src/utils/arraylist.hpp new file mode 100644 index 00000000000..ea803ab8cf0 --- /dev/null +++ b/src/utils/arraylist.hpp @@ -0,0 +1,158 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +#include +#include + +// # Mehah +// Arraylist is a very fast container for adding to the front and back, +// it uses two vectors to do this juggling and doesn't allow you to remove the front, as it is slow, +// use std::list for this case. + +namespace stdext { + template + class arraylist { + public: + bool contains(const T &v) { + update(); + return std::ranges::find(backContainer, v) != backContainer.end(); + } + + bool erase(const T &v) { + update(); + + const auto &it = std::ranges::find(backContainer, v); + if (it == backContainer.end()) { + return false; + } + backContainer.erase(it); + + return true; + } + + bool erase(const size_t begin, const size_t end) { + update(); + + if (begin > size() || end > size()) { + return false; + } + + backContainer.erase(backContainer.begin() + begin, backContainer.begin() + end); + return true; + } + + template + bool erase_if(F fnc) { + update(); + return std::erase_if(backContainer, std::move(fnc)) > 0; + } + + auto &front() { + update(); + return backContainer.front(); + } + + void pop_back() { + update(); + backContainer.pop_back(); + } + + auto &back() { + update(); + return backContainer.back(); + } + + void push_front(const T &v) { + needUpdate = true; + frontContainer.push_back(v); + } + + void push_front(T &&_Val) { + needUpdate = true; + frontContainer.push_back(std::move(_Val)); + } + + template + decltype(auto) emplace_front(_Valty &&... v) { + needUpdate = true; + return frontContainer.emplace_back(std::forward<_Valty>(v)...); + } + + void push_back(const T &v) { + backContainer.push_back(v); + } + + void push_back(T &&_Val) { + backContainer.push_back(std::move(_Val)); + } + + template + decltype(auto) emplace_back(_Valty &&... v) { + return backContainer.emplace_back(std::forward<_Valty>(v)...); + } + + bool empty() const noexcept { + return backContainer.empty() && frontContainer.empty(); + } + + size_t size() const noexcept { + return backContainer.size() + frontContainer.size(); + } + + auto begin() noexcept { + update(); + return backContainer.begin(); + } + + auto end() noexcept { + return backContainer.end(); + } + + void clear() noexcept { + frontContainer.clear(); + return backContainer.clear(); + } + + void reserve(size_t newCap) noexcept { + backContainer.reserve(newCap); + frontContainer.reserve(newCap); + } + + const auto &data() noexcept { + update(); + return backContainer.data(); + } + + T &operator[](const size_t i) { + update(); + return backContainer[i]; + } + + private: + inline void update() noexcept { + if (!needUpdate) { + return; + } + + needUpdate = false; + std::ranges::reverse(frontContainer); + frontContainer.insert(frontContainer.end(), make_move_iterator(backContainer.begin()), make_move_iterator(backContainer.end())); + backContainer.clear(); + backContainer.insert(backContainer.end(), make_move_iterator(frontContainer.begin()), make_move_iterator(frontContainer.end())); + frontContainer.clear(); + } + + std::vector backContainer; + std::vector frontContainer; + + bool needUpdate = false; + }; +} diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index ed1217302f2..7b467346d39 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -212,6 +212,7 @@ + From 10d6002556900b722ae7f7d035ed58f37e5ca74d Mon Sep 17 00:00:00 2001 From: Luan Colombo <94877887+luancolombo@users.noreply.github.com> Date: Fri, 20 Oct 2023 03:36:09 +0100 Subject: [PATCH 05/25] fix: fire axe quest (#1717) Added fire axe to quest chest. --- data-otservbr-global/startup/tables/chest.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data-otservbr-global/startup/tables/chest.lua b/data-otservbr-global/startup/tables/chest.lua index 2ca7c272215..f5049f93f61 100644 --- a/data-otservbr-global/startup/tables/chest.lua +++ b/data-otservbr-global/startup/tables/chest.lua @@ -653,7 +653,7 @@ ChestUnique = { itemId = 2472, itemPos = { x = 33078, y = 31656, z = 11 }, container = 2853, - reward = { { 3098, 1 }, { 3085, 200 }, { 3028, 7 } }, + reward = { { 3098, 1 }, { 3085, 200 }, { 3028, 7 }, { 3320, 1 } }, weight = 27, storage = Storage.Quest.U6_4.FireAxe.Rewards.Bag, }, From 450e04bee279ab626abb68099886b9ef19cf2e91 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Fri, 20 Oct 2023 10:10:43 -0700 Subject: [PATCH 06/25] improve: createItemBatch (#1714) Makes bulk purchases from NPCs _way_ faster and less prone to exploits (players will no longer be able to abuse NPCs by buying a lot of items at once). --- src/game/game.cpp | 100 ++++++++++++++++++ src/game/game.hpp | 3 + src/items/thing.hpp | 4 + .../functions/creatures/npc/npc_functions.cpp | 82 +------------- 4 files changed, 109 insertions(+), 80 deletions(-) diff --git a/src/game/game.cpp b/src/game/game.cpp index cec2e9ab3e2..2e5620d0e9d 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -2022,6 +2022,106 @@ ReturnValue Game::internalRemoveItem(std::shared_ptr item, int32_t count / return RETURNVALUE_NOERROR; } +std::tuple Game::addItemBatch(const std::shared_ptr &toCylinder, const std::vector> &items, uint32_t flags /* = 0 */, bool dropOnMap /* = true */, uint32_t autoContainerId /* = 0 */) { + const auto player = toCylinder->getPlayer(); + bool dropping = false; + ReturnValue ret = RETURNVALUE_NOTPOSSIBLE; + uint32_t totalAdded = 0; + uint32_t containersCreated = 0; + + auto setupDestination = [&]() -> std::shared_ptr { + if (autoContainerId == 0) { + return toCylinder; + } + auto autoContainer = Item::CreateItem(autoContainerId); + if (!autoContainer) { + g_logger().error("[{}] Failed to create auto container", __FUNCTION__); + return toCylinder; + } + if (internalAddItem(toCylinder, autoContainer, CONST_SLOT_WHEREEVER, flags) != RETURNVALUE_NOERROR) { + if (internalAddItem(toCylinder->getTile(), autoContainer, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + g_logger().error("[{}] Failed to add auto container", __FUNCTION__); + return toCylinder; + } + } + auto container = autoContainer->getContainer(); + if (!container) { + g_logger().error("[{}] Failed to get auto container", __FUNCTION__); + return toCylinder; + } + containersCreated++; + return container; + }; + auto destination = setupDestination(); + + for (const auto &item : items) { + auto container = destination->getContainer(); + if (container && container->getFreeSlots() == 0) { + destination = setupDestination(); + } + if (!dropping) { + uint32_t remainderCount = 0; + ret = internalAddItem(destination, item, CONST_SLOT_WHEREEVER, flags, false, remainderCount); + if (remainderCount != 0) { + std::shared_ptr remainderItem = Item::CreateItem(item->getID(), remainderCount); + ReturnValue remaindRet = internalAddItem(destination->getTile(), remainderItem, INDEX_WHEREEVER, FLAG_NOLIMIT); + if (player && remaindRet != RETURNVALUE_NOERROR) { + player->sendLootStats(item, static_cast(item->getItemCount())); + } + } + } + + if (dropping || ret != RETURNVALUE_NOERROR && dropOnMap) { + dropping = true; + ret = internalAddItem(destination->getTile(), item, INDEX_WHEREEVER, FLAG_NOLIMIT); + } + + if (player && ret == RETURNVALUE_NOERROR) { + player->sendForgingData(); + } + if (ret != RETURNVALUE_NOERROR) { + break; + } else { + totalAdded += item->getItemCount(); + } + } + + return std::make_tuple(ret, totalAdded, containersCreated); +} + +std::tuple Game::createItemBatch(const std::shared_ptr &toCylinder, const std::vector> &itemCounts, uint32_t flags /* = 0 */, bool dropOnMap /* = true */, uint32_t autoContainerId /* = 0 */) { + std::vector> items; + for (const auto &[itemId, count, subType] : itemCounts) { + const auto &itemType = Item::items[itemId]; + if (itemType.id <= 0) { + continue; + } + if (count == 0) { + continue; + } + uint32_t countPerItem = itemType.stackable ? itemType.stackSize : 1; + for (uint32_t i = 0; i < count; ++i) { + std::shared_ptr item; + if (itemType.isWrappable()) { + countPerItem = 1; + item = Item::CreateItem(ITEM_DECORATION_KIT, subType); + item->setAttribute(ItemAttribute_t::DESCRIPTION, "Unwrap this item in your own house to create a <" + itemType.name + ">."); + item->setCustomAttribute("unWrapId", static_cast(itemId)); + } else { + item = Item::CreateItem(itemId, itemType.stackable ? std::min(countPerItem, count - i) : subType); + } + items.push_back(item); + i += countPerItem - 1; + } + } + + return addItemBatch(toCylinder, items, flags, dropOnMap, autoContainerId); +} + +std::tuple Game::createItem(const std::shared_ptr &toCylinder, uint16_t itemId, uint32_t count, uint16_t subType, uint32_t flags /* = 0 */, bool dropOnMap /* = true */, uint32_t autoContainerId /* = 0 */) { + return createItemBatch(toCylinder, { std::make_tuple(itemId, count, subType) }, flags, dropOnMap, autoContainerId); +} + ReturnValue Game::internalPlayerAddItem(std::shared_ptr player, std::shared_ptr item, bool dropOnMap /*= true*/, Slots_t slot /*= CONST_SLOT_WHEREEVER*/) { uint32_t remainderCount = 0; ReturnValue ret = internalAddItem(player, item, static_cast(slot), 0, false, remainderCount); diff --git a/src/game/game.hpp b/src/game/game.hpp index f94e456819c..aa2d81ba831 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -210,6 +210,9 @@ class Game { ReturnValue checkMoveItemToCylinder(std::shared_ptr player, std::shared_ptr fromCylinder, std::shared_ptr toCylinder, std::shared_ptr item, Position toPos); ReturnValue internalMoveItem(std::shared_ptr fromCylinder, std::shared_ptr toCylinder, int32_t index, std::shared_ptr item, uint32_t count, std::shared_ptr* movedItem, uint32_t flags = 0, std::shared_ptr actor = nullptr, std::shared_ptr tradeItem = nullptr, bool checkTile = true); + std::tuple addItemBatch(const std::shared_ptr &toCylinder, const std::vector> &items, uint32_t flags = 0, bool dropOnMap = true, uint32_t autoContainerId = 0); + std::tuple createItemBatch(const std::shared_ptr &toCylinder, const std::vector> &itemCounts, uint32_t flags = 0, bool dropOnMap = true, uint32_t autoContainerId = 0); + std::tuple createItem(const std::shared_ptr &toCylinder, uint16_t itemId, uint32_t count, uint16_t subType, uint32_t flags = 0, bool dropOnMap = true, uint32_t autoContainerId = 0); ReturnValue internalAddItem(std::shared_ptr toCylinder, std::shared_ptr item, int32_t index = INDEX_WHEREEVER, uint32_t flags = 0, bool test = false); ReturnValue internalAddItem(std::shared_ptr toCylinder, std::shared_ptr item, int32_t index, uint32_t flags, bool test, uint32_t &remainderCount); ReturnValue internalRemoveItem(std::shared_ptr item, int32_t count = -1, bool test = false, uint32_t flags = 0, bool force = false); diff --git a/src/items/thing.hpp b/src/items/thing.hpp index 0a6161783b3..5b75e716cae 100644 --- a/src/items/thing.hpp +++ b/src/items/thing.hpp @@ -16,6 +16,7 @@ class Cylinder; class Item; class Creature; class Container; +class Player; class Thing { public: @@ -47,6 +48,9 @@ class Thing { virtual int32_t getThrowRange() const = 0; virtual bool isPushable() = 0; + virtual std::shared_ptr getPlayer() { + return nullptr; + } virtual std::shared_ptr getContainer() { return nullptr; } diff --git a/src/lua/functions/creatures/npc/npc_functions.cpp b/src/lua/functions/creatures/npc/npc_functions.cpp index fc8c8272c68..e0ae3c2bfaf 100644 --- a/src/lua/functions/creatures/npc/npc_functions.cpp +++ b/src/lua/functions/creatures/npc/npc_functions.cpp @@ -524,85 +524,7 @@ int NpcFunctions::luaNpcSellItem(lua_State* L) { } } - uint32_t itemsPurchased = 0; - uint8_t backpacksPurchased = 0; - uint8_t internalCount = it.stackable ? it.stackSize : 1; - auto remainingAmount = static_cast(amount); - if (inBackpacks) { - while (remainingAmount > 0) { - std::shared_ptr container = Item::CreateItem(ITEM_SHOPPING_BAG); - if (!container) { - break; - } - - if (g_game().internalPlayerAddItem(player, container, ignoreCap, CONST_SLOT_WHEREEVER) != RETURNVALUE_NOERROR) { - break; - } - - backpacksPurchased++; - uint8_t internalAmount = (remainingAmount > internalCount) ? internalCount : static_cast(remainingAmount); - const ItemType &iType = Item::items[itemId]; - std::shared_ptr item; - if (iType.isWrappable()) { - item = Item::CreateItem(ITEM_DECORATION_KIT, subType); - item->setAttribute(ItemAttribute_t::DESCRIPTION, "Unwrap this item in your own house to create a <" + iType.name + ">."); - item->setCustomAttribute("unWrapId", static_cast(itemId)); - } else { - item = Item::CreateItem(itemId, it.stackable ? internalAmount : subType); - } - if (actionId != 0) { - item->setAttribute(ItemAttribute_t::ACTIONID, actionId); - } - - while (remainingAmount > 0) { - if (g_game().internalAddItem(container->getContainer(), item, INDEX_WHEREEVER, 0) != RETURNVALUE_NOERROR) { - break; - } - - itemsPurchased += internalAmount; - remainingAmount -= internalAmount; - internalAmount = (remainingAmount > internalCount) ? internalCount : static_cast(remainingAmount); - if (iType.isWrappable()) { - item = Item::CreateItem(ITEM_DECORATION_KIT, subType); - item->setAttribute(ItemAttribute_t::DESCRIPTION, "Unwrap this item in your own house to create a <" + iType.name + ">."); - item->setCustomAttribute("unWrapId", static_cast(itemId)); - } else { - item = Item::CreateItem(itemId, it.stackable ? internalAmount : subType); - } - } - } - } else { - uint8_t internalAmount = (remainingAmount > internalCount) ? internalCount : static_cast(remainingAmount); - const ItemType &iType = Item::items[itemId]; - std::shared_ptr item; - if (iType.isWrappable()) { - item = Item::CreateItem(ITEM_DECORATION_KIT, subType); - item->setAttribute(ItemAttribute_t::DESCRIPTION, "Unwrap this item in your own house to create a <" + iType.name + ">."); - item->setCustomAttribute("unWrapId", static_cast(itemId)); - } else { - item = Item::CreateItem(itemId, it.stackable ? internalAmount : subType); - } - if (actionId != 0) { - item->setAttribute(ItemAttribute_t::ACTIONID, actionId); - } - - while (remainingAmount > 0) { - if (g_game().internalPlayerAddItem(player, item, ignoreCap, CONST_SLOT_WHEREEVER) != RETURNVALUE_NOERROR) { - break; - } - - itemsPurchased += internalAmount; - remainingAmount -= internalAmount; - internalAmount = (remainingAmount > internalCount) ? internalCount : static_cast(remainingAmount); - if (iType.isWrappable()) { - item = Item::CreateItem(ITEM_DECORATION_KIT, subType); - item->setAttribute(ItemAttribute_t::DESCRIPTION, "Unwrap this item in your own house to create a <" + iType.name + ">."); - item->setCustomAttribute("unWrapId", static_cast(itemId)); - } else { - item = Item::CreateItem(itemId, it.stackable ? internalAmount : subType); - } - } - } + const auto &[_, itemsPurchased, backpacksPurchased] = g_game().createItem(player, itemId, amount, subType, actionId, ignoreCap, inBackpacks ? ITEM_SHOPPING_BAG : 0); std::stringstream ss; uint64_t itemCost = itemsPurchased * pricePerUnit; @@ -610,7 +532,7 @@ int NpcFunctions::luaNpcSellItem(lua_State* L) { if (npc->getCurrency() == ITEM_GOLD_COIN) { if (!g_game().removeMoney(player, itemCost + backpackCost, 0, true)) { g_logger().error("[NpcFunctions::luaNpcSellItem (removeMoney)] - Player {} have a problem for buy item {} on shop for npc {}", player->getName(), itemId, npc->getName()); - g_logger().debug("[Information] Player {} buyed item {} on shop for npc {}, at position {}", player->getName(), itemId, npc->getName(), player->getPosition().toString()); + g_logger().debug("[Information] Player {} bought {} x item {} on shop for npc {}, at position {}", player->getName(), itemsPurchased, itemId, npc->getName(), player->getPosition().toString()); } else if (backpacksPurchased > 0) { ss << "Bought " << std::to_string(itemsPurchased) << "x " << it.name << " and " << std::to_string(backpacksPurchased); if (backpacksPurchased > 1) { From b8e668880f2925a3dcf7d69807f407d8fc6c1724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Lu=C3=ADs=20Lucarelo=20Lamonato?= Date: Fri, 20 Oct 2023 14:18:49 -0300 Subject: [PATCH 07/25] fix: house_lists version column in database with wrong type (#1716) The column is defined with the type int, which receives a maximum value of 4.294.967.295. Therefore, it was showing an error when saving the database table. The solution I found was to change the type of the version column from INT to BIGINT. --- data-otservbr-global/migrations/40.lua | 9 ++++++--- schema.sql | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/data-otservbr-global/migrations/40.lua b/data-otservbr-global/migrations/40.lua index 6e9828d66b7..22bfea0da95 100644 --- a/data-otservbr-global/migrations/40.lua +++ b/data-otservbr-global/migrations/40.lua @@ -1,8 +1,5 @@ function onUpdateDatabase() logger.info("Updating database to version 41 (optimize house_lists)") - db.query([[ - ALTER TABLE `house_lists` DROP COLUMN `id`; - ]]) db.query([[ ALTER TABLE `house_lists` @@ -10,5 +7,11 @@ function onUpdateDatabase() ADD INDEX `version` (`version`), ADD PRIMARY KEY (`house_id`, `listid`); ]]) + + db.query([[ + ALTER TABLE `house_lists` + MODIFY `version` bigint(20) NOT NULL DEFAULT '0'; + ]]) + return true end diff --git a/schema.sql b/schema.sql index b54d9e2990a..dc58d3a71ac 100644 --- a/schema.sql +++ b/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `server_config` ( CONSTRAINT `server_config_pk` PRIMARY KEY (`config`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '38'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '41'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); -- Table structure `accounts` CREATE TABLE IF NOT EXISTS `accounts` ( @@ -438,7 +438,7 @@ DELIMITER ; CREATE TABLE IF NOT EXISTS `house_lists` ( `house_id` int NOT NULL, `listid` int NOT NULL, - `version` int NOT NULL DEFAULT '0', + `version` bigint NOT NULL DEFAULT '0', `list` text NOT NULL, PRIMARY KEY (`house_id`, `listid`), KEY `house_id_index` (`house_id`), From 4c32854236183cd4341ad38fc8a078ad13918f70 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Fri, 20 Oct 2023 10:20:26 -0700 Subject: [PATCH 08/25] feat: datapack improvements (#1713) Another small collection of datapack improvements. These are small quest fixes, coordinate fixes, etc. --- data-otservbr-global/npc/....lua | 4 +- data-otservbr-global/npc/battlemart.lua | 3 ++ data-otservbr-global/npc/john_bounac.lua | 0 data-otservbr-global/npc/the_bone_master.lua | 2 +- data-otservbr-global/npc/the_dream_master.lua | 2 +- .../scripts/actions/addons/addons.lua | 3 ++ .../scripts/actions/other/bag_you_desire.lua | 39 ++++++++++--------- .../actions/other/exercise_training.lua | 2 +- .../actions/other/others/quest_system1.lua | 4 ++ .../quests/a_pirates_tail/cheesy_key.lua | 39 +++++++++++++++++++ .../feaster_of_souls/dread_maiden_lever.lua | 10 ++--- .../feaster_of_souls/fear_feaster_lever.lua | 10 ++--- .../feaster_of_souls/pale_worm_lever.lua | 20 +++++----- .../feaster_of_souls/unwelcome_lever.lua | 10 ++--- .../actions/quests/first_dragon/rewards.lua | 8 +++- .../quests/first_dragon/treasure_chest.lua | 3 +- .../quests/grimvale/portal_minis_grimvale.lua | 8 ++-- .../actions/quests/lions_rock/lions_rock.lua | 2 +- .../quests/secret_library/ghulosh_lever.lua | 10 ++--- .../quests/secret_library/gorzindel_lever.lua | 4 +- .../quests/secret_library/mazzinor_lever.lua | 10 ++--- .../quests/soul_war/soulwar_entrances.lua | 2 +- .../scripts/actions/tools/rust_remover.lua | 9 +++++ .../quests/a_pirates_tail/ratmiral_death.lua | 20 ++++++++++ .../quests/a_pirates_tail/tentugly_death.lua | 19 +++++++++ .../druid_outfits_blooming_griffinclaw.lua | 2 + .../quests/pits_of_inferno/throne.lua | 1 + data-otservbr-global/world/otservbr-npc.xml | 6 +++ data/libs/achievements_lib.lua | 4 ++ data/libs/functions/player.lua | 10 ++++- data/libs/functions/tile.lua | 10 ++++- .../monster/ondroploot__base.lua | 1 + data/scripts/reward_chest/boss_death.lua | 14 +++---- data/scripts/talkactions/player/bank.lua | 23 +++++------ data/scripts/talkactions/player/bless.lua | 3 +- data/scripts/talkactions/player/buy_house.lua | 14 +++---- .../talkactions/player/leave_house.lua | 6 +-- .../scripts/talkactions/player/sell_house.lua | 6 +-- 38 files changed, 240 insertions(+), 103 deletions(-) create mode 100644 data-otservbr-global/npc/john_bounac.lua create mode 100644 data-otservbr-global/scripts/actions/quests/a_pirates_tail/cheesy_key.lua create mode 100644 data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/ratmiral_death.lua create mode 100644 data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/tentugly_death.lua diff --git a/data-otservbr-global/npc/....lua b/data-otservbr-global/npc/....lua index 8555383b67e..38222c4534a 100644 --- a/data-otservbr-global/npc/....lua +++ b/data-otservbr-global/npc/....lua @@ -14,11 +14,11 @@ npcConfig.outfit = { lookBody = 0, lookLegs = 0, lookFeet = 0, - lookAddons = 0 + lookAddons = 0, } npcConfig.flags = { - floorchange = false + floorchange = false, } local keywordHandler = KeywordHandler:new() diff --git a/data-otservbr-global/npc/battlemart.lua b/data-otservbr-global/npc/battlemart.lua index d8109854bc0..ec668947aa9 100644 --- a/data-otservbr-global/npc/battlemart.lua +++ b/data-otservbr-global/npc/battlemart.lua @@ -86,6 +86,7 @@ npcConfig.shop = { { itemName = "enhanced exercise bow", clientId = 35282, buy = 2340000 }, { itemName = "enhanced exercise club", clientId = 35281, buy = 2340000 }, { itemName = "enhanced exercise rod", clientId = 35283, buy = 2340000 }, + { itemName = "enhanced exercise shield", clientId = 44066, buy = 2340000 }, { itemName = "enhanced exercise sword", clientId = 35279, buy = 2340000 }, { itemName = "enhanced exercise wand", clientId = 35284, buy = 2340000 }, { itemName = "envenomed arrow", clientId = 16143, buy = 12 }, @@ -124,10 +125,12 @@ npcConfig.shop = { { itemName = "magic wall rune", clientId = 3180, buy = 116 }, { itemName = "magma amulet", clientId = 817, buy = 15000 }, { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "mana shield potion", clientId = 35563, buy = 200000 }, { itemName = "masterful exercise axe", clientId = 35286, buy = 2700000 }, { itemName = "masterful exercise bow", clientId = 35288, buy = 2700000 }, { itemName = "masterful exercise club", clientId = 35287, buy = 2700000 }, { itemName = "masterful exercise rod", clientId = 35289, buy = 2700000 }, + { itemName = "masterful exercise shield", clientId = 44067, buy = 2700000 }, { itemName = "masterful exercise sword", clientId = 35285, buy = 2700000 }, { itemName = "masterful exercise wand", clientId = 35290, buy = 2700000 }, { itemName = "might ring", clientId = 3048, buy = 5000 }, diff --git a/data-otservbr-global/npc/john_bounac.lua b/data-otservbr-global/npc/john_bounac.lua new file mode 100644 index 00000000000..e69de29bb2d diff --git a/data-otservbr-global/npc/the_bone_master.lua b/data-otservbr-global/npc/the_bone_master.lua index 0d94d2bac86..67a6322f299 100644 --- a/data-otservbr-global/npc/the_bone_master.lua +++ b/data-otservbr-global/npc/the_bone_master.lua @@ -68,7 +68,7 @@ local function creatureSayCallback(npc, creature, type, message) }, npc, creature) npcHandler:setTopic(playerId, 1) end - elseif MsgContains(message, "advancement") then + elseif MsgContains(message, "advancement") or MsgContains(message, "demonic") or MsgContains(message, "essence") then if player:getStorageValue(Storage.OutfitQuest.BrotherhoodOutfit) == 1 then npcHandler:say("So you want to advance to a {Hyaena} rank? Did you bring 500 demonic essences with you?", npc, creature) npcHandler:setTopic(playerId, 3) diff --git a/data-otservbr-global/npc/the_dream_master.lua b/data-otservbr-global/npc/the_dream_master.lua index cee652f28ee..b282908b375 100644 --- a/data-otservbr-global/npc/the_dream_master.lua +++ b/data-otservbr-global/npc/the_dream_master.lua @@ -67,7 +67,7 @@ local function creatureSayCallback(npc, creature, type, message) }, npc, creature) npcHandler:setTopic(playerId, 1) end - elseif MsgContains(message, "advancement") then + elseif MsgContains(message, "advancement") or MsgContains(message, "demonic") or MsgContains(message, "essence") then if player:getStorageValue(Storage.OutfitQuest.NightmareOutfit) == 1 then npcHandler:say("So you want to advance to a {Initiate} rank? Did you bring 500 demonic essences with you?", npc, creature) npcHandler:setTopic(playerId, 3) diff --git a/data-otservbr-global/scripts/actions/addons/addons.lua b/data-otservbr-global/scripts/actions/addons/addons.lua index 157187d189f..8fea3d0b074 100644 --- a/data-otservbr-global/scripts/actions/addons/addons.lua +++ b/data-otservbr-global/scripts/actions/addons/addons.lua @@ -17,6 +17,9 @@ local config = { -- poltergeist [32630] = { female = 1271, male = 1270, addon = 1, effect = CONST_ME_BLUE_GHOST, achievement = "Mainstreet Nightmare" }, [32631] = { female = 1271, male = 1270, addon = 2, effect = CONST_ME_BLUE_GHOST, achievement = "Mainstreet Nightmare" }, + -- rascoohan + [35595] = { female = 1372, male = 1371, addon = 1, achievement = "Honorary Rascoohan" }, + [35695] = { female = 1372, male = 1371, addon = 2, achievement = "Honorary Rascoohan" }, } local addons = Action() diff --git a/data-otservbr-global/scripts/actions/other/bag_you_desire.lua b/data-otservbr-global/scripts/actions/other/bag_you_desire.lua index 247651b38a0..7cf5357fa57 100644 --- a/data-otservbr-global/scripts/actions/other/bag_you_desire.lua +++ b/data-otservbr-global/scripts/actions/other/bag_you_desire.lua @@ -1,27 +1,30 @@ local rewards = { - { id = 34082, name = "Soulcutter" }, - { id = 34083, name = "Soulshredder" }, - { id = 34084, name = "Soulbiter" }, - { id = 34085, name = "Souleater" }, - { id = 34086, name = "Soulcrusher" }, - { id = 34087, name = "Soulmaimer" }, - { id = 34088, name = "Soulbleeder" }, - { id = 34089, name = "Soulpiercer" }, - { id = 34090, name = "Soultainter" }, - { id = 34091, name = "Soulhexer" }, - { id = 34092, name = "Soulshanks" }, - { id = 34093, name = "Soulstrider" }, - { id = 34094, name = "Soulshell" }, - { id = 34095, name = "Soulmantle" }, - { id = 34096, name = "Soulshroud" }, - { id = 34097, name = "Pair of Soulwalkers" }, - { id = 34098, name = "Pair of Soulstalkers" }, - { id = 34099, name = "Soulbastion" }, + { id = 34082, name = "soulcutter" }, + { id = 34083, name = "soulshredder" }, + { id = 34084, name = "soulbiter" }, + { id = 34085, name = "souleater" }, + { id = 34086, name = "soulcrusher" }, + { id = 34087, name = "soulmaimer" }, + { id = 34097, name = "pair of soulwalkers" }, + { id = 34099, name = "soulbastion" }, + { id = 34088, name = "soulbleeder" }, + { id = 34089, name = "soulpiercer" }, + { id = 34094, name = "soulshell" }, + { id = 34098, name = "pair of soulstalkers" }, + { id = 34090, name = "soultainter" }, + { id = 34092, name = "soulshanks" }, + { id = 34095, name = "soulmantle" }, + { id = 34091, name = "soulhexer" }, + { id = 34093, name = "soulstrider" }, + { id = 34096, name = "soulshroud" }, } local bagyouDesire = Action() function bagyouDesire.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not player then + return false + end local randId = math.random(1, #rewards) local rewardItem = rewards[randId] diff --git a/data-otservbr-global/scripts/actions/other/exercise_training.lua b/data-otservbr-global/scripts/actions/other/exercise_training.lua index d03dc1cb928..353c1459c21 100644 --- a/data-otservbr-global/scripts/actions/other/exercise_training.lua +++ b/data-otservbr-global/scripts/actions/other/exercise_training.lua @@ -48,7 +48,7 @@ function exerciseTraining.onUse(player, item, fromPosition, target, toPosition, playersOnDummy = playersOnDummy + 1 end - if playersOnDummy == maxAllowedOnADummy then + if playersOnDummy >= maxAllowedOnADummy then player:sendTextMessage(MESSAGE_FAILURE, "That exercise dummy is busy.") return true end diff --git a/data-otservbr-global/scripts/actions/other/others/quest_system1.lua b/data-otservbr-global/scripts/actions/other/others/quest_system1.lua index 7fe74d5f729..836ce8b20f5 100644 --- a/data-otservbr-global/scripts/actions/other/others/quest_system1.lua +++ b/data-otservbr-global/scripts/actions/other/others/quest_system1.lua @@ -88,6 +88,10 @@ function questSystem1.onUse(player, item, fromPosition, target, toPosition, isHo end else local container = Container(item.uid) + if not container then + logger.error("[questSystem1.onUse] failed to create container") + return false + end for i = 0, container:getSize() - 1 do local originalItem = container:getItem(i) local newItem = Game.createItem(originalItem.itemid, originalItem.type) diff --git a/data-otservbr-global/scripts/actions/quests/a_pirates_tail/cheesy_key.lua b/data-otservbr-global/scripts/actions/quests/a_pirates_tail/cheesy_key.lua new file mode 100644 index 00000000000..aea03186fa6 --- /dev/null +++ b/data-otservbr-global/scripts/actions/quests/a_pirates_tail/cheesy_key.lua @@ -0,0 +1,39 @@ +local config = { + sorcerer = { + id = 1367, + name = "Bladespark", + }, + druid = { + id = 1364, + name = "Mossmasher", + }, + paladin = { + id = 1366, + name = "Sandscourge", + }, + knight = { + id = 1365, + name = "Snowbash", + }, +} + +local action = Action() + +function action.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local vocation = config[player:getVocation():getBase():getName():lower()] + if not vocation then + return true + end + if player:hasFamiliar(vocation.id) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You already have the " .. vocation.name .. " familiar.") + return false + end + + player:addFamiliar(vocation.id) + item:remove() + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You obtained the " .. vocation.name .. " familiar.") + return true +end + +action:id(35508) +action:register() diff --git a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/dread_maiden_lever.lua b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/dread_maiden_lever.lua index 1f398adc276..92ad5202162 100644 --- a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/dread_maiden_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/dread_maiden_lever.lua @@ -6,11 +6,11 @@ local config = { requiredLevel = 250, playerPositions = { - { pos = Position(33739, 31506, 14), teleport = Position(33711, 31510, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33740, 31506, 14), teleport = Position(33711, 31510, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33741, 31506, 14), teleport = Position(33711, 31510, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33742, 31506, 14), teleport = Position(33711, 31510, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33743, 31506, 14), teleport = Position(33711, 31510, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33739, 31506, 14), teleport = Position(33712, 31509, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33740, 31506, 14), teleport = Position(33712, 31509, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33741, 31506, 14), teleport = Position(33712, 31509, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33742, 31506, 14), teleport = Position(33712, 31509, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33743, 31506, 14), teleport = Position(33712, 31509, 14), effect = CONST_ME_TELEPORT }, }, specPos = { from = Position(33703, 31494, 14), diff --git a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/fear_feaster_lever.lua b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/fear_feaster_lever.lua index ffb9a98df82..f671cb1e136 100644 --- a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/fear_feaster_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/fear_feaster_lever.lua @@ -6,11 +6,11 @@ local config = { requiredLevel = 250, playerPositions = { - { pos = Position(33734, 31471, 14), teleport = Position(33711, 31476, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33735, 31471, 14), teleport = Position(33711, 31476, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33736, 31471, 14), teleport = Position(33711, 31476, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33737, 31471, 14), teleport = Position(33711, 31476, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33738, 31471, 14), teleport = Position(33711, 31476, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33734, 31471, 14), teleport = Position(33711, 31474, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33735, 31471, 14), teleport = Position(33711, 31474, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33736, 31471, 14), teleport = Position(33711, 31474, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33737, 31471, 14), teleport = Position(33711, 31474, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33738, 31471, 14), teleport = Position(33711, 31474, 14), effect = CONST_ME_TELEPORT }, }, specPos = { from = Position(33705, 31463, 14), diff --git a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/pale_worm_lever.lua b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/pale_worm_lever.lua index 537bbf2aa32..74b49470618 100644 --- a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/pale_worm_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/pale_worm_lever.lua @@ -7,16 +7,16 @@ local config = { timeToDefeat = 25 * 60, playerPositions = { - { pos = Position(33772, 31504, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33773, 31504, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33774, 31504, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33775, 31504, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33773, 31503, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33774, 31503, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33775, 31503, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33773, 31505, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33774, 31505, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33775, 31505, 14), teleport = Position(33808, 31515, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33772, 31504, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33773, 31504, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33774, 31504, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33775, 31504, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33773, 31503, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33774, 31503, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33775, 31503, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33773, 31505, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33774, 31505, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33775, 31505, 14), teleport = Position(33808, 31513, 14), effect = CONST_ME_TELEPORT }, }, specPos = { from = Position(33793, 31496, 14), diff --git a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/unwelcome_lever.lua b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/unwelcome_lever.lua index 59f1665eacf..93c4e833b9e 100644 --- a/data-otservbr-global/scripts/actions/quests/feaster_of_souls/unwelcome_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/feaster_of_souls/unwelcome_lever.lua @@ -6,11 +6,11 @@ local config = { requiredLevel = 250, playerPositions = { - { pos = Position(33736, 31537, 14), teleport = Position(33708, 31547, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33737, 31537, 14), teleport = Position(33708, 31547, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33738, 31537, 14), teleport = Position(33708, 31547, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33739, 31537, 14), teleport = Position(33708, 31547, 14), effect = CONST_ME_TELEPORT }, - { pos = Position(33740, 31537, 14), teleport = Position(33708, 31547, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33736, 31537, 14), teleport = Position(33707, 31545, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33737, 31537, 14), teleport = Position(33707, 31545, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33738, 31537, 14), teleport = Position(33707, 31545, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33739, 31537, 14), teleport = Position(33707, 31545, 14), effect = CONST_ME_TELEPORT }, + { pos = Position(33740, 31537, 14), teleport = Position(33707, 31545, 14), effect = CONST_ME_TELEPORT }, }, specPos = { from = Position(33699, 31529, 14), diff --git a/data-otservbr-global/scripts/actions/quests/first_dragon/rewards.lua b/data-otservbr-global/scripts/actions/quests/first_dragon/rewards.lua index c855009543e..2d1c8ac2a90 100644 --- a/data-otservbr-global/scripts/actions/quests/first_dragon/rewards.lua +++ b/data-otservbr-global/scripts/actions/quests/first_dragon/rewards.lua @@ -6,7 +6,7 @@ local bpItems = { { name = "gold token", count = 3 }, { name = "blue gem", count = 1 }, { name = "yellow gem", count = 1 }, - { name = "red gem", count = 1 }, + { id = 3039, count = 1 }, -- red gem { name = "demon horn", count = 2 }, { name = "slime heart", count = 2 }, { name = "energy vein", count = 2 }, @@ -46,7 +46,11 @@ function finalReward.onUse(player, item, fromPosition, target, toPosition, isHot local bp = Game.createItem("Backpack", 1) if bp then for i = 1, #bpItems do - bp:addItem(bpItems[i].name, count) + if bpItems[i].id then + bp:addItem(bpItems[i].id, count) + else + bp:addItem(bpItems[i].name, count) + end end bp:moveTo(player) end diff --git a/data-otservbr-global/scripts/actions/quests/first_dragon/treasure_chest.lua b/data-otservbr-global/scripts/actions/quests/first_dragon/treasure_chest.lua index b82c52cce56..d40aec690d2 100644 --- a/data-otservbr-global/scripts/actions/quests/first_dragon/treasure_chest.lua +++ b/data-otservbr-global/scripts/actions/quests/first_dragon/treasure_chest.lua @@ -4,7 +4,8 @@ local UniqueTable = { count = 1, }, [14002] = { - name = "gold nugget", + -- gold nugget, + itemId = 3040, count = 2, }, [14003] = { diff --git a/data-otservbr-global/scripts/actions/quests/grimvale/portal_minis_grimvale.lua b/data-otservbr-global/scripts/actions/quests/grimvale/portal_minis_grimvale.lua index 109c04189fe..09c464fc7f6 100644 --- a/data-otservbr-global/scripts/actions/quests/grimvale/portal_minis_grimvale.lua +++ b/data-otservbr-global/scripts/actions/quests/grimvale/portal_minis_grimvale.lua @@ -4,7 +4,7 @@ local config = { bossName = "Bloodback", timeToFightAgain = 10, -- In hour timeToDefeat = 10, -- In minutes - destination = Position(33180, 32012, 8), + destination = Position(33182, 32012, 8), bossPosition = Position(33184, 32016, 8), specPos = { from = Position(33174, 32007, 8), @@ -32,7 +32,7 @@ local config = { bossName = "Sharpclaw", timeToFightAgain = 10, -- In hour timeToDefeat = 10, -- In minutes - destination = Position(33120, 31997, 9), + destination = Position(33121, 31998, 9), bossPosition = Position(33120, 32002, 9), specPos = { from = Position(33113, 31994, 9), @@ -60,7 +60,7 @@ local config = { bossName = "Black Vixen", timeToFightAgain = 10, -- In hour timeToDefeat = 10, -- In minutes - destination = Position(33447, 32040, 9), + destination = Position(33448, 32038, 9), bossPosition = Position(33450, 32034, 9), specPos = { from = Position(33442, 32027, 9), @@ -74,7 +74,7 @@ local config = { exitPosition = Position(33167, 31978, 8), }, [7] = { - teleportPosition = { x = 33055, y = 31888, z = 9 }, + teleportPosition = { x = 33056, y = 31890, z = 9 }, exitPosition = Position(33055, 31911, 9), }, [8] = { diff --git a/data-otservbr-global/scripts/actions/quests/lions_rock/lions_rock.lua b/data-otservbr-global/scripts/actions/quests/lions_rock/lions_rock.lua index cb945df5a47..652ba60f1ef 100644 --- a/data-otservbr-global/scripts/actions/quests/lions_rock/lions_rock.lua +++ b/data-otservbr-global/scripts/actions/quests/lions_rock/lions_rock.lua @@ -5,7 +5,7 @@ local rewards = { "giant shimmering pearl", "gold ingot", "green gem", - "red gem", + 3039, -- red gem "lion's heart", "yellow gem", } diff --git a/data-otservbr-global/scripts/actions/quests/secret_library/ghulosh_lever.lua b/data-otservbr-global/scripts/actions/quests/secret_library/ghulosh_lever.lua index dcf90e0a0ed..7e7e3d57c3c 100644 --- a/data-otservbr-global/scripts/actions/quests/secret_library/ghulosh_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/secret_library/ghulosh_lever.lua @@ -6,11 +6,11 @@ local config = { requiredLevel = 250, playerPositions = { - { pos = Position(32747, 32773, 10), teleport = Position(32756, 32729, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32748, 32773, 10), teleport = Position(32756, 32729, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32749, 32773, 10), teleport = Position(32756, 32729, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32750, 32773, 10), teleport = Position(32756, 32729, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32751, 32773, 10), teleport = Position(32756, 32729, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32747, 32773, 10), teleport = Position(32757, 32727, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32748, 32773, 10), teleport = Position(32757, 32727, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32749, 32773, 10), teleport = Position(32757, 32727, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32750, 32773, 10), teleport = Position(32757, 32727, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32751, 32773, 10), teleport = Position(32757, 32727, 10), effect = CONST_ME_TELEPORT }, }, specPos = { from = Position(32748, 32713, 10), diff --git a/data-otservbr-global/scripts/actions/quests/secret_library/gorzindel_lever.lua b/data-otservbr-global/scripts/actions/quests/secret_library/gorzindel_lever.lua index 082f94ef8a5..fe2fcd74cda 100644 --- a/data-otservbr-global/scripts/actions/quests/secret_library/gorzindel_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/secret_library/gorzindel_lever.lua @@ -9,8 +9,8 @@ local config = { { pos = Position(32747, 32749, 10), teleport = Position(32686, 32721, 10), effect = CONST_ME_TELEPORT }, { pos = Position(32748, 32749, 10), teleport = Position(32686, 32721, 10), effect = CONST_ME_TELEPORT }, { pos = Position(32749, 32749, 10), teleport = Position(32686, 32721, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32747, 32750, 10), teleport = Position(32686, 32721, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32747, 32751, 10), teleport = Position(32686, 32721, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32750, 32749, 10), teleport = Position(32686, 32721, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32751, 32749, 10), teleport = Position(32686, 32721, 10), effect = CONST_ME_TELEPORT }, }, specPos = { from = Position(32680, 32711, 10), diff --git a/data-otservbr-global/scripts/actions/quests/secret_library/mazzinor_lever.lua b/data-otservbr-global/scripts/actions/quests/secret_library/mazzinor_lever.lua index 4071002a863..eb7afac5509 100644 --- a/data-otservbr-global/scripts/actions/quests/secret_library/mazzinor_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/secret_library/mazzinor_lever.lua @@ -6,11 +6,11 @@ local config = { requiredLevel = 250, playerPositions = { - { pos = Position(32721, 32773, 10), teleport = Position(32725, 32728, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32721, 32773, 10), teleport = Position(32725, 32728, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32721, 32773, 10), teleport = Position(32725, 32728, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32721, 32773, 10), teleport = Position(32725, 32728, 10), effect = CONST_ME_TELEPORT }, - { pos = Position(32721, 32773, 10), teleport = Position(32725, 32728, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32721, 32773, 10), teleport = Position(32726, 32726, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32722, 32773, 10), teleport = Position(32726, 32726, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32723, 32773, 10), teleport = Position(32726, 32726, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32724, 32773, 10), teleport = Position(32726, 32726, 10), effect = CONST_ME_TELEPORT }, + { pos = Position(32725, 32773, 10), teleport = Position(32726, 32726, 10), effect = CONST_ME_TELEPORT }, }, specPos = { from = Position(32716, 32713, 10), diff --git a/data-otservbr-global/scripts/actions/quests/soul_war/soulwar_entrances.lua b/data-otservbr-global/scripts/actions/quests/soul_war/soulwar_entrances.lua index fc9f5929854..51f8281a04d 100644 --- a/data-otservbr-global/scripts/actions/quests/soul_war/soulwar_entrances.lua +++ b/data-otservbr-global/scripts/actions/quests/soul_war/soulwar_entrances.lua @@ -9,7 +9,7 @@ local config = { { position = { x = 34022, y = 31091, z = 11 }, destination = { x = 33685, y = 31599, z = 14 } }, -- goshnar's malice entrance { position = { x = 33856, y = 31884, z = 5 }, destination = { x = 33857, y = 31865, z = 6 } }, -- goshnar's cruelty entrance { position = { x = 33889, y = 31873, z = 3 }, destination = { x = 33830, y = 31881, z = 4 } }, -- 1st to 2nd floor cloak - { position = { x = 33829, y = 31880, z = 4 }, destination = { x = 33856, y = 31890, z = 5 } }, -- 2nd to 3rd floor cloak + { position = { x = 33829, y = 31880, z = 4 }, destination = { x = 33856, y = 31889, z = 5 } }, -- 2nd to 3rd floor cloak } local portal = { position = { x = 33914, y = 31032, z = 12 }, destination = { x = 33780, y = 31601, z = 14 } } -- goshnar's hatred entrance diff --git a/data-otservbr-global/scripts/actions/tools/rust_remover.lua b/data-otservbr-global/scripts/actions/tools/rust_remover.lua index f5adad4511f..93e64a66292 100644 --- a/data-otservbr-global/scripts/actions/tools/rust_remover.lua +++ b/data-otservbr-global/scripts/actions/tools/rust_remover.lua @@ -1,6 +1,7 @@ local CHAIN_ARMOR, SCALE_ARMOR, BRASS_ARMOR, PLATE_ARMOR, KNIGHT_ARMOR, PALADIN_ARMOR, CROWN_ARMOR, GOLDEN_ARMOR, DRAGON_SCALE_MAIL, MAGIC_PLATE_ARMOR = 3358, 3377, 3359, 3357, 3370, 8063, 3381, 3360, 3386, 3366 local STUDDED_LEGS, CHAIN_LEGS, BRASS_LEGS, PLATE_LEGS, KNIGHT_LEGS, CROWN_LEGS, GOLDEN_LEGS = 3362, 3558, 3372, 3557, 3371, 3382, 3364 local BRASS_HELMET, IRON_HELMET, STEEL_HELMET, CROWN_HELMET, CRUSADER_HELMET, ROYAL_HELMET = 3354, 3353, 3351, 3385, 3391, 3392 +local PLATE_SHIELD, ANCIENT_SHIELD, NORSE_SHIELD, CROWN_SHIELD, VAMPIRE_SHIELD = 3410, 3432, 7460, 3419, 3434 local config = { [8894] = { -- common rusty armor @@ -71,6 +72,14 @@ local config = { { { 97501, 99000 }, CROWN_LEGS }, { { 99001, 100000 }, GOLDEN_LEGS }, }, + [8902] = { -- slightly rusted shield + { { 1, 28070 } }, + { { 28071, 59440 }, PLATE_SHIELD }, + { { 59441, 88310 }, ANCIENT_SHIELD }, + { { 88311, 97601 }, NORSE_SHIELD }, + { { 97602, 99901 }, CROWN_SHIELD }, + { { 99902, 100000 }, VAMPIRE_SHIELD }, + }, } local rustRemover = Action() diff --git a/data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/ratmiral_death.lua b/data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/ratmiral_death.lua new file mode 100644 index 00000000000..ea00d6ead07 --- /dev/null +++ b/data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/ratmiral_death.lua @@ -0,0 +1,20 @@ +local event = CreatureEvent("RatmiralBlackwhiskersDeath") +local outfits = { 1371, 1372 } + +function event.onDeath(creature, corpse, killer, mostDamage, unjustified, mostDamage_unjustified) + if not creature or not creature:getMonster() then + return + end + local damageMap = creature:getMonster():getDamageMap() + + for key, _ in pairs(damageMap) do + local player = Player(key) + if player and not player:hasOutfit(outfits[1]) and not player:hasOutfit(outfits[2]) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations you received the Rascoohan Outfit.") + player:addOutfit(1371, 0) + player:addOutfit(1372, 0) + end + end +end + +event:register() diff --git a/data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/tentugly_death.lua b/data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/tentugly_death.lua new file mode 100644 index 00000000000..58eeed5e10d --- /dev/null +++ b/data-otservbr-global/scripts/creaturescripts/quests/a_pirates_tail/tentugly_death.lua @@ -0,0 +1,19 @@ +local tentuglysHeadDeath = CreatureEvent("TentuglysHeadDeath") + +function tentuglysHeadDeath.onDeath(creature, corpse, killer, mostDamage, unjustified, mostDamage_unjustified) + if not creature or not creature:getMonster() then + return + end + local damageMap = creature:getMonster():getDamageMap() + + for key, value in pairs(damageMap) do + local player = Player(key) + if player then + player:setStorageValue(Storage.Quest.U12_60.APiratesTail.TentuglyKilled, 1) -- Access to wreckoning + player:addAchievement("Release the Kraken") + player:addMount(175) + end + end +end + +tentuglysHeadDeath:register() diff --git a/data-otservbr-global/scripts/globalevents/quests/druid_outfits_blooming_griffinclaw.lua b/data-otservbr-global/scripts/globalevents/quests/druid_outfits_blooming_griffinclaw.lua index 0943af42595..fbb1dc44d05 100644 --- a/data-otservbr-global/scripts/globalevents/quests/druid_outfits_blooming_griffinclaw.lua +++ b/data-otservbr-global/scripts/globalevents/quests/druid_outfits_blooming_griffinclaw.lua @@ -8,6 +8,7 @@ local function decayFlower(tile) end local function bloom() + -- 1/7 chance of blooming if math.random(7) ~= 1 then addEvent(bloom, 60 * 60 * 1000) return @@ -17,6 +18,7 @@ local function bloom() if not tile then return false end + local item = tile:getItemById(5687) if item then item:transform(5658) diff --git a/data-otservbr-global/scripts/movements/quests/pits_of_inferno/throne.lua b/data-otservbr-global/scripts/movements/quests/pits_of_inferno/throne.lua index c910622a5ab..92e1c656673 100644 --- a/data-otservbr-global/scripts/movements/quests/pits_of_inferno/throne.lua +++ b/data-otservbr-global/scripts/movements/quests/pits_of_inferno/throne.lua @@ -58,6 +58,7 @@ function throne.onStepIn(creature, item, position, fromPosition) if player:getStorageValue(throne.storage) ~= 1 then player:setStorageValue(throne.storage, 1) + player:setStorageValue(Storage.PitsOfInferno.ShortcutHubDoor, 1) player:getPosition():sendMagicEffect(throne.effect) player:say(throne.text, TALKTYPE_MONSTER_SAY) else diff --git a/data-otservbr-global/world/otservbr-npc.xml b/data-otservbr-global/world/otservbr-npc.xml index 68721530346..b82ea1ad59a 100644 --- a/data-otservbr-global/world/otservbr-npc.xml +++ b/data-otservbr-global/world/otservbr-npc.xml @@ -117,6 +117,9 @@ + + + @@ -2223,6 +2226,9 @@ + + + diff --git a/data/libs/achievements_lib.lua b/data/libs/achievements_lib.lua index 2ec9b5ad318..49318843c1c 100644 --- a/data/libs/achievements_lib.lua +++ b/data/libs/achievements_lib.lua @@ -574,6 +574,10 @@ achievements = { [477] = { name = "Waypoint Explorer", grade = 1, points = 1, description = "You've explored all the towns of Tibia and discovered each town's waypoint." }, [478] = { name = "Up the Molehill", grade = 1, points = 3, description = "Putting this candle stump on your new mount was kind of a waiting game. You're even tempted to call it whack-a-mole. But in the end you found a loyal companion for your journeys into the depths." }, [479] = { name = "Inquisition's Arm", grade = 1, points = 2, description = "Your special outfit, bestowed exclusively on a dedicated hand of the Inquisition, is now complete." }, + + --12.60 + [480] = { name = "Honorary Rascoohan", grade = 1, points = 2, description = "When in Rascacoon, do as the Rascoohans do!" }, + [481] = { name = "Release the Kraken", grade = 1, points = 3, description = "Riding around on this squishy companion gives you the feeling of flying through the air... uhm... swimming through the seven seas!" }, } ACHIEVEMENT_FIRST = 1 diff --git a/data/libs/functions/player.lua b/data/libs/functions/player.lua index c2ae610cfd2..18737d66f3e 100644 --- a/data/libs/functions/player.lua +++ b/data/libs/functions/player.lua @@ -203,7 +203,15 @@ function Player.transferMoneyTo(self, target, amount) if not target then return false end - return Bank.transfer(self, target, amount) + if not Bank.transfer(self, target, amount) then + return false + end + + local targetPlayer = Player(target) + if targetPlayer then + targetPlayer:sendTextMessage(MESSAGE_LOOK, self:getName() .. " has transferred " .. FormatNumber(amount) .. " gold coins to you.") + end + return true end function Player.withdrawMoney(self, amount) diff --git a/data/libs/functions/tile.lua b/data/libs/functions/tile.lua index 0201459018c..0e35d24afbd 100644 --- a/data/libs/functions/tile.lua +++ b/data/libs/functions/tile.lua @@ -46,6 +46,9 @@ function Tile:isWalkable(pz, creature, floorchange, block, proj) if self:hasProperty(CONST_PROP_BLOCKSOLID) or self:hasProperty(CONST_PROP_BLOCKPROJECTILE) then return false end + if self:hasProperty(CONST_PROP_IMMOVABLEBLOCKSOLID) then + return false + end if pz and (self:hasFlag(TILESTATE_HOUSE) or self:hasFlag(TILESTATE_PROTECTIONZONE)) then return false end @@ -56,6 +59,9 @@ function Tile:isWalkable(pz, creature, floorchange, block, proj) return false end if block then + if self:hasProperty(CONST_PROP_BLOCKPATH) or self:hasProperty(CONST_PROP_IMMOVABLEBLOCKPATH) or self:hasProperty(CONST_PROP_IMMOVABLENOFIELDBLOCKPATH) or self:hasProperty(CONST_PROP_NOFIELDBLOCKPATH) then + return false + end local topStackItem = self:getTopTopItem() if topStackItem and topStackItem:hasProperty(CONST_PROP_BLOCKPATH) then return false @@ -66,7 +72,9 @@ function Tile:isWalkable(pz, creature, floorchange, block, proj) if #items > 0 then for i = 1, #items do local itemType = ItemType(items[i]) - if itemType:getType() ~= ITEM_TYPE_MAGICFIELD and not itemType:isMovable() and items[i]:hasProperty(CONST_PROP_BLOCKSOLID) then + local blockSolid = items[i]:hasProperty(CONST_PROP_BLOCKSOLID) + local blockProjectile = items[i]:hasProperty(CONST_PROP_BLOCKPROJECTILE) + if itemType:getType() ~= ITEM_TYPE_MAGICFIELD and not itemType:isMovable() and (blockSolid or blockProjectile) then return false end end diff --git a/data/scripts/eventcallbacks/monster/ondroploot__base.lua b/data/scripts/eventcallbacks/monster/ondroploot__base.lua index 5cdf62b5893..c30f0026316 100644 --- a/data/scripts/eventcallbacks/monster/ondroploot__base.lua +++ b/data/scripts/eventcallbacks/monster/ondroploot__base.lua @@ -11,6 +11,7 @@ function callback.monsterOnDropLoot(monster, corpse) end local mType = monster:getType() if not mType then + logger.warning("monsterOnDropLoot: monster has no type") return end diff --git a/data/scripts/reward_chest/boss_death.lua b/data/scripts/reward_chest/boss_death.lua index 9e33d0bebd7..7b3f992b384 100644 --- a/data/scripts/reward_chest/boss_death.lua +++ b/data/scripts/reward_chest/boss_death.lua @@ -13,7 +13,7 @@ function bossDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUn local monsterType = creature:getType() -- Make sure it is a boss if monsterType and monsterType:isRewardBoss() then - if not corpse:isContainer() then + if not corpse.isContainer or not corpse:isContainer() then logger.warn("[bossDeath.onDeath] Corpse (id: {}) for reward boss {} is not a container.", corpse:getId(), creature:getName()) end corpse:registerReward() @@ -83,12 +83,12 @@ function bossDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUn local rolls = 1 local isBoostedBoss = creature:getName():lower() == (Game.getBoostedBoss()):lower() local bossRaceIds = { player:getSlotBossId(1), player:getSlotBossId(2) } - local isBoss = table.contains(bossRaceIds, monsterType:bossRaceId()) or isBoostedBoss - if isBoss and monsterType:bossRaceId() ~= 0 then - if monsterType:bossRaceId() == player:getSlotBossId(1) then - rolls = rolls + player:getBossBonus(1) / 100 - elseif monsterType:bossRaceId() == player:getSlotBossId(2) then - rolls = rolls + player:getBossBonus(2) / 100 + local isBoss = table.contains(bossRaceIds, monsterType:raceId()) or isBoostedBoss + if isBoss and monsterType:raceId() ~= 0 then + if monsterType:raceId() == player:getSlotBossId(1) then + rolls = rolls + player:getBossBonus(1) / 100.0 + elseif monsterType:raceId() == player:getSlotBossId(2) then + rolls = rolls + player:getBossBonus(2) / 100.0 else rolls = rolls + configManager.getNumber(configKeys.BOOSTED_BOSS_LOOT_BONUS) / 100 end diff --git a/data/scripts/talkactions/player/bank.lua b/data/scripts/talkactions/player/bank.lua index b25c2811938..19acaf4a239 100644 --- a/data/scripts/talkactions/player/bank.lua +++ b/data/scripts/talkactions/player/bank.lua @@ -11,6 +11,7 @@ local balance = TalkAction("!balance") function balance.onSay(player, words, param) player:sendTextMessage(config.messageStyle, "Your current bank balance is " .. FormatNumber(Bank.balance(player)) .. ".") + return true end balance:separator(" ") @@ -27,17 +28,17 @@ function deposit.onSay(player, words, param) amount = tonumber(param) if not amount or amount <= 0 and isValidMoney(amount) then player:sendTextMessage(config.messageStyle, "Invalid amount.") - return false + return true end end if not Bank.deposit(player, amount) then player:sendTextMessage(config.messageStyle, "You don't have enough money.") - return false + return true end player:sendTextMessage(config.messageStyle, "You have deposited " .. FormatNumber(amount) .. " gold coins.") - return false + return true end deposit:separator(" ") @@ -50,16 +51,16 @@ function withdraw.onSay(player, words, param) local amount = tonumber(param) if not amount or amount <= 0 and isValidMoney(amount) then player:sendTextMessage(config.messageStyle, "Invalid amount.") - return false + return true end if not Bank.withdraw(player, amount) then player:sendTextMessage(config.messageStyle, "You don't have enough money.") - return false + return true end player:sendTextMessage(config.messageStyle, "You have withdrawn " .. FormatNumber(amount) .. " gold coins.") - return false + return true end withdraw:separator(" ") @@ -74,23 +75,23 @@ function transfer.onSay(player, words, param) local amount = tonumber(split[2]) if not amount or amount <= 0 and isValidMoney(amount) then player:sendTextMessage(config.messageStyle, "Invalid amount.") - return false + return true end local normalizedName = Game.getNormalizedPlayerName(name) if not normalizedName then player:sendTextMessage(config.messageStyle, "A player with name " .. name .. " does not exist.") - return false + return true end name = normalizedName - if not Bank.transfer(player, name, amount) then + if not player:transferMoneyTo(name, amount) then player:sendTextMessage(config.messageStyle, "You don't have enough money.") - return false + return true end player:sendTextMessage(config.messageStyle, "You have transferred " .. FormatNumber(amount) .. " gold coins to " .. name .. ".") - return false + return true end transfer:separator(" ") diff --git a/data/scripts/talkactions/player/bless.lua b/data/scripts/talkactions/player/bless.lua index 0f91986f556..9e4c60825ce 100644 --- a/data/scripts/talkactions/player/bless.lua +++ b/data/scripts/talkactions/player/bless.lua @@ -1,7 +1,8 @@ local bless = TalkAction("!bless") function bless.onSay(player, words, param) - return Blessings.BuyAllBlesses(player) + Blessings.BuyAllBlesses(player) + return true end bless:groupType("normal") diff --git a/data/scripts/talkactions/player/buy_house.lua b/data/scripts/talkactions/player/buy_house.lua index 0d32e233d87..90b33aef735 100644 --- a/data/scripts/talkactions/player/buy_house.lua +++ b/data/scripts/talkactions/player/buy_house.lua @@ -8,13 +8,13 @@ function buyHouse.onSay(player, words, param) if not player:isPremium() then player:sendCancelMessage("You need a premium account.") - return false + return true end local houseBuyLevel = configManager.getNumber(configKeys.HOUSE_BUY_LEVEL) if player:getLevel() < houseBuyLevel then player:sendCancelMessage("You need to be level " .. houseBuyLevel .. " to buy a house.") - return false + return true end local position = player:getPosition() @@ -27,17 +27,17 @@ function buyHouse.onSay(player, words, param) if not house or playerPos ~= houseEntry then player:sendCancelMessage("You have to be looking at the door of the house you would like to buy.") - return false + return true end if house:getOwnerGuid() > 0 then player:sendCancelMessage("This house already has an owner.") - return false + return true end if player:getHouse() then player:sendCancelMessage("You are already the owner of a house.") - return false + return true end if house:hasItemOnTile() then @@ -48,12 +48,12 @@ function buyHouse.onSay(player, words, param) local price = house:getPrice() if not player:removeMoneyBank(price) then player:sendCancelMessage("You do not have enough money.") - return false + return true end house:setHouseOwner(player:getGuid()) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have successfully bought this house, be sure to have the money for the rent in the bank.") - return false + return true end buyHouse:separator(" ") diff --git a/data/scripts/talkactions/player/leave_house.lua b/data/scripts/talkactions/player/leave_house.lua index 694dbdb2f5b..8743ed17a23 100644 --- a/data/scripts/talkactions/player/leave_house.lua +++ b/data/scripts/talkactions/player/leave_house.lua @@ -7,13 +7,13 @@ function leaveHouse.onSay(player, words, param) if not house then player:sendCancelMessage("You are not inside a house.") playerPosition:sendMagicEffect(CONST_ME_POFF) - return false + return true end if house:getOwnerGuid() ~= player:getGuid() then player:sendCancelMessage("You are not the owner of this house.") playerPosition:sendMagicEffect(CONST_ME_POFF) - return false + return true end if house:hasNewOwnership() then @@ -39,7 +39,7 @@ function leaveHouse.onSay(player, words, param) house:setNewOwnerGuid(0) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have successfully left your house.") playerPosition:sendMagicEffect(CONST_ME_POFF) - return false + return true end leaveHouse:separator(" ") diff --git a/data/scripts/talkactions/player/sell_house.lua b/data/scripts/talkactions/player/sell_house.lua index 27ad85e61ad..c96cb5f71c3 100644 --- a/data/scripts/talkactions/player/sell_house.lua +++ b/data/scripts/talkactions/player/sell_house.lua @@ -4,20 +4,20 @@ function sellHouse.onSay(player, words, param) local tradePartner = Player(param) if not tradePartner or tradePartner == player then player:sendCancelMessage("Trade player not found.") - return false + return true end local house = player:getTile():getHouse() if not house then player:sendCancelMessage("You must stand in your house to initiate the trade.") - return false + return true end local returnValue = house:startTrade(player, tradePartner) if returnValue ~= RETURNVALUE_NOERROR then player:sendCancelMessage(returnValue) end - return false + return true end sellHouse:separator(" ") From 14c7500d9d149ce24f624a3db5092a05e4e83ae4 Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Fri, 20 Oct 2023 14:24:18 -0300 Subject: [PATCH 09/25] improve: ip cache (#1691) The getIP is used a lot in the project and is a value that doesn't change, so we can cache it to avoid unnecessary lock. --- src/server/network/connection/connection.cpp | 12 +++++++----- src/server/network/connection/connection.hpp | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/server/network/connection/connection.cpp b/src/server/network/connection/connection.cpp index 204ff83834e..8e637c053ee 100644 --- a/src/server/network/connection/connection.cpp +++ b/src/server/network/connection/connection.cpp @@ -54,6 +54,7 @@ void Connection::close(bool force) { ConnectionManager::getInstance().releaseConnection(shared_from_this()); std::lock_guard lockClass(connectionLock); + ip = 0; if (connectionState == CONNECTION_STATE_CLOSED) { return; } @@ -319,16 +320,17 @@ void Connection::internalWorker() { } uint32_t Connection::getIP() { + if (ip != 1) { + return ip; + } + std::lock_guard lockClass(connectionLock); // IP-address is expressed in network byte order std::error_code error; const asio::ip::tcp::endpoint endpoint = socket.remote_endpoint(error); - if (error) { - return 0; - } - - return htonl(endpoint.address().to_v4().to_ulong()); + ip = error ? 0 : htonl(endpoint.address().to_v4().to_uint()); + return ip; } void Connection::internalSend(const OutputMessage_ptr &outputMessage) { diff --git a/src/server/network/connection/connection.hpp b/src/server/network/connection/connection.hpp index c240046a01c..3a80bf6ca54 100644 --- a/src/server/network/connection/connection.hpp +++ b/src/server/network/connection/connection.hpp @@ -101,6 +101,7 @@ class Connection : public std::enable_shared_from_this { time_t timeConnected; uint32_t packetsSent = 0; + uint32_t ip = 1; std::underlying_type_t connectionState = CONNECTION_STATE_OPEN; bool receivedFirst = false; From 85bc99751541ab2fd42889634b75360228d70c46 Mon Sep 17 00:00:00 2001 From: Majesty <32709570+majestyotbr@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:01:07 -0300 Subject: [PATCH 10/25] build: fix visual studio solution (#1708) --- vcproj/canary.vcxproj | 2 +- vcproj/settings.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index 7b467346d39..bdb891be8df 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -473,7 +473,7 @@ $(ProjectName)-sln - false + true false diff --git a/vcproj/settings.props b/vcproj/settings.props index aa958c24b0b..9f01a78902b 100644 --- a/vcproj/settings.props +++ b/vcproj/settings.props @@ -51,7 +51,7 @@ Level3 true true - MultiThreaded + MultiThreadedDLL
$(CANARY_LIBDEPS) From 8e3d4a060e5a802eacf1e3870c7a100416d64b35 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Fri, 20 Oct 2023 16:20:11 -0700 Subject: [PATCH 11/25] fix: divine grenade (#1715) --- src/creatures/players/wheel/player_wheel.cpp | 56 ++++++++++++++++--- .../players/wheel/wheel_definitions.hpp | 4 +- src/game/game.cpp | 7 +++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/creatures/players/wheel/player_wheel.cpp b/src/creatures/players/wheel/player_wheel.cpp index e6b02777828..7f97edbb54b 100644 --- a/src/creatures/players/wheel/player_wheel.cpp +++ b/src/creatures/players/wheel/player_wheel.cpp @@ -1146,6 +1146,14 @@ void PlayerWheel::registerPlayerBonusData() { setSpellInstant("Divine Empowerment", false); } + if (m_playerBonusData.stages.divineGrenade > 0) { + for (int i = 0; i < m_playerBonusData.stages.divineGrenade; ++i) { + setSpellInstant("Divine Grenade", true); + } + } else { + setSpellInstant("Divine Grenade", false); + } + if (m_playerBonusData.stages.drainBody > 0) { for (int i = 0; i < m_playerBonusData.stages.drainBody; ++i) { setSpellInstant("Drain Body", true); @@ -1328,6 +1336,9 @@ void PlayerWheel::printPlayerWheelMethodsBonusData(const PlayerWheelMethodsBonus if (bonusData.stages.divineEmpowerment > 0) { g_logger().debug(" divineEmpowerment: {}", bonusData.stages.divineEmpowerment); } + if (bonusData.stages.divineGrenade > 0) { + g_logger().debug(" divineGrenade: {}", bonusData.stages.divineGrenade); + } if (bonusData.stages.blessingOfTheGrove > 0) { g_logger().debug(" blessingOfTheGrove: {}", bonusData.stages.blessingOfTheGrove); } @@ -1435,6 +1446,7 @@ void PlayerWheel::loadRevelationPerks() { addSpellToVector("Great Death Beam"); } } else if (vocationEnum == Vocation_t::VOCATION_PALADIN_CIP) { + m_playerBonusData.stages.divineGrenade = redStageValue; for (uint8_t i = 0; i < redStageValue; ++i) { addSpellToVector("Divine Grenade"); } @@ -1791,7 +1803,7 @@ bool PlayerWheel::checkCombatMastery() { bool PlayerWheel::checkDivineEmpowerment() { bool updateClient = false; - setOnThinkTimer(WheelOnThink_t::DIVINE_EMPOWERMENT, OTSYS_TIME() + 2000); + setOnThinkTimer(WheelOnThink_t::DIVINE_EMPOWERMENT, OTSYS_TIME() + 1000); const auto tile = m_player.getTile(); if (!tile) { @@ -1821,16 +1833,34 @@ bool PlayerWheel::checkDivineEmpowerment() { } else if (stage >= 1) { damageBonus = 8; } - - if (damageBonus != getMajorStat(WheelMajor_t::DAMAGE)) { - setMajorStat(WheelMajor_t::DAMAGE, damageBonus); - updateClient = true; - } + } + if (damageBonus != getMajorStat(WheelMajor_t::DAMAGE)) { + setMajorStat(WheelMajor_t::DAMAGE, damageBonus); + updateClient = true; } return updateClient; } +int32_t PlayerWheel::checkDivineGrenade(std::shared_ptr target) const { + if (!target || target == m_player.getPlayer()) { + return 0; + } + + int32_t damageBonus = 0; + uint8_t stage = getStage(WheelStage_t::DIVINE_GRENADE); + + if (stage >= 3) { + damageBonus = 100; + } else if (stage >= 2) { + damageBonus = 60; + } else if (stage >= 1) { + damageBonus = 30; + } + + return damageBonus; +} + void PlayerWheel::checkGiftOfLife() { // Healing CombatDamage giftDamage; @@ -2055,7 +2085,9 @@ void PlayerWheel::onThink(bool force /* = false*/) { m_player.sendStats(); g_game().reloadCreature(m_player.getPlayer()); } - return; + if (!force) { + return; + } } // Battle Instinct if (getInstant("Battle Instinct") && (force || getOnThinkTimer(WheelOnThink_t::BATTLE_INSTINCT) < OTSYS_TIME()) && checkBattleInstinct()) { @@ -2297,6 +2329,12 @@ void PlayerWheel::setSpellInstant(const std::string &name, bool value) { } else { setStage(WheelStage_t::DIVINE_EMPOWERMENT, 0); } + } else if (name == "Divine Grenade") { + if (value) { + setStage(WheelStage_t::DIVINE_GRENADE, getStage(WheelStage_t::DIVINE_GRENADE) + 1); + } else { + setStage(WheelStage_t::DIVINE_GRENADE, 0); + } } else if (name == "Twin Burst") { if (value) { setStage(WheelStage_t::TWIN_BURST, getStage(WheelStage_t::TWIN_BURST) + 1); @@ -2380,6 +2418,8 @@ uint8_t PlayerWheel::getStage(const std::string name) const { return PlayerWheel::getStage(WheelStage_t::DRAIN_BODY); } else if (name == "Divine Empowerment") { return PlayerWheel::getStage(WheelStage_t::DIVINE_EMPOWERMENT); + } else if (name == "Divine Grenade") { + return PlayerWheel::getStage(WheelStage_t::DIVINE_GRENADE); } else if (name == "Twin Burst") { return PlayerWheel::getStage(WheelStage_t::TWIN_BURST); } else if (name == "Executioner's Throw") { @@ -2502,6 +2542,8 @@ bool PlayerWheel::getInstant(const std::string name) const { return PlayerWheel::getStage(WheelStage_t::DRAIN_BODY); } else if (name == "Divine Empowerment") { return PlayerWheel::getStage(WheelStage_t::DIVINE_EMPOWERMENT); + } else if (name == "Divine Grenade") { + return PlayerWheel::getStage(WheelStage_t::DIVINE_GRENADE); } else if (name == "Twin Burst") { return PlayerWheel::getStage(WheelStage_t::TWIN_BURST); } else if (name == "Executioner's Throw") { diff --git a/src/creatures/players/wheel/wheel_definitions.hpp b/src/creatures/players/wheel/wheel_definitions.hpp index 6bcf9d50805..91666a8f1be 100644 --- a/src/creatures/players/wheel/wheel_definitions.hpp +++ b/src/creatures/players/wheel/wheel_definitions.hpp @@ -92,8 +92,9 @@ enum class WheelStage_t : uint8_t { AVATAR_OF_NATURE = 9, AVATAR_OF_STEEL = 10, AVATAR_OF_STORM = 11, + DIVINE_GRENADE = 12, - TOTAL_COUNT = 12 + TOTAL_COUNT = 13 }; enum class WheelOnThink_t : uint8_t { @@ -226,6 +227,7 @@ struct PlayerWheelMethodsBonusData { int combatMastery = 0; // Knight int giftOfLife = 0; // Knight/Paladin/Druid/Sorcerer int divineEmpowerment = 0; // Paladin + int divineGrenade = 0; // Paladin int blessingOfTheGrove = 0; // Druid int drainBody = 0; // Sorcerer int beamMastery = 0; // Sorcerer diff --git a/src/game/game.cpp b/src/game/game.cpp index 2e5620d0e9d..776a1483bfd 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -6294,6 +6294,13 @@ void Game::applyWheelOfDestinyEffectsToDamage(CombatDamage &damage, std::shared_ damage.secondary.value += (damage.secondary.value * damageBonus) / 100.; } } + if (damage.instantSpellName == "Divine Grenade") { + int32_t damageBonus = attackerPlayer->wheel()->checkDivineGrenade(target); + if (damageBonus != 0) { + damage.primary.value += (damage.primary.value * damageBonus) / 100.; + damage.secondary.value += (damage.secondary.value * damageBonus) / 100.; + } + } } } From 400a0c39a7710a3ad743b49e64fd1f42b65ac993 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Fri, 20 Oct 2023 16:43:02 -0700 Subject: [PATCH 12/25] fix: add missing john (bounac) script (#1722) --- data-otservbr-global/npc/john_bounac.lua | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/data-otservbr-global/npc/john_bounac.lua b/data-otservbr-global/npc/john_bounac.lua index e69de29bb2d..7cc4c1d4a6e 100644 --- a/data-otservbr-global/npc/john_bounac.lua +++ b/data-otservbr-global/npc/john_bounac.lua @@ -0,0 +1,74 @@ +local npcName = "John" +local npcType = Game.createNpcType("John (Bounac)") +local npcConfig = {} + +npcConfig.name = npcName +npcConfig.description = npcName + +npcConfig.health = 100 +npcConfig.maxHealth = npcConfig.health +npcConfig.walkInterval = 2000 +npcConfig.walkRadius = 2 + +npcConfig.outfit = { + lookType = 1245, + lookHead = 40, + lookBody = 0, + lookLegs = 99, + lookFeet = 28, + lookAddons = 3, +} + +npcConfig.flags = { + floorchange = false, +} + +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) + +npcType.onThink = function(npc, interval) + npcHandler:onThink(npc, interval) +end + +npcType.onAppear = function(npc, creature) + npcHandler:onAppear(npc, creature) +end + +npcType.onDisappear = function(npc, creature) + npcHandler:onDisappear(npc, creature) +end + +npcType.onMove = function(npc, creature, fromPosition, toPosition) + npcHandler:onMove(npc, creature, fromPosition, toPosition) +end + +npcType.onSay = function(npc, creature, type, message) + npcHandler:onSay(npc, creature, type, message) +end + +npcType.onCloseChannel = function(npc, creature) + npcHandler:onCloseChannel(npc, creature) +end + +npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) + +npcConfig.shop = { + { clientId = 36951, buy = 10000 }, + { clientId = 36952, buy = 10000 }, + { clientId = 36953, buy = 10000 }, + { clientId = 36954, buy = 10000 }, + { clientId = 36955, buy = 10000 }, + { clientId = 36956, buy = 10000 }, +} +-- On buy npc shop message +npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) + npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) +end +-- On sell npc shop message +npcType.onSellItem = function(npc, player, itemId, subtype, amount, ignore, name, totalCost) + player:sendTextMessage(MESSAGE_INFO_DESCR, string.format("Sold %ix %s for %i gold.", amount, name, totalCost)) +end +-- On check npc shop message (look item) +npcType.onCheckItem = function(npc, player, clientId, subType) end + +npcType:register(npcConfig) From 834741205062b50d55d78480fcc789fedec9cfc8 Mon Sep 17 00:00:00 2001 From: Luan Luciano Date: Fri, 20 Oct 2023 20:50:43 -0300 Subject: [PATCH 13/25] =?UTF-8?q?improve:=20removing=20-1=20values=20?= =?UTF-8?q?=E2=80=8B=E2=80=8Bin=20gameStorages=20from=20cache=20(#1670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When someone sets the value -1 they will be clearing the cache of that gameStorages key --- data/libs/functions/game.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data/libs/functions/game.lua b/data/libs/functions/game.lua index f9242c32d88..4be079586d2 100644 --- a/data/libs/functions/game.lua +++ b/data/libs/functions/game.lua @@ -136,5 +136,12 @@ function Game.setStorageValue(key, value) return end + if value == -1 then + if globalStorageTable[key] then + table.remove(globalStorageTable, key) + end + return + end + globalStorageTable[key] = value end From e3675f54343d3e740fcf40b8bb2f4cacb8909d82 Mon Sep 17 00:00:00 2001 From: Luan Luciano Date: Fri, 20 Oct 2023 21:00:25 -0300 Subject: [PATCH 14/25] improve: remove unused attribute targetTicks from Monster class (#1671) The unused attribute causes some PRs to fail due to code quality checks. --- src/creatures/monsters/monster.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index 858b3ac8dd8..adcb0258503 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -357,7 +357,6 @@ class Monster final : public Creature { int64_t lastMeleeAttack = 0; uint32_t attackTicks = 0; - uint32_t targetTicks = 0; uint32_t targetChangeTicks = 0; uint32_t defenseTicks = 0; uint32_t yellTicks = 0; From f1eefb9337c733388ddf70288279f25c07e65606 Mon Sep 17 00:00:00 2001 From: sebbesiren <35768829+sebbesiren@users.noreply.github.com> Date: Sat, 21 Oct 2023 02:17:30 +0200 Subject: [PATCH 15/25] fix: spell upgrade in wheel of destiny (#1681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog: •Removed an if-statement that was preventing spells from being added to the list. •The number of spells in the list now determines the upgrade grade of the spell. Issue Details: • Calculate Points: The code initially calculates points for each slice of the wheel (e.g., red, green, purple, etc.). • Determine Grade: It then determines the grade for each slice based on the total points spent in that slice. The grades range from 0 to 3. • Load Grade: For example, if the slice is red and its grade is 2 for the Sorcerer vocation, the grade is loaded. Add Spells to Vector: The code loops through the loaded grade and adds instances of the corresponding spell to a vector. For example, a grade 2 would add two instances of the spell, while grade 3 would add three. •Upgrade Spells: Lastly, the code loops through the vector, upgrading each spell based on the number of instances. For instance, two instances of the same spell would result in a rank 2 of that spell. --- .../scripts/spells/attack/divine_grenade.lua | 23 +++++++------------ src/creatures/players/wheel/player_wheel.cpp | 4 +--- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/data-otservbr-global/scripts/spells/attack/divine_grenade.lua b/data-otservbr-global/scripts/spells/attack/divine_grenade.lua index 44c012484d0..a2112af4c85 100644 --- a/data-otservbr-global/scripts/spells/attack/divine_grenade.lua +++ b/data-otservbr-global/scripts/spells/attack/divine_grenade.lua @@ -5,15 +5,14 @@ combatGrenade:setArea(createCombatArea(AREA_CIRCLE2X2)) function onGetFormulaValues(player, level, maglevel) local min = (level / 5) + (maglevel * 4) local max = (level / 5) + (maglevel * 6) - local multiplier = 1.0 local grade = player:upgradeSpellsWOD("Divine Grenade") - if grade >= WHEEL_GRADE_MAX then - multiplier = 2.0 - elseif grade >= WHEEL_GRADE_UPGRADED then - multiplier = 1.6 - elseif grade >= WHEEL_GRADE_REGULAR then - multiplier = 1.3 + + local multiplier = 1.0 + if grade ~= WHEEL_GRADE_NONE then + local multiplierByGrade = { 1.3, 1.6, 2.0 } + multiplier = multiplierByGrade[grade] end + min = min * multiplier max = max * multiplier return -min, -max @@ -69,14 +68,8 @@ function spell.onCastSpell(creature, var) return false end - local cooldown = 0 - if grade >= WHEEL_GRADE_MAX then - cooldown = 14 - elseif grade >= WHEEL_GRADE_UPGRADED then - cooldown = 20 - elseif grade >= WHEEL_GRADE_REGULAR then - cooldown = 26 - end + local cooldownByGrade = { 26, 20, 14 } + local cooldown = cooldownByGrade[grade] var.instantName = "Divine Grenade Cast" if combatCast:execute(creature, var) then diff --git a/src/creatures/players/wheel/player_wheel.cpp b/src/creatures/players/wheel/player_wheel.cpp index 7f97edbb54b..8e42a72c3fc 100644 --- a/src/creatures/players/wheel/player_wheel.cpp +++ b/src/creatures/players/wheel/player_wheel.cpp @@ -1410,9 +1410,7 @@ void PlayerWheel::loadDedicationAndConvictionPerks() { } void PlayerWheel::addSpellToVector(const std::string &spellName) { - if (std::ranges::find(m_playerBonusData.spells.begin(), m_playerBonusData.spells.end(), spellName) == m_playerBonusData.spells.end()) { - m_playerBonusData.spells.emplace_back(spellName); - } + m_playerBonusData.spells.emplace_back(spellName); } void PlayerWheel::loadRevelationPerks() { From a828a75cabdfb259b609e402c134ee28f692c250 Mon Sep 17 00:00:00 2001 From: Leonardo Antelmi <57509424+FenaLerin@users.noreply.github.com> Date: Fri, 20 Oct 2023 21:25:41 -0300 Subject: [PATCH 16/25] fix: home offers linking correctly (#1723) Fixing the offers in home page of store, linking correctly to open the selected offer --- data/modules/scripts/gamestore/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua index 23c2669fe01..68a691216da 100644 --- a/data/modules/scripts/gamestore/init.lua +++ b/data/modules/scripts/gamestore/init.lua @@ -319,14 +319,15 @@ function parseRequestStoreOffers(playerId, msg) local actionType = msg:getByte() local oldProtocol = player:getClient().version < 1200 - local categoryName = msg:getString() if oldProtocol then + local categoryName = msg:getString() local category = GameStore.getCategoryByName(categoryName) if category then addPlayerEvent(sendShowStoreOffersOnOldProtocol, 350, playerId, category) end elseif actionType == GameStore.ActionType.OPEN_CATEGORY then + local categoryName = msg:getString() local category = GameStore.getCategoryByName(categoryName) if category then addPlayerEvent(sendShowStoreOffers, 50, playerId, category) From e2263fe02920876eb8d0160d7190a86e1ad4bd18 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 20 Oct 2023 22:06:52 -0300 Subject: [PATCH 17/25] fix: stop loop on 'internalDecayItem' function (#1725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a critical issue where an infinite loop is triggered if an item attempts to decay to an ID that matches its equip or unequip transformation ID. Previously, this scenario would lead to an endless loop, causing performance degradation and potentially leading to application instability. The fix involves adding a conditional check to identify this edge case and take appropriate action, such as removing the item and logging an error, thereby breaking the loop, and maintaining application performance. Includes: • Additional conditional checks in the decay logic • Enhanced logging for debugging --- src/items/decay/decay.cpp | 12 ++++++++++++ src/items/functions/item/item_parse.cpp | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/src/items/decay/decay.cpp b/src/items/decay/decay.cpp index 6d4d9730692..a1b709b4479 100644 --- a/src/items/decay/decay.cpp +++ b/src/items/decay/decay.cpp @@ -138,6 +138,18 @@ void Decay::checkDecay() { void Decay::internalDecayItem(std::shared_ptr item) { const ItemType &it = Item::items[item->getID()]; + // Remove the item and halt the decay process if a player triggers a bug where the item's decay ID matches its equip or de-equip transformation ID + if (it.id == it.transformEquipTo || it.id == it.transformDeEquipTo) { + g_game().internalRemoveItem(item); + auto player = item->getHoldingPlayer(); + if (player) { + g_logger().error("[{}] - internalDecayItem failed to player {}, item id is same from transform equip/deequip, " + " item id: {}, equip to id: '{}', deequip to id '{}'", + __FUNCTION__, player->getName(), it.id, it.transformEquipTo, it.transformDeEquipTo); + } + return; + } + if (it.decayTo != 0) { std::shared_ptr player = item->getHoldingPlayer(); if (player) { diff --git a/src/items/functions/item/item_parse.cpp b/src/items/functions/item/item_parse.cpp index 986b1f532b2..ec9f42cd78a 100644 --- a/src/items/functions/item/item_parse.cpp +++ b/src/items/functions/item/item_parse.cpp @@ -384,11 +384,20 @@ void ItemParse::parseTransform(const std::string &tmpStrValue, pugi::xml_attribu std::string stringValue = tmpStrValue; if (stringValue == "transformequipto") { itemType.transformEquipTo = pugi::cast(valueAttribute.value()); + if (itemType.transformEquipTo == itemType.decayTo) { + g_logger().warn("[{}] item with id {} is transforming on equip to the same id of decay to '{}'", __FUNCTION__, itemType.id, itemType.decayTo); + itemType.decayTo = 0; + } if (ItemType &transform = Item::items.getItemType(itemType.transformEquipTo); transform.type == ITEM_TYPE_NONE) { transform.type = itemType.type; } } else if (stringValue == "transformdeequipto") { + if (itemType.transformDeEquipTo == itemType.decayTo) { + g_logger().warn("[{}] item with id {} is transforming on de-equip to the same id of decay to '{}'", __FUNCTION__, itemType.id, itemType.decayTo); + itemType.decayTo = 0; + } + itemType.transformDeEquipTo = pugi::cast(valueAttribute.value()); } else if (stringValue == "transformto") { itemType.transformToFree = pugi::cast(valueAttribute.value()); From 393179ce34c8a44ecd94e9055bf19af7f61c9aa3 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Fri, 20 Oct 2023 18:58:30 -0700 Subject: [PATCH 18/25] feat: implement OTBM Zones, encounters, and raids systems (#1712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces three new systems: • OTBM Zones: Enables direct editing of zones into the map binary, facilitating the creation of large or irregularly shaped zones. A companion PR has been submitted to RME (https://github.com/opentibiabr/remeres-map-editor/pull/56). • Encounters: Introduces a flexible framework for defining scripted fights, simplifying the API for easy understanding and implementation. Encounters are tied to Zones and can be triggered via a boss lever. See the Magma Bubble script in this PR for a comprehensive example. • Raids: A specialized subset of Encounters, Raids are self-scheduling and can spawn within defined zones. This PR replaces Thais' Wild Horse and Rat raids with examples utilizing the new system. --- data-otservbr-global/raids/raids.xml | 2 - .../scripts/globalevents/spawn/raids.lua | 6 - .../scripts/lib/register_actions.lua | 3 +- .../scripts/lib/register_monster_type.lua | 5 + .../magma_bubble_fight.lua | 103 ++++--- .../scripts/raids/thais/rats.lua | 41 +++ .../scripts/raids/thais/wild_horses.lua | 27 ++ data/events/events.xml | 1 + data/libs/encounters_lib.lua | 157 ++++++---- data/libs/functions/bosslever.lua | 9 +- data/libs/functions/functions.lua | 11 + data/libs/hazard_lib.lua | 53 +++- data/libs/libs.lua | 1 + data/libs/raids_lib.lua | 145 ++++++++++ data/libs/zones_lib.lua | 35 ++- .../scripts/eventcallbacks/player/on_look.lua | 2 +- data/scripts/globalevents/encounters.lua | 13 + data/scripts/globalevents/raids.lua | 17 ++ src/canary_server.cpp | 2 + src/creatures/creature.cpp | 8 +- src/creatures/creature.hpp | 2 +- src/creatures/monsters/monsters.cpp | 6 +- src/creatures/monsters/monsters.hpp | 5 +- .../monsters/spawns/spawn_monster.cpp | 20 +- .../monsters/spawns/spawn_monster.hpp | 6 + src/creatures/npcs/npc.cpp | 4 +- src/creatures/npcs/spawns/spawn_npc.cpp | 3 +- src/creatures/players/grouping/party.cpp | 14 +- src/creatures/players/player.cpp | 15 +- src/creatures/players/player.hpp | 2 +- src/game/game.cpp | 103 ++++--- src/game/game.hpp | 8 +- src/game/zones/zone.cpp | 272 +++++++++--------- src/game/zones/zone.hpp | 135 +++++++-- src/io/functions/iologindata_load_player.cpp | 2 - src/io/io_definitions.hpp | 4 +- src/io/iobestiary.cpp | 2 +- src/io/iomap.cpp | 79 +++-- src/io/iomap.hpp | 32 +++ src/io/iomapserialize.cpp | 9 +- src/items/item.cpp | 1 - src/items/tile.cpp | 47 ++- src/items/tile.hpp | 6 +- .../functions/core/game/game_functions.cpp | 40 ++- .../functions/core/game/zone_functions.cpp | 43 ++- .../functions/core/game/zone_functions.hpp | 6 +- .../monster/monster_type_functions.cpp | 19 ++ .../monster/monster_type_functions.hpp | 4 + src/lua/functions/lua_functions_loader.hpp | 7 + src/map/map.cpp | 44 ++- src/map/map.hpp | 11 +- src/map/mapcache.cpp | 4 + src/utils/utils_definitions.hpp | 3 + 53 files changed, 1159 insertions(+), 440 deletions(-) create mode 100644 data-otservbr-global/scripts/raids/thais/rats.lua create mode 100644 data-otservbr-global/scripts/raids/thais/wild_horses.lua create mode 100644 data/libs/raids_lib.lua create mode 100644 data/scripts/globalevents/encounters.lua create mode 100644 data/scripts/globalevents/raids.lua diff --git a/data-otservbr-global/raids/raids.xml b/data-otservbr-global/raids/raids.xml index 82af716354d..035aab91e6c 100644 --- a/data-otservbr-global/raids/raids.xml +++ b/data-otservbr-global/raids/raids.xml @@ -99,8 +99,6 @@ - - diff --git a/data-otservbr-global/scripts/globalevents/spawn/raids.lua b/data-otservbr-global/scripts/globalevents/spawn/raids.lua index 27b60628852..83d065349dd 100644 --- a/data-otservbr-global/scripts/globalevents/spawn/raids.lua +++ b/data-otservbr-global/scripts/globalevents/spawn/raids.lua @@ -1,10 +1,4 @@ local raids = { - -- Weekly - --Segunda-Feira - ["Monday"] = { - ["06:00"] = { raidName = "RatsThais" }, - }, - --Terça-Feira ["Tuesday"] = { ["16:00"] = { raidName = "Midnight Panther" }, diff --git a/data-otservbr-global/scripts/lib/register_actions.lua b/data-otservbr-global/scripts/lib/register_actions.lua index e931fa87027..1ba181553d7 100644 --- a/data-otservbr-global/scripts/lib/register_actions.lua +++ b/data-otservbr-global/scripts/lib/register_actions.lua @@ -465,8 +465,7 @@ function onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) if table.contains(holes, target.itemid) then target:transform(target.itemid + 1) target:decay() - toPosition:moveDownstairs() - toPosition.y = toPosition.y - 1 + toPosition.z = toPosition.z + 1 if Tile(toPosition):hasFlag(TILESTATE_PROTECTIONZONE) and player:isPzLocked() then player:sendCancelMessage(RETURNVALUE_PLAYERISPZLOCKED) return true diff --git a/data-otservbr-global/scripts/lib/register_monster_type.lua b/data-otservbr-global/scripts/lib/register_monster_type.lua index 21a10d94aac..609a4079e59 100644 --- a/data-otservbr-global/scripts/lib/register_monster_type.lua +++ b/data-otservbr-global/scripts/lib/register_monster_type.lua @@ -24,6 +24,11 @@ registerMonsterType.description = function(mtype, mask) mtype:nameDescription(mask.description) end end +registerMonsterType.variant = function(mtype, mask) + if mask.variant then + mtype:variant(mask.variant) + end +end registerMonsterType.experience = function(mtype, mask) if mask.experience then mtype:experience(mask.experience) diff --git a/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua b/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua index f13c55d89e4..91f57104d85 100644 --- a/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua +++ b/data-otservbr-global/scripts/quests/primal_ordeal_quest/magma_bubble_fight.lua @@ -25,71 +25,67 @@ spawnZone:addArea({ x = 33647, y = 32900, z = 15 }, { x = 33659, y = 32913, z = local encounter = Encounter("Magma Bubble", { zone = bossZone, spawnZone = spawnZone, - timeToSpawnMonsters = 2, + timeToSpawnMonsters = "2s", }) -encounter:addStage({ - prepare = function() - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've entered the volcano.") - end, - - start = function() - encounter:spawnMonsters({ - name = "The End of Days", - amount = 3, - event = "fight.magma-bubble.TheEndOfDaysHealth", - }) - encounter:spawnMonsters({ - name = "Magma Crystal", - event = "fight.magma-bubble.MagmaCrystalDeath", - positions = { - Position(33647, 32891, 15), - Position(33647, 32926, 15), - Position(33670, 32898, 15), - }, - }) - end, +function encounter:onReset(position) + encounter:removeMonsters() +end - finish = function() - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The whole Volcano starts to vibrate! Prepare yourself!") - end, +encounter:addRemoveMonsters():autoAdvance() +encounter:addBroadcast("You've entered the volcano."):autoAdvance("1s") + +encounter:addSpawnMonsters({ + { + name = "The End of Days", + amount = 3, + event = "fight.magma-bubble.TheEndOfDaysHealth", + }, + { + name = "Magma Crystal", + event = "fight.magma-bubble.MagmaCrystalDeath", + positions = { + Position(33647, 32891, 15), + Position(33647, 32926, 15), + Position(33670, 32898, 15), + }, + }, }) -encounter:addIntermission(3000) - -encounter:addStage({ - start = function() - encounter:spawnMonsters({ - name = "The End of Days", - amount = 8, - event = "fight.magma-bubble.TheEndOfDaysDeath", - }) - end, +encounter:addRemoveMonsters():autoAdvance() +encounter:addBroadcast("The whole Volcano starts to vibrate! Prepare yourself!"):autoAdvance("3s") - finish = function() - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You've upset the volcano and now it's going to take its revenge!") - end, +encounter:addSpawnMonsters({ + { + name = "The End of Days", + amount = 8, + event = "fight.magma-bubble.TheEndOfDaysDeath", + }, }) -encounter:addIntermission(3000) +encounter:addRemoveMonsters():autoAdvance() +encounter:addBroadcast("You've upset the volcano and now it's going to take its revenge!"):autoAdvance("3s") -encounter:addStage({ - start = function() - encounter:spawnMonsters({ +encounter + :addSpawnMonsters({ + { name = "Magma Bubble", event = "fight.magma-bubble.MagmaBubbleDeath", positions = { Position(33654, 32909, 15), }, - }) - for i = 0, 4 do - table.insert(encounter.events, addEvent(encounter.spawnMonsters, (45 * i + 10) * 1000, encounter, { name = "Unchained Fire", amount = 5 })) - end - end, -}) + }, + }) + :autoAdvance("10s") -function encounter.beforeEach() - encounter:removeMonsters() +for i = 0, 4 do + local stage = encounter:addSpawnMonsters({ + { name = "Unchained Fire", amount = 5 }, + }) + + if i < 4 then + stage:autoAdvance("45s") + end end encounter:register() @@ -139,7 +135,7 @@ function overheatedDamage.onThink(interval, lastExecution) player:getPosition():sendMagicEffect(effect) else local damage = player:getMaxHealth() * 0.6 * -1 - doTargetCombatHealth(0, player, COMBAT_NEUTRALDAMAGE, damage, damage, CONST_ME_NONE) + doTargetCombatHealth(0, player, COMBAT_AGONYDAMAGE, damage, damage, CONST_ME_NONE) end ::continue:: end @@ -224,7 +220,8 @@ function chargedFlameAction.onUse(player, item, fromPosition, target, toPosition } local position = randomPosition(positions) position:sendMagicEffect(CONST_ME_FIREAREA) - Game.createItem(magicFieldId, 1, position) + local field = Game.createItem(magicFieldId, 1, position) + field:decay() item:remove() end @@ -271,7 +268,7 @@ function magmaCrystalDeath.onDeath() if crystals == 0 then encounter:nextStage() else - encounter:sendTextMessage(MESSAGE_EVENT_ADVANCE, "A magma crystal has been destroyed! " .. crystals .. " remaining.") + encounter:broadcast(MESSAGE_EVENT_ADVANCE, "A magma crystal has been destroyed! " .. crystals .. " remaining.") end end diff --git a/data-otservbr-global/scripts/raids/thais/rats.lua b/data-otservbr-global/scripts/raids/thais/rats.lua new file mode 100644 index 00000000000..c63b327462f --- /dev/null +++ b/data-otservbr-global/scripts/raids/thais/rats.lua @@ -0,0 +1,41 @@ +local zone = Zone("thais.rats") +zone:addArea(Position(32331, 32182, 7), Position(32426, 32261, 7)) + +local raid = Raid("thais.rats", { + zone = zone, + allowedDays = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + minActivePlayers = 0, + targetChancePerDay = 30, + maxChancePerCheck = 50, + minGapBetween = "36h", +}) + +raid:addBroadcast("Rat Plague in Thais!"):autoAdvance("5s") + +raid + :addSpawnMonsters({ + { + name = "Rat", + amount = 10, + }, + { + name = "Cave Rat", + amount = 10, + }, + }) + :autoAdvance("10m") + +raid + :addSpawnMonsters({ + { + name = "Rat", + amount = 20, + }, + { + name = "Cave Rat", + amount = 20, + }, + }) + :autoAdvance("10m") + +raid:register() diff --git a/data-otservbr-global/scripts/raids/thais/wild_horses.lua b/data-otservbr-global/scripts/raids/thais/wild_horses.lua new file mode 100644 index 00000000000..45ac7a3d262 --- /dev/null +++ b/data-otservbr-global/scripts/raids/thais/wild_horses.lua @@ -0,0 +1,27 @@ +local zone = Zone("thais.wild-horses") +zone:addArea(Position(32456, 32193, 7), Position(32491, 32261, 7)) +zone:addArea(Position(32431, 32240, 7), Position(32464, 32280, 7)) + +local raid = Raid("thais.wild-horses", { + zone = zone, + allowedDays = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }, + minActivePlayers = 0, + initialChance = 30, + targetChancePerDay = 50, + maxChancePerCheck = 50, + maxChecksPerDay = 2, + minGapBetween = "23h", +}) + +for _ = 1, 7 do + raid + :addSpawnMonsters({ + { + name = "Wild Horse", + amount = 3, + }, + }) + :autoAdvance("3h") +end + +raid:register() diff --git a/data/events/events.xml b/data/events/events.xml index a0eceaa76c5..f719242c1aa 100644 --- a/data/events/events.xml +++ b/data/events/events.xml @@ -15,6 +15,7 @@ Therefore, we strongly encourage avoiding the use of this file when possible, as + diff --git a/data/libs/encounters_lib.lua b/data/libs/encounters_lib.lua index 9085ea88010..1707799b8ac 100644 --- a/data/libs/encounters_lib.lua +++ b/data/libs/encounters_lib.lua @@ -5,8 +5,6 @@ ---@field finish function EncounterStage = {} -local unstarted = 0 - setmetatable(EncounterStage, { ---@param self EncounterStage ---@param config table @@ -20,18 +18,35 @@ setmetatable(EncounterStage, { end, }) +---Automatically advances to the next stage after the given delay +---@param delay number|string The delay time to advance to the next stage +function EncounterStage:autoAdvance(delay) + local originalStart = self.start + function self.start() + delay = delay or 50 -- 50ms is minimum delay; used here for close to instant advance + originalStart() + self.encounter:debug("Encounter[{}]:autoAdvance | next stage in: {}", self.encounter.name, delay == 50 and "instant" or delay) + self.encounter:addEvent(function() + self.encounter:nextStage() + end, delay) + end +end + ---@class Encounter ---@field name string ----@field private zone Zone ----@field private spawnZone Zone ----@field private stages EncounterStage[] ----@field private currentStage number ----@field private events table ----@field private registered boolean ----@field private timeToSpawnMonsters number ----@field beforeEach function +---@field protected zone Zone +---@field protected spawnZone Zone +---@field protected stages EncounterStage[] +---@field protected currentStage number +---@field protected events table +---@field protected registered boolean +---@field protected global boolean +---@field protected timeToSpawnMonsters number|string +---@field onReset function Encounter = { registry = {}, + unstarted = 0, + enableDebug = true, } setmetatable(Encounter, { @@ -57,17 +72,17 @@ setmetatable(Encounter, { end, }) +---@alias EncounterConfig { zone: Zone, spawnZone: Zone, global: boolean, timeToSpawnMonsters: number } ---Resets the encounter configuration ----@param config table The new configuration +---@param config EncounterConfig The new configuration function Encounter:resetConfig(config) self.zone = config.zone self.spawnZone = config.spawnZone or config.zone self.stages = {} - self.currentStage = unstarted - self.beforeEach = config.beforeEach + self.currentStage = Encounter.unstarted self.registered = false self.global = config.global or false - self.timeToSpawnMonsters = config.timeToSpawnMonsters or 3 + self.timeToSpawnMonsters = ParseDuration(config.timeToSpawnMonsters or "3s") self.events = {} end @@ -78,7 +93,7 @@ function Encounter:addEvent(callable, delay, ...) local event = addEvent(function(callable, ...) pcall(callable, ...) table.remove(self.events, index) - end, delay, callable, ...) + end, ParseDuration(delay), callable, ...) table.insert(self.events, index, event) end @@ -102,6 +117,7 @@ end ---@param abort boolean? A flag to determine whether to abort the current stage without calling the finish function. Optional. ---@return boolean True if the stage is entered successfully, false otherwise function Encounter:enterStage(stageNumber, abort) + self:debug("Encounter[{}]:enterStage | stageNumber: {} | abort: {}", self.name, stageNumber, abort) if not abort then local currentStage = self:getStage(self.currentStage) if currentStage and currentStage.finish then @@ -110,12 +126,9 @@ function Encounter:enterStage(stageNumber, abort) end self:cancelEvents() - if self.beforeEach then - self:beforeEach() - end - if stageNumber == unstarted then - self.currentStage = unstarted + if stageNumber == Encounter.unstarted then + self.currentStage = Encounter.unstarted return true end @@ -133,8 +146,10 @@ function Encounter:enterStage(stageNumber, abort) return true end +---@alias SpawnMonsterConfig { name: string, amount: number, event: string?, timeLimit: number?, position: Position|table?, positions: Position|table[]?, spawn: function? } + ---Spawns monsters based on the given configuration ----@param config {name: string, amount: number, event: string?, timeLimit: number?, position: Position|table?, positions: Position|table[]?, spawn: function?} The configuration for spawning monsters +---@param config SpawnMonsterConfig The configuration for spawning monsters function Encounter:spawnMonsters(config) local positions = config.positions local amount = config.amount @@ -155,7 +170,7 @@ function Encounter:spawnMonsters(config) end end for _, position in ipairs(positions) do - for i = 1, self.timeToSpawnMonsters do + for i = 1, self.timeToSpawnMonsters / 1000 do self:addEvent(function(position) position:sendMagicEffect(CONST_ME_TELEPORT) end, i * 1000, position) @@ -180,15 +195,19 @@ function Encounter:spawnMonsters(config) monster:remove() end, config.timeLimit, monster:getID()) end - end, self.timeToSpawnMonsters * 1000, config.name, position, config.event, config.spawn, config.timeLimit) + end, self.timeToSpawnMonsters, config.name, position, config.event, config.spawn, config.timeLimit) end end ---Broadcasts a message to all players function Encounter:broadcast(...) - for _, player in ipairs(Game.getPlayers()) do - player:sendTextMessage(...) + if self.global then + for _, player in ipairs(Game.getPlayers()) do + player:sendTextMessage(...) + end + return end + self.zone:sendTextMessage(...) end ---Counts the number of monsters with the given name in the encounter zone @@ -204,11 +223,6 @@ function Encounter:countPlayers() return self.zone:countPlayers(IgnoredByMonsters) end ----Sends a text message to all creatures in the encounter zone -function Encounter:sendTextMessage(...) - self.zone:sendTextMessage(...) -end - ---Removes all monsters from the encounter zone function Encounter:removeMonsters() self.zone:removeMonsters() @@ -217,10 +231,14 @@ end ---Resets the encounter to its initial state ---@return boolean True if the encounter is reset successfully, false otherwise function Encounter:reset() - if self.currentStage == unstarted then + if self.currentStage == Encounter.unstarted then return true end - return self:enterStage(unstarted) + self:debug("Encounter[{}]:reset", self.name) + if self.onReset then + self:onReset() + end + return self:enterStage(Encounter.unstarted) end ---Checks if a position is inside the encounter zone @@ -248,23 +266,25 @@ end ---Starts the encounter ---@return boolean True if the encounter is started successfully, false otherwise function Encounter:start() - Encounter.registerTickEvent() - if self.currentStage ~= unstarted then + if self.currentStage ~= Encounter.unstarted then return false end + self:debug("Encounter[{}]:start", self.name) return self:enterStage(1) end ---Adds a new stage to the encounter ----@param stage table The stage to add +---@param config table The stage to add ---@return boolean True if the stage is added successfully, false otherwise -function Encounter:addStage(stage) - table.insert(self.stages, EncounterStage(stage)) - return true +function Encounter:addStage(config) + local stage = EncounterStage(config) + stage.encounter = self + table.insert(self.stages, stage) + return stage end ---Adds an intermission stage to the encounter ----@param interval number The duration of the intermission +---@param interval number|string The duration of the intermission ---@return boolean True if the intermission stage is added successfully, false otherwise function Encounter:addIntermission(interval) return self:addStage({ @@ -276,6 +296,47 @@ function Encounter:addIntermission(interval) }) end +---Adds a stage that just sends a message to all players +---@param message string The message to send +---@return boolean True if the message stage is added successfully, false otherwise +function Encounter:addBroadcast(message, type) + type = type or MESSAGE_EVENT_ADVANCE + return self:addStage({ + start = function() + self:broadcast(type, message) + end, + }) +end + +---Adds a stage that spawns monsters +---@param configs SpawnMonsterConfig[] The configurations for spawning monsters +---@return boolean True if the spawn monsters stage is added successfully, false otherwise +function Encounter:addSpawnMonsters(configs) + if not configs then + return false + end + if not configs[1] then + configs = { configs } + end -- convert single config to array + return self:addStage({ + start = function() + for _, config in ipairs(configs) do + self:spawnMonsters(config) + end + end, + }) +end + +---Adds a stage that removes all monsters from the encounter zone +---@return boolean True if the remove monsters stage is added successfully, false otherwise +function Encounter:addRemoveMonsters() + return self:addStage({ + start = function() + self:removeMonsters() + end, + }) +end + ---Automatically starts the encounter when players enter the zone function Encounter:startOnEnter() local zoneEvents = ZoneEvent(self.zone) @@ -321,21 +382,9 @@ function Encounter:register() return true end -function Encounter.registerTickEvent() - if Encounter.tick then +function Encounter:debug(...) + if not Encounter.enableDebug then return end - Encounter.tick = GlobalEvent("encounter.ticks.onThink") - function Encounter.tick.onThink(interval, lastExecution) - for _, encounter in pairs(Encounter.registry) do - local stage = encounter:getStage() - if stage and stage.tick then - stage.tick(encounter, interval, lastExecution) - end - end - return true - end - - Encounter.tick:interval(1000) - Encounter.tick:register() + logger.debug(...) end diff --git a/data/libs/functions/bosslever.lua b/data/libs/functions/bosslever.lua index ac6736fb394..0cb84ec18ae 100644 --- a/data/libs/functions/bosslever.lua +++ b/data/libs/functions/bosslever.lua @@ -16,6 +16,7 @@ ---@field private area {from: Position, to: Position} ---@field private monsters {name: string, pos: Position}[] ---@field private exit Position +---@field private encounter Encounter ---@field private timeoutEvent Event BossLever = {} @@ -151,10 +152,6 @@ function BossLever:onUse(player) return false end self.onUseExtra(creature) - if self.encounter then - local encounter = Encounter(self.encounter) - encounter:start() - end return true end) @@ -177,6 +174,10 @@ function BossLever:onUse(player) monster:registerEvent("BossLeverOnDeath") end lever:teleportPlayers() + if self.encounter then + local encounter = Encounter(self.encounter) + encounter:start() + end lever:setStorageAllPlayers(self.storage, os.time() + self.timeToFightAgain) if self.timeoutEvent then stopEvent(self.timeoutEvent) diff --git a/data/libs/functions/functions.lua b/data/libs/functions/functions.lua index 74dbba95985..42163bc88a3 100644 --- a/data/libs/functions/functions.lua +++ b/data/libs/functions/functions.lua @@ -1139,3 +1139,14 @@ end function toKey(str) return str:lower():gsub(" ", "-"):gsub("%s+", "") end + +function toboolean(value) + if type(value) == "boolean" then + return value + end + if value == "true" then + return true + elseif value == "false" then + return false + end +end diff --git a/data/libs/hazard_lib.lua b/data/libs/hazard_lib.lua index a93da2a82c0..dc8ec0a0d44 100644 --- a/data/libs/hazard_lib.lua +++ b/data/libs/hazard_lib.lua @@ -9,14 +9,16 @@ function Hazard.new(prototype) instance.from = prototype.from instance.to = prototype.to instance.maxLevel = prototype.maxLevel - instance.storageMax = prototype.storageMax - instance.storageCurrent = prototype.storageCurrent + instance.storageMax = prototype.storageMax ---@deprecated + instance.storageCurrent = prototype.storageCurrent ---@deprecated instance.crit = prototype.crit instance.dodge = prototype.dodge instance.damageBoost = prototype.damageBoost instance.zone = Zone(instance.name) - instance.zone:addArea(instance.from, instance.to) + if instance.from and instance.to then + instance.zone:addArea(instance.from, instance.to) + end setmetatable(instance, { __index = Hazard }) @@ -46,8 +48,12 @@ function Hazard:getHazardPlayerAndPoints(damageMap) end function Hazard:getPlayerCurrentLevel(player) - local fromStorage = player:getStorageValue(self.storageCurrent) - return fromStorage <= 0 and 1 or fromStorage + if self.storageCurrent then + local fromStorage = player:getStorageValue(self.storageCurrent) + return fromStorage <= 0 and 1 or fromStorage + end + local fromKV = player:kv():scoped(self.name):get("currentLevel") or 1 + return fromKV <= 0 and 1 or fromKV end function Hazard:setPlayerCurrentLevel(player, level) @@ -55,7 +61,11 @@ function Hazard:setPlayerCurrentLevel(player, level) if level > max then return false end - player:setStorageValue(self.storageCurrent, level) + if self.storageCurrent then + player:setStorageValue(self.storageCurrent, level) + else + player:kv():scoped(self.name):set("currentLevel", level) + end local zones = player:getZones() if not zones then return true @@ -74,15 +84,28 @@ function Hazard:setPlayerCurrentLevel(player, level) end function Hazard:getPlayerMaxLevel(player) - local fromStorage = player:getStorageValue(self.storageMax) - return fromStorage <= 0 and 1 or fromStorage + if self.storageMax then + local fromStorage = player:getStorageValue(self.storageMax) + return fromStorage <= 0 and 1 or fromStorage + end + local fromKV = player:kv():scoped(self.name):get("maxLevel") + return fromKV <= 0 and 1 or fromKV end function Hazard:levelUp(player) - local current = self:getPlayerCurrentLevel(player) - local max = self:getPlayerMaxLevel(player) + if self.storageMax and self.storageCurrent then + local current = self:getPlayerCurrentLevel(player) + local max = self:getPlayerMaxLevel(player) + if current == max then + self:setPlayerMaxLevel(player, max + 1) + end + return + end + + local current = player:kv(self.name):get("currentLevel") + local max = player:kv(self.name):get("maxLevel") if current == max then - self:setPlayerMaxLevel(player, max + 1) + player:kv(self.name):set("maxLevel", max + 1) end end @@ -90,7 +113,12 @@ function Hazard:setPlayerMaxLevel(player, level) if level > self.maxLevel then level = self.maxLevel end - player:setStorageValue(self.storageMax, level) + + if self.storageMax then + player:setStorageValue(self.storageMax, level) + return + end + player:kv():scoped(self.name):set("maxLevel", level) end function Hazard:isInZone(position) @@ -119,6 +147,7 @@ function Hazard:register() if not player then return end + logger.debug("Player {} entered hazard zone {}", player:getName(), zone:getName()) player:setHazardSystemPoints(self:getPlayerCurrentLevel(player)) end diff --git a/data/libs/libs.lua b/data/libs/libs.lua index b4efaf02b6a..ccc71dc8961 100644 --- a/data/libs/libs.lua +++ b/data/libs/libs.lua @@ -30,4 +30,5 @@ dofile(CORE_DIRECTORY .. "/libs/zones_lib.lua") dofile(CORE_DIRECTORY .. "/libs/hazard_lib.lua") dofile(CORE_DIRECTORY .. "/libs/loyalty_lib.lua") dofile(CORE_DIRECTORY .. "/libs/encounters_lib.lua") +dofile(CORE_DIRECTORY .. "/libs/raids_lib.lua") dofile(CORE_DIRECTORY .. "/libs/concoctions_lib.lua") diff --git a/data/libs/raids_lib.lua b/data/libs/raids_lib.lua new file mode 100644 index 00000000000..4f1ea9b8cdf --- /dev/null +++ b/data/libs/raids_lib.lua @@ -0,0 +1,145 @@ +---@alias Weekday 'Monday'|'Tuesday'|'Wednesday'|'Thursday'|'Friday'|'Saturday'|'Sunday + +---@class Raid : Encounter +---@field allowedDays Weekday|Weekday[] The days of the week the raid is allowed to start +---@field minActivePlayers number The minimum number of players required to start the raid +---@field initialChance number|nil The initial chance to start the raid +---@field targetChancePerDay number The chance per enabled day to start the raid +---@field maxChancePerCheck number The maximum chance to start the raid in a single check (1m) +---@field minGapBetween string|number The minimum gap between raids of this type in seconds +---@field maxChecksPerDay number The maximum number of checks per day +---@field kv KV +Raid = { + registry = {}, + checkInterval = "1m", + idleTime = "5m", +} + +-- Set the metatable so that Raid inherits from Encounter +setmetatable(Raid, { + __index = Encounter, + ---@param config { name: string, global: boolean, allowedDays: Weekday|Weekday[], minActivePlayers: number, targetChancePerDay: number, maxChancePerCheck: number, minGapBetween: string|number, initialChance: number, maxChecksPerDay: number } + __call = function(self, name, config) + config.global = true + local raid = setmetatable(Encounter(name, config), { __index = Raid }) + raid.allowedDays = config.allowedDays + raid.minActivePlayers = config.minActivePlayers + raid.targetChancePerDay = config.targetChancePerDay + raid.maxChancePerCheck = config.maxChancePerCheck + raid.minGapBetween = ParseDuration(config.minGapBetween) + raid.initialChance = config.initialChance + raid.maxChecksPerDay = config.maxChecksPerDay + raid.kv = kv.scoped("raids"):scoped(name) + return raid + end, +}) + +---Registers the raid +---@param self Raid The raid to register +---@return boolean True if the raid is registered successfully, false otherwise +function Raid:register() + Encounter.register(self) + Raid.registry[self.name] = self + self.registered = true + return true +end + +---Starts the raid if it can be started +---@param self Raid The raid to try to start +---@return boolean True if the raid was started, false otherwise +function Raid:tryStart() + if not self:canStart() then + return false + end + logger.info("Starting raid {}", self.name) + self.kv:set("last-occurrence", os.time()) + self:start() + return true +end + +---Checks if the raid can be started +---@param self Raid The raid to check +---@return boolean True if the raid can be started, false otherwise +function Raid:canStart() + if self.currentStage ~= Encounter.unstarted then + logger.debug("Raid {} is already running", self.name) + return false + end + if self.allowedDays and not self:isAllowedDay() then + logger.debug("Raid {} is not allowed today ({})", self.name, os.date("%A")) + return false + end + if self.minActivePlayers and self:getActivePlayerCount() < self.minActivePlayers then + logger.debug("Raid {} does not have enough players (active: {}, min: {})", self.name, self:getActivePlayerCount(), self.minActivePlayers) + return false + end + local lastOccurrence = (self.kv:get("last-occurrence") or 0) * 1000 + local currentTime = os.time() * 1000 + if self.minGapBetween and lastOccurrence and currentTime - lastOccurrence < self.minGapBetween then + logger.debug("Raid {} occurred too recently (last: {} ago, min: {})", self.name, FormatDuration(currentTime - lastOccurrence), FormatDuration(self.minGapBetween)) + return false + end + + if not self.targetChancePerDay or not self.maxChancePerCheck then + logger.debug("Raid {} does not have a chance configured (targetChancePerDay: {}, maxChancePerCheck: {})", self.name, self.targetChancePerDay, self.maxChancePerCheck) + return false + end + + local checksToday = tonumber(self.kv:get("checks-today") or 0) + if self.maxChecksPerDay and checksToday >= self.maxChecksPerDay then + logger.debug("Raid {} has already checked today (checks today: {}, max: {})", self.name, checksToday, self.maxChecksPerDay) + return false + end + self.kv:set("checks-today", checksToday + 1) + + local failedAttempts = self.kv:get("failed-attempts") or 0 + local checksPerDay = ParseDuration("23h") / ParseDuration(Raid.checkInterval) + local initialChance = self.initialChance or (self.targetChancePerDay / checksPerDay) + local chanceIncrease = (self.targetChancePerDay - initialChance) / checksPerDay + local chance = initialChance + (chanceIncrease * failedAttempts) + if chance > self.maxChancePerCheck then + chance = self.maxChancePerCheck + end + chance = chance * 1000 + + -- offset the chance by 1000 to allow for fractional chances + local roll = math.random(100 * 1000) + if roll > chance then + logger.debug("Raid {} failed to start (roll: {}, chance: {}, failed attempts: {})", self.name, roll, chance, failedAttempts) + self.kv:set("failed-attempts", failedAttempts + 1) + return false + end + self.kv:set("failed-attempts", 0) + return true +end + +---Checks if the raid is allowed to start today +---@param self Raid The raid to check +---@return boolean True if the raid is allowed to start today, false otherwise +function Raid:isAllowedDay() + local day = os.date("%A") + if self.allowedDays == day then + return true + end + if type(self.allowedDays) == "table" then + for _, allowedDay in pairs(self.allowedDays) do + if allowedDay == day then + return true + end + end + end + return false +end + +---Gets the number of players in the game +---@param self Raid The raid to check +---@return number The number of players in the game +function Raid:getActivePlayerCount() + local count = 0 + for _, player in pairs(Game.getPlayers()) do + if player:getIdleTime() < ParseDuration(Raid.idleTime) then + count = count + 1 + end + end + return count +end diff --git a/data/libs/zones_lib.lua b/data/libs/zones_lib.lua index 33565bc9780..833c4d2cd4b 100644 --- a/data/libs/zones_lib.lua +++ b/data/libs/zones_lib.lua @@ -11,6 +11,10 @@ function Zone:randomPosition() local positions = self:getPositions() + if #positions == 0 then + logger.error("Zone:randomPosition() - Zone {} has no positions", self:getName()) + return nil + end local destination = positions[math.random(1, #positions)] local tile = destination:getTile() while not tile or not tile:isWalkable(false, false, false, false, true) do @@ -71,6 +75,7 @@ end ---@field public beforeLeave function ---@field public afterEnter function ---@field public afterLeave function +---@field public onSpawn function ZoneEvent = {} setmetatable(ZoneEvent, { @@ -79,8 +84,6 @@ setmetatable(ZoneEvent, { local obj = {} setmetatable(obj, { __index = ZoneEvent }) obj.zone = zone - obj.onEnter = nil - obj.onLeave = nil return obj end, }) @@ -133,6 +136,22 @@ function ZoneEvent:register() afterLeave:register() end + + if self.onSpawn then + local afterEnter = EventCallback() + function afterEnter.zoneAfterCreatureEnter(zone, creature) + if zone ~= self.zone then + return true + end + local monster = creature:getMonster() + if not monster then + return true + end + self.onSpawn(monster, monster:getPosition()) + end + + afterEnter:register() + end end function Zone:blockFamiliars() @@ -154,3 +173,15 @@ function Zone:trapMonsters() event:register() end + +function Zone:monsterIcon(category, icon, count) + local event = ZoneEvent(self) + function event.afterEnter(_zone, creature) + if not creature:isMonster() then + return + end + creature:setIcon(category, icon, count) + end + + event:register() +end diff --git a/data/scripts/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua index 359cc8a8bee..9424ebf5785 100644 --- a/data/scripts/eventcallbacks/player/on_look.lua +++ b/data/scripts/eventcallbacks/player/on_look.lua @@ -62,7 +62,7 @@ function callback.playerOnLook(player, thing, position, distance) description = string.format(str, description, thing:getHealth(), thing:getMaxHealth()) .. "." end - description = string.format("%s\nPosition: %d, %d, %d", description, position.x, position.y, position.z) + description = string.format("%s\nPosition: (%d, %d, %d)", description, position.x, position.y, position.z) if thing:isCreature() then local speedBase = thing:getBaseSpeed() diff --git a/data/scripts/globalevents/encounters.lua b/data/scripts/globalevents/encounters.lua new file mode 100644 index 00000000000..7fb46c73b23 --- /dev/null +++ b/data/scripts/globalevents/encounters.lua @@ -0,0 +1,13 @@ +local encounterTick = GlobalEvent("encounters.tick.onThink") +function encounterTick.onThink(interval, lastExecution) + for _, encounter in pairs(Encounter.registry) do + local stage = encounter:getStage() + if stage and stage.tick then + stage.tick(encounter, interval, lastExecution) + end + end + return true +end + +encounterTick:interval(1000) +encounterTick:register() diff --git a/data/scripts/globalevents/raids.lua b/data/scripts/globalevents/raids.lua new file mode 100644 index 00000000000..709baf53bce --- /dev/null +++ b/data/scripts/globalevents/raids.lua @@ -0,0 +1,17 @@ +local serverSaveTime = GetNextOccurrence(configManager.getString(configKeys.GLOBAL_SERVER_SAVE_TIME)) +local stopExecutionAt = serverSaveTime - ParseDuration("1h") / ParseDuration("1s") -- stop rolling raids 1 hour before server save +local raidCheck = GlobalEvent("raids.check.onThink") + +function raidCheck.onThink(interval, lastExecution) + if os.time() > stopExecutionAt then + return true + end + + for _, raid in pairs(Raid.registry) do + raid:tryStart() + end + return true +end + +raidCheck:interval(ParseDuration(Raid.checkInterval)) +raidCheck:register() diff --git a/src/canary_server.cpp b/src/canary_server.cpp index 8476509c820..7d895cdaaa9 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -16,6 +16,7 @@ #include "creatures/players/storages/storages.hpp" #include "database/databasemanager.hpp" #include "game/game.hpp" +#include "game/zones/zone.hpp" #include "game/scheduling/dispatcher.hpp" #include "game/scheduling/events_scheduler.hpp" #include "io/iomarket.hpp" @@ -153,6 +154,7 @@ void CanaryServer::loadMaps() const { if (g_configManager().getBoolean(TOGGLE_MAP_CUSTOM)) { g_game().loadCustomMaps(g_configManager().getString(DATA_DIRECTORY) + "/world/custom/"); } + Zone::refreshAll(); } catch (const std::exception &err) { throw FailedToInitializeCanary(err.what()); } diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 9825f50076a..e0df6f8b80f 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -1789,8 +1789,12 @@ void Creature::setIncreasePercent(CombatType_t combat, int32_t value) { } } -const phmap::parallel_flat_hash_set> Creature::getZones() { - return Zone::getZones(getPosition()); +phmap::flat_hash_set> Creature::getZones() { + auto tile = getTile(); + if (tile) { + return tile->getZones(); + } + return {}; } void Creature::iconChanged() { diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index 9dd5745c353..0195ec17c30 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -263,7 +263,7 @@ class Creature : virtual public Thing, public SharedObject { return ZONE_NORMAL; } - const phmap::parallel_flat_hash_set> getZones(); + phmap::flat_hash_set> getZones(); // walk functions void startAutoWalk(const std::forward_list &listDir, bool ignoreConditions = false); diff --git a/src/creatures/monsters/monsters.cpp b/src/creatures/monsters/monsters.cpp index eb74178c0d0..ba05f5293d5 100644 --- a/src/creatures/monsters/monsters.cpp +++ b/src/creatures/monsters/monsters.cpp @@ -291,7 +291,7 @@ bool MonsterType::loadCallback(LuaScriptInterface* scriptInterface) { return true; } -std::shared_ptr Monsters::getMonsterType(const std::string &name) { +std::shared_ptr Monsters::getMonsterType(const std::string &name, bool silent /* = false*/) const { std::string lowerCaseName = asLowerCaseString(name); if (auto it = monsters.find(lowerCaseName); it != monsters.end() @@ -299,7 +299,9 @@ std::shared_ptr Monsters::getMonsterType(const std::string &name) { && it->first.find(lowerCaseName) != it->first.npos) { return it->second; } - g_logger().error("[Monsters::getMonsterType] - Monster with name {} not exist", lowerCaseName); + if (!silent) { + g_logger().error("[Monsters::getMonsterType] - Monster with name {} not exist", lowerCaseName); + } return nullptr; } diff --git a/src/creatures/monsters/monsters.hpp b/src/creatures/monsters/monsters.hpp index b214b478e3b..9f8a123e0e1 100644 --- a/src/creatures/monsters/monsters.hpp +++ b/src/creatures/monsters/monsters.hpp @@ -162,7 +162,7 @@ class MonsterType { public: MonsterType() = default; explicit MonsterType(const std::string &initName) : - name(initName), typeName(initName), nameDescription(initName) {}; + name(initName), typeName(initName), nameDescription(initName), variantName("") {}; // non-copyable MonsterType(const MonsterType &) = delete; @@ -173,6 +173,7 @@ class MonsterType { std::string name; std::string typeName; std::string nameDescription; + std::string variantName; MonsterInfo info; @@ -264,7 +265,7 @@ class Monsters { monsters.clear(); } - std::shared_ptr getMonsterType(const std::string &name); + std::shared_ptr getMonsterType(const std::string &name, bool silent = false) const; std::shared_ptr getMonsterTypeByRaceId(uint16_t raceId, bool isBoss = false) const; bool tryAddMonsterType(const std::string &name, const std::shared_ptr mType); bool deserializeSpell(const std::shared_ptr spell, spellBlock_t &sb, const std::string &description = ""); diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp index 85de96aed32..aa9bcb14cf0 100644 --- a/src/creatures/monsters/spawns/spawn_monster.cpp +++ b/src/creatures/monsters/spawns/spawn_monster.cpp @@ -18,6 +18,7 @@ #include "lua/callbacks/event_callback.hpp" #include "lua/callbacks/events_callbacks.hpp" #include "utils/pugicast.hpp" +#include "game/zones/zone.hpp" #include "map/spectators.hpp" static constexpr int32_t MONSTER_MINSPAWN_INTERVAL = 1000; // 1 second @@ -257,7 +258,7 @@ void SpawnMonster::cleanup() { while (it != spawnedMonsterMap.end()) { uint32_t spawnMonsterId = it->first; std::shared_ptr monster = it->second; - if (monster->isRemoved()) { + if (!monster || monster->isRemoved()) { spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME(); it = spawnedMonsterMap.erase(it); } else { @@ -267,7 +268,14 @@ void SpawnMonster::cleanup() { } bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t scheduleInterval) { - const auto monsterType = g_monsters().getMonsterType(name); + std::string variant = ""; + for (const auto &zone : Zone::getZones(pos)) { + if (!zone->getMonsterVariant().empty()) { + variant = zone->getMonsterVariant() + "|"; + break; + } + } + const auto monsterType = g_monsters().getMonsterType(variant + name); if (!monsterType) { g_logger().error("Can not find {}", name); return false; @@ -296,6 +304,14 @@ void SpawnMonster::removeMonster(std::shared_ptr monster) { } } +void SpawnMonster::setMonsterVariant(const std::string &variant) { + for (auto &it : spawnMonsterMap) { + auto variantName = variant + it.second.monsterType->typeName; + auto variantType = g_monsters().getMonsterType(variantName, false); + it.second.monsterType = variantType ? variantType : it.second.monsterType; + } +} + void SpawnMonster::stopEvent() { if (checkSpawnMonsterEvent != 0) { g_dispatcher().stopEvent(checkSpawnMonsterEvent); diff --git a/src/creatures/monsters/spawns/spawn_monster.hpp b/src/creatures/monsters/spawns/spawn_monster.hpp index 3c361f95634..749856525d9 100644 --- a/src/creatures/monsters/spawns/spawn_monster.hpp +++ b/src/creatures/monsters/spawns/spawn_monster.hpp @@ -47,6 +47,12 @@ class SpawnMonster { bool isInSpawnMonsterZone(const Position &pos); void cleanup(); + const Position &getCenterPos() const { + return centerPos; + } + + void setMonsterVariant(const std::string &variant); + private: // map of the spawned creatures using SpawnedMap = std::multimap>; diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp index 3e3e761536a..89eabdae28b 100644 --- a/src/creatures/npcs/npc.cpp +++ b/src/creatures/npcs/npc.cpp @@ -249,7 +249,7 @@ void Npc::onPlayerBuyItem(std::shared_ptr player, uint16_t itemId, uint8 if (std::shared_ptr tile = ignore ? player->getTile() : nullptr; tile) { double slotsNedeed = 0; if (itemType.stackable) { - slotsNedeed = inBackpacks ? std::ceil(std::ceil(static_cast(amount) / 100) / shoppingBagSlots) : std::ceil(static_cast(amount) / 100); + slotsNedeed = inBackpacks ? std::ceil(std::ceil(static_cast(amount) / itemType.stackSize) / shoppingBagSlots) : std::ceil(static_cast(amount) / itemType.stackSize); } else { slotsNedeed = inBackpacks ? std::ceil(static_cast(amount) / shoppingBagSlots) : static_cast(amount); } @@ -271,7 +271,7 @@ void Npc::onPlayerBuyItem(std::shared_ptr player, uint16_t itemId, uint8 uint32_t totalCost = buyPrice * amount; uint32_t bagsCost = 0; if (inBackpacks && itemType.stackable) { - bagsCost = shoppingBagPrice * static_cast(std::ceil(std::ceil(static_cast(amount) / 100) / shoppingBagSlots)); + bagsCost = shoppingBagPrice * static_cast(std::ceil(std::ceil(static_cast(amount) / itemType.stackSize) / shoppingBagSlots)); } else if (inBackpacks && !itemType.stackable) { bagsCost = shoppingBagPrice * static_cast(std::ceil(static_cast(amount) / shoppingBagSlots)); } diff --git a/src/creatures/npcs/spawns/spawn_npc.cpp b/src/creatures/npcs/spawns/spawn_npc.cpp index e30ed3b2abe..68fa0c2407b 100644 --- a/src/creatures/npcs/spawns/spawn_npc.cpp +++ b/src/creatures/npcs/spawns/spawn_npc.cpp @@ -137,7 +137,8 @@ void SpawnNpc::startSpawnNpcCheck() { SpawnNpc::~SpawnNpc() { for (const auto &it : spawnedNpcMap) { - it.second->setSpawnNpc(nullptr); + auto npc = it.second; + npc->setSpawnNpc(nullptr); } } diff --git a/src/creatures/players/grouping/party.cpp b/src/creatures/players/grouping/party.cpp index 57cfab4da75..b0a67a8eaf3 100644 --- a/src/creatures/players/grouping/party.cpp +++ b/src/creatures/players/grouping/party.cpp @@ -139,11 +139,6 @@ bool Party::leaveParty(std::shared_ptr player) { g_game().updatePlayerHelpers(member); } - leader->sendCreatureSkull(player); - player->sendCreatureSkull(player); - player->sendPlayerPartyIcons(leader); - leader->sendPartyCreatureUpdate(player); - player->sendTextMessage(MESSAGE_PARTY_MANAGEMENT, "You have left the party."); updateSharedExperience(); @@ -158,6 +153,11 @@ bool Party::leaveParty(std::shared_ptr player) { disband(); } + player->sendCreatureSkull(player); + leader->sendCreatureSkull(player); + player->sendPlayerPartyIcons(leader); + leader->sendPartyCreatureUpdate(player); + return true; } @@ -233,11 +233,13 @@ bool Party::joinParty(const std::shared_ptr &player) { for (auto member : getMembers()) { member->sendCreatureSkull(player); + member->sendPlayerPartyIcons(player); player->sendPlayerPartyIcons(member); } - player->sendCreatureSkull(player); leader->sendCreatureSkull(player); + player->sendCreatureSkull(player); + leader->sendPlayerPartyIcons(player); player->sendPlayerPartyIcons(leader); memberList.push_back(player); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 996c8de5d41..7dd02371688 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -1277,6 +1277,7 @@ void Player::sendPing() { } if (noPongTime >= 60000 && canLogout() && g_creatureEvents().playerLogout(static_self_cast())) { + g_logger().info("Player {} has been kicked due to ping timeout. (has client: {})", getName(), client != nullptr); if (client) { client->logout(true, true); } else { @@ -1596,6 +1597,11 @@ void Player::onCreatureAppear(std::shared_ptr creature, bool isLogin) } } + // Refresh bosstiary tracker onLogin + refreshCyclopediaMonsterTracker(true); + // Refresh bestiary tracker onLogin + refreshCyclopediaMonsterTracker(false); + for (const auto &condition : storedConditionList) { addCondition(condition); } @@ -1716,9 +1722,8 @@ void Player::onAttackedCreatureChangeZone(ZoneType_t zone) { void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) { Creature::onRemoveCreature(creature, isLogout); - auto player = getPlayer(); - if (creature == player) { + if (auto player = getPlayer(); player == creature) { if (isLogout) { if (party) { party->leaveParty(player); @@ -1731,7 +1736,7 @@ void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) loginPosition = getPosition(); lastLogout = time(nullptr); g_logger().info("{} has logged out", getName()); - g_chat().removeUserFromAllChannels(getPlayer()); + g_chat().removeUserFromAllChannels(player); clearPartyInvitations(); IOLoginData::updateOnlineStatus(guid, false); } @@ -4854,8 +4859,8 @@ bool Player::canFamiliar(uint16_t lookType) const { } for (const FamiliarEntry &familiarEntry : familiars) { - if (familiarEntry.lookType != lookType) { - continue; + if (familiarEntry.lookType == lookType) { + return true; } } return false; diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 306fed3f2b3..74398777104 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -490,7 +490,7 @@ class Player final : public Creature, public Cylinder, public Bankable { void addStorageValueByName(const std::string &storageName, const int32_t value, const bool isLogin = false); std::shared_ptr kv() const { - return g_kv().scoped("player")->scoped(fmt::format("{}", getID())); + return g_kv().scoped("player")->scoped(fmt::format("{}", getGUID())); } void genReservedStorageRange(); diff --git a/src/game/game.cpp b/src/game/game.cpp index 776a1483bfd..bb5e28a1e5d 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -419,7 +419,7 @@ bool Game::loadItemsPrice() { void Game::loadMainMap(const std::string &filename) { Monster::despawnRange = g_configManager().getNumber(DEFAULT_DESPAWNRANGE); Monster::despawnRadius = g_configManager().getNumber(DEFAULT_DESPAWNRADIUS); - map.loadMap(g_configManager().getString(DATA_DIRECTORY) + "/world/" + filename + ".otbm", true, true, true, true); + map.loadMap(g_configManager().getString(DATA_DIRECTORY) + "/world/" + filename + ".otbm", true, true, true, true, true); } void Game::loadCustomMaps(const std::filesystem::path &customMapPath) { @@ -460,7 +460,7 @@ void Game::loadCustomMaps(const std::filesystem::path &customMapPath) { continue; } - map.loadMapCustom(filename, true, true, true, customMapIndex); + map.loadMapCustom(filename, true, true, true, true, customMapIndex); customMapIndex++; } @@ -470,7 +470,7 @@ void Game::loadCustomMaps(const std::filesystem::path &customMapPath) { } void Game::loadMap(const std::string &path, const Position &pos) { - map.loadMap(path, false, false, false, false, pos); + map.loadMap(path, false, false, false, false, false, pos); } std::shared_ptr Game::internalGetCylinder(std::shared_ptr player, const Position &pos) { @@ -700,7 +700,7 @@ std::shared_ptr Game::getPlayerByID(uint32_t id, bool loadTmp /* = false if (!loadTmp) { return nullptr; } - std::shared_ptr tmpPlayer(nullptr); + std::shared_ptr tmpPlayer = std::make_shared(nullptr); if (!IOLoginData::loadPlayerById(tmpPlayer, id)) { return nullptr; } @@ -768,7 +768,7 @@ std::shared_ptr Game::getPlayerByName(const std::string &s, bool loadTmp return it->second.lock(); } -std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid) { +std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid, bool loadTmp /* = false */) { if (guid == 0) { return nullptr; } @@ -777,7 +777,29 @@ std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid) { return it.second; } } - return nullptr; + if (!loadTmp) { + return nullptr; + } + std::shared_ptr tmpPlayer = std::make_shared(nullptr); + if (!IOLoginData::loadPlayerById(tmpPlayer, guid)) { + return nullptr; + } + return tmpPlayer; +} + +std::string Game::getPlayerNameByGUID(const uint32_t &guid) { + if (guid == 0) { + return ""; + } + if (m_playerNameCache.contains(guid)) { + return m_playerNameCache.at(guid); + } + auto player = getPlayerByGUID(guid, true); + auto name = player ? player->getName() : ""; + if (!name.empty()) { + m_playerNameCache[guid] = name; + } + return name; } ReturnValue Game::getPlayerByNameWildcard(const std::string &s, std::shared_ptr &player) { @@ -819,7 +841,11 @@ bool Game::internalPlaceCreature(std::shared_ptr creature, const Posit if (creature->getParent() != nullptr) { return false; } - auto toZones = Zone::getZones(pos); + const auto &tile = map.getTile(pos); + if (!tile) { + return false; + } + auto toZones = tile->getZones(); if (auto ret = beforeCreatureZoneChange(creature, {}, toZones); ret != RETURNVALUE_NOERROR) { return false; } @@ -857,44 +883,52 @@ bool Game::placeCreature(std::shared_ptr creature, const Position &pos addCreatureCheck(creature); } - creature->getParent()->postAddNotification(creature, nullptr, 0); + auto parent = creature->getParent(); + if (parent) { + parent->postAddNotification(creature, nullptr, 0); + } creature->onPlacedCreature(); return true; } bool Game::removeCreature(std::shared_ptr creature, bool isLogout /* = true*/) { - if (creature->isRemoved()) { + if (!creature || creature->isRemoved()) { return false; } std::shared_ptr tile = creature->getTile(); + if (!tile) { + g_logger().error("[{}] tile on position '{}' for creature '{}' not exist", __FUNCTION__, creature->getPosition().toString(), creature->getName()); + } + auto fromZones = creature->getZones(); - std::vector oldStackPosVector; - - auto spectators = Spectators().find(tile->getPosition(), true); - auto playersSpectators = spectators.filter(); + if (tile) { + std::vector oldStackPosVector; + auto spectators = Spectators().find(tile->getPosition(), true); + auto playersSpectators = spectators.filter(); - for (const auto &spectator : playersSpectators) { - if (const auto &player = spectator->getPlayer()) { - oldStackPosVector.push_back(player->canSeeCreature(creature) ? tile->getStackposOfCreature(player, creature) : -1); + for (const auto &spectator : playersSpectators) { + if (const auto &player = spectator->getPlayer()) { + oldStackPosVector.push_back(player->canSeeCreature(creature) ? tile->getStackposOfCreature(player, creature) : -1); + } } - } - tile->removeCreature(creature); + tile->removeCreature(creature); - const Position &tilePosition = tile->getPosition(); + const Position &tilePosition = tile->getPosition(); - // Send to client - size_t i = 0; - for (const auto &spectator : playersSpectators) { - if (const auto &player = spectator->getPlayer()) { - player->sendRemoveTileThing(tilePosition, oldStackPosVector[i++]); + // Send to client + size_t i = 0; + for (const auto &spectator : playersSpectators) { + if (const auto &player = spectator->getPlayer()) { + player->sendRemoveTileThing(tilePosition, oldStackPosVector[i++]); + } } - } - // event method - for (auto spectator : spectators) { - spectator->onRemoveCreature(creature, isLogout); + // event method + for (auto spectator : spectators) { + spectator->onRemoveCreature(creature, isLogout); + } } if (creature->getMaster() && !creature->getMaster()->isRemoved()) { @@ -902,14 +936,14 @@ bool Game::removeCreature(std::shared_ptr creature, bool isLogout /* = } creature->getParent()->postRemoveNotification(creature, nullptr, 0); - afterCreatureZoneChange(creature, creature->getZones(), {}); + afterCreatureZoneChange(creature, fromZones, {}); creature->removeList(); creature->setRemoved(); removeCreatureCheck(creature); - for (const auto &summon : creature->m_summons) { + for (auto summon : creature->getSummons()) { summon->setSkillLoss(false); removeCreature(summon); } @@ -1475,7 +1509,8 @@ void Game::playerMoveItem(std::shared_ptr player, const Position &fromPo } } - if ((Position::getDistanceX(playerPos, mapToPos) > item->getThrowRange()) || (Position::getDistanceY(playerPos, mapToPos) > item->getThrowRange()) || (Position::getDistanceZ(mapFromPos, mapToPos) * 4 > item->getThrowRange())) { + auto throwRange = item->getThrowRange(); + if ((Position::getDistanceX(playerPos, mapToPos) > throwRange) || (Position::getDistanceY(playerPos, mapToPos) > throwRange) || (Position::getDistanceZ(mapFromPos, mapToPos) * 4 > throwRange)) { player->sendCancelMessage(RETURNVALUE_DESTINATIONOUTOFREACH); return; } @@ -9884,7 +9919,7 @@ void Game::setTransferPlayerHouseItems(uint32_t houseId, uint32_t playerId) { } template -phmap::parallel_flat_hash_set setDifference(const phmap::parallel_flat_hash_set &setA, const phmap::parallel_flat_hash_set &setB) { +phmap::parallel_flat_hash_set setDifference(const phmap::flat_hash_set &setA, const phmap::flat_hash_set &setB) { phmap::parallel_flat_hash_set setResult; for (const auto &elem : setA) { if (setB.find(elem) == setB.end()) { @@ -9894,7 +9929,7 @@ phmap::parallel_flat_hash_set setDifference(const phmap::parallel_flat_hash_s return setResult; } -ReturnValue Game::beforeCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones, bool force /* = false*/) const { +ReturnValue Game::beforeCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones, bool force /* = false*/) const { if (!creature) { return RETURNVALUE_NOTPOSSIBLE; } @@ -9924,7 +9959,7 @@ ReturnValue Game::beforeCreatureZoneChange(std::shared_ptr creature, c return RETURNVALUE_NOERROR; } -void Game::afterCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones) const { +void Game::afterCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones) const { if (!creature) { return; } diff --git a/src/game/game.hpp b/src/game/game.hpp index aa2d81ba831..e84cb4a513b 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -149,7 +149,9 @@ class Game { std::shared_ptr getPlayerByName(const std::string &s, bool allowOffline = false); - std::shared_ptr getPlayerByGUID(const uint32_t &guid); + std::shared_ptr getPlayerByGUID(const uint32_t &guid, bool allowOffline = false); + + std::string getPlayerNameByGUID(const uint32_t &guid); ReturnValue getPlayerByNameWildcard(const std::string &s, std::shared_ptr &player); @@ -667,8 +669,8 @@ class Game { */ bool tryRetrieveStashItems(std::shared_ptr player, std::shared_ptr item); - ReturnValue beforeCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones, bool force = false) const; - void afterCreatureZoneChange(std::shared_ptr creature, const phmap::parallel_flat_hash_set> &fromZones, const phmap::parallel_flat_hash_set> &toZones) const; + ReturnValue beforeCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones, bool force = false) const; + void afterCreatureZoneChange(std::shared_ptr creature, const phmap::flat_hash_set> &fromZones, const phmap::flat_hash_set> &toZones) const; std::unique_ptr &getIOWheel(); const std::unique_ptr &getIOWheel() const; diff --git a/src/game/zones/zone.cpp b/src/game/zones/zone.cpp index f1aa300efee..fdeccb9bfaa 100644 --- a/src/game/zones/zone.cpp +++ b/src/game/zones/zone.cpp @@ -14,46 +14,51 @@ #include "creatures/monsters/monster.hpp" #include "creatures/npcs/npc.hpp" #include "creatures/players/player.hpp" +#include "utils/pugicast.hpp" phmap::parallel_flat_hash_map> Zone::zones = {}; +phmap::parallel_flat_hash_map> Zone::zonesByID = {}; const static std::shared_ptr nullZone = nullptr; -std::shared_ptr Zone::addZone(const std::string &name) { +std::shared_ptr Zone::addZone(const std::string &name, uint32_t zoneID /* = 0 */) { if (name == "default") { g_logger().error("Zone name {} is reserved", name); return nullZone; } + if (zoneID != 0 && zonesByID.contains(zoneID)) { + g_logger().debug("Found with ID {} while adding {}, linking them together...", zoneID, name); + auto zone = zonesByID[zoneID]; + zone->name = name; + zones[name] = zone; + return zone; + } + if (zones[name]) { g_logger().error("Zone {} already exists", name); return nullZone; } - zones[name] = std::make_shared(name); + zones[name] = std::make_shared(name, zoneID); + if (zoneID != 0) { + zonesByID[zoneID] = zones[name]; + } return zones[name]; } void Zone::addArea(Area area) { - for (const Position &pos : area) { - positions.insert(pos); + for (const auto &pos : area) { + addPosition(pos); } refresh(); } void Zone::subtractArea(Area area) { - for (const Position &pos : area) { - positions.erase(pos); - std::shared_ptr tile = g_game().map.getTile(pos); - if (tile) { - for (auto item : *tile->getItemList()) { - itemRemoved(item); - } - for (auto creature : *tile->getCreatures()) { - creatureRemoved(creature); - } - } + for (const auto &pos : area) { + removePosition(pos); } + refresh(); } -bool Zone::isPositionInZone(const Position &pos) const { +bool Zone::contains(const Position &pos) const { return positions.contains(pos); } @@ -74,115 +79,106 @@ std::shared_ptr Zone::getZone(const std::string &name) { return zones[name]; } -const phmap::parallel_flat_hash_set &Zone::getPositions() const { - return positions; +std::shared_ptr Zone::getZone(uint32_t zoneID) { + if (zoneID == 0) { + return nullZone; + } + if (zonesByID.contains(zoneID)) { + return zonesByID[zoneID]; + } + auto zone = std::make_shared(zoneID); + zonesByID[zoneID] = zone; + return zone; } -const phmap::parallel_flat_hash_set> &Zone::getTiles() const { - static phmap::parallel_flat_hash_set> tiles; - tiles.clear(); - for (const auto &position : positions) { - const auto tile = g_game().map.getTile(position); - if (tile) { - tiles.insert(tile); - } +std::vector Zone::getPositions() const { + std::vector result; + for (const auto &pos : positions) { + result.push_back(pos); } - return tiles; + return result; } -const phmap::parallel_flat_hash_set> &Zone::getCreatures() const { - static phmap::parallel_flat_hash_set> creatures; - creatures.clear(); - for (const auto creatureId : creaturesCache) { - const auto creature = g_game().getCreatureByID(creatureId); - if (creature) { - creatures.insert(creature); - } - } - return creatures; +std::vector> Zone::getCreatures() { + return weak::lock(creaturesCache); } -const phmap::parallel_flat_hash_set> &Zone::getPlayers() const { - static phmap::parallel_flat_hash_set> players; - players.clear(); - for (const auto playerId : playersCache) { - const auto player = g_game().getPlayerByID(playerId); - if (player) { - players.insert(player); - } - } - return players; +std::vector> Zone::getPlayers() { + return weak::lock(playersCache); } -const phmap::parallel_flat_hash_set> &Zone::getMonsters() const { - static phmap::parallel_flat_hash_set> monsters; - monsters.clear(); - for (const auto monsterId : monstersCache) { - const auto monster = g_game().getMonsterByID(monsterId); - if (monster) { - monsters.insert(monster); - } - } - return monsters; +std::vector> Zone::getMonsters() { + return weak::lock(monstersCache); } -const phmap::parallel_flat_hash_set> &Zone::getNpcs() const { - static phmap::parallel_flat_hash_set> npcs; - npcs.clear(); - for (const auto npcId : npcsCache) { - const auto npc = g_game().getNpcByID(npcId); - if (npc) { - npcs.insert(npc); - } - } - return npcs; +std::vector> Zone::getNpcs() { + return weak::lock(npcsCache); } -const phmap::parallel_flat_hash_set> &Zone::getItems() const { - return itemsCache; +std::vector> Zone::getItems() { + return weak::lock(itemsCache); } -void Zone::removePlayers() const { - for (auto player : getPlayers()) { +void Zone::removePlayers() { + for (const auto &player : getPlayers()) { g_game().internalTeleport(player, getRemoveDestination(player)); } } -void Zone::removeMonsters() const { - for (auto monster : getMonsters()) { - g_game().removeCreature(monster); +void Zone::removeMonsters() { + for (const auto &monster : getMonsters()) { + g_game().removeCreature(monster->getCreature()); } } -void Zone::removeNpcs() const { - for (auto npc : getNpcs()) { - g_game().removeCreature(npc); +void Zone::removeNpcs() { + for (const auto &npc : getNpcs()) { + g_game().removeCreature(npc->getCreature()); } } void Zone::clearZones() { + for (const auto &[_, zone] : zones) { + // do not clear zones loaded from the map (id > 0) + if (!zone || zone->isStatic()) { + continue; + } + zone->refresh(); + } zones.clear(); + for (const auto &[_, zone] : zonesByID) { + zones[zone->name] = zone; + } } -phmap::parallel_flat_hash_set> Zone::getZones(const Position postion) { - phmap::parallel_flat_hash_set> zonesSet; +std::vector> Zone::getZones(const Position position) { + Benchmark bm_getZones; + std::vector> result; for (const auto &[_, zone] : zones) { - if (zone && zone->isPositionInZone(postion)) { - zonesSet.insert(zone); + if (zone && zone->contains(position)) { + result.push_back(zone); } } - return zonesSet; + auto duration = bm_getZones.duration(); + if (duration > 100) { + g_logger().warn("Listed {} zones for position {} in {} milliseconds", result.size(), position.toString(), duration); + } + return result; } -const phmap::parallel_flat_hash_set> &Zone::getZones() { - static phmap::parallel_flat_hash_set> zonesSet; - zonesSet.clear(); +std::vector> Zone::getZones() { + Benchmark bm_getZones; + std::vector> result; for (const auto &[_, zone] : zones) { if (zone) { - zonesSet.insert(zone); + result.push_back(zone); } } - return zonesSet; + auto duration = bm_getZones.duration(); + if (duration > 100) { + g_logger().warn("Listed {} zones in {} milliseconds", result.size(), duration); + } + return result; } void Zone::creatureAdded(const std::shared_ptr &creature) { @@ -190,48 +186,25 @@ void Zone::creatureAdded(const std::shared_ptr &creature) { return; } - uint32_t id = 0; - if (creature->getPlayer()) { - id = creature->getPlayer()->getID(); - auto [_, playerInserted] = playersCache.insert(id); - if (playerInserted) { - g_logger().trace("Player {} (ID: {}) added to zone {}", creature->getName(), id, name); - } - } - if (creature->getMonster()) { - id = creature->getMonster()->getID(); - auto [_, monsterInserted] = monstersCache.insert(id); - if (monsterInserted) { - g_logger().trace("Monster {} (ID: {}) added to zone {}", creature->getName(), id, name); - } - } - if (creature->getNpc()) { - id = creature->getNpc()->getID(); - auto [_, npcInserted] = npcsCache.insert(id); - if (npcInserted) { - g_logger().trace("Npc {} (ID: {}) added to zone {}", creature->getName(), id, name); - } + if (const auto &player = creature->getPlayer()) { + playersCache.insert(player); + } else if (const auto &monster = creature->getMonster()) { + monstersCache.insert(monster); + } else if (const auto &npc = creature->getNpc()) { + npcsCache.insert(npc); } - if (id != 0) { - creaturesCache.insert(id); - } + creaturesCache.insert(creature); } void Zone::creatureRemoved(const std::shared_ptr &creature) { if (!creature) { return; } - creaturesCache.erase(creature->getID()); - if (creature->getPlayer() && playersCache.erase(creature->getID())) { - g_logger().trace("Player {} (ID: {}) removed from zone {}", creature->getName(), creature->getID(), name); - } - if (creature->getMonster() && monstersCache.erase(creature->getID())) { - g_logger().trace("Monster {} (ID: {}) removed from zone {}", creature->getName(), creature->getID(), name); - } - if (creature->getNpc() && npcsCache.erase(creature->getID())) { - g_logger().trace("Npc {} (ID: {}) removed from zone {}", creature->getName(), creature->getID(), name); - } + creaturesCache.erase(creature); + playersCache.erase(creature->getPlayer()); + monstersCache.erase(creature->getMonster()); + npcsCache.erase(creature->getNpc()); } void Zone::thingAdded(const std::shared_ptr &thing) { @@ -239,10 +212,10 @@ void Zone::thingAdded(const std::shared_ptr &thing) { return; } - if (thing->getItem()) { - itemAdded(thing->getItem()); - } else if (thing->getCreature()) { - creatureAdded(thing->getCreature()); + if (const auto &item = thing->getItem()) { + itemAdded(item); + } else if (const auto &creature = thing->getCreature()) { + creatureAdded(creature); } } @@ -261,30 +234,45 @@ void Zone::itemRemoved(const std::shared_ptr &item) { } void Zone::refresh() { + Benchmark bm_refresh; creaturesCache.clear(); monstersCache.clear(); npcsCache.clear(); playersCache.clear(); itemsCache.clear(); - for (const auto &position : positions) { - const auto tile = g_game().map.getTile(position); - if (!tile) { - continue; - } - const auto &items = tile->getItemList(); - if (!items) { - continue; - } - for (const auto &item : *items) { - itemAdded(item); - } - const auto &creatures = tile->getCreatures(); - if (!creatures) { + for (const auto &position : getPositions()) { + g_game().map.refreshZones(position); + } + g_logger().debug("Refreshed zone '{}' in {} milliseconds", name, bm_refresh.duration()); +} + +void Zone::setMonsterVariant(const std::string &variant) { + monsterVariant = variant; + g_logger().debug("Zone {} monster variant set to {}", name, variant); + for (auto &spawnMonster : g_game().map.spawnsMonster.getspawnMonsterList()) { + if (!contains(spawnMonster.getCenterPos())) { continue; } - for (const auto &creature : *creatures) { - creatureAdded(creature); - } + spawnMonster.setMonsterVariant(variant); + } + + removeMonsters(); +} + +bool Zone::loadFromXML(const std::string &fileName, uint16_t shiftID /* = 0 */) { + pugi::xml_document doc; + g_logger().debug("Loading zones from {}", fileName); + pugi::xml_parse_result result = doc.load_file(fileName.c_str()); + if (!result) { + printXMLError(__FUNCTION__, fileName, result); + return false; + } + + for (auto zoneNode : doc.child("zones").children()) { + auto name = zoneNode.attribute("name").value(); + auto zoneId = pugi::cast(zoneNode.attribute("zoneid").value()) << shiftID; + addZone(name, zoneId); } + return true; } diff --git a/src/game/zones/zone.hpp b/src/game/zones/zone.hpp index 0aa5e76fd0a..4a0c0318006 100644 --- a/src/game/zones/zone.hpp +++ b/src/game/zones/zone.hpp @@ -9,8 +9,10 @@ #pragma once +#include #include "game/movement/position.hpp" #include "items/item.hpp" +#include "creatures/creature.hpp" class Tile; class Creature; @@ -40,6 +42,10 @@ struct Area { Position from; Position to; + std::string toString() const { + return fmt::format("Area(from: {}, to: {})", from.toString(), to.toString()); + } + class PositionIterator { public: PositionIterator(Position startPosition, const Area &refArea) : @@ -79,10 +85,69 @@ struct Area { } }; +namespace weak { + template + struct ThingHasher { + std::size_t operator()(std::weak_ptr thing) const { + if (thing.expired()) { + return 0; + } + return std::hash {}(thing.lock().get()); + } + }; + + template + struct ThingComparator { + bool operator()(const std::weak_ptr &lhs, const std::weak_ptr &rhs) const { + return lhs.lock() == rhs.lock(); + } + }; + + template <> + struct ThingHasher { + std::size_t operator()(const std::weak_ptr &weakCreature) const { + auto locked = weakCreature.lock(); + if (!locked) { + return 0; + } + return std::hash {}(locked->getID()); + } + }; + + template <> + struct ThingComparator { + bool operator()(const std::weak_ptr &lhs, const std::weak_ptr &rhs) const { + if (lhs.expired() || rhs.expired()) { + return false; + } + return lhs.lock()->getID() == rhs.lock()->getID(); + } + }; + + template + using set = std::unordered_set, ThingHasher, ThingComparator>; + + template + std::vector> lock(set &weakSet) { + std::vector> result; + for (auto it = weakSet.begin(); it != weakSet.end();) { + if (it->expired()) { + it = weakSet.erase(it); + } else { + result.push_back(it->lock()); + ++it; + } + } + return result; + } +} + class Zone { public: - explicit Zone(const std::string &name) : - name(name) { } + explicit Zone(const std::string &name, uint32_t id = 0) : + name(name), id(id) { } + explicit Zone(uint32_t id) : + id(id) { } // Deleted copy constructor and assignment operator. Zone(const Zone &) = delete; @@ -93,19 +158,23 @@ class Zone { } void addArea(Area area); void subtractArea(Area area); - bool isPositionInZone(const Position &position) const; + void addPosition(const Position &position) { + positions.emplace(position); + } + void removePosition(const Position &position) { + positions.erase(position); + } Position getRemoveDestination(const std::shared_ptr &creature = nullptr) const; void setRemoveDestination(const Position &position) { removeDestination = position; } - const phmap::parallel_flat_hash_set &getPositions() const; - const phmap::parallel_flat_hash_set> &getTiles() const; - const phmap::parallel_flat_hash_set> &getCreatures() const; - const phmap::parallel_flat_hash_set> &getPlayers() const; - const phmap::parallel_flat_hash_set> &getMonsters() const; - const phmap::parallel_flat_hash_set> &getNpcs() const; - const phmap::parallel_flat_hash_set> &getItems() const; + std::vector getPositions() const; + std::vector> getCreatures(); + std::vector> getPlayers(); + std::vector> getMonsters(); + std::vector> getNpcs(); + std::vector> getItems(); void creatureAdded(const std::shared_ptr &creature); void creatureRemoved(const std::shared_ptr &creature); @@ -113,28 +182,50 @@ class Zone { void itemAdded(const std::shared_ptr &item); void itemRemoved(const std::shared_ptr &item); - void removePlayers() const; - void removeMonsters() const; - void removeNpcs() const; + void removePlayers(); + void removeMonsters(); + void removeNpcs(); void refresh(); - static std::shared_ptr addZone(const std::string &name); + void setMonsterVariant(const std::string &variant); + const std::string &getMonsterVariant() const { + return monsterVariant; + } + + bool isStatic() const { + return id != 0; + } + + static std::shared_ptr addZone(const std::string &name, uint32_t id = 0); static std::shared_ptr getZone(const std::string &name); - static phmap::parallel_flat_hash_set> getZones(const Position position); - const static phmap::parallel_flat_hash_set> &getZones(); + static std::shared_ptr getZone(uint32_t id); + static std::vector> getZones(const Position position); + static std::vector> getZones(); + static void refreshAll() { + for (const auto &[_, zone] : zones) { + zone->refresh(); + } + } static void clearZones(); + static bool loadFromXML(const std::string &fileName, uint16_t shiftID = 0); + private: + bool contains(const Position &position) const; + Position removeDestination = Position(); std::string name; - phmap::parallel_flat_hash_set positions; + std::string monsterVariant; + std::unordered_set positions; + uint32_t id = 0; // ID 0 is used in zones created dynamically from lua. The map editor uses IDs starting from 1 (automatically generated). - phmap::parallel_flat_hash_set> itemsCache; - phmap::parallel_flat_hash_set creaturesCache; - phmap::parallel_flat_hash_set monstersCache; - phmap::parallel_flat_hash_set npcsCache; - phmap::parallel_flat_hash_set playersCache; + weak::set itemsCache; + weak::set creaturesCache; + weak::set monstersCache; + weak::set npcsCache; + weak::set playersCache; static phmap::parallel_flat_hash_map> zones; + static phmap::parallel_flat_hash_map> zonesByID; }; diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 0b1cf0c1f3e..f404a52b86c 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -30,8 +30,6 @@ void IOLoginDataLoad::loadItems(ItemsMap &itemsMap, DBResult_ptr result, const s if (item) { if (!item->unserializeAttr(propStream)) { g_logger().warn("[{}] - Failed to deserialize item attributes {}, from player {}, from account id {}", __FUNCTION__, item->getID(), player->getName(), player->getAccountId()); - g_logger().info("[{}] - Deleting wrong item: {}", __FUNCTION__, item->getID()); - continue; } itemsMap[sid] = std::make_pair(item, pid); diff --git a/src/io/io_definitions.hpp b/src/io/io_definitions.hpp index 5a7d9bea6d4..22d61883ac8 100644 --- a/src/io/io_definitions.hpp +++ b/src/io/io_definitions.hpp @@ -45,7 +45,8 @@ enum OTBM_AttrTypes_t { OTBM_ATTR_SLEEPERGUID = 20, OTBM_ATTR_SLEEPSTART = 21, OTBM_ATTR_CHARGES = 22, - OTBM_ATTR_EXT_SPAWN_NPC_FILE = 23 + OTBM_ATTR_EXT_SPAWN_NPC_FILE = 23, + OTBM_ATTR_EXT_ZONE_FILE = 24, }; enum OTBM_NodeTypes_t { @@ -65,6 +66,7 @@ enum OTBM_NodeTypes_t { OTBM_HOUSETILE = 14, OTBM_WAYPOINTS = 15, OTBM_WAYPOINT = 16, + OTBM_TILE_ZONE = 19 }; enum OTBM_TileFlag_t : uint32_t { diff --git a/src/io/iobestiary.cpp b/src/io/iobestiary.cpp index 0797428898b..c9fb933bc8c 100644 --- a/src/io/iobestiary.cpp +++ b/src/io/iobestiary.cpp @@ -403,7 +403,7 @@ phmap::parallel_flat_hash_set IOBestiary::getBestiaryFinished(std::sha for (const auto &[monsterTypeRaceId, monsterTypeName] : bestiaryMap) { uint32_t thisKilled = player->getBestiaryKillCount(monsterTypeRaceId); auto mtype = g_monsters().getMonsterType(monsterTypeName); - if (mtype && thisKilled >= mtype->info.bestiaryFirstUnlock) { + if (mtype && thisKilled >= mtype->info.bestiaryToUnlock) { finishedMonsters.insert(monsterTypeRaceId); } } diff --git a/src/io/iomap.cpp b/src/io/iomap.cpp index 51306b7f756..f11e33204ce 100644 --- a/src/io/iomap.cpp +++ b/src/io/iomap.cpp @@ -103,6 +103,11 @@ void IOMap::parseMapDataAttributes(FileStream &stream, Map* map) { map->housefile += stream.getString(); } break; + case OTBM_ATTR_EXT_ZONE_FILE: { + map->zonesfile = map->path.string().substr(0, map->path.string().rfind('/') + 1); + map->zonesfile += stream.getString(); + } break; + default: stream.back(); end = true; @@ -182,36 +187,50 @@ void IOMap::parseTileArea(FileStream &stream, Map &map, const Position &pos) { } while (stream.startNode()) { - if (stream.getU8() != OTBM_ITEM) { - throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Could not read item node.", x, y, z)); - } - - const uint16_t id = stream.getU16(); - - const auto &iType = Item::items[id]; - - if (iType.blockSolid) { - tileIsStatic = true; - } - - const auto item = std::make_shared(); - item->id = id; - - if (!item->unserializeItemNode(stream, x, y, z)) { - throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Failed to load item {}, Node Type.", x, y, z, id)); - } - - if (tile->isHouse() && iType.isBed()) { - // nothing - } else if (tile->isHouse() && iType.moveable) { - g_logger().warn("[IOMap::loadMap] - " - "Moveable item with ID: {}, in house: {}, " - "at position: x {}, y {}, z {}", - id, tile->houseId, x, y, z); - } else if (iType.isGroundTile()) { - tile->ground = map.tryReplaceItemFromCache(item); - } else { - tile->items.emplace_back(map.tryReplaceItemFromCache(item)); + auto type = stream.getU8(); + switch (type) { + case OTBM_ITEM: { + const uint16_t id = stream.getU16(); + + const auto &iType = Item::items[id]; + + if (iType.blockSolid) { + tileIsStatic = true; + } + + const auto item = std::make_shared(); + item->id = id; + + if (!item->unserializeItemNode(stream, x, y, z)) { + throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Failed to load item {}, Node Type.", x, y, z, id)); + } + + if (tile->isHouse() && iType.isBed()) { + // nothing + } else if (tile->isHouse() && iType.moveable) { + g_logger().warn("[IOMap::loadMap] - " + "Moveable item with ID: {}, in house: {}, " + "at position: x {}, y {}, z {}", + id, tile->houseId, x, y, z); + } else if (iType.isGroundTile()) { + tile->ground = map.tryReplaceItemFromCache(item); + } else { + tile->items.emplace_back(map.tryReplaceItemFromCache(item)); + } + } break; + case OTBM_TILE_ZONE: { + const auto zoneCount = stream.getU16(); + for (uint16_t i = 0; i < zoneCount; ++i) { + const auto zoneId = stream.getU16(); + if (!zoneId) { + throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Invalid zone id.", x, y, z)); + } + auto zone = Zone::getZone(zoneId); + zone->addPosition(Position(x, y, z)); + } + } break; + default: + throw IOMapException(fmt::format("[x:{}, y:{}, z:{}] Could not read item/zone node.", x, y, z)); } if (!stream.endNode()) { diff --git a/src/io/iomap.hpp b/src/io/iomap.hpp index d483ad58bb8..0c93b4a8e69 100644 --- a/src/io/iomap.hpp +++ b/src/io/iomap.hpp @@ -17,6 +17,7 @@ #include "map/map.hpp" #include "creatures/monsters/spawns/spawn_monster.hpp" #include "creatures/npcs/spawns/spawn_npc.hpp" +#include "game/zones/zone.hpp" class IOMap { public: @@ -38,6 +39,22 @@ class IOMap { return map->spawnsMonster.loadFromXML(map->monsterfile); } + /** + * Load main map zones + * \param map Is the map class + * \returns true if the zones spawn map was loaded successfully + */ + static bool loadZones(Map* map) { + if (map->zonesfile.empty()) { + // OTBM file doesn't tell us about the zonesfile, + // Lets guess it is mapname-zone.xml. + map->zonesfile = g_configManager().getString(MAP_NAME); + map->zonesfile += "-zones.xml"; + } + + return Zone::loadFromXML(map->zonesfile); + } + /** * Load main map npcs * \param map Is the map class @@ -85,6 +102,21 @@ class IOMap { return map->spawnsMonsterCustomMaps[customMapIndex].loadFromXML(map->monsterfile); } + /** + * Load custom map zones + * \param map Is the map class + * \returns true if the zones spawn map custom was loaded successfully + */ + static bool loadZonesCustom(Map* map, const std::string &mapName, int customMapIndex) { + if (map->zonesfile.empty()) { + // OTBM file doesn't tell us about the zonesfile, + // Lets guess it is mapname-zones.xml. + map->zonesfile = mapName; + map->zonesfile += "-zones.xml"; + } + return Zone::loadFromXML(map->zonesfile, customMapIndex); + } + /** * Load custom map npcs * \param map Is the map class diff --git a/src/io/iomapserialize.cpp b/src/io/iomapserialize.cpp index c089fe022db..8e35f5acb0d 100644 --- a/src/io/iomapserialize.cpp +++ b/src/io/iomapserialize.cpp @@ -77,7 +77,7 @@ bool IOMapSerialize::SaveHouseItemsGuard() { PropWriteStream stream; for (const auto &[key, house] : g_game().map.houses.getHouses()) { // save house items - for (std::shared_ptr tile : house->getTiles()) { + for (const auto &tile : house->getTiles()) { saveTile(stream, tile); size_t attributesSize; @@ -233,7 +233,12 @@ void IOMapSerialize::saveTile(PropWriteStream &stream, std::shared_ptr til std::forward_list> items; uint16_t count = 0; for (auto &item : *tileItems) { - if (!item->isSavedToHouses()) { + if (item->getID() == ITEM_BATHTUB_FILLED_NOTMOVABLE) { + std::shared_ptr tub = Item::CreateItem(ITEM_BATHTUB_FILLED); + items.push_front(tub); + ++count; + continue; + } else if (!item->isSavedToHouses()) { continue; } diff --git a/src/items/item.cpp b/src/items/item.cpp index 1b60d82b4e8..1632cba95dc 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -3082,7 +3082,6 @@ void Item::startDecaying() { } void Item::stopDecaying() { - g_logger().debug("Item::stopDecaying"); g_decay().stopDecay(static_self_cast()); } diff --git a/src/items/tile.cpp b/src/items/tile.cpp index 0d9a5db0ead..92beb20d52f 100644 --- a/src/items/tile.cpp +++ b/src/items/tile.cpp @@ -461,7 +461,7 @@ void Tile::onRemoveTileItem(const CreatureVector &spectators, const std::vector< } } } - for (const auto zone : getZones()) { + for (auto &zone : getZones()) { zone->itemRemoved(item); } @@ -1732,6 +1732,47 @@ std::shared_ptr Tile::getDoorItem() const { return nullptr; } -const phmap::parallel_flat_hash_set> Tile::getZones() { - return Zone::getZones(getPosition()); +phmap::flat_hash_set> Tile::getZones() { + return zones; +} + +void Tile::addZone(std::shared_ptr zone) { + zones.insert(zone); + const auto &items = getItemList(); + if (items) { + for (const auto &item : *items) { + zone->itemAdded(item); + } + } + const auto &creatures = getCreatures(); + if (creatures) { + for (const auto &creature : *creatures) { + zone->creatureAdded(creature); + } + } +} + +void Tile::clearZones() { + phmap::flat_hash_set> zonesToRemove; + for (const auto &zone : zones) { + if (zone->isStatic()) { + continue; + } + zonesToRemove.insert(zone); + const auto &items = getItemList(); + if (items) { + for (const auto &item : *items) { + zone->itemRemoved(item); + } + } + const auto &creatures = getCreatures(); + if (creatures) { + for (const auto &creature : *creatures) { + zone->creatureRemoved(creature); + } + } + } + for (const auto &zone : zonesToRemove) { + zones.erase(zone); + } } diff --git a/src/items/tile.hpp b/src/items/tile.hpp index 2158d635de8..de5dd4f0e43 100644 --- a/src/items/tile.hpp +++ b/src/items/tile.hpp @@ -172,8 +172,10 @@ class Tile : public Cylinder, public SharedObject { void resetFlag(uint32_t flag) { this->flags &= ~flag; } + void addZone(std::shared_ptr zone); + void clearZones(); - const phmap::parallel_flat_hash_set> getZones(); + phmap::flat_hash_set> getZones(); ZoneType_t getZoneType() const { if (hasFlag(TILESTATE_PROTECTIONZONE)) { @@ -261,7 +263,7 @@ class Tile : public Cylinder, public SharedObject { std::shared_ptr ground = nullptr; Position tilePos; uint32_t flags = 0; - std::shared_ptr zone; + phmap::flat_hash_set> zones; }; // Used for walkable tiles, where there is high likeliness of diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index 6e80bf88759..ea01813c66b 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -30,23 +30,47 @@ // Game int GameFunctions::luaGameCreateMonsterType(lua_State* L) { - // Game.createMonsterType(name) + // Game.createMonsterType(name[, variant = ""[, alternateName = ""]]) if (isString(L, 1)) { - std::string name = getString(L, 1); + const auto name = getString(L, 1); + std::string uniqueName = name; + auto variant = getString(L, 2, ""); + const auto alternateName = getString(L, 3, ""); + std::set names; auto monsterType = std::make_shared(name); - if (!g_monsters().tryAddMonsterType(name, monsterType)) { - lua_pushstring(L, fmt::format("The monster with name {} already registered", name).c_str()); - lua_error(L); - return 1; - } - if (!monsterType) { lua_pushstring(L, "MonsterType is nullptr"); lua_error(L); return 1; } + // if variant starts with !, then it's the only variant for this monster, so we register it with both names + if (variant.starts_with("!")) { + names.insert(name); + variant = variant.substr(1); + } + if (!variant.empty()) { + uniqueName = variant + "|" + name; + } + names.insert(uniqueName); + + monsterType->name = name; + if (!alternateName.empty()) { + names.insert(alternateName); + monsterType->name = alternateName; + } + + monsterType->variantName = variant; monsterType->nameDescription = "a " + name; + + for (const auto &alternateName : names) { + if (!g_monsters().tryAddMonsterType(alternateName, monsterType)) { + lua_pushstring(L, fmt::format("The monster with name {} already registered", alternateName).c_str()); + lua_error(L); + return 1; + } + } + pushUserdata(L, monsterType); setMetatable(L, -1, "MonsterType"); } else { diff --git a/src/lua/functions/core/game/zone_functions.cpp b/src/lua/functions/core/game/zone_functions.cpp index a624f36fe78..5471d01895a 100644 --- a/src/lua/functions/core/game/zone_functions.cpp +++ b/src/lua/functions/core/game/zone_functions.cpp @@ -121,27 +121,6 @@ int ZoneFunctions::luaZoneGetPositions(lua_State* L) { return 1; } -int ZoneFunctions::luaZoneGetTiles(lua_State* L) { - // Zone:getTiles() - auto zone = getUserdataShared(L, 1); - if (!zone) { - reportErrorFunc(getErrorDesc(LUA_ERROR_ZONE_NOT_FOUND)); - pushBoolean(L, false); - return 1; - } - auto tiles = zone->getTiles(); - lua_createtable(L, static_cast(tiles.size()), 0); - - int index = 0; - for (auto tile : tiles) { - index++; - pushUserdata(L, tile.get()); - setMetatable(L, -1, "Tile"); - lua_rawseti(L, -2, index); - } - return 1; -} - int ZoneFunctions::luaZoneGetCreatures(lua_State* L) { // Zone:getCreatures() auto zone = getUserdataShared(L, 1); @@ -284,6 +263,24 @@ int ZoneFunctions::luaZoneRemoveNpcs(lua_State* L) { return 1; } +int ZoneFunctions::luaZoneSetMonsterVariant(lua_State* L) { + // Zone:setMonsterVariant(variant) + auto zone = getUserdataShared(L, 1); + if (!zone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto variant = getString(L, 2); + if (variant.empty()) { + pushBoolean(L, false); + return 1; + } + zone->setMonsterVariant(variant); + pushBoolean(L, true); + return 1; +} + int ZoneFunctions::luaZoneGetByName(lua_State* L) { // Zone.getByName(name) auto name = getString(L, 1); @@ -306,7 +303,7 @@ int ZoneFunctions::luaZoneGetByPosition(lua_State* L) { return 1; } int index = 0; - const auto zones = tile->getZones(); + auto zones = tile->getZones(); lua_createtable(L, static_cast(zones.size()), 0); for (auto zone : zones) { index++; @@ -319,7 +316,7 @@ int ZoneFunctions::luaZoneGetByPosition(lua_State* L) { int ZoneFunctions::luaZoneGetAll(lua_State* L) { // Zone.getAll() - const auto zones = Zone::getZones(); + auto zones = Zone::getZones(); lua_createtable(L, static_cast(zones.size()), 0); int index = 0; for (auto zone : zones) { diff --git a/src/lua/functions/core/game/zone_functions.hpp b/src/lua/functions/core/game/zone_functions.hpp index c8ce215f6fd..2a3bbd0f8ea 100644 --- a/src/lua/functions/core/game/zone_functions.hpp +++ b/src/lua/functions/core/game/zone_functions.hpp @@ -16,7 +16,6 @@ class ZoneFunctions final : LuaScriptInterface { registerMethod(L, "Zone", "getRemoveDestination", ZoneFunctions::luaZoneGetRemoveDestination); registerMethod(L, "Zone", "setRemoveDestination", ZoneFunctions::luaZoneSetRemoveDestination); registerMethod(L, "Zone", "getPositions", ZoneFunctions::luaZoneGetPositions); - registerMethod(L, "Zone", "getTiles", ZoneFunctions::luaZoneGetTiles); registerMethod(L, "Zone", "getCreatures", ZoneFunctions::luaZoneGetCreatures); registerMethod(L, "Zone", "getPlayers", ZoneFunctions::luaZoneGetPlayers); registerMethod(L, "Zone", "getMonsters", ZoneFunctions::luaZoneGetMonsters); @@ -28,6 +27,8 @@ class ZoneFunctions final : LuaScriptInterface { registerMethod(L, "Zone", "removeNpcs", ZoneFunctions::luaZoneRemoveNpcs); registerMethod(L, "Zone", "refresh", ZoneFunctions::luaZoneRefresh); + registerMethod(L, "Zone", "setMonsterVariant", ZoneFunctions::luaZoneSetMonsterVariant); + // static methods registerMethod(L, "Zone", "getByPosition", ZoneFunctions::luaZoneGetByPosition); registerMethod(L, "Zone", "getByName", ZoneFunctions::luaZoneGetByName); @@ -45,7 +46,6 @@ class ZoneFunctions final : LuaScriptInterface { static int luaZoneSetRemoveDestination(lua_State* L); static int luaZoneRefresh(lua_State* L); static int luaZoneGetPositions(lua_State* L); - static int luaZoneGetTiles(lua_State* L); static int luaZoneGetCreatures(lua_State* L); static int luaZoneGetPlayers(lua_State* L); static int luaZoneGetMonsters(lua_State* L); @@ -56,6 +56,8 @@ class ZoneFunctions final : LuaScriptInterface { static int luaZoneRemoveMonsters(lua_State* L); static int luaZoneRemoveNpcs(lua_State* L); + static int luaZoneSetMonsterVariant(lua_State* L); + static int luaZoneGetByPosition(lua_State* L); static int luaZoneGetByName(lua_State* L); static int luaZoneGetAll(lua_State* L); diff --git a/src/lua/functions/creatures/monster/monster_type_functions.cpp b/src/lua/functions/creatures/monster/monster_type_functions.cpp index ba77c49a90a..d481b874c30 100644 --- a/src/lua/functions/creatures/monster/monster_type_functions.cpp +++ b/src/lua/functions/creatures/monster/monster_type_functions.cpp @@ -1678,3 +1678,22 @@ int MonsterTypeFunctions::luaMonsterTypedeathSound(lua_State* L) { return 1; } + +int MonsterTypeFunctions::luaMonsterTypeVariant(lua_State* L) { + // get: monsterType:variant() set: monsterType:variant(variantName) + const auto monsterType = getUserdataShared(L, 1); + if (!monsterType) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + if (lua_gettop(L) == 1) { + pushString(L, monsterType->variantName); + } else { + monsterType->variantName = getString(L, 2); + pushBoolean(L, true); + } + + return 1; +} diff --git a/src/lua/functions/creatures/monster/monster_type_functions.hpp b/src/lua/functions/creatures/monster/monster_type_functions.hpp index 9d892219250..199b109daa5 100644 --- a/src/lua/functions/creatures/monster/monster_type_functions.hpp +++ b/src/lua/functions/creatures/monster/monster_type_functions.hpp @@ -140,6 +140,8 @@ class MonsterTypeFunctions final : LuaScriptInterface { registerMethod(L, "MonsterType", "addSound", MonsterTypeFunctions::luaMonsterTypeAddSound); registerMethod(L, "MonsterType", "getSounds", MonsterTypeFunctions::luaMonsterTypeGetSounds); registerMethod(L, "MonsterType", "deathSound", MonsterTypeFunctions::luaMonsterTypedeathSound); + + registerMethod(L, "MonsterType", "variant", MonsterTypeFunctions::luaMonsterTypeVariant); } private: @@ -263,4 +265,6 @@ class MonsterTypeFunctions final : LuaScriptInterface { static int luaMonsterTypeGetSounds(lua_State* L); static int luaMonsterTypedeathSound(lua_State* L); static int luaMonsterTypeCritChance(lua_State* L); + + static int luaMonsterTypeVariant(lua_State* L); }; diff --git a/src/lua/functions/lua_functions_loader.hpp b/src/lua/functions/lua_functions_loader.hpp index 0fee2196553..89baf958e18 100644 --- a/src/lua/functions/lua_functions_loader.hpp +++ b/src/lua/functions/lua_functions_loader.hpp @@ -101,6 +101,13 @@ class LuaFunctionsLoader { static std::string getFormatedLoggerMessage(lua_State* L); static std::string getString(lua_State* L, int32_t arg); + static std::string getString(lua_State* L, int32_t arg, std::string defaultValue) { + const auto parameters = lua_gettop(L); + if (parameters == 0 || arg > parameters) { + return defaultValue; + } + return getString(L, arg); + } static CombatDamage getCombatDamage(lua_State* L); static Position getPosition(lua_State* L, int32_t arg, int32_t &stackpos); static Position getPosition(lua_State* L, int32_t arg); diff --git a/src/map/map.cpp b/src/map/map.cpp index 39fbe47872d..53f7c98db38 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -32,7 +32,7 @@ void Map::load(const std::string &identifier, const Position &pos) { } } -void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool loadHouses /*= false*/, bool loadMonsters /*= false*/, bool loadNpcs /*= false*/, const Position &pos /*= Position()*/) { +void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool loadHouses /*= false*/, bool loadMonsters /*= false*/, bool loadNpcs /*= false*/, bool loadZones /*= false*/, const Position &pos /*= Position()*/) { // Only download map if is loading the main map and it is not already downloaded if (mainMap && g_configManager().getBoolean(TOGGLE_DOWNLOAD_MAP) && !std::filesystem::exists(identifier)) { const auto mapDownloadUrl = g_configManager().getString(MAP_DOWNLOAD_URL); @@ -86,6 +86,10 @@ void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool IOMap::loadNpcs(this); } + if (loadZones) { + IOMap::loadZones(this); + } + // Files need to be cleaned up if custom map is enabled to open, or will try to load main map files if (g_configManager().getBoolean(TOGGLE_MAP_CUSTOM)) { monsterfile.clear(); @@ -94,7 +98,7 @@ void Map::loadMap(const std::string &identifier, bool mainMap /*= false*/, bool } } -void Map::loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, int customMapIndex) { +void Map::loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, bool loadZones, int customMapIndex) { // Load the map load(g_configManager().getString(DATA_DIRECTORY) + "/world/custom/" + mapName + ".otbm"); @@ -110,6 +114,10 @@ void Map::loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMo g_logger().warn("Failed to load npc custom spawn data"); } + if (loadZones && !IOMap::loadZonesCustom(this, mapName, customMapIndex)) { + g_logger().warn("Failed to load zones custom data"); + } + // Files need to be cleaned up or will try to load previous map files again monsterfile.clear(); housefile.clear(); @@ -149,6 +157,25 @@ std::shared_ptr Map::getOrCreateTile(uint16_t x, uint16_t y, uint8_t z, bo return tile; } +std::shared_ptr Map::getLoadedTile(uint16_t x, uint16_t y, uint8_t z) { + if (z >= MAP_MAX_LAYERS) { + return nullptr; + } + + const auto leaf = getQTNode(x, y); + if (!leaf) { + return nullptr; + } + + const auto &floor = leaf->getFloor(z); + if (!floor) { + return nullptr; + } + + const auto tile = floor->getTile(x, y); + return tile; +} + std::shared_ptr Map::getTile(uint16_t x, uint16_t y, uint8_t z) { if (z >= MAP_MAX_LAYERS) { return nullptr; @@ -168,6 +195,19 @@ std::shared_ptr Map::getTile(uint16_t x, uint16_t y, uint8_t z) { return tile ? tile : getOrCreateTileFromCache(floor, x, y); } +void Map::refreshZones(uint16_t x, uint16_t y, uint8_t z) { + const auto tile = getLoadedTile(x, y, z); + if (!tile) { + return; + } + + tile->clearZones(); + const auto &zones = Zone::getZones(tile->getPosition()); + for (const auto &zone : zones) { + tile->addZone(zone); + } +} + void Map::setTile(uint16_t x, uint16_t y, uint8_t z, std::shared_ptr newTile) { if (z >= MAP_MAX_LAYERS) { g_logger().error("Attempt to set tile on invalid coordinate: {}", Position(x, y, z).toString()); diff --git a/src/map/map.hpp b/src/map/map.hpp index 501f7578b6d..ac0211c7daf 100644 --- a/src/map/map.hpp +++ b/src/map/map.hpp @@ -50,7 +50,7 @@ class Map : protected MapCache { * \param loadNpcs if true, the main map npcs is loaded * \returns true if the main map was loaded successfully */ - void loadMap(const std::string &identifier, bool mainMap = false, bool loadHouses = false, bool loadMonsters = false, bool loadNpcs = false, const Position &pos = Position()); + void loadMap(const std::string &identifier, bool mainMap = false, bool loadHouses = false, bool loadMonsters = false, bool loadNpcs = false, bool loadZones = false, const Position &pos = Position()); /** * Load the custom map * \param identifier Is the map custom folder @@ -59,7 +59,7 @@ class Map : protected MapCache { * \param loadNpcs if true, the map custom npcs is loaded * \returns true if the custom map was loaded successfully */ - void loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, const int customMapIndex); + void loadMapCustom(const std::string &mapName, bool loadHouses, bool loadMonsters, bool loadNpcs, bool loadZones, const int customMapIndex); void loadHouseInfo(); @@ -78,6 +78,11 @@ class Map : protected MapCache { return getTile(pos.x, pos.y, pos.z); } + void refreshZones(uint16_t x, uint16_t y, uint8_t z); + void refreshZones(const Position &pos) { + refreshZones(pos.x, pos.y, pos.z); + } + std::shared_ptr getOrCreateTile(uint16_t x, uint16_t y, uint8_t z, bool isDynamic = false); std::shared_ptr getOrCreateTile(const Position &pos, bool isDynamic = false) { return getOrCreateTile(pos.x, pos.y, pos.z, isDynamic); @@ -147,11 +152,13 @@ class Map : protected MapCache { void setTile(const Position &pos, std::shared_ptr newTile) { setTile(pos.x, pos.y, pos.z, newTile); } + std::shared_ptr getLoadedTile(uint16_t x, uint16_t y, uint8_t z); std::filesystem::path path; std::string monsterfile; std::string housefile; std::string npcfile; + std::string zonesfile; uint32_t width = 0; uint32_t height = 0; diff --git a/src/map/mapcache.cpp b/src/map/mapcache.cpp index dfbbc58c3e3..c3962980698 100644 --- a/src/map/mapcache.cpp +++ b/src/map/mapcache.cpp @@ -16,6 +16,7 @@ #include "io/iologindata.hpp" #include "items/item.hpp" #include "game/game.hpp" +#include "game/zones/zone.hpp" #include "map/map.hpp" #include "utils/hash.hpp" #include "io/filestream.hpp" @@ -132,6 +133,9 @@ std::shared_ptr MapCache::getOrCreateTileFromCache(const std::unique_ptrsetFlag(static_cast(cachedTile->flags)); + for (const auto &zone : Zone::getZones(pos)) { + tile->addZone(zone); + } floor->setTile(x, y, tile); diff --git a/src/utils/utils_definitions.hpp b/src/utils/utils_definitions.hpp index ee6b8da2f2b..e84dcf58a64 100644 --- a/src/utils/utils_definitions.hpp +++ b/src/utils/utils_definitions.hpp @@ -655,6 +655,9 @@ enum ItemID_t : uint16_t { ITEM_PRIMAL_POD = 39176, ITEM_DIVINE_EMPOWERMENT = 40450, + ITEM_BATHTUB_FILLED = 26077, + ITEM_BATHTUB_FILLED_NOTMOVABLE = 26100, + ITEM_NONE = 0 }; From d6dec451f43512146643e7dd52faf0fba5a0eb2e Mon Sep 17 00:00:00 2001 From: Luan Colombo <94877887+luancolombo@users.noreply.github.com> Date: Sat, 21 Oct 2023 03:04:48 +0100 Subject: [PATCH 19/25] fix: koshei door wrong id and positions (#1682) --- .../scripts/actions/quests/koshei_amulet/switch.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data-otservbr-global/scripts/actions/quests/koshei_amulet/switch.lua b/data-otservbr-global/scripts/actions/quests/koshei_amulet/switch.lua index 82e14341ea5..aefa1e8a707 100644 --- a/data-otservbr-global/scripts/actions/quests/koshei_amulet/switch.lua +++ b/data-otservbr-global/scripts/actions/quests/koshei_amulet/switch.lua @@ -1,9 +1,9 @@ local config = { - { position = Position(33281, 32442, 8), itemId = 2063 }, - { position = Position(33286, 32444, 8), itemId = 2063 }, - { position = Position(33276, 32444, 8), itemId = 2062 }, - { position = Position(33278, 32450, 8), itemId = 2062 }, - { position = Position(33284, 32450, 8), itemId = 2062 }, + { position = Position(33276, 32444, 8), itemId = 2063 }, + { position = Position(33278, 32450, 8), itemId = 2063 }, + { position = Position(33284, 32450, 8), itemId = 2063 }, + { position = Position(33281, 32442, 8), itemId = 2062 }, + { position = Position(33286, 32444, 8), itemId = 2062 }, } local coffinPosition = Position(33273, 32458, 8) From 590b2e7796aba79684a9f38ba296fe90b33f3e62 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Lisboa <39601778+Glatharth@users.noreply.github.com> Date: Fri, 20 Oct 2023 23:09:00 -0300 Subject: [PATCH 20/25] fix: Oberon to recognize only talktype say messages (#1693) --- .../quests/the_secret_library/bosses/grand_master_oberon.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data-otservbr-global/monster/quests/the_secret_library/bosses/grand_master_oberon.lua b/data-otservbr-global/monster/quests/the_secret_library/bosses/grand_master_oberon.lua index 4be6206d156..aec3143fb58 100644 --- a/data-otservbr-global/monster/quests/the_secret_library/bosses/grand_master_oberon.lua +++ b/data-otservbr-global/monster/quests/the_secret_library/bosses/grand_master_oberon.lua @@ -142,6 +142,9 @@ mType.onDisappear = function(monster, creature) end mType.onMove = function(monster, creature, fromPosition, toPosition) end mType.onSay = function(monster, creature, type, message) + if type ~= TALKTYPE_SAY then + return false + end local exhaust = GrandMasterOberonConfig.Storage.Exhaust if creature:isPlayer() and monster:getStorageValue(exhaust) <= os.time() then message = message:lower() From e18af034c0ea093981b25e1fff58cfb56304438c Mon Sep 17 00:00:00 2001 From: Roberto Carlos PMG <67324932+RCP91@users.noreply.github.com> Date: Fri, 20 Oct 2023 22:17:25 -0400 Subject: [PATCH 21/25] fix: transform foreshock and aftershock (#1635) They don't transform, now the count is like tibia global. The removed realityQuakeStage is not necessary as we use life storage of transformations with a global variable. --- .../heart_of_destruction/quake_lever.lua | 1 - .../aftershock_transform.lua | 20 ++++++++---------- .../foreshock_transform.lua | 21 ++++++++----------- .../heart_of_destruction/shocks_death.lua | 18 +++------------- 4 files changed, 21 insertions(+), 39 deletions(-) diff --git a/data-otservbr-global/scripts/actions/quests/heart_of_destruction/quake_lever.lua b/data-otservbr-global/scripts/actions/quests/heart_of_destruction/quake_lever.lua index 608028ea5e7..12e5d1260fc 100644 --- a/data-otservbr-global/scripts/actions/quests/heart_of_destruction/quake_lever.lua +++ b/data-otservbr-global/scripts/actions/quests/heart_of_destruction/quake_lever.lua @@ -109,7 +109,6 @@ function heartDestructionQuake.onUse(player, item, fromPosition, itemEx, toPosit foreshockHealth = 105000 aftershockHealth = 105000 - realityQuakeStage = 0 foreshockStage = 0 aftershockStage = 0 diff --git a/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/aftershock_transform.lua b/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/aftershock_transform.lua index 2bb9584e206..abe45bbf7e2 100644 --- a/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/aftershock_transform.lua +++ b/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/aftershock_transform.lua @@ -15,23 +15,21 @@ function aftershockTransform.onThink(creature) [80] = { fromStage = 0, toStage = 1 }, [60] = { fromStage = 1, toStage = 2 }, [40] = { fromStage = 2, toStage = 3 }, - [25] = { fromStage = 3, toStage = 4 }, + [20] = { fromStage = 3, toStage = 4 }, [10] = { fromStage = 4, toStage = 5 }, } for index, value in pairs(monsterTable) do local hp = (creature:getHealth() / creature:getMaxHealth()) * 100 - if realityQuakeStage == 0 then - local aftershockHealth = creature:getHealth() - if hp <= index and aftershockStage == value.fromStage then - creature:remove() - for i = 1, #sparkOfDestructionPositions do - Game.createMonster("spark of destruction", #sparkOfDestructionPositions[i], false, true) - end - local monster = Game.createMonster("foreshock", { x = 32208, y = 31248, z = 14 }, false, true) - monster:addHealth(-monster:getHealth() + aftershockHealth, COMBAT_PHYSICALDAMAGE) - aftershockStage = value.toStage + aftershockHealth = creature:getHealth() + if hp <= index and aftershockStage == value.fromStage then + creature:remove() + for i = 1, #sparkOfDestructionPositions do + Game.createMonster("spark of destruction", sparkOfDestructionPositions[i], false, true) end + local monster = Game.createMonster("foreshock", { x = 32208, y = 31248, z = 14 }, false, true) + monster:addHealth(-monster:getHealth() + foreshockHealth, COMBAT_PHYSICALDAMAGE) + aftershockStage = value.toStage end end return true diff --git a/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/foreshock_transform.lua b/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/foreshock_transform.lua index 7a4e60e4d85..6e15d2596e6 100644 --- a/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/foreshock_transform.lua +++ b/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/foreshock_transform.lua @@ -15,26 +15,23 @@ function foreshockTransform.onThink(creature) [80] = { fromStage = 0, toStage = 1 }, [60] = { fromStage = 1, toStage = 2 }, [40] = { fromStage = 2, toStage = 3 }, - [25] = { fromStage = 3, toStage = 4 }, + [20] = { fromStage = 3, toStage = 4 }, [10] = { fromStage = 4, toStage = 5 }, } for index, value in pairs(monsterTable) do local hp = (creature:getHealth() / creature:getMaxHealth()) * 100 - if realityQuakeStage == 0 then - local foreshockHealth = creature:getHealth() - if hp <= index and aftershockStage == value.fromStage then - creature:remove() - for i = 1, #sparkOfDestructionPositions do - Game.createMonster("spark of destruction", #sparkOfDestructionPositions[i], false, true) - end - local monster = Game.createMonster("aftershock", { x = 32208, y = 31248, z = 14 }, false, true) - monster:addHealth(-monster:getHealth() + foreshockHealth, COMBAT_PHYSICALDAMAGE) - foreshockStage = value.toStage + foreshockHealth = creature:getHealth() + if hp <= index and foreshockStage == value.fromStage then + creature:remove() + for i = 1, #sparkOfDestructionPositions do + Game.createMonster("spark of destruction", sparkOfDestructionPositions[i], false, true) end + local monster = Game.createMonster("aftershock", { x = 32208, y = 31248, z = 14 }, false, true) + monster:addHealth(-monster:getHealth() + aftershockHealth, COMBAT_PHYSICALDAMAGE) + foreshockStage = value.toStage end end return true end - foreshockTransform:register() diff --git a/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/shocks_death.lua b/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/shocks_death.lua index 6e6ac5eebf2..400304c5912 100644 --- a/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/shocks_death.lua +++ b/data-otservbr-global/scripts/creaturescripts/quests/heart_of_destruction/shocks_death.lua @@ -5,26 +5,16 @@ function shocksDeath.onDeath(creature) end local name = creature:getName():lower() - if name == "foreshock" and realityQuakeStage == 0 then - if realityQuakeStage == 0 then - local monster = Game.createMonster("aftershock", { x = 32208, y = 31248, z = 14 }, false, true) - monster:addHealth(-monster:getHealth() + aftershockHealth, COMBAT_PHYSICALDAMAGE) - Game.createMonster("spark of destruction", { x = 32203, y = 31246, z = 14 }, false, true) - Game.createMonster("spark of destruction", { x = 32205, y = 31251, z = 14 }, false, true) - Game.createMonster("spark of destruction", { x = 32210, y = 31251, z = 14 }, false, true) - Game.createMonster("spark of destruction", { x = 32212, y = 31246, z = 14 }, false, true) - end - elseif name == "aftershock" and realityQuakeStage == 0 then - local monster = Game.createMonster("foreshock", { x = 32208, y = 31248, z = 14 }, false, true) + if name == "foreshock" then + local monster = Game.createMonster("aftershock", { x = 32208, y = 31248, z = 14 }, false, true) monster:addHealth(-monster:getHealth() + aftershockHealth, COMBAT_PHYSICALDAMAGE) Game.createMonster("spark of destruction", { x = 32203, y = 31246, z = 14 }, false, true) Game.createMonster("spark of destruction", { x = 32205, y = 31251, z = 14 }, false, true) Game.createMonster("spark of destruction", { x = 32210, y = 31251, z = 14 }, false, true) Game.createMonster("spark of destruction", { x = 32212, y = 31246, z = 14 }, false, true) end - realityQuakeStage = realityQuakeStage + 1 - if realityQuakeStage == 2 then + if name == "aftershock" then local pos = creature:getPosition() local monster = Game.createMonster("realityquake", pos, false, true) Game.createMonster("spark of destruction", { x = 32203, y = 31246, z = 14 }, false, true) @@ -32,8 +22,6 @@ function shocksDeath.onDeath(creature) Game.createMonster("spark of destruction", { x = 32210, y = 31251, z = 14 }, false, true) Game.createMonster("spark of destruction", { x = 32212, y = 31246, z = 14 }, false, true) end - return true end - shocksDeath:register() From d4a992567ae05ef3b55148a9232ee9d1c8a3f2be Mon Sep 17 00:00:00 2001 From: Luan Luciano Date: Fri, 20 Oct 2023 23:18:42 -0300 Subject: [PATCH 22/25] fix: gamestore adjustment (#1720) --- data/modules/scripts/gamestore/gamestore.lua | 73 +++++++++++--------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/data/modules/scripts/gamestore/gamestore.lua b/data/modules/scripts/gamestore/gamestore.lua index 3336c1fcfa7..c4313dc9d5b 100644 --- a/data/modules/scripts/gamestore/gamestore.lua +++ b/data/modules/scripts/gamestore/gamestore.lua @@ -349,7 +349,7 @@ GameStore.Categories = { state = GameStore.States.STATE_NONE, offers = { { - icons = { "Exercise_Axe.png" }, + icons = { "Durable_Exercise_Axe.png" }, name = "Durable Exercise Axe", price = 90, itemtype = 35280, @@ -358,7 +358,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Bow.png" }, + icons = { "Durable_Exercise_Bow.png" }, name = "Durable Exercise Bow", price = 90, itemtype = 35282, @@ -367,7 +367,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Club.png" }, + icons = { "Durable_Exercise_Club.png" }, name = "Durable Exercise Club", price = 90, itemtype = 35281, @@ -376,7 +376,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Rod.png" }, + icons = { "Durable_Exercise_Rod.png" }, name = "Durable Exercise Rod", price = 90, itemtype = 35283, @@ -385,7 +385,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Sword.png" }, + icons = { "Durable_Exercise_Sword.png" }, name = "Durable Exercise Sword", price = 90, itemtype = 35279, @@ -394,7 +394,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Wand.png" }, + icons = { "Durable_Exercise_Wand.png" }, name = "Durable Exercise Wand", price = 90, itemtype = 35284, @@ -403,7 +403,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Shield.png" }, + icons = { "Durable_Exercise_Shield.png" }, name = "Durable Exercise Shield", price = 90, itemtype = 44066, @@ -475,7 +475,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Axe.png" }, + icons = { "Lasting_Exercise_Axe.png" }, name = "Lasting Exercise Axe", price = 720, itemtype = 35286, @@ -484,7 +484,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Bow.png" }, + icons = { "Lasting_Exercise_Bow.png" }, name = "Lasting Exercise Bow", price = 720, itemtype = 35288, @@ -493,7 +493,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Club.png" }, + icons = { "Lasting_Exercise_Club.png" }, name = "Lasting Exercise Club", price = 720, itemtype = 35287, @@ -502,7 +502,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Rod.png" }, + icons = { "Lasting_Exercise_Rod.png" }, name = "Lasting Exercise Rod", price = 720, itemtype = 35289, @@ -511,7 +511,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Sword.png" }, + icons = { "Lasting_Exercise_Sword.png" }, name = "Lasting Exercise Sword", price = 720, itemtype = 35285, @@ -520,7 +520,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Wand.png" }, + icons = { "Lasting_Exercise_Wand.png" }, name = "Lasting Exercise Wand", price = 720, itemtype = 35290, @@ -529,7 +529,7 @@ GameStore.Categories = { type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, }, { - icons = { "Exercise_Shield.png" }, + icons = { "Lasting_Exercise_Shield.png" }, name = "Lasting Exercise Shield", price = 720, itemtype = 44067, @@ -2339,6 +2339,15 @@ GameStore.Categories = { description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThey are skilled, they are disciplined, they wield their weapon with deadly precision as a form of art. Fencers are true masters of the blade who can cut through anything and anyone in the blink of an eye. While being feared for their lethal attacks, they are also admired for their elegant and fierce style, their dashing looks. Do not be on the fence, be a fencer, or at least dress like one with this fashionable, cutting-edge outfit.", type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, }, + { + icons = { "Outfit_Flamefury_Mage_Male_Addon_3.png", "Outfit_Flamefury_Mage_Female_Addon_3.png" }, + name = "Full Flamefury Mage Outfit", + price = 870, + sexId = { female = 1681, male = 1680 }, + addon = 3, + description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFlame fury mages not only embraced the fury to fire, they became it! Relishing in purging destruction they don't just want to see the world burn, but be an active part in it. They just love the smell of sulphur in the morning!", + type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, + }, { icons = { "Outfit_Forest_Warden_Male_Addon_3.png", "Outfit_Forest_Warden_Female_Addon_3.png" }, name = "Full Forest Warden Outfit", @@ -2609,6 +2618,24 @@ GameStore.Categories = { description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou spend hours in the woods in search of wild and rare animals? Countless stuffed skulls of deer, wolves and other creatures are decorating your walls? Now you have the chance to present your trophies in public. Become a Trophy Hunter and cover your shoulders with the finest bear skulls!", type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, }, + { + icons = { "Outfit_Veteran_Paladin_Male_Addon_3.png", "Outfit_Veteran_Paladin_Female_Addon_3.png" }, + name = "Full Veteran Paladin Outfit", + price = 750, + sexId = { female = 1205, male = 1204 }, + addon = 3, + description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nA Veteran Paladin has mastered the art of distance fighting. No matter how far away his prey may be, a marksman like the Veteran Paladin will always hit with extraordinary precision. No one can escape his keen hawk-eyed vision and even small stones become deadly weapons in his hands.", + type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, + }, + { + icons = { "Outfit_Void_Master_Male_Addon_3.png", "Outfit_Void_Master_Female_Addon_3.png" }, + name = "Full Void Master Outfit", + price = 750, + sexId = { female = 1203, male = 1202 }, + addon = 3, + description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAccording to ancient rumours, the pulsating orb that the Void Master balances skilfully on the tip of his staff consists of powerful cosmic spheres. If you gaze too long into the infinite emptiness inside the orb, its powers will absorb your mind.", + type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, + }, { icons = { "Outfit_Winter_Warden_Male_Addon_3.png", "Outfit_Winter_Warden_Female_Addon_3.png" }, name = "Full Winter Warden Outfit", @@ -6654,15 +6681,6 @@ GameStore.Categories = { description = "{house}\n{box}\n{storeinbox}\n{useicon} use an unwrapped carpet to roll it out or up\n{backtoinbox}", type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, }, - { - icons = { "Outfit_Veteran_Paladin_Male_Addon_3.png", "Outfit_Veteran_Paladin_Female_Addon_3.png" }, - name = "Full Veteran Paladin Outfit", - price = 1750, - sexId = { female = 1205, male = 1204 }, - addon = 3, - description = "{info} usable by all characters of the account\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nA Veteran Paladin has mastered the art of distance fighting. No matter how far away his prey may be, a marksman like the Veteran Paladin will always hit with extraordinary precision. No one can escape his keen hawk-eyed vision and even small stones become deadly weapons in his hands.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, { icons = { "Vexclaw_Doll.png" }, name = "Vexclaw Doll", @@ -6672,15 +6690,6 @@ GameStore.Categories = { description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, }, - { - icons = { "Outfit_Void_Master_Male_Addon_3.png", "Outfit_Void_Master_Female_Addon_3.png" }, - name = "Full Void Master Outfit", - price = 1750, - sexId = { female = 1203, male = 1202 }, - addon = 3, - description = "{info} usable by all characters of the account\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAccording to ancient rumours, the pulsating orb that the Void Master balances skilfully on the tip of his staff consists of powerful cosmic spheres. If you gaze too long into the infinite emptiness inside the orb, its powers will absorb your mind.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, }, }, } From 8096d819539b1840342c315dcc2eef6072fe36c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Lu=C3=ADs=20Lucarelo=20Lamonato?= Date: Sat, 21 Oct 2023 00:08:17 -0300 Subject: [PATCH 23/25] feat: add ancient lion knight spawn (#1709) Add the Ancient Lion Knight miniboss respawn to otservbr-monsters.xml --- data-otservbr-global/world/otservbr-monster.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data-otservbr-global/world/otservbr-monster.xml b/data-otservbr-global/world/otservbr-monster.xml index a4c60f6b7f9..493d167b02b 100644 --- a/data-otservbr-global/world/otservbr-monster.xml +++ b/data-otservbr-global/world/otservbr-monster.xml @@ -42056,6 +42056,9 @@ + + + From 35ce8e381081606b8b5595297e5aab10004ce091 Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Fri, 20 Oct 2023 20:13:39 -0700 Subject: [PATCH 24/25] fix: earth protection parsing (#1706) --- src/items/functions/item/item_parse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/items/functions/item/item_parse.cpp b/src/items/functions/item/item_parse.cpp index ec9f42cd78a..8d29dfb71a9 100644 --- a/src/items/functions/item/item_parse.cpp +++ b/src/items/functions/item/item_parse.cpp @@ -570,7 +570,7 @@ void ItemParse::parseAbsorbPercent(const std::string &tmpStrValue, pugi::xml_att itemType.getAbilities().absorbPercent[combatTypeToIndex(COMBAT_ENERGYDAMAGE)] += pugi::cast(valueAttribute.value()); } else if (stringValue == "absorbpercentfire") { itemType.getAbilities().absorbPercent[combatTypeToIndex(COMBAT_FIREDAMAGE)] += pugi::cast(valueAttribute.value()); - } else if (stringValue == "absorbpercentpoison") { + } else if (stringValue == "absorbpercentpoison" || stringValue == "absorbpercentearth") { itemType.getAbilities().absorbPercent[combatTypeToIndex(COMBAT_EARTHDAMAGE)] += pugi::cast(valueAttribute.value()); } else if (stringValue == "absorbpercentice") { itemType.getAbilities().absorbPercent[combatTypeToIndex(COMBAT_ICEDAMAGE)] += pugi::cast(valueAttribute.value()); From 4f09a64994782eda163dad6a8d984c8dabf35a2f Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 21 Oct 2023 00:36:55 -0300 Subject: [PATCH 25/25] ci/cd: cancel previous workflows for same branch (#1726) --- .github/workflows/analysis-sonarcloud.yml | 16 +++++++++------- .github/workflows/build-docker.yml | 16 +++++++++------- .github/workflows/build-ubuntu.yml | 16 +++++++++------- .github/workflows/build-windows-cmake.yml | 16 +++++++++------- .github/workflows/build-windows-solution.yml | 16 +++++++++------- .github/workflows/clang-lint.yml | 9 +++++++++ .github/workflows/cron-stale.yml | 9 +++++++++ .github/workflows/pr-labeler.yml | 9 +++++++++ 8 files changed, 72 insertions(+), 35 deletions(-) diff --git a/.github/workflows/analysis-sonarcloud.yml b/.github/workflows/analysis-sonarcloud.yml index 957653a2a9a..1fea280e798 100644 --- a/.github/workflows/analysis-sonarcloud.yml +++ b/.github/workflows/analysis-sonarcloud.yml @@ -19,18 +19,20 @@ env: VCPKG_BINARY_SOURCES: clear;default,readwrite jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + sonarcloud: name: SonarCloud runs-on: ubuntu-22.04 steps: - - name: Cancel Previous Runs - if: github.ref != 'refs/heads/main' - uses: fkirc/skip-duplicate-actions@master - with: - concurrent_skipping: 'same_content' - cancel_others: true - - uses: actions/checkout@v3 if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} with: diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 16285df71f0..96b63e2424e 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -14,17 +14,19 @@ on: - main jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + build_docker_x86: if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - - name: Cancel Previous Runs - if: github.ref != 'refs/heads/main' - uses: fkirc/skip-duplicate-actions@master - with: - concurrent_skipping: 'same_content' - cancel_others: true - - name: Checkout uses: actions/checkout@main with: diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index b42384c1fc5..cadf04de2f9 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -18,6 +18,15 @@ env: MAKEFLAGS: '-j 2' jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + job: if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} name: ${{ matrix.os }}-${{ matrix.buildtype }} @@ -35,13 +44,6 @@ jobs: triplet: x64-linux steps: - - name: Cancel Previous Runs - if: github.ref != 'refs/heads/main' - uses: fkirc/skip-duplicate-actions@master - with: - concurrent_skipping: 'same_content' - cancel_others: true - - name: Checkout repository uses: actions/checkout@main diff --git a/.github/workflows/build-windows-cmake.yml b/.github/workflows/build-windows-cmake.yml index e834895ecb2..cc3f2e873af 100644 --- a/.github/workflows/build-windows-cmake.yml +++ b/.github/workflows/build-windows-cmake.yml @@ -15,6 +15,15 @@ env: CMAKE_BUILD_PARALLEL_LEVEL: 2 MAKEFLAGS: '-j 2' jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + job: if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} name: ${{ matrix.os }}-${{ matrix.buildtype }} @@ -29,13 +38,6 @@ jobs: packages: > sccache steps: - - name: Cancel Previous Runs - if: github.ref != 'refs/heads/main' - uses: fkirc/skip-duplicate-actions@master - with: - concurrent_skipping: 'same_content' - cancel_others: true - - name: Checkout repository uses: actions/checkout@main diff --git a/.github/workflows/build-windows-solution.yml b/.github/workflows/build-windows-solution.yml index 47e2069a2ad..741e94776a0 100644 --- a/.github/workflows/build-windows-solution.yml +++ b/.github/workflows/build-windows-solution.yml @@ -18,6 +18,15 @@ env: MAKEFLAGS: '-j 2' jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + job: if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} name: ${{ matrix.os }}-${{ matrix.buildtype }} @@ -32,13 +41,6 @@ jobs: packages: > sccache steps: - - name: Cancel Previous Runs - if: github.ref != 'refs/heads/main' - uses: fkirc/skip-duplicate-actions@master - with: - concurrent_skipping: 'same_content' - cancel_others: true - - name: Setup MSBuild.exe uses: microsoft/setup-msbuild@v1.1 diff --git a/.github/workflows/clang-lint.yml b/.github/workflows/clang-lint.yml index 070fd5ea997..ead8f6666b9 100644 --- a/.github/workflows/clang-lint.yml +++ b/.github/workflows/clang-lint.yml @@ -8,6 +8,15 @@ on: paths: - 'src/**' jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + build: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/cron-stale.yml b/.github/workflows/cron-stale.yml index 57df64dae6a..4b7f409b26a 100644 --- a/.github/workflows/cron-stale.yml +++ b/.github/workflows/cron-stale.yml @@ -5,6 +5,15 @@ on: - cron: '30 1 * * *' jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + stale: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 066aaa23595..c21b72ab371 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -4,6 +4,15 @@ on: - pull_request_target jobs: + cancel-runs: + if: github.ref != 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + triage: runs-on: ubuntu-latest steps: