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 const char *pLink = Localize(pStr: "https://ddnet.org/discord");
47 if(!open_link(link: pLink))
48 {
49 dbg_msg(sys: "menus", fmt: "couldn't open link '%s'", pLink);
50 }
51 }
52
53 ExtMenu.HSplitBottom(Cut: 5.0f, pTop: &ExtMenu, pBottom: 0); // little space
54 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
55 static CButtonContainer s_LearnButton;
56 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)))
57 {
58 const char *pLink = Localize(pStr: "https://wiki.ddnet.org/");
59 if(!open_link(link: pLink))
60 {
61 dbg_msg(sys: "menus", fmt: "couldn't open link '%s'", pLink);
62 }
63 }
64
65 ExtMenu.HSplitBottom(Cut: 5.0f, pTop: &ExtMenu, pBottom: 0); // little space
66 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
67 static CButtonContainer s_TutorialButton;
68 static float s_JoinTutorialTime = 0.0f;
69 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)) ||
70 (s_JoinTutorialTime != 0.0f && Client()->LocalTime() >= s_JoinTutorialTime))
71 {
72 // Activate internet tab before joining tutorial to make sure the server info
73 // for the tutorial servers is available.
74 SetMenuPage(PAGE_INTERNET);
75 RefreshBrowserTab(Force: true);
76 const char *pAddr = ServerBrowser()->GetTutorialServer();
77 if(pAddr)
78 {
79 Client()->Connect(pAddress: pAddr);
80 s_JoinTutorialTime = 0.0f;
81 }
82 else if(s_JoinTutorialTime == 0.0f)
83 {
84 dbg_msg(sys: "menus", fmt: "couldn't find tutorial server, retrying in 5 seconds");
85 s_JoinTutorialTime = Client()->LocalTime() + 5.0f;
86 }
87 else
88 {
89 Client()->AddWarning(Warning: SWarning(Localize(pStr: "Can't find a Tutorial server")));
90 s_JoinTutorialTime = 0.0f;
91 }
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_WebsiteButton;
97 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)))
98 {
99 const char *pLink = "https://ddnet.org/";
100 if(!open_link(link: pLink))
101 {
102 dbg_msg(sys: "menus", fmt: "couldn't open link '%s'", pLink);
103 }
104 }
105
106 ExtMenu.HSplitBottom(Cut: 5.0f, pTop: &ExtMenu, pBottom: 0); // little space
107 ExtMenu.HSplitBottom(Cut: 20.0f, pTop: &ExtMenu, pBottom: &Button);
108 static CButtonContainer s_NewsButton;
109 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))
110 NewPage = PAGE_NEWS;
111
112 CUIRect Menu;
113 MainView.VMargin(Cut: VMargin, pOtherRect: &Menu);
114 Menu.HSplitBottom(Cut: 25.0f, pTop: &Menu, pBottom: 0);
115
116 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
117 static CButtonContainer s_QuitButton;
118 bool UsedEscape = false;
119 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))
120 {
121 if(UsedEscape || m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0))
122 {
123 m_Popup = POPUP_QUIT;
124 }
125 else
126 {
127 Client()->Quit();
128 }
129 }
130
131 Menu.HSplitBottom(Cut: 100.0f, pTop: &Menu, pBottom: 0);
132 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
133 static CButtonContainer s_SettingsButton;
134 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))
135 NewPage = PAGE_SETTINGS;
136
137 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
138 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
139 static CButtonContainer s_LocalServerButton;
140
141 if(!is_process_alive(process: m_ServerProcess.m_Process))
142 KillServer();
143
144 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)))
145 {
146 if(m_ServerProcess.m_Process)
147 {
148 KillServer();
149 }
150 else
151 {
152 char aBuf[IO_MAX_PATH_LENGTH];
153 Storage()->GetBinaryPath(PLAT_SERVER_EXEC, pBuffer: aBuf, BufferSize: sizeof(aBuf));
154 // No / in binary path means to search in $PATH, so it is expected that the file can't be opened. Just try executing anyway.
155 if(str_find(haystack: aBuf, needle: "/") == 0 || fs_is_file(path: aBuf))
156 {
157 m_ServerProcess.m_Process = shell_execute(file: aBuf, window_state: EShellExecuteWindowState::BACKGROUND);
158 }
159 else
160 {
161 Client()->AddWarning(Warning: SWarning(Localize(pStr: "Server executable not found, can't run server")));
162 }
163 }
164 }
165
166 static bool EditorHotkeyWasPressed = true;
167 static float EditorHotKeyChecktime = 0.0f;
168 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
169 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
170 static CButtonContainer s_MapEditorButton;
171 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)))
172 {
173 g_Config.m_ClEditor = 1;
174 Input()->MouseModeRelative();
175 EditorHotkeyWasPressed = true;
176 }
177 if(!Input()->KeyIsPressed(Key: KEY_E))
178 {
179 EditorHotkeyWasPressed = false;
180 EditorHotKeyChecktime = Client()->LocalTime();
181 }
182
183 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
184 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
185 static CButtonContainer s_DemoButton;
186 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))
187 {
188 NewPage = PAGE_DEMOS;
189 }
190
191 Menu.HSplitBottom(Cut: 5.0f, pTop: &Menu, pBottom: 0); // little space
192 Menu.HSplitBottom(Cut: 40.0f, pTop: &Menu, pBottom: &Button);
193 static CButtonContainer s_PlayButton;
194 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))
195 {
196 NewPage = g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 ? g_Config.m_UiPage : PAGE_INTERNET;
197 }
198
199 // render version
200 CUIRect VersionUpdate, CurVersion;
201 MainView.HSplitBottom(Cut: 20.0f, pTop: 0, pBottom: &VersionUpdate);
202
203 VersionUpdate.VSplitRight(Cut: 50.0f, pLeft: &CurVersion, pRight: 0);
204 VersionUpdate.VMargin(Cut: VMargin, pOtherRect: &VersionUpdate);
205
206#if defined(CONF_AUTOUPDATE)
207 char aBuf[64];
208 CUIRect Part;
209 int State = Updater()->GetCurrentState();
210 bool NeedUpdate = str_comp(a: Client()->LatestVersion(), b: "0");
211 if(State == IUpdater::CLEAN && NeedUpdate)
212 {
213 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet %s is out!"), Client()->LatestVersion());
214 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
215 }
216 else if(State == IUpdater::CLEAN)
217 {
218 aBuf[0] = '\0';
219 }
220 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
221 {
222 char aCurrentFile[64];
223 Updater()->GetCurrentFile(pBuf: aCurrentFile, BufSize: sizeof(aCurrentFile));
224 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Downloading %s:"), aCurrentFile);
225 }
226 else if(State == IUpdater::FAIL)
227 {
228 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Update failed! Check log…"));
229 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
230 }
231 else if(State == IUpdater::NEED_RESTART)
232 {
233 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet Client updated!"));
234 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
235 }
236 Ui()->DoLabel(pRect: &VersionUpdate, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
237 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
238
239 VersionUpdate.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: 0, pRight: &Part);
240
241 if(State == IUpdater::CLEAN && NeedUpdate)
242 {
243 CUIRect Update;
244 Part.VSplitLeft(Cut: 100.0f, pLeft: &Update, NULL);
245
246 static CButtonContainer s_VersionUpdate;
247 if(DoButton_Menu(pButtonContainer: &s_VersionUpdate, pText: Localize(pStr: "Update now"), Checked: 0, pRect: &Update, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
248 {
249 Updater()->InitiateUpdate();
250 }
251 }
252 else if(State == IUpdater::NEED_RESTART)
253 {
254 CUIRect Restart;
255 Part.VSplitLeft(Cut: 50.0f, pLeft: &Restart, pRight: &Part);
256
257 static CButtonContainer s_VersionUpdate;
258 if(DoButton_Menu(pButtonContainer: &s_VersionUpdate, pText: Localize(pStr: "Restart"), Checked: 0, pRect: &Restart, pImageName: 0, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
259 {
260 Client()->Restart();
261 }
262 }
263 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
264 {
265 CUIRect ProgressBar, Percent;
266 Part.VSplitLeft(Cut: 100.0f, pLeft: &ProgressBar, pRight: &Percent);
267 ProgressBar.y += 2.0f;
268 ProgressBar.HMargin(Cut: 1.0f, pOtherRect: &ProgressBar);
269 ProgressBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
270 ProgressBar.w = clamp(val: (float)Updater()->GetCurrentPercent(), lo: 10.0f, hi: 100.0f);
271 ProgressBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
272 }
273#elif defined(CONF_INFORM_UPDATE)
274 if(str_comp(Client()->LatestVersion(), "0") != 0)
275 {
276 char aBuf[64];
277 str_format(aBuf, sizeof(aBuf), Localize("DDNet %s is out!"), Client()->LatestVersion());
278 TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
279 Ui()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_MC);
280 TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
281 }
282#endif
283
284 Ui()->DoLabel(pRect: &CurVersion, GAME_RELEASE_VERSION, Size: 14.0f, Align: TEXTALIGN_MR);
285
286 if(NewPage != -1)
287 {
288 m_MenuPage = NewPage;
289 m_ShowStart = false;
290 }
291}
292
293void CMenus::KillServer()
294{
295 if(m_ServerProcess.m_Process)
296 {
297 if(kill_process(process: m_ServerProcess.m_Process))
298 {
299 m_ServerProcess.m_Process = INVALID_PROCESS;
300 }
301 }
302}
303