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 showing 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 up to 10 map names as strings which should be added to the list of maps in the given order. Currently the maximum size of map names on the server is 128 bytes, so this should always fit in one packet. Packing 10 map names into one packet instead of only 1 reduces overhead and makes maplist sending 10x faster (since it is currently bound to server ticks). When sending only 1 map name per message, sending the entire list of ddnet-maps would take almost an hour on a LAN server. When sending 10 map names per message it only takes a few minutes. The client is expected to unpack as many strings as possible from the message, as it may contain less than 10 map names in case of the last message and this also allows forward compatibility if we try to pack more map names depending on whether they will fit (which is not easily possible with `CPacker` at the moment).
- `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.

This does not include handling for `access_level` being 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 should logout and login again to get the updated maplist.

This does not include support for the 0.7 maplist protocol.

Closes 5727.
  • Loading branch information
Robyt3 committed Oct 2, 2024
1 parent 8095578 commit 686f300
Show file tree
Hide file tree
Showing 9 changed files with 278 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 @@ -1783,6 +1795,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 @@ -2159,6 +2173,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
163 changes: 163 additions & 0 deletions src/engine/server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,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 @@ -1047,6 +1048,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 @@ -1077,6 +1079,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 @@ -1164,6 +1167,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 @@ -1395,6 +1399,87 @@ 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)
{
if(!Client.m_DDNetVersionSettled || Client.m_DDNetVersion < VERSION_DDNET_MAPLIST)
{
Client.m_MaplistEntryToSend = CClient::MAPLIST_DISABLED;
return;
}

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);
for(int i = 0; i < 10 && (size_t)Client.m_MaplistEntryToSend < m_vMaplistEntries.size(); i++)
{
Msg.AddString(m_vMaplistEntries[Client.m_MaplistEntryToSend].m_aName);
++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 @@ -2789,6 +2874,7 @@ int CServer::Run()
}

ReadAnnouncementsFile();
InitMaplist();

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

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

m_Fifo.Update();

Expand Down Expand Up @@ -3639,6 +3726,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 @@ -3707,6 +3800,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 @@ -3897,6 +3991,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 @@ -4013,6 +4108,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(pFilename[0] == '.' && (pFilename[1] == '\0' || (pFilename[1] == '.' && pFilename[2] == '\0'))) // hidden files
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 @@ -342,6 +349,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 @@ -418,6 +439,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 @@ -454,6 +476,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 686f300

Please sign in to comment.