From a8f3c5f64e5666c579375e82da4b616d3d50c9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sat, 16 Dec 2023 10:54:43 +0100 Subject: [PATCH] List available maps as console arguments for `sv_map`/`change_map` Send list of all maps available in the `maps` folder on the server to authed clients 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 #5727. --- src/engine/client.h | 3 + src/engine/client/client.cpp | 42 ++++++ src/engine/client/client.h | 6 + src/engine/server/server.cpp | 169 +++++++++++++++++++++++++ src/engine/server/server.h | 25 ++++ src/engine/shared/protocol_ex_msgs.h | 3 + src/game/client/components/console.cpp | 42 ++++-- src/game/client/components/console.h | 2 + 8 files changed, 283 insertions(+), 9 deletions(-) diff --git a/src/engine/client.h b/src/engine/client.h index 753568ffc24..26aa250cde0 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -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 &MaplistEntries() const = 0; // server info virtual void GetServerInfo(class CServerInfo *pServerInfo) const = 0; diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index ef7f19973c3..777efb910ff 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -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; @@ -667,6 +677,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(); @@ -1842,6 +1854,8 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { m_pConsole->DeregisterTempAll(); m_ExpectedRconCommands = -1; + m_vMaplistEntries.clear(); + m_ExpectedMaplistEntries = -1; } } } @@ -2221,6 +2235,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) { diff --git a/src/engine/client/client.h b/src/engine/client/client.h index f05d7848613..5a03c2a9996 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -124,6 +124,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 m_vMaplistEntries; + // version-checking char m_aVersionStr[10] = "0"; @@ -299,6 +302,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 &MaplistEntries() const override { return m_vMaplistEntries; } bool ConnectionProblems() const override; diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 02b677a8437..ad3b5166c02 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -1035,6 +1035,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; @@ -1064,6 +1065,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; @@ -1094,6 +1096,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; @@ -1181,6 +1184,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; @@ -1419,6 +1423,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) @@ -2817,6 +2908,7 @@ int CServer::Run() } ReadAnnouncementsFile(); + InitMaplist(); // process pending commands m_pConsole->StoreCommands(false); @@ -2978,6 +3070,7 @@ int CServer::Run() const int CommandSendingClientId = Tick() % MAX_CLIENTS; UpdateClientRconCommands(CommandSendingClientId); + UpdateClientMaplistEntries(CommandSendingClientId); m_Fifo.Update(); @@ -3676,6 +3769,12 @@ void CServer::ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData) pThis->ReadAnnouncementsFile(); } +void CServer::ConReloadMaplist(IConsole::IResult *pResult, void *pUserData) +{ + CServer *pThis = static_cast(pUserData); + pThis->InitMaplist(); +} + void CServer::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { pfnCallback(pResult, pCallbackUserData); @@ -3744,6 +3843,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) @@ -3934,6 +4034,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()); @@ -4049,6 +4150,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(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; diff --git a/src/engine/server/server.h b/src/engine/server/server.h index 6e2f1ffff64..a45e7b01984 100644 --- a/src/engine/server/server.h +++ b/src/engine/server/server.h @@ -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; @@ -344,6 +351,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 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); @@ -420,6 +441,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); @@ -456,6 +478,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); diff --git a/src/engine/shared/protocol_ex_msgs.h b/src/engine/shared/protocol_ex_msgs.h index 19e75b9528a..2cb6e836f12 100644 --- a/src/engine/shared/protocol_ex_msgs.h +++ b/src/engine/shared/protocol_ex_msgs.h @@ -37,3 +37,6 @@ UUID(NETMSG_RCON_CMD_GROUP_START, "rcon-cmd-group-start@ddnet.org") UUID(NETMSG_RCON_CMD_GROUP_END, "rcon-cmd-group-end@ddnet.org") UUID(NETMSG_MAP_RELOAD, "map-reload@ddnet.org") UUID(NETMSG_RECONNECT, "reconnect@ddnet.org") +UUID(NETMSG_MAPLIST_ADD, "sv-maplist-add@ddnet.org") +UUID(NETMSG_MAPLIST_GROUP_START, "sv-maplist-start@ddnet.org") +UUID(NETMSG_MAPLIST_GROUP_END, "sv-maplist-end@ddnet.org") diff --git a/src/game/client/components/console.cpp b/src/game/client/components/console.cpp index ddac7afdc7f..751c1430484 100644 --- a/src/game/client/components/console.cpp +++ b/src/game/client/components/console.cpp @@ -76,6 +76,7 @@ void CConsoleLogger::OnConsoleDeletion() enum class EArgumentCompletionType { NONE, + MAP, TUNE, SETTING, KEY, @@ -90,6 +91,8 @@ class CArgumentCompletionEntry }; static const CArgumentCompletionEntry gs_aArgumentCompletionEntries[] = { + {EArgumentCompletionType::MAP, "sv_map", 0}, + {EArgumentCompletionType::MAP, "change_map", 0}, {EArgumentCompletionType::TUNE, "tune", 0}, {EArgumentCompletionType::TUNE, "tune_reset", 0}, {EArgumentCompletionType::TUNE, "toggle_tune", 0}, @@ -485,10 +488,11 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) char aSearch[IConsole::CMDLINE_LENGTH]; GetCommand(m_aCompletionBuffer, aSearch); - // command completion - const bool UseTempCommands = m_Type == CGameConsole::CONSOLETYPE_REMOTE && m_pGameConsole->Client()->RconAuthed() && m_pGameConsole->Client()->UseTempRconCommands(); + // Command completion + const bool RemoteConsoleCompletion = m_Type == CGameConsole::CONSOLETYPE_REMOTE && m_pGameConsole->Client()->RconAuthed(); + const bool UseTempCommands = RemoteConsoleCompletion && m_pGameConsole->Client()->UseTempRconCommands(); int CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(aSearch, m_CompletionFlagmask, UseTempCommands); - if(m_Type == CGameConsole::CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) + if(m_Type == CGameConsole::CONSOLETYPE_LOCAL || RemoteConsoleCompletion) { if(CompletionEnumerationCount) { @@ -507,7 +511,9 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) // Argument completion const auto [CompletionType, CompletionPos] = ArgumentCompletion(GetString()); - if(CompletionType == EArgumentCompletionType::TUNE) + if(CompletionType == EArgumentCompletionType::MAP) + CompletionEnumerationCount = m_pGameConsole->PossibleMaps(m_aCompletionBufferArgument); + else if(CompletionType == EArgumentCompletionType::TUNE) CompletionEnumerationCount = PossibleTunings(m_aCompletionBufferArgument); else if(CompletionType == EArgumentCompletionType::SETTING) CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands); @@ -520,7 +526,9 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) m_CompletionChosenArgument = 0; m_CompletionChosenArgument = (m_CompletionChosenArgument + Direction + CompletionEnumerationCount) % CompletionEnumerationCount; m_CompletionArgumentPosition = CompletionPos; - if(CompletionType == EArgumentCompletionType::TUNE) + if(CompletionType == EArgumentCompletionType::MAP) + m_pGameConsole->PossibleMaps(m_aCompletionBufferArgument, PossibleArgumentsCompleteCallback, this); + else if(CompletionType == EArgumentCompletionType::TUNE) PossibleTunings(m_aCompletionBufferArgument, PossibleArgumentsCompleteCallback, this); else if(CompletionType == EArgumentCompletionType::SETTING) m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands, PossibleArgumentsCompleteCallback, this); @@ -915,6 +923,20 @@ void CGameConsole::OnReset() m_RemoteConsole.Reset(); } +int CGameConsole::PossibleMaps(const char *pStr, IConsole::FPossibleCallback pfnCallback, void *pUser) +{ + int Index = 0; + for(const std::string &Entry : Client()->MaplistEntries()) + { + if(str_find_nocase(Entry.c_str(), pStr)) + { + pfnCallback(Index, Entry.c_str(), pUser); + Index++; + } + } + return Index; +} + // only defined for 0<=t<=1 static float ConsoleScaleFunc(float t) { @@ -1222,7 +1244,9 @@ void CGameConsole::OnRender() Info.m_WantedCompletion = pConsole->m_CompletionChosenArgument; Info.m_TotalWidth = 0.0f; Info.m_pCurrentCmd = pConsole->m_aCompletionBufferArgument; - if(CompletionType == EArgumentCompletionType::TUNE) + if(CompletionType == EArgumentCompletionType::MAP) + NumArguments = PossibleMaps(Info.m_pCurrentCmd, PossibleCommandsRenderCallback, &Info); + else if(CompletionType == EArgumentCompletionType::TUNE) NumArguments = PossibleTunings(Info.m_pCurrentCmd, PossibleCommandsRenderCallback, &Info); else if(CompletionType == EArgumentCompletionType::SETTING) NumArguments = m_pConsole->PossibleCommands(Info.m_pCurrentCmd, pConsole->m_CompletionFlagmask, m_ConsoleType != CGameConsole::CONSOLETYPE_LOCAL && Client()->RconAuthed() && Client()->UseTempRconCommands(), PossibleCommandsRenderCallback, &Info); @@ -1425,15 +1449,15 @@ void CGameConsole::OnRender() str_format(aBuf, sizeof(aBuf), Localize("Lines %d - %d (%s)"), pConsole->m_BacklogCurLine + 1, pConsole->m_BacklogCurLine + pConsole->m_LinesRendered, pConsole->m_BacklogCurLine != 0 ? Localize("Locked") : Localize("Following")); TextRender()->Text(10.0f, FONT_SIZE / 2.f, FONT_SIZE, aBuf); - if(m_ConsoleType == CONSOLETYPE_REMOTE && Client()->ReceivingRconCommands()) + if(m_ConsoleType == CONSOLETYPE_REMOTE && (Client()->ReceivingRconCommands() || Client()->ReceivingMaplist())) { - float Percentage = Client()->GotRconCommandsPercentage(); + const float Percentage = Client()->ReceivingRconCommands() ? Client()->GotRconCommandsPercentage() : Client()->GotMaplistPercentage(); SProgressSpinnerProperties ProgressProps; ProgressProps.m_Progress = Percentage; Ui()->RenderProgressSpinner(vec2(Screen.w / 4.0f + FONT_SIZE / 2.f, FONT_SIZE), FONT_SIZE / 2.f, ProgressProps); char aLoading[128]; - str_copy(aLoading, Localize("Loading commands…")); + str_copy(aLoading, Client()->ReceivingRconCommands() ? Localize("Loading commands…") : Localize("Loading maps…")); if(Percentage > 0) { char aPercentage[8]; diff --git a/src/game/client/components/console.h b/src/game/client/components/console.h index d80f6cf2fd8..43bc564ced0 100644 --- a/src/game/client/components/console.h +++ b/src/game/client/components/console.h @@ -161,6 +161,8 @@ class CGameConsole : public CComponent static const ColorRGBA ms_SearchHighlightColor; static const ColorRGBA ms_SearchSelectedColor; + int PossibleMaps(const char *pStr, IConsole::FPossibleCallback pfnCallback = IConsole::EmptyPossibleCommandCallback, void *pUser = nullptr); + static void PossibleCommandsRenderCallback(int Index, const char *pStr, void *pUser); static void ConToggleLocalConsole(IConsole::IResult *pResult, void *pUserData); static void ConToggleRemoteConsole(IConsole::IResult *pResult, void *pUserData);