From 7c9b1fbbb44a5efbb4eea1836c67474c4f49a496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sun, 21 Jan 2024 11:06:25 +0100 Subject: [PATCH] Add tabs for favorite communities, separate country/type filters Support adding up to three communities as favorites in the server browser. Favorites can be changed with favorite buttons which are shown in the community filter on the Internet and Favorites tabs. The commands `add_favorite_community` and `remove_favorite_community` are added to change the favorite communities via the console and for saving the favorite communities to the config file. For the favorite communities, additional tabs using the communities' icons are shown in the server browser next to the Internet, LAN and Favorites tabs. Each community tab shows only the servers from the respective community, hence the community filters UI is not shown on the community tabs but only on the Internet and Favorites tabs. The country and type filters on community tabs cover only the countries and types from the respective community. Favorite communities are added from left to right. When more than three favorite communities are added, the oldest (leftmost) favorite community will be removed from the list. When starting the client for the first time, i.e. with `cl_show_welcome 1`, the DDNet tab will be created as the only favorite community and selected initially. The community, country and type filters are unset when starting for the first time, so the Internet tab now shows all servers per default. When starting with a `ui_page` for a favorite community that is not configured, the page is reset to the Internet tab. This also affects those who upgrade from versions with the old DDNet and KoG tabs. The server browser is now also correctly updated when changing `ui_page` via the console. Track country and type filters for every community separately, to avoid filters resetting when switching between community tabs or changing the community filter. The commands `add_excluded_community`, `remove_excluded_community`, `add_excluded_country`, `remove_excluded_country`, `add_excluded_type` and `remove_excluded_type` are added to change the exclusion filters via the console and for saving the exclusion filters to the config file. Render community filters above the toolbox (filter, info and friends) tabs when on the Internet and Favorites tab, so this setting is more visible and can be changed also when the other toolbox tabs are selected. Add icon for the none community, based on the tee country flag color. This icon is hard-coded in the client, as the none community also is, so fetching the icon from the server would be inconvenient. Load community icons already when rendering the menu instead of only when rendering the server browser, so the icons are immediately available when using the start menu. Find tutorial server by searching for community type "Tutorial" instead of searching for "(Tutorial)" in the server name. Avoid cleaning favorite communities and filters when there are no communities, i.e. when the DDNet info failed to be loaded or does not contain any communities, to avoid losing all favorite communities and filters in this case. Closes #7774. --- CMakeLists.txt | 1 + data/communityicons/none.png | Bin 0 -> 2491 bytes src/engine/client.h | 1 - src/engine/client/client.h | 1 - src/engine/client/serverbrowser.cpp | 746 ++++++++++++++++--- src/engine/client/serverbrowser.h | 206 ++++- src/engine/serverbrowser.h | 14 +- src/engine/shared/config_variables.h | 5 +- src/game/client/components/menus.cpp | 119 ++- src/game/client/components/menus.h | 39 +- src/game/client/components/menus_browser.cpp | 157 ++-- src/game/client/components/menus_ingame.cpp | 35 +- src/game/client/components/menus_start.cpp | 7 +- 13 files changed, 1111 insertions(+), 220 deletions(-) create mode 100644 data/communityicons/none.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f0a0fbde..178ef3500 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1117,6 +1117,7 @@ set(EXPECTED_DATA autoexec_server.cfg blob.png censorlist.txt + communityicons/none.png console.png console_bar.png countryflags/AD.png diff --git a/data/communityicons/none.png b/data/communityicons/none.png new file mode 100644 index 0000000000000000000000000000000000000000..e29fccfce32975ad6ed2acf212796a0cdda7eb75 GIT binary patch literal 2491 zcmV;s2}JgZP) z>r)%o8Hb;}>n;!o0Twp4akv>%aAG&jc;W`v-}2YyYdh1%ou-|}Ht`J`(@Y#+Ktc#~ zS!uP}OFw`}NNpp55IR)PjAoG7J!hr!JMX^d-S?a$(MG=h`s;QU*W&|x^UXJHD@$ru zLA}QBEbS6OTLjQ+{L`TwA~?1HdX0a&99IO#5j?OO=XvE8vM3N+AS#IC(><~I80MGL%SE|_c zdZ(%s5?#}Xgn}sYk&JW8bj||!K6brM#j16xN@4N+K*#FpjLdp;EgiD}-GC(R-dSZ? z#xQgOh5>+O)v#?F*Yg1Wul$rVF91cB(G8vC-~ba77dbzcCKipLY8tAl0N^?zmkJ>_q3vYc4~D2l>BqK}#BPnf$l%gAtwNF+ie5<%BAB>AA9 z?fE`+yN+p^l*?r@nN1!)dB(G6&)Ll7ah$U$gfk}qNs<^y^f5Jgg_}1&15v=p+*TjEsyhIXS_r>kItzU;pOW$|||7Jf3&9ZVzWl0E(h8dF2wf zZhg+&+$_;(bl=Rkil(V(T8x?5DaOvHdHQsPd-uL$<;5C~u`7eLv{)0zYXJj~gCIk>L48HioXWUu3&AId_ zigL7G|1mU8V`AbGp-_-mG|Jz;{Rfq*)%JQijqq^*RaIDAnB(r z=&MhU4s&PeHsj;xPqgR%7Fm`V8%uNN&TYmoq&rscNg%8M3|(hoVUEekD`=-+id9u% zV&XE3i}M77or~SK8zBWCNfPJBMp<0^9g#@5>GNFI#c`bXhI)P9$8nsNB7ebPfW?J* zE{>o7XpEto5mEq#p>u8iD(UpOrhjp~2_Tv7q+xYqXHp>rx;KlP7 zRIAmd%9JFD;o%_`7p@T!)CveW4D9cVGe3V7!)R`-@47Clt1r3t?cXuWCNr~}+`9Dz z@xC|!<#L((-{0ql16wMss}&CAu76bi+r%9bRF zSS-qg3u7Hi;%FzrU9qaDOig}*t{Y9Cv$MU!W;VNj@3$<@!|yxqj>>XwHlR5W&geJ`xFWV9LK?NTndH4 z0msm+RIqBzhkA8glk~b3J_T z;PD?rk|cCpCmM}3eNML^o&dt(mV-Wus-o)$V+Fxr5LuQ{WSL+vcwjW4>v~fT;IC0t zg=o(LoIH_8>jH>`!^Gq9{r9pglS~ex>pGgQkxV8Jlo^l52!~s1Ohr+NL?Vb54iHKJ z61x$=R?I&@Vjw|!EWO{W2L}fkN~KU$6;)LkOb(JtrH~{EMNvqPr5P9)XsHfamN9gl zHcq(i2tv87K2WV$2ZA`wU>F87Gt-nyWeSA?bJyk=8cOb){p8>vpMG{7RaJ>Z!pzRj z5DW!ds>AbKs+P5PmEs_j007Emv*kohNs<^I8Rm~)-Nv#k`uqFQe-%X1bdBlhDN@5J zbX_MNkF^}ybsUFMsf=g|#li`|G%H8S)RkqKcwd}2@%R2;(=}45p(9_H>$;exdF1m? z3!!`ffQnh!wE`{P22K*+_o-Sn3dQo_2X`6537}@%tgZb>t&4K(_5v9-VXp z!U+Iir%>dddhcGhNrkbsXxBgKgW?>veq3+Ybt9x{ju4D2jrjD7%X{>oa&4I4>C+dF@#*z>KL7o7VlnY@0YE?j^u=Qg4D=r``PE8=cW>WP zEEYc+CiXm!(jmvRf+R_V!yzUvk25p<32Fx?r?d;<1dvGdq5mcw%=djv)8y@&H{|pA z5B6@)^DrxwwmWRWV1V%pX~N;wo*;Gu!U;f8?E;rBj?v#2|D|DN>Co|bW6fgE_3%6oNtRF)6}icCeb2}BTwK?s zR5Dp#&$7OerBbzoj0OrP0N8E7&0L<`Rvt-`QTNim66itOBpF2h8EC2{OfBvk~+cfHoaMVqsdfUdG0ZwJxxCT8D zoGz!jab$y@2u_Et*gCdBPXwK%t2U2r&=WytXy^9v{{RegisterCallback(CServerBrowser::ConfigSaveCallback, this); + m_pConsole->Register("add_favorite_community", "s[community_id]", CFGFLAG_CLIENT, Con_AddFavoriteCommunity, this, "Add a community as a favorite"); + m_pConsole->Register("remove_favorite_community", "s[community_id]", CFGFLAG_CLIENT, Con_RemoveFavoriteCommunity, this, "Remove a community from the favorites"); + m_pConsole->Register("add_excluded_community", "s[community_id]", CFGFLAG_CLIENT, Con_AddExcludedCommunity, this, "Add a community to the exclusion filter"); + m_pConsole->Register("remove_excluded_community", "s[community_id]", CFGFLAG_CLIENT, Con_RemoveExcludedCommunity, this, "Remove a community from the exclusion filter"); + m_pConsole->Register("add_excluded_country", "s[community_id] s[country_code]", CFGFLAG_CLIENT, Con_AddExcludedCountry, this, "Add a country to the exclusion filter for a specific community"); + m_pConsole->Register("remove_excluded_country", "s[community_id] s[country_code]", CFGFLAG_CLIENT, Con_RemoveExcludedCountry, this, "Remove a country from the exclusion filter for a specific community"); + m_pConsole->Register("add_excluded_type", "s[community_id] s[type]", CFGFLAG_CLIENT, Con_AddExcludedType, this, "Add a type to the exclusion filter for a specific community"); + m_pConsole->Register("remove_excluded_type", "s[community_id] s[type]", CFGFLAG_CLIENT, Con_RemoveExcludedType, this, "Remove a type from the exclusion filter for a specific community"); m_pConsole->Register("leak_ip_address_to_all_servers", "", CFGFLAG_CLIENT, Con_LeakIpAddress, this, "Leaks your IP address to all servers by pinging each of them, also acquiring the latency in the process"); } +void CServerBrowser::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + pThis->FavoriteCommunitiesFilter().Save(pConfigManager); + pThis->CommunitiesFilter().Save(pConfigManager); + pThis->CountriesFilter().Save(pConfigManager); + pThis->TypesFilter().Save(pConfigManager); +} + +void CServerBrowser::Con_AddFavoriteCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->FavoriteCommunitiesFilter().Add(pCommunityId); +} + +void CServerBrowser::Con_RemoveFavoriteCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->FavoriteCommunitiesFilter().Remove(pCommunityId); +} + +void CServerBrowser::Con_AddExcludedCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->CommunitiesFilter().Add(pCommunityId); +} + +void CServerBrowser::Con_RemoveExcludedCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->CommunitiesFilter().Remove(pCommunityId); +} + +void CServerBrowser::Con_AddExcludedCountry(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pCountryName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName)) + return; + pThis->CountriesFilter().Add(pCommunityId, pCountryName); +} + +void CServerBrowser::Con_RemoveExcludedCountry(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pCountryName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName)) + return; + pThis->CountriesFilter().Remove(pCommunityId, pCountryName); +} + +void CServerBrowser::Con_AddExcludedType(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pTypeName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName)) + return; + pThis->TypesFilter().Add(pCommunityId, pTypeName); +} + +void CServerBrowser::Con_RemoveExcludedType(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pTypeName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName)) + return; + pThis->TypesFilter().Remove(pCommunityId, pTypeName); +} + void CServerBrowser::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData) { - CServerBrowser *pThis = (CServerBrowser *)pUserData; + CServerBrowser *pThis = static_cast(pUserData); // We only consider the first address of every server. @@ -170,6 +263,50 @@ void CServerBrowser::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserDa } } +static bool ValidIdentifier(const char *pId, size_t MaxLength) +{ + if(pId[0] == '\0' || (size_t)str_length(pId) >= MaxLength) + { + return false; + } + + for(int i = 0; pId[i] != '\0'; ++i) + { + if(pId[i] == '"' || pId[i] == '/' || pId[i] == '\\') + { + return false; + } + } + return true; +} + +static bool ValidateIdentifier(const char *pId, size_t MaxLength, const char *pContext, IConsole *pConsole) +{ + if(!ValidIdentifier(pId, MaxLength)) + { + char aError[32 + IConsole::CMDLINE_LENGTH]; + str_format(aError, sizeof(aError), "%s '%s' is not valid", pContext, pId); + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowser", aError); + return false; + } + return true; +} + +bool CServerBrowser::ValidateCommunityId(const char *pCommunityId) const +{ + return ValidateIdentifier(pCommunityId, CServerInfo::MAX_COMMUNITY_ID_LENGTH, "Community ID", m_pConsole); +} + +bool CServerBrowser::ValidateCountryName(const char *pCountryName) const +{ + return ValidateIdentifier(pCountryName, CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH, "Country name", m_pConsole); +} + +bool CServerBrowser::ValidateTypeName(const char *pTypeName) const +{ + return ValidateIdentifier(pTypeName, CServerInfo::MAX_COMMUNITY_TYPE_LENGTH, "Type name", m_pConsole); +} + int CServerBrowser::Players(const CServerInfo &Item) const { return g_Config.m_BrFilterSpectators ? Item.m_NumPlayers : Item.m_NumClients; @@ -299,11 +436,18 @@ void CServerBrowser::Filter() Filtered = true; else { - if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) + if(!Communities().empty()) { - Filtered = CommunitiesFilter().Filtered(Info.m_aCommunityId); - Filtered = Filtered || CountriesFilter().Filtered(Info.m_aCommunityCountry); - Filtered = Filtered || TypesFilter().Filtered(Info.m_aCommunityType); + if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) + { + Filtered = CommunitiesFilter().Filtered(Info.m_aCommunityId); + } + if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES || + (m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_3)) + { + Filtered = Filtered || CountriesFilter().Filtered(Info.m_aCommunityCountry); + Filtered = Filtered || TypesFilter().Filtered(Info.m_aCommunityType); + } } if(!Filtered && g_Config.m_BrFilterCountry) @@ -771,9 +915,9 @@ void CServerBrowser::OnServerInfoUpdate(const NETADDR &Addr, int Token, const CS RequestResort(); } -void CServerBrowser::Refresh(int Type) +void CServerBrowser::Refresh(int Type, bool Force) { - bool ServerListTypeChanged = m_ServerlistType != Type; + bool ServerListTypeChanged = Force || m_ServerlistType != Type; int OldServerListType = m_ServerlistType; m_ServerlistType = Type; secure_random_fill(m_aTokenSeed, sizeof(m_aTokenSeed)); @@ -813,7 +957,7 @@ void CServerBrowser::Refresh(int Type) if(g_Config.m_Debug) m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "serverbrowser", "broadcasting for servers"); } - else if(Type == IServerBrowser::TYPE_FAVORITES || Type == IServerBrowser::TYPE_INTERNET) + else { m_pHttp->Refresh(); m_pPingCache->Load(); @@ -915,7 +1059,37 @@ void CServerBrowser::UpdateFromHttp() std::function Want = [](const NETADDR *pAddrs, int NumAddrs) { return true; }; if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { - Want = [&](const NETADDR *pAddrs, int NumAddrs) -> bool { return m_pFavorites->IsFavorite(pAddrs, NumAddrs) != TRISTATE::NONE; }; + Want = [this](const NETADDR *pAddrs, int NumAddrs) -> bool { + return m_pFavorites->IsFavorite(pAddrs, NumAddrs) != TRISTATE::NONE; + }; + } + else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_3) + { + const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; + std::vector vpFavoriteCommunities = FavoriteCommunities(); + dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid community index"); + const CCommunity *pWantedCommunity = vpFavoriteCommunities[CommunityIndex]; + const bool IsNoneCommunity = str_comp(pWantedCommunity->Id(), COMMUNITY_NONE) == 0; + Want = [this, pWantedCommunity, IsNoneCommunity](const NETADDR *pAddrs, int NumAddrs) -> bool { + for(int AddressIndex = 0; AddressIndex < NumAddrs; AddressIndex++) + { + const auto CommunityServer = m_CommunityServersByAddr.find(pAddrs[AddressIndex]); + if(CommunityServer != m_CommunityServersByAddr.end()) + { + if(IsNoneCommunity) + { + // Servers with community "none" are not present in m_CommunityServersByAddr, so we ignore + // any server that is found in this map to determine only the servers without community. + return false; + } + else if(str_comp(CommunityServer->second.CommunityId(), pWantedCommunity->Id()) == 0) + { + return true; + } + } + } + return IsNoneCommunity; + }; } for(int i = 0; i < NumServers; i++) @@ -1233,14 +1407,12 @@ void CServerBrowser::LoadDDNetServers() if(!m_pDDNetInfo) { - CleanFilters(); return; } const json_value &Communities = (*m_pDDNetInfo)["communities"]; if(Communities.type != json_array) { - CleanFilters(); return; } @@ -1382,11 +1554,6 @@ void CServerBrowser::UpdateServerRank(CServerInfo *pInfo) const const char *CServerBrowser::GetTutorialServer() { - // Use internet tab as default after joining tutorial, also makes sure Find() actually works. - // Note that when no server info has been loaded yet, this will not return a result immediately. - m_pConfigManager->Reset("ui_page"); - Refresh(IServerBrowser::TYPE_INTERNET); - const CCommunity *pCommunity = Community(COMMUNITY_DDNET); if(pCommunity == nullptr) return nullptr; @@ -1397,10 +1564,10 @@ const char *CServerBrowser::GetTutorialServer() { for(const auto &Server : Country.Servers()) { - CServerEntry *pEntry = Find(Server.Address()); - if(!pEntry) + if(str_comp(Server.TypeName(), "Tutorial") != 0) continue; - if(str_find(pEntry->m_Info.m_aName, "(Tutorial)") == 0) + const CServerEntry *pEntry = Find(Server.Address()); + if(!pEntry) continue; if(pEntry->m_Info.m_NumPlayers > pEntry->m_Info.m_MaxPlayers - 10) continue; @@ -1433,6 +1600,20 @@ int CServerBrowser::LoadingProgression() const return 100.0f * Loaded / Servers; } +bool CCommunity::HasCountry(const char *pCountryName) const +{ + return std::find_if(Countries().begin(), Countries().end(), [pCountryName](const auto &Elem) { + return str_comp(Elem.Name(), pCountryName) == 0; + }) != Countries().end(); +} + +bool CCommunity::HasType(const char *pTypeName) const +{ + return std::find_if(Types().begin(), Types().end(), [pTypeName](const auto &Elem) { + return str_comp(Elem.Name(), pTypeName) == 0; + }) != Types().end(); +} + CServerInfo::ERankState CCommunity::HasRank(const char *pMap) const { if(!HasRanks()) @@ -1467,121 +1648,474 @@ std::vector CServerBrowser::SelectedCommunities() const return vpSelected; } -void CFilterList::Add(const char *pElement) +std::vector CServerBrowser::FavoriteCommunities() const { - if(Filtered(pElement)) - return; - - if(m_pFilter[0] != '\0') - str_append(m_pFilter, ",", m_FilterSize); - str_append(m_pFilter, pElement, m_FilterSize); + // This is done differently than SelectedCommunities because the favorite + // communities should be returned in the order specified by the user. + std::vector vpFavorites; + for(const auto &CommunityId : FavoriteCommunitiesFilter().Entries()) + { + const CCommunity *pCommunity = Community(CommunityId.Id()); + if(pCommunity) + { + vpFavorites.push_back(pCommunity); + } + } + return vpFavorites; } -void CFilterList::Remove(const char *pElement) +std::vector CServerBrowser::CurrentCommunities() const { - if(!Filtered(pElement)) - return; - - // rewrite exclude/filter list - char aBuf[512]; - - str_copy(aBuf, m_pFilter); - m_pFilter[0] = '\0'; - - char aToken[512]; - for(const char *pTok = aBuf; (pTok = str_next_token(pTok, ",", aToken, sizeof(aToken)));) + if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { - if(str_comp_nocase(pElement, aToken) != 0) + return SelectedCommunities(); + } + else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_3) + { + const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; + std::vector vpFavoriteCommunities = FavoriteCommunities(); + dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid favorite community serverbrowser type"); + return {vpFavoriteCommunities[CommunityIndex]}; + } + else + { + return {}; + } +} + +unsigned CServerBrowser::CurrentCommunitiesHash() const +{ + std::vector vpCommunities = CurrentCommunities(); + unsigned Hash = 5381; + for(const CCommunity *pCommunity : CurrentCommunities()) + { + Hash = (Hash << 5) + Hash + str_quickhash(pCommunity->Id()); + } + return Hash; +} + +void CFavoriteCommunityFilterList::Add(const char *pCommunityId) +{ + // Remove community if it's already a favorite, so it will be added again at + // the end of the list, to allow setting the entire list easier with binds. + Remove(pCommunityId); + + // Ensure maximum number of favorite communities, by removing least-recently + // added communities from the beginning. One more than the maximum is removed + // to make room for the new community. + constexpr size_t MaxFavoriteCommunities = 3; + if(m_vEntries.size() >= MaxFavoriteCommunities) + { + m_vEntries.erase(m_vEntries.begin(), m_vEntries.begin() + (MaxFavoriteCommunities - 1)); + } + m_vEntries.emplace_back(pCommunityId); +} + +void CFavoriteCommunityFilterList::Remove(const char *pCommunityId) +{ + auto FoundCommunity = std::find(m_vEntries.begin(), m_vEntries.end(), CCommunityId(pCommunityId)); + if(FoundCommunity != m_vEntries.end()) + { + m_vEntries.erase(FoundCommunity); + } +} + +void CFavoriteCommunityFilterList::Clear() +{ + m_vEntries.clear(); +} + +bool CFavoriteCommunityFilterList::Filtered(const char *pCommunityId) const +{ + return std::find(m_vEntries.begin(), m_vEntries.end(), CCommunityId(pCommunityId)) != m_vEntries.end(); +} + +bool CFavoriteCommunityFilterList::Empty() const +{ + return m_vEntries.empty(); +} + +void CFavoriteCommunityFilterList::Clean(const std::vector &vAllowedCommunities) +{ + auto It = std::remove_if(m_vEntries.begin(), m_vEntries.end(), [&](const auto &Community) { + return std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(Community.Id(), AllowedCommunity.Id()) == 0; + }) == vAllowedCommunities.end(); + }); + m_vEntries.erase(It, m_vEntries.end()); +} + +void CFavoriteCommunityFilterList::Save(IConfigManager *pConfigManager) const +{ + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + for(const auto &FavoriteCommunity : m_vEntries) + { + str_copy(aBuf, "add_favorite_community \""); + str_append(aBuf, FavoriteCommunity.Id()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } +} + +const std::vector &CFavoriteCommunityFilterList::Entries() const +{ + return m_vEntries; +} + +void CExcludedCommunityFilterList::Add(const char *pCommunityId) +{ + m_Entries.emplace(pCommunityId); +} + +void CExcludedCommunityFilterList::Remove(const char *pCommunityId) +{ + m_Entries.erase(CCommunityId(pCommunityId)); +} + +void CExcludedCommunityFilterList::Clear() +{ + m_Entries.clear(); +} + +bool CExcludedCommunityFilterList::Filtered(const char *pCommunityId) const +{ + return std::find(m_Entries.begin(), m_Entries.end(), CCommunityId(pCommunityId)) != m_Entries.end(); +} + +bool CExcludedCommunityFilterList::Empty() const +{ + return m_Entries.empty(); +} + +void CExcludedCommunityFilterList::Clean(const std::vector &vAllowedCommunities) +{ + for(auto It = m_Entries.begin(); It != m_Entries.end();) + { + const bool Found = std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(It->Id(), AllowedCommunity.Id()) == 0; + }) != vAllowedCommunities.end(); + if(Found) { - if(m_pFilter[0] != '\0') - str_append(m_pFilter, ",", m_FilterSize); - str_append(m_pFilter, aToken, m_FilterSize); + ++It; + } + else + { + It = m_Entries.erase(It); + } + } + // Prevent filter that would exclude all allowed communities + if(m_Entries.size() == vAllowedCommunities.size()) + { + m_Entries.clear(); + } +} + +void CExcludedCommunityFilterList::Save(IConfigManager *pConfigManager) const +{ + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + for(const auto &ExcludedCommunity : m_Entries) + { + str_copy(aBuf, "add_excluded_community \""); + str_append(aBuf, ExcludedCommunity.Id()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } +} + +void CExcludedCommunityCountryFilterList::Add(const char *pCountryName) +{ + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) + { + if(pCommunity->HasCountry(pCountryName)) + { + Add(pCommunity->Id(), pCountryName); } } } -void CFilterList::Clear() +void CExcludedCommunityCountryFilterList::Add(const char *pCommunityId, const char *pCountryName) { - m_pFilter[0] = '\0'; + CCommunityId CommunityId(pCommunityId); + if(m_Entries.find(CommunityId) == m_Entries.end()) + { + m_Entries[CommunityId] = {}; + } + m_Entries[CommunityId].emplace(pCountryName); } -bool CFilterList::Filtered(const char *pElement) const +void CExcludedCommunityCountryFilterList::Remove(const char *pCountryName) +{ + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) + { + Remove(pCommunity->Id(), pCountryName); + } +} + +void CExcludedCommunityCountryFilterList::Remove(const char *pCommunityId, const char *pCountryName) +{ + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunityId)); + if(CommunityEntry != m_Entries.end()) + { + CommunityEntry->second.erase(pCountryName); + } +} + +void CExcludedCommunityCountryFilterList::Clear() +{ + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) + { + auto CommunityEntry = m_Entries.find(pCommunity->Id()); + if(CommunityEntry != m_Entries.end()) + { + CommunityEntry->second.clear(); + } + } +} + +bool CExcludedCommunityCountryFilterList::Filtered(const char *pCountryName) const { // If the needle is not defined, we exclude it if there is any other // exclusion, i.e. we only show those elements when the filter is empty. - if(pElement[0] == '\0') + if(pCountryName[0] == '\0') return !Empty(); - // Special case: "*element" means anything except that element is excluded. - // Necessary because the default filter cannot exclude unknown elements, - // but we want to select only the DDNet community by default. - if(m_pFilter[0] == '*') - return str_comp(m_pFilter + 1, pElement) != 0; + const auto Communities = m_CurrentCommunitiesGetter(); + return std::none_of(Communities.begin(), Communities.end(), [&](const CCommunity *pCommunity) { + if(!pCommunity->HasCountry(pCountryName)) + return false; - // Comma separated list of excluded elements. - return str_in_list(m_pFilter, ",", pElement); + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); + if(CommunityEntry == m_Entries.end()) + return true; + + const auto &CountryEntries = CommunityEntry->second; + if(CountryEntries.find(CCommunityCountryName(pCountryName)) == CountryEntries.end()) + return true; + + return false; + }); } -bool CFilterList::Empty() const +bool CExcludedCommunityCountryFilterList::Empty() const { - return m_pFilter[0] == '\0'; -} - -void CFilterList::Clean(const std::vector &vpAllowedElements) -{ - size_t NumFiltered = 0; - char aNewList[512]; - aNewList[0] = '\0'; - - for(const char *pElement : vpAllowedElements) + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) { - if(Filtered(pElement)) + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); + return CommunityEntry == m_Entries.end() || CommunityEntry->second.empty(); + } + return false; +} + +void CExcludedCommunityCountryFilterList::Clean(const std::vector &vAllowedCommunities) +{ + for(auto It = m_Entries.begin(); It != m_Entries.end();) + { + const bool Found = std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(It->first.Id(), AllowedCommunity.Id()) == 0; + }) != vAllowedCommunities.end(); + if(Found) { - if(aNewList[0] != '\0') - str_append(aNewList, ","); - str_append(aNewList, pElement); - ++NumFiltered; + ++It; + } + else + { + It = m_Entries.erase(It); } } - // Prevent filter that would exclude all allowed elements - if(NumFiltered == vpAllowedElements.size()) - m_pFilter[0] = '\0'; - else - str_copy(m_pFilter, aNewList, m_FilterSize); + for(const CCommunity &AllowedCommunity : vAllowedCommunities) + { + auto CommunityEntry = m_Entries.find(CCommunityId(AllowedCommunity.Id())); + if(CommunityEntry != m_Entries.end()) + { + auto &CountryEntries = CommunityEntry->second; + for(auto It = CountryEntries.begin(); It != CountryEntries.end();) + { + if(AllowedCommunity.HasCountry(It->Name())) + { + ++It; + } + else + { + It = CountryEntries.erase(It); + } + } + // Prevent filter that would exclude all allowed countries + if(CountryEntries.size() == AllowedCommunity.Countries().size()) + { + CountryEntries.clear(); + } + } + } +} + +void CExcludedCommunityCountryFilterList::Save(IConfigManager *pConfigManager) const +{ + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH]; + for(const auto &[Community, Countries] : m_Entries) + { + for(const auto &Country : Countries) + { + str_copy(aBuf, "add_excluded_country \""); + str_append(aBuf, Community.Id()); + str_append(aBuf, "\" \""); + str_append(aBuf, Country.Name()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } + } +} + +void CExcludedCommunityTypeFilterList::Add(const char *pTypeName) +{ + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) + { + if(pCommunity->HasType(pTypeName)) + { + Add(pCommunity->Id(), pTypeName); + } + } +} + +void CExcludedCommunityTypeFilterList::Add(const char *pCommunityId, const char *pTypeName) +{ + CCommunityId CommunityId(pCommunityId); + if(m_Entries.find(CommunityId) == m_Entries.end()) + { + m_Entries[CommunityId] = {}; + } + m_Entries[CommunityId].emplace(pTypeName); +} + +void CExcludedCommunityTypeFilterList::Remove(const char *pTypeName) +{ + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) + { + Remove(pCommunity->Id(), pTypeName); + } +} + +void CExcludedCommunityTypeFilterList::Remove(const char *pCommunityId, const char *pTypeName) +{ + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunityId)); + if(CommunityEntry != m_Entries.end()) + { + CommunityEntry->second.erase(pTypeName); + } +} + +void CExcludedCommunityTypeFilterList::Clear() +{ + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) + { + auto CommunityEntry = m_Entries.find(pCommunity->Id()); + if(CommunityEntry != m_Entries.end()) + { + CommunityEntry->second.clear(); + } + } +} + +bool CExcludedCommunityTypeFilterList::Filtered(const char *pTypeName) const +{ + // If the needle is not defined, we exclude it if there is any other + // exclusion, i.e. we only show those elements when the filter is empty. + if(pTypeName[0] == '\0') + return !Empty(); + + const auto Communities = m_CurrentCommunitiesGetter(); + return std::none_of(Communities.begin(), Communities.end(), [&](const CCommunity *pCommunity) { + if(!pCommunity->HasType(pTypeName)) + return false; + + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); + if(CommunityEntry == m_Entries.end()) + return true; + + const auto &TypeEntries = CommunityEntry->second; + return TypeEntries.find(CCommunityTypeName(pTypeName)) == TypeEntries.end(); + }); +} + +bool CExcludedCommunityTypeFilterList::Empty() const +{ + for(const CCommunity *pCommunity : m_CurrentCommunitiesGetter()) + { + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunity->Id())); + return CommunityEntry == m_Entries.end() || CommunityEntry->second.empty(); + } + return false; +} + +void CExcludedCommunityTypeFilterList::Clean(const std::vector &vAllowedCommunities) +{ + for(auto It = m_Entries.begin(); It != m_Entries.end();) + { + const bool Found = std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(It->first.Id(), AllowedCommunity.Id()) == 0; + }) != vAllowedCommunities.end(); + if(Found) + { + ++It; + } + else + { + It = m_Entries.erase(It); + } + } + + for(const CCommunity &AllowedCommunity : vAllowedCommunities) + { + auto CommunityEntry = m_Entries.find(CCommunityId(AllowedCommunity.Id())); + if(CommunityEntry != m_Entries.end()) + { + auto &TypeEntries = CommunityEntry->second; + for(auto It = TypeEntries.begin(); It != TypeEntries.end();) + { + if(AllowedCommunity.HasType(It->Name())) + { + ++It; + } + else + { + It = TypeEntries.erase(It); + } + } + // Prevent filter that would exclude all allowed countries + if(TypeEntries.size() == AllowedCommunity.Types().size()) + { + TypeEntries.clear(); + } + } + } +} + +void CExcludedCommunityTypeFilterList::Save(IConfigManager *pConfigManager) const +{ + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_TYPE_LENGTH]; + for(const auto &[Community, Types] : m_Entries) + { + for(const auto &Type : Types) + { + str_copy(aBuf, "add_excluded_type \""); + str_append(aBuf, Community.Id()); + str_append(aBuf, "\" \""); + str_append(aBuf, Type.Name()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } + } } void CServerBrowser::CleanFilters() { - CommunitiesFilterClean(); - CountriesFilterClean(); - TypesFilterClean(); -} - -void CServerBrowser::CommunitiesFilterClean() -{ - std::vector vpCommunityNames; - for(const auto &Community : Communities()) - vpCommunityNames.push_back(Community.Id()); - m_CommunitiesFilter.Clean(vpCommunityNames); -} - -void CServerBrowser::CountriesFilterClean() -{ - std::vector vpCountryNames; - for(const CCommunity *pCommunity : SelectedCommunities()) - for(const auto &Country : pCommunity->Countries()) - vpCountryNames.push_back(Country.Name()); - m_CountriesFilter.Clean(vpCountryNames); -} - -void CServerBrowser::TypesFilterClean() -{ - std::vector vpTypeNames; - for(const CCommunity *pCommunity : SelectedCommunities()) - for(const auto &Type : pCommunity->Types()) - vpTypeNames.push_back(Type.Name()); - m_TypesFilter.Clean(vpTypeNames); + // Keep filters if we failed to load any communities + if(Communities().empty()) + return; + FavoriteCommunitiesFilter().Clean(Communities()); + CommunitiesFilter().Clean(Communities()); + CountriesFilter().Clean(Communities()); + TypesFilter().Clean(Communities()); } bool CServerBrowser::IsRegistered(const NETADDR &Addr) diff --git a/src/engine/client/serverbrowser.h b/src/engine/client/serverbrowser.h index a8df4ec97..ce40d86f5 100644 --- a/src/engine/client/serverbrowser.h +++ b/src/engine/client/serverbrowser.h @@ -9,7 +9,9 @@ #include #include +#include #include +#include typedef struct _json_value json_value; class CNetClient; @@ -23,6 +25,87 @@ class IServerBrowserPingCache; class IStorage; class IHttp; +class CCommunityId +{ + char m_aId[CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + +public: + CCommunityId(const char *pCommunityId) + { + str_copy(m_aId, pCommunityId); + } + + const char *Id() const { return m_aId; } + + bool operator==(const CCommunityId &Other) const + { + return str_comp(Id(), Other.Id()) == 0; + } +}; + +template<> +struct std::hash +{ + size_t operator()(const CCommunityId &Elem) const noexcept + { + return str_quickhash(Elem.Id()); + } +}; + +class CCommunityCountryName +{ + char m_aName[CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH]; + +public: + CCommunityCountryName(const char *pCountryName) + { + str_copy(m_aName, pCountryName); + } + + const char *Name() const { return m_aName; } + + bool operator==(const CCommunityCountryName &Other) const + { + return str_comp(Name(), Other.Name()) == 0; + } +}; + +template<> +struct std::hash +{ + size_t operator()(const CCommunityCountryName &Elem) const noexcept + { + return str_quickhash(Elem.Name()); + } +}; + +class CCommunityTypeName +{ + char m_aName[CServerInfo::MAX_COMMUNITY_TYPE_LENGTH]; + +public: + CCommunityTypeName(const char *pTypeName) + { + str_copy(m_aName, pTypeName); + } + + const char *Name() const { return m_aName; } + + bool operator==(const CCommunityTypeName &Other) const + { + return str_comp(Name(), Other.Name()) == 0; + } +}; + +template<> +struct std::hash +{ + size_t operator()(const CCommunityTypeName &Elem) const noexcept + { + return str_quickhash(Elem.Name()); + } +}; + class CCommunityServer { char m_aCommunityId[CServerInfo::MAX_COMMUNITY_ID_LENGTH]; @@ -42,23 +125,81 @@ public: const char *TypeName() const { return m_aTypeName; } }; -class CFilterList : public IFilterList +class CFavoriteCommunityFilterList : public IFilterList { - char *m_pFilter; - size_t m_FilterSize; - public: - CFilterList(char *pFilter, size_t FilterSize) : - m_pFilter(pFilter), m_FilterSize(FilterSize) + void Add(const char *pCommunityId) override; + void Remove(const char *pCommunityId) override; + void Clear() override; + bool Filtered(const char *pCommunityId) const override; + bool Empty() const override; + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; + const std::vector &Entries() const; + +private: + std::vector m_vEntries; +}; + +class CExcludedCommunityFilterList : public IFilterList +{ +public: + void Add(const char *pCommunityId) override; + void Remove(const char *pCommunityId) override; + void Clear() override; + bool Filtered(const char *pCommunityId) const override; + bool Empty() const override; + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; + +private: + std::unordered_set m_Entries; +}; + +class CExcludedCommunityCountryFilterList : public IFilterList +{ +public: + CExcludedCommunityCountryFilterList(std::function()> CurrentCommunitiesGetter) : + m_CurrentCommunitiesGetter(CurrentCommunitiesGetter) { } - void Add(const char *pElement) override; - void Remove(const char *pElement) override; + void Add(const char *pCountryName) override; + void Add(const char *pCommunityId, const char *pCountryName); + void Remove(const char *pCountryName) override; + void Remove(const char *pCommunityId, const char *pCountryName); void Clear() override; - bool Filtered(const char *pElement) const override; + bool Filtered(const char *pCountryName) const override; bool Empty() const override; - void Clean(const std::vector &vpAllowedElements); + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; + +private: + std::function()> m_CurrentCommunitiesGetter; + std::unordered_map> m_Entries; +}; + +class CExcludedCommunityTypeFilterList : public IFilterList +{ +public: + CExcludedCommunityTypeFilterList(std::function()> CurrentCommunitiesGetter) : + m_CurrentCommunitiesGetter(CurrentCommunitiesGetter) + { + } + + void Add(const char *pTypeName) override; + void Add(const char *pCommunityId, const char *pTypeName); + void Remove(const char *pTypeName) override; + void Remove(const char *pCommunityId, const char *pTypeName); + void Clear() override; + bool Filtered(const char *pTypeName) const override; + bool Empty() const override; + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; + +private: + std::function()> m_CurrentCommunitiesGetter; + std::unordered_map> m_Entries; }; class CServerBrowser : public IServerBrowser @@ -80,7 +221,7 @@ public: virtual ~CServerBrowser(); // interface functions - void Refresh(int Type) override; + void Refresh(int Type, bool Force = false) override; bool IsRefreshing() const override; bool IsGettingServerlist() const override; int LoadingProgression() const override; @@ -106,20 +247,23 @@ public: const std::vector &Communities() const override; const CCommunity *Community(const char *pCommunityId) const override; std::vector SelectedCommunities() const override; + std::vector FavoriteCommunities() const override; + std::vector CurrentCommunities() const override; + unsigned CurrentCommunitiesHash() const override; + + bool DDNetInfoAvailable() const override { return m_pDDNetInfo != nullptr; } int64_t DDNetInfoUpdateTime() const override { return m_DDNetInfoUpdateTime; } - CFilterList &CommunitiesFilter() override { return m_CommunitiesFilter; } - CFilterList &CountriesFilter() override { return m_CountriesFilter; } - CFilterList &TypesFilter() override { return m_TypesFilter; } - const CFilterList &CommunitiesFilter() const override { return m_CommunitiesFilter; } - const CFilterList &CountriesFilter() const override { return m_CountriesFilter; } - const CFilterList &TypesFilter() const override { return m_TypesFilter; } + CFavoriteCommunityFilterList &FavoriteCommunitiesFilter() override { return m_FavoriteCommunitiesFilter; } + CExcludedCommunityFilterList &CommunitiesFilter() override { return m_CommunitiesFilter; } + CExcludedCommunityCountryFilterList &CountriesFilter() override { return m_CountriesFilter; } + CExcludedCommunityTypeFilterList &TypesFilter() override { return m_TypesFilter; } + const CFavoriteCommunityFilterList &FavoriteCommunitiesFilter() const override { return m_FavoriteCommunitiesFilter; } + const CExcludedCommunityFilterList &CommunitiesFilter() const override { return m_CommunitiesFilter; } + const CExcludedCommunityCountryFilterList &CountriesFilter() const override { return m_CountriesFilter; } + const CExcludedCommunityTypeFilterList &TypesFilter() const override { return m_TypesFilter; } void CleanFilters() override; - void CommunitiesFilterClean(); - void CountriesFilterClean(); - void TypesFilterClean(); - // void Update(); void OnServerInfoUpdate(const NETADDR &Addr, int Token, const CServerInfo *pInfo); @@ -162,9 +306,10 @@ private: int m_OwnLocation = CServerInfo::LOC_UNKNOWN; - CFilterList m_CommunitiesFilter; - CFilterList m_CountriesFilter; - CFilterList m_TypesFilter; + CFavoriteCommunityFilterList m_FavoriteCommunitiesFilter; + CExcludedCommunityFilterList m_CommunitiesFilter; + CExcludedCommunityCountryFilterList m_CountriesFilter; + CExcludedCommunityTypeFilterList m_TypesFilter; json_value *m_pDDNetInfo; int64_t m_DDNetInfoUpdateTime; @@ -217,8 +362,21 @@ private: void RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int *pBasicToken, int *pToken, bool RandomToken) const; void RegisterCommands(); + static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData); + static void Con_AddFavoriteCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveFavoriteCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_AddExcludedCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveExcludedCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_AddExcludedCountry(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveExcludedCountry(IConsole::IResult *pResult, void *pUserData); + static void Con_AddExcludedType(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveExcludedType(IConsole::IResult *pResult, void *pUserData); static void Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData); + bool ValidateCommunityId(const char *pCommunityId) const; + bool ValidateCountryName(const char *pCountryName) const; + bool ValidateTypeName(const char *pTypeName) const; + void SetInfo(CServerEntry *pEntry, const CServerInfo &Info) const; void SetLatency(NETADDR Addr, int Latency); diff --git a/src/engine/serverbrowser.h b/src/engine/serverbrowser.h index c3e83a200..2e9cb6351 100644 --- a/src/engine/serverbrowser.h +++ b/src/engine/serverbrowser.h @@ -231,6 +231,8 @@ public: const SHA256_DIGEST &IconSha256() const { return m_IconSha256; } const std::vector &Countries() const { return m_vCountries; } const std::vector &Types() const { return m_vTypes; } + bool HasCountry(const char *pCountryName) const; + bool HasType(const char *pTypeName) const; bool HasRanks() const { return m_HasFinishes; } CServerInfo::ERankState HasRank(const char *pMap) const; }; @@ -271,6 +273,9 @@ public: TYPE_INTERNET = 0, TYPE_LAN, TYPE_FAVORITES, + TYPE_FAVORITE_COMMUNITY_1, + TYPE_FAVORITE_COMMUNITY_2, + TYPE_FAVORITE_COMMUNITY_3, NUM_TYPES, }; @@ -279,7 +284,7 @@ public: static constexpr const char *SEARCH_EXCLUDE_TOKEN = ";"; - virtual void Refresh(int Type) = 0; + virtual void Refresh(int Type, bool Force = false) = 0; virtual bool IsGettingServerlist() const = 0; virtual bool IsRefreshing() const = 0; virtual int LoadingProgression() const = 0; @@ -296,11 +301,18 @@ public: virtual const std::vector &Communities() const = 0; virtual const CCommunity *Community(const char *pCommunityId) const = 0; virtual std::vector SelectedCommunities() const = 0; + virtual std::vector FavoriteCommunities() const = 0; + virtual std::vector CurrentCommunities() const = 0; + virtual unsigned CurrentCommunitiesHash() const = 0; + + virtual bool DDNetInfoAvailable() const = 0; virtual int64_t DDNetInfoUpdateTime() const = 0; + virtual IFilterList &FavoriteCommunitiesFilter() = 0; virtual IFilterList &CommunitiesFilter() = 0; virtual IFilterList &CountriesFilter() = 0; virtual IFilterList &TypesFilter() = 0; + virtual const IFilterList &FavoriteCommunitiesFilter() const = 0; virtual const IFilterList &CommunitiesFilter() const = 0; virtual const IFilterList &CountriesFilter() const = 0; virtual const IFilterList &TypesFilter() const = 0; diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 2c4a289f9..79b5c1deb 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -134,7 +134,7 @@ MACRO_CONFIG_INT(ClPlayerDefaultEyes, player_default_eyes, 0, 0, 5, CFGFLAG_CLIE MACRO_CONFIG_STR(ClSkinPrefix, cl_skin_prefix, 12, "", CFGFLAG_CLIENT | CFGFLAG_SAVE, "Replace the skins by skins with this prefix (e.g. kitty, santa)") MACRO_CONFIG_INT(ClFatSkins, cl_fat_skins, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable fat skins") -MACRO_CONFIG_INT(UiPage, ui_page, 6, 6, 10, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface page") +MACRO_CONFIG_INT(UiPage, ui_page, 6, 6, 11, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface page") MACRO_CONFIG_INT(UiSettingsPage, ui_settings_page, 0, 0, 9, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface settings page") MACRO_CONFIG_INT(UiToolboxPage, ui_toolbox_page, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Toolbox page") MACRO_CONFIG_STR(UiServerAddress, ui_server_address, 1024, "localhost:8303", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Interface server address") @@ -286,9 +286,6 @@ MACRO_CONFIG_INT(BrFilterConnectingPlayers, br_filter_connecting_players, 1, 0, MACRO_CONFIG_STR(BrFilterServerAddress, br_filter_serveraddress, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Server address to filter") MACRO_CONFIG_INT(BrFilterUnfinishedMap, br_filter_unfinished_map, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Show only servers with unfinished maps") -MACRO_CONFIG_STR(BrFilterExcludeCommunities, br_filter_exclude_communities, 512, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out servers by community") -MACRO_CONFIG_STR(BrFilterExcludeCountries, br_filter_exclude_countries, 512, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out communities' servers by country") -MACRO_CONFIG_STR(BrFilterExcludeTypes, br_filter_exclude_types, 512, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out communities' servers by gametype") MACRO_CONFIG_INT(BrIndicateFinished, br_indicate_finished, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Show whether you have finished a DDNet map (transmits your player name to info.ddnet.org/info)") MACRO_CONFIG_STR(BrLocation, br_location, 16, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Override location for ping estimation, available: auto, af, as, as:cn, eu, na, oc, sa (Automatic, Africa, Asia, China, Europe, North America, Oceania/Australia, South America") MACRO_CONFIG_STR(BrCachedBestServerinfoUrl, br_cached_best_serverinfo_url, 256, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Do not set this variable, instead create a ddnet-serverlist-urls.cfg next to settings_ddnet.cfg to specify all possible serverlist URLs") diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 3a50623df..d084eb7f7 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -59,7 +59,6 @@ CMenus::CMenus() m_ActivePage = PAGE_INTERNET; m_MenuPage = 0; m_GamePage = PAGE_GAME; - m_JoinTutorial = false; m_NeedRestartGraphics = false; m_NeedRestartSound = false; @@ -157,7 +156,7 @@ int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, return UI()->DoButtonLogic(pButtonContainer, Checked, pRect); } -int CMenus::DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator, const ColorRGBA *pDefaultColor, const ColorRGBA *pActiveColor, const ColorRGBA *pHoverColor, float EdgeRounding) +int CMenus::DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator, const ColorRGBA *pDefaultColor, const ColorRGBA *pActiveColor, const ColorRGBA *pHoverColor, float EdgeRounding, const SCommunityIcon *pCommunityIcon) { const bool MouseInside = UI()->HotItem() == pButtonContainer; CUIRect Rect = *pRect; @@ -230,9 +229,18 @@ int CMenus::DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pTe } } - CUIRect Temp; - Rect.HMargin(2.0f, &Temp); - UI()->DoLabel(&Temp, pText, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + if(pCommunityIcon) + { + CUIRect CommunityIcon; + Rect.Margin(2.0f, &CommunityIcon); + RenderCommunityIcon(pCommunityIcon, CommunityIcon, true); + } + else + { + CUIRect Label; + Rect.HMargin(2.0f, &Label); + UI()->DoLabel(&Label, pText, Label.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + } return UI()->DoButtonLogic(pButtonContainer, Checked, pRect); } @@ -591,7 +599,7 @@ void CMenus::RenderMenubar(CUIRect Box) { if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET) { - if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES) + if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) Client()->RequestDDNetInfo(); ServerBrowser()->Refresh(IServerBrowser::TYPE_INTERNET); } @@ -615,7 +623,7 @@ void CMenus::RenderMenubar(CUIRect Box) { if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES) { - if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET) + if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) Client()->RequestDDNetInfo(); ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITES); } @@ -623,6 +631,33 @@ void CMenus::RenderMenubar(CUIRect Box) } GameClient()->m_Tooltips.DoToolTip(&s_FavoritesButton, &Button, Localize("Favorites")); + size_t FavoriteCommunityIndex = 0; + static CButtonContainer s_aFavoriteCommunityButtons[3]; + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)PAGE_FAVORITE_COMMUNITY_3 - PAGE_FAVORITE_COMMUNITY_1 + 1); + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)BIT_TAB_FAVORITE_COMMUNITY_3 - BIT_TAB_FAVORITE_COMMUNITY_1 + 1); + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)IServerBrowser::TYPE_FAVORITE_COMMUNITY_3 - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 + 1); + for(const CCommunity *pCommunity : ServerBrowser()->FavoriteCommunities()) + { + Box.VSplitLeft(75.0f, &Button, &Box); + const int Page = PAGE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex; + if(DoButton_MenuTab(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], FONT_ICON_ELLIPSIS, m_ActivePage == Page, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIT_TAB_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex], nullptr, nullptr, nullptr, 10.0f, FindCommunityIcon(pCommunity->Id()))) + { + const int BrowserType = IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex; + if(ServerBrowser()->GetCurrentType() != BrowserType) + { + if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) + Client()->RequestDDNetInfo(); + ServerBrowser()->Refresh(BrowserType); + } + NewPage = Page; + } + GameClient()->m_Tooltips.DoToolTip(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], &Button, pCommunity->Name()); + + ++FavoriteCommunityIndex; + if(FavoriteCommunityIndex >= std::size(s_aFavoriteCommunityButtons)) + break; + } + TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); } @@ -808,10 +843,22 @@ void CMenus::OnInit() if(g_Config.m_ClShowWelcome) { m_Popup = POPUP_LANGUAGE; - str_copy(g_Config.m_BrFilterExcludeCommunities, "*ddnet"); + m_CreateDefaultFavoriteCommunities = true; } + + if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_3 && + (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size()) + { + // Reset page to internet when there is no favorite community for this page. + g_Config.m_UiPage = PAGE_INTERNET; + } + if(g_Config.m_ClSkipStartMenu) + { m_ShowStart = false; + } + + SetMenuPage(g_Config.m_UiPage); m_RefreshButton.Init(UI(), -1); m_ConnectButton.Init(UI(), -1); @@ -820,7 +867,15 @@ void CMenus::OnInit() Console()->Chain("remove_favorite", ConchainFavoritesUpdate, this); Console()->Chain("add_friend", ConchainFriendlistUpdate, this); Console()->Chain("remove_friend", ConchainFriendlistUpdate, this); - Console()->Chain("br_filter_exclude_communities", ConchainCommunitiesUpdate, this); + + Console()->Chain("add_excluded_community", ConchainCommunitiesUpdate, this); + Console()->Chain("remove_excluded_community", ConchainCommunitiesUpdate, this); + Console()->Chain("add_excluded_country", ConchainCommunitiesUpdate, this); + Console()->Chain("remove_excluded_country", ConchainCommunitiesUpdate, this); + Console()->Chain("add_excluded_type", ConchainCommunitiesUpdate, this); + Console()->Chain("remove_excluded_type", ConchainCommunitiesUpdate, this); + + Console()->Chain("ui_page", ConchainUiPageUpdate, this); Console()->Chain("snd_enable", ConchainUpdateMusicState, this); Console()->Chain("snd_enable_music", ConchainUpdateMusicState, this); @@ -946,15 +1001,32 @@ void CMenus::Render() static int s_Frame = 0; if(s_Frame == 0) { - SetMenuPage(g_Config.m_UiPage); + RefreshBrowserTab(g_Config.m_UiPage); s_Frame++; } else if(s_Frame == 1) { UpdateMusicState(); - RefreshBrowserTab(g_Config.m_UiPage); s_Frame++; } + else + { + UpdateCommunityIcons(); + } + + // Initially add DDNet as favorite community and select its tab. + // This must be delayed until the DDNet info is available. + if(m_CreateDefaultFavoriteCommunities && ServerBrowser()->DDNetInfoAvailable()) + { + m_CreateDefaultFavoriteCommunities = false; + if(ServerBrowser()->Community(IServerBrowser::COMMUNITY_DDNET) != nullptr) + { + ServerBrowser()->FavoriteCommunitiesFilter().Clear(); + ServerBrowser()->FavoriteCommunitiesFilter().Add(IServerBrowser::COMMUNITY_DDNET); + SetMenuPage(PAGE_FAVORITE_COMMUNITY_1); + ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITE_COMMUNITY_1); + } + } if(Client()->State() == IClient::STATE_ONLINE || Client()->State() == IClient::STATE_DEMOPLAYBACK) { @@ -983,13 +1055,18 @@ void CMenus::Render() if(m_Popup == POPUP_NONE) { - if(m_JoinTutorial && !Client()->InfoTaskRunning() && !ServerBrowser()->IsGettingServerlist()) + if(m_JoinTutorial && ServerBrowser()->DDNetInfoAvailable() && !ServerBrowser()->IsGettingServerlist()) { m_JoinTutorial = false; + // This is only reached on first launch, when the DDNet community tab has been created and + // activated by default, so the server info for the tutorial server should be available. const char *pAddr = ServerBrowser()->GetTutorialServer(); if(pAddr) + { Client()->Connect(pAddr); + } } + if(m_ShowStart && Client()->State() == IClient::STATE_OFFLINE) { m_pBackground->ChangePosition(CMenuBackground::POS_START); @@ -1065,10 +1142,16 @@ void CMenus::Render() m_pBackground->ChangePosition(CMenuBackground::POS_BROWSER_FAVORITES); RenderServerbrowser(MainView); } + else if(m_MenuPage >= PAGE_FAVORITE_COMMUNITY_1 && m_MenuPage <= PAGE_FAVORITE_COMMUNITY_3) + { + m_pBackground->ChangePosition(m_MenuPage - PAGE_FAVORITE_COMMUNITY_1 + CMenuBackground::POS_BROWSER_CUSTOM0); + RenderServerbrowser(MainView); + } else if(m_MenuPage == PAGE_SETTINGS) + { RenderSettings(MainView); + } - // do tab bar RenderMenubar(TabBar); } } @@ -2197,11 +2280,8 @@ const CMenus::CMenuImage *CMenus::FindMenuImage(const char *pName) void CMenus::SetMenuPage(int NewPage) { - if(NewPage == PAGE_DDNET_LEGACY || NewPage == PAGE_KOG_LEGACY) - NewPage = PAGE_INTERNET; - m_MenuPage = NewPage; - if(NewPage >= PAGE_INTERNET && NewPage <= PAGE_FAVORITES) + if(NewPage >= PAGE_INTERNET && NewPage <= PAGE_FAVORITE_COMMUNITY_3) g_Config.m_UiPage = NewPage; } @@ -2221,4 +2301,9 @@ void CMenus::RefreshBrowserTab(int UiPage) Client()->RequestDDNetInfo(); ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITES); } + else if(UiPage >= PAGE_FAVORITE_COMMUNITY_1 && UiPage <= PAGE_FAVORITE_COMMUNITY_3) + { + Client()->RequestDDNetInfo(); + ServerBrowser()->Refresh(UiPage - PAGE_FAVORITE_COMMUNITY_1 + IServerBrowser::TYPE_FAVORITE_COMMUNITY_1); + } } diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index ceb41b5fc..0018464f8 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -45,6 +45,14 @@ public: virtual bool OnInput(const IInput::CEvent &Event) override; }; +struct SCommunityIcon +{ + char m_aCommunityId[CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + SHA256_DIGEST m_Sha256; + IGraphics::CTextureHandle m_OrgTexture; + IGraphics::CTextureHandle m_GreyTexture; +}; + class CMenus : public CComponent { static ColorRGBA ms_GuiColor; @@ -61,7 +69,7 @@ class CMenus : public CComponent int DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners = IGraphics::CORNER_ALL, bool Enabled = true); int DoButton_Toggle(const void *pID, int Checked, const CUIRect *pRect, bool Active); int DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, const char *pImageName = nullptr, int Corners = IGraphics::CORNER_ALL, float Rounding = 5.0f, float FontFactor = 0.0f, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f)); - int DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator = nullptr, const ColorRGBA *pDefaultColor = nullptr, const ColorRGBA *pActiveColor = nullptr, const ColorRGBA *pHoverColor = nullptr, float EdgeRounding = 10.0f); + int DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator = nullptr, const ColorRGBA *pDefaultColor = nullptr, const ColorRGBA *pActiveColor = nullptr, const ColorRGBA *pHoverColor = nullptr, float EdgeRounding = 10.0f, const SCommunityIcon *pCommunityIcon = nullptr); int DoButton_CheckBox_Common(const void *pID, const char *pText, const char *pBoxText, const CUIRect *pRect); int DoButton_CheckBox(const void *pID, const char *pText, int Checked, const CUIRect *pRect); @@ -159,7 +167,9 @@ protected: int m_ActivePage; bool m_ShowStart; bool m_MenuActive; - bool m_JoinTutorial; + + bool m_JoinTutorial = false; + bool m_CreateDefaultFavoriteCommunities = false; char m_aNextServer[256]; @@ -493,10 +503,12 @@ protected: static void ConchainFriendlistUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainFavoritesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainCommunitiesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); + static void ConchainUiPageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); struct SCommunityCache { int64_t m_UpdateTime = 0; - bool m_PageWithCommunities; + int m_LastPage = 0; + unsigned m_SelectedCommunitiesHash; std::vector m_vpSelectedCommunities; std::vector m_vpSelectableCountries; std::vector m_vpSelectableTypes; @@ -522,7 +534,7 @@ protected: public: const char *CommunityId() const { return m_aCommunityId; } bool Success() const { return m_Success; } - SHA256_DIGEST &&Sha256() { return std::move(m_Sha256); } + const SHA256_DIGEST &Sha256() const { return m_Sha256; } }; class CCommunityIconLoadJob : public IJob, public CAbstractCommunityIconJob @@ -536,7 +548,7 @@ protected: CCommunityIconLoadJob(CMenus *pMenus, const char *pCommunityId, int StorageType); ~CCommunityIconLoadJob(); - CImageInfo &&ImageInfo() { return std::move(m_ImageInfo); } + CImageInfo &ImageInfo() { return m_ImageInfo; } }; class CCommunityIconDownloadJob : public CHttpRequest, public CAbstractCommunityIconJob @@ -545,13 +557,6 @@ protected: CCommunityIconDownloadJob(CMenus *pMenus, const char *pCommunityId, const char *pUrl, const SHA256_DIGEST &Sha256); }; - struct SCommunityIcon - { - char m_aCommunityId[CServerInfo::MAX_COMMUNITY_ID_LENGTH]; - SHA256_DIGEST m_Sha256; - IGraphics::CTextureHandle m_OrgTexture; - IGraphics::CTextureHandle m_GreyTexture; - }; std::vector m_vCommunityIcons; std::deque> m_CommunityIconLoadJobs; std::deque> m_CommunityIconDownloadJobs; @@ -559,7 +564,7 @@ protected: static int CommunityIconScan(const char *pName, int IsDir, int DirType, void *pUser); const SCommunityIcon *FindCommunityIcon(const char *pCommunityId); bool LoadCommunityIconFile(const char *pPath, int DirType, CImageInfo &Info, SHA256_DIGEST &Sha256); - void LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &&Info, SHA256_DIGEST &&Sha256); + void LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &Info, const SHA256_DIGEST &Sha256); void RenderCommunityIcon(const SCommunityIcon *pIcon, CUIRect Rect, bool Active); void UpdateCommunityIcons(); @@ -634,8 +639,9 @@ public: PAGE_INTERNET, PAGE_LAN, PAGE_FAVORITES, - PAGE_DDNET_LEGACY, // removed, redirects to PAGE_INTERNET - PAGE_KOG_LEGACY, // removed, redirects to PAGE_INTERNET + PAGE_FAVORITE_COMMUNITY_1, + PAGE_FAVORITE_COMMUNITY_2, + PAGE_FAVORITE_COMMUNITY_3, PAGE_DEMOS, PAGE_SETTINGS, PAGE_NETWORK, @@ -660,6 +666,9 @@ public: BIG_TAB_INTERNET, BIG_TAB_LAN, BIG_TAB_FAVORITES, + BIT_TAB_FAVORITE_COMMUNITY_1, + BIT_TAB_FAVORITE_COMMUNITY_2, + BIT_TAB_FAVORITE_COMMUNITY_3, BIG_TAB_DEMOS, BIG_TAB_LENGTH, diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp index b31f9f13a..7d07e3e79 100644 --- a/src/game/client/components/menus_browser.cpp +++ b/src/game/client/components/menus_browser.cpp @@ -701,21 +701,6 @@ void CMenus::RenderServerbrowserFilters(CUIRect View) if(DoButton_CheckBox(&g_Config.m_BrFilterConnectingPlayers, Localize("Filter connecting players"), g_Config.m_BrFilterConnectingPlayers, &Button)) g_Config.m_BrFilterConnectingPlayers ^= 1; - // community filter - if((g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES) && !ServerBrowser()->Communities().empty()) - { - CUIRect Row; - View.HSplitTop(6.0f, nullptr, &View); - View.HSplitTop(19.0f, &Row, &View); - Row.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_T, 4.0f); - UI()->DoLabel(&Row, Localize("Communities"), 12.0f, TEXTALIGN_MC); - - View.HSplitTop(4.0f * 17.0f + CScrollRegion::HEIGHT_MAGIC_FIX, &Row, &View); - View.HSplitTop(3.0f, nullptr, &View); - Row.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_B, 4.0f); - RenderServerbrowserCommunitiesFilter(Row); - } - // map finish filters if(m_CommunityCache.m_AnyRanksAvailable) { @@ -801,13 +786,24 @@ void CMenus::ResetServerbrowserFilters() g_Config.m_BrFilterGametype[0] = '\0'; g_Config.m_BrFilterGametypeStrict = 0; g_Config.m_BrFilterConnectingPlayers = 1; - g_Config.m_BrFilterUnfinishedMap = 0; g_Config.m_BrFilterServerAddress[0] = '\0'; - ConfigManager()->Reset("br_filter_exclude_communities"); - ConfigManager()->Reset("br_filter_exclude_countries"); - ConfigManager()->Reset("br_filter_exclude_types"); + + if(g_Config.m_UiPage != PAGE_LAN) + { + if(m_CommunityCache.m_AnyRanksAvailable) + { + g_Config.m_BrFilterUnfinishedMap = 0; + } + if(g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES) + { + ServerBrowser()->CommunitiesFilter().Clear(); + } + ServerBrowser()->CountriesFilter().Clear(); + ServerBrowser()->TypesFilter().Clear(); + UpdateCommunityCache(true); + } + Client()->ServerBrowserUpdate(); - UpdateCommunityCache(true); } void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, @@ -914,11 +910,18 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, void CMenus::RenderServerbrowserCommunitiesFilter(CUIRect View) { + CUIRect Tab; + View.HSplitTop(19.0f, &Tab, &View); + Tab.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_T, 4.0f); + UI()->DoLabel(&Tab, Localize("Communities"), 12.0f, TEXTALIGN_MC); + View.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_B, 4.0f); + const int MaxEntries = ServerBrowser()->Communities().size(); const int EntriesPerRow = 1; static CScrollRegion s_ScrollRegion; static std::vector s_vItemIds; + static std::vector s_vFavoriteButtonIds; const float ItemHeight = 13.0f; const float Spacing = 2.0f; @@ -932,12 +935,14 @@ void CMenus::RenderServerbrowserCommunitiesFilter(CUIRect View) const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) { const float Alpha = (Active ? 0.9f : 0.2f) + (UI()->HotItem() == pItemId ? 0.1f : 0.0f); - CUIRect Icon, Label; + CUIRect Icon, Label, FavoriteButton; + Item.VSplitRight(Item.h, &Item, &FavoriteButton); Item.Margin(Spacing, &Item); Item.VSplitLeft(Item.h * 2.0f, &Icon, &Label); Label.VSplitLeft(Spacing, nullptr, &Label); - const SCommunityIcon *pIcon = FindCommunityIcon(GetItemName(ItemIndex)); + const char *pItemName = GetItemName(ItemIndex); + const SCommunityIcon *pIcon = FindCommunityIcon(pItemName); if(pIcon != nullptr) { RenderCommunityIcon(pIcon, Icon, Active); @@ -946,8 +951,22 @@ void CMenus::RenderServerbrowserCommunitiesFilter(CUIRect View) TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); UI()->DoLabel(&Label, GetItemDisplayName(ItemIndex), Label.h * CUI::ms_FontmodHeight, TEXTALIGN_ML); TextRender()->TextColor(TextRender()->DefaultTextColor()); + + const bool Favorite = ServerBrowser()->FavoriteCommunitiesFilter().Filtered(pItemName); + if(DoButton_Favorite(&s_vFavoriteButtonIds[ItemIndex], pItemId, Favorite, &FavoriteButton)) + { + if(Favorite) + { + ServerBrowser()->FavoriteCommunitiesFilter().Remove(pItemName); + } + else + { + ServerBrowser()->FavoriteCommunitiesFilter().Add(pItemName); + } + } }; + s_vFavoriteButtonIds.resize(MaxEntries); RenderServerbrowserDDNetFilter(View, ServerBrowser()->CommunitiesFilter(), ItemHeight + 2.0f * Spacing, MaxEntries, EntriesPerRow, s_ScrollRegion, s_vItemIds, true, GetItemName, RenderItem); } @@ -1656,17 +1675,16 @@ void CMenus::RenderServerbrowserToolBox(CUIRect ToolBox) void CMenus::RenderServerbrowser(CUIRect MainView) { UpdateCommunityCache(false); - UpdateCommunityIcons(); /* - +-----------------+ +--tabs--+ - | | | | - | | | | - | server list | | tool | - | | | box | - | | | | - +-----------------+ | | - status box +--------+ + +---------------------------+ +---communities---+ + | | | | + | | +------tabs-------+ + | server list | | | + | | | tool | + | | | box | + +---------------------------+ | | + status box +-----------------+ */ CUIRect ServerList, StatusBox, ToolBox, TabBar; @@ -1674,6 +1692,15 @@ void CMenus::RenderServerbrowser(CUIRect MainView) MainView.Margin(10.0f, &MainView); MainView.VSplitRight(205.0f, &ServerList, &ToolBox); ServerList.VSplitRight(5.0f, &ServerList, nullptr); + + if((g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES) && !ServerBrowser()->Communities().empty()) + { + CUIRect CommunityFilter; + ToolBox.HSplitTop(19.0f + 4.0f * 17.0f + CScrollRegion::HEIGHT_MAGIC_FIX, &CommunityFilter, &ToolBox); + ToolBox.HSplitTop(8.0f, nullptr, &ToolBox); + RenderServerbrowserCommunitiesFilter(CommunityFilter); + } + ToolBox.HSplitTop(24.0f, &TabBar, &ToolBox); ServerList.HSplitBottom(65.0f, &ServerList, &StatusBox); @@ -1762,28 +1789,70 @@ void CMenus::ConchainCommunitiesUpdate(IConsole::IResult *pResult, void *pUserDa { pfnCallback(pResult, pCallbackUserData); CMenus *pThis = static_cast(pUserData); - if(pResult->NumArguments() >= 1 && (g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES)) + if(pResult->NumArguments() >= 1 && (g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES || (g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_3))) { pThis->UpdateCommunityCache(true); pThis->Client()->ServerBrowserUpdate(); } } +void CMenus::ConchainUiPageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) +{ + const int OldPage = g_Config.m_UiPage; + pfnCallback(pResult, pCallbackUserData); + CMenus *pThis = static_cast(pUserData); + if(pResult->NumArguments() >= 1) + { + if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_3 && + (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= pThis->ServerBrowser()->FavoriteCommunities().size()) + { + // Reset page to internet when there is no favorite community for this page. + g_Config.m_UiPage = PAGE_INTERNET; + } + + pThis->SetMenuPage(g_Config.m_UiPage); + + if(!pThis->m_ShowStart && g_Config.m_UiPage != OldPage) + { + pThis->RefreshBrowserTab(g_Config.m_UiPage); + } + } +} + void CMenus::UpdateCommunityCache(bool Force) { - const bool PageWithCommunities = g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES; - if(!Force && m_CommunityCache.m_UpdateTime != 0 && m_CommunityCache.m_UpdateTime == ServerBrowser()->DDNetInfoUpdateTime() && m_CommunityCache.m_PageWithCommunities == PageWithCommunities) + if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_3 && + (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size()) + { + // Reset page to internet when there is no favorite community for this page, + // i.e. when favorite community is removed via console while the page is open. + SetMenuPage(PAGE_INTERNET); + RefreshBrowserTab(g_Config.m_UiPage); + } + + const unsigned CommunitiesHash = ServerBrowser()->CurrentCommunitiesHash(); + const bool PageChanged = m_CommunityCache.m_LastPage != 0 && m_CommunityCache.m_LastPage != g_Config.m_UiPage; + const bool CurrentCommunitiesChanged = m_CommunityCache.m_LastPage != 0 && m_CommunityCache.m_LastPage == g_Config.m_UiPage && m_CommunityCache.m_SelectedCommunitiesHash != CommunitiesHash; + if(CurrentCommunitiesChanged && g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_3) + { + // Favorite community was changed while its page is active, + // refresh to get correct serverlist for updated community. + ServerBrowser()->Refresh(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + IServerBrowser::TYPE_FAVORITE_COMMUNITY_1, true); + } + + if(!Force && m_CommunityCache.m_UpdateTime != 0 && + m_CommunityCache.m_UpdateTime == ServerBrowser()->DDNetInfoUpdateTime() && + !CurrentCommunitiesChanged && !PageChanged) + { return; + } ServerBrowser()->CleanFilters(); m_CommunityCache.m_UpdateTime = ServerBrowser()->DDNetInfoUpdateTime(); - m_CommunityCache.m_PageWithCommunities = PageWithCommunities; - - if(m_CommunityCache.m_PageWithCommunities) - m_CommunityCache.m_vpSelectedCommunities = ServerBrowser()->SelectedCommunities(); - else - m_CommunityCache.m_vpSelectedCommunities.clear(); + m_CommunityCache.m_LastPage = g_Config.m_UiPage; + m_CommunityCache.m_SelectedCommunitiesHash = CommunitiesHash; + m_CommunityCache.m_vpSelectedCommunities = ServerBrowser()->CurrentCommunities(); m_CommunityCache.m_vpSelectableCountries.clear(); m_CommunityCache.m_vpSelectableTypes.clear(); @@ -1866,7 +1935,7 @@ int CMenus::CommunityIconScan(const char *pName, int IsDir, int DirType, void *p return 0; } -const CMenus::SCommunityIcon *CMenus::FindCommunityIcon(const char *pCommunityId) +const SCommunityIcon *CMenus::FindCommunityIcon(const char *pCommunityId) { auto Icon = std::find_if(m_vCommunityIcons.begin(), m_vCommunityIcons.end(), [pCommunityId](const SCommunityIcon &Element) { return str_comp(Element.m_aCommunityId, pCommunityId) == 0; @@ -1900,7 +1969,7 @@ bool CMenus::LoadCommunityIconFile(const char *pPath, int DirType, CImageInfo &I return true; } -void CMenus::LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &&Info, SHA256_DIGEST &&Sha256) +void CMenus::LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &Info, const SHA256_DIGEST &Sha256) { SCommunityIcon CommunityIcon; str_copy(CommunityIcon.m_aCommunityId, pCommunityId); @@ -1979,14 +2048,14 @@ void CMenus::UpdateCommunityIcons() { std::shared_ptr pLoadJob = std::make_shared(this, pJob->CommunityId(), IStorage::TYPE_SAVE); Engine()->AddJob(pLoadJob); - m_CommunityIconLoadJobs.emplace_back(std::move(pLoadJob)); + m_CommunityIconLoadJobs.push_back(pLoadJob); } m_CommunityIconDownloadJobs.pop_front(); } } // Rescan for changed communities only when necessary - if(m_CommunityIconsUpdateTime != 0 && m_CommunityIconsUpdateTime == ServerBrowser()->DDNetInfoUpdateTime()) + if(!ServerBrowser()->DDNetInfoAvailable() || (m_CommunityIconsUpdateTime != 0 && m_CommunityIconsUpdateTime == ServerBrowser()->DDNetInfoUpdateTime())) return; m_CommunityIconsUpdateTime = ServerBrowser()->DDNetInfoUpdateTime(); diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index eeb2c35e0..315d21771 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -837,9 +837,9 @@ void CMenus::RenderInGameNetwork(CUIRect MainView) static CButtonContainer s_InternetButton; if(DoButton_MenuTab(&s_InternetButton, FONT_ICON_EARTH_AMERICAS, g_Config.m_UiPage == PAGE_INTERNET, &Button, IGraphics::CORNER_NONE)) { - if(g_Config.m_UiPage != PAGE_INTERNET) + if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET) { - if(g_Config.m_UiPage != PAGE_FAVORITES) + if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) Client()->RequestDDNetInfo(); ServerBrowser()->Refresh(IServerBrowser::TYPE_INTERNET); } @@ -851,7 +851,7 @@ void CMenus::RenderInGameNetwork(CUIRect MainView) static CButtonContainer s_LanButton; if(DoButton_MenuTab(&s_LanButton, FONT_ICON_NETWORK_WIRED, g_Config.m_UiPage == PAGE_LAN, &Button, IGraphics::CORNER_NONE)) { - if(g_Config.m_UiPage != PAGE_LAN) + if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_LAN) ServerBrowser()->Refresh(IServerBrowser::TYPE_LAN); NewPage = PAGE_LAN; } @@ -861,9 +861,9 @@ void CMenus::RenderInGameNetwork(CUIRect MainView) static CButtonContainer s_FavoritesButton; if(DoButton_MenuTab(&s_FavoritesButton, FONT_ICON_STAR, g_Config.m_UiPage == PAGE_FAVORITES, &Button, IGraphics::CORNER_NONE)) { - if(g_Config.m_UiPage != PAGE_FAVORITES) + if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES) { - if(g_Config.m_UiPage != PAGE_INTERNET) + if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) Client()->RequestDDNetInfo(); ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITES); } @@ -871,6 +871,31 @@ void CMenus::RenderInGameNetwork(CUIRect MainView) } GameClient()->m_Tooltips.DoToolTip(&s_FavoritesButton, &Button, Localize("Favorites")); + size_t FavoriteCommunityIndex = 0; + static CButtonContainer s_aFavoriteCommunityButtons[3]; + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)PAGE_FAVORITE_COMMUNITY_3 - PAGE_FAVORITE_COMMUNITY_1 + 1); + for(const CCommunity *pCommunity : ServerBrowser()->FavoriteCommunities()) + { + TabBar.VSplitLeft(75.0f, &Button, &TabBar); + const int Page = PAGE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex; + if(DoButton_MenuTab(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], FONT_ICON_ELLIPSIS, g_Config.m_UiPage == Page, &Button, IGraphics::CORNER_NONE, nullptr, nullptr, nullptr, nullptr, 10.0f, FindCommunityIcon(pCommunity->Id()))) + { + const int BrowserType = IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex; + if(ServerBrowser()->GetCurrentType() != BrowserType) + { + if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) + Client()->RequestDDNetInfo(); + ServerBrowser()->Refresh(BrowserType); + } + NewPage = Page; + } + GameClient()->m_Tooltips.DoToolTip(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], &Button, pCommunity->Name()); + + ++FavoriteCommunityIndex; + if(FavoriteCommunityIndex >= std::size(s_aFavoriteCommunityButtons)) + break; + } + TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); diff --git a/src/game/client/components/menus_start.cpp b/src/game/client/components/menus_start.cpp index adc5c56f7..763630991 100644 --- a/src/game/client/components/menus_start.cpp +++ b/src/game/client/components/menus_start.cpp @@ -67,6 +67,10 @@ void CMenus::RenderStartMenu(CUIRect MainView) if(DoButton_Menu(&s_TutorialButton, Localize("Tutorial"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (s_JoinTutorialTime != 0.0f && Client()->LocalTime() >= s_JoinTutorialTime)) { + // Activate internet tab before joining tutorial to make sure the server info + // for the tutorial servers is available. + SetMenuPage(PAGE_INTERNET); + RefreshBrowserTab(IServerBrowser::TYPE_INTERNET); const char *pAddr = ServerBrowser()->GetTutorialServer(); if(pAddr) { @@ -187,12 +191,11 @@ void CMenus::RenderStartMenu(CUIRect MainView) static CButtonContainer s_PlayButton; if(DoButton_Menu(&s_PlayButton, Localize("Play", "Start menu"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "play_game" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || CheckHotKey(KEY_P)) { - NewPage = g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= PAGE_FAVORITES ? g_Config.m_UiPage : PAGE_INTERNET; + NewPage = g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_3 ? g_Config.m_UiPage : PAGE_INTERNET; } // render version CUIRect VersionUpdate, CurVersion; - MainView.HSplitBottom(30.0f, 0, 0); MainView.HSplitBottom(20.0f, 0, &VersionUpdate); VersionUpdate.VSplitRight(50.0f, &CurVersion, 0);