1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include <engine/graphics.h>
4#include <engine/keys.h>
5#include <engine/serverbrowser.h>
6#include <engine/textrender.h>
7
8#include <engine/client/updater.h>
9#include <engine/shared/config.h>
10
11#include <game/client/gameclient.h>
12#include <game/client/ui.h>
13
14#include <game/generated/client_data.h>
15#include <game/localization.h>
16#include <game/version.h>
17
18#include "menus.h"
19
20void CMenus::RenderStartMenu(CUIRect MainView)
21{
22 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_START);
23
24 // render logo
25 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_BANNER].m_Id);
26 Graphics()->QuadsBegin();
27 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
28 IGraphics::CQuadItem QuadItem(MainView.w / 2 - 170, 60, 360, 103);
29 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
30 Graphics()->QuadsEnd();
31
32 const float Rounding = 10.0f;
33 const float VMargin = MainView.w / 2 - 190.0f;
34
35 CUIRect Button;
36 int NewPage = -1;
37
38 CUIRect ExtMenu;
39 MainView.VSplitLeft(Cut: 30.0f, pLeft: 0, pRight: &ExtMenu);
40 ExtMenu.VSplitLeft(Cut: 100.0f, pLeft: &ExtMenu, pRight: 0);
41
42 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
43 static CButtonContainer s_DiscordButton;
44 if(DoButton_Menu(pButtonContainer: &s_DiscordButton, pText: Localize(pStr: "Discord"), Checked: 0, pRect: &Button, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
45 {
46 Client()->ViewLink(pLink: Localize(pStr: "https://ddnet.org/discord"));
47 }
48
49 ExtMenu.HSplitBottom(Cut: 5.0f, pTop: &ExtMenu, pBottom: 0); // little space
50 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
51 static CButtonContainer s_LearnButton;
52 if(DoButton_Menu(pButtonContainer: &s_LearnButton, pText: Localize(pStr: "Learn"), Checked: 0, pRect: &Button, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
53 {
54 Client()->ViewLink(pLink: Localize(pStr: "https://wiki.ddnet.org/"));
55 }
56
57 ExtMenu.HSplitBottom(Cut: 5.0f, pTop: &ExtMenu, pBottom: 0); // little space
58 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
59 static CButtonContainer s_TutorialButton;
60 static float s_JoinTutorialTime = 0.0f;
61 if(DoButton_Menu(pButtonContainer: &s_TutorialButton, pText: Localize(pStr: "Tutorial"), Checked: 0, pRect: &Button, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) ||
62 (s_JoinTutorialTime != 0.0f && Client()->LocalTime() >= s_JoinTutorialTime))
63 {
64 // Activate internet tab before joining tutorial to make sure the server info
65 // for the tutorial servers is available.
66 SetMenuPage(PAGE_INTERNET);
67 RefreshBrowserTab(Force: true);
68 const char *pAddr = ServerBrowser()->GetTutorialServer();
69 if(pAddr)
70 {
71 Client()->Connect(pAddress: pAddr);
72 s_JoinTutorialTime = 0.0f;
73 }
74 else if(s_JoinTutorialTime == 0.0f)
75 {
76 dbg_msg(sys: "menus", fmt: "couldn't find tutorial server, retrying in 5 seconds");
77 s_JoinTutorialTime = Client()->LocalTime() + 5.0f;
78 }
79 else
80 {
81 Client()->AddWarning(Warning: SWarning(Localize(pStr: "Can't find a Tutorial server")));
82 s_JoinTutorialTime = 0.0f;
83 }
84 }
85
86 ExtMenu.HSplitBottom(Cut: 5.0f, pTop: &ExtMenu, pBottom: 0); // little space
87 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
88 static CButtonContainer s_WebsiteButton;
89 if(DoButton_Menu(pButtonContainer: &s_WebsiteButton, pText: Localize(pStr: "Website"), Checked: 0, pRect: &Button, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
90 {
91 Client()->ViewLink(pLink: "https://ddnet.org/");
92 }
93
94 ExtMenu.HSplitBottom(Cut: 5.0f, pTop: &ExtMenu, pBottom: 0); // little space
95 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
96 static CButtonContainer s_NewsButton;
97 if(DoButton_Menu(pButtonContainer: &s_NewsButton, pText: Localize(pStr: "News"), Checked: 0, pRect: &Button, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: g_Config.m_UiUnreadNews ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(Key: KEY_N))
98 NewPage = PAGE_NEWS;
99
100 CUIRect Menu;
101 MainView.VMargin(Cut: VMargin, pOtherRect: &Menu);
102 Menu.HSplitBottom(Cut: 25.0f, pTop: &Menu, pBottom: 0);
103
104 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
105 static CButtonContainer s_QuitButton;
106 bool UsedEscape = false;
107 if(DoButton_Menu(pButtonContainer: &s_QuitButton, pText: Localize(pStr: "Quit"), Checked: 0, pRect: &Button, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding, FontFactor: 0.5f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (UsedEscape = Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)) || CheckHotKey(Key: KEY_Q))
108 {
109 if(UsedEscape || m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0))
110 {
111 m_Popup = POPUP_QUIT;
112 }
113 else
114 {
115 Client()->Quit();
116 }
117 }
118
119 Menu.HSplitBottom(Cut: 100.0f, pTop: &Menu, pBottom: 0);
120 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
121 static CButtonContainer s_SettingsButton;
122 if(DoButton_Menu(pButtonContainer: &s_SettingsButton, pText: Localize(pStr: "Settings"), Checked: 0, pRect: &Button, pImageName: g_Config.m_ClShowStartMenuImages ? "settings" : 0, Corners: IGraphics::CORNER_ALL, Rounding, FontFactor: 0.5f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(Key: KEY_S))
123 NewPage = PAGE_SETTINGS;
124
125 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
126 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
127 static CButtonContainer s_LocalServerButton;
128
129 if(!is_process_alive(process: m_ServerProcess.m_Process))
130 KillServer();
131
132 if(DoButton_Menu(pButtonContainer: &s_LocalServerButton, pText: m_ServerProcess.m_Process ? Localize(pStr: "Stop server") : Localize(pStr: "Run server"), Checked: 0, pRect: &Button, pImageName: g_Config.m_ClShowStartMenuImages ? "local_server" : 0, Corners: IGraphics::CORNER_ALL, Rounding, FontFactor: 0.5f, Color: m_ServerProcess.m_Process ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (CheckHotKey(Key: KEY_R) && Input()->KeyPress(Key: KEY_R)))
133 {
134 if(m_ServerProcess.m_Process)
135 {
136 KillServer();
137 }
138 else
139 {
140 char aBuf[IO_MAX_PATH_LENGTH];
141 Storage()->GetBinaryPath(PLAT_SERVER_EXEC, pBuffer: aBuf, BufferSize: sizeof(aBuf));
142 // No / in binary path means to search in $PATH, so it is expected that the file can't be opened. Just try executing anyway.
143 if(str_find(haystack: aBuf, needle: "/") == 0 || fs_is_file(path: aBuf))
144 {
145 m_ServerProcess.m_Process = shell_execute(file: aBuf, window_state: EShellExecuteWindowState::BACKGROUND);
146 m_ForceRefreshLanPage = true;
147 }
148 else
149 {
150 Client()->AddWarning(Warning: SWarning(Localize(pStr: "Server executable not found, can't run server")));
151 }
152 }
153 }
154
155 static bool EditorHotkeyWasPressed = true;
156 static float EditorHotKeyChecktime = 0.0f;
157 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
158 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
159 static CButtonContainer s_MapEditorButton;
160 if(DoButton_Menu(pButtonContainer: &s_MapEditorButton, pText: Localize(pStr: "Editor"), Checked: 0, pRect: &Button, pImageName: g_Config.m_ClShowStartMenuImages ? "editor" : 0, Corners: IGraphics::CORNER_ALL, Rounding, FontFactor: 0.5f, Color: m_pClient->Editor()->HasUnsavedData() ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (!EditorHotkeyWasPressed && Client()->LocalTime() - EditorHotKeyChecktime < 0.1f && CheckHotKey(Key: KEY_E)))
161 {
162 g_Config.m_ClEditor = 1;
163 Input()->MouseModeRelative();
164 EditorHotkeyWasPressed = true;
165 }
166 if(!Input()->KeyIsPressed(Key: KEY_E))
167 {
168 EditorHotkeyWasPressed = false;
169 EditorHotKeyChecktime = Client()->LocalTime();
170 }
171
172 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
173 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
174 static CButtonContainer s_DemoButton;
175 if(DoButton_Menu(pButtonContainer: &s_DemoButton, pText: Localize(pStr: "Demos"), Checked: 0, pRect: &Button, pImageName: g_Config.m_ClShowStartMenuImages ? "demos" : 0, Corners: IGraphics::CORNER_ALL, Rounding, FontFactor: 0.5f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(Key: KEY_D))
176 {
177 NewPage = PAGE_DEMOS;
178 }
179
180 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
181 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
182 static CButtonContainer s_PlayButton;
183 if(DoButton_Menu(pButtonContainer: &s_PlayButton, pText: Localize(pStr: "Play", pContext: "Start menu"), Checked: 0, pRect: &Button, pImageName: g_Config.m_ClShowStartMenuImages ? "play_game" : 0, Corners: IGraphics::CORNER_ALL, Rounding, FontFactor: 0.5f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) || CheckHotKey(Key: KEY_P))
184 {
185 NewPage = g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 ? g_Config.m_UiPage : PAGE_INTERNET;
186 }
187
188 // render version
189 CUIRect VersionUpdate, CurVersion;
190 MainView.HSplitBottom(Cut: 20.0f, pTop: nullptr, pBottom: &VersionUpdate);
191 VersionUpdate.VSplitRight(Cut: 50.0f, pLeft: &CurVersion, pRight: nullptr);
192 VersionUpdate.VMargin(Cut: VMargin, pOtherRect: &VersionUpdate);
193
194 Ui()->DoLabel(pRect: &CurVersion, GAME_RELEASE_VERSION, Size: 14.0f, Align: TEXTALIGN_MR);
195
196#if defined(CONF_AUTOUPDATE)
197 CUIRect UpdateButton;
198 VersionUpdate.VSplitRight(Cut: 100.0f, pLeft: &VersionUpdate, pRight: &UpdateButton);
199 VersionUpdate.VSplitRight(Cut: 10.0f, pLeft: &VersionUpdate, pRight: nullptr);
200
201 char aBuf[128];
202 const IUpdater::EUpdaterState State = Updater()->GetCurrentState();
203 const bool NeedUpdate = str_comp(a: Client()->LatestVersion(), b: "0");
204
205 if(State == IUpdater::CLEAN && NeedUpdate)
206 {
207 static CButtonContainer s_VersionUpdate;
208 if(DoButton_Menu(pButtonContainer: &s_VersionUpdate, pText: Localize(pStr: "Update now"), Checked: 0, pRect: &UpdateButton, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
209 {
210 Updater()->InitiateUpdate();
211 }
212 }
213 else if(State == IUpdater::NEED_RESTART)
214 {
215 static CButtonContainer s_VersionUpdate;
216 if(DoButton_Menu(pButtonContainer: &s_VersionUpdate, pText: Localize(pStr: "Restart"), Checked: 0, pRect: &UpdateButton, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
217 {
218 Client()->Restart();
219 }
220 }
221 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
222 {
223 Ui()->RenderProgressBar(ProgressBar: UpdateButton, Progress: Updater()->GetCurrentPercent() / 100.0f);
224 }
225
226 if(State == IUpdater::CLEAN && NeedUpdate)
227 {
228 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet %s is out!"), Client()->LatestVersion());
229 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
230 }
231 else if(State == IUpdater::CLEAN)
232 {
233 aBuf[0] = '\0';
234 }
235 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
236 {
237 char aCurrentFile[64];
238 Updater()->GetCurrentFile(pBuf: aCurrentFile, BufSize: sizeof(aCurrentFile));
239 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Downloading %s:"), aCurrentFile);
240 }
241 else if(State == IUpdater::FAIL)
242 {
243 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Update failed! Check log…"));
244 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
245 }
246 else if(State == IUpdater::NEED_RESTART)
247 {
248 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet Client updated!"));
249 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
250 }
251 Ui()->DoLabel(pRect: &VersionUpdate, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
252 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
253#elif defined(CONF_INFORM_UPDATE)
254 if(str_comp(Client()->LatestVersion(), "0") != 0)
255 {
256 char aBuf[64];
257 str_format(aBuf, sizeof(aBuf), Localize("DDNet %s is out!"), Client()->LatestVersion());
258 TextRender()->TextColor(TextRender()->DefaultTextColor());
259 Ui()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_MC);
260 }
261#endif
262
263 if(NewPage != -1)
264 {
265 m_ShowStart = false;
266 SetMenuPage(NewPage);
267 }
268}
269
270void CMenus::KillServer()
271{
272 if(m_ServerProcess.m_Process)
273 {
274 if(kill_process(process: m_ServerProcess.m_Process))
275 {
276 m_ServerProcess.m_Process = INVALID_PROCESS;
277 m_ForceRefreshLanPage = true;
278 }
279 }
280}
281