ddnet/src/game/client/components/menus_start.cpp
Robert Müller d39247f726 Add community server filter and icons, remove DDNet and KoG tabs
Remove the DDNet and KoG tabs and replace them with a community filter, which works like the country and type filters. Community information (ID, name, icon SHA256, countries, types, servers, ranked maps) is loaded dynamically from the DDNet info json.

The official checkmark icon is replaced with community icons. The icons are downloaded to the `communityicons` folder in the config directory from a URL specified in the DDNet info json. The icons are only re-downloaded when their SHA256 has changed.

Per default, the Internet tab with all communities is selected. When starting the client for the first time, only the DDNet community is selected. The community, country and type filters can also be used on the Favorites tab now. The LAN tab remains unchanged.

The country and type filters are refactored. Previously, using these filters changed the total number of servers and players that were displayed, as the filter was implemented as a separate step. Now the community, country and type filters work like the other browser filters. This also has the benefit that the server list and DDNet info json are not reloaded each time these filters are changed, as updating the local filtering/sorting is enough now.

The check for finished maps is made more efficient by using an `std::unordered_set` instead of linear search.

Closes #5654.
2023-11-04 15:32:27 +01:00

302 lines
11 KiB
C++

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <engine/graphics.h>
#include <engine/keys.h>
#include <engine/serverbrowser.h>
#include <engine/textrender.h>
#include <engine/client/updater.h>
#include <engine/shared/config.h>
#include <game/client/gameclient.h>
#include <game/client/ui.h>
#include <game/generated/client_data.h>
#include <game/localization.h>
#include <game/version.h>
#include "menus.h"
void CMenus::RenderStartMenu(CUIRect MainView)
{
// render logo
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_BANNER].m_Id);
Graphics()->QuadsBegin();
Graphics()->SetColor(1, 1, 1, 1);
IGraphics::CQuadItem QuadItem(MainView.w / 2 - 170, 60, 360, 103);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
const float Rounding = 10.0f;
const float VMargin = MainView.w / 2 - 190.0f;
CUIRect Button;
int NewPage = -1;
CUIRect ExtMenu;
MainView.VSplitLeft(30.0f, 0, &ExtMenu);
ExtMenu.VSplitLeft(100.0f, &ExtMenu, 0);
ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button);
static CButtonContainer s_DiscordButton;
if(DoButton_Menu(&s_DiscordButton, Localize("Discord"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)))
{
const char *pLink = Localize("https://ddnet.org/discord");
if(!open_link(pLink))
{
dbg_msg("menus", "couldn't open link '%s'", pLink);
}
}
ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space
ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button);
static CButtonContainer s_LearnButton;
if(DoButton_Menu(&s_LearnButton, Localize("Learn"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)))
{
const char *pLink = Localize("https://wiki.ddnet.org/");
if(!open_link(pLink))
{
dbg_msg("menus", "couldn't open link '%s'", pLink);
}
}
ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space
ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button);
static CButtonContainer s_TutorialButton;
static float s_JoinTutorialTime = 0.0f;
if(DoButton_Menu(&s_TutorialButton, Localize("Tutorial"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)) ||
(s_JoinTutorialTime != 0.0f && Client()->LocalTime() >= s_JoinTutorialTime))
{
const char *pAddr = ServerBrowser()->GetTutorialServer();
if(pAddr)
{
Client()->Connect(pAddr);
s_JoinTutorialTime = 0.0f;
}
else if(s_JoinTutorialTime == 0.0f)
{
dbg_msg("menus", "couldn't find tutorial server, retrying in 5 seconds");
s_JoinTutorialTime = Client()->LocalTime() + 5.0f;
}
else
{
Client()->AddWarning(SWarning(Localize("Can't find a Tutorial server")));
s_JoinTutorialTime = 0.0f;
}
}
ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space
ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button);
static CButtonContainer s_WebsiteButton;
if(DoButton_Menu(&s_WebsiteButton, Localize("Website"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)))
{
const char *pLink = "https://ddnet.org/";
if(!open_link(pLink))
{
dbg_msg("menus", "couldn't open link '%s'", pLink);
}
}
ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space
ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button);
static CButtonContainer s_NewsButton;
if(DoButton_Menu(&s_NewsButton, Localize("News"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), g_Config.m_UiUnreadNews ? vec4(0.0f, 1.0f, 0.0f, 0.25f) : vec4(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_N))
NewPage = PAGE_NEWS;
CUIRect Menu;
MainView.VMargin(VMargin, &Menu);
Menu.HSplitBottom(25.0f, &Menu, 0);
Menu.HSplitBottom(40.0f, &Menu, &Button);
static CButtonContainer s_QuitButton;
bool UsedEscape = false;
if(DoButton_Menu(&s_QuitButton, Localize("Quit"), 0, &Button, 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)) || (UsedEscape = UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) || CheckHotKey(KEY_Q))
{
if(UsedEscape || m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0))
{
m_Popup = POPUP_QUIT;
}
else
{
Client()->Quit();
}
}
Menu.HSplitBottom(100.0f, &Menu, 0);
Menu.HSplitBottom(40.0f, &Menu, &Button);
static CButtonContainer s_SettingsButton;
if(DoButton_Menu(&s_SettingsButton, Localize("Settings"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "settings" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_S))
NewPage = PAGE_SETTINGS;
Menu.HSplitBottom(5.0f, &Menu, 0); // little space
Menu.HSplitBottom(40.0f, &Menu, &Button);
static CButtonContainer s_LocalServerButton;
if(!is_process_alive(m_ServerProcess.m_Process))
KillServer();
if(DoButton_Menu(&s_LocalServerButton, m_ServerProcess.m_Process ? Localize("Stop server") : Localize("Run server"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "local_server" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), m_ServerProcess.m_Process ? vec4(0.0f, 1.0f, 0.0f, 0.25f) : vec4(0.0f, 0.0f, 0.0f, 0.25f)) || (CheckHotKey(KEY_R) && Input()->KeyPress(KEY_R)))
{
if(m_ServerProcess.m_Process)
{
KillServer();
}
else
{
char aBuf[IO_MAX_PATH_LENGTH];
Storage()->GetBinaryPath(PLAT_SERVER_EXEC, aBuf, sizeof(aBuf));
// No / in binary path means to search in $PATH, so it is expected that the file can't be opened. Just try executing anyway.
if(str_find(aBuf, "/") == 0)
{
m_ServerProcess.m_Process = shell_execute(aBuf);
}
else if(fs_is_file(aBuf))
{
m_ServerProcess.m_Process = shell_execute(aBuf);
}
else
{
Client()->AddWarning(SWarning(Localize("Server executable not found, can't run server")));
}
}
}
static bool EditorHotkeyWasPressed = true;
static float EditorHotKeyChecktime = 0.0f;
Menu.HSplitBottom(5.0f, &Menu, 0); // little space
Menu.HSplitBottom(40.0f, &Menu, &Button);
static CButtonContainer s_MapEditorButton;
if(DoButton_Menu(&s_MapEditorButton, Localize("Editor"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "editor" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), m_pClient->Editor()->HasUnsavedData() ? vec4(0.0f, 1.0f, 0.0f, 0.25f) : vec4(0.0f, 0.0f, 0.0f, 0.25f)) || (!EditorHotkeyWasPressed && Client()->LocalTime() - EditorHotKeyChecktime < 0.1f && CheckHotKey(KEY_E)))
{
g_Config.m_ClEditor = 1;
Input()->MouseModeRelative();
EditorHotkeyWasPressed = true;
}
if(!Input()->KeyIsPressed(KEY_E))
{
EditorHotkeyWasPressed = false;
EditorHotKeyChecktime = Client()->LocalTime();
}
Menu.HSplitBottom(5.0f, &Menu, 0); // little space
Menu.HSplitBottom(40.0f, &Menu, &Button);
static CButtonContainer s_DemoButton;
if(DoButton_Menu(&s_DemoButton, Localize("Demos"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "demos" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_D))
{
NewPage = PAGE_DEMOS;
}
Menu.HSplitBottom(5.0f, &Menu, 0); // little space
Menu.HSplitBottom(40.0f, &Menu, &Button);
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, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(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;
}
// render version
CUIRect VersionUpdate, CurVersion;
MainView.HSplitBottom(30.0f, 0, 0);
MainView.HSplitBottom(20.0f, 0, &VersionUpdate);
VersionUpdate.VSplitRight(50.0f, &CurVersion, 0);
VersionUpdate.VMargin(VMargin, &VersionUpdate);
#if defined(CONF_AUTOUPDATE)
char aBuf[64];
CUIRect Part;
int State = Updater()->GetCurrentState();
bool NeedUpdate = str_comp(Client()->LatestVersion(), "0");
if(State == IUpdater::CLEAN && NeedUpdate)
{
str_format(aBuf, sizeof(aBuf), Localize("DDNet %s is out!"), Client()->LatestVersion());
TextRender()->TextColor(1.0f, 0.4f, 0.4f, 1.0f);
}
else if(State == IUpdater::CLEAN)
{
aBuf[0] = '\0';
}
else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
{
char aCurrentFile[64];
Updater()->GetCurrentFile(aCurrentFile, sizeof(aCurrentFile));
str_format(aBuf, sizeof(aBuf), Localize("Downloading %s:"), aCurrentFile);
}
else if(State == IUpdater::FAIL)
{
str_format(aBuf, sizeof(aBuf), Localize("Update failed! Check log..."));
TextRender()->TextColor(1.0f, 0.4f, 0.4f, 1.0f);
}
else if(State == IUpdater::NEED_RESTART)
{
str_format(aBuf, sizeof(aBuf), Localize("DDNet Client updated!"));
TextRender()->TextColor(1.0f, 0.4f, 0.4f, 1.0f);
}
UI()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_ML);
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
VersionUpdate.VSplitLeft(TextRender()->TextWidth(14.0f, aBuf, -1, -1.0f) + 10.0f, 0, &Part);
if(State == IUpdater::CLEAN && NeedUpdate)
{
CUIRect Update;
Part.VSplitLeft(100.0f, &Update, NULL);
static CButtonContainer s_VersionUpdate;
if(DoButton_Menu(&s_VersionUpdate, Localize("Update now"), 0, &Update, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)))
{
Updater()->InitiateUpdate();
}
}
else if(State == IUpdater::NEED_RESTART)
{
CUIRect Restart;
Part.VSplitLeft(50.0f, &Restart, &Part);
static CButtonContainer s_VersionUpdate;
if(DoButton_Menu(&s_VersionUpdate, Localize("Restart"), 0, &Restart, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)))
{
Client()->Restart();
}
}
else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
{
CUIRect ProgressBar, Percent;
Part.VSplitLeft(100.0f, &ProgressBar, &Percent);
ProgressBar.y += 2.0f;
ProgressBar.HMargin(1.0f, &ProgressBar);
ProgressBar.Draw(vec4(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 5.0f);
ProgressBar.w = clamp((float)Updater()->GetCurrentPercent(), 10.0f, 100.0f);
ProgressBar.Draw(vec4(1.0f, 1.0f, 1.0f, 0.5f), IGraphics::CORNER_ALL, 5.0f);
}
#elif defined(CONF_INFORM_UPDATE)
if(str_comp(Client()->LatestVersion(), "0") != 0)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), Localize("DDNet %s is out!"), Client()->LatestVersion());
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
UI()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_MC);
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
}
#endif
UI()->DoLabel(&CurVersion, GAME_RELEASE_VERSION, 14.0f, TEXTALIGN_MR);
if(NewPage != -1)
{
m_MenuPage = NewPage;
m_ShowStart = false;
}
}
void CMenus::KillServer()
{
if(m_ServerProcess.m_Process)
{
if(kill_process(m_ServerProcess.m_Process))
{
m_ServerProcess.m_Process = INVALID_PROCESS;
}
}
}