From 926a5f3e1605f73b0560c994a0116cec5685be7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Lu=C3=ADs=20Lucarelo=20Lamonato?= Date: Tue, 2 Jul 2024 16:13:00 -0300 Subject: [PATCH 1/2] feat: possibility to persist NPC on map with /n talkaction (#2682) --- data/scripts/talkactions/god/create_npc.lua | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/data/scripts/talkactions/god/create_npc.lua b/data/scripts/talkactions/god/create_npc.lua index 4aeec3dde80..b6d0412d391 100644 --- a/data/scripts/talkactions/god/create_npc.lua +++ b/data/scripts/talkactions/god/create_npc.lua @@ -1,3 +1,6 @@ +-- To summon a temporary npc use /n npcname +-- To summon a permanent npc use /n npcname,true + local createNpc = TalkAction("/n") function createNpc.onSay(player, words, param) @@ -9,11 +12,44 @@ function createNpc.onSay(player, words, param) return true end + local split = param:split(",") + local name = split[1] + local permanentStr = split[2] + local position = player:getPosition() - local npc = Game.createNpc(param, position) + local npc = Game.createNpc(name, position) if npc then npc:setMasterPos(position) position:sendMagicEffect(CONST_ME_MAGIC_RED) + + if permanentStr and permanentStr == "true" then + local mapName = configManager.getString(configKeys.MAP_NAME) + local mapNpcsPath = mapName .. "-npc.xml" + local filePath = string.format("%s/world/%s", DATA_DIRECTORY, mapNpcsPath) + local npcsFile = io.open(filePath, "r") + if not npcsFile then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There was an error when trying to add permanent NPC. NPC File not found.") + return true + end + local fileContent = npcsFile:read("*all") + npcsFile:close() + local endTag = "" + if not fileContent:find(endTag, 1, true) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There was an error when trying to add permanent NPC. The NPC file format is incorrect. Missing end tag " .. endTag .. ".") + return true + end + local textToAdd = string.format('\t\n\t\t\n\t', position.x, position.y, position.z, name, position.z) + local newFileContent = fileContent:gsub(endTag, textToAdd .. "\n" .. endTag) + npcsFile = io.open(filePath, "w") + if not npcsFile then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There was an error when trying to write to the NPC file.") + return true + end + npcsFile:write(newFileContent) + npcsFile:close() + + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Permanent NPC added successfully.") + end else player:sendCancelMessage("There is not enough room.") position:sendMagicEffect(CONST_ME_POFF) From 3aeb3e75d567f94a1e865630c3ed496b819e45b3 Mon Sep 17 00:00:00 2001 From: Elson Costa Date: Fri, 5 Jul 2024 17:44:53 -0300 Subject: [PATCH 2/2] feat: cyclopedia improvements (#2629) --- data/modules/scripts/blessings/blessings.lua | 4 +- data/modules/scripts/gamestore/gamestore.lua | 4 +- data/modules/scripts/gamestore/init.lua | 31 +-- .../scripts/eventcallbacks/player/on_look.lua | 7 +- src/creatures/CMakeLists.txt | 1 + .../achievement/player_achievement.cpp | 13 +- .../achievement/player_achievement.hpp | 6 +- .../players/cyclopedia/player_cyclopedia.cpp | 185 ++++++++++++++++++ .../players/cyclopedia/player_cyclopedia.hpp | 46 +++++ .../players/cyclopedia/player_title.cpp | 3 +- src/creatures/players/player.cpp | 27 ++- src/creatures/players/player.hpp | 43 ++-- src/enums/player_cyclopedia.hpp | 10 + src/game/game.cpp | 173 ++++++---------- src/game/game.hpp | 12 ++ src/game/game_definitions.hpp | 11 ++ src/io/functions/iologindata_load_player.cpp | 7 +- .../functions/core/game/game_functions.cpp | 1 + .../creatures/player/player_functions.cpp | 27 ++- .../creatures/player/player_functions.hpp | 5 + src/server/network/protocol/protocolgame.cpp | 167 ++++++++++++---- src/server/network/protocol/protocolgame.hpp | 1 + src/utils/tools.cpp | 25 +++ src/utils/tools.hpp | 2 + src/utils/utils_definitions.hpp | 11 -- vcproj/canary.vcxproj | 2 + 26 files changed, 597 insertions(+), 227 deletions(-) create mode 100644 src/creatures/players/cyclopedia/player_cyclopedia.cpp create mode 100644 src/creatures/players/cyclopedia/player_cyclopedia.hpp diff --git a/data/modules/scripts/blessings/blessings.lua b/data/modules/scripts/blessings/blessings.lua index adfa364e7e1..e061501a330 100644 --- a/data/modules/scripts/blessings/blessings.lua +++ b/data/modules/scripts/blessings/blessings.lua @@ -21,7 +21,7 @@ Blessings.Credits = { Blessings.Config = { AdventurerBlessingLevel = configManager.getNumber(configKeys.ADVENTURERSBLESSING_LEVEL), -- Free full bless until level - HasToF = false, -- Enables/disables twist of fate + HasToF = not configManager.getBoolean(configKeys.TOGGLE_SERVER_IS_RETRO), -- Enables/disables twist of fate InquisitonBlessPriceMultiplier = 1.1, -- Bless price multiplied by henricus SkulledDeathLoseStoreItem = configManager.getBoolean(configKeys.SKULLED_DEATH_LOSE_STORE_ITEM), -- Destroy all items on store when dying with red/blackskull InventoryGlowOnFiveBless = configManager.getBoolean(configKeys.INVENTORY_GLOW), -- Glow in yellow inventory items when the player has 5 or more bless, @@ -142,7 +142,7 @@ Blessings.sendBlessDialog = function(player) msg:addU16(Blessings.BitWiseTable[v.id]) msg:addByte(player:getBlessingCount(v.id)) if player:getClient().version > 1200 then - msg:addByte(0) -- Store Blessings Count + msg:addByte(player:getBlessingCount(v.id, true)) -- Store Blessings Count end end end diff --git a/data/modules/scripts/gamestore/gamestore.lua b/data/modules/scripts/gamestore/gamestore.lua index f000c4079af..ed17c456c0d 100644 --- a/data/modules/scripts/gamestore/gamestore.lua +++ b/data/modules/scripts/gamestore/gamestore.lua @@ -135,7 +135,7 @@ GameStore.Categories = { icons = { "Blood_of_the_Mountain.png" }, name = "Blood of the Mountain", price = 25, - blessid = 8, + blessid = 7, count = 1, id = GameStore.SubActions.BLESSING_BLOOD, description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", @@ -154,7 +154,7 @@ GameStore.Categories = { icons = { "Heart_of_the_Mountain.png" }, name = "Heart of the Mountain", price = 25, - blessid = 7, + blessid = 8, count = 1, id = GameStore.SubActions.BLESSING_HEART, description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua index 322a7749c93..433abf34492 100644 --- a/data/modules/scripts/gamestore/init.lua +++ b/data/modules/scripts/gamestore/init.lua @@ -47,8 +47,8 @@ GameStore.SubActions = { BLESSING_SUNS = 6, BLESSING_SPIRITUAL = 7, BLESSING_EMBRACE = 8, - BLESSING_HEART = 9, - BLESSING_BLOOD = 10, + BLESSING_BLOOD = 9, + BLESSING_HEART = 10, BLESSING_ALL_PVE = 11, BLESSING_ALL_PVP = 12, CHARM_EXPANSION = 13, @@ -399,6 +399,17 @@ function parseRequestStoreOffers(playerId, msg) player:updateUIExhausted() end +-- Used on cyclopedia store summary +local function insertPlayerTransactionSummary(player, offer) + local id = offer.id + if offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE then + id = offer.itemtype + elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_BLESSINGS then + id = offer.blessid + end + player:createTransactionSummary(offer.type, math.max(1, offer.count or 1), id) +end + function parseBuyStoreOffer(playerId, msg) local player = Player(playerId) local id = msg:getU32() @@ -450,9 +461,7 @@ function parseBuyStoreOffer(playerId, msg) -- Handled errors are thrown to indicate that the purchase has failed; -- Handled errors have a code index and unhandled errors do not local pcallOk, pcallError = pcall(function() - if offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM then - GameStore.processItemPurchase(player, offer.itemtype, offer.count or 1, offer.movable, offer.setOwner) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE then + if offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM or offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE then GameStore.processItemPurchase(player, offer.itemtype, offer.count or 1, offer.movable, offer.setOwner) elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS then GameStore.processInstantRewardAccess(player, offer.count) @@ -466,11 +475,9 @@ function parseBuyStoreOffer(playerId, msg) GameStore.processPremiumPurchase(player, offer.id) elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_STACKABLE then GameStore.processStackablePurchase(player, offer.itemtype, offer.count, offer.name, offer.movable, offer.setOwner) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE then + elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE or offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_BED then GameStore.processHouseRelatedPurchase(player, offer) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT then - GameStore.processOutfitPurchase(player, offer.sexId, offer.addon) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then + elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT or offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then GameStore.processOutfitPurchase(player, offer.sexId, offer.addon) elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then GameStore.processMountPurchase(player, offer.id) @@ -504,8 +511,6 @@ function parseBuyStoreOffer(playerId, msg) GameStore.processHirelingSkillPurchase(player, offer) elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT then GameStore.processHirelingOutfitPurchase(player, offer) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_BED then - GameStore.processHouseRelatedPurchase(player, offer) else -- This should never happen by our convention, but just in case the guarding condition is messed up... error({ code = 0, message = "This offer is unavailable [2]" }) @@ -523,6 +528,9 @@ function parseBuyStoreOffer(playerId, msg) return queueSendStoreAlertToUser(alertMessage, 500, playerId) end + if table.contains({ GameStore.OfferTypes.OFFER_TYPE_HOUSE, GameStore.OfferTypes.OFFER_TYPE_EXPBOOST, GameStore.OfferTypes.OFFER_TYPE_PREYBONUS, GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS, GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS }, offer.type) then + insertPlayerTransactionSummary(player, offer) + end local configure = useOfferConfigure(offer.type) if configure ~= GameStore.ConfigureOffers.SHOW_CONFIGURE then if not player:makeCoinTransaction(offer) then @@ -1827,6 +1835,7 @@ function GameStore.processHirelingPurchase(player, offer, productType, hirelingN player:makeCoinTransaction(offer, hirelingName) local message = "You have successfully bought " .. hirelingName + player:createTransactionSummary(offer.type, 1) return addPlayerEvent(sendStorePurchaseSuccessful, 650, player:getId(), message) -- If not, we ask him to do! else diff --git a/data/scripts/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua index 90d3089052d..022aebbcc36 100644 --- a/data/scripts/eventcallbacks/player/on_look.lua +++ b/data/scripts/eventcallbacks/player/on_look.lua @@ -60,11 +60,12 @@ function callback.playerOnLook(player, thing, position, distance) description = string.format("%s\nDecays to: %d", description, decayId) end elseif thing:isCreature() then - local str = "%s\nHealth: %d / %d" + local str, pId = "%s\n%s\nHealth: %d / %d" if thing:isPlayer() and thing:getMaxMana() > 0 then + pId = string.format("Player ID: %i", thing:getGuid()) str = string.format("%s, Mana: %d / %d", str, thing:getMana(), thing:getMaxMana()) end - description = string.format(str, description, thing:getHealth(), thing:getMaxHealth()) .. "." + description = string.format(str, description, pId, thing:getHealth(), thing:getMaxHealth()) end description = string.format("%s\nPosition: (%d, %d, %d)", description, position.x, position.y, position.z) @@ -76,7 +77,7 @@ function callback.playerOnLook(player, thing, position, distance) description = string.format("%s\nSpeed: %d", description, speed) if thing:isPlayer() then - description = string.format("%s\nIP: %s.", description, Game.convertIpToString(thing:getIp())) + description = string.format("%s\nIP: %s", description, Game.convertIpToString(thing:getIp())) end end end diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt index af6ba129eac..c87a45a0aa0 100644 --- a/src/creatures/CMakeLists.txt +++ b/src/creatures/CMakeLists.txt @@ -23,6 +23,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE players/player.cpp players/achievement/player_achievement.cpp players/cyclopedia/player_badge.cpp + players/cyclopedia/player_cyclopedia.cpp players/cyclopedia/player_title.cpp players/wheel/player_wheel.cpp players/wheel/wheel_gems.cpp diff --git a/src/creatures/players/achievement/player_achievement.cpp b/src/creatures/players/achievement/player_achievement.cpp index 69d1d7ab1fa..cd0735ab54c 100644 --- a/src/creatures/players/achievement/player_achievement.cpp +++ b/src/creatures/players/achievement/player_achievement.cpp @@ -35,7 +35,7 @@ bool PlayerAchievement::add(uint16_t id, bool message /* = true*/, uint32_t time addPoints(achievement.points); int toSaveTimeStamp = timestamp != 0 ? timestamp : (OTSYS_TIME() / 1000); getUnlockedKV()->set(achievement.name, toSaveTimeStamp); - m_achievementsUnlocked.push_back({ achievement.id, toSaveTimeStamp }); + m_achievementsUnlocked.emplace_back(achievement.id, toSaveTimeStamp); m_achievementsUnlocked.shrink_to_fit(); return true; } @@ -80,7 +80,8 @@ bool PlayerAchievement::isUnlocked(uint16_t id) const { } uint16_t PlayerAchievement::getPoints() const { - return m_player.kv()->scoped("achievements")->get("points")->getNumber(); + auto kvScoped = m_player.kv()->scoped("achievements")->get("points"); + return kvScoped ? static_cast(kvScoped->getNumber()) : 0; } void PlayerAchievement::addPoints(uint16_t toAddPoints) { @@ -109,12 +110,12 @@ void PlayerAchievement::loadUnlockedAchievements() { g_logger().debug("[{}] - Achievement {} found for player {}.", __FUNCTION__, achievementName, m_player.getName()); - m_achievementsUnlocked.push_back({ achievement.id, getUnlockedKV()->get(achievementName)->getNumber() }); + m_achievementsUnlocked.emplace_back(achievement.id, getUnlockedKV()->get(achievementName)->getNumber()); } } void PlayerAchievement::sendUnlockedSecretAchievements() { - std::vector> m_achievementsUnlocked; + std::vector> achievementsUnlocked; uint16_t unlockedSecret = 0; for (const auto &[achievId, achievCreatedTime] : getUnlockedAchievements()) { Achievement achievement = g_game().getAchievementById(achievId); @@ -126,10 +127,10 @@ void PlayerAchievement::sendUnlockedSecretAchievements() { unlockedSecret++; } - m_achievementsUnlocked.push_back({ achievement, achievCreatedTime }); + achievementsUnlocked.emplace_back(achievement, achievCreatedTime); } - m_player.sendCyclopediaCharacterAchievements(unlockedSecret, m_achievementsUnlocked); + m_player.sendCyclopediaCharacterAchievements(unlockedSecret, achievementsUnlocked); } const std::shared_ptr &PlayerAchievement::getUnlockedKV() { diff --git a/src/creatures/players/achievement/player_achievement.hpp b/src/creatures/players/achievement/player_achievement.hpp index d1073a9bf1e..e0c027e5808 100644 --- a/src/creatures/players/achievement/player_achievement.hpp +++ b/src/creatures/players/achievement/player_achievement.hpp @@ -31,11 +31,11 @@ class PlayerAchievement { explicit PlayerAchievement(Player &player); bool add(uint16_t id, bool message = true, uint32_t timestamp = 0); bool remove(uint16_t id); - bool isUnlocked(uint16_t id) const; - uint16_t getPoints() const; + [[nodiscard]] bool isUnlocked(uint16_t id) const; + [[nodiscard]] uint16_t getPoints() const; void addPoints(uint16_t toAddPoints); void removePoints(uint16_t toRemovePoints); - std::vector> getUnlockedAchievements() const; + [[nodiscard]] std::vector> getUnlockedAchievements() const; void loadUnlockedAchievements(); void sendUnlockedSecretAchievements(); const std::shared_ptr &getUnlockedKV(); diff --git a/src/creatures/players/cyclopedia/player_cyclopedia.cpp b/src/creatures/players/cyclopedia/player_cyclopedia.cpp new file mode 100644 index 00000000000..abbc920d322 --- /dev/null +++ b/src/creatures/players/cyclopedia/player_cyclopedia.cpp @@ -0,0 +1,185 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 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/ + */ + +#include "pch.hpp" + +#include "database/databasetasks.hpp" +#include "creatures/players/player.hpp" +#include "player_cyclopedia.hpp" +#include "game/game.hpp" +#include "kv/kv.hpp" + +PlayerCyclopedia::PlayerCyclopedia(Player &player) : + m_player(player) { } + +Summary PlayerCyclopedia::getSummary() { + return { getAmount(Summary_t::PREY_CARDS), + getAmount(Summary_t::INSTANT_REWARDS), + getAmount(Summary_t::HIRELINGS) }; +} + +void PlayerCyclopedia::loadSummaryData() { + DBResult_ptr result = g_database().storeQuery(fmt::format("SELECT COUNT(*) as `count` FROM `player_hirelings` WHERE `player_id` = {}", m_player.getGUID())); + auto kvScoped = m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(static_cast(Summary_t::HIRELINGS))); + if (result && !kvScoped->get("amount").has_value()) { + kvScoped->set("amount", result->getNumber("count")); + } +} + +void PlayerCyclopedia::loadDeathHistory(uint16_t page, uint16_t entriesPerPage) { + Benchmark bm_check; + uint32_t offset = static_cast(page - 1) * entriesPerPage; + auto query = fmt::format("SELECT `time`, `level`, `killed_by`, `mostdamage_by`, (select count(*) FROM `player_deaths` WHERE `player_id` = {}) as `entries` FROM `player_deaths` WHERE `player_id` = {} AND `time` >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY)) ORDER BY `time` DESC LIMIT {}, {}", m_player.getGUID(), m_player.getGUID(), offset, entriesPerPage); + + uint32_t playerID = m_player.getID(); + std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) { + std::shared_ptr player = g_game().getPlayerByID(playerID); + if (!player) { + return; + } + + player->resetAsyncOngoingTask(PlayerAsyncTask_RecentDeaths); + if (!result) { + player->sendCyclopediaCharacterRecentDeaths(0, 0, {}); + return; + } + + auto pages = result->getNumber("entries"); + pages += entriesPerPage - 1; + pages /= entriesPerPage; + + std::vector entries; + entries.reserve(result->countResults()); + do { + std::string killed_by = result->getString("killed_by"); + std::string mostdamage_by = result->getString("mostdamage_by"); + + std::string cause = fmt::format("Died at Level {}", result->getNumber("level")); + + if (!killed_by.empty()) { + cause.append(fmt::format(" by{}", formatWithArticle(killed_by))); + } + + if (!mostdamage_by.empty()) { + cause.append(fmt::format("{}{}", !killed_by.empty() ? " and" : "", formatWithArticle(mostdamage_by))); + } + + entries.emplace_back(cause, result->getNumber("time")); + } while (result->next()); + player->sendCyclopediaCharacterRecentDeaths(page, static_cast(pages), entries); + }; + g_databaseTasks().store(query, callback); + m_player.addAsyncOngoingTask(PlayerAsyncTask_RecentDeaths); + + g_logger().debug("Loading death history from the player {} took {} milliseconds.", m_player.getName(), bm_check.duration()); +} + +void PlayerCyclopedia::loadRecentKills(uint16_t page, uint16_t entriesPerPage) { + Benchmark bm_check; + + const std::string &escapedName = g_database().escapeString(m_player.getName()); + uint32_t offset = static_cast(page - 1) * entriesPerPage; + auto query = fmt::format("SELECT `d`.`time`, `d`.`killed_by`, `d`.`mostdamage_by`, `d`.`unjustified`, `d`.`mostdamage_unjustified`, `p`.`name`, (select count(*) FROM `player_deaths` WHERE ((`killed_by` = {} AND `is_player` = 1) OR (`mostdamage_by` = {} AND `mostdamage_is_player` = 1))) as `entries` FROM `player_deaths` AS `d` INNER JOIN `players` AS `p` ON `d`.`player_id` = `p`.`id` WHERE ((`d`.`killed_by` = {} AND `d`.`is_player` = 1) OR (`d`.`mostdamage_by` = {} AND `d`.`mostdamage_is_player` = 1)) AND `time` >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 70 DAY)) ORDER BY `time` DESC LIMIT {}, {}", escapedName, escapedName, escapedName, escapedName, offset, entriesPerPage); + + uint32_t playerID = m_player.getID(); + std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) { + std::shared_ptr player = g_game().getPlayerByID(playerID); + if (!player) { + return; + } + + player->resetAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills); + if (!result) { + player->sendCyclopediaCharacterRecentPvPKills(0, 0, {}); + return; + } + + auto pages = result->getNumber("entries"); + pages += entriesPerPage - 1; + pages /= entriesPerPage; + + std::vector entries; + entries.reserve(result->countResults()); + do { + std::string cause1 = result->getString("killed_by"); + std::string cause2 = result->getString("mostdamage_by"); + std::string name = result->getString("name"); + + uint8_t status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_JUSTIFIED; + if (player->getName() == cause1) { + if (result->getNumber("unjustified") == 1) { + status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED; + } + } else if (player->getName() == cause2) { + if (result->getNumber("mostdamage_unjustified") == 1) { + status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED; + } + } + + entries.emplace_back(fmt::format("Killed {}.", name), result->getNumber("time"), status); + } while (result->next()); + player->sendCyclopediaCharacterRecentPvPKills(page, static_cast(pages), entries); + }; + g_databaseTasks().store(query, callback); + m_player.addAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills); + + g_logger().debug("Loading recent kills from the player {} took {} milliseconds.", m_player.getName(), bm_check.duration()); +} + +void PlayerCyclopedia::updateStoreSummary(uint8_t type, uint16_t amount, const std::string &id) { + switch (type) { + case Summary_t::HOUSE_ITEMS: + case Summary_t::BLESSINGS: + insertValue(type, amount, id); + break; + case Summary_t::ALL_BLESSINGS: + for (int i = 1; i < 8; ++i) { + insertValue(static_cast(Summary_t::BLESSINGS), amount, fmt::format("{}", i)); + } + break; + default: + updateAmount(type, amount); + break; + } +} + +uint16_t PlayerCyclopedia::getAmount(uint8_t type) { + auto kvScope = m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type))->get("amount"); + return static_cast(kvScope ? kvScope->getNumber() : 0); +} + +void PlayerCyclopedia::updateAmount(uint8_t type, uint16_t amount) { + auto oldAmount = getAmount(type); + m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type))->set("amount", oldAmount + amount); +} + +std::map PlayerCyclopedia::getResult(uint8_t type) const { + auto kvScope = m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type)); + std::map result; // ID, amount + for (const auto &scope : kvScope->keys()) { + size_t pos = scope.find('.'); + if (pos == std::string::npos) { + g_logger().error("[{}] Invalid key format: {}", __FUNCTION__, scope); + continue; + } + std::string id = scope.substr(0, pos); + auto amount = kvScope->scoped(id)->get("amount"); + result.emplace(std::stoll(id), static_cast(amount ? amount->getNumber() : 0)); + } + return result; +} + +void PlayerCyclopedia::insertValue(uint8_t type, uint16_t amount, const std::string &id) { + auto result = getResult(type); + auto it = result.find(std::stoll(id)); + auto oldAmount = (it != result.end() ? it->second : 0); + auto newAmount = oldAmount + amount; + m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type))->scoped(id)->set("amount", newAmount); + g_logger().debug("[{}] type: {}, id: {}, old amount: {}, added amount: {}, new amount: {}", __FUNCTION__, type, id, oldAmount, amount, newAmount); +} diff --git a/src/creatures/players/cyclopedia/player_cyclopedia.hpp b/src/creatures/players/cyclopedia/player_cyclopedia.hpp new file mode 100644 index 00000000000..32c446cc368 --- /dev/null +++ b/src/creatures/players/cyclopedia/player_cyclopedia.hpp @@ -0,0 +1,46 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 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 "creatures/creatures_definitions.hpp" +#include "enums/player_cyclopedia.hpp" + +class Player; +class KV; + +struct Summary { + uint16_t m_preyWildcards = 0; + uint16_t m_instantRewards = 0; + uint16_t m_hirelings = 0; + + [[maybe_unused]] Summary(uint16_t mPreyWildcards, uint16_t mInstantRewards, uint16_t mHirelings) : + m_preyWildcards(mPreyWildcards), m_instantRewards(mInstantRewards), m_hirelings(mHirelings) { } +}; + +class PlayerCyclopedia { +public: + explicit PlayerCyclopedia(Player &player); + + Summary getSummary(); + + void loadSummaryData(); + void loadDeathHistory(uint16_t page, uint16_t entriesPerPage); + void loadRecentKills(uint16_t page, uint16_t entriesPerPage); + + void updateStoreSummary(uint8_t type, uint16_t amount = 1, const std::string &id = ""); + uint16_t getAmount(uint8_t type); + void updateAmount(uint8_t type, uint16_t amount = 1); + + [[nodiscard]] std::map getResult(uint8_t type) const; + void insertValue(uint8_t type, uint16_t amount = 1, const std::string &id = ""); + +private: + Player &m_player; +}; diff --git a/src/creatures/players/cyclopedia/player_title.cpp b/src/creatures/players/cyclopedia/player_title.cpp index a6b44f3d3c4..7c348cbf79d 100644 --- a/src/creatures/players/cyclopedia/player_title.cpp +++ b/src/creatures/players/cyclopedia/player_title.cpp @@ -84,7 +84,8 @@ const std::vector> &PlayerTitle::getUnlockedTitles() } uint8_t PlayerTitle::getCurrentTitle() const { - return static_cast(m_player.kv()->scoped("titles")->get("current-title")->getNumber()); + auto title = m_player.kv()->scoped("titles")->get("current-title"); + return title ? static_cast(title->getNumber()) : 0; } void PlayerTitle::setCurrentTitle(uint8_t id) { diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index a9320ffba5f..eddffa20ae2 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -17,6 +17,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" #include "creatures/players/storages/storages.hpp" #include "game/game.hpp" @@ -53,6 +54,7 @@ Player::Player(ProtocolGame_ptr p) : m_wheelPlayer = std::make_unique(*this); m_playerAchievement = std::make_unique(*this); m_playerBadge = std::make_unique(*this); + m_playerCyclopedia = std::make_unique(*this); m_playerTitle = std::make_unique(*this); } @@ -633,20 +635,6 @@ phmap::flat_hash_map> Player::getAllSlotItems() c return itemMap; } -phmap::flat_hash_map Player::getBlessingNames() const { - static phmap::flat_hash_map blessingNames = { - { TWIST_OF_FATE, "Twist of Fate" }, - { WISDOM_OF_SOLITUDE, "The Wisdom of Solitude" }, - { SPARK_OF_THE_PHOENIX, "The Spark of the Phoenix" }, - { FIRE_OF_THE_SUNS, "The Fire of the Suns" }, - { SPIRITUAL_SHIELDING, "The Spiritual Shielding" }, - { EMBRACE_OF_TIBIA, "The Embrace of Tibia" }, - { BLOOD_OF_THE_MOUNTAIN, "Blood of the Mountain" }, - { HEARTH_OF_THE_MOUNTAIN, "Heart of the Mountain" }, - }; - return blessingNames; -} - void Player::setTraining(bool value) { for (const auto &[key, player] : g_game().getPlayers()) { if (!this->isInGhostMode() || player->isAccessPlayer()) { @@ -6619,7 +6607,7 @@ std::string Player::getBlessingsName() const { } }); - auto BlessingNames = getBlessingNames(); + auto BlessingNames = g_game().getBlessingNames(); std::ostringstream os; for (uint8_t i = 1; i <= 8; i++) { if (hasBlessing(i)) { @@ -8026,6 +8014,15 @@ const std::unique_ptr &Player::vip() const { return m_playerVIP; } +// Cyclopedia +std::unique_ptr &Player::cyclopedia() { + return m_playerCyclopedia; +} + +const std::unique_ptr &Player::cyclopedia() const { + return m_playerCyclopedia; +} + void Player::sendLootMessage(const std::string &message) const { auto party = getParty(); if (!party) { diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 04b6a139a02..627b640ad93 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -36,6 +36,7 @@ #include "enums/object_category.hpp" #include "enums/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" #include "creatures/players/vip/player_vip.hpp" @@ -54,6 +55,7 @@ class Spell; class PlayerWheel; class PlayerAchievement; class PlayerBadge; +class PlayerCyclopedia; class PlayerTitle; class PlayerVIP; class Spectators; @@ -475,13 +477,18 @@ class Player final : public Creature, public Cylinder, public Bankable { bool hasBlessing(uint8_t index) const { return blessings[index - 1] != 0; } - uint8_t getBlessingCount(uint8_t index) const { - if (index > 0 && index <= blessings.size()) { - return blessings[index - 1]; - } else { - g_logger().error("[{}] - index outside range 0-10.", __FUNCTION__); - return 0; + + uint8_t getBlessingCount(uint8_t index, bool storeCount = false) const { + if (!storeCount) { + if (index > 0 && index <= blessings.size()) { + return blessings[index - 1]; + } else { + g_logger().error("[{}] - index outside range 0-10.", __FUNCTION__); + return 0; + } } + auto amount = kv()->scoped("summary")->scoped("blessings")->scoped(fmt::format("{}", index))->get("amount"); + return amount ? static_cast(amount->getNumber()) : 0; } std::string getBlessingsName() const; @@ -1642,11 +1649,7 @@ class Player final : public Creature, public Cylinder, public Bankable { client->sendCyclopediaCharacterRecentDeaths(page, pages, entries); } } - void sendCyclopediaCharacterRecentPvPKills( - uint16_t page, uint16_t pages, - const std::vector< - RecentPvPKillEntry> &entries - ) { + void sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t pages, const std::vector &entries) { if (client) { client->sendCyclopediaCharacterRecentPvPKills(page, pages, entries); } @@ -2599,9 +2602,6 @@ class Player final : public Creature, public Cylinder, public Bankable { // This get all players slot items phmap::flat_hash_map> getAllSlotItems() const; - // This get all blessings - phmap::flat_hash_map getBlessingNames() const; - // Gets the equipped items with augment by type std::vector> getEquippedAugmentItemsByType(Augment_t augmentType) const; @@ -2631,6 +2631,10 @@ class Player final : public Creature, public Cylinder, public Bankable { std::unique_ptr &title(); const std::unique_ptr &title() const; + // Player summary interface + std::unique_ptr &cyclopedia(); + const std::unique_ptr &cyclopedia() const; + // Player vip interface std::unique_ptr &vip(); const std::unique_ptr &vip() const; @@ -2952,6 +2956,8 @@ class Player final : public Creature, public Cylinder, public Bankable { int32_t magicShieldCapacityFlat = 0; int32_t magicShieldCapacityPercent = 0; + int32_t marriageSpouse = -1; + void updateItemsLight(bool internal = false); uint16_t getStepSpeed() const override { return std::max(PLAYER_MIN_SPEED, std::min(PLAYER_MAX_SPEED, getSpeed())); @@ -3028,12 +3034,14 @@ class Player final : public Creature, public Cylinder, public Bankable { friend class IOLoginDataSave; friend class PlayerAchievement; friend class PlayerBadge; + friend class PlayerCyclopedia; friend class PlayerTitle; friend class PlayerVIP; std::unique_ptr m_wheelPlayer; std::unique_ptr m_playerAchievement; std::unique_ptr m_playerBadge; + std::unique_ptr m_playerCyclopedia; std::unique_ptr m_playerTitle; std::unique_ptr m_playerVIP; @@ -3059,4 +3067,11 @@ class Player final : public Creature, public Cylinder, public Bankable { bool hasOtherRewardContainerOpen(const std::shared_ptr container) const; void checkAndShowBlessingMessage(); + + void setMarriageSpouse(const int32_t spouseId) { + marriageSpouse = spouseId; + } + int32_t getMarriageSpouse() const { + return marriageSpouse; + } }; diff --git a/src/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp index c6e1b7032c0..295e573984f 100644 --- a/src/enums/player_cyclopedia.hpp +++ b/src/enums/player_cyclopedia.hpp @@ -37,6 +37,16 @@ enum CyclopediaTitle_t : uint8_t { OTHERS, }; +enum Summary_t : uint8_t { + HOUSE_ITEMS = 9, + BOOSTS = 10, + PREY_CARDS = 12, + BLESSINGS = 14, + ALL_BLESSINGS = 17, + INSTANT_REWARDS = 18, + HIRELINGS = 20, +}; + enum class CyclopediaMapData_t : uint8_t { MinimapMarker = 0, DiscoveryData = 1, diff --git a/src/game/game.cpp b/src/game/game.cpp index bae7975a295..7ef6b7a9038 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -38,6 +38,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" #include "creatures/npcs/npc.hpp" #include "server/network/webhook/webhook.hpp" @@ -362,6 +363,45 @@ Game::Game() { HighscoreCategory("Fishing", static_cast(HighscoreCategories_t::FISHING)), HighscoreCategory("Magic Level", static_cast(HighscoreCategories_t::MAGIC_LEVEL)) }; + + m_blessingNames = { + { static_cast(TWIST_OF_FATE), "Twist of Fate" }, + { static_cast(WISDOM_OF_SOLITUDE), "The Wisdom of Solitude" }, + { static_cast(SPARK_OF_THE_PHOENIX), "The Spark of the Phoenix" }, + { static_cast(FIRE_OF_THE_SUNS), "The Fire of the Suns" }, + { static_cast(SPIRITUAL_SHIELDING), "The Spiritual Shielding" }, + { static_cast(EMBRACE_OF_TIBIA), "The Embrace of Tibia" }, + { static_cast(BLOOD_OF_THE_MOUNTAIN), "Blood of the Mountain" }, + { static_cast(HEARTH_OF_THE_MOUNTAIN), "Heart of the Mountain" }, + }; + + m_summaryCategories = { + { static_cast(Summary_t::HOUSE_ITEMS), "house-items" }, + { static_cast(Summary_t::BOOSTS), "xp-boosts" }, + { static_cast(Summary_t::PREY_CARDS), "prey-cards" }, + { static_cast(Summary_t::BLESSINGS), "blessings" }, + { static_cast(Summary_t::INSTANT_REWARDS), "instant-rewards" }, + { static_cast(Summary_t::HIRELINGS), "hirelings" }, + }; + + m_hirelingSkills = { + { 1001, "banker" }, + { 1002, "cooker" }, + { 1003, "steward" }, + { 1004, "trader" } + }; + + m_hirelingOutfits = { + { 2001, "banker" }, + { 2002, "cooker" }, + { 2003, "steward" }, + { 2004, "trader" }, + { 2005, "servant" }, + { 2006, "hydra" }, + { 2007, "ferumbras" }, + { 2008, "bonelord" }, + { 2009, "dragon" }, + }; } Game::~Game() = default; @@ -8300,121 +8340,12 @@ void Game::playerCyclopediaCharacterInfo(std::shared_ptr player, uint32_ case CYCLOPEDIA_CHARACTERINFO_COMBATSTATS: player->sendCyclopediaCharacterCombatStats(); break; - case CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS: { - std::ostringstream query; - uint32_t offset = static_cast(page - 1) * entriesPerPage; - query << "SELECT `time`, `level`, `killed_by`, `mostdamage_by`, (select count(*) FROM `player_deaths` WHERE `player_id` = " << playerGUID << ") as `entries` FROM `player_deaths` WHERE `player_id` = " << playerGUID << " ORDER BY `time` DESC LIMIT " << offset << ", " << entriesPerPage; - - uint32_t playerID = player->getID(); - std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) { - std::shared_ptr player = g_game().getPlayerByID(playerID); - if (!player) { - return; - } - - player->resetAsyncOngoingTask(PlayerAsyncTask_RecentDeaths); - if (!result) { - player->sendCyclopediaCharacterRecentDeaths(0, 0, {}); - return; - } - - uint32_t pages = result->getNumber("entries"); - pages += entriesPerPage - 1; - pages /= entriesPerPage; - - std::vector entries; - entries.reserve(result->countResults()); - do { - std::string cause1 = result->getString("killed_by"); - std::string cause2 = result->getString("mostdamage_by"); - - std::ostringstream cause; - cause << "Died at Level " << result->getNumber("level") << " by"; - if (!cause1.empty()) { - const char &character = cause1.front(); - if (character == 'a' || character == 'e' || character == 'i' || character == 'o' || character == 'u') { - cause << " an "; - } else { - cause << " a "; - } - cause << cause1; - } - - if (!cause2.empty()) { - if (!cause1.empty()) { - cause << " and "; - } - - const char &character = cause2.front(); - if (character == 'a' || character == 'e' || character == 'i' || character == 'o' || character == 'u') { - cause << " an "; - } else { - cause << " a "; - } - cause << cause2; - } - cause << '.'; - entries.emplace_back(std::move(cause.str()), result->getNumber("time")); - } while (result->next()); - player->sendCyclopediaCharacterRecentDeaths(page, static_cast(pages), entries); - }; - g_databaseTasks().store(query.str(), callback); - player->addAsyncOngoingTask(PlayerAsyncTask_RecentDeaths); + case CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS: + player->cyclopedia()->loadDeathHistory(page, entriesPerPage); break; - } - case CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS: { - // TODO: add guildwar, assists and arena kills - Database &db = Database::getInstance(); - const std::string &escapedName = db.escapeString(player->getName()); - std::ostringstream query; - uint32_t offset = static_cast(page - 1) * entriesPerPage; - query << "SELECT `d`.`time`, `d`.`killed_by`, `d`.`mostdamage_by`, `d`.`unjustified`, `d`.`mostdamage_unjustified`, `p`.`name`, (select count(*) FROM `player_deaths` WHERE ((`killed_by` = " << escapedName << " AND `is_player` = 1) OR (`mostdamage_by` = " << escapedName << " AND `mostdamage_is_player` = 1))) as `entries` FROM `player_deaths` AS `d` INNER JOIN `players` AS `p` ON `d`.`player_id` = `p`.`id` WHERE ((`d`.`killed_by` = " << escapedName << " AND `d`.`is_player` = 1) OR (`d`.`mostdamage_by` = " << escapedName << " AND `d`.`mostdamage_is_player` = 1)) ORDER BY `time` DESC LIMIT " << offset << ", " << entriesPerPage; - - uint32_t playerID = player->getID(); - std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) { - std::shared_ptr player = g_game().getPlayerByID(playerID); - if (!player) { - return; - } - - player->resetAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills); - if (!result) { - player->sendCyclopediaCharacterRecentPvPKills(0, 0, {}); - return; - } - - uint32_t pages = result->getNumber("entries"); - pages += entriesPerPage - 1; - pages /= entriesPerPage; - - std::vector entries; - entries.reserve(result->countResults()); - do { - std::string cause1 = result->getString("killed_by"); - std::string cause2 = result->getString("mostdamage_by"); - std::string name = result->getString("name"); - - uint8_t status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_JUSTIFIED; - if (player->getName() == cause1) { - if (result->getNumber("unjustified") == 1) { - status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED; - } - } else if (player->getName() == cause2) { - if (result->getNumber("mostdamage_unjustified") == 1) { - status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED; - } - } - - std::ostringstream description; - description << "Killed " << name << '.'; - entries.emplace_back(std::move(description.str()), result->getNumber("time"), status); - } while (result->next()); - player->sendCyclopediaCharacterRecentPvPKills(page, static_cast(pages), entries); - }; - g_databaseTasks().store(query.str(), callback); - player->addAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills); + case CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS: + player->cyclopedia()->loadRecentKills(page, entriesPerPage); break; - } case CYCLOPEDIA_CHARACTERINFO_ACHIEVEMENTS: player->achiev()->sendUnlockedSecretAchievements(); break; @@ -10735,3 +10666,19 @@ Title Game::getTitleByName(const std::string &name) { } return {}; } + +const std::string &Game::getSummaryKeyByType(uint8_t type) { + return m_summaryCategories[type]; +} + +const std::map &Game::getBlessingNames() { + return m_blessingNames; +} + +const std::unordered_map &Game::getHirelingSkills() { + return m_hirelingSkills; +} + +const std::unordered_map &Game::getHirelingOutfits() { + return m_hirelingOutfits; +} diff --git a/src/game/game.hpp b/src/game/game.hpp index 2de00410e5e..0537ee030a7 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -732,6 +732,12 @@ class Game { Title getTitleById(uint8_t id); Title getTitleByName(const std::string &name); + const std::string &getSummaryKeyByType(uint8_t type); + + const std::map &getBlessingNames(); + const std::unordered_map &getHirelingSkills(); + const std::unordered_map &getHirelingOutfits(); + private: std::map m_achievements; std::map m_achievementsNameToId; @@ -742,6 +748,12 @@ class Game { std::vector m_highscoreCategories; std::unordered_map m_highscoreCategoriesNames; + std::map m_blessingNames; + + std::unordered_map m_summaryCategories; + std::unordered_map m_hirelingSkills; + std::unordered_map m_hirelingOutfits; + std::map forgeMonsterEventIds; std::unordered_set fiendishMonsters; std::unordered_set influencedMonsters; diff --git a/src/game/game_definitions.hpp b/src/game/game_definitions.hpp index a6ce6e7eaa8..8b165bc725d 100644 --- a/src/game/game_definitions.hpp +++ b/src/game/game_definitions.hpp @@ -102,6 +102,17 @@ enum class HighscoreCategories_t : uint8_t { BOSS_POINTS = 14, }; +enum Blessings_t : uint8_t { + TWIST_OF_FATE = 1, + WISDOM_OF_SOLITUDE = 2, + SPARK_OF_THE_PHOENIX = 3, + FIRE_OF_THE_SUNS = 4, + SPIRITUAL_SHIELDING = 5, + EMBRACE_OF_TIBIA = 6, + BLOOD_OF_THE_MOUNTAIN = 7, + HEARTH_OF_THE_MOUNTAIN = 8, +}; + enum HighscoreType_t : uint8_t { HIGHSCORE_GETENTRIES = 0, HIGHSCORE_OURRANK = 1 diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 8f7cf461fdf..8406cf11010 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -182,6 +182,8 @@ bool IOLoginDataLoad::loadPlayerFirst(std::shared_ptr player, DBResult_p player->setManaShield(result->getNumber("manashield")); player->setMaxManaShield(result->getNumber("max_manashield")); + + player->setMarriageSpouse(result->getNumber("marriage_spouse")); return true; } @@ -215,9 +217,7 @@ void IOLoginDataLoad::loadPlayerBlessings(std::shared_ptr player, DBResu } for (int i = 1; i <= 8; i++) { - std::ostringstream ss; - ss << "blessings" << i; - player->addBlessing(static_cast(i), static_cast(result->getNumber(ss.str()))); + player->addBlessing(static_cast(i), static_cast(result->getNumber(fmt::format("blessings{}", i)))); } } @@ -913,6 +913,7 @@ void IOLoginDataLoad::loadPlayerInitializeSystem(std::shared_ptr player) player->achiev()->loadUnlockedAchievements(); player->badge()->checkAndUpdateNewBadges(); player->title()->checkAndUpdateNewTitles(); + player->cyclopedia()->loadSummaryData(); player->initializePrey(); player->initializeTaskHunting(); diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index ec5c5102794..83ecd091850 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -28,6 +28,7 @@ #include "lua/callbacks/events_callbacks.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" #include "map/spectators.hpp" diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 9cc5434abd5..52fd4517f31 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -16,6 +16,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" #include "game/game.hpp" #include "io/iologindata.hpp" @@ -2732,7 +2733,7 @@ int PlayerFunctions::luaPlayerRemoveBlessing(lua_State* L) { } int PlayerFunctions::luaPlayerGetBlessingCount(lua_State* L) { - // player:getBlessingCount(index) + // player:getBlessingCount(index[, storeCount = false]) std::shared_ptr player = getUserdataShared(L, 1); uint8_t index = getNumber(L, 2); if (index == 0) { @@ -2740,7 +2741,7 @@ int PlayerFunctions::luaPlayerGetBlessingCount(lua_State* L) { } if (player) { - lua_pushnumber(L, player->getBlessingCount(index)); + lua_pushnumber(L, player->getBlessingCount(index, getBoolean(L, 3, false))); } else { lua_pushnil(L); } @@ -4355,3 +4356,25 @@ int PlayerFunctions::luaPlayerSetCurrentTitle(lua_State* L) { pushBoolean(L, true); return 1; } + +int PlayerFunctions::luaPlayerCreateTransactionSummary(lua_State* L) { + // player:createTransactionSummary(type, amount[, id = 0]) + const auto &player = getUserdataShared(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + auto type = getNumber(L, 2, 0); + if (type == 0) { + reportErrorFunc(getErrorDesc(LUA_ERROR_VARIANT_NOT_FOUND)); + return 1; + } + + auto amount = getNumber(L, 3, 1); + auto id = getString(L, 4, ""); + + player->cyclopedia()->updateStoreSummary(type, amount, id); + pushBoolean(L, true); + return 1; +} diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index 4d89a86d27f..281e51ae88f 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -372,6 +372,9 @@ class PlayerFunctions final : LuaScriptInterface { registerMethod(L, "Player", "getTitles", PlayerFunctions::luaPlayerGetTitles); registerMethod(L, "Player", "setCurrentTitle", PlayerFunctions::luaPlayerSetCurrentTitle); + // Store Summary + registerMethod(L, "Player", "createTransactionSummary", PlayerFunctions::luaPlayerCreateTransactionSummary); + GroupFunctions::init(L); GuildFunctions::init(L); MountFunctions::init(L); @@ -732,5 +735,7 @@ class PlayerFunctions final : LuaScriptInterface { static int luaPlayerGetTitles(lua_State* L); static int luaPlayerSetCurrentTitle(lua_State* L); + static int luaPlayerCreateTransactionSummary(lua_State* L); + friend class CreatureFunctions; }; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 1476f255135..18c23fbb1aa 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -28,6 +28,7 @@ #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" #include "creatures/players/grouping/familiars.hpp" #include "server/network/protocol/protocolgame.hpp" @@ -3456,7 +3457,7 @@ void ProtocolGame::sendCyclopediaCharacterGeneralStats() { msg.add(player->getOfflineTrainingTime() / 60 / 1000); msg.add(player->getSpeed()); msg.add(player->getBaseSpeed()); - msg.add(player->getBonusCapacity()); + msg.add(player->getCapacity()); msg.add(player->getBaseCapacity()); msg.add(player->hasFlag(PlayerFlags_t::HasInfiniteCapacity) ? 1000000 : player->getFreeCapacity()); msg.addByte(8); @@ -3660,21 +3661,15 @@ void ProtocolGame::sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t p NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS); - msg.addByte(0x00); - - uint16_t totalPages = static_cast(std::ceil(static_cast(entries.size()) / pages)); - uint16_t currentPage = std::min(page, totalPages); - uint16_t firstObject = (currentPage - 1) * pages; - uint16_t finalObject = firstObject + pages; - - msg.add(currentPage); - msg.add(totalPages); + msg.addByte(0x00); // 0x00 Here means 'no error' + msg.add(page); msg.add(pages); - for (uint16_t i = firstObject; i < finalObject; i++) { - RecentDeathEntry entry = entries[i]; + msg.add(entries.size()); + for (const RecentDeathEntry &entry : entries) { msg.add(entry.timestamp); msg.addString(entry.cause, "ProtocolGame::sendCyclopediaCharacterRecentDeaths - entry.cause"); } + writeToOutputBuffer(msg); } @@ -3686,22 +3681,16 @@ void ProtocolGame::sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS); - msg.addByte(0x00); - - uint16_t totalPages = static_cast(std::ceil(static_cast(entries.size()) / pages)); - uint16_t currentPage = std::min(page, totalPages); - uint16_t firstObject = (currentPage - 1) * pages; - uint16_t finalObject = firstObject + pages; - - msg.add(currentPage); - msg.add(totalPages); + msg.addByte(0x00); // 0x00 Here means 'no error' + msg.add(page); msg.add(pages); - for (uint16_t i = firstObject; i < finalObject; i++) { - RecentPvPKillEntry entry = entries[i]; + msg.add(entries.size()); + for (const RecentPvPKillEntry &entry : entries) { msg.add(entry.timestamp); msg.addString(entry.description, "ProtocolGame::sendCyclopediaCharacterRecentPvPKills - entry.description"); msg.addByte(entry.status); } + writeToOutputBuffer(msg); } @@ -3959,17 +3948,72 @@ void ProtocolGame::sendCyclopediaCharacterStoreSummary() { msg.addByte(CYCLOPEDIA_CHARACTERINFO_STORESUMMARY); msg.addByte(0x00); // 0x00 Here means 'no error' msg.add(player->getXpBoostTime()); // Remaining Store Xp Boost Time - msg.add(0); // RemainingDailyRewardXpBoostTime + auto remaining = player->kv()->get("daily-reward-xp-boost"); + msg.add(remaining ? static_cast(remaining->getNumber()) : 0); // Remaining Daily Reward Xp Boost Time + + auto cyclopediaSummary = player->cyclopedia()->getSummary(); + + // getBlessingsObtained + auto blessingNames = g_game().getBlessingNames(); + msg.addByte(static_cast(blessingNames.size())); + for (const auto &bless : blessingNames) { + msg.addString(bless.second, "ProtocolGame::sendCyclopediaCharacterStoreSummary - blessing.name"); + uint8_t blessingIndex = bless.first - 1; + msg.addByte((blessingIndex < player->blessings.size()) ? static_cast(player->blessings[blessingIndex]) : 0); + } + + uint8_t preySlotsUnlocked = 0; + // Prey third slot unlocked + if (const auto &slotP = player->getPreySlotById(PreySlot_Three); + slotP && slotP->state != PreyDataState_Locked) { + preySlotsUnlocked++; + } + // Task hunting third slot unlocked + if (const auto &slotH = player->getTaskHuntingSlotById(PreySlot_Three); + slotH && slotH->state != PreyTaskDataState_Locked) { + preySlotsUnlocked++; + } + msg.addByte(preySlotsUnlocked); // getPreySlotById + getTaskHuntingSlotById + + msg.addByte(cyclopediaSummary.m_preyWildcards); // getPreyCardsObtained + msg.addByte(cyclopediaSummary.m_instantRewards); // getRewardCollectionObtained + msg.addByte(player->hasCharmExpansion() ? 0x01 : 0x00); + msg.addByte(cyclopediaSummary.m_hirelings); // getHirelingsObtained + + std::vector m_hSkills; + for (const auto &it : g_game().getHirelingSkills()) { + if (player->kv()->scoped("hireling-skills")->get(it.second)) { + m_hSkills.emplace_back(it.first); + g_logger().debug("skill id: {}, name: {}", it.first, it.second); + } + } + msg.addByte(m_hSkills.size()); + for (const auto &id : m_hSkills) { + msg.addByte(id - 1000); + } + + /*std::vector m_hOutfits; + for (const auto &it : g_game().getHirelingOutfits()) { + if (player->kv()->scoped("hireling-outfits")->get(it.second)) { + m_hOutfits.emplace_back(it.first); + g_logger().debug("outfit id: {}, name: {}", it.first, it.second); + } + } + msg.addByte(m_hOutfits.size()); + for (const auto &id : m_hOutfits) { + msg.addByte(0x01); // TODO need to get the correct id from hireling outfit + }*/ + msg.addByte(0x00); // hireling outfit size + + auto houseItems = player->cyclopedia()->getResult(static_cast(Summary_t::HOUSE_ITEMS)); + msg.add(houseItems.size()); + for (const auto &hItem_it : houseItems) { + const ItemType &it = Item::items[hItem_it.first]; + msg.add(it.id); // Item ID + msg.addString(it.name, "ProtocolGame::sendCyclopediaCharacterStoreSummary - houseItem.name"); + msg.addByte(hItem_it.second); + } - msg.addByte(0x00); // getBlessingsObtained - msg.addByte(0x00); // getTaskHuntingSlotById - msg.addByte(0x00); // getPreyCardsObtained - msg.addByte(0x00); // getRewardCollectionObtained - msg.addByte(0x00); // player->hasCharmExpansion() ? 0x01 : 0x00 - msg.addByte(0x00); // getHirelingsObtained - msg.addByte(0x00); // getHirelinsJobsObtained - msg.addByte(0x00); // getHirelinsOutfitsObtained - msg.add(0); // getHouseItemsObtained writeToOutputBuffer(msg); } @@ -4028,23 +4072,23 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { auto playerDescriptionPosition = msg.getBufferPosition(); msg.skipBytes(1); + // Player title + if (player->title()->getCurrentTitle() != 0) { + playerDescriptionSize++; + msg.addString("Character Title", "ProtocolGame::sendCyclopediaCharacterInspection - Title"); + msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->title()->getCurrentTitleName()"); + } + // Level description playerDescriptionSize++; msg.addString("Level", "ProtocolGame::sendCyclopediaCharacterInspection - Level"); + msg.addString(std::to_string(player->getLevel()), "ProtocolGame::sendCyclopediaCharacterInspection - std::to_string(player->getLevel())"); // Vocation description playerDescriptionSize++; - msg.addString(std::to_string(player->getLevel()), "ProtocolGame::sendCyclopediaCharacterInspection - std::to_string(player->getLevel())"); msg.addString("Vocation", "ProtocolGame::sendCyclopediaCharacterInspection - Vocation"); msg.addString(player->getVocation()->getVocName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getVocation()->getVocName()"); - // Player title - if (player->title()->getCurrentTitle() != 0) { - playerDescriptionSize++; - msg.addString("Title", "ProtocolGame::sendCyclopediaCharacterInspection - Title"); - msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->title()->getCurrentTitleName()"); - } - // Loyalty title if (!player->getLoyaltyTitle().empty()) { playerDescriptionSize++; @@ -4052,6 +4096,47 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { msg.addString(player->getLoyaltyTitle(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getLoyaltyTitle()"); } + // Marriage description + if (const auto spouseId = player->getMarriageSpouse(); spouseId > 0) { + if (const auto &spouse = g_game().getPlayerByID(spouseId, true); spouse) { + playerDescriptionSize++; + msg.addString("Married to", "ProtocolGame::sendCyclopediaCharacterInspection - Married to"); + msg.addString(spouse->getName(), "ProtocolGame::sendCyclopediaCharacterInspection - spouse->getName()"); + } + } + + // Prey description + for (uint8_t slotId = PreySlot_First; slotId <= PreySlot_Last; slotId++) { + if (const auto &slot = player->getPreySlotById(static_cast(slotId)); + slot && slot->isOccupied()) { + playerDescriptionSize++; + std::string activePrey = fmt::format("Active Prey {}", slotId + 1); + msg.addString(activePrey, "ProtocolGame::sendCyclopediaCharacterInspection - active prey"); + + std::string desc; + if (auto mtype = g_monsters().getMonsterTypeByRaceId(slot->selectedRaceId)) { + desc.append(mtype->name); + } else { + desc.append("Unknown creature"); + } + + if (slot->bonus == PreyBonus_Damage) { + desc.append(" (Improved Damage +"); + } else if (slot->bonus == PreyBonus_Defense) { + desc.append(" (Improved Defense +"); + } else if (slot->bonus == PreyBonus_Experience) { + desc.append(" (Improved Experience +"); + } else if (slot->bonus == PreyBonus_Loot) { + desc.append(" (Improved Loot +"); + } + desc.append(fmt::format("{}%, remaining", slot->bonusPercentage)); + uint8_t hours = slot->bonusTimeLeft / 3600; + uint8_t minutes = (slot->bonusTimeLeft - (hours * 3600)) / 60; + desc.append(fmt::format("{}:{}{}h", hours, (minutes < 10 ? "0" : ""), minutes)); + msg.addString(desc, "ProtocolGame::sendCyclopediaCharacterInspection - prey description"); + } + } + // Outfit description playerDescriptionSize++; msg.addString("Outfit", "ProtocolGame::sendCyclopediaCharacterInspection - Outfit"); diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 24341f15e1e..55e9cbfbaea 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -14,6 +14,7 @@ #include "creatures/creature.hpp" #include "enums/forge_conversion.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" class NetworkMessage; diff --git a/src/utils/tools.cpp b/src/utils/tools.cpp index 584d4a6b5b6..6504fda859d 100644 --- a/src/utils/tools.cpp +++ b/src/utils/tools.cpp @@ -1834,6 +1834,31 @@ std::string getVerbForPronoun(PlayerPronoun_t pronoun, bool pastTense) { return pastTense ? "was" : "is"; } +std::string formatWithArticle(const std::string &value, bool withSpace) { + if (value.empty()) { + return ""; + } + + auto removeArticle = [](const std::string &str) -> std::string { + const std::string articles[] = { "a ", "an " }; + for (const auto &article : articles) { + if (str.size() > article.size() && std::equal(article.begin(), article.end(), str.begin(), [](char a, char b) { return std::tolower(a) == std::tolower(b); })) { + return str.substr(article.size()); + } + } + return str; + }; + + std::string modifiedValue = removeArticle(value); + if (modifiedValue.empty()) { + return ""; + } + + const char &character = std::tolower(modifiedValue.front()); + auto article = character == 'a' || character == 'e' || character == 'i' || character == 'o' || character == 'u' ? "an" : "a"; + return fmt::format("{}{} {}.", withSpace ? " " : "", article, modifiedValue); +} + std::vector split(const std::string &str, char delimiter /* = ','*/) { std::vector tokens; std::string token; diff --git a/src/utils/tools.hpp b/src/utils/tools.hpp index 25683bf22bb..1f8ed382244 100644 --- a/src/utils/tools.hpp +++ b/src/utils/tools.hpp @@ -201,6 +201,8 @@ std::string getPlayerPossessivePronoun(PlayerPronoun_t pronoun, PlayerSex_t sex, std::string getPlayerReflexivePronoun(PlayerPronoun_t pronoun, PlayerSex_t sex, const std::string &name); std::string getVerbForPronoun(PlayerPronoun_t pronoun, bool pastTense = false); +std::string formatWithArticle(const std::string &value, bool withSpace = true); + std::string toKey(const std::string &str); static inline double quadraticPoly(double a, double b, double c, double x) { diff --git a/src/utils/utils_definitions.hpp b/src/utils/utils_definitions.hpp index af2c40ba533..7466c83a8f2 100644 --- a/src/utils/utils_definitions.hpp +++ b/src/utils/utils_definitions.hpp @@ -709,17 +709,6 @@ enum class PlayerFlags_t : uint8_t { FlagLast }; -enum Blessings_t : uint8_t { - TWIST_OF_FATE = 1, - WISDOM_OF_SOLITUDE = 2, - SPARK_OF_THE_PHOENIX = 3, - FIRE_OF_THE_SUNS = 4, - SPIRITUAL_SHIELDING = 5, - EMBRACE_OF_TIBIA = 6, - BLOOD_OF_THE_MOUNTAIN = 7, - HEARTH_OF_THE_MOUNTAIN = 8, -}; - enum BedItemPart_t : uint8_t { BED_NONE_PART, BED_PILLOW_PART, diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index 4cb91d1cb94..77ece957935 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -46,6 +46,7 @@ + @@ -261,6 +262,7 @@ +