diff --git a/Config.ini b/Config.ini index 6f9d19f..c5d495b 100644 --- a/Config.ini +++ b/Config.ini @@ -655,24 +655,20 @@ Part.FormatNoReason={NAME} has left {CHAN}! [RenX.MinPlayers] -; [SetJoins] -; Join messages are stored here. -; Note: Usage of the "setjoin" command syncs the memory-stored -; file to the drive, which will destroy all comments in the process. -; -; String(User)=String(SetJoin) +; [RenX.SetJoin] +; SetJoinFile=String(Default: RenX.SetJoin.ini) ; -[SetJoins] +[RenX.SetJoin] -; [RenX.SetJoin] -; Renenegade-X game join messages are stored here. +; [SetJoins] +; Join messages are stored here. ; Note: Usage of the "setjoin" command syncs the memory-stored ; file to the drive, which will destroy all comments in the process. ; ; String(User)=String(SetJoin) ; -[RenX.SetJoin] +[SetJoins] ;EOF \ No newline at end of file diff --git a/Jupiter b/Jupiter index e656b6f..87f1b47 160000 --- a/Jupiter +++ b/Jupiter @@ -1 +1 @@ -Subproject commit e656b6f6ba3614b57bc19cb4c4835801e33daf8e +Subproject commit 87f1b47149f7974ab5faa81b8b9d3c4b26b68838 diff --git a/Jupiter Bot.sln b/Jupiter Bot.sln index ad75497..7baa86c 100644 --- a/Jupiter Bot.sln +++ b/Jupiter Bot.sln @@ -210,6 +210,13 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RenX.Ladder.Daily", "RenX.L {9103DF3D-8B4A-48E5-A6B3-CBE2554630E2} = {9103DF3D-8B4A-48E5-A6B3-CBE2554630E2} EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RenX.ServerList", "RenX.ServerList\RenX.ServerList.vcxproj", "{6B0D59BA-B153-4DE8-8DD4-FBE5D810B033}" + ProjectSection(ProjectDependencies) = postProject + {C188871B-5F32-4946-B301-24CA2EBB275D} = {C188871B-5F32-4946-B301-24CA2EBB275D} + {9103DF3D-8B4A-48E5-A6B3-CBE2554630E2} = {9103DF3D-8B4A-48E5-A6B3-CBE2554630E2} + {BB048D6F-F001-4E9B-95F4-886081E0807A} = {BB048D6F-F001-4E9B-95F4-886081E0807A} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -360,6 +367,10 @@ Global {73F0EEF6-CE5E-46EB-80B4-2B319AE2B258}.Debug|Win32.Build.0 = Debug|Win32 {73F0EEF6-CE5E-46EB-80B4-2B319AE2B258}.Release|Win32.ActiveCfg = Release|Win32 {73F0EEF6-CE5E-46EB-80B4-2B319AE2B258}.Release|Win32.Build.0 = Release|Win32 + {6B0D59BA-B153-4DE8-8DD4-FBE5D810B033}.Debug|Win32.ActiveCfg = Debug|Win32 + {6B0D59BA-B153-4DE8-8DD4-FBE5D810B033}.Debug|Win32.Build.0 = Debug|Win32 + {6B0D59BA-B153-4DE8-8DD4-FBE5D810B033}.Release|Win32.ActiveCfg = Release|Win32 + {6B0D59BA-B153-4DE8-8DD4-FBE5D810B033}.Release|Win32.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Release/Bot.lib b/Release/Bot.lib index 45ef926..056c160 100644 Binary files a/Release/Bot.lib and b/Release/Bot.lib differ diff --git a/Release/Plugins/RenX.Core.lib b/Release/Plugins/RenX.Core.lib index e24f701..697b720 100644 Binary files a/Release/Plugins/RenX.Core.lib and b/Release/Plugins/RenX.Core.lib differ diff --git a/RenX.Core/RenX.Core.vcxproj b/RenX.Core/RenX.Core.vcxproj index 896baa5..cf248c7 100644 --- a/RenX.Core/RenX.Core.vcxproj +++ b/RenX.Core/RenX.Core.vcxproj @@ -83,6 +83,7 @@ + diff --git a/RenX.Core/RenX.Core.vcxproj.filters b/RenX.Core/RenX.Core.vcxproj.filters index b1c8e83..7783403 100644 --- a/RenX.Core/RenX.Core.vcxproj.filters +++ b/RenX.Core/RenX.Core.vcxproj.filters @@ -62,6 +62,9 @@ Header Files + + Header Files + diff --git a/RenX.Core/RenX_Functions.cpp b/RenX.Core/RenX_Functions.cpp index b15b637..0c87e69 100644 --- a/RenX.Core/RenX_Functions.cpp +++ b/RenX.Core/RenX_Functions.cpp @@ -44,7 +44,8 @@ Jupiter::ReferenceString GDILongName = "Global Defense Initiative"; Jupiter::ReferenceString OtherLongName = "Unknown"; /** RenegadeX RCON protocol message deliminator */ -const char RenX::DelimC = '\xA0'; +const char RenX::DelimC = '\x02'; +const char RenX::DelimC3 = '\xA0'; const Jupiter::ReferenceString RenX::DevBotName = "DevBot"_jrs; /** WinType translations */ @@ -877,16 +878,6 @@ Jupiter::StringS RenX::formatGUID(const RenX::Map &map) return Jupiter::StringS::Format("%.16llX%.16llX", map.guid[0], map.guid[1]); } -void RenX::sanitizeString(Jupiter::StringType &str) -{ - if (str.isNotEmpty()) - { - str.replace('|', '/'); - if (str.get(str.size() - 1) == '\\') - str.set(str.size() - 1, '/'); - } -} - std::chrono::milliseconds RenX::getServerTime(const RenX::PlayerInfo *player) { return std::chrono::duration_cast(std::chrono::steady_clock::now() - player->joinTime); @@ -911,4 +902,81 @@ double RenX::getHeadshotKillRatio(const RenX::PlayerInfo *player) { if (player->kills == 0) return 0; return ((double)player->headshots) / ((double)player->kills); -} \ No newline at end of file +} + +Jupiter::String RenX::escapifyRCON(const Jupiter::ReadableString &str) +{ + const char *ptr = str.ptr(); + size_t length = str.size(); + Jupiter::String result(str.size() + 32); + uint16_t value; + + while (length != 0) + { + if ((*ptr & 0x80) != 0) // UTF-8 sequence + { + if (length < 2) + break; + + if ((*ptr & 0x40) != 0) // validity check + { + // get codepoint value + if ((*ptr & 0x20) != 0) + { + if (length < 3) + break; + + if ((*ptr & 0x10) != 0) // This is a 4 byte sequence, which we can not fit into a 16-bit codepoint. ignore it. + { + if (length < 4) + break; + + ptr += 4; + length -= 4; + continue; + } + else + { + // this is a 3 byte sequence + value = (*ptr & 0x0F) << 12; + value += (*++ptr & 0x3F) << 6; + value += *++ptr & 0x3F; + + length -= 3; + } + } + else + { + // This is a 2 byte sequence + value = (*ptr & 0x1F) << 6; + value += *++ptr & 0x3F; + + length -= 2; + } + + // write escape sequence + result += '\\'; + result += 'u'; + result += Jupiter_asHex_upper(value >> 8); + result += Jupiter_asHex_upper(value & 0x00FF); + + printf(ENDL ENDL ENDL "\\u%x%x" ENDL ENDL ENDL ENDL, value >> 8, value & 0x00FF); + } + // else // This is an invalid 1 byte sequence + } + else if (*ptr == '\\') // backslash, which is used for escape sequencing + { + result += '\\'; + result += '\\'; + --length; + } + else // an ordinary character + { + result += *ptr; + --length; + } + + ++ptr; + } + return result; +} diff --git a/RenX.Core/RenX_Functions.h b/RenX.Core/RenX_Functions.h index 18084a1..8f6b920 100644 --- a/RenX.Core/RenX_Functions.h +++ b/RenX.Core/RenX_Functions.h @@ -145,15 +145,6 @@ namespace RenX */ RENX_API Jupiter::StringS formatGUID(const RenX::Map &map); - /** - * @brief Sanitizes a string into a RCON-ready state by replacing special - * characters with HTML-style character codes. - * Note: This resolves the pipe character ('|') exploit. - * - * @brief str String to sanitize. - */ - RENX_API void sanitizeString(Jupiter::StringType &str); - /** * @brief Calculates for how many seconds a player has been in the server. * @@ -190,8 +181,17 @@ namespace RenX */ RENX_API double getHeadshotKillRatio(const RenX::PlayerInfo *player); + /** + * @brief Escapifies a string so that it can be safely transmitted over RCON. + * + * @param str String to escapify + * @return Escapified version of str. + */ + RENX_API Jupiter::String escapifyRCON(const Jupiter::ReadableString &str); + /** Constant variables */ RENX_API extern const char DelimC; /** RCON message deliminator */ + RENX_API extern const char DelimC3; /** RCON message deliminator for RCON version number 003 */ RENX_API extern const Jupiter::ReferenceString DevBotName; } diff --git a/RenX.Core/RenX_PlayerInfo.h b/RenX.Core/RenX_PlayerInfo.h index 006a0dd..f33c4ea 100644 --- a/RenX.Core/RenX_PlayerInfo.h +++ b/RenX.Core/RenX_PlayerInfo.h @@ -56,6 +56,7 @@ namespace RenX Jupiter::StringS character; Jupiter::StringS vehicle; Jupiter::StringS rdns; + Jupiter::StringS hwid; std::mutex rdns_mutex; uint64_t steamid = 0; uint32_t ip32 = 0; diff --git a/RenX.Core/RenX_Plugin.cpp b/RenX.Core/RenX_Plugin.cpp index fea2447..beeb3dc 100644 --- a/RenX.Core/RenX_Plugin.cpp +++ b/RenX.Core/RenX_Plugin.cpp @@ -71,6 +71,11 @@ void RenX::Plugin::RenX_OnServerCreate(Server *) return; } +void RenX::Plugin::RenX_OnServerFullyConnected(Server *) +{ + return; +} + void RenX::Plugin::RenX_OnServerDisconnect(Server *, RenX::DisconnectReason) { return; diff --git a/RenX.Core/RenX_Plugin.h b/RenX.Core/RenX_Plugin.h index 3ee54d8..f0f3c20 100644 --- a/RenX.Core/RenX_Plugin.h +++ b/RenX.Core/RenX_Plugin.h @@ -50,6 +50,7 @@ namespace RenX virtual void RenX_OnPlayerUUIDChange(Server *server, const PlayerInfo *player, const Jupiter::ReadableString &newUUID); virtual void RenX_OnPlayerRDNS(Server *server, const PlayerInfo *player); virtual void RenX_OnServerCreate(Server *server); + virtual void RenX_OnServerFullyConnected(Server *server); virtual void RenX_OnServerDisconnect(Server *server, RenX::DisconnectReason reason); virtual bool RenX_OnBan(Server *server, const PlayerInfo *player, Jupiter::StringType &data); diff --git a/RenX.Core/RenX_Server.cpp b/RenX.Core/RenX_Server.cpp index b718407..5ce57a2 100644 --- a/RenX.Core/RenX_Server.cpp +++ b/RenX.Core/RenX_Server.cpp @@ -187,6 +187,16 @@ bool RenX::Server::isConnected() const return RenX::Server::connected; } +bool RenX::Server::isSubscribed() const +{ + return RenX::Server::subscribed; +} + +bool RenX::Server::isFullyConnected() const +{ + return RenX::Server::fully_connected; +} + bool RenX::Server::hasSeenStart() const { return RenX::Server::seenStart; @@ -239,29 +249,28 @@ bool RenX::Server::isPure() const int RenX::Server::send(const Jupiter::ReadableString &command) { - return RenX::Server::sock.send("c"_jrs + command + '\n'); + return RenX::Server::sock.send("c"_jrs + RenX::escapifyRCON(command) + '\n'); } int RenX::Server::sendMessage(const Jupiter::ReadableString &message) { + Jupiter::String msg = RenX::escapifyRCON(message); if (RenX::Server::neverSay) { int r = 0; if (RenX::Server::players.size() != 0) for (Jupiter::DLList::Node *node = RenX::Server::players.getNode(0); node != nullptr; node = node->next) if (node->data->isBot == false) - r += RenX::Server::sock.send(Jupiter::StringS::Format("chostprivatesay pid%d %.*s\n", node->data->id, message.size(), message.ptr())); + r += RenX::Server::sock.send(Jupiter::StringS::Format("chostprivatesay pid%d %.*s\n", node->data->id, msg.size(), msg.ptr())); return r; } else - return RenX::Server::sock.send("chostsay "_jrs + message + '\n'); + return RenX::Server::sock.send("chostsay "_jrs + msg + '\n'); } int RenX::Server::sendMessage(const RenX::PlayerInfo *player, const Jupiter::ReadableString &message) { - auto cmd = "chostprivatesay pid"_jrs + Jupiter::StringS::Format("%d ", player->id) + message + '\n'; - RenX::sanitizeString(cmd); - return RenX::Server::sock.send(cmd); + return RenX::Server::sock.send("chostprivatesay pid"_jrs + Jupiter::StringS::Format("%d ", player->id) + RenX::escapifyRCON(message) + '\n'); } int RenX::Server::sendData(const Jupiter::ReadableString &data) @@ -418,8 +427,10 @@ Jupiter::StringS RenX::Server::formatSteamID(uint64_t id) const } } -void RenX::Server::kickPlayer(int id, const Jupiter::ReadableString &reason) +void RenX::Server::kickPlayer(int id, const Jupiter::ReadableString &in_reason) { + Jupiter::String reason = RenX::escapifyRCON(in_reason); + if (reason.isEmpty()) RenX::Server::sock.send(Jupiter::StringS::Format("ckick pid%d\n", id)); else @@ -432,8 +443,10 @@ void RenX::Server::kickPlayer(const RenX::PlayerInfo *player, const Jupiter::Rea RenX::Server::kickPlayer(player->id, reason); } -void RenX::Server::forceKickPlayer(int id, const Jupiter::ReadableString &reason) +void RenX::Server::forceKickPlayer(int id, const Jupiter::ReadableString &in_reason) { + Jupiter::String reason = RenX::escapifyRCON(in_reason); + if (reason.isEmpty()) RenX::Server::sock.send(Jupiter::StringS::Format("cfkick pid%d You were kicked from the server.\n", id)); else @@ -611,7 +624,10 @@ void RenX::Server::banCheck(RenX::PlayerInfo *player) void RenX::Server::banPlayer(int id, const Jupiter::ReadableString &banner, const Jupiter::ReadableString &reason) { if (RenX::Server::rconBan) - RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", id, reason.size(), reason.ptr())); + { + Jupiter::String out_reason = RenX::escapifyRCON(reason); + RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", id, out_reason.size(), out_reason.ptr())); + } else { RenX::PlayerInfo *player = RenX::Server::getPlayer(id); @@ -630,7 +646,10 @@ void RenX::Server::banPlayer(const RenX::PlayerInfo *player, const Jupiter::Read if (length == std::chrono::seconds::zero()) { if (RenX::Server::rconBan) - RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", player->id, reason.size(), reason.ptr())); + { + Jupiter::String out_reason = RenX::escapifyRCON(reason); + RenX::Server::sock.send(Jupiter::StringS::Format("ckickban pid%d %.*s\n", player->id, out_reason.size(), out_reason.ptr())); + } else if (banner.isNotEmpty()) RenX::Server::forceKickPlayer(player, Jupiter::StringS::Format("You are permanently banned from %.*s by %.*s for: %.*s", RenX::Server::ban_from_str.size(), RenX::Server::ban_from_str.ptr(), banner.size(), banner.ptr(), reason.size(), reason.ptr())); else @@ -680,8 +699,12 @@ bool RenX::Server::removePlayer(RenX::PlayerInfo *player) bool RenX::Server::fetchClientList() { RenX::Server::lastClientListUpdate = std::chrono::steady_clock::now(); - return RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cclientvarlist KILLS\xA0""DEATHS\xA0""SCORE\xA0""CREDITS\xA0""CHARACTER\xA0""VEHICLE\xA0""PING\xA0""ADMIN\xA0""STEAM\xA0""IP\xA0""PLAYERLOG\n")) > 0 - && RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cbotvarlist KILLS\xA0""DEATHS\xA0""SCORE\xA0""CREDITS\xA0""CHARACTER\xA0""VEHICLE\xA0""PLAYERLOG\n")) > 0; + if (this->rconVersion >= 4) + return RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cclientvarlist KILLS DEATHS SCORE CREDITS CHARACTER VEHICLE PING ADMIN STEAM IP PLAYERLOG\n")) > 0 + && RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cbotvarlist KILLS DEATHS SCORE CREDITS CHARACTER VEHICLE PLAYERLOG\n")) > 0; + else + return RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cclientvarlist KILLS\xA0""DEATHS\xA0""SCORE\xA0""CREDITS\xA0""CHARACTER\xA0""VEHICLE\xA0""PING\xA0""ADMIN\xA0""STEAM\xA0""IP\xA0""PLAYERLOG\n")) > 0 + && RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cbotvarlist KILLS\xA0""DEATHS\xA0""SCORE\xA0""CREDITS\xA0""CHARACTER\xA0""VEHICLE\xA0""PLAYERLOG\n")) > 0; } bool RenX::Server::updateClientList() @@ -690,10 +713,20 @@ bool RenX::Server::updateClientList() int r = 0; if (RenX::Server::players.size() != RenX::Server::bot_count) - r = RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cclientvarlist ID\xA0""SCORE\xA0""CREDITS\xA0""PING\n")) > 0; + { + if (this->rconVersion >= 4) + r = RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cclientvarlist ID SCORE CREDITS PING\n")) > 0; + else + r = RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cclientvarlist ID\xA0""SCORE\xA0""CREDITS\xA0""PING\n")) > 0; + } if (RenX::Server::bot_count != 0) - r |= RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cbotvarlist ID\xA0""SCORE\xA0""CREDITS\n")) > 0; + { + if (this->rconVersion >= 4) + r |= RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cbotvarlist ID SCORE CREDITS\n")) > 0; + else + r |= RenX::Server::sock.send(STRING_LITERAL_AS_REFERENCE("cbotvarlist ID\xA0""SCORE\xA0""CREDITS\n")) > 0; + } return r != 0; } @@ -920,11 +953,61 @@ std::chrono::milliseconds RenX::Server::getDelay() const return RenX::Server::delay; } +int RenX::Server::getMineLimit() const +{ + return RenX::Server::mineLimit; +} + +int RenX::Server::getPlayerLimit() const +{ + return RenX::Server::playerLimit; +} + +int RenX::Server::getVehicleLimit() const +{ + return RenX::Server::vehicleLimit; +} + +int RenX::Server::getTimeLimit() const +{ + return RenX::Server::timeLimit; +} + +double RenX::Server::getCrateRespawnDelay() const +{ + return RenX::Server::crateRespawnAfterPickup; +} + +bool RenX::Server::isSteamRequired() const +{ + return RenX::Server::steamRequired; +} + +bool RenX::Server::isPrivateMessageTeamOnly() const +{ + return RenX::Server::privateMessageTeamOnly; +} + +bool RenX::Server::isPrivateMessagingEnabled() const +{ + return RenX::Server::allowPrivateMessaging; +} + bool RenX::Server::isPassworded() const { return RenX::Server::passworded; } +bool RenX::Server::isAutoBalanceEnabled() const +{ + return RenX::Server::autoBalanceTeams; +} + +bool RenX::Server::isCratesEnabled() const +{ + return RenX::Server::spawnCrates; +} + const Jupiter::ReadableString &RenX::Server::getPassword() const { return RenX::Server::pass; @@ -1212,7 +1295,7 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) return; Jupiter::ArrayList &xPlugins = *RenX::getCore()->getPlugins(); - Jupiter::ReadableString::TokenizeResult tokens = Jupiter::StringS::tokenize(line, RenX::DelimC); + Jupiter::ReadableString::TokenizeResult tokens = Jupiter::StringS::tokenize(line, this->rconVersion >= 4 ? RenX::DelimC : RenX::DelimC3); for (size_t index = 0; index != tokens.token_count; ++index) tokens.tokens[index].processEscapeSequences(); @@ -1398,9 +1481,16 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) size_t offset = index; while (index != 0) - offset += tokens.tokens[--index].size(); + offset += tokens.tokens[--index].size() + 1; - return Jupiter::ReferenceString::substring(line, offset + 1); + return Jupiter::ReferenceString::substring(line, offset); + }; + auto finished_connecting = [this, &xPlugins]() + { + this->fully_connected = true; + + for (size_t index = 0; index < xPlugins.size(); ++index) + xPlugins.get(index)->RenX_OnServerFullyConnected(this); }; if (tokens.tokens[0].isNotEmpty()) @@ -1743,7 +1833,12 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) } } else if (this->lastCommand.equalsi("ping")) - RenX::Server::awaitingPong = false; + { + if (tokens.getToken(1).equals("srv_init_done"_jrs)) + finished_connecting(); + else + RenX::Server::awaitingPong = false; + } else if (this->lastCommand.equalsi("map")) { // Map | Guid @@ -2419,10 +2514,26 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) { PARSE_PLAYER_DATA_P(tokens.getToken(2)); uint64_t steamid = 0; - if (tokens.getToken(5).equals("steamid")) - steamid = tokens.getToken(6).asUnsignedLongLong(); + RenX::PlayerInfo *player; + + if (tokens.getToken(5).equals("hwid")) + { + // New format + if (tokens.getToken(7).equals("steamid")) + steamid = tokens.getToken(8).asUnsignedLongLong(); + + player = getPlayerOrAdd(name, id, team, isBot, steamid, tokens.getToken(4)); + player->hwid = tokens.getToken(6); + } + else + { + // Old format + if (tokens.getToken(5).equals("steamid")) + steamid = tokens.getToken(6).asUnsignedLongLong(); + + player = getPlayerOrAdd(name, id, team, isBot, steamid, tokens.getToken(4)); + } - RenX::PlayerInfo *player = getPlayerOrAdd(name, id, team, isBot, steamid, tokens.getToken(4)); if (steamid != 0ULL && default_ladder_database != nullptr && (player->ban_flags & RenX::BanDatabase::Entry::FLAG_TYPE_LADDER) == 0) { RenX::LadderDatabase::Entry *itr = RenX::default_ladder_database->getHead(); @@ -2434,7 +2545,10 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) if (this->devBot) { player->global_rank = itr->rank; - this->sendData(Jupiter::StringS::Format("xset_rank %d\n", player->id)); + if (this->rconVersion >= 4) + this->sendData(Jupiter::StringS::Format("xset_rank %d %d\n", player->id, player->global_rank)); + else + this->sendData(Jupiter::StringS::Format("xset_rank%c%d%c%d\n", RenX::DelimC3, player->id, RenX::DelimC3, player->global_rank)); } break; } @@ -2500,7 +2614,12 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) if (player->isBot == false) this->banCheck(player); if (this->devBot && player->global_rank != 0U) - this->sendData(Jupiter::StringS::Format("xset_rank %d\n", player->id)); + { + if (this->rconVersion >= 4) + this->sendData(Jupiter::StringS::Format("xset_rank %d %d\n", player->id, player->global_rank)); + else + this->sendData(Jupiter::StringS::Format("xset_rank%c%d%c%d\n", RenX::DelimC, player->id, RenX::DelimC, player->global_rank)); + } for (size_t i = 0; i < xPlugins.size(); i++) xPlugins.get(i)->RenX_OnIDChange(this, player, oldID); } @@ -2580,6 +2699,10 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) { // User Jupiter::ReferenceString user = tokens.getToken(2); + + if (user.equals(this->rconUser)) + this->subscribed = true; + for (size_t i = 0; i < xPlugins.size(); i++) xPlugins.get(i)->RenX_OnSubscribe(this, user); } @@ -2587,6 +2710,10 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) { // User Jupiter::ReferenceString user = tokens.getToken(2); + + if (user.equals(this->rconUser)) + this->subscribed = false; + for (size_t i = 0; i < xPlugins.size(); i++) xPlugins.get(i)->RenX_OnUnsubscribe(this, user); } @@ -3045,6 +3172,7 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) RenX::Server::send("rotation"_jrs); RenX::Server::fetchClientList(); RenX::Server::updateBuildingList(); + RenX::Server::send("ping srv_init_done"_jrs); RenX::Server::gameStart = std::chrono::steady_clock::now(); this->seenStart = false; @@ -3087,13 +3215,14 @@ void RenX::Server::processLine(const Jupiter::ReadableString &line) void RenX::Server::disconnect(RenX::DisconnectReason reason) { - Jupiter::ArrayList xPlugins; + RenX::Server::connected = false; + + Jupiter::ArrayList &xPlugins = *RenX::getCore()->getPlugins(); for (size_t i = 0; i < xPlugins.size(); i++) xPlugins.get(i)->RenX_OnServerDisconnect(this, reason); RenX::Server::sock.close(); RenX::Server::wipeData(); - RenX::Server::connected = false; } bool RenX::Server::connect() @@ -3137,6 +3266,8 @@ void RenX::Server::wipeData() delete player; } + RenX::Server::subscribed = false; + RenX::Server::fully_connected = false; RenX::Server::bot_count = 0; RenX::Server::player_rdns_resolutions_pending = 0; RenX::Server::buildings.emptyAndDelete(); diff --git a/RenX.Core/RenX_Server.h b/RenX.Core/RenX_Server.h index ad90fd7..8cb68b1 100644 --- a/RenX.Core/RenX_Server.h +++ b/RenX.Core/RenX_Server.h @@ -97,6 +97,21 @@ namespace RenX */ bool isConnected() const; + /** + * @brief Checks if the server is currently subscribed to. + * + * @return True if the server is subscribed, false otherwise. + */ + bool isSubscribed() const; + + /** + * @brief Checks if the server has been fully connected to. + * Note: This is true after all of the initial commands have received a response, after isSubscribed() is true. + * + * @return True if the server is fully connected, false otherwise. + */ + bool isFullyConnected() const; + /** * @brief Checks if a map start event has fired. * @@ -699,12 +714,38 @@ namespace RenX */ std::chrono::milliseconds getDelay() const; + /** + server->getMineLimit(), + json_bool_as_cstring(server->isSteamRequired()), + json_bool_as_cstring(server->isPrivateMessageTeamOnly()), + json_bool_as_cstring(server->isPassworded()), + json_bool_as_cstring(server->isPrivateMessagingEnabled()), + server->getPlayerLimit(), + server->getVehicleLimit(), + json_bool_as_cstring(server->isAutoBalanceTeams()), + json_bool_as_cstring(server->isCratesEnabled()), + server->getCrateRespawnDelay(), + server->getTimeLimit(), + */ + + int getMineLimit() const; + int getPlayerLimit() const; + int getVehicleLimit() const; + int getTimeLimit() const; + double getCrateRespawnDelay() const; + bool isSteamRequired() const; + bool isPrivateMessageTeamOnly() const; + bool isPrivateMessagingEnabled() const; + /** * @brief Checks if the server has a game password. * * @return True if the game is passworded, false otherwise. */ bool isPassworded() const; + + bool isAutoBalanceEnabled() const; + bool isCratesEnabled() const; /** * @brief Fetches the RCON password of a server. @@ -952,6 +993,8 @@ namespace RenX bool gameover_pending = false; bool pure = false; bool connected = false; + bool subscribed = false; + bool fully_connected = false; bool seamless = false; bool needsCList = false; bool silenceParts = false; diff --git a/RenX.Core/RenX_TeamInfo.h b/RenX.Core/RenX_TeamInfo.h new file mode 100644 index 0000000..fb31cdf --- /dev/null +++ b/RenX.Core/RenX_TeamInfo.h @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2016 Jessica James. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Written by Jessica James + */ + +#if !defined _RENX_TEAMINFO_H_HEADER +#define _RENX_TEAMINFO_H_HEADER + +/** + * @file RenX_BuildingInfo.h + * @brief Defines the BuildingInfo structure. + */ + +#include "Jupiter/String.h" +#include "Jupiter/INIFile.h" +#include "RenX.h" + +/** DLL Linkage Nagging */ +#if defined _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4251) +#endif + +namespace RenX +{ + + /** + * @brief Includes all of the tracked information about a team. + */ + struct RENX_API TeamInfo + { + uint8_t id; + int32_t score; + int32_t kills; + int32_t deaths; + int32_t mine_count; + int32_t mine_limit; + int32_t vehicle_count; + int32_t vehicle_limit; + Jupiter::StringS name; + }; + +} + +/** Re-enable warnings */ +#if defined _MSC_VER +#pragma warning(pop) +#endif + +#endif // _RENX_TEAMINFO_H_HEADER diff --git a/RenX.ModSystem/RenX_ModSystem.cpp b/RenX.ModSystem/RenX_ModSystem.cpp index da12fc9..b36dde8 100644 --- a/RenX.ModSystem/RenX_ModSystem.cpp +++ b/RenX.ModSystem/RenX_ModSystem.cpp @@ -176,7 +176,12 @@ int RenX_ModSystemPlugin::auth(RenX::Server *server, const RenX::PlayerInfo *pla { server->sendMessage(player, Jupiter::StringS::Format("You are now authenticated with access level %d; group: %.*s.", player->access, group->name.size(), group->name.ptr())); if (server->isDevBot()) - server->sendData(Jupiter::StringS::Format("d%d\n", player->id)); + { + if (server->getVersion() >= 4) + server->sendData(Jupiter::StringS::Format("xset_dev %d\n", player->id)); + else + server->sendData(Jupiter::StringS::Format("xset_dev%c%d\n", RenX::DelimC, player->id)); + } } Jupiter::String playerName = RenX::getFormattedPlayerName(player); server->sendLogChan(IRCCOLOR "03[Authentication] " IRCBOLD "%.*s" IRCBOLD IRCCOLOR " is now authenticated with access level %d; group: %.*s.", playerName.size(), playerName.ptr(), player->access, group->name.size(), group->name.ptr()); diff --git a/RenX.ServerList/RenX.ServerList.vcxproj b/RenX.ServerList/RenX.ServerList.vcxproj new file mode 100644 index 0000000..7061318 --- /dev/null +++ b/RenX.ServerList/RenX.ServerList.vcxproj @@ -0,0 +1,86 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {6B0D59BA-B153-4DE8-8DD4-FBE5D810B033} + RenX.ServerList + + + + Application + true + v140 + MultiByte + + + DynamicLibrary + false + v140 + true + Unicode + + + + + + + + + + + + + $(SolutionDir)$(Configuration)\Plugins\ + AllRules.ruleset + + + + Level3 + Disabled + true + + + true + + + + + Level3 + MaxSpeed + true + true + true + ../Bot;../Jupiter;../RenX.Core;../HTTPServer + _CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + + + true + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RenX.ServerList/RenX.ServerList.vcxproj.filters b/RenX.ServerList/RenX.ServerList.vcxproj.filters new file mode 100644 index 0000000..14c4699 --- /dev/null +++ b/RenX.ServerList/RenX.ServerList.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + + + Source Files + + + + + Resource Files + + + Resource Files + + + Resource Files + + + Resource Files + + + \ No newline at end of file diff --git a/RenX.ServerList/RenX_ServerList.cpp b/RenX.ServerList/RenX_ServerList.cpp new file mode 100644 index 0000000..5161ded --- /dev/null +++ b/RenX.ServerList/RenX_ServerList.cpp @@ -0,0 +1,487 @@ +/** + * Copyright (C) 2016 Jessica James. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Written by Jessica James + */ + +#include "Jupiter/IRC_Client.h" +#include "Jupiter/INIFile.h" +#include "Jupiter/HTTP.h" +#include "Jupiter/HTTP_QueryString.h" +#include "HTTPServer.h" +#include "RenX_Core.h" +#include "RenX_Server.h" +#include "RenX_ServerList.h" +#include "RenX_Functions.h" + +using namespace Jupiter::literals; + +static STRING_LITERAL_AS_NAMED_REFERENCE(CONTENT_TYPE_APPLICATION_JSON, "application/json"); + +Jupiter::String jsonify(const Jupiter::ReadableString &in_str) +{ + const char *ptr = in_str.ptr(); + size_t str_length = in_str.size(); + Jupiter::String result(str_length); + + while (str_length != 0) + { + if (*ptr == '\\') // backslash + { + result += '\\'; + result += '\\'; + } + else if (*ptr == '\"') // quotation + { + result += '\\'; + result += '\"'; + } + else if (*ptr < 0x20) // control characters + result += Jupiter::StringS::Format("\\u00%x", *ptr); + else if ((*ptr & 0x80) != 0) // UTF-8 sequence; copy to bypass above processing + { + result += *ptr; + + if ((*ptr & 0x40) != 0) + { + // this is a 2+ byte sequence + + if ((*ptr & 0x20) != 0) + { + // this is a 3+ byte sequence + + if ((*ptr & 0x10) != 0) + { + // this is a 4 byte sequnce + result += *++ptr; + } + + result += *++ptr; + } + + result += *++ptr; + } + } + else // Character in standard ASCII table + result += *ptr; + + ++ptr; + --str_length; + } + + return result; +} + +RenX_ServerListPlugin::RenX_ServerListPlugin() +{ + RenX_ServerListPlugin::web_hostname = Jupiter::IRC::Client::Config->get(this->name, "Hostname"_jrs, ""_jrs); + RenX_ServerListPlugin::web_path = Jupiter::IRC::Client::Config->get(this->name, "Path"_jrs, "/"_jrs); + RenX_ServerListPlugin::server_list_page_name = Jupiter::IRC::Client::Config->get(this->name, "ServersPageName"_jrs, "servers.json"_jrs); + RenX_ServerListPlugin::server_page_name = Jupiter::IRC::Client::Config->get(this->name, "ServerPageName"_jrs, "server.json"_jrs); + + /** Initialize content */ + Jupiter::HTTP::Server &server = getHTTPServer(); + + // Server list page + Jupiter::HTTP::Server::Content *content = new Jupiter::HTTP::Server::Content(RenX_ServerListPlugin::server_list_page_name, handle_server_list_page); + content->language = &Jupiter::HTTP::Content::Language::ENGLISH; + content->type = &CONTENT_TYPE_APPLICATION_JSON; + content->charset = &Jupiter::HTTP::Content::Type::Text::Charset::UTF8; + content->free_result = false; + server.hook(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, content); + + // Server page (GUIDs) + content = new Jupiter::HTTP::Server::Content(RenX_ServerListPlugin::server_page_name, handle_server_page); + content->language = &Jupiter::HTTP::Content::Language::ENGLISH; + content->type = &CONTENT_TYPE_APPLICATION_JSON; + content->charset = &Jupiter::HTTP::Content::Type::Text::Charset::UTF8; + content->free_result = true; + server.hook(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, content); + + this->updateServerList(); +} + +RenX_ServerListPlugin::~RenX_ServerListPlugin() +{ + Jupiter::HTTP::Server &server = getHTTPServer(); + server.remove(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, RenX_ServerListPlugin::server_list_page_name); + server.remove(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, RenX_ServerListPlugin::server_page_name); +} + +Jupiter::ReadableString *RenX_ServerListPlugin::getServerListJSON() +{ + return std::addressof(RenX_ServerListPlugin::server_list_json); +} + +const char *json_bool_as_cstring(bool in) +{ + if (in) + return "true"; + return "false"; +} + +Jupiter::StringS server_as_json(const RenX::Server *server) +{ + Jupiter::String server_json_block(128); + + Jupiter::String server_name = jsonify(server->getName()); + Jupiter::String server_map = jsonify(server->getMap().name); + Jupiter::String server_version = jsonify(server->getGameVersion()); + + server_json_block.format(R"json({"Name":"%.*s","Current Map":"%.*s","Bots":%u,"Players":%u,"Game Version":"%.*s","Variables":{"Mine Limit":%d,"bSteamRequired":%s,"bPrivateMessageTeamOnly":%s,"bPassworded":%s,"bAllowPrivateMessaging":%s,"Player Limit":%d,"Vehicle Limit":%d,"bAutoBalanceTeams":%s,"bSpawnCrates":%s,"CrateRespawnAfterPickup":%f,"Time Limit":%d},"Port":%u,"IP":"%.*s")json", + server_name.size(), server_name.ptr(), + server_map.size(), server_map.ptr(), + server->getBotCount(), + server->players.size() - server->getBotCount(), + server_version.size(), server_version.ptr(), + server->getMineLimit(), + json_bool_as_cstring(server->isSteamRequired()), + json_bool_as_cstring(server->isPrivateMessageTeamOnly()), + json_bool_as_cstring(server->isPassworded()), + json_bool_as_cstring(server->isPrivateMessagingEnabled()), + server->getPlayerLimit(), + server->getVehicleLimit(), + json_bool_as_cstring(server->isAutoBalanceEnabled()), + json_bool_as_cstring(server->isCratesEnabled()), + server->getCrateRespawnDelay(), + server->getTimeLimit(), + server->getPort(), + server->getSocketHostname().size(), server->getSocketHostname().ptr()); + + + // Level Rotation + /*if (server->maps.size() != 0) + { + server_json_block += ",\"Levels\":["_jrs; + + server_json_block += "{\"Name\":\""_jrs; + server_json_block += server->maps.get(0)->name; + server_json_block += "\",\"GUID\":\""_jrs; + server_json_block += RenX::formatGUID(*server->maps.get(0)); + server_json_block += "\"}"_jrs; + + for (size_t index = 1; index != server->maps.size(); ++index) + { + server_json_block += ",{\"Name\":\""_jrs; + server_json_block += server->maps.get(index)->name; + server_json_block += "\",\"GUID\":\""_jrs; + server_json_block += RenX::formatGUID(*server->maps.get(index)); + server_json_block += "\"}"_jrs; + } + + server_json_block += "]"_jrs; + } + + // Mutators + if (server->mutators.size() != 0) + { + server_json_block += ",\"Mutators\": ["_jrs; + + server_json_block += "{\"Name\":\""_jrs; + server_json_block += *server->mutators.get(0); + server_json_block += "\"}"_jrs; + + for (size_t index = 1; index != server->mutators.size(); ++index) + { + server_json_block += ",{\"Name\":\""_jrs; + server_json_block += *server->mutators.get(index); + server_json_block += "\"}"_jrs; + } + + server_json_block += "]"_jrs; + }*/ + + server_json_block += "}"_jrs; + + return server_json_block; +} + +Jupiter::StringS server_as_hr_json(const RenX::Server *server) +{ + Jupiter::String server_json_block(128); + + Jupiter::String server_name = jsonify(server->getName()); + Jupiter::String server_map = jsonify(server->getMap().name); + Jupiter::String server_version = jsonify(server->getGameVersion()); + + server_json_block.format(R"json({ + "Name": "%.*s", + "Current Map": "%.*s", + "Bots": %u, + "Players": %u, + "Game Version": "%.*s", + "Variables": { + "Mine Limit": %d, + "bSteamRequired": %s, + "bPrivateMessageTeamOnly": %s, + "bPassworded": %s, + "bAllowPrivateMessaging": %s, + "Player Limit": %d, + "Vehicle Limit": %d, + "bAutoBalanceTeams": %s, + "bSpawnCrates": %s, + "CrateRespawnAfterPickup": %f, + "Time Limit": %d + }, + "Port": %u, + "IP": "%.*s")json", + + server_name.size(), server_name.ptr(), + server_map.size(), server_map.ptr(), + server->getBotCount(), + server->players.size() - server->getBotCount(), + server_version.size(), server_version.ptr(), + + server->getMineLimit(), + json_bool_as_cstring(server->isSteamRequired()), + json_bool_as_cstring(server->isPrivateMessageTeamOnly()), + json_bool_as_cstring(server->isPassworded()), + json_bool_as_cstring(server->isPrivateMessagingEnabled()), + server->getPlayerLimit(), + server->getVehicleLimit(), + json_bool_as_cstring(server->isAutoBalanceEnabled()), + json_bool_as_cstring(server->isCratesEnabled()), + server->getCrateRespawnDelay(), + server->getTimeLimit(), + + server->getPort(), + server->getSocketHostname().size(), server->getSocketHostname().ptr()); + + + // Level Rotation + if (server->maps.size() != 0) + { + server_json_block += ",\n\t\t\"Levels\": ["_jrs; + + server_json_block += "\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; + server_json_block += server->maps.get(0)->name; + server_json_block += "\",\n\t\t\t\t\"GUID\": \""_jrs; + server_json_block += RenX::formatGUID(*server->maps.get(0)); + server_json_block += "\"\n\t\t\t}"_jrs; + + for (size_t index = 1; index != server->maps.size(); ++index) + { + server_json_block += ",\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; + server_json_block += server->maps.get(index)->name; + server_json_block += "\",\n\t\t\t\t\"GUID\": \""_jrs; + server_json_block += RenX::formatGUID(*server->maps.get(index)); + server_json_block += "\"\n\t\t\t}"_jrs; + } + + server_json_block += "\n\t\t]"_jrs; + } + + // Mutators + if (server->mutators.size() != 0) + { + server_json_block += ",\n\t\t\"Mutators\": ["_jrs; + + server_json_block += "\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; + server_json_block += *server->mutators.get(0); + server_json_block += "\"\n\t\t\t}"_jrs; + + for (size_t index = 1; index != server->mutators.size(); ++index) + { + server_json_block += ",\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; + server_json_block += *server->mutators.get(index); + server_json_block += "\"\n\t\t\t}"_jrs; + } + + server_json_block += "\n\t\t]"_jrs; + } + + server_json_block += "\n\t}"_jrs; + + return server_json_block; +} + +void RenX_ServerListPlugin::addServerToServerList(const RenX::Server *server) +{ + // append to server_list_json + + if (RenX_ServerListPlugin::server_list_json.isEmpty()) + { + RenX_ServerListPlugin::server_list_json = '['; + RenX_ServerListPlugin::server_list_json += server_as_json(server); + RenX_ServerListPlugin::server_list_json += ']'; + } + else + { + RenX_ServerListPlugin::server_list_json.truncate(1); // remove trailing ']'. + RenX_ServerListPlugin::server_list_json += ','; + RenX_ServerListPlugin::server_list_json += server_as_json(server); + RenX_ServerListPlugin::server_list_json += ']'; + } +} + +void RenX_ServerListPlugin::updateServerList() +{ + Jupiter::ArrayList servers = RenX::getCore()->getServers(); + size_t index = 0; + RenX::Server *server; + + // regenerate server_list_json + + RenX_ServerListPlugin::server_list_json = '['; + + while (index != servers.size()) + { + server = servers.get(index); + if (server->isConnected() && server->isFullyConnected()) + { + RenX_ServerListPlugin::server_list_json += server_as_json(server); + ++index; + break; + } + ++index; + } + while (index != servers.size()) + { + server = servers.get(index); + if (server->isConnected() && server->isFullyConnected()) + { + RenX_ServerListPlugin::server_list_json += ','; + RenX_ServerListPlugin::server_list_json += server_as_json(server); + } + ++index; + } + + RenX_ServerListPlugin::server_list_json += ']'; +} + +void RenX_ServerListPlugin::RenX_OnServerFullyConnected(RenX::Server *server) +{ + Jupiter::String server_json_block(256); + + this->addServerToServerList(server); + + // add to individual listing + + server_json_block = '{'; + + if (server->maps.size() != 0) + { + server_json_block += ",\"Levels\":["_jrs; + + server_json_block += "{\"Name\":\""_jrs; + server_json_block += jsonify(server->maps.get(0)->name); + server_json_block += "\",\"GUID\":\""_jrs; + server_json_block += RenX::formatGUID(*server->maps.get(0)); + server_json_block += "\"}"_jrs; + + for (size_t index = 1; index != server->maps.size(); ++index) + { + server_json_block += ",{\"Name\":\""_jrs; + server_json_block += jsonify(server->maps.get(index)->name); + server_json_block += "\",\"GUID\":\""_jrs; + server_json_block += RenX::formatGUID(*server->maps.get(index)); + server_json_block += "\"}"_jrs; + } + + server_json_block += ']'; + } + + // Mutators + if (server->mutators.size() != 0) + { + server_json_block += ",\"Mutators\":["_jrs; + + server_json_block += "{\"Name\":\""_jrs; + server_json_block += jsonify(*server->mutators.get(0)); + server_json_block += "\"}"_jrs; + + for (size_t index = 1; index != server->mutators.size(); ++index) + { + server_json_block += ",{\"Name\":\""_jrs; + server_json_block += jsonify(*server->mutators.get(index)); + server_json_block += "\"}"_jrs; + } + + server_json_block += ']'; + } + + server_json_block += '}'; + + server->varData.set(this->name, "j"_jrs, server_json_block); +} + +void RenX_ServerListPlugin::RenX_OnServerDisconnect(RenX::Server *server, RenX::DisconnectReason) +{ + this->updateServerList(); + + // remove from individual listing + server->varData.remove(this->name, "j"_jrs); +} + +void RenX_ServerListPlugin::RenX_OnJoin(RenX::Server *, const RenX::PlayerInfo *) +{ + this->updateServerList(); +} + +void RenX_ServerListPlugin::RenX_OnPart(RenX::Server *, const RenX::PlayerInfo *) +{ + this->updateServerList(); +} + +// Plugin instantiation and entry point. +RenX_ServerListPlugin pluginInstance; + +Jupiter::ReadableString *handle_server_list_page(const Jupiter::ReadableString &) +{ + return pluginInstance.getServerListJSON(); +} + +Jupiter::ReadableString *handle_server_page(const Jupiter::ReadableString &query_string) +{ + Jupiter::HTTP::HTMLFormResponse html_form_response(query_string); + Jupiter::ReferenceString address; + int port = 0; + RenX::Server *server; + + // parse form data + + if (html_form_response.table.size() < 2) + return new Jupiter::ReferenceString(); + + if (html_form_response.table.size() != 0) + { + address = html_form_response.table.get("ip"_jrs, address); + port = html_form_response.table.getInt("port"_jrs, port); + } + + // search for server + Jupiter::ArrayList servers = RenX::getCore()->getServers(); + size_t index = 0; + + while (true) + { + if (index == servers.size()) + return new Jupiter::ReferenceString(); + + server = servers.get(index); + if (server->getSocketHostname().equals(address) && server->getPort() == port) + break; + + ++index; + } + + // return server data + return new Jupiter::ReferenceString(server->varData.get(pluginInstance.getName(), "j"_jrs)); +} + +extern "C" __declspec(dllexport) Jupiter::Plugin *getPlugin() +{ + return &pluginInstance; +} diff --git a/RenX.ServerList/RenX_ServerList.h b/RenX.ServerList/RenX_ServerList.h new file mode 100644 index 0000000..44297dd --- /dev/null +++ b/RenX.ServerList/RenX_ServerList.h @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2016 Jessica James. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Written by Jessica James + */ + +#if !defined _RENX_SERVERLIST_H_HEADER +#define _RENX_SERVERLIST_H_HEADER + +#include "Jupiter/Plugin.h" +#include "Jupiter/Reference_String.h" +#include "RenX_Plugin.h" + +class RenX_ServerListPlugin : public RenX::Plugin +{ +public: // RenX_ServerListPlugin + Jupiter::ReadableString *getServerListJSON(); + + void addServerToServerList(const RenX::Server *server); + void updateServerList(); + + RenX_ServerListPlugin(); + ~RenX_ServerListPlugin(); + +public: // RenX::Plugin + void RenX_OnServerFullyConnected(RenX::Server *server) override; + + void RenX_OnServerDisconnect(RenX::Server *server, RenX::DisconnectReason reason) override; + void RenX_OnJoin(RenX::Server *server, const RenX::PlayerInfo *player) override; + void RenX_OnPart(RenX::Server *server, const RenX::PlayerInfo *player) override; + +public: // Jupiter::Plugin + const Jupiter::ReadableString &getName() override { return name; } + +private: + STRING_LITERAL_AS_NAMED_REFERENCE(name, "RenX.ServerList"); + + Jupiter::StringS server_list_json; + Jupiter::StringS web_hostname, web_path, server_list_page_name, server_page_name; +}; + +Jupiter::ReadableString *handle_server_list_page(const Jupiter::ReadableString &); +Jupiter::ReadableString *handle_server_page(const Jupiter::ReadableString &query_string); + +#endif // _RENX_SERVERLIST_H_HEADER