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.
This commit is contained in:
Robert Müller 2024-01-21 11:06:25 +01:00
parent a8f3b56850
commit 7c9b1fbbb4
13 changed files with 1111 additions and 220 deletions

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -280,7 +280,6 @@ public:
virtual SWarning *GetCurWarning() = 0;
virtual CChecksumData *ChecksumData() = 0;
virtual bool InfoTaskRunning() = 0;
virtual int UdpConnectivity(int NetType) = 0;
#if defined(CONF_FAMILY_WINDOWS)

View file

@ -499,7 +499,6 @@ public:
SWarning *GetCurWarning() override;
CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; }
bool InfoTaskRunning() override { return m_pDDNetInfoTask != nullptr; }
int UdpConnectivity(int NetType) override;
#if defined(CONF_FAMILY_WINDOWS)

View file

@ -50,9 +50,8 @@ bool matchesExactly(const char *a, const char *b)
}
CServerBrowser::CServerBrowser() :
m_CommunitiesFilter(g_Config.m_BrFilterExcludeCommunities, sizeof(g_Config.m_BrFilterExcludeCommunities)),
m_CountriesFilter(g_Config.m_BrFilterExcludeCountries, sizeof(g_Config.m_BrFilterExcludeCountries)),
m_TypesFilter(g_Config.m_BrFilterExcludeTypes, sizeof(g_Config.m_BrFilterExcludeTypes))
m_CountriesFilter([this]() { return CurrentCommunities(); }),
m_TypesFilter([this]() { return CurrentCommunities(); })
{
m_ppServerlist = nullptr;
m_pSortedServerlist = nullptr;
@ -108,12 +107,106 @@ void CServerBrowser::OnInit()
void CServerBrowser::RegisterCommands()
{
m_pConfigManager->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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<CServerBrowser *>(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<bool(const NETADDR *, int)> 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<const CCommunity *> 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<const CCommunity *> CServerBrowser::SelectedCommunities() const
return vpSelected;
}
void CFilterList::Add(const char *pElement)
std::vector<const CCommunity *> 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<const CCommunity *> 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<const CCommunity *> 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<const CCommunity *> vpFavoriteCommunities = FavoriteCommunities();
dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid favorite community serverbrowser type");
return {vpFavoriteCommunities[CommunityIndex]};
}
else
{
return {};
}
}
unsigned CServerBrowser::CurrentCommunitiesHash() const
{
std::vector<const CCommunity *> 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<CCommunity> &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<CCommunityId> &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<CCommunity> &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<const char *> &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<CCommunity> &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<CCommunity> &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<const char *> vpCommunityNames;
for(const auto &Community : Communities())
vpCommunityNames.push_back(Community.Id());
m_CommunitiesFilter.Clean(vpCommunityNames);
}
void CServerBrowser::CountriesFilterClean()
{
std::vector<const char *> 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<const char *> 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)

View file

@ -9,7 +9,9 @@
#include <engine/serverbrowser.h>
#include <engine/shared/memheap.h>
#include <functional>
#include <unordered_map>
#include <unordered_set>
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<CCommunityId>
{
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<CCommunityCountryName>
{
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<CCommunityTypeName>
{
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<CCommunity> &vAllowedCommunities);
void Save(IConfigManager *pConfigManager) const;
const std::vector<CCommunityId> &Entries() const;
private:
std::vector<CCommunityId> 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<CCommunity> &vAllowedCommunities);
void Save(IConfigManager *pConfigManager) const;
private:
std::unordered_set<CCommunityId> m_Entries;
};
class CExcludedCommunityCountryFilterList : public IFilterList
{
public:
CExcludedCommunityCountryFilterList(std::function<std::vector<const CCommunity *>()> 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<const char *> &vpAllowedElements);
void Clean(const std::vector<CCommunity> &vAllowedCommunities);
void Save(IConfigManager *pConfigManager) const;
private:
std::function<std::vector<const CCommunity *>()> m_CurrentCommunitiesGetter;
std::unordered_map<CCommunityId, std::unordered_set<CCommunityCountryName>> m_Entries;
};
class CExcludedCommunityTypeFilterList : public IFilterList
{
public:
CExcludedCommunityTypeFilterList(std::function<std::vector<const CCommunity *>()> 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<CCommunity> &vAllowedCommunities);
void Save(IConfigManager *pConfigManager) const;
private:
std::function<std::vector<const CCommunity *>()> m_CurrentCommunitiesGetter;
std::unordered_map<CCommunityId, std::unordered_set<CCommunityTypeName>> 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<CCommunity> &Communities() const override;
const CCommunity *Community(const char *pCommunityId) const override;
std::vector<const CCommunity *> SelectedCommunities() const override;
std::vector<const CCommunity *> FavoriteCommunities() const override;
std::vector<const CCommunity *> 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);

View file

@ -231,6 +231,8 @@ public:
const SHA256_DIGEST &IconSha256() const { return m_IconSha256; }
const std::vector<CCommunityCountry> &Countries() const { return m_vCountries; }
const std::vector<CCommunityType> &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<CCommunity> &Communities() const = 0;
virtual const CCommunity *Community(const char *pCommunityId) const = 0;
virtual std::vector<const CCommunity *> SelectedCommunities() const = 0;
virtual std::vector<const CCommunity *> FavoriteCommunities() const = 0;
virtual std::vector<const CCommunity *> 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;

View file

@ -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")

View file

@ -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);
}
}

View file

@ -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<const CCommunity *> m_vpSelectedCommunities;
std::vector<const CCommunityCountry *> m_vpSelectableCountries;
std::vector<const CCommunityType *> 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<SCommunityIcon> m_vCommunityIcons;
std::deque<std::shared_ptr<CCommunityIconLoadJob>> m_CommunityIconLoadJobs;
std::deque<std::shared_ptr<CCommunityIconDownloadJob>> 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,

View file

@ -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<unsigned char> s_vItemIds;
static std::vector<unsigned char> 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<CMenus *>(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<CMenus *>(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<CCommunityIconLoadJob> pLoadJob = std::make_shared<CCommunityIconLoadJob>(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();

View file

@ -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);

View file

@ -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);