From 367a00ffd97aeebbe11bb2e2db47528a97576f45 Mon Sep 17 00:00:00 2001 From: JAJames Date: Tue, 10 May 2016 06:28:34 -0400 Subject: [PATCH] Added support for RCON protocol 4 Added event: 'RenX_OnFullyConnected' Added RenX.ServerList plugin Updated Jupiter --- Config.ini | 16 +- Jupiter | 2 +- Jupiter Bot.sln | 11 + Release/Bot.lib | Bin 24762 -> 24762 bytes Release/Plugins/RenX.Core.lib | Bin 197340 -> 201884 bytes RenX.Core/RenX.Core.vcxproj | 1 + RenX.Core/RenX.Core.vcxproj.filters | 3 + RenX.Core/RenX_Functions.cpp | 92 +++- RenX.Core/RenX_Functions.h | 18 +- RenX.Core/RenX_PlayerInfo.h | 1 + RenX.Core/RenX_Plugin.cpp | 5 + RenX.Core/RenX_Plugin.h | 1 + RenX.Core/RenX_Server.cpp | 181 ++++++- RenX.Core/RenX_Server.h | 43 ++ RenX.Core/RenX_TeamInfo.h | 63 +++ RenX.ModSystem/RenX_ModSystem.cpp | 7 +- RenX.ServerList/RenX.ServerList.vcxproj | 86 ++++ .../RenX.ServerList.vcxproj.filters | 41 ++ RenX.ServerList/RenX_ServerList.cpp | 487 ++++++++++++++++++ RenX.ServerList/RenX_ServerList.h | 57 ++ 20 files changed, 1057 insertions(+), 58 deletions(-) create mode 100644 RenX.Core/RenX_TeamInfo.h create mode 100644 RenX.ServerList/RenX.ServerList.vcxproj create mode 100644 RenX.ServerList/RenX.ServerList.vcxproj.filters create mode 100644 RenX.ServerList/RenX_ServerList.cpp create mode 100644 RenX.ServerList/RenX_ServerList.h 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 45ef926d0f2f031b399a77f2e6ebf8e302d1df1f..056c1606e55fd6bffcbb6cef6c82dd19c63c0132 100644 GIT binary patch delta 2218 zcmZuyNla5w6m10*he&O)pHPL$98jnVtsUsVQ0Yi1rGl-c8sbuMp-~f!3w7lbZ`?VM zn7DGlxFd03sSCxWMxq-NR|Yr6L?hn!-)sMUt?llebI(0@c<(PP>K7LEH|neHd-mD) z_Su{;n^T@TCoI~*T&(WdW6LRe5;E8JIviI1%WsJ4D|ELm?>_J>?f9lAfgv+worX#V z-Jf-qAlF=mDu%p=6V66B<*>lAYncWs@WE|TgJH!zAS3R6(?%BDq|@p2x=Q%5y$-Gf zHbTy8l!--&kDhXg^#zFv`0T0wzrdq@(JX2Rs*uCILi#z z1J&h*u6spI544w8jp!bsva zU4tdz*g1|o9Nb5R&9G*umLv5;b`22)sMGJko)|IZ%GwTWwzQxM6dkV}CMj9?@~}vG zdN?QxBccj-q3~7|p0~C@V~_}2BMNk7MEGow%xMlM_%$eyKf#nH%<3;r{qlY~b&C9y z5i`U`MEyrcr20G6ecXi>Ex2npceTRm2(dVu6C05TjZhQf^lgY3FoeaaH+)3nvL3w* z^M&rfG>vSbw-~JOH%!KAjfmKIB&)?r!~PN}>AX8?QkS_KRhwWewiRCOsD%qrA}bAO zDOxh1zL@B6G!`XEV};`6Qw5xH!B52Fl&`?^xXHlowG*whV}ZWIGcz!j>4S4glg6}- z3gz7BjCN`WEAS*Klz)wq)-mKvLLfI1c{!$LjIUP&b9Ui+Et-d*VX9hA$r^Ac#fdwS zxD$y_l~(vZM!JNr0>2yHWKzz7zSRBLkD=YTA8>D-4}1x-Ahz*BP2(5kSoEikhxxO? zyNPD-r@R_PI8!Oi*>08cDb=7+iv6-pkbX%seKSF3${wnoG%q%ZlD$~$ig%*oV zJzTi6-_k^wh1X2-f>Ri7XPf$aPgITMmV1#4VcRm_B%tixOmZ% zS#r^B<8`o*HS&^{vKO>snXluAxUVRt^JBV|_8su}Q2C726=E|EsCVDq0`VL%R7`z0 oM^dvfS51rM-!@(HZMjrHZcmrz<8}>svKs6qIhhw~SMuHe0KczuHvj+t delta 2218 zcmZuyNla5w6m10*he&O)pHPJ=lL3XQKljEP3Q@4wgn`&!%GJLjHz?(p7USkx~p>TlInb~z4q zAMDxR1JhY$&M{`u4rZb?^Rt#S^dxAm?zG#j{FmDl(UU<%YA~$1`eek_Yue0$ck6UIy{;U-@2r7q z{>_l}7-eEX;)A{}*`DYudtS4VbAM|Ddlj!D?@VoaRnHXVoFAg0swU z(_dL?=%z>1^vL5R8c_xQ<`J_ur6Gnyt7_AZ%9YUJYk?(ivj+PnT6v3FeR1v7HmbnN zgh=$vcV0tqx^QwpAUS_XLuP^sy!H#^liw#J-adh(`rI-?N3j)0@v6)MnSQcROc+Vr zrnA2&96QI6$NfE2*bM6isyR|S(5)e&0CoBu*b^hBTwA{fn=LJ<0tLtG2T4j6zA`9M zo*fLx!icECZ794Qg`Zg)p*}!_tziY)(jt61K;|@q6Z{+y$nQW>6K3@nrhfGhojOH+ z$cPzYL!$oULsI>%>OSs6ix%9q+xwc}?+~##ofR7q4-Qci;&drU3>ZS<)Dt?UaaoUE zhWJA7!8G-3p|=^V@FzsZY7L9nSU97_O2hsXF6z81Vp5m69aVS3NOU{A+EopgB1Bdi z&~l_`KwVMM;Yc(>l1B5z$)^f9VuBx!#VB8a=P{Fk-D?M0X~6=$N2jJ>B;5rU5+;pl z8y3ph;VJFZ5K`c2LMZp|nYz~x8T0ADInj493mDm8^I*o8KO&Tzh&hK}cY;obD zCo<%s+eT|(A!FnvEoUxi#WG(fk8ocROy|RNYc0EA_DJcB)fHkh_N#ZlyAfhpVyKY% pewL(WW3HGK%fDl?=-YB7kKCOs&ByH;a%45wOL96V)UM^){{afVsh0o% diff --git a/Release/Plugins/RenX.Core.lib b/Release/Plugins/RenX.Core.lib index e24f7016a328720290f7d8adcf094855af1b0480..697b720f5d2bd26608c7a6cdc0ab9b90194f9228 100644 GIT binary patch delta 27521 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 + 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