diff --git a/src/engine/client.h b/src/engine/client.h index 8dcc9839e2a..4ed0df05162 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 fbf5277da09..9a98dc2fbe6 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; @@ -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(); @@ -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; } } } @@ -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) { diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 9b3104727e7..a44c002f80b 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -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 m_vMaplistEntries; + // version-checking char m_aVersionStr[10] = "0"; @@ -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 &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 fe6aecf3a41..0b1c8c18716 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -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; @@ -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; @@ -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; @@ -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; @@ -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) @@ -2784,6 +2875,7 @@ int CServer::Run() } ReadAnnouncementsFile(); + InitMaplist(); // process pending commands m_pConsole->StoreCommands(false); @@ -2945,6 +3037,7 @@ int CServer::Run() const int CommandSendingClientId = Tick() % MAX_CLIENTS; UpdateClientRconCommands(CommandSendingClientId); + UpdateClientMaplistEntries(CommandSendingClientId); m_Fifo.Update(); @@ -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(pUserData); + pThis->InitMaplist(); +} + void CServer::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { pfnCallback(pResult, pCallbackUserData); @@ -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) @@ -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()); @@ -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(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 6a1505f19d6..149c4a924cb 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; @@ -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 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); @@ -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); @@ -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); diff --git a/src/engine/shared/protocol_ex_msgs.h b/src/engine/shared/protocol_ex_msgs.h index d137d5c1762..a43b822e2de 100644 --- a/src/engine/shared/protocol_ex_msgs.h +++ b/src/engine/shared/protocol_ex_msgs.h @@ -36,3 +36,6 @@ UUID(NETMSG_REDIRECT, "redirect@ddnet.org") 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_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 d892898f789..0b7130a0339 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}, @@ -479,10 +482,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) { @@ -501,7 +505,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); @@ -514,7 +520,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); @@ -896,6 +904,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) { @@ -1215,7 +1237,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); @@ -1418,15 +1442,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 e8ab47900f4..3049776b59d 100644 --- a/src/game/client/components/console.h +++ b/src/game/client/components/console.h @@ -160,6 +160,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);