diff --git a/cmake/modules/BaseConfig.cmake b/cmake/modules/BaseConfig.cmake index 14dc6e22153..0dc74b85ae0 100644 --- a/cmake/modules/BaseConfig.cmake +++ b/cmake/modules/BaseConfig.cmake @@ -36,6 +36,7 @@ find_package(pugixml CONFIG REQUIRED) find_package(spdlog REQUIRED) find_package(unofficial-argon2 CONFIG REQUIRED) find_package(unofficial-libmariadb CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) find_path(BOOST_DI_INCLUDE_DIRS "boost/di.hpp") diff --git a/cmake/modules/CanaryLib.cmake b/cmake/modules/CanaryLib.cmake index 916970c37ea..2aa803484aa 100644 --- a/cmake/modules/CanaryLib.cmake +++ b/cmake/modules/CanaryLib.cmake @@ -86,6 +86,7 @@ target_link_libraries(${PROJECT_NAME}_lib unofficial::argon2::libargon2 unofficial::libmariadb protobuf + nlohmann_json::nlohmann_json ) if(FEATURE_METRICS) diff --git a/src/game/game.cpp b/src/game/game.cpp index e69654a47ad..af0697a78db 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -10882,3 +10882,32 @@ void Game::updatePlayersOnline() const { g_logger().error("[Game::updatePlayersOnline] Failed to update players online."); } } + +std::map>> Game::groupPlayersByIP() { + std::map>> groupedPlayers; + + for (const auto &player : g_game().getPlayers() | std::views::values) { + uint32_t ip = player->getIP(); + if (ip != 0 && player->idleTime <= 900000) { + groupedPlayers[ip].emplace_back(player); + } + } + + return groupedPlayers; +} + +PlayerStats Game::getPlayerStats() { + auto groupedPlayers = groupPlayersByIP(); + + uint32_t totalUniqueIPs = 0; + uint32_t totalPlayers = 0; + + for (const auto &players : groupedPlayers | std::views::values) { + totalUniqueIPs++; + + int activePlayers = std::min(static_cast(players.size()), 4); + totalPlayers += activePlayers; + } + + return { totalPlayers, totalUniqueIPs }; +} diff --git a/src/game/game.hpp b/src/game/game.hpp index b1afd810100..974dcd23aef 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -85,6 +85,11 @@ struct HighscoreCacheEntry { std::chrono::time_point timestamp; }; +struct PlayerStats { + uint32_t totalPlayers; + uint32_t totalUniqueIPs; +}; + class Game { public: Game(); @@ -713,6 +718,8 @@ class Game { const std::unordered_map &getHirelingSkills(); const std::unordered_map &getHirelingOutfits(); + PlayerStats getPlayerStats(); + private: std::map m_achievements; std::map m_achievementsNameToId; @@ -948,6 +955,7 @@ class Game { std::string generateHighscoreOrGetCachedQueryForOurRank(const std::string &categoryName, uint8_t entriesPerPage, uint32_t playerGUID, uint32_t vocation); void updatePlayersOnline() const; + std::map>> groupPlayersByIP(); }; constexpr auto g_game = Game::getInstance; diff --git a/src/lua/functions/core/game/global_functions.cpp b/src/lua/functions/core/game/global_functions.cpp index 6eb21910701..9d91aad3af1 100644 --- a/src/lua/functions/core/game/global_functions.cpp +++ b/src/lua/functions/core/game/global_functions.cpp @@ -263,7 +263,8 @@ int GlobalFunctions::luaGetWorldLight(lua_State* L) { int GlobalFunctions::luaGetWorldUpTime(lua_State* L) { // getWorldUpTime() - const uint64_t uptime = (OTSYS_TIME(true) - ProtocolStatus::start) / 1000; + const auto now = std::chrono::steady_clock::now(); + const auto uptime = std::chrono::duration_cast(now - ProtocolStatus::start).count(); lua_pushnumber(L, uptime); return 1; } diff --git a/src/lua/functions/core/network/network_message_functions.cpp b/src/lua/functions/core/network/network_message_functions.cpp index bfd3b86897e..5214b5f8a31 100644 --- a/src/lua/functions/core/network/network_message_functions.cpp +++ b/src/lua/functions/core/network/network_message_functions.cpp @@ -11,9 +11,10 @@ #include "server/network/protocol/protocolgame.hpp" #include "creatures/players/player.hpp" -#include "server/network/protocol/protocolstatus.hpp" #include "lua/functions/lua_functions_loader.hpp" +#include + void NetworkMessageFunctions::init(lua_State* L) { Lua::registerSharedClass(L, "NetworkMessage", "", NetworkMessageFunctions::luaNetworkMessageCreate); Lua::registerMetaMethod(L, "NetworkMessage", "__eq", Lua::luaUserdataCompare); diff --git a/src/server/network/protocol/protocolstatus.cpp b/src/server/network/protocol/protocolstatus.cpp index fd2bd2968aa..e3fa3e62af5 100644 --- a/src/server/network/protocol/protocolstatus.cpp +++ b/src/server/network/protocol/protocolstatus.cpp @@ -15,33 +15,32 @@ #include "game/game.hpp" #include "game/scheduling/dispatcher.hpp" #include "server/network/message/outputmessage.hpp" +#include -std::string ProtocolStatus::SERVER_NAME = "Canary"; -std::string ProtocolStatus::SERVER_VERSION = "3.0"; -std::string ProtocolStatus::SERVER_DEVELOPERS = "OpenTibiaBR Organization"; - -std::map ProtocolStatus::ipConnectMap; -const uint64_t ProtocolStatus::start = OTSYS_TIME(true); - -void ProtocolStatus::onRecvFirstMessage(NetworkMessage &msg) { +void ProtocolStatus::onRecvFirstMessage(NetworkMessage &msg) noexcept { const uint32_t ip = getIP(); if (ip != 0x0100007F) { - const std::string ipStr = convertIPToString(ip); + const auto ipStr = convertIPToString(ip); if (ipStr != g_configManager().getString(IP)) { const auto it = ipConnectMap.find(ip); - if (it != ipConnectMap.end() && (OTSYS_TIME() < (it->second + g_configManager().getNumber(STATUSQUERY_TIMEOUT)))) { - disconnect(); - return; + if (it != ipConnectMap.end()) { + const auto now = std::chrono::steady_clock::now(); + const auto timeout = std::chrono::milliseconds(g_configManager().getNumber(STATUSQUERY_TIMEOUT)); + if (now < (it->second + timeout)) { + disconnect(); + return; + } } } } - ipConnectMap[ip] = OTSYS_TIME(); + ipConnectMap[ip] = std::chrono::steady_clock::now(); switch (msg.getByte()) { // XML info protocol case 0xFF: { - if (msg.getString(4) == "info") { + const auto command = msg.getString(4); + if (command == "info") { g_dispatcher().addEvent( [self = std::static_pointer_cast(shared_from_this())] { self->sendStatusString(); @@ -50,23 +49,35 @@ void ProtocolStatus::onRecvFirstMessage(NetworkMessage &msg) { ); return; } + + if (command == "json") { + g_dispatcher().addEvent( + [self = std::static_pointer_cast(shared_from_this())] { + self->sendStatusStringJson(); + }, + __FUNCTION__ + ); + return; + } break; } // Another ServerInfo protocol case 0x01: { - auto requestedInfo = msg.get(); // only a Byte is necessary, though we could add new info here + const auto requestedInfo = msg.get(); std::string characterName; if (requestedInfo & REQUEST_PLAYER_STATUS_INFO) { characterName = msg.getString(); } + g_dispatcher().addEvent( - [self = std::static_pointer_cast(shared_from_this()), requestedInfo, characterName] { + [self = std::static_pointer_cast(shared_from_this()), + requestedInfo, + characterName = std::move(characterName)] { self->sendInfo(requestedInfo, characterName); }, __FUNCTION__ ); - return; } @@ -89,67 +100,68 @@ void ProtocolStatus::sendStatusString() { pugi::xml_node tsqp = doc.append_child("tsqp"); tsqp.append_attribute("version") = "1.0"; + const auto now = std::chrono::steady_clock::now(); + const auto uptime = std::chrono::duration_cast(now - start).count(); + const auto clientVersion = fmt::format("{}.{}", CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER); + + // Server Information + auto &config = g_configManager(); pugi::xml_node serverinfo = tsqp.append_child("serverinfo"); - const uint64_t uptime = (OTSYS_TIME() - ProtocolStatus::start) / 1000; serverinfo.append_attribute("uptime") = std::to_string(uptime).c_str(); - serverinfo.append_attribute("ip") = g_configManager().getString(IP).c_str(); - serverinfo.append_attribute("servername") = g_configManager().getString(ConfigKey_t::SERVER_NAME).c_str(); - serverinfo.append_attribute("port") = std::to_string(g_configManager().getNumber(LOGIN_PORT)).c_str(); - serverinfo.append_attribute("location") = g_configManager().getString(LOCATION).c_str(); - serverinfo.append_attribute("url") = g_configManager().getString(URL).c_str(); - serverinfo.append_attribute("server") = ProtocolStatus::SERVER_NAME.c_str(); - serverinfo.append_attribute("version") = ProtocolStatus::SERVER_VERSION.c_str(); - serverinfo.append_attribute("client") = fmt::format("{}.{}", CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER).c_str(); - + serverinfo.append_attribute("ip") = config.getString(IP).c_str(); + serverinfo.append_attribute("servername") = config.getString(ConfigKey_t::SERVER_NAME).c_str(); + serverinfo.append_attribute("port") = std::to_string(config.getNumber(LOGIN_PORT)).c_str(); + serverinfo.append_attribute("location") = config.getString(LOCATION).c_str(); + serverinfo.append_attribute("url") = config.getString(URL).c_str(); + serverinfo.append_attribute("server") = SERVER_NAME.c_str(); + serverinfo.append_attribute("version") = SERVER_VERSION.c_str(); + serverinfo.append_attribute("client") = clientVersion.c_str(); + + // Owner Information pugi::xml_node owner = tsqp.append_child("owner"); - owner.append_attribute("name") = g_configManager().getString(OWNER_NAME).c_str(); - owner.append_attribute("email") = g_configManager().getString(OWNER_EMAIL).c_str(); + owner.append_attribute("name") = config.getString(OWNER_NAME).c_str(); + owner.append_attribute("email") = config.getString(OWNER_EMAIL).c_str(); + // Player Statistics + const auto &[totalPlayers, totalUniqueIPs] = g_game().getPlayerStats(); pugi::xml_node players = tsqp.append_child("players"); - uint32_t real = 0; - std::map listIP; - for (const auto &[key, player] : g_game().getPlayers()) { - if (player->getIP() != 0) { - auto ip = listIP.find(player->getIP()); - if (ip != listIP.end()) { - listIP[player->getIP()]++; - if (listIP[player->getIP()] < 5) { - real++; - } - } else { - listIP[player->getIP()] = 1; - real++; - } - } - } - players.append_attribute("online") = std::to_string(real).c_str(); - players.append_attribute("max") = std::to_string(g_configManager().getNumber(MAX_PLAYERS)).c_str(); + players.append_attribute("online") = std::to_string(totalPlayers).c_str(); + players.append_attribute("unique") = std::to_string(totalUniqueIPs).c_str(); + players.append_attribute("max") = std::to_string(config.getNumber(MAX_PLAYERS)).c_str(); players.append_attribute("peak") = std::to_string(g_game().getPlayersRecord()).c_str(); + // Monster and NPC Statistics pugi::xml_node monsters = tsqp.append_child("monsters"); monsters.append_attribute("total") = std::to_string(g_game().getMonstersOnline()).c_str(); pugi::xml_node npcs = tsqp.append_child("npcs"); npcs.append_attribute("total") = std::to_string(g_game().getNpcsOnline()).c_str(); + // Rates Information pugi::xml_node rates = tsqp.append_child("rates"); - rates.append_attribute("experience") = std::to_string(g_configManager().getNumber(RATE_EXPERIENCE)).c_str(); - rates.append_attribute("skill") = std::to_string(g_configManager().getNumber(RATE_SKILL)).c_str(); - rates.append_attribute("loot") = std::to_string(g_configManager().getNumber(RATE_LOOT)).c_str(); - rates.append_attribute("magic") = std::to_string(g_configManager().getNumber(RATE_MAGIC)).c_str(); - rates.append_attribute("spawn") = std::to_string(g_configManager().getNumber(RATE_SPAWN)).c_str(); - - pugi::xml_node map = tsqp.append_child("map"); - map.append_attribute("name") = g_configManager().getString(MAP_NAME).c_str(); - map.append_attribute("author") = g_configManager().getString(MAP_AUTHOR).c_str(); + std::vector> rateAttributes = { + { "experience", config.getNumber(RATE_EXPERIENCE) }, + { "skill", config.getNumber(RATE_SKILL) }, + { "loot", config.getNumber(RATE_LOOT) }, + { "magic", config.getNumber(RATE_MAGIC) }, + { "spawn", config.getNumber(RATE_SPAWN) } + }; + for (const auto &[name, value] : rateAttributes) { + rates.append_attribute(name) = std::to_string(value).c_str(); + } + // Map Information uint32_t mapWidth, mapHeight; g_game().getMapDimensions(mapWidth, mapHeight); + pugi::xml_node map = tsqp.append_child("map"); + map.append_attribute("name") = config.getString(MAP_NAME).c_str(); + map.append_attribute("author") = config.getString(MAP_AUTHOR).c_str(); map.append_attribute("width") = std::to_string(mapWidth).c_str(); map.append_attribute("height") = std::to_string(mapHeight).c_str(); + // Message of the Day pugi::xml_node motd = tsqp.append_child("motd"); - motd.text() = g_configManager().getString(SERVER_MOTD).c_str(); + motd.text() = config.getString(SERVER_MOTD).c_str(); std::ostringstream ss; doc.save(ss, "", pugi::format_raw); @@ -160,6 +172,78 @@ void ProtocolStatus::sendStatusString() { disconnect(); } +void ProtocolStatus::sendStatusStringJson() { + const auto output = OutputMessagePool::getOutputMessage(); + + setRawMessages(true); + + nlohmann::json statusJson; + + // Server Information + const auto serverName = g_configManager().getString(ConfigKey_t::SERVER_NAME); + const auto serverURL = g_configManager().getString(URL); + statusJson["serverinfo"] = { + { "uptime", std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count() }, + { "ip", g_configManager().getString(IP) }, + { "servername", serverName }, + { "port", g_configManager().getNumber(LOGIN_PORT) }, + { "location", g_configManager().getString(LOCATION) }, + { "url", serverURL }, + { "server", SERVER_NAME }, + { "version", SERVER_VERSION }, + { "client", fmt::format("{}.{}", CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER) } + }; + + // Owner Information + statusJson["owner"] = { + { "name", g_configManager().getString(OWNER_NAME) }, + { "email", g_configManager().getString(OWNER_EMAIL) } + }; + + // Players Information + const auto &[totalPlayers, totalUniqueIPs] = g_game().getPlayerStats(); + statusJson["players"] = { + { "online", totalPlayers }, + { "player_unique", totalUniqueIPs }, + { "max", g_configManager().getNumber(MAX_PLAYERS) }, + { "peak", g_game().getPlayersRecord() } + }; + + // Monsters and NPCs + statusJson["monsters"] = { { "total", g_game().getMonstersOnline() } }; + statusJson["npcs"] = { { "total", g_game().getNpcsOnline() } }; + + // Rates Information + statusJson["rates"] = { + { "experience", g_configManager().getNumber(RATE_EXPERIENCE) }, + { "skill", g_configManager().getNumber(RATE_SKILL) }, + { "loot", g_configManager().getNumber(RATE_LOOT) }, + { "magic", g_configManager().getNumber(RATE_MAGIC) }, + { "spawn", g_configManager().getNumber(RATE_SPAWN) } + }; + + // Map Information + uint32_t mapWidth, mapHeight; + g_game().getMapDimensions(mapWidth, mapHeight); + statusJson["map"] = { + { "name", g_configManager().getString(MAP_NAME) }, + { "author", g_configManager().getString(MAP_AUTHOR) }, + { "width", mapWidth }, + { "height", mapHeight } + }; + + // Message of the Day + statusJson["motd"] = g_configManager().getString(SERVER_MOTD); + + // Serialize JSON to string + const std::string data = statusJson.dump(); + + // Send JSON data + output->addBytes(data.data(), data.size()); + send(output); + disconnect(); +} + void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &characterName) const { const auto output = OutputMessagePool::getOutputMessage(); @@ -181,7 +265,7 @@ void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &charact output->addString(g_configManager().getString(SERVER_MOTD)); output->addString(g_configManager().getString(LOCATION)); output->addString(g_configManager().getString(URL)); - output->add((OTSYS_TIME() - ProtocolStatus::start) / 1000); + output->add(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); } if (requestedInfo & REQUEST_PLAYERS_INFO) { @@ -206,9 +290,9 @@ void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &charact const auto players = g_game().getPlayers(); output->add(players.size()); - for (const auto &it : players) { - output->addString(it.second->getName()); - output->add(it.second->getLevel()); + for (const auto &player : players | std::views::values) { + output->addString(player->getName()); + output->add(player->getLevel()); } } @@ -223,8 +307,8 @@ void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &charact if (requestedInfo & REQUEST_SERVER_SOFTWARE_INFO) { output->addByte(0x23); // server software info - output->addString(ProtocolStatus::SERVER_NAME); - output->addString(ProtocolStatus::SERVER_VERSION); + output->addString(SERVER_NAME); + output->addString(SERVER_VERSION); output->addString(fmt::format("{}.{}", CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER)); } send(output); diff --git a/src/server/network/protocol/protocolstatus.hpp b/src/server/network/protocol/protocolstatus.hpp index d5600438734..455926459f8 100644 --- a/src/server/network/protocol/protocolstatus.hpp +++ b/src/server/network/protocol/protocolstatus.hpp @@ -9,33 +9,32 @@ #pragma once -#include "server/network/message/networkmessage.hpp" #include "server/network/protocol/protocol.hpp" class ProtocolStatus final : public Protocol { public: // static protocol information - enum { SERVER_SENDS_FIRST = false }; - enum { PROTOCOL_IDENTIFIER = 0xFF }; - enum { USE_CHECKSUM = false }; - static const char* protocol_name() { + static constexpr bool SERVER_SENDS_FIRST = false; + static constexpr uint8_t PROTOCOL_IDENTIFIER = 0xFF; + static constexpr bool USE_CHECKSUM = false; + static constexpr const char* protocol_name() noexcept { return "status protocol"; } - explicit ProtocolStatus(const Connection_ptr &conn) : + explicit ProtocolStatus(const Connection_ptr &conn) noexcept : Protocol(conn) { } - void onRecvFirstMessage(NetworkMessage &msg) override; + void onRecvFirstMessage(NetworkMessage &msg) noexcept override; void sendStatusString(); + void sendStatusStringJson(); void sendInfo(uint16_t requestedInfo, const std::string &characterName) const; - static const uint64_t start; - - static std::string SERVER_NAME; - static std::string SERVER_VERSION; - static std::string SERVER_DEVELOPERS; + static inline const auto start = std::chrono::steady_clock::now(); + static inline const std::string SERVER_NAME = "Canary"; + static inline const std::string SERVER_VERSION = "3.0"; + static inline const std::string SERVER_DEVELOPERS = "OpenTibiaBR Organization"; private: - static std::map ipConnectMap; + static inline phmap::flat_hash_map ipConnectMap {}; }; diff --git a/vcpkg.json b/vcpkg.json index e5a83b21263..fdcffbbc9b6 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -20,6 +20,7 @@ "zlib", "bshoshany-thread-pool", "atomic-queue", + "nlohmann-json", { "name": "opentelemetry-cpp", "default-features": true,