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