Merge pull request #7915 from Robyt3/Browser-Community-Tabs

Add tabs for favorite communities, separate country/type filters
This commit is contained in:
Dennis Felsing 2024-02-03 23:08:24 +00:00 committed by GitHub
commit f0da0aa4a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1165 additions and 243 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

@ -584,7 +584,13 @@ bool CGraphics_Threaded::LoadPNG(CImageInfo *pImg, const char *pFilename, int St
if(File)
{
io_seek(File, 0, IOSEEK_END);
unsigned int FileSize = io_tell(File);
long int FileSize = io_tell(File);
if(FileSize <= 0)
{
io_close(File);
log_error("game/png", "failed to get file size (%ld). filename='%s'", FileSize, pFilename);
return false;
}
io_seek(File, 0, IOSEEK_START);
TImageByteBuffer ByteBuffer;

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;
@ -298,13 +435,20 @@ void CServerBrowser::Filter()
else if(g_Config.m_BrFilterUnfinishedMap && Info.m_HasRank == CServerInfo::RANK_RANKED)
Filtered = true;
else
{
if(!Communities().empty())
{
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)
{
if(m_pFilter[0] != '\0')
str_append(m_pFilter, ",", m_FilterSize);
str_append(m_pFilter, aToken, m_FilterSize);
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)
{
++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';
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 CFilterList::Clean(const std::vector<const char *> &vpAllowedElements)
void CExcludedCommunityCountryFilterList::Clean(const std::vector<CCommunity> &vAllowedCommunities)
{
size_t NumFiltered = 0;
char aNewList[512];
aNewList[0] = '\0';
for(const char *pElement : vpAllowedElements)
for(auto It = m_Entries.begin(); It != m_Entries.end();)
{
if(Filtered(pElement))
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;
}
}
// Prevent filter that would exclude all allowed elements
if(NumFiltered == vpAllowedElements.size())
m_pFilter[0] = '\0';
else
str_copy(m_pFilter, aNewList, m_FilterSize);
{
It = m_Entries.erase(It);
}
}
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);
}
@ -250,6 +258,24 @@ int CMenus::DoButton_GridHeader(const void *pID, const char *pText, int Checked,
return UI()->DoButtonLogic(pID, Checked, pRect);
}
int CMenus::DoButton_Favorite(const void *pButtonId, const void *pParentId, bool Checked, const CUIRect *pRect)
{
if(Checked || (pParentId != nullptr && UI()->HotItem() == pParentId) || UI()->HotItem() == pButtonId)
{
TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
const float Alpha = UI()->HotItem() == pButtonId ? 0.2f : 0.0f;
TextRender()->TextColor(Checked ? ColorRGBA(1.0f, 0.85f, 0.3f, 0.8f + Alpha) : ColorRGBA(0.5f, 0.5f, 0.5f, 0.8f + Alpha));
SLabelProperties Props;
Props.m_MaxWidth = pRect->w;
UI()->DoLabel(pRect, FONT_ICON_STAR, 12.0f, TEXTALIGN_MC, Props);
TextRender()->TextColor(TextRender()->DefaultTextColor());
TextRender()->SetRenderFlags(0);
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
}
return UI()->DoButtonLogic(pButtonId, 0, pRect);
}
int CMenus::DoButton_CheckBox_Common(const void *pID, const char *pText, const char *pBoxText, const CUIRect *pRect)
{
CUIRect Box, Label;
@ -573,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);
}
@ -597,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);
}
@ -605,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);
}
@ -790,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);
@ -802,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);
@ -928,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)
{
@ -965,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);
@ -1047,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);
}
}
@ -2179,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;
}
@ -2203,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);
@ -72,6 +80,7 @@ class CMenus : public CComponent
ColorHSLA DoButton_ColorPicker(const CUIRect *pRect, unsigned int *pHslaColor, bool Alpha);
void DoLaserPreview(const CUIRect *pRect, ColorHSLA OutlineColor, ColorHSLA InnerColor, const int LaserType);
int DoButton_GridHeader(const void *pID, const char *pText, int Checked, const CUIRect *pRect);
int DoButton_Favorite(const void *pButtonId, const void *pParentId, bool Checked, const CUIRect *pRect);
int DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int ModifierCombination, int *pNewModifierCombination);
@ -158,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];
@ -492,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;
@ -521,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
@ -535,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
@ -544,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;
@ -558,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();
@ -633,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,
@ -659,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,15 +786,26 @@ 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");
Client()->ServerBrowserUpdate();
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();
}
void CMenus::RenderServerbrowserDDNetFilter(CUIRect View,
IFilterList &Filter,
float ItemHeight, int MaxItems, int ItemsPerRow,
@ -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--+
+---------------------------+ +---communities---+
| | | |
| | | |
| server list | | tool |
| | +------tabs-------+
| server list | | |
| | | tool |
| | | box |
| | | |
+-----------------+ | |
status 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

@ -801,19 +801,6 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
m_SkinListNeedsUpdate = false;
}
const auto &&RenderFavIcon = [&](const CUIRect &FavIcon, bool AsFav, bool Hot) {
TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
TextRender()->TextColor(AsFav ? ColorRGBA(1.0f, 0.85f, 0.3f, 0.8f + (Hot ? 0.2f : 0.0f)) : ColorRGBA(0.5f, 0.5f, 0.5f, 0.8f + (Hot ? 0.2f : 0.0f)));
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
SLabelProperties Props;
Props.m_MaxWidth = FavIcon.w;
UI()->DoLabel(&FavIcon, FONT_ICON_STAR, 12.0f, TEXTALIGN_MR, Props);
TextRender()->TextColor(TextRender()->DefaultTextColor());
TextRender()->SetRenderFlags(0);
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
};
int OldSelected = -1;
s_ListBox.DoStart(50.0f, s_vSkinList.size(), 4, 1, OldSelected, &MainView);
for(size_t i = 0; i < s_vSkinList.size(); ++i)
@ -861,11 +848,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
CUIRect FavIcon;
Item.m_Rect.HSplitTop(20.0f, &FavIcon, nullptr);
FavIcon.VSplitRight(20.0f, nullptr, &FavIcon);
if(IsFav || UI()->HotItem() == pSkinToBeDraw || UI()->HotItem() == &pSkinToBeDraw->m_Metrics.m_Body)
{
RenderFavIcon(FavIcon, IsFav, UI()->HotItem() == &pSkinToBeDraw->m_Metrics.m_Body);
}
if(UI()->DoButtonLogic(&pSkinToBeDraw->m_Metrics.m_Body, 0, &FavIcon))
if(DoButton_Favorite(&pSkinToBeDraw->m_Metrics.m_Body, pSkinToBeDraw, IsFav, &FavIcon))
{
if(IsFav)
{

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

View file

@ -12,7 +12,13 @@ int DilateFile(const char *pFilename)
if(File)
{
io_seek(File, 0, IOSEEK_END);
unsigned int FileSize = io_tell(File);
long int FileSize = io_tell(File);
if(FileSize <= 0)
{
io_close(File);
dbg_msg("dilate", "failed to get file size (%ld). filename='%s'", FileSize, pFilename);
return false;
}
io_seek(File, 0, IOSEEK_START);
TImageByteBuffer ByteBuffer;
SImageByteBuffer ImageByteBuffer(&ByteBuffer);

View file

@ -31,7 +31,13 @@ int LoadPNG(CImageInfo *pImg, const char *pFilename)
if(File)
{
io_seek(File, 0, IOSEEK_END);
unsigned int FileSize = io_tell(File);
long int FileSize = io_tell(File);
if(FileSize <= 0)
{
io_close(File);
dbg_msg("map_convert_07", "failed to get file size (%ld). filename='%s'", FileSize, pFilename);
return false;
}
io_seek(File, 0, IOSEEK_START);
TImageByteBuffer ByteBuffer;
SImageByteBuffer ImageByteBuffer(&ByteBuffer);

View file

@ -319,7 +319,13 @@ bool LoadPNG(CImageInfo *pImg, const char *pFilename)
}
io_seek(File, 0, IOSEEK_END);
unsigned int FileSize = io_tell(File);
long int FileSize = io_tell(File);
if(FileSize <= 0)
{
io_close(File);
dbg_msg("map_create_pixelart", "ERROR: Failed to get file size (%ld). filename='%s'", FileSize, pFilename);
return false;
}
io_seek(File, 0, IOSEEK_START);
TImageByteBuffer ByteBuffer;
SImageByteBuffer ImageByteBuffer(&ByteBuffer);

View file

@ -29,7 +29,12 @@ bool LoadPNG(CImageInfo *pImg, const char *pFilename)
if(File)
{
io_seek(File, 0, IOSEEK_END);
unsigned int FileSize = io_tell(File);
long int FileSize = io_tell(File);
if(FileSize <= 0)
{
io_close(File);
return false;
}
io_seek(File, 0, IOSEEK_START);
TImageByteBuffer ByteBuffer;
SImageByteBuffer ImageByteBuffer(&ByteBuffer);