From 46802c606fbbc3d1ab7679496b465008224befc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rey=20Alem=C3=A1n?= Date: Tue, 31 May 2022 04:38:46 -0500 Subject: [PATCH] WIP: HIGHSCORES needs testing & code review --- data/events/events.xml | 1 + data/events/scripts/player.lua | 12 +++ data/lib/core/achievements.lua | 4 + data/lib/core/core.lua | 1 + data/lib/core/highscores.lua | 138 ++++++++++++++++++++++++++++++++ data/lib/core/storages.lua | 1 + data/lib/core/vocation.lua | 12 +++ src/definitions.h | 4 +- src/events.cpp | 58 ++++++++++++++ src/events.h | 4 + src/game.cpp | 30 +++++++ src/game.h | 2 + src/highscores.h | 115 ++++++++++++++++++++++++++ src/iologindata.cpp | 11 +++ src/iologindata.h | 2 + src/luascript.cpp | 13 +++ src/player.h | 7 ++ src/protocolgame.cpp | 102 ++++++++++++++++++++++- src/protocolgame.h | 6 ++ src/vocation.cpp | 11 +++ src/vocation.h | 2 + vc17/theforgottenserver.vcxproj | 3 +- 22 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 data/lib/core/highscores.lua create mode 100644 src/highscores.h diff --git a/data/events/events.xml b/data/events/events.xml index 10eedcfa51..632595371a 100644 --- a/data/events/events.xml +++ b/data/events/events.xml @@ -35,6 +35,7 @@ + diff --git a/data/events/scripts/player.lua b/data/events/scripts/player.lua index 1fd6d67806..5498ba364c 100644 --- a/data/events/scripts/player.lua +++ b/data/events/scripts/player.lua @@ -315,3 +315,15 @@ function Player:onInventoryUpdate(item, slot, equip) EventCallback.onInventoryUpdate(self, item, slot, equip) end end + +function Player:onRequestHighscores(params) + local cacheKey = getHighscoresCacheKey(params) + if highscoresCache[cacheKey] ~= nil and ((os.time() - highscoresTimestamps[cacheKey]) < highscoresCacheTime) then + return highscoresCache[cacheKey], highscoresTimestamps[cacheKey] + end + + highscoresCache[cacheKey] = getHighscores(params) + highscoresTimestamps[cacheKey] = os.time() + + return highscoresCache[cacheKey], highscoresTimestamps[cacheKey] +end diff --git a/data/lib/core/achievements.lua b/data/lib/core/achievements.lua index 92a618b6ef..38317f922e 100644 --- a/data/lib/core/achievements.lua +++ b/data/lib/core/achievements.lua @@ -702,6 +702,8 @@ function Player.addAchievement(self, ach, hideMsg) if not self:hasAchievement(achievement.id) then self:setStorageValue(PlayerStorageKeys.achievementsBase + achievement.id, 1) + local value = self:getStorageValue(PlayerStorageKeys.achievementPoints) + self:setStorageValue(PlayerStorageKeys.achievementPoints, value + 1) if not hideMsg then self:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations! You earned the achievement \"" .. achievement.name .. "\".") end @@ -723,6 +725,8 @@ function Player.removeAchievement(self, ach) if self:hasAchievement(achievement.id) then self:setStorageValue(PlayerStorageKeys.achievementsBase + achievement.id, -1) + local value = self:getStorageValue(PlayerStorageKeys.achievementPoints) + self:setStorageValue(PlayerStorageKeys.achievementPoints, value - 1) end return true end diff --git a/data/lib/core/core.lua b/data/lib/core/core.lua index ee806e03e2..25c4eab00d 100644 --- a/data/lib/core/core.lua +++ b/data/lib/core/core.lua @@ -8,6 +8,7 @@ dofile('data/lib/core/constants.lua') dofile('data/lib/core/container.lua') dofile('data/lib/core/creature.lua') dofile('data/lib/core/game.lua') +dofile('data/lib/core/highscores.lua') dofile('data/lib/core/item.lua') dofile('data/lib/core/itemtype.lua') dofile('data/lib/core/party.lua') diff --git a/data/lib/core/highscores.lua b/data/lib/core/highscores.lua new file mode 100644 index 0000000000..17b44d2fdb --- /dev/null +++ b/data/lib/core/highscores.lua @@ -0,0 +1,138 @@ +highscoresCache = {} +highscoresTimestamps = {} +highscoresMaxResults = 100 +highscoresCacheTime = 30 * 60 -- 30 minutes +highscoresExcludedGroups = {4, 5, 6} + +-- VocationId : VocationClientId +local vocationIdsToClientIds = { + [0] = 0, + [1] = 3, + [2] = 4, + [3] = 2, + [4] = 1, + [5] = 13, + [6] = 14, + [7] = 12, + [8] = 11 +} + +highscoresQueries = { + [HIGHSCORES_CATEGORY_LEVEL] = [[ + SELECT `id`, `name`, `vocation`, `level`, `experience` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_MAGLEVEL] = [[ + SELECT `id`, `name`, `vocation`, `level`, `maglevel` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `manaspent` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_FIST_FIGHTING] = [[ + SELECT `id`, `name`, `vocation`, `level`, `skill_fist` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `skill_fist_tries` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_AXE_FIGHTING] = [[ + SELECT `id`, `name`, `vocation`, `level`, `skill_axe` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `skill_axe_tries` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_CLUB_FIGHTING] = [[ + SELECT `id`, `name`, `vocation`, `level`, `skill_club` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `skill_club_tries` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_SWORD_FIGHTING] = [[ + SELECT `id`, `name`, `vocation`, `level`, `skill_sword` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `skill_sword_tries` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_DISTANCE_FIGHTING] = [[ + SELECT `id`, `name`, `vocation`, `level`, `skill_dist` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `skill_dist_tries` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_SHIELDING] = [[ + SELECT `id`, `name`, `vocation`, `level`, `skill_shielding` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `skill_shielding_tries` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_FISHING] = [[ + SELECT `id`, `name`, `vocation`, `level`, `skill_fishing` AS `points` FROM `players` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `skill_fishing_tries` DESC LIMIT %d, %d + ]], + [HIGHSCORES_CATEGORY_ACHIEVEMENTS] = [[ + SELECT `id`, `name`, `vocation`, `level`, `value` AS `points` FROM `player_storage` + LEFT JOIN `players` ON `players`.`id` = `player_storage`.`player_id` + WHERE `deletion` = 0 %s + AND `player_storage`.`key` = ]] .. PlayerStorageKeys.achievementPoints .. [[ + AND `group_id` NOT IN (]] .. table.concat(highscoresExcludedGroups, ", ") .. [[) + ORDER BY `points` DESC, `name` ASC LIMIT %d, %d + ]] + -- CUSTOM HIGHSCORE + --[[, + [HIGHSCORES_CATEGORY_DEATHS] = [[ + SELECT DISTINCT(`id`) `id`, `name`, `vocation`, `players`.`level`, + (SELECT COUNT(*) FROM `player_deaths` WHERE `player_deaths`.`player_id` = `players`.`id`) AS `points` + FROM `players` + LEFT JOIN `player_deaths` ON `player_deaths`.`player_id` = `players`.`id` + WHERE `deletion` = 0 %s + AND `group_id` NOT IN (4, 5, 6) + ORDER BY `points` DESC, `name` ASC LIMIT %d, %d + ]] + --]] +} + +function getHighscores(params) + if not highscoresQueries[params.category] then + return {}, os.time() + end + + local from = (params.page - 1) * 20 + local to = params.page * 20 + + local vocWhere = "" + if params.vocation ~= 0xFFFFFFFF then + local ids = Vocation(params.vocation):getPromotions() or {VOCATION_NONE} + vocWhere = "AND `vocation` IN (" .. table.concat(ids, ", ") .. ")" + end + + local entries = {} + local rank = from + 1 + local resultId = db.storeQuery(string.format(highscoresQueries[params.category], vocWhere, from, to)) + if resultId then + repeat + table.insert(entries, { + id = result.getNumber(resultId, "id"), + rank = rank, + name = result.getString(resultId, "name"), + title = "", + vocation = vocationIdsToClientIds[result.getNumber(resultId, "vocation")], + world = configManager.getString(configKeys.SERVER_NAME), + level = result.getNumber(resultId, "level"), + points = result.getNumber(resultId, "points") + }) + rank = rank + 1 + until not result.next(resultId) + result.free(resultId) + end + return entries +end + +function getHighscoresCacheKey(params) + if (params.world == "OWN" or params.world == "") then + params.world = configManager.getString(configKeys.SERVER_NAME) + end + return string.format("%d%s%d%d", params.category, params.world, params.vocation, params.page) +end diff --git a/data/lib/core/storages.lua b/data/lib/core/storages.lua index 9bfcaaabe8..931faf9cc5 100644 --- a/data/lib/core/storages.lua +++ b/data/lib/core/storages.lua @@ -30,6 +30,7 @@ PlayerStorageKeys = { achievementsBase = 300000, achievementsCounter = 20000, + achievementPoints = 19999 } GlobalStorageKeys = { diff --git a/data/lib/core/vocation.lua b/data/lib/core/vocation.lua index acf31afec7..1fa8bcbd6a 100644 --- a/data/lib/core/vocation.lua +++ b/data/lib/core/vocation.lua @@ -5,3 +5,15 @@ function Vocation.getBase(self) end return base end + +function Vocation.getPromotions(self) + local ids = {} + local base = self:getBase() + + table.insert(ids, base:getId()) + while base:getPromotion() do + base = base:getPromotion() + table.insert(ids, base:getId()) + end + return ids +end diff --git a/src/definitions.h b/src/definitions.h index d43c51f8b6..1185c73d29 100644 --- a/src/definitions.h +++ b/src/definitions.h @@ -9,8 +9,8 @@ static constexpr auto STATUS_SERVER_VERSION = "1.5"; static constexpr auto STATUS_SERVER_DEVELOPERS = "The Forgotten Server Team"; static constexpr auto CLIENT_VERSION_MIN = 1280; -static constexpr auto CLIENT_VERSION_MAX = 1286; -static constexpr auto CLIENT_VERSION_STR = "12.86"; +static constexpr auto CLIENT_VERSION_MAX = 1287; +static constexpr auto CLIENT_VERSION_STR = "12.87"; static constexpr auto AUTHENTICATOR_DIGITS = 6U; static constexpr auto AUTHENTICATOR_PERIOD = 30U; diff --git a/src/events.cpp b/src/events.cpp index 73eed12880..cab715853e 100644 --- a/src/events.cpp +++ b/src/events.cpp @@ -108,6 +108,8 @@ bool Events::load() info.playerOnWrapItem = event; } else if (methodName == "onInventoryUpdate") { info.playerOnInventoryUpdate = event; + } else if (methodName == "onRequestHighscores") { + info.playerOnRequestHighscores = event; } else { std::cout << "[Warning - Events::load] Unknown player method: " << methodName << std::endl; } @@ -1117,6 +1119,62 @@ void Events::eventPlayerOnInventoryUpdate(Player* player, Item* item, slots_t sl scriptInterface.callVoidFunction(4); } +void Events::eventPlayerOnRequestHighscores(Player* player, std::vector& entries, + HighscoresParams& params) +{ + // Player:onRequestHighscores(params) + if (info.playerOnRequestHighscores == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnRequestHighscores] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnRequestHighscores, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnRequestHighscores); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_createtable(L, 0, 4); + LuaScriptInterface::setField(L, "world", params.world); + LuaScriptInterface::setField(L, "vocation", params.vocation); + LuaScriptInterface::setField(L, "category", params.category); + LuaScriptInterface::setField(L, "page", params.page); + + if (scriptInterface.protectedCall(L, 2, 2) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + + lua_pushnil(L); + while (lua_next(L, -3) != 0) { + const auto tableIndex = lua_gettop(L); + uint32_t id = LuaScriptInterface::getField(L, tableIndex, "id"); + uint32_t rank = LuaScriptInterface::getField(L, tableIndex, "rank"); + std::string name = LuaScriptInterface::getFieldString(L, tableIndex, "name"); + std::string title = LuaScriptInterface::getFieldString(L, tableIndex, "title"); + uint8_t vocation = LuaScriptInterface::getField(L, tableIndex, "vocation"); + std::string world = LuaScriptInterface::getFieldString(L, tableIndex, "world"); + uint16_t level = LuaScriptInterface::getField(L, tableIndex, "level"); + uint64_t points = LuaScriptInterface::getField(L, tableIndex, "points"); + + entries.emplace_back(id, rank, name, title, vocation, world, level, points); + lua_pop(L, 9); + } + + params.totalPages = entries.size() < 20 ? params.page : params.totalPages; + params.timestamp = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 2); + } + + scriptInterface.resetScriptEnv(); +} + void Events::eventMonsterOnDropLoot(Monster* monster, Container* corpse) { // Monster:onDropLoot(corpse) diff --git a/src/events.h b/src/events.h index 9d7696d273..e71dbf9359 100644 --- a/src/events.h +++ b/src/events.h @@ -6,6 +6,7 @@ #include "const.h" #include "creature.h" +#include "highscores.h" #include "luascript.h" class ItemType; @@ -56,6 +57,7 @@ class Events int32_t playerOnGainSkillTries = -1; int32_t playerOnWrapItem = -1; int32_t playerOnInventoryUpdate = -1; + int32_t playerOnRequestHighscores = -1; // Monster int32_t monsterOnDropLoot = -1; @@ -109,6 +111,8 @@ class Events void eventPlayerOnGainSkillTries(Player* player, skills_t skill, uint64_t& tries); void eventPlayerOnWrapItem(Player* player, Item* item); void eventPlayerOnInventoryUpdate(Player* player, Item* item, slots_t slot, bool equip); + void eventPlayerOnRequestHighscores(Player* player, std::vector& entries, + HighscoresParams& params); // Monster void eventMonsterOnDropLoot(Monster* monster, Container* corpse); diff --git a/src/game.cpp b/src/game.cpp index 68898dfe01..00d8650498 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -3516,6 +3516,36 @@ void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit) } } +void Game::playerShowHighscores(uint32_t playerId, uint8_t category, uint32_t vocation, const std::string& world, + uint16_t page) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + std::vector entries; + HighscoresParams params; + params.category = category; + params.vocation = vocation; + params.world = world; + params.type = 0x00; + params.battlEye = 0xFF; + + uint32_t totalPlayers = IOLoginData::getTotalExistingPlayers(); + uint16_t totalPages = totalPlayers / 20; + if ((totalPlayers % 20) > 0) { + totalPages++; + } + + params.page = page > totalPages ? 1 : page; + params.totalPages = totalPages; + + g_events->eventPlayerOnRequestHighscores(player, entries, params); + + player->sendHighscores(entries, params); +} + void Game::playerShowQuestLog(uint32_t playerId) { Player* player = getPlayerByID(playerId); diff --git a/src/game.h b/src/game.h index 4d7fc22bd0..7ce0d828cf 100644 --- a/src/game.h +++ b/src/game.h @@ -385,6 +385,8 @@ class Game const uint16_t spriteId); void playerEditPodium(uint32_t playerId, Outfit_t outfit, const Position& position, uint8_t stackPos, const uint16_t spriteId, bool podiumVisible, Direction direction); + void playerShowHighscores(uint32_t playerId, uint8_t category, uint32_t vocation, const std::string& world, + uint16_t page); void playerShowQuestLog(uint32_t playerId); void playerShowQuestLine(uint32_t playerId, uint16_t questId); void playerResetQuestTracker(uint32_t playerId, const std::vector& missionIds); diff --git a/src/highscores.h b/src/highscores.h new file mode 100644 index 0000000000..674515ed5e --- /dev/null +++ b/src/highscores.h @@ -0,0 +1,115 @@ +// Copyright 2022 The Forgotten Server Authors. All rights reserved. +// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file. + +#ifndef FS_HIGHSCORES_H +#define FS_HIGHSCORES_H + +#include "configmanager.h" + +enum HighscoresType : uint8_t +{ + HIGHSCORES_TYPE_SKILLS = 0x00, + HIGHSCORES_TYPE_POINTS = 0x01, + HIGHSCORES_TYPE_SCORE = 0x02 +}; + +enum HighscoresCategoryTypes +{ + HIGHSCORES_CATEGORY_LEVEL = 0, + HIGHSCORES_CATEGORY_MAGLEVEL = 1, + HIGHSCORES_CATEGORY_FIST_FIGHTING = 2, + HIGHSCORES_CATEGORY_AXE_FIGHTING = 3, + HIGHSCORES_CATEGORY_CLUB_FIGHTING = 4, + HIGHSCORES_CATEGORY_SWORD_FIGHTING = 5, + HIGHSCORES_CATEGORY_DISTANCE_FIGHTING = 6, + HIGHSCORES_CATEGORY_SHIELDING = 7, + HIGHSCORES_CATEGORY_FISHING = 8, + HIGHSCORES_CATEGORY_ACHIEVEMENTS = 9, + HIGHSCORES_CATEGORY_LOYALTY = 10, + // HIGHSCORES_CATEGORY_DEATHS = 10, + HIGHSCORES_CATEGORY_FIRST = HIGHSCORES_CATEGORY_LEVEL, + HIGHSCORES_CATEGORY_LAST = HIGHSCORES_CATEGORY_ACHIEVEMENTS +}; + +struct Highscores +{ + static std::map init() + { + std::map map; + map[HIGHSCORES_CATEGORY_ACHIEVEMENTS] = "Achievement Points"; + map[HIGHSCORES_CATEGORY_AXE_FIGHTING] = "Axe Fighting"; + map[HIGHSCORES_CATEGORY_CLUB_FIGHTING] = "Club Fighting"; + map[HIGHSCORES_CATEGORY_DISTANCE_FIGHTING] = "Distance Fighting"; + map[HIGHSCORES_CATEGORY_LEVEL] = "Experience Points"; + map[HIGHSCORES_CATEGORY_FISHING] = "Fishing"; + map[HIGHSCORES_CATEGORY_FIST_FIGHTING] = "Fist Fighting"; + map[HIGHSCORES_CATEGORY_MAGLEVEL] = "Magic Level"; + map[HIGHSCORES_CATEGORY_SHIELDING] = "Shielding"; + map[HIGHSCORES_CATEGORY_SWORD_FIGHTING] = "Sword Fighting"; + // map[HIGHSCORES_CATEGORY_LOYALTY] = "Loyalty"; + // map[HIGHSCORES_CATEGORY_DEATHS] = "Most Deaths"; + return map; + } + + static uint8_t getType(HighscoresCategoryTypes type) + { + std::map map; + map[HIGHSCORES_CATEGORY_LEVEL] = HIGHSCORES_TYPE_POINTS; + map[HIGHSCORES_CATEGORY_MAGLEVEL] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_FIST_FIGHTING] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_AXE_FIGHTING] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_CLUB_FIGHTING] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_SWORD_FIGHTING] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_DISTANCE_FIGHTING] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_SHIELDING] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_FISHING] = HIGHSCORES_TYPE_SKILLS; + map[HIGHSCORES_CATEGORY_ACHIEVEMENTS] = HIGHSCORES_TYPE_POINTS; + // map[HIGHSCORES_CATEGORY_LOYALTY] = HIGHSCORES_TYPE_POINTS; + // map[HIGHSCORES_CATEGORY_DEATHS] = HIGHSCORES_TYPE_POINTS; + return map[type]; + } +}; + +enum HighscoresAction : uint8_t +{ + HIGHSCORES_ACTION_BROWSE = 0x00, + HIGHSCORES_ACTION_OWN = 0x01 +}; + +struct HighscoresParams +{ + std::string world; + uint32_t vocation = 0xFFFFFFFF; + uint32_t category = HIGHSCORES_CATEGORY_FIRST; + uint8_t type = 0; + uint8_t battlEye = 0xFF; + uint16_t page = 1; + uint16_t totalPages = 0; + uint64_t timestamp = 0; +}; + +struct HighscoresEntry +{ + HighscoresEntry(uint32_t id, uint32_t rank, std::string name, std::string title, uint8_t vocation, + std::string world, uint16_t level, uint64_t points) : + id(id), + rank(rank), + name(std::move(name)), + title(std::move(title)), + vocation(vocation), + world(std::move(world)), + level(level), + points(points) + {} + + uint32_t id = 0; + uint32_t rank = 0; + std::string name; + std::string title; + uint8_t vocation = VOCATION_NONE; + std::string world; + uint16_t level = 1; + uint64_t points = 0; +}; + +#endif // FS_HIGHSCORES_H diff --git a/src/iologindata.cpp b/src/iologindata.cpp index c2e4aef1f7..fd57db5b25 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -1107,3 +1107,14 @@ void IOLoginData::updatePremiumTime(uint32_t accountId, time_t endTime) Database::getInstance().executeQuery( fmt::format("UPDATE `accounts` SET `premium_ends_at` = {:d} WHERE `id` = {:d}", endTime, accountId)); } + +uint32_t IOLoginData::getTotalExistingPlayers() +{ + Database& db = Database::getInstance(); + + DBResult_ptr result = db.storeQuery("SELECT COUNT(*) AS `players_total` FROM `players` WHERE `deletion` = 0 AND `group_id` NOT IN (4, 5, 6)"); + if (!result) { + return 0; + } + return result->getNumber("players_total"); +} diff --git a/src/iologindata.h b/src/iologindata.h index 169257834f..399d4895d3 100644 --- a/src/iologindata.h +++ b/src/iologindata.h @@ -50,6 +50,8 @@ class IOLoginData static void updatePremiumTime(uint32_t accountId, time_t endTime); + static uint32_t getTotalExistingPlayers(); + private: using ItemMap = std::map>; diff --git a/src/luascript.cpp b/src/luascript.cpp index 2c79bc0900..c652a8ddc6 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -2072,6 +2072,19 @@ void LuaScriptInterface::registerFunctions() registerEnum(DECAYING_TRUE); registerEnum(DECAYING_PENDING); + registerEnum(HIGHSCORES_CATEGORY_LEVEL); + registerEnum(HIGHSCORES_CATEGORY_MAGLEVEL); + registerEnum(HIGHSCORES_CATEGORY_FIST_FIGHTING); + registerEnum(HIGHSCORES_CATEGORY_AXE_FIGHTING); + registerEnum(HIGHSCORES_CATEGORY_CLUB_FIGHTING); + registerEnum(HIGHSCORES_CATEGORY_SWORD_FIGHTING); + registerEnum(HIGHSCORES_CATEGORY_DISTANCE_FIGHTING); + registerEnum(HIGHSCORES_CATEGORY_SHIELDING); + registerEnum(HIGHSCORES_CATEGORY_FISHING); + registerEnum(HIGHSCORES_CATEGORY_ACHIEVEMENTS); + registerEnum(HIGHSCORES_CATEGORY_LOYALTY); + // registerEnum(HIGHSCORES_CATEGORY_DEATHS); + // _G registerGlobalVariable("INDEX_WHEREEVER", INDEX_WHEREEVER); registerGlobalBoolean("VIRTUAL_PARENT", true); diff --git a/src/player.h b/src/player.h index 8aa5223246..6bf0c1c281 100644 --- a/src/player.h +++ b/src/player.h @@ -15,6 +15,7 @@ #include "vocation.h" class DepotChest; +class Game; class House; class NetworkMessage; class Npc; @@ -1052,6 +1053,12 @@ class Player final : public Creature, public Cylinder client->sendAddMarker(pos, markType, desc); } } + void sendHighscores(std::vector entries, const HighscoresParams& params) + { + if (client) { + client->sendHighscores(entries, params); + } + } void sendQuestLog() { if (client) { diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp index 72fad64582..ee9936f864 100644 --- a/src/protocolgame.cpp +++ b/src/protocolgame.cpp @@ -21,17 +21,21 @@ #include "podium.h" #include "scheduler.h" #include "storeinbox.h" +#include "vocation.h" extern ConfigManager g_config; extern Actions actions; extern CreatureEvents* g_creatureEvents; extern Chat* g_chat; +extern Vocations g_vocations; namespace { std::deque> waitList; // (timeout, player guid) auto priorityEnd = waitList.end(); +const std::map highscoresCategories = Highscores::init(); + auto findClient(uint32_t guid) { std::size_t slot = 1; @@ -741,7 +745,9 @@ void ProtocolGame::parsePacket(NetworkMessage& msg) case 0xAC: parseChannelExclude(msg); break; - // case 0xB1: break; // request highscores + case 0xB1: + parseRequestHighscores(msg); + break; case 0xBE: addGameTask([playerID = player->getID()]() { g_game.playerCancelAttackAndFollow(playerID); }); break; @@ -1641,6 +1647,30 @@ void ProtocolGame::parseSeekInContainer(NetworkMessage& msg) addGameTask([=, playerID = player->getID()]() { g_game.playerSeekInContainer(playerID, containerId, index); }); } +void ProtocolGame::parseRequestHighscores(NetworkMessage& msg) +{ + HighscoresAction action = static_cast(msg.getByte()); + if (action == HIGHSCORES_ACTION_OWN) { + // NOT IMPLEMENTED YET + sendEmptyHighscores(); + return; + } + + uint8_t category = msg.getByte(); + uint32_t vocation = msg.get(); + std::string world = msg.getString(); + + msg.getByte(); // type? + msg.getByte(); // battlEye + uint16_t page = msg.get(); + + // uint8_t items = msg.getByte(); // 20 + + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, [=, playerID = player->getID(), world = std::move(world)]() { + g_game.playerShowHighscores(playerID, category, vocation, world, page); + }); +} + // Send methods void ProtocolGame::sendOpenPrivateChannel(const std::string& receiver) { @@ -2424,6 +2454,76 @@ void ProtocolGame::sendMarketBrowseOwnHistory(const HistoryMarketOfferList& buyO writeToOutputBuffer(msg); } +void ProtocolGame::sendEmptyHighscores() +{ + NetworkMessage msg; + msg.addByte(0xB1); + msg.addByte(HIGHSCORES_ACTION_OWN); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHighscores(std::vector entries, const HighscoresParams& params) +{ + NetworkMessage msg; + msg.addByte(0xB1); + + msg.addByte(HIGHSCORES_ACTION_BROWSE); + + // WORLDS BLOCK + msg.addByte(0x01); // WORLDS COUNT + msg.addString(g_config.getString(ConfigManager::SERVER_NAME)); // DEFAULT + msg.addString(params.world); // SELECTED + msg.addByte(params.type); // WORLD TYPE + msg.addByte(params.battlEye); // WORLD BATTLEYE + // WORLDS BLOCK + + // VOCATIONS BLOCK + const auto& vocations = g_vocations.getBaseVocations(); + msg.addByte(vocations.size() + 1); // SIZE + msg.add(0xFFFFFFFF); // DEFAULT ID + msg.addString("All Vocations"); // DEFAULT NAME + for (auto& [id, vocation] : vocations) { + msg.add(id); // ID + msg.addString(vocation.getVocName()); // NAME + } + msg.add(params.vocation); // SELECTED + // VOCATIONS BLOCK + + // CATEGORIES BLOCK + msg.addByte(highscoresCategories.size()); // SIZE + for (const auto& category : highscoresCategories) { + msg.addByte(category.first); // ID + msg.addString(category.second); // NAME + } + + HighscoresCategoryTypes categoryType = static_cast(params.category); + msg.addByte(categoryType); // SELECTED + // CATEGORIES BLOCK + + msg.add(params.page); // CURRENT PAGE + msg.add(params.totalPages); // TOTAL PAGES + + msg.addByte(entries.size()); + for (const auto& entry : entries) { + msg.add(entry.rank); + msg.addString(entry.name); + msg.addString(""); + msg.addByte(entry.vocation); + msg.addString(entry.world); + msg.add(entry.level); + msg.addByte(player->getGUID() == entry.id ? 0x01 : 0x00); + msg.add(entry.points); + } + + msg.addByte(0xFF); // UNKNOWN + msg.addByte(categoryType == HIGHSCORES_CATEGORY_LOYALTY ? 0x01 : 0x00); + msg.addByte(Highscores::getType(categoryType)); + + msg.add(params.timestamp); + + writeToOutputBuffer(msg); +} + void ProtocolGame::sendQuestLog() { NetworkMessage msg; diff --git a/src/protocolgame.h b/src/protocolgame.h index 53baa7ea6b..ded57d5bc4 100644 --- a/src/protocolgame.h +++ b/src/protocolgame.h @@ -6,6 +6,7 @@ #include "chat.h" #include "creature.h" +#include "highscores.h" #include "protocol.h" #include "tasks.h" @@ -162,6 +163,8 @@ class ProtocolGame final : public Protocol void parseOpenPrivateChannel(NetworkMessage& msg); void parseCloseChannel(NetworkMessage& msg); + void parseRequestHighscores(NetworkMessage& msg); + // Send functions void sendChannelMessage(const std::string& author, const std::string& text, SpeakClasses type, uint16_t channel); void sendChannelEvent(uint16_t channelId, const std::string& playerName, ChannelEvent_t channelEvent); @@ -186,6 +189,9 @@ class ProtocolGame final : public Protocol void sendCreatureSay(const Creature* creature, SpeakClasses type, const std::string& text, const Position* pos = nullptr); + void sendEmptyHighscores(); + void sendHighscores(std::vector entries, const HighscoresParams& params); + void sendQuestLog(); void sendQuestLine(const Quest* quest); void sendQuestTracker(); diff --git a/src/vocation.cpp b/src/vocation.cpp index 0be58ba130..7727bb0b57 100644 --- a/src/vocation.cpp +++ b/src/vocation.cpp @@ -137,6 +137,17 @@ uint16_t Vocations::getPromotedVocation(uint16_t id) const return it != vocationsMap.end() ? it->first : VOCATION_NONE; } +std::map Vocations::getBaseVocations() +{ + std::map vocations; + for (auto& [id, vocation] : vocationsMap) { + if (id == vocation.fromVocation) { + vocations.emplace(id, vocation); + } + } + return vocations; +} + static const uint32_t skillBase[SKILL_LAST + 1] = {50, 50, 50, 50, 30, 100, 20}; uint64_t Vocation::getReqSkillTries(uint8_t skill, uint16_t level) diff --git a/src/vocation.h b/src/vocation.h index cf4f5f63f9..a8487bce81 100644 --- a/src/vocation.h +++ b/src/vocation.h @@ -85,6 +85,8 @@ class Vocations int32_t getVocationId(const std::string& name) const; uint16_t getPromotedVocation(uint16_t vocationId) const; + std::map getBaseVocations(); + private: std::map vocationsMap; }; diff --git a/vc17/theforgottenserver.vcxproj b/vc17/theforgottenserver.vcxproj index 2150b7c141..73adc2d534 100644 --- a/vc17/theforgottenserver.vcxproj +++ b/vc17/theforgottenserver.vcxproj @@ -272,6 +272,7 @@ + @@ -337,4 +338,4 @@ - + \ No newline at end of file