diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 32f3c598b81..6bf610ad691 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -875,7 +875,7 @@ void Creature::getPathSearchParams(const std::shared_ptr &, FindPathPa void Creature::goToFollowCreature_async(std::function &&onComplete) { if (!hasAsyncTaskFlag(Pathfinder) && onComplete) { - g_dispatcher().addEvent(std::move(onComplete), "goToFollowCreature_async"); + g_dispatcher().addEvent(std::move(onComplete), "Creature::goToFollowCreature_async"); } setAsyncTaskFlag(Pathfinder, true); diff --git a/src/creatures/players/imbuements/imbuements.cpp b/src/creatures/players/imbuements/imbuements.cpp index c71a56fd588..0af0b21d642 100644 --- a/src/creatures/players/imbuements/imbuements.cpp +++ b/src/creatures/players/imbuements/imbuements.cpp @@ -12,9 +12,12 @@ #include "config/configmanager.hpp" #include "creatures/players/player.hpp" #include "items/item.hpp" +#include "items/tile.hpp" +#include "game/scheduling/dispatcher.hpp" #include "lib/di/container.hpp" #include "utils/pugicast.hpp" -#include +#include "utils/const.hpp" +#include "utils/tools.hpp" Imbuements &Imbuements::getInstance() { return inject(); @@ -417,3 +420,165 @@ const std::vector> &Imbuement::getItems() const { uint16_t Imbuement::getIconID() const { return icon + (baseid - 1); } + +ImbuementDecay &ImbuementDecay::getInstance() { + return inject(); +} + +void ImbuementDecay::startImbuementDecay(const std::shared_ptr &item) { + if (!item) { + g_logger().error("[{}] item is nullptr", __FUNCTION__); + return; + } + + if (m_itemsToDecay.find(item) != m_itemsToDecay.end()) { + return; + } + + g_logger().debug("Starting imbuement decay for item {}", item->getName()); + + m_itemsToDecay.insert(item); + + if (m_eventId == 0) { + m_lastUpdateTime = OTSYS_TIME(); + m_eventId = g_dispatcher().scheduleEvent( + 60000, [this] { checkImbuementDecay(); }, "ImbuementDecay::checkImbuementDecay" + ); + g_logger().trace("Scheduled imbuement decay check every 60 seconds."); + } +} + +void ImbuementDecay::stopImbuementDecay(const std::shared_ptr &item) { + if (!item) { + g_logger().error("[{}] item is nullptr", __FUNCTION__); + return; + } + + g_logger().debug("Stopping imbuement decay for item {}", item->getName()); + + int64_t currentTime = OTSYS_TIME(); + int64_t elapsedTime = currentTime - m_lastUpdateTime; + + for (uint8_t slotid = 0; slotid < item->getImbuementSlot(); slotid++) { + ImbuementInfo imbuementInfo; + if (!item->getImbuementInfo(slotid, &imbuementInfo)) { + continue; + } + + uint32_t duration = imbuementInfo.duration > elapsedTime / 1000 ? imbuementInfo.duration - static_cast(elapsedTime / 1000) : 0; + item->decayImbuementTime(slotid, imbuementInfo.imbuement->getID(), duration); + + if (duration == 0) { + if (auto player = item->getHoldingPlayer()) { + player->removeItemImbuementStats(imbuementInfo.imbuement); + player->updateImbuementTrackerStats(); + } + } + } + + m_itemsToDecay.erase(item); + + if (m_itemsToDecay.empty() && m_eventId != 0) { + g_dispatcher().stopEvent(m_eventId); + m_eventId = 0; + g_logger().trace("No more items to decay. Stopped imbuement decay scheduler."); + } +} + +void ImbuementDecay::checkImbuementDecay() { + int64_t currentTime = OTSYS_TIME(); + int64_t elapsedTime = currentTime - m_lastUpdateTime; + m_lastUpdateTime = currentTime; + + g_logger().trace("Checking imbuement decay for {} items.", m_itemsToDecay.size()); + + std::vector> itemsToRemove; + + for (const auto &item : m_itemsToDecay) { + if (!item) { + g_logger().error("[{}] item is nullptr", __FUNCTION__); + itemsToRemove.push_back(item); + continue; + } + + // Get the player holding the item (if any) + auto player = item->getHoldingPlayer(); + if (!player) { + g_logger().debug("Item {} is not held by any player. Skipping decay.", item->getName()); + continue; + } + + bool removeItem = true; + + for (uint8_t slotid = 0; slotid < item->getImbuementSlot(); ++slotid) { + ImbuementInfo imbuementInfo; + if (!item->getImbuementInfo(slotid, &imbuementInfo)) { + g_logger().debug("No imbuement info found for slot {} in item {}", slotid, item->getName()); + continue; + } + + // Get the tile the player is currently on + const auto &playerTile = player->getTile(); + // Check if the player is in a protection zone + const bool &isInProtectionZone = playerTile && playerTile->hasFlag(TILESTATE_PROTECTIONZONE); + // Check if the player is in fight mode + bool isInFightMode = player->hasCondition(CONDITION_INFIGHT); + bool nonAggressiveFightOnly = g_configManager().getBoolean(TOGGLE_IMBUEMENT_NON_AGGRESSIVE_FIGHT_ONLY); + + // Imbuement from imbuementInfo, this variable reduces code complexity + const auto imbuement = imbuementInfo.imbuement; + // Get the category of the imbuement + const CategoryImbuement* categoryImbuement = g_imbuements().getCategoryByID(imbuement->getCategory()); + // Parent of the imbued item + const auto &parent = item->getParent(); + const bool &isInBackpack = parent && parent->getContainer(); + // If the imbuement is aggressive and the player is not in fight mode or is in a protection zone, or the item is in a container, ignore it. + if (categoryImbuement && (categoryImbuement->agressive || nonAggressiveFightOnly) && (isInProtectionZone || !isInFightMode || isInBackpack)) { + continue; + } + // If the item is not in the backpack slot and it's not a agressive imbuement, ignore it. + if (categoryImbuement && !categoryImbuement->agressive && parent && parent != player) { + continue; + } + + // If the imbuement's duration is 0, remove its stats and continue to the next slot + if (imbuementInfo.duration == 0) { + player->removeItemImbuementStats(imbuement); + player->updateImbuementTrackerStats(); + continue; + } + + g_logger().debug("Decaying imbuement {} from item {} of player {}", imbuement->getName(), item->getName(), player->getName()); + // Calculate the new duration of the imbuement, making sure it doesn't go below 0 + uint32_t duration = imbuementInfo.duration > elapsedTime / 1000 ? imbuementInfo.duration - static_cast(elapsedTime / 1000) : 0; + item->decayImbuementTime(slotid, imbuement->getID(), duration); + + if (duration == 0) { + player->removeItemImbuementStats(imbuement); + player->updateImbuementTrackerStats(); + } + + removeItem = false; + } + + if (removeItem) { + itemsToRemove.push_back(item); + } + } + + // Remove items whose imbuements have expired + for (const auto &item : itemsToRemove) { + m_itemsToDecay.erase(item); + } + + // Reschedule the event if there are still items + if (!m_itemsToDecay.empty()) { + m_eventId = g_dispatcher().scheduleEvent( + 60000, [this] { checkImbuementDecay(); }, "ImbuementDecay::checkImbuementDecay" + ); + g_logger().trace("Re-scheduled imbuement decay check."); + } else { + m_eventId = 0; + g_logger().trace("No more items to decay. Stopped imbuement decay scheduler."); + } +} diff --git a/src/creatures/players/imbuements/imbuements.hpp b/src/creatures/players/imbuements/imbuements.hpp index b2429cc8137..4dcbd04b2c6 100644 --- a/src/creatures/players/imbuements/imbuements.hpp +++ b/src/creatures/players/imbuements/imbuements.hpp @@ -124,3 +124,25 @@ class Imbuement { std::vector> items; }; + +class ImbuementDecay { +public: + ImbuementDecay() = default; + + // Non-copyable + ImbuementDecay(const ImbuementDecay &) = delete; + ImbuementDecay &operator=(const ImbuementDecay &) = delete; + + static ImbuementDecay &getInstance(); + + void startImbuementDecay(const std::shared_ptr &item); + void stopImbuementDecay(const std::shared_ptr &item); + void checkImbuementDecay(); + +private: + std::unordered_set> m_itemsToDecay; + int64_t m_lastUpdateTime = 0; + uint32_t m_eventId { 0 }; +}; + +constexpr auto g_imbuementDecay = ImbuementDecay::getInstance; diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 3ef1d64e982..9cdea21d723 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -47,6 +47,7 @@ #include "io/iobestiary.hpp" #include "io/iologindata.hpp" #include "io/ioprey.hpp" +#include "creatures/players/imbuements/imbuements.hpp" #include "items/bed.hpp" #include "items/containers/depot/depotchest.hpp" #include "items/containers/depot/depotlocker.hpp" @@ -680,63 +681,6 @@ void Player::updateInventoryWeight() { } } -void Player::updateInventoryImbuement() { - // Get the tile the player is currently on - const auto &playerTile = getTile(); - // Check if the player is in a protection zone - const bool &isInProtectionZone = playerTile && playerTile->hasFlag(TILESTATE_PROTECTIONZONE); - // Check if the player is in fight mode - bool isInFightMode = hasCondition(CONDITION_INFIGHT); - bool nonAggressiveFightOnly = g_configManager().getBoolean(TOGGLE_IMBUEMENT_NON_AGGRESSIVE_FIGHT_ONLY); - - // Iterate through all items in the player's inventory - for (const auto &[slodNumber, item] : getAllSlotItems()) { - // Iterate through all imbuement slots on the item - for (uint8_t slotid = 0; slotid < item->getImbuementSlot(); slotid++) { - ImbuementInfo imbuementInfo; - // Get the imbuement information for the current slot - if (!item->getImbuementInfo(slotid, &imbuementInfo)) { - // If no imbuement is found, continue to the next slot - continue; - } - - // Imbuement from imbuementInfo, this variable reduces code complexity - const auto imbuement = imbuementInfo.imbuement; - // Get the category of the imbuement - const CategoryImbuement* categoryImbuement = g_imbuements().getCategoryByID(imbuement->getCategory()); - // Parent of the imbued item - const auto &parent = item->getParent(); - const bool &isInBackpack = parent && parent->getContainer(); - // If the imbuement is aggressive and the player is not in fight mode or is in a protection zone, or the item is in a container, ignore it. - if (categoryImbuement && (categoryImbuement->agressive || nonAggressiveFightOnly) && (isInProtectionZone || !isInFightMode || isInBackpack)) { - continue; - } - // If the item is not in the backpack slot and it's not a agressive imbuement, ignore it. - if (categoryImbuement && !categoryImbuement->agressive && parent && parent != getPlayer()) { - continue; - } - - // If the imbuement's duration is 0, remove its stats and continue to the next slot - if (imbuementInfo.duration == 0) { - removeItemImbuementStats(imbuement); - updateImbuementTrackerStats(); - continue; - } - - g_logger().trace("Decaying imbuement {} from item {} of player {}", imbuement->getName(), item->getName(), getName()); - // Calculate the new duration of the imbuement, making sure it doesn't go below 0 - const uint32_t duration = std::max(0, imbuementInfo.duration - EVENT_IMBUEMENT_INTERVAL / 1000); - // Update the imbuement's duration in the item - item->decayImbuementTime(slotid, imbuement->getID(), duration); - - if (duration == 0) { - removeItemImbuementStats(imbuement); - updateImbuementTrackerStats(); - } - } - } -} - phmap::flat_hash_map> Player::getAllSlotItems() const { phmap::flat_hash_map> itemMap; for (uint8_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { @@ -2338,6 +2282,7 @@ void Player::onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr< addItemImbuementStats(imbuement); } + g_imbuementDecay().startImbuementDecay(item); item->addImbuement(slot, imbuement->getID(), baseImbuement->duration); openImbuementWindow(item); } diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 6ab3be4ca7a..e9f8f9cb5c8 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -1283,11 +1283,6 @@ class Player final : public Creature, public Cylinder, public Bankable { void removeExperience(uint64_t exp, bool sendText = false); void updateInventoryWeight(); - /** - * @brief Starts checking the imbuements in the item so that the time decay is performed - * Registers the player in an unordered_map in game.h so that the function can be initialized by the task - */ - void updateInventoryImbuement(); void setNextWalkActionTask(const std::shared_ptr &task); void setNextWalkTask(const std::shared_ptr &task); diff --git a/src/game/game.cpp b/src/game/game.cpp index cde008d4f65..a555ccf1d3f 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -541,9 +541,6 @@ void Game::start(ServiceManager* manager) { g_dispatcher().cycleEvent( EVENT_CHECK_CREATURE_INTERVAL, [this] { checkCreatures(); }, "Game::checkCreatures" ); - g_dispatcher().cycleEvent( - EVENT_IMBUEMENT_INTERVAL, [this] { checkImbuements(); }, "Game::checkImbuements" - ); g_dispatcher().cycleEvent( EVENT_LUA_GARBAGE_COLLECTION, [this] { g_luaEnvironment().collectGarbage(); }, "Calling GC" ); @@ -8008,16 +8005,6 @@ void Game::addDistanceEffect(const CreatureVector &spectators, const Position &f } } -void Game::checkImbuements() const { - for (const auto &[mapPlayerId, mapPlayer] : getPlayers()) { - if (!mapPlayer) { - continue; - } - - mapPlayer->updateInventoryImbuement(); - } -} - void Game::checkLight() { lightHour += lightHourDelta; diff --git a/src/game/game.hpp b/src/game/game.hpp index b1afd810100..b848d98b313 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -730,7 +730,6 @@ class Game { std::map forgeMonsterEventIds; std::unordered_set fiendishMonsters; std::unordered_set influencedMonsters; - void checkImbuements() const; bool playerSaySpell(const std::shared_ptr &player, SpeakClasses type, const std::string &text); void playerWhisper(const std::shared_ptr &player, const std::string &text); bool playerYell(const std::shared_ptr &player, const std::string &text); diff --git a/src/game/scheduling/task.hpp b/src/game/scheduling/task.hpp index c01dbe2f676..afca7222b3f 100644 --- a/src/game/scheduling/task.hpp +++ b/src/game/scheduling/task.hpp @@ -69,7 +69,6 @@ class Task { "Game::checkCreatureAttack", "Game::checkCreatureWalk", "Game::checkCreatures", - "Game::checkImbuements", "Game::checkLight", "Game::createFiendishMonsters", "Game::createInfluencedMonsters", @@ -88,7 +87,9 @@ class Task { "SpawnNpc::checkSpawnNpc", "Webhook::run", "Protocol::sendRecvMessageCallback", - "Player::addInFightTicks" + "Player::addInFightTicks", + "Map::moveCreature", + "Creature::goToFollowCreature_async" }; return tasksContext.contains(context); diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 7cb08a7b9a6..c10f6f1d0e8 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -29,6 +29,7 @@ #include "io/ioprey.hpp" #include "items/containers/depot/depotchest.hpp" #include "items/containers/inbox/inbox.hpp" +#include "creatures/players/imbuements/imbuements.hpp" #include "items/containers/rewards/reward.hpp" #include "items/containers/rewards/rewardchest.hpp" #include "creatures/players/player.hpp" @@ -516,6 +517,7 @@ void IOLoginDataLoad::loadPlayerInventoryItems(const std::shared_ptr &pl ItemsMap inventoryItems; std::vector>> openContainersList; std::vector> itemsToStartDecaying; + std::vector> itemsToStartDecayImbuement; try { if ((result = g_database().storeQuery(query))) { @@ -543,11 +545,17 @@ void IOLoginDataLoad::loadPlayerInventoryItems(const std::shared_ptr &pl container->internalAddThing(item); // Here, the sub-containers do not yet have a parent, since the main backpack has not yet been added to the player, so we need to postpone itemsToStartDecaying.emplace_back(item); + if (container->hasImbuements()) { + itemsToStartDecayImbuement.emplace_back(container); + } } } const std::shared_ptr &itemContainer = item->getContainer(); if (itemContainer) { + if (itemContainer->hasImbuements()) { + itemsToStartDecayImbuement.emplace_back(itemContainer); + } if (!oldProtocol) { auto cid = item->getAttribute(ItemAttribute_t::OPENCONTAINER); if (cid > 0) { @@ -575,6 +583,11 @@ void IOLoginDataLoad::loadPlayerInventoryItems(const std::shared_ptr &pl item->startDecaying(); } + // Start imbuement decay on login for backpacks + for (const auto &item : itemsToStartDecayImbuement) { + g_imbuementDecay().startImbuementDecay(item); + } + if (!oldProtocol) { std::ranges::sort(openContainersList.begin(), openContainersList.end(), [](const std::pair> &left, const std::pair> &right) { return left.first < right.first; diff --git a/src/lua/creature/movement.cpp b/src/lua/creature/movement.cpp index 6804cdfa7e9..1be79dcef22 100644 --- a/src/lua/creature/movement.cpp +++ b/src/lua/creature/movement.cpp @@ -17,6 +17,7 @@ #include "lua/callbacks/event_callback.hpp" #include "lua/callbacks/events_callbacks.hpp" #include "lua/creature/events.hpp" +#include "creatures/players/imbuements/imbuements.hpp" #include "lua/scripts/scripts.hpp" #include "creatures/players/vocations/vocation.hpp" #include "items/item.hpp" @@ -554,6 +555,8 @@ uint32_t MoveEvent::EquipItem(const std::shared_ptr &moveEvent, const continue; } + g_imbuementDecay().startImbuementDecay(item); + player->addItemImbuementStats(imbuementInfo.imbuement); } @@ -652,6 +655,7 @@ uint32_t MoveEvent::DeEquipItem(const std::shared_ptr &, const std::s continue; } + g_imbuementDecay().stopImbuementDecay(item); player->removeItemImbuementStats(imbuementInfo.imbuement); }