From 9efbe4c10f19555208bbb755cb3dc09cc1ac8e99 Mon Sep 17 00:00:00 2001 From: Elson Costa Date: Sat, 4 May 2024 15:16:32 -0300 Subject: [PATCH] feature: badge system (#2533) --- data/scripts/talkactions/god/manage_badge.lua | 33 ++++ src/canary_server.cpp | 1 + src/creatures/CMakeLists.txt | 1 + .../players/cyclopedia/player_badge.cpp | 156 ++++++++++++++++++ .../players/cyclopedia/player_badge.hpp | 65 ++++++++ src/creatures/players/player.cpp | 59 +++++-- src/creatures/players/player.hpp | 22 +++ src/creatures/players/wheel/player_wheel.cpp | 33 +--- src/creatures/players/wheel/player_wheel.hpp | 1 - src/game/game.cpp | 93 +++++++++-- src/game/game.hpp | 11 ++ src/game/game_definitions.hpp | 26 +++ src/io/functions/iologindata_load_player.cpp | 1 + .../functions/core/game/game_functions.cpp | 1 + .../creatures/player/player_functions.cpp | 14 ++ .../creatures/player/player_functions.hpp | 5 + src/server/network/protocol/protocolgame.cpp | 146 +++++++++++----- src/server/network/protocol/protocolgame.hpp | 4 + vcproj/canary.vcxproj | 4 +- 19 files changed, 573 insertions(+), 103 deletions(-) create mode 100644 data/scripts/talkactions/god/manage_badge.lua create mode 100644 src/creatures/players/cyclopedia/player_badge.cpp create mode 100644 src/creatures/players/cyclopedia/player_badge.hpp diff --git a/data/scripts/talkactions/god/manage_badge.lua b/data/scripts/talkactions/god/manage_badge.lua new file mode 100644 index 00000000000..f2899a0d592 --- /dev/null +++ b/data/scripts/talkactions/god/manage_badge.lua @@ -0,0 +1,33 @@ +local addBadge = TalkAction("/addbadge") + +function addBadge.onSay(player, words, param) + -- create log + logCommand(player, words, param) + + if param == "" then + player:sendCancelMessage("Command param required.") + return true + end + + local split = param:split(",") + if not split[2] then + player:sendCancelMessage("Insufficient parameters. Usage: /addbadge playerName, badgeID") + return true + end + + local target = Player(split[1]) + if not target then + player:sendCancelMessage("A player with that name is not online.") + return true + end + + -- Trim left + split[2] = split[2]:gsub("^%s*(.-)$", "%1") + local id = tonumber(split[2]) + target:addBadge(id) + return true +end + +addBadge:separator(" ") +addBadge:groupType("god") +addBadge:register() diff --git a/src/canary_server.cpp b/src/canary_server.cpp index c40534deb89..bfabbf51ab8 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -376,6 +376,7 @@ void CanaryServer::loadModules() { g_game().loadBoostedCreature(); g_ioBosstiary().loadBoostedBoss(); g_ioprey().initializeTaskHuntOptions(); + g_game().logCyclopediaStats(); } void CanaryServer::modulesLoadHelper(bool loaded, std::string moduleName) { diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt index f3dedf3a8f3..630812c7996 100644 --- a/src/creatures/CMakeLists.txt +++ b/src/creatures/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE players/storages/storages.cpp players/player.cpp players/achievement/player_achievement.cpp + players/cyclopedia/player_badge.cpp players/wheel/player_wheel.cpp players/wheel/wheel_gems.cpp players/vocations/vocation.cpp diff --git a/src/creatures/players/cyclopedia/player_badge.cpp b/src/creatures/players/cyclopedia/player_badge.cpp new file mode 100644 index 00000000000..9b892a6164c --- /dev/null +++ b/src/creatures/players/cyclopedia/player_badge.cpp @@ -0,0 +1,156 @@ +/** + * 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 "player_badge.hpp" + +#include "creatures/players/player.hpp" +#include "game/game.hpp" +#include "kv/kv.hpp" + +PlayerBadge::PlayerBadge(Player &player) : + m_player(player) { } + +bool PlayerBadge::hasBadge(uint8_t id) const { + if (id == 0) { + return false; + } + + if (auto it = std::find_if(m_badgesUnlocked.begin(), m_badgesUnlocked.end(), [id](auto badge_it) { + return badge_it.first.m_id == id; + }); + it != m_badgesUnlocked.end()) { + return true; + } + + return false; +} + +bool PlayerBadge::add(uint8_t id, uint32_t timestamp /* = 0*/) { + if (hasBadge(id)) { + return false; + } + + const Badge &badge = g_game().getBadgeById(id); + if (badge.m_id == 0) { + return false; + } + + int toSaveTimeStamp = timestamp != 0 ? timestamp : (OTSYS_TIME() / 1000); + getUnlockedKV()->set(badge.m_name, toSaveTimeStamp); + m_badgesUnlocked.emplace_back(badge, toSaveTimeStamp); + m_badgesUnlocked.shrink_to_fit(); + return true; +} + +void PlayerBadge::checkAndUpdateNewBadges() { + for (const auto &badge : g_game().getBadges()) { + switch (badge.m_type) { + case CyclopediaBadge_t::ACCOUNT_AGE: + if (accountAge(badge.m_amount)) { + add(badge.m_id); + } + break; + case CyclopediaBadge_t::LOYALTY: + if (loyalty(badge.m_amount)) { + add(badge.m_id); + } + break; + case CyclopediaBadge_t::ACCOUNT_ALL_LEVEL: + if (accountAllLevel(badge.m_amount)) { + add(badge.m_id); + } + break; + case CyclopediaBadge_t::ACCOUNT_ALL_VOCATIONS: + if (accountAllVocations(badge.m_amount)) { + add(badge.m_id); + } + break; + case CyclopediaBadge_t::TOURNAMENT_PARTICIPATION: + case CyclopediaBadge_t::TOURNAMENT_POINTS: + break; + } + } + + loadUnlockedBadges(); +} + +void PlayerBadge::loadUnlockedBadges() { + const auto &unlockedBadges = getUnlockedKV()->keys(); + g_logger().debug("[{}] - Loading unlocked badges: {}", __FUNCTION__, unlockedBadges.size()); + for (const auto &badgeName : unlockedBadges) { + const Badge &badge = g_game().getBadgeByName(badgeName); + if (badge.m_id == 0) { + g_logger().error("[{}] - Badge {} not found.", __FUNCTION__, badgeName); + continue; + } + + g_logger().debug("[{}] - Badge {} found for player {}.", __FUNCTION__, badge.m_name, m_player.getName()); + + m_badgesUnlocked.emplace_back(badge, getUnlockedKV()->get(badgeName)->getNumber()); + } +} + +const std::shared_ptr &PlayerBadge::getUnlockedKV() { + if (m_badgeUnlockedKV == nullptr) { + m_badgeUnlockedKV = m_player.kv()->scoped("badges")->scoped("unlocked"); + } + + return m_badgeUnlockedKV; +} + +// Badge Calculate Functions +bool PlayerBadge::accountAge(uint8_t amount) { + return std::floor(m_player.getLoyaltyPoints() / 365) >= amount; +} + +bool PlayerBadge::loyalty(uint8_t amount) { + return m_player.getLoyaltyPoints() >= amount; +} + +bool PlayerBadge::accountAllLevel(uint8_t amount) { + const auto &players = g_game().getPlayersByAccount(m_player.getAccount(), true); + uint16_t total = std::accumulate(players.begin(), players.end(), 0, [](uint16_t sum, const std::shared_ptr &player) { + return sum + player->getLevel(); + }); + return total >= amount; +} + +bool PlayerBadge::accountAllVocations(uint8_t amount) { + auto knight = false; + auto paladin = false; + auto druid = false; + auto sorcerer = false; + for (const auto &player : g_game().getPlayersByAccount(m_player.getAccount(), true)) { + if (player->getLevel() >= amount) { + auto vocationEnum = player->getPlayerVocationEnum(); + if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { + knight = true; + } else if (vocationEnum == Vocation_t::VOCATION_SORCERER_CIP) { + sorcerer = true; + } else if (vocationEnum == Vocation_t::VOCATION_PALADIN_CIP) { + paladin = true; + } else if (vocationEnum == Vocation_t::VOCATION_DRUID_CIP) { + druid = true; + } + } + } + return knight && paladin && druid && sorcerer; +} + +bool PlayerBadge::tournamentParticipation(uint8_t skill) { + // todo check if is used + return false; +} + +bool PlayerBadge::tournamentPoints(uint8_t race) { + // todo check if is used + return false; +} diff --git a/src/creatures/players/cyclopedia/player_badge.hpp b/src/creatures/players/cyclopedia/player_badge.hpp new file mode 100644 index 00000000000..01c9dc0e63f --- /dev/null +++ b/src/creatures/players/cyclopedia/player_badge.hpp @@ -0,0 +1,65 @@ +/** + * 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 "game/game_definitions.hpp" + +class Player; +class KV; + +struct Badge { + uint8_t m_id = 0; + CyclopediaBadge_t m_type; + std::string m_name; + uint16_t m_amount = 0; + + Badge() = default; + + Badge(uint8_t id, CyclopediaBadge_t type, std::string name, uint16_t amount) : + m_id(id), m_type(type), m_name(std::move(name)), m_amount(amount) { } + + bool operator==(const Badge &other) const { + return m_id == other.m_id; + } +}; + +namespace std { + template <> + struct hash { + std::size_t operator()(const Badge &b) const { + return hash()(b.m_id); + } + }; +} + +class PlayerBadge { +public: + explicit PlayerBadge(Player &player); + + [[nodiscard]] bool hasBadge(uint8_t id) const; + bool add(uint8_t id, uint32_t timestamp = 0); + void checkAndUpdateNewBadges(); + void loadUnlockedBadges(); + const std::shared_ptr &getUnlockedKV(); + + // Badge Calculate Functions + bool accountAge(uint8_t amount); + bool loyalty(uint8_t amount); + bool accountAllLevel(uint8_t amount); + bool accountAllVocations(uint8_t amount); + [[nodiscard]] bool tournamentParticipation(uint8_t skill); + [[nodiscard]] bool tournamentPoints(uint8_t race); + +private: + // {badge ID, time when it was unlocked} + std::shared_ptr m_badgeUnlockedKV; + std::vector> m_badgesUnlocked; + Player &m_player; +}; diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 149f41380ec..8b889ac200b 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -16,6 +16,7 @@ #include "creatures/players/player.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" +#include "creatures/players/cyclopedia/player_badge.hpp" #include "creatures/players/storages/storages.hpp" #include "game/game.hpp" #include "game/modal_window/modal_window.hpp" @@ -49,6 +50,7 @@ Player::Player(ProtocolGame_ptr p) : client(std::move(p)) { m_wheelPlayer = std::make_unique(*this); m_playerAchievement = std::make_unique(*this); + m_playerBadge = std::make_unique(*this); } Player::~Player() { @@ -129,7 +131,7 @@ std::string Player::getDescription(int32_t lookDistance) { s << " You have no vocation."; } - if (loyaltyTitle.length() != 0) { + if (!loyaltyTitle.empty()) { s << " You are a " << loyaltyTitle << "."; } @@ -141,9 +143,8 @@ std::string Player::getDescription(int32_t lookDistance) { if (!group->access) { s << " (Level " << level << ')'; } - s << '.'; - s << " " << subjectPronoun; + s << ". " << subjectPronoun; if (group->access) { s << " " << getSubjectVerb() << " " << group->name << '.'; @@ -153,7 +154,7 @@ std::string Player::getDescription(int32_t lookDistance) { s << " has no vocation."; } - if (loyaltyTitle.length() != 0) { + if (!loyaltyTitle.empty()) { std::string article = "a"; if (loyaltyTitle[0] == 'A' || loyaltyTitle[0] == 'E' || loyaltyTitle[0] == 'I' || loyaltyTitle[0] == 'O' || loyaltyTitle[0] == 'U') { article = "an"; @@ -628,6 +629,20 @@ 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()) { @@ -6579,17 +6594,6 @@ void Player::initializeTaskHunting() { } std::string Player::getBlessingsName() const { - static const 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" }, - }; - uint8_t count = 0; std::for_each(blessings.begin(), blessings.end(), [&count](uint8_t amount) { if (amount != 0) { @@ -6597,6 +6601,7 @@ std::string Player::getBlessingsName() const { } }); + auto BlessingNames = getBlessingNames(); std::ostringstream os; for (uint8_t i = 1; i <= 8; i++) { if (hasBlessing(i)) { @@ -7954,6 +7959,15 @@ const std::unique_ptr &Player::achiev() const { return m_playerAchievement; } +// Badge interface +std::unique_ptr &Player::badge() { + return m_playerBadge; +} + +const std::unique_ptr &Player::badge() const { + return m_playerBadge; +} + void Player::sendLootMessage(const std::string &message) const { auto party = getParty(); if (!party) { @@ -8073,3 +8087,18 @@ bool Player::canSpeakWithHireling(uint8_t speechbubble) { return true; } + +uint16_t Player::getPlayerVocationEnum() const { + int cipTibiaId = getVocation()->getClientId(); + if (cipTibiaId == 1 || cipTibiaId == 11) { + return Vocation_t::VOCATION_KNIGHT_CIP; // Knight + } else if (cipTibiaId == 2 || cipTibiaId == 12) { + return Vocation_t::VOCATION_PALADIN_CIP; // Paladin + } else if (cipTibiaId == 3 || cipTibiaId == 13) { + return Vocation_t::VOCATION_SORCERER_CIP; // Sorcerer + } else if (cipTibiaId == 4 || cipTibiaId == 14) { + return Vocation_t::VOCATION_DRUID_CIP; // Druid + } + + return Vocation_t::VOCATION_NONE; +} diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index c2a9f29831a..ca9c944bb2e 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -34,6 +34,7 @@ #include "creatures/npcs/npc.hpp" #include "game/bank/bank.hpp" #include "enums/object_category.hpp" +#include "creatures/players/cyclopedia/player_badge.hpp" class House; class NetworkMessage; @@ -49,11 +50,13 @@ class TaskHuntingSlot; class Spell; class PlayerWheel; class PlayerAchievement; +class PlayerBadge; class Spectators; class Account; struct ModalWindow; struct Achievement; +struct Badge; struct ForgeHistory { ForgeAction_t actionType = ForgeAction_t::FUSION; @@ -735,6 +738,13 @@ class Player final : public Creature, public Cylinder, public Bankable { uint32_t getCapacity() const; + uint32_t getBonusCapacity() const { + if (hasFlag(PlayerFlags_t::CannotPickupItem) || hasFlag(PlayerFlags_t::HasInfiniteCapacity)) { + return std::numeric_limits::max(); + } + return bonusCapacity; + } + uint32_t getFreeCapacity() const { if (hasFlag(PlayerFlags_t::CannotPickupItem)) { return 0; @@ -2577,6 +2587,9 @@ 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; + /** * @brief Get the equipped items of the player-> * @details This function returns a vector containing the items currently equipped by the player @@ -2592,6 +2605,10 @@ class Player final : public Creature, public Cylinder, public Bankable { std::unique_ptr &achiev(); const std::unique_ptr &achiev() const; + // Player badge interface + std::unique_ptr &badge(); + const std::unique_ptr &badge() const; + void sendLootMessage(const std::string &message) const; std::shared_ptr getLootPouch(); @@ -2602,6 +2619,8 @@ class Player final : public Creature, public Cylinder, public Bankable { bool canSpeakWithHireling(uint8_t speechbubble); + uint16_t getPlayerVocationEnum() const; + private: friend class PlayerLock; std::mutex mutex; @@ -2659,6 +2678,7 @@ class Player final : public Creature, public Cylinder, public Bankable { uint32_t getItemTypeCount(uint16_t itemId, int32_t subType = -1) const override; void stashContainer(StashContainerList itemDict); ItemsTierCountList getInventoryItemsId() const; + // todo ItemsTierCountList getStoreInboxItemsId() const; // This function is a override function of base class std::map &getAllItemTypeCount(std::map &countMap) const override; @@ -2984,9 +3004,11 @@ class Player final : public Creature, public Cylinder, public Bankable { friend class IOLoginDataLoad; friend class IOLoginDataSave; friend class PlayerAchievement; + friend class PlayerBadge; std::unique_ptr m_wheelPlayer; std::unique_ptr m_playerAchievement; + std::unique_ptr m_playerBadge; std::mutex quickLootMutex; diff --git a/src/creatures/players/wheel/player_wheel.cpp b/src/creatures/players/wheel/player_wheel.cpp index 20e8f683bf9..5c63e9aecc2 100644 --- a/src/creatures/players/wheel/player_wheel.cpp +++ b/src/creatures/players/wheel/player_wheel.cpp @@ -689,7 +689,7 @@ bool PlayerWheel::getSpellAdditionalArea(const std::string &spellName) const { return false; } - auto vocationEnum = getPlayerVocationEnum(); + auto vocationEnum = m_player.getPlayerVocationEnum(); if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { return checkSpellArea(g_game().getIOWheel()->getWheelBonusData().spells.knight, spellName, stage); } else if (vocationEnum == Vocation_t::VOCATION_PALADIN_CIP) { @@ -709,7 +709,7 @@ int PlayerWheel::getSpellAdditionalTarget(const std::string &spellName) const { return 0; } - auto vocationEnum = getPlayerVocationEnum(); + auto vocationEnum = m_player.getPlayerVocationEnum(); if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { return checkSpellAdditionalTarget(g_game().getIOWheel()->getWheelBonusData().spells.knight, spellName, stage); } else if (vocationEnum == Vocation_t::VOCATION_PALADIN_CIP) { @@ -729,7 +729,7 @@ int PlayerWheel::getSpellAdditionalDuration(const std::string &spellName) const return 0; } - auto vocationEnum = getPlayerVocationEnum(); + auto vocationEnum = m_player.getPlayerVocationEnum(); if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { return checkSpellAdditionalDuration(g_game().getIOWheel()->getWheelBonusData().spells.knight, spellName, stage); } else if (vocationEnum == Vocation_t::VOCATION_PALADIN_CIP) { @@ -1016,7 +1016,7 @@ void PlayerWheel::sendOpenWheelWindow(NetworkMessage &msg, uint32_t ownerId) con } msg.addByte(getOptions(ownerId)); // Options - msg.addByte(getPlayerVocationEnum()); // Vocation id + msg.addByte(m_player.getPlayerVocationEnum()); // Vocation id msg.add(getWheelPoints(false)); // Points (false param for not send extra points) msg.add(getExtraPoints()); // Extra points @@ -1262,7 +1262,7 @@ uint16_t PlayerWheel::getWheelPoints(bool includeExtraPoints /* = true*/) const bool PlayerWheel::canOpenWheel() const { // Vocation check - if (getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + if (m_player.getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { return false; } @@ -1304,21 +1304,6 @@ uint8_t PlayerWheel::getOptions(uint32_t ownerId) const { return 2; } -uint8_t PlayerWheel::getPlayerVocationEnum() const { - int cipTibiaId = m_player.getVocation()->getClientId(); - if (cipTibiaId == 1 || cipTibiaId == 11) { - return Vocation_t::VOCATION_KNIGHT_CIP; // Knight - } else if (cipTibiaId == 2 || cipTibiaId == 12) { - return Vocation_t::VOCATION_PALADIN_CIP; // Paladin - } else if (cipTibiaId == 3 || cipTibiaId == 13) { - return Vocation_t::VOCATION_SORCERER_CIP; // Sorcerer - } else if (cipTibiaId == 4 || cipTibiaId == 14) { - return Vocation_t::VOCATION_DRUID_CIP; // Druid - } - - return Vocation_t::VOCATION_NONE; -} - bool PlayerWheel::canSelectSlotFullOrPartial(WheelSlots_t slot) const { if (getPointsBySlotType(slot) == getMaxPointsPerSlot(slot)) { g_logger().debug("[{}] points on slot {}, max points {}", __FUNCTION__, getPointsBySlotType(slot), getMaxPointsPerSlot(slot)); @@ -1785,7 +1770,7 @@ void PlayerWheel::printPlayerWheelMethodsBonusData(const PlayerWheelMethodsBonus void PlayerWheel::loadDedicationAndConvictionPerks() { using VocationBonusFunction = std::function &, uint16_t, uint8_t, PlayerWheelMethodsBonusData &)>; auto wheelFunctions = g_game().getIOWheel()->getWheelMapFunctions(); - auto vocationCipId = getPlayerVocationEnum(); + auto vocationCipId = m_player.getPlayerVocationEnum(); if (vocationCipId < VOCATION_KNIGHT_CIP || vocationCipId > VOCATION_DRUID_CIP) { return; } @@ -1826,7 +1811,7 @@ void PlayerWheel::loadRevelationPerks() { m_playerBonusData.stats.healing += statsHealing; auto redStageValue = static_cast(redStageEnum); - auto vocationEnum = getPlayerVocationEnum(); + auto vocationEnum = m_player.getPlayerVocationEnum(); if (vocationEnum == Vocation_t::VOCATION_DRUID_CIP) { m_playerBonusData.stages.blessingOfTheGrove = redStageValue; } else if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { @@ -1854,7 +1839,7 @@ void PlayerWheel::loadRevelationPerks() { m_playerBonusData.stats.healing += statsHealing; auto purpleStage = static_cast(purpleStageEnum); - auto vocationEnum = getPlayerVocationEnum(); + auto vocationEnum = m_player.getPlayerVocationEnum(); if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { m_playerBonusData.avatar.steel = purpleStage; for (uint8_t i = 0; i < purpleStage; ++i) { @@ -1885,7 +1870,7 @@ void PlayerWheel::loadRevelationPerks() { m_playerBonusData.stats.healing += statsHealing; auto blueStage = static_cast(blueStageEnum); - auto vocationEnum = getPlayerVocationEnum(); + auto vocationEnum = m_player.getPlayerVocationEnum(); if (vocationEnum == Vocation_t::VOCATION_KNIGHT_CIP) { m_playerBonusData.stages.combatMastery = blueStage; } else if (vocationEnum == Vocation_t::VOCATION_SORCERER_CIP) { diff --git a/src/creatures/players/wheel/player_wheel.hpp b/src/creatures/players/wheel/player_wheel.hpp index 303063beb9b..14f922e9597 100644 --- a/src/creatures/players/wheel/player_wheel.hpp +++ b/src/creatures/players/wheel/player_wheel.hpp @@ -205,7 +205,6 @@ class PlayerWheel { * indicating that the player can increase points but cannot decrease the ID. */ uint8_t getOptions(uint32_t ownerId) const; - uint8_t getPlayerVocationEnum() const; std::shared_ptr gemsKV() const; diff --git a/src/game/game.cpp b/src/game/game.cpp index b1d76f9f760..eeaaabb39f7 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -37,6 +37,7 @@ #include "creatures/players/imbuements/imbuements.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" +#include "creatures/players/cyclopedia/player_badge.hpp" #include "creatures/npcs/npc.hpp" #include "server/network/webhook/webhook.hpp" #include "server/network/protocol/protocollogin.hpp" @@ -53,23 +54,6 @@ #include -enum class HighscoreCategories_t : uint8_t { - EXPERIENCE = 0, - FIST_FIGHTING = 1, - CLUB_FIGHTING = 2, - SWORD_FIGHTING = 3, - AXE_FIGHTING = 4, - DISTANCE_FIGHTING = 5, - SHIELDING = 6, - FISHING = 7, - MAGIC_LEVEL = 8, - LOYALTY = 9, - ACHIEVEMENTS = 10, - CHARMS = 11, - DROME = 12, - GOSHNAR = 13, -}; - namespace InternalGame { void sendBlockEffect(BlockType_t blockType, CombatType_t combatType, const Position &targetPos, std::shared_ptr source) { if (blockType == BLOCK_DEFENSE) { @@ -209,6 +193,35 @@ Game::Game() { wildcardTree = std::make_shared(false); + m_badges = { + Badge(1, CyclopediaBadge_t::ACCOUNT_AGE, "Fledegeling Hero", 1), + Badge(2, CyclopediaBadge_t::ACCOUNT_AGE, "Veteran Hero", 5), + Badge(3, CyclopediaBadge_t::ACCOUNT_AGE, "Senior Hero", 10), + Badge(4, CyclopediaBadge_t::ACCOUNT_AGE, "Ancient Hero", 15), + Badge(5, CyclopediaBadge_t::ACCOUNT_AGE, "Exalted Hero", 20), + + Badge(6, CyclopediaBadge_t::LOYALTY, "Tibia Loyalist (Grade 1)", 100), + Badge(7, CyclopediaBadge_t::LOYALTY, "Tibia Loyalist (Grade 2)", 1000), + Badge(8, CyclopediaBadge_t::LOYALTY, "Tibia Loyalist (Grade 3)", 5000), + + Badge(9, CyclopediaBadge_t::ACCOUNT_ALL_LEVEL, "Global Player (Grade 1)", 500), + Badge(10, CyclopediaBadge_t::ACCOUNT_ALL_LEVEL, "Global Player (Grade 2)", 1000), + Badge(11, CyclopediaBadge_t::ACCOUNT_ALL_LEVEL, "Global Player (Grade 3)", 2000), + + Badge(12, CyclopediaBadge_t::ACCOUNT_ALL_VOCATIONS, "Master Class (Grade 1)", 100), + Badge(13, CyclopediaBadge_t::ACCOUNT_ALL_VOCATIONS, "Master Class (Grade 2)", 250), + Badge(14, CyclopediaBadge_t::ACCOUNT_ALL_VOCATIONS, "Master Class (Grade 3)", 500), + + Badge(15, CyclopediaBadge_t::TOURNAMENT_PARTICIPATION, "Freshman of the Tournament", 1), + Badge(16, CyclopediaBadge_t::TOURNAMENT_PARTICIPATION, "Regular of the Tournament", 5), + Badge(17, CyclopediaBadge_t::TOURNAMENT_PARTICIPATION, "Hero of the Tournament", 10), + + Badge(18, CyclopediaBadge_t::TOURNAMENT_POINTS, "Tournament Competitor", 1000), + Badge(19, CyclopediaBadge_t::TOURNAMENT_POINTS, "Tournament Challenger", 2500), + Badge(20, CyclopediaBadge_t::TOURNAMENT_POINTS, "Tournament Master", 5000), + Badge(21, CyclopediaBadge_t::TOURNAMENT_POINTS, "Tournament Champion", 10000), + }; + m_highscoreCategoriesNames = { { static_cast(HighscoreCategories_t::ACHIEVEMENTS), "Achievement Points" }, { static_cast(HighscoreCategories_t::AXE_FIGHTING), "Axe Fighting" }, @@ -8147,6 +8160,17 @@ void Game::kickPlayer(uint32_t playerId, bool displayEffect) { player->removePlayer(displayEffect); } +void Game::playerFriendSystemAction(std::shared_ptr player, uint8_t type, uint8_t titleId) { + uint32_t playerGUID = player->getGUID(); + if (type == 0x0E) { + // todo in titles system PR + // player->title()->setCurrentTitle(titleId); + // player->sendCyclopediaCharacterBaseInformation(); + // player->sendCyclopediaCharacterTitles(); + return; + } +} + void Game::playerCyclopediaCharacterInfo(std::shared_ptr player, uint32_t characterID, CyclopediaCharacterInfoType_t characterInfoType, uint16_t entriesPerPage, uint16_t page) { uint32_t playerGUID = player->getGUID(); if (characterID != playerGUID) { @@ -10538,3 +10562,38 @@ std::vector Game::getPublicAchievements() { std::map Game::getAchievements() { return m_achievements; } + +void Game::logCyclopediaStats() { + g_logger().info("Loaded {} badges from Badge System", m_badges.size()); + // todo in title system: g_logger().info("Loaded {} titles from Title system", m_titles.size()); +} + +std::unordered_set Game::getBadges() { + return m_badges; +} + +Badge Game::getBadgeById(uint8_t id) { + if (id == 0) { + return {}; + } + auto it = std::find_if(m_badges.begin(), m_badges.end(), [id](const Badge &b) { + return b.m_id == id; + }); + if (it != m_badges.end()) { + return *it; + } + return {}; +} + +Badge Game::getBadgeByName(const std::string &name) { + if (name.empty()) { + return {}; + } + auto it = std::find_if(m_badges.begin(), m_badges.end(), [name](const Badge &b) { + return b.m_name == name; + }); + if (it != m_badges.end()) { + return *it; + } + return {}; +} diff --git a/src/game/game.hpp b/src/game/game.hpp index 708aa4b1782..9d743895c4c 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -50,6 +50,7 @@ class Spectators; struct Achievement; struct HighscoreCategory; +struct Badge; static constexpr uint16_t SERVER_BEAT = 0x32; static constexpr int32_t EVENT_MS = 10000; @@ -98,6 +99,8 @@ class Game { void forceRemoveCondition(uint32_t creatureId, ConditionType_t type, ConditionId_t conditionId); + void logCyclopediaStats(); + /** * Load the main map * \param filename Is the map custom name (Example: "map".otbm, not is necessary add extension .otbm) @@ -305,6 +308,8 @@ class Game { void playerReportRuleViolationReport(uint32_t playerId, const std::string &targetName, uint8_t reportType, uint8_t reportReason, const std::string &comment, const std::string &translation); + void playerFriendSystemAction(std::shared_ptr player, uint8_t type, uint8_t titleId); + void playerCyclopediaCharacterInfo(std::shared_ptr player, uint32_t characterID, CyclopediaCharacterInfoType_t characterInfoType, uint16_t entriesPerPage, uint16_t page); void playerHighscores(std::shared_ptr player, HighscoreType_t type, uint8_t category, uint32_t vocation, const std::string &worldName, uint16_t page, uint8_t entriesPerPage); @@ -717,10 +722,16 @@ class Game { std::vector getPublicAchievements(); std::map getAchievements(); + std::unordered_set getBadges(); + Badge getBadgeById(uint8_t id); + Badge getBadgeByName(const std::string &name); + private: std::map m_achievements; std::map m_achievementsNameToId; + std::unordered_set m_badges; + std::vector m_highscoreCategories; std::unordered_map m_highscoreCategoriesNames; diff --git a/src/game/game_definitions.hpp b/src/game/game_definitions.hpp index 2dadcbbd87c..2c2b40fac78 100644 --- a/src/game/game_definitions.hpp +++ b/src/game/game_definitions.hpp @@ -61,6 +61,15 @@ enum LightState_t { LIGHT_STATE_SUNRISE, }; +enum CyclopediaBadge_t : uint8_t { + ACCOUNT_AGE = 1, + LOYALTY, + ACCOUNT_ALL_LEVEL, + ACCOUNT_ALL_VOCATIONS, + TOURNAMENT_PARTICIPATION, + TOURNAMENT_POINTS, +}; + enum CyclopediaCharacterInfoType_t : uint8_t { CYCLOPEDIA_CHARACTERINFO_BASEINFORMATION = 0, CYCLOPEDIA_CHARACTERINFO_GENERALSTATS = 1, @@ -84,6 +93,23 @@ enum CyclopediaCharacterInfo_RecentKillStatus_t : uint8_t { CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_ARENA = 4 }; +enum class HighscoreCategories_t : uint8_t { + EXPERIENCE = 0, + FIST_FIGHTING = 1, + CLUB_FIGHTING = 2, + SWORD_FIGHTING = 3, + AXE_FIGHTING = 4, + DISTANCE_FIGHTING = 5, + SHIELDING = 6, + FISHING = 7, + MAGIC_LEVEL = 8, + LOYALTY = 9, + ACHIEVEMENTS = 10, + CHARMS = 11, + DROME = 12, + GOSHNAR = 13, +}; + 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 2c3cbf48fea..041cd323e28 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -889,6 +889,7 @@ void IOLoginDataLoad::loadPlayerInitializeSystem(std::shared_ptr player) player->wheel()->initializePlayerData(); player->achiev()->loadUnlockedAchievements(); + player->badge()->checkAndUpdateNewBadges(); 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 ea418dede66..5f705cf426d 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -27,6 +27,7 @@ #include "lua/callbacks/event_callback.hpp" #include "lua/callbacks/events_callbacks.hpp" #include "creatures/players/achievement/player_achievement.hpp" +#include "creatures/players/cyclopedia/player_badge.hpp" #include "map/spectators.hpp" // Game diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 94100a1f406..81b70263a0a 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -15,6 +15,7 @@ #include "creatures/players/player.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" +#include "creatures/players/cyclopedia/player_badge.hpp" #include "game/game.hpp" #include "io/iologindata.hpp" #include "io/ioprey.hpp" @@ -4279,3 +4280,16 @@ int PlayerFunctions::luaPlayerRemoveAchievementPoints(lua_State* L) { pushBoolean(L, true); return 1; } + +int PlayerFunctions::luaPlayerAddBadge(lua_State* L) { + // player:addBadge(id) + const auto &player = getUserdataShared(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + return 1; + } + + player->badge()->add(getNumber(L, 2, 0)); + 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 2be6e5076cd..a30f9f755ca 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -364,6 +364,9 @@ class PlayerFunctions final : LuaScriptInterface { registerMethod(L, "Player", "addAchievementPoints", PlayerFunctions::luaPlayerAddAchievementPoints); registerMethod(L, "Player", "removeAchievementPoints", PlayerFunctions::luaPlayerRemoveAchievementPoints); + // Badge Functions + registerMethod(L, "Player", "addBadge", PlayerFunctions::luaPlayerAddBadge); + GroupFunctions::init(L); GuildFunctions::init(L); MountFunctions::init(L); @@ -718,5 +721,7 @@ class PlayerFunctions final : LuaScriptInterface { static int luaPlayerAddAchievementPoints(lua_State* L); static int luaPlayerRemoveAchievementPoints(lua_State* L); + static int luaPlayerAddBadge(lua_State* L); + friend class CreatureFunctions; }; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index c822ff4af7c..db850af5ecc 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -27,6 +27,7 @@ #include "creatures/players/player.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "creatures/players/achievement/player_achievement.hpp" +#include "creatures/players/cyclopedia/player_badge.hpp" #include "creatures/players/grouping/familiars.hpp" #include "server/network/protocol/protocolgame.hpp" #include "game/scheduling/dispatcher.hpp" @@ -1090,6 +1091,9 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0x80: g_game().playerCloseTrade(player->getID()); break; + case 0x81: + parseFriendSystemAction(msg); + break; case 0x82: parseUseItem(msg); break; @@ -2051,6 +2055,15 @@ void ProtocolGame::sendItemInspection(uint16_t itemId, uint8_t itemCount, std::s writeToOutputBuffer(msg); } +void ProtocolGame::parseFriendSystemAction(NetworkMessage &msg) { + uint8_t state = msg.getByte(); + if (state == 0x0E) { + // todo title system pr + // uint8_t titleId = msg.getByte(); + // g_game().playerFriendSystemAction(player, state, titleId); + } +} + void ProtocolGame::parseCyclopediaCharacterInfo(NetworkMessage &msg) { if (oldProtocol) { return; @@ -2125,7 +2138,7 @@ void ProtocolGame::sendHighscores(const std::vector &charact NetworkMessage msg; msg.addByte(0xB1); - msg.addByte(0x00); // No data available + msg.addByte(0x00); // All data available msg.addByte(1); // Worlds msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // First World @@ -3367,8 +3380,9 @@ void ProtocolGame::sendCyclopediaCharacterBaseInformation() { msg.add(player->getLevel()); AddOutfit(msg, player->getDefaultOutfit(), false); - msg.addByte(0x00); // hide stamina + msg.addByte(0x00); // Store summary & Character titles msg.addString("", "ProtocolGame::sendCyclopediaCharacterBaseInformation - empty"); // character title + // msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterBaseInformation - player->title()->getCurrentTitleName()"); // character title writeToOutputBuffer(msg); } @@ -3384,7 +3398,8 @@ void ProtocolGame::sendCyclopediaCharacterGeneralStats() { // 1: No data available at the moment. // 2: You are not allowed to see this character's data. // 3: You are not allowed to inspect this character. - msg.addByte(0x00); + msg.addByte(0x00); // 0x00 Here means 'no error' + msg.add(player->getExperience()); msg.add(player->getLevel()); msg.addByte(player->getLevelPercent()); @@ -3393,7 +3408,7 @@ void ProtocolGame::sendCyclopediaCharacterGeneralStats() { msg.add(player->getXpBoostPercent()); // XPBoost msg.add(player->getStaminaXpBoost()); // StaminaMultiplier(100=x1.0) msg.add(player->getXpBoostTime()); // xpBoostRemainingTime - msg.addByte(0x01); // canBuyXpBoost + msg.addByte(player->getXpBoostTime() > 0 ? 0x00 : 0x01); // canBuyXpBoost msg.add(std::min(player->getHealth(), std::numeric_limits::max())); msg.add(std::min(player->getMaxHealth(), std::numeric_limits::max())); msg.add(std::min(player->getMana(), std::numeric_limits::max())); @@ -3406,8 +3421,8 @@ void ProtocolGame::sendCyclopediaCharacterGeneralStats() { msg.add(player->getOfflineTrainingTime() / 60 / 1000); msg.add(player->getSpeed()); msg.add(player->getBaseSpeed()); - msg.add(player->getCapacity()); - msg.add(player->getCapacity()); + msg.add(player->getBonusCapacity()); + msg.add(player->getBaseCapacity()); msg.add(player->hasFlag(PlayerFlags_t::HasInfiniteCapacity) ? 1000000 : player->getFreeCapacity()); msg.addByte(8); msg.addByte(1); @@ -3611,10 +3626,17 @@ void ProtocolGame::sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t p msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS); msg.addByte(0x00); - msg.add(page); + + 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.add(pages); - msg.add(entries.size()); - for (const RecentDeathEntry &entry : entries) { + for (uint16_t i = firstObject; i < finalObject; i++) { + RecentDeathEntry entry = entries[i]; msg.add(entry.timestamp); msg.addString(entry.cause, "ProtocolGame::sendCyclopediaCharacterRecentDeaths - entry.cause"); } @@ -3630,10 +3652,17 @@ void ProtocolGame::sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS); msg.addByte(0x00); - msg.add(page); + + 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.add(pages); - msg.add(entries.size()); - for (const RecentPvPKillEntry &entry : entries) { + for (uint16_t i = firstObject; i < finalObject; i++) { + RecentPvPKillEntry entry = entries[i]; msg.add(entry.timestamp); msg.addString(entry.description, "ProtocolGame::sendCyclopediaCharacterRecentPvPKills - entry.description"); msg.addByte(entry.status); @@ -3678,13 +3707,13 @@ void ProtocolGame::sendCyclopediaCharacterItemSummary() { NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_ITEMSUMMARY); - msg.addByte(0x00); + msg.addByte(0x00); // 0x00 Here means 'no error' - msg.add(0); - msg.add(0); - msg.add(0); - msg.add(0); - msg.add(0); + msg.add(0); // inventoryItems.size() + msg.add(0); // storeInboxItems.size() + msg.add(0); // supplyStashItems.size() + msg.add(0); // depotBoxItems.size() + msg.add(0); // inboxItems.size() writeToOutputBuffer(msg); } @@ -3800,20 +3829,19 @@ void ProtocolGame::sendCyclopediaCharacterStoreSummary() { NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_STORESUMMARY); - msg.addByte(0x00); - // Remaining Store Xp Boost Time - msg.add(player->getXpBoostTime()); - // RemainingDailyRewardXpBoostTime - msg.add(0); - msg.addByte(0x00); - msg.addByte(0x00); - msg.addByte(0x00); - msg.addByte(0x00); - msg.addByte(0x00); - msg.addByte(0x00); - msg.addByte(0x00); - msg.addByte(0x00); - msg.add(0); + msg.addByte(0x00); // 0x00 Here means 'no error' + msg.add(player->getXpBoostTime()); // Remaining Store Xp Boost Time + msg.add(0); // RemainingDailyRewardXpBoostTime + + 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); } @@ -3837,7 +3865,24 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { msg.addByte(slot); msg.addString(inventoryItem->getName(), "ProtocolGame::sendCyclopediaCharacterInspection - inventoryItem->getName()"); AddItem(msg, inventoryItem); - msg.addByte(0); + + uint8_t itemImbuements = 0; + auto startImbuements = msg.getBufferPosition(); + msg.skipBytes(1); + for (uint8_t slotid = 0; slotid < inventoryItem->getImbuementSlot(); slotid++) { + ImbuementInfo imbuementInfo; + if (!inventoryItem->getImbuementInfo(slotid, &imbuementInfo)) { + continue; + } + + msg.add(imbuementInfo.imbuement->getID()); + itemImbuements++; + } + + auto endImbuements = msg.getBufferPosition(); + msg.setBufferPosition(startImbuements); + msg.addByte(itemImbuements); + msg.setBufferPosition(endImbuements); auto descriptions = Item::getDescriptions(Item::items[inventoryItem->getID()], inventoryItem); msg.addByte(descriptions.size()); @@ -3866,7 +3911,7 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { msg.addString(player->getVocation()->getVocName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getVocation()->getVocName()"); // Loyalty title - if (player->getLoyaltyTitle().length() != 0) { + if (!player->getLoyaltyTitle().empty()) { playerDescriptionSize++; msg.addString("Loyalty Title", "ProtocolGame::sendCyclopediaCharacterInspection - Loyalty Title"); msg.addString(player->getLoyaltyTitle(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getLoyaltyTitle()"); @@ -3899,19 +3944,29 @@ void ProtocolGame::sendCyclopediaCharacterBadges() { msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_BADGES); msg.addByte(0x00); - // ShowAccountInformation - msg.addByte(0x01); - // if ShowAccountInformation show IsOnline, IsPremium, character title, badges - // IsOnline + msg.addByte(0x01); // ShowAccountInformation, if 0x01 will show IsOnline, IsPremium, character title, badges + const auto loggedPlayer = g_game().getPlayerUniqueLogin(player->getName()); - msg.addByte(loggedPlayer ? 0x01 : 0x00); - // IsPremium (GOD has always 'Premium') - msg.addByte(player->isPremium() ? 0x01 : 0x00); + msg.addByte(loggedPlayer ? 0x01 : 0x00); // IsOnline + msg.addByte(player->isPremium() ? 0x01 : 0x00); // IsPremium (GOD has always 'Premium') // Character loyalty title msg.addString(player->getLoyaltyTitle(), "ProtocolGame::sendCyclopediaCharacterBadges - player->getLoyaltyTitle()"); - // Enable badges - msg.addByte(0x00); - // Todo badges loop + // msg.addByte(0x01); // Enable badges + + uint8_t badgesSize = 0; + auto badgesSizePosition = msg.getBufferPosition(); + msg.skipBytes(1); + for (const auto &badge : g_game().getBadges()) { + if (player->badge()->hasBadge(badge.m_id)) { + msg.add(badge.m_id); + msg.addString(badge.m_name, "ProtocolGame::sendCyclopediaCharacterBadges - name"); + badgesSize++; + } + } + + msg.setBufferPosition(badgesSizePosition); + msg.addByte(badgesSize); + writeToOutputBuffer(msg); } @@ -3923,9 +3978,10 @@ void ProtocolGame::sendCyclopediaCharacterTitles() { NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_TITLES); + msg.addByte(0x00); // 0x00 Here means 'no error' msg.addByte(0x00); msg.addByte(0x00); - msg.addByte(0x00); + writeToOutputBuffer(msg); } diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 306bb873f25..7aea8ff805d 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -13,6 +13,7 @@ #include "creatures/interactions/chat.hpp" #include "creatures/creature.hpp" #include "enums/forge_conversion.hpp" +#include "creatures/players/cyclopedia/player_badge.hpp" class NetworkMessage; class Player; @@ -29,6 +30,7 @@ class TaskHuntingOption; struct ModalWindow; struct Achievement; +struct Badge; using ProtocolGame_ptr = std::shared_ptr; @@ -126,6 +128,8 @@ class ProtocolGame final : public Protocol { void sendItemInspection(uint16_t itemId, uint8_t itemCount, std::shared_ptr item, bool cyclopedia); void parseInspectionObject(NetworkMessage &msg); + void parseFriendSystemAction(NetworkMessage &msg); + void parseCyclopediaCharacterInfo(NetworkMessage &msg); void parseHighscores(NetworkMessage &msg); diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index e31fdbad08a..5d40d930646 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -45,6 +45,7 @@ + @@ -257,6 +258,7 @@ + @@ -595,4 +597,4 @@ - \ No newline at end of file +