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 1/4] 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 2/4] 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 3/4] 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 4/4] 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 @@ +