Skip to content

Commit

Permalink
List available maps as console arguments for sv_map/change_map
Browse files Browse the repository at this point in the history
Send list of all maps available in the `maps` folder on the server to authed clients, using version 18.7 or newer, that have access to the `sv_map` or `change_map` command, so the maps can be shown as console arguments for these commands. Progress of maplist sending is shown similar to rcon command sending progress.

The maplist sending is implemented similar to the rcon command sending. Each tick the maplist for one particular client is updated. The maplist will be sent only after all rcon commands have been sent.

The server will send the following new system messages:

- `NETMSG_MAPLIST_ADD`: Contains 1 or more map names as strings which should be added to the list of maps in the given order. To reduce overhead and make the maplist sending significantly faster (since it is currently bound to server ticks) this message contains as many strings as possible (with some leeway for protocol overhead). The client is expected to unpack as many strings as possible from the message.
- `NETMSG_MAPLIST_GROUP_START`: Indicates the start of maplist sending. Contains an integer that specifies the number of expected maplist entries for progress reporting. The previous maplist should be cleared when receiving this message.
- `NETMSG_MAPLIST_GROUP_END`: Indicates the end of maplist sending.

The server sorts the maplist after initializing it and sends the entries in order. Clients therefore do not need to perform their own sorting of the maplist.

The maplist is initialized when starting the server. The command `reload_maplist` is added to reload the maplist manually. When the maplist is reloaded, the server will resend the maplist to clients that already received it or are currently receiving it.

This does not include handling for the `access_level` command being used to change the access level for the `sv_map` or `change_map` command after a client has already logged in. Active maplist sending will not be canceled if the access level for all map commands is removed and it will not be started if access to a map command is granted while already logged in. Clients can logout and login again to get the updated maplist instead.

This does not include support for the 0.7 maplist protocol.

Closes ddnet#5727.
  • Loading branch information
Robyt3 committed Oct 25, 2024
1 parent 6ee2129 commit 0218f5d
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 9 deletions.
3 changes: 3 additions & 0 deletions src/engine/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ class IClient : public IInterface
virtual void Rcon(const char *pLine) = 0;
virtual bool ReceivingRconCommands() const = 0;
virtual float GotRconCommandsPercentage() const = 0;
virtual bool ReceivingMaplist() const = 0;
virtual float GotMaplistPercentage() const = 0;
virtual const std::vector<std::string> &MaplistEntries() const = 0;

// server info
virtual void GetServerInfo(class CServerInfo *pServerInfo) const = 0;
Expand Down
42 changes: 42 additions & 0 deletions src/engine/client/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,16 @@ float CClient::GotRconCommandsPercentage() const
return (float)m_GotRconCommands / (float)m_ExpectedRconCommands;
}

float CClient::GotMaplistPercentage() const
{
if(m_ExpectedMaplistEntries <= 0)
return -1.0f;
if(m_vMaplistEntries.size() > (size_t)m_ExpectedMaplistEntries)
return -1.0f;

return (float)m_vMaplistEntries.size() / (float)m_ExpectedMaplistEntries;
}

bool CClient::ConnectionProblems() const
{
return m_aNetClient[g_Config.m_ClDummy].GotProblems(MaxLatencyTicks() * time_freq() / GameTickSpeed()) != 0;
Expand Down Expand Up @@ -658,6 +668,8 @@ void CClient::DisconnectWithReason(const char *pReason)
m_ExpectedRconCommands = -1;
m_GotRconCommands = 0;
m_pConsole->DeregisterTempAll();
m_ExpectedMaplistEntries = -1;
m_vMaplistEntries.clear();
m_aNetClient[CONN_MAIN].Disconnect(pReason);
SetState(IClient::STATE_OFFLINE);
m_pMap->Unload();
Expand Down Expand Up @@ -1776,6 +1788,8 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy)
{
m_pConsole->DeregisterTempAll();
m_ExpectedRconCommands = -1;
m_vMaplistEntries.clear();
m_ExpectedMaplistEntries = -1;
}
}
}
Expand Down Expand Up @@ -2152,6 +2166,34 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy)
{
m_ExpectedRconCommands = -1;
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_ADD)
{
while(true)
{
const char *pMapName = Unpacker.GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
if(Unpacker.Error())
{
return;
}
if(pMapName[0] != '\0')
{
m_vMaplistEntries.emplace_back(pMapName);
}
}
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_GROUP_START)
{
const int ExpectedMaplistEntries = Unpacker.GetInt();
if(Unpacker.Error() || ExpectedMaplistEntries < 0)
return;

m_vMaplistEntries.clear();
m_ExpectedMaplistEntries = ExpectedMaplistEntries;
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_GROUP_END)
{
m_ExpectedMaplistEntries = -1;
}
}
else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0)
{
Expand Down
6 changes: 6 additions & 0 deletions src/engine/client/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ class CClient : public IClient, public CDemoPlayer::IListener
char m_aPassword[sizeof(g_Config.m_Password)] = "";
bool m_SendPassword = false;

int m_ExpectedMaplistEntries = -1;
std::vector<std::string> m_vMaplistEntries;

// version-checking
char m_aVersionStr[10] = "0";

Expand Down Expand Up @@ -297,6 +300,9 @@ class CClient : public IClient, public CDemoPlayer::IListener
void Rcon(const char *pCmd) override;
bool ReceivingRconCommands() const override { return m_ExpectedRconCommands > 0; }
float GotRconCommandsPercentage() const override;
bool ReceivingMaplist() const override { return m_ExpectedMaplistEntries > 0; }
float GotMaplistPercentage() const override;
const std::vector<std::string> &MaplistEntries() const override { return m_vMaplistEntries; }

bool ConnectionProblems() const override;

Expand Down
169 changes: 169 additions & 0 deletions src/engine/server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ int CServer::ClientRejoinCallback(int ClientId, void *pUser)
pThis->m_aClients[ClientId].m_Authed = AUTHED_NO;
pThis->m_aClients[ClientId].m_AuthKey = -1;
pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr;
pThis->m_aClients[ClientId].m_MaplistEntryToSend = CClient::MAPLIST_UNINITIALIZED;
pThis->m_aClients[ClientId].m_DDNetVersion = VERSION_NONE;
pThis->m_aClients[ClientId].m_GotDDNetVersionPacket = false;
pThis->m_aClients[ClientId].m_DDNetVersionSettled = false;
Expand Down Expand Up @@ -1048,6 +1049,7 @@ int CServer::NewClientNoAuthCallback(int ClientId, void *pUser)
pThis->m_aClients[ClientId].m_AuthKey = -1;
pThis->m_aClients[ClientId].m_AuthTries = 0;
pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr;
pThis->m_aClients[ClientId].m_MaplistEntryToSend = CClient::MAPLIST_UNINITIALIZED;
pThis->m_aClients[ClientId].m_ShowIps = false;
pThis->m_aClients[ClientId].m_DebugDummy = false;
pThis->m_aClients[ClientId].m_DDNetVersion = VERSION_NONE;
Expand Down Expand Up @@ -1078,6 +1080,7 @@ int CServer::NewClientCallback(int ClientId, void *pUser, bool Sixup)
pThis->m_aClients[ClientId].m_AuthKey = -1;
pThis->m_aClients[ClientId].m_AuthTries = 0;
pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr;
pThis->m_aClients[ClientId].m_MaplistEntryToSend = CClient::MAPLIST_UNINITIALIZED;
pThis->m_aClients[ClientId].m_Traffic = 0;
pThis->m_aClients[ClientId].m_TrafficSince = 0;
pThis->m_aClients[ClientId].m_ShowIps = false;
Expand Down Expand Up @@ -1165,6 +1168,7 @@ int CServer::DelClientCallback(int ClientId, const char *pReason, void *pUser)
pThis->m_aClients[ClientId].m_AuthKey = -1;
pThis->m_aClients[ClientId].m_AuthTries = 0;
pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr;
pThis->m_aClients[ClientId].m_MaplistEntryToSend = CClient::MAPLIST_UNINITIALIZED;
pThis->m_aClients[ClientId].m_Traffic = 0;
pThis->m_aClients[ClientId].m_TrafficSince = 0;
pThis->m_aClients[ClientId].m_ShowIps = false;
Expand Down Expand Up @@ -1396,6 +1400,93 @@ void CServer::UpdateClientRconCommands(int ClientId)
}
}

CServer::CMaplistEntry::CMaplistEntry(const char *pName)
{
str_copy(m_aName, pName);
}

bool CServer::CMaplistEntry::operator<(const CMaplistEntry &Other) const
{
return str_comp_filenames(m_aName, Other.m_aName) < 0;
}

void CServer::SendMaplistGroupStart(int ClientId)
{
CMsgPacker Msg(NETMSG_MAPLIST_GROUP_START, true);
Msg.AddInt(m_vMaplistEntries.size());
SendMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CServer::SendMaplistGroupEnd(int ClientId)
{
CMsgPacker Msg(NETMSG_MAPLIST_GROUP_END, true);
SendMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

void CServer::UpdateClientMaplistEntries(int ClientId)
{
CClient &Client = m_aClients[ClientId];
if(Client.m_State != CClient::STATE_INGAME ||
!Client.m_Authed ||
Client.m_Sixup ||
Client.m_pRconCmdToSend != nullptr || // wait for command sending
Client.m_MaplistEntryToSend == CClient::MAPLIST_DISABLED ||
Client.m_MaplistEntryToSend == CClient::MAPLIST_DONE)
{
return;
}

if(Client.m_MaplistEntryToSend == CClient::MAPLIST_UNINITIALIZED)
{
static const char *const MAP_COMMANDS[] = {"sv_map", "change_map"};
const int ConsoleAccessLevel = Client.ConsoleAccessLevel();
const bool MapCommandAllowed = std::any_of(std::begin(MAP_COMMANDS), std::end(MAP_COMMANDS), [&](const char *pMapCommand) {
const IConsole::CCommandInfo *pInfo = Console()->GetCommandInfo(pMapCommand, CFGFLAG_SERVER, false);
dbg_assert(pInfo != nullptr, "Map command not found");
return ConsoleAccessLevel <= pInfo->GetAccessLevel();
});
if(MapCommandAllowed)
{
Client.m_MaplistEntryToSend = 0;
SendMaplistGroupStart(ClientId);
}
else
{
Client.m_MaplistEntryToSend = CClient::MAPLIST_DISABLED;
return;
}
}

if((size_t)Client.m_MaplistEntryToSend < m_vMaplistEntries.size())
{
CMsgPacker Msg(NETMSG_MAPLIST_ADD, true);
int Limit = NET_MAX_PAYLOAD - 128;
while((size_t)Client.m_MaplistEntryToSend < m_vMaplistEntries.size())
{
// Space for null termination not included in Limit
const int SizeBefore = Msg.Size();
Msg.AddString(m_vMaplistEntries[Client.m_MaplistEntryToSend].m_aName, Limit - 1, false);
if(Msg.Error())
{
break;
}
Limit -= Msg.Size() - SizeBefore;
if(Limit <= 1)
{
break;
}
++Client.m_MaplistEntryToSend;
}
SendMsg(&Msg, MSGFLAG_VITAL, ClientId);
}

if((size_t)Client.m_MaplistEntryToSend >= m_vMaplistEntries.size())
{
SendMaplistGroupEnd(ClientId);
Client.m_MaplistEntryToSend = CClient::MAPLIST_DONE;
}
}

static inline int MsgFromSixup(int Msg, bool System)
{
if(System)
Expand Down Expand Up @@ -2784,6 +2875,7 @@ int CServer::Run()
}

ReadAnnouncementsFile();
InitMaplist();

// process pending commands
m_pConsole->StoreCommands(false);
Expand Down Expand Up @@ -2945,6 +3037,7 @@ int CServer::Run()

const int CommandSendingClientId = Tick() % MAX_CLIENTS;
UpdateClientRconCommands(CommandSendingClientId);
UpdateClientMaplistEntries(CommandSendingClientId);

m_Fifo.Update();

Expand Down Expand Up @@ -3635,6 +3728,12 @@ void CServer::ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData)
pThis->ReadAnnouncementsFile();
}

void CServer::ConReloadMaplist(IConsole::IResult *pResult, void *pUserData)
{
CServer *pThis = static_cast<CServer *>(pUserData);
pThis->InitMaplist();
}

void CServer::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
Expand Down Expand Up @@ -3703,6 +3802,7 @@ void CServer::LogoutClient(int ClientId, const char *pReason)

m_aClients[ClientId].m_AuthTries = 0;
m_aClients[ClientId].m_pRconCmdToSend = nullptr;
m_aClients[ClientId].m_MaplistEntryToSend = CClient::MAPLIST_UNINITIALIZED;

char aBuf[64];
if(*pReason)
Expand Down Expand Up @@ -3893,6 +3993,7 @@ void CServer::RegisterCommands()
Console()->Register("auth_list", "", CFGFLAG_SERVER, ConAuthList, this, "List all rcon keys");

Console()->Register("reload_announcement", "", CFGFLAG_SERVER, ConReloadAnnouncement, this, "Reload the announcements");
Console()->Register("reload_maplist", "", CFGFLAG_SERVER, ConReloadMaplist, this, "Reload the maplist");

RustVersionRegister(*Console());

Expand Down Expand Up @@ -4009,6 +4110,74 @@ const char *CServer::GetAnnouncementLine()
return m_vAnnouncements[m_AnnouncementLastLine].c_str();
}

struct CSubdirCallbackUserdata
{
CServer *m_pServer;
char m_aCurrentFolder[IO_MAX_PATH_LENGTH];
};

int CServer::MaplistEntryCallback(const char *pFilename, int IsDir, int DirType, void *pUser)
{
CSubdirCallbackUserdata *pUserdata = static_cast<CSubdirCallbackUserdata *>(pUser);
CServer *pThis = pUserdata->m_pServer;

if(str_comp(pFilename, ".") == 0 || str_comp(pFilename, "..") == 0)
return 0;

char aFilename[IO_MAX_PATH_LENGTH];
if(pUserdata->m_aCurrentFolder[0] != '\0')
str_format(aFilename, sizeof(aFilename), "%s/%s", pUserdata->m_aCurrentFolder, pFilename);
else
str_copy(aFilename, pFilename);

if(IsDir)
{
CSubdirCallbackUserdata Userdata;
Userdata.m_pServer = pThis;
str_copy(Userdata.m_aCurrentFolder, aFilename);
char aFindPath[IO_MAX_PATH_LENGTH];
str_format(aFindPath, sizeof(aFindPath), "maps/%s/", aFilename);
pThis->Storage()->ListDirectory(IStorage::TYPE_ALL, aFindPath, MaplistEntryCallback, &Userdata);
return 0;
}

const char *pSuffix = str_endswith(aFilename, ".map");
if(!pSuffix) // not ending with .map
return 0;
const size_t FilenameLength = pSuffix - aFilename;
aFilename[FilenameLength] = '\0'; // remove suffix
if(FilenameLength >= sizeof(CMaplistEntry().m_aName)) // name too long
return 0;

pThis->m_vMaplistEntries.emplace_back(aFilename);
return 0;
}

void CServer::InitMaplist()
{
m_vMaplistEntries.clear();

CSubdirCallbackUserdata Userdata;
Userdata.m_pServer = this;
Userdata.m_aCurrentFolder[0] = '\0';
Storage()->ListDirectory(IStorage::TYPE_ALL, "maps/", MaplistEntryCallback, &Userdata);

std::sort(m_vMaplistEntries.begin(), m_vMaplistEntries.end());
log_info("server", "Found %d maps for maplist", (int)m_vMaplistEntries.size());

for(CClient &Client : m_aClients)
{
if(Client.m_State != CClient::STATE_INGAME)
continue;

// Resend maplist to clients that already got it or are currently getting it
if(Client.m_MaplistEntryToSend == CClient::MAPLIST_DONE || Client.m_MaplistEntryToSend >= 0)
{
Client.m_MaplistEntryToSend = CClient::MAPLIST_UNINITIALIZED;
}
}
}

int *CServer::GetIdMap(int ClientId)
{
return m_aIdMap + VANILLA_MAX_CLIENTS * ClientId;
Expand Down
25 changes: 25 additions & 0 deletions src/engine/server/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ class CServer : public IServer
bool m_DebugDummy;

const IConsole::CCommandInfo *m_pRconCmdToSend;
enum
{
MAPLIST_UNINITIALIZED = -1,
MAPLIST_DISABLED = -2,
MAPLIST_DONE = -3,
};
int m_MaplistEntryToSend;

bool m_HasPersistentData;
void *m_pPersistentData;
Expand Down Expand Up @@ -343,6 +350,20 @@ class CServer : public IServer
int NumRconCommands(int ClientId);
void UpdateClientRconCommands(int ClientId);

class CMaplistEntry
{
public:
char m_aName[128];

CMaplistEntry() = default;
CMaplistEntry(const char *pName);
bool operator<(const CMaplistEntry &Other) const;
};
std::vector<CMaplistEntry> m_vMaplistEntries;
void SendMaplistGroupStart(int ClientId);
void SendMaplistGroupEnd(int ClientId);
void UpdateClientMaplistEntries(int ClientId);

bool CheckReservedSlotAuth(int ClientId, const char *pPassword);
void ProcessClientPacket(CNetChunk *pPacket);

Expand Down Expand Up @@ -419,6 +440,7 @@ class CServer : public IServer
static void ConDumpSqlServers(IConsole::IResult *pResult, void *pUserData);

static void ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData);
static void ConReloadMaplist(IConsole::IResult *pResult, void *pUserData);

static void ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
static void ConchainMaxclientsperipUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
Expand Down Expand Up @@ -455,6 +477,9 @@ class CServer : public IServer
const char *GetAnnouncementLine() override;
void ReadAnnouncementsFile();

static int MaplistEntryCallback(const char *pFilename, int IsDir, int DirType, void *pUser);
void InitMaplist();

int *GetIdMap(int ClientId) override;

void InitDnsbl(int ClientId);
Expand Down
Loading

0 comments on commit 0218f5d

Please sign in to comment.