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
4#include "menus.h"
5
6#include <base/color.h>
7#include <base/dbg.h>
8#include <base/fs.h>
9#include <base/log.h>
10#include <base/str.h>
11#include <base/time.h>
12#include <base/vmath.h>
13
14#include <engine/client.h>
15#include <engine/client/updater.h>
16#include <engine/config.h>
17#include <engine/editor.h>
18#include <engine/font_icons.h>
19#include <engine/friends.h>
20#include <engine/gfx/image_manipulation.h>
21#include <engine/graphics.h>
22#include <engine/keys.h>
23#include <engine/serverbrowser.h>
24#include <engine/shared/config.h>
25#include <engine/storage.h>
26#include <engine/textrender.h>
27
28#include <generated/client_data.h>
29#include <generated/protocol.h>
30
31#include <game/client/animstate.h>
32#include <game/client/components/binds.h>
33#include <game/client/components/console.h>
34#include <game/client/components/key_binder.h>
35#include <game/client/components/menu_background.h>
36#include <game/client/components/sounds.h>
37#include <game/client/gameclient.h>
38#include <game/client/ui_listbox.h>
39#include <game/localization.h>
40
41#include <algorithm>
42#include <chrono>
43#include <cmath>
44#include <vector>
45
46using namespace std::chrono_literals;
47
48ColorRGBA CMenus::ms_GuiColor;
49ColorRGBA CMenus::ms_ColorTabbarInactiveOutgame;
50ColorRGBA CMenus::ms_ColorTabbarActiveOutgame;
51ColorRGBA CMenus::ms_ColorTabbarHoverOutgame;
52ColorRGBA CMenus::ms_ColorTabbarInactive;
53ColorRGBA CMenus::ms_ColorTabbarActive = ColorRGBA(0, 0, 0, 0.5f);
54ColorRGBA CMenus::ms_ColorTabbarHover;
55ColorRGBA CMenus::ms_ColorTabbarInactiveIngame;
56ColorRGBA CMenus::ms_ColorTabbarActiveIngame;
57ColorRGBA CMenus::ms_ColorTabbarHoverIngame;
58
59float CMenus::ms_ButtonHeight = 25.0f;
60float CMenus::ms_ListheaderHeight = 17.0f;
61
62CMenus::CMenus()
63{
64 m_Popup = POPUP_NONE;
65 m_MenuPage = 0;
66 m_GamePage = PAGE_GAME;
67
68 m_NeedRestartGraphics = false;
69 m_NeedRestartSound = false;
70 m_NeedSendinfo = false;
71 m_NeedSendDummyinfo = false;
72 m_MenuActive = true;
73 m_ShowStart = true;
74
75 str_copy(dst&: m_aCurrentDemoFolder, src: "demos");
76 m_DemolistStorageType = IStorage::TYPE_ALL;
77
78 m_DemoPlayerState = DEMOPLAYER_NONE;
79 m_Dummy = false;
80
81 for(SUIAnimator &Animator : m_aAnimatorsSettingsTab)
82 {
83 Animator.m_YOffset = -2.5f;
84 Animator.m_HOffset = 5.0f;
85 Animator.m_WOffset = 5.0f;
86 Animator.m_RepositionLabel = true;
87 }
88
89 for(SUIAnimator &Animator : m_aAnimatorsBigPage)
90 {
91 Animator.m_YOffset = -5.0f;
92 Animator.m_HOffset = 5.0f;
93 }
94
95 for(SUIAnimator &Animator : m_aAnimatorsSmallPage)
96 {
97 Animator.m_YOffset = -2.5f;
98 Animator.m_HOffset = 2.5f;
99 }
100
101 m_PasswordInput.SetBuffer(pStr: g_Config.m_Password, MaxSize: sizeof(g_Config.m_Password));
102 m_PasswordInput.SetHidden(true);
103}
104
105int CMenus::DoButton_Toggle(const void *pId, int Checked, const CUIRect *pRect, bool Active, const unsigned Flags)
106{
107 Graphics()->TextureSet(Texture: g_pData->m_aImages[IMAGE_GUIBUTTONS].m_Id);
108 Graphics()->QuadsBegin();
109 if(!Active)
110 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.5f);
111 Graphics()->SelectSprite(Id: Checked ? SPRITE_GUIBUTTON_ON : SPRITE_GUIBUTTON_OFF);
112 IGraphics::CQuadItem QuadItem(pRect->x, pRect->y, pRect->w, pRect->h);
113 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
114 if(Ui()->HotItem() == pId && Active)
115 {
116 Graphics()->SelectSprite(Id: SPRITE_GUIBUTTON_HOVER);
117 QuadItem = IGraphics::CQuadItem(pRect->x, pRect->y, pRect->w, pRect->h);
118 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
119 }
120 Graphics()->QuadsEnd();
121
122 return Active ? Ui()->DoButtonLogic(pId, Checked, pRect, Flags) : 0;
123}
124
125int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, const unsigned Flags, const char *pImageName, int Corners, float Rounding, float FontFactor, ColorRGBA Color)
126{
127 CUIRect Text = *pRect;
128
129 if(Checked)
130 Color = ColorRGBA(0.6f, 0.6f, 0.6f, 0.5f);
131 Color.a *= Ui()->ButtonColorMul(pId: pButtonContainer);
132
133 pRect->Draw(Color, Corners, Rounding);
134
135 if(pImageName)
136 {
137 CUIRect Image;
138 pRect->VSplitRight(Cut: pRect->h * 4.0f, pLeft: &Text, pRight: &Image); // always correct ratio for image
139
140 // render image
141 const CMenuImage *pImage = FindMenuImage(pName: pImageName);
142 if(pImage)
143 {
144 Graphics()->TextureSet(Texture: Ui()->HotItem() == pButtonContainer ? pImage->m_OrgTexture : pImage->m_GreyTexture);
145 Graphics()->WrapClamp();
146 Graphics()->QuadsBegin();
147 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
148 IGraphics::CQuadItem QuadItem(Image.x, Image.y, Image.w, Image.h);
149 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
150 Graphics()->QuadsEnd();
151 Graphics()->WrapNormal();
152 }
153 }
154
155 Text.HMargin(Cut: pRect->h >= 20.0f ? 2.0f : 1.0f, pOtherRect: &Text);
156 Text.HMargin(Cut: (Text.h * FontFactor) / 2.0f, pOtherRect: &Text);
157 Ui()->DoLabel(pRect: &Text, pText, Size: Text.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
158
159 return Ui()->DoButtonLogic(pId: pButtonContainer, Checked, pRect, Flags);
160}
161
162int 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 CCommunityIcon *pCommunityIcon)
163{
164 const bool MouseInside = Ui()->HotItem() == pButtonContainer;
165 CUIRect Rect = *pRect;
166
167 if(pAnimator != nullptr)
168 {
169 auto Time = time_get_nanoseconds();
170
171 if(pAnimator->m_Time + 100ms < Time)
172 {
173 pAnimator->m_Value = pAnimator->m_Active ? 1 : 0;
174 pAnimator->m_Time = Time;
175 }
176
177 pAnimator->m_Active = Checked || MouseInside;
178
179 if(pAnimator->m_Active)
180 pAnimator->m_Value = std::clamp<float>(val: pAnimator->m_Value + (Time - pAnimator->m_Time).count() / (double)std::chrono::nanoseconds(100ms).count(), lo: 0, hi: 1);
181 else
182 pAnimator->m_Value = std::clamp<float>(val: pAnimator->m_Value - (Time - pAnimator->m_Time).count() / (double)std::chrono::nanoseconds(100ms).count(), lo: 0, hi: 1);
183
184 Rect.w += pAnimator->m_Value * pAnimator->m_WOffset;
185 Rect.h += pAnimator->m_Value * pAnimator->m_HOffset;
186 Rect.x += pAnimator->m_Value * pAnimator->m_XOffset;
187 Rect.y += pAnimator->m_Value * pAnimator->m_YOffset;
188
189 pAnimator->m_Time = Time;
190 }
191
192 if(Checked)
193 {
194 ColorRGBA ColorMenuTab = ms_ColorTabbarActive;
195 if(pActiveColor)
196 ColorMenuTab = *pActiveColor;
197
198 Rect.Draw(Color: ColorMenuTab, Corners, Rounding: EdgeRounding);
199 }
200 else
201 {
202 if(MouseInside)
203 {
204 ColorRGBA HoverColorMenuTab = ms_ColorTabbarHover;
205 if(pHoverColor)
206 HoverColorMenuTab = *pHoverColor;
207
208 Rect.Draw(Color: HoverColorMenuTab, Corners, Rounding: EdgeRounding);
209 }
210 else
211 {
212 ColorRGBA ColorMenuTab = ms_ColorTabbarInactive;
213 if(pDefaultColor)
214 ColorMenuTab = *pDefaultColor;
215
216 Rect.Draw(Color: ColorMenuTab, Corners, Rounding: EdgeRounding);
217 }
218 }
219
220 if(pAnimator != nullptr)
221 {
222 if(pAnimator->m_RepositionLabel)
223 {
224 Rect.x += Rect.w - pRect->w + Rect.x - pRect->x;
225 Rect.y += Rect.h - pRect->h + Rect.y - pRect->y;
226 }
227
228 if(!pAnimator->m_ScaleLabel)
229 {
230 Rect.w = pRect->w;
231 Rect.h = pRect->h;
232 }
233 }
234
235 if(pCommunityIcon)
236 {
237 CUIRect CommunityIcon;
238 Rect.Margin(Cut: 2.0f, pOtherRect: &CommunityIcon);
239 m_CommunityIcons.Render(pIcon: pCommunityIcon, Rect: CommunityIcon, Active: true);
240 }
241 else
242 {
243 CUIRect Label;
244 Rect.HMargin(Cut: 2.0f, pOtherRect: &Label);
245 Ui()->DoLabel(pRect: &Label, pText, Size: Label.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
246 }
247
248 return Ui()->DoButtonLogic(pId: pButtonContainer, Checked, pRect, Flags: BUTTONFLAG_LEFT);
249}
250
251int CMenus::DoButton_GridHeader(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Align)
252{
253 if(Checked == 2)
254 pRect->Draw(Color: ColorRGBA(1, 0.98f, 0.5f, 0.55f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
255 else if(Checked)
256 pRect->Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
257
258 CUIRect Temp;
259 pRect->VMargin(Cut: 5.0f, pOtherRect: &Temp);
260 Ui()->DoLabel(pRect: &Temp, pText, Size: pRect->h * CUi::ms_FontmodHeight, Align);
261 return Ui()->DoButtonLogic(pId, Checked, pRect, Flags: BUTTONFLAG_LEFT);
262}
263
264int CMenus::DoButton_Favorite(const void *pButtonId, const void *pParentId, bool Checked, const CUIRect *pRect)
265{
266 if(Checked || (pParentId != nullptr && Ui()->HotItem() == pParentId) || Ui()->HotItem() == pButtonId)
267 {
268 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
269 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_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
270 const float Alpha = Ui()->HotItem() == pButtonId ? 0.2f : 0.0f;
271 TextRender()->TextColor(Color: Checked ? ColorRGBA(1.0f, 0.85f, 0.3f, 0.8f + Alpha) : ColorRGBA(0.5f, 0.5f, 0.5f, 0.8f + Alpha));
272 SLabelProperties Props;
273 Props.m_MaxWidth = pRect->w;
274 Ui()->DoLabel(pRect, pText: FontIcon::STAR, Size: 12.0f, Align: TEXTALIGN_MC, LabelProps: Props);
275 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
276 TextRender()->SetRenderFlags(0);
277 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
278 }
279 return Ui()->DoButtonLogic(pId: pButtonId, Checked: 0, pRect, Flags: BUTTONFLAG_LEFT);
280}
281
282int CMenus::DoButton_CheckBox_Common(const void *pId, const char *pText, const char *pBoxText, const CUIRect *pRect, const unsigned Flags)
283{
284 CUIRect Box, Label;
285 pRect->VSplitLeft(Cut: pRect->h, pLeft: &Box, pRight: &Label);
286 Label.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Label);
287
288 Box.Margin(Cut: 2.0f, pOtherRect: &Box);
289 Box.Draw(Color: ColorRGBA(1, 1, 1, 0.25f * Ui()->ButtonColorMul(pId)), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
290
291 const bool Checkable = *pBoxText == 'X';
292 if(Checkable)
293 {
294 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_OVERSIZE | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT);
295 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
296 Ui()->DoLabel(pRect: &Box, pText: FontIcon::XMARK, Size: Box.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
297 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
298 }
299 else
300 {
301 Ui()->DoLabel(pRect: &Box, pText: pBoxText, Size: Box.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
302 }
303
304 TextRender()->SetRenderFlags(0);
305 Ui()->DoLabel(pRect: &Label, pText, Size: Box.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_ML);
306
307 return Ui()->DoButtonLogic(pId, Checked: 0, pRect, Flags);
308}
309
310bool CMenus::DoLine_RadioMenu(CUIRect &View, const char *pLabel, std::vector<CButtonContainer> &vButtonContainers, const std::vector<const char *> &vLabels, const std::vector<int> &vValues, int &Value)
311{
312 dbg_assert(vButtonContainers.size() == vValues.size(), "vButtonContainers and vValues must have the same size");
313 dbg_assert(vButtonContainers.size() == vLabels.size(), "vButtonContainers and vLabels must have the same size");
314 const int N = vButtonContainers.size();
315 const float Spacing = 2.0f;
316 const float ButtonHeight = 20.0f;
317 CUIRect Label, Buttons;
318 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
319 View.HSplitTop(Cut: ButtonHeight, pTop: &Buttons, pBottom: &View);
320 Buttons.VSplitMid(pLeft: &Label, pRight: &Buttons, Spacing: 10.0f);
321 Buttons.HMargin(Cut: 2.0f, pOtherRect: &Buttons);
322 Ui()->DoLabel(pRect: &Label, pText: pLabel, Size: 13.0f, Align: TEXTALIGN_ML);
323 const float W = Buttons.w / N;
324 bool Pressed = false;
325 for(int i = 0; i < N; ++i)
326 {
327 CUIRect Button;
328 Buttons.VSplitLeft(Cut: W, pLeft: &Button, pRight: &Buttons);
329 int Corner = IGraphics::CORNER_NONE;
330 if(i == 0)
331 Corner = IGraphics::CORNER_L;
332 if(i == N - 1)
333 Corner = IGraphics::CORNER_R;
334 if(DoButton_Menu(pButtonContainer: &vButtonContainers[i], pText: vLabels[i], Checked: vValues[i] == Value, pRect: &Button, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: Corner))
335 {
336 Pressed = true;
337 Value = vValues[i];
338 }
339 }
340 return Pressed;
341}
342
343ColorHSLA CMenus::DoLine_ColorPicker(CButtonContainer *pResetId, const float LineSize, const float LabelSize, const float BottomMargin, CUIRect *pMainRect, const char *pText, unsigned int *pColorValue, const ColorRGBA DefaultColor, bool CheckBoxSpacing, int *pCheckBoxValue, bool Alpha)
344{
345 CUIRect Section, ColorPickerButton, ResetButton, Label;
346
347 pMainRect->HSplitTop(Cut: LineSize, pTop: &Section, pBottom: pMainRect);
348 pMainRect->HSplitTop(Cut: BottomMargin, pTop: nullptr, pBottom: pMainRect);
349
350 Section.VSplitRight(Cut: 60.0f, pLeft: &Section, pRight: &ResetButton);
351 Section.VSplitRight(Cut: 8.0f, pLeft: &Section, pRight: nullptr);
352 Section.VSplitRight(Cut: Section.h, pLeft: &Section, pRight: &ColorPickerButton);
353 Section.VSplitRight(Cut: 8.0f, pLeft: &Label, pRight: nullptr);
354
355 if(pCheckBoxValue != nullptr)
356 {
357 Label.Margin(Cut: 2.0f, pOtherRect: &Label);
358 if(DoButton_CheckBox(pId: pCheckBoxValue, pText, Checked: *pCheckBoxValue, pRect: &Label))
359 *pCheckBoxValue ^= 1;
360 }
361 else if(CheckBoxSpacing)
362 {
363 Label.VSplitLeft(Cut: Label.h + 5.0f, pLeft: nullptr, pRight: &Label);
364 }
365 if(pCheckBoxValue == nullptr)
366 {
367 Ui()->DoLabel(pRect: &Label, pText, Size: LabelSize, Align: TEXTALIGN_ML);
368 }
369
370 const ColorHSLA PickedColor = DoButton_ColorPicker(pRect: &ColorPickerButton, pHslaColor: pColorValue, Alpha);
371
372 ResetButton.HMargin(Cut: 2.0f, pOtherRect: &ResetButton);
373 if(DoButton_Menu(pButtonContainer: pResetId, pText: Localize(pStr: "Reset"), Checked: 0, pRect: &ResetButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 4.0f, FontFactor: 0.1f, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f)))
374 {
375 *pColorValue = color_cast<ColorHSLA>(rgb: DefaultColor).Pack(Alpha);
376 }
377
378 return PickedColor;
379}
380
381ColorHSLA CMenus::DoButton_ColorPicker(const CUIRect *pRect, unsigned int *pHslaColor, bool Alpha)
382{
383 ColorHSLA HslaColor = ColorHSLA(*pHslaColor, Alpha);
384
385 ColorRGBA Outline = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f);
386 Outline.a *= Ui()->ButtonColorMul(pId: pHslaColor);
387
388 CUIRect Rect;
389 pRect->Margin(Cut: 3.0f, pOtherRect: &Rect);
390
391 pRect->Draw(Color: Outline, Corners: IGraphics::CORNER_ALL, Rounding: 4.0f);
392 Rect.Draw(Color: color_cast<ColorRGBA>(hsl: HslaColor), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f);
393
394 if(Ui()->DoButtonLogic(pId: pHslaColor, Checked: 0, pRect, Flags: BUTTONFLAG_LEFT))
395 {
396 m_ColorPickerPopupContext.m_pHslaColor = pHslaColor;
397 m_ColorPickerPopupContext.m_HslaColor = HslaColor;
398 m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(hsl: HslaColor);
399 m_ColorPickerPopupContext.m_RgbaColor = color_cast<ColorRGBA>(hsv: m_ColorPickerPopupContext.m_HsvaColor);
400 m_ColorPickerPopupContext.m_Alpha = Alpha;
401 Ui()->ShowPopupColorPicker(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &m_ColorPickerPopupContext);
402 }
403 else if(Ui()->IsPopupOpen(pId: &m_ColorPickerPopupContext) && m_ColorPickerPopupContext.m_pHslaColor == pHslaColor)
404 {
405 HslaColor = color_cast<ColorHSLA>(hsv: m_ColorPickerPopupContext.m_HsvaColor);
406 }
407
408 return HslaColor;
409}
410
411int CMenus::DoButton_CheckBoxAutoVMarginAndSet(const void *pId, const char *pText, int *pValue, CUIRect *pRect, float VMargin)
412{
413 CUIRect CheckBoxRect;
414 pRect->HSplitTop(Cut: VMargin, pTop: &CheckBoxRect, pBottom: pRect);
415
416 int Logic = DoButton_CheckBox_Common(pId, pText, pBoxText: *pValue ? "X" : "", pRect: &CheckBoxRect, Flags: BUTTONFLAG_LEFT);
417
418 if(Logic)
419 *pValue ^= 1;
420
421 return Logic;
422}
423
424int CMenus::DoButton_CheckBox(const void *pId, const char *pText, int Checked, const CUIRect *pRect)
425{
426 return DoButton_CheckBox_Common(pId, pText, pBoxText: Checked ? "X" : "", pRect, Flags: BUTTONFLAG_LEFT);
427}
428
429int CMenus::DoButton_CheckBox_Number(const void *pId, const char *pText, int Checked, const CUIRect *pRect)
430{
431 char aBuf[16];
432 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Checked);
433 return DoButton_CheckBox_Common(pId, pText, pBoxText: aBuf, pRect, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
434}
435
436void CMenus::RenderMenubar(CUIRect Box, IClient::EClientState ClientState)
437{
438 CUIRect Button;
439
440 int NewPage = -1;
441 int ActivePage = -1;
442 if(ClientState == IClient::STATE_OFFLINE)
443 {
444 ActivePage = m_MenuPage;
445 }
446 else if(ClientState == IClient::STATE_ONLINE)
447 {
448 ActivePage = m_GamePage;
449 }
450 else
451 {
452 dbg_assert_failed("Client state %d is invalid for RenderMenubar", ClientState);
453 }
454
455 // First render buttons aligned from right side so remaining
456 // width is known when rendering buttons from left side.
457 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
458 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_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
459
460 Box.VSplitRight(Cut: 33.0f, pLeft: &Box, pRight: &Button);
461 static CButtonContainer s_QuitButton;
462 ColorRGBA QuitColor(1, 0, 0, 0.5f);
463 if(DoButton_MenuTab(pButtonContainer: &s_QuitButton, pText: FontIcon::POWER_OFF, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_QUIT], pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: &QuitColor, EdgeRounding: 10.0f))
464 {
465 if(GameClient()->Editor()->HasUnsavedData() || (GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0) || m_MenusIngameTouchControls.UnsavedChanges() || GameClient()->m_TouchControls.HasEditingChanges())
466 {
467 m_Popup = POPUP_QUIT;
468 }
469 else
470 {
471 Client()->Quit();
472 }
473 }
474 GameClient()->m_Tooltips.DoToolTip(pId: &s_QuitButton, pNearRect: &Button, pText: Localize(pStr: "Quit"));
475
476 Box.VSplitRight(Cut: 10.0f, pLeft: &Box, pRight: nullptr);
477 Box.VSplitRight(Cut: 33.0f, pLeft: &Box, pRight: &Button);
478 static CButtonContainer s_SettingsButton;
479 if(DoButton_MenuTab(pButtonContainer: &s_SettingsButton, pText: FontIcon::GEAR, Checked: ActivePage == PAGE_SETTINGS, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_SETTINGS]))
480 {
481 NewPage = PAGE_SETTINGS;
482 }
483 GameClient()->m_Tooltips.DoToolTip(pId: &s_SettingsButton, pNearRect: &Button, pText: Localize(pStr: "Settings"));
484
485 Box.VSplitRight(Cut: 10.0f, pLeft: &Box, pRight: nullptr);
486 Box.VSplitRight(Cut: 33.0f, pLeft: &Box, pRight: &Button);
487 static CButtonContainer s_EditorButton;
488 if(DoButton_MenuTab(pButtonContainer: &s_EditorButton, pText: FontIcon::PEN_TO_SQUARE, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_EDITOR]))
489 {
490 g_Config.m_ClEditor = 1;
491 }
492 GameClient()->m_Tooltips.DoToolTip(pId: &s_EditorButton, pNearRect: &Button, pText: Localize(pStr: "Editor"));
493
494 if(ClientState == IClient::STATE_OFFLINE)
495 {
496 Box.VSplitRight(Cut: 10.0f, pLeft: &Box, pRight: nullptr);
497 Box.VSplitRight(Cut: 33.0f, pLeft: &Box, pRight: &Button);
498 static CButtonContainer s_DemoButton;
499 if(DoButton_MenuTab(pButtonContainer: &s_DemoButton, pText: FontIcon::CLAPPERBOARD, Checked: ActivePage == PAGE_DEMOS, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_DEMOBUTTON]))
500 {
501 NewPage = PAGE_DEMOS;
502 }
503 GameClient()->m_Tooltips.DoToolTip(pId: &s_DemoButton, pNearRect: &Button, pText: Localize(pStr: "Demos"));
504 Box.VSplitRight(Cut: 10.0f, pLeft: &Box, pRight: nullptr);
505
506 Box.VSplitLeft(Cut: 33.0f, pLeft: &Button, pRight: &Box);
507
508 bool GotNewsOrUpdate = false;
509
510#if defined(CONF_AUTOUPDATE)
511 int State = Updater()->GetCurrentState();
512 bool NeedUpdate = str_comp(a: Client()->LatestVersion(), b: "0");
513 if(State == IUpdater::CLEAN && NeedUpdate)
514 {
515 GotNewsOrUpdate = true;
516 }
517#endif
518
519 GotNewsOrUpdate |= (bool)g_Config.m_UiUnreadNews;
520
521 ColorRGBA HomeButtonColorAlert(0, 1, 0, 0.25f);
522 ColorRGBA HomeButtonColorAlertHover(0, 1, 0, 0.5f);
523 ColorRGBA *pHomeButtonColor = nullptr;
524 ColorRGBA *pHomeButtonColorHover = nullptr;
525
526 const char *pHomeScreenButtonLabel = FontIcon::HOUSE;
527 if(GotNewsOrUpdate)
528 {
529 pHomeScreenButtonLabel = FontIcon::NEWSPAPER;
530 pHomeButtonColor = &HomeButtonColorAlert;
531 pHomeButtonColorHover = &HomeButtonColorAlertHover;
532 }
533
534 static CButtonContainer s_StartButton;
535 if(DoButton_MenuTab(pButtonContainer: &s_StartButton, pText: pHomeScreenButtonLabel, Checked: false, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_HOME], pDefaultColor: pHomeButtonColor, pActiveColor: pHomeButtonColor, pHoverColor: pHomeButtonColorHover, EdgeRounding: 10.0f))
536 {
537 m_ShowStart = true;
538 }
539 GameClient()->m_Tooltips.DoToolTip(pId: &s_StartButton, pNearRect: &Button, pText: Localize(pStr: "Main menu"));
540
541 const float BrowserButtonWidth = 75.0f;
542 Box.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Box);
543 Box.VSplitLeft(Cut: BrowserButtonWidth, pLeft: &Button, pRight: &Box);
544 static CButtonContainer s_InternetButton;
545 if(DoButton_MenuTab(pButtonContainer: &s_InternetButton, pText: FontIcon::EARTH_AMERICAS, Checked: ActivePage == PAGE_INTERNET, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsBigPage[BIG_TAB_INTERNET]))
546 {
547 NewPage = PAGE_INTERNET;
548 }
549 GameClient()->m_Tooltips.DoToolTip(pId: &s_InternetButton, pNearRect: &Button, pText: Localize(pStr: "Internet"));
550
551 Box.VSplitLeft(Cut: BrowserButtonWidth, pLeft: &Button, pRight: &Box);
552 static CButtonContainer s_LanButton;
553 if(DoButton_MenuTab(pButtonContainer: &s_LanButton, pText: FontIcon::NETWORK_WIRED, Checked: ActivePage == PAGE_LAN, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsBigPage[BIG_TAB_LAN]))
554 {
555 NewPage = PAGE_LAN;
556 }
557 GameClient()->m_Tooltips.DoToolTip(pId: &s_LanButton, pNearRect: &Button, pText: Localize(pStr: "LAN"));
558
559 Box.VSplitLeft(Cut: BrowserButtonWidth, pLeft: &Button, pRight: &Box);
560 static CButtonContainer s_FavoritesButton;
561 if(DoButton_MenuTab(pButtonContainer: &s_FavoritesButton, pText: FontIcon::STAR, Checked: ActivePage == PAGE_FAVORITES, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsBigPage[BIG_TAB_FAVORITES]))
562 {
563 NewPage = PAGE_FAVORITES;
564 }
565 GameClient()->m_Tooltips.DoToolTip(pId: &s_FavoritesButton, pNearRect: &Button, pText: Localize(pStr: "Favorites"));
566
567 const int MaxPage = PAGE_FAVORITES + ServerBrowser()->FavoriteCommunities().size();
568 if(
569 !Ui()->IsPopupOpen() &&
570 CLineInput::GetActiveInput() == nullptr &&
571 (g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= MaxPage) &&
572 (m_MenuPage >= PAGE_INTERNET && m_MenuPage <= PAGE_FAVORITE_COMMUNITY_5))
573 {
574 if(Input()->KeyPress(Key: KEY_RIGHT))
575 {
576 NewPage = g_Config.m_UiPage + 1;
577 if(NewPage > MaxPage)
578 NewPage = PAGE_INTERNET;
579 }
580 if(Input()->KeyPress(Key: KEY_LEFT))
581 {
582 NewPage = g_Config.m_UiPage - 1;
583 if(NewPage < PAGE_INTERNET)
584 NewPage = MaxPage;
585 }
586 }
587
588 size_t FavoriteCommunityIndex = 0;
589 static CButtonContainer s_aFavoriteCommunityButtons[5];
590 static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)PAGE_FAVORITE_COMMUNITY_5 - PAGE_FAVORITE_COMMUNITY_1 + 1);
591 static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)BIT_TAB_FAVORITE_COMMUNITY_5 - BIT_TAB_FAVORITE_COMMUNITY_1 + 1);
592 static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)IServerBrowser::TYPE_FAVORITE_COMMUNITY_5 - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 + 1);
593 for(const CCommunity *pCommunity : ServerBrowser()->FavoriteCommunities())
594 {
595 if(Box.w < BrowserButtonWidth)
596 break;
597 Box.VSplitLeft(Cut: BrowserButtonWidth, pLeft: &Button, pRight: &Box);
598 const int Page = PAGE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex;
599 if(DoButton_MenuTab(pButtonContainer: &s_aFavoriteCommunityButtons[FavoriteCommunityIndex], pText: FontIcon::ELLIPSIS, Checked: ActivePage == Page, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsBigPage[BIT_TAB_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex], pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 10.0f, pCommunityIcon: m_CommunityIcons.Find(pCommunityId: pCommunity->Id())))
600 {
601 NewPage = Page;
602 }
603 GameClient()->m_Tooltips.DoToolTip(pId: &s_aFavoriteCommunityButtons[FavoriteCommunityIndex], pNearRect: &Button, pText: pCommunity->Name());
604
605 ++FavoriteCommunityIndex;
606 if(FavoriteCommunityIndex >= std::size(s_aFavoriteCommunityButtons))
607 break;
608 }
609
610 TextRender()->SetRenderFlags(0);
611 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
612 }
613 else
614 {
615 TextRender()->SetRenderFlags(0);
616 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
617
618 // online menus
619 Box.VSplitLeft(Cut: 90.0f, pLeft: &Button, pRight: &Box);
620 static CButtonContainer s_GameButton;
621 if(DoButton_MenuTab(pButtonContainer: &s_GameButton, pText: Localize(pStr: "Game"), Checked: ActivePage == PAGE_GAME, pRect: &Button, Corners: IGraphics::CORNER_TL))
622 NewPage = PAGE_GAME;
623
624 Box.VSplitLeft(Cut: 90.0f, pLeft: &Button, pRight: &Box);
625 static CButtonContainer s_PlayersButton;
626 if(DoButton_MenuTab(pButtonContainer: &s_PlayersButton, pText: Localize(pStr: "Players"), Checked: ActivePage == PAGE_PLAYERS, pRect: &Button, Corners: IGraphics::CORNER_NONE))
627 NewPage = PAGE_PLAYERS;
628
629 Box.VSplitLeft(Cut: 130.0f, pLeft: &Button, pRight: &Box);
630 static CButtonContainer s_ServerInfoButton;
631 if(DoButton_MenuTab(pButtonContainer: &s_ServerInfoButton, pText: Localize(pStr: "Server info"), Checked: ActivePage == PAGE_SERVER_INFO, pRect: &Button, Corners: IGraphics::CORNER_NONE))
632 NewPage = PAGE_SERVER_INFO;
633
634 Box.VSplitLeft(Cut: 90.0f, pLeft: &Button, pRight: &Box);
635 static CButtonContainer s_NetworkButton;
636 if(DoButton_MenuTab(pButtonContainer: &s_NetworkButton, pText: Localize(pStr: "Browser"), Checked: ActivePage == PAGE_NETWORK, pRect: &Button, Corners: IGraphics::CORNER_NONE))
637 NewPage = PAGE_NETWORK;
638
639 if(GameClient()->m_GameInfo.m_Race)
640 {
641 Box.VSplitLeft(Cut: 90.0f, pLeft: &Button, pRight: &Box);
642 static CButtonContainer s_GhostButton;
643 if(DoButton_MenuTab(pButtonContainer: &s_GhostButton, pText: Localize(pStr: "Ghost"), Checked: ActivePage == PAGE_GHOST, pRect: &Button, Corners: IGraphics::CORNER_NONE))
644 NewPage = PAGE_GHOST;
645 }
646
647 Box.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: &Box);
648 Box.VSplitLeft(Cut: 4.0f, pLeft: nullptr, pRight: &Box);
649 static CButtonContainer s_CallVoteButton;
650 if(DoButton_MenuTab(pButtonContainer: &s_CallVoteButton, pText: Localize(pStr: "Call vote"), Checked: ActivePage == PAGE_CALLVOTE, pRect: &Button, Corners: IGraphics::CORNER_TR))
651 {
652 NewPage = PAGE_CALLVOTE;
653 m_ControlPageOpening = true;
654 }
655
656 if(Box.w >= 10.0f + 33.0f + 10.0f)
657 {
658 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
659 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_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
660
661 Box.VSplitRight(Cut: 10.0f, pLeft: &Box, pRight: nullptr);
662 Box.VSplitRight(Cut: 33.0f, pLeft: &Box, pRight: &Button);
663 static CButtonContainer s_DemoButton;
664 if(DoButton_MenuTab(pButtonContainer: &s_DemoButton, pText: FontIcon::CLAPPERBOARD, Checked: ActivePage == PAGE_DEMOS, pRect: &Button, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_DEMOBUTTON]))
665 {
666 NewPage = PAGE_DEMOS;
667 }
668 GameClient()->m_Tooltips.DoToolTip(pId: &s_DemoButton, pNearRect: &Button, pText: Localize(pStr: "Demos"));
669 Box.VSplitRight(Cut: 10.0f, pLeft: &Box, pRight: nullptr);
670
671 TextRender()->SetRenderFlags(0);
672 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
673 }
674 }
675
676 if(NewPage != -1)
677 {
678 if(ClientState == IClient::STATE_OFFLINE)
679 SetMenuPage(NewPage);
680 else
681 m_GamePage = NewPage;
682 }
683}
684
685void CMenus::RenderLoading(const char *pCaption, const char *pContent, int IncreaseCounter)
686{
687 // TODO: not supported right now due to separate render thread
688
689 const int CurLoadRenderCount = m_LoadingState.m_Current;
690 m_LoadingState.m_Current += IncreaseCounter;
691 dbg_assert(m_LoadingState.m_Current <= m_LoadingState.m_Total, "Invalid progress for RenderLoading");
692
693 // make sure that we don't render for each little thing we load
694 // because that will slow down loading if we have vsync
695 const std::chrono::nanoseconds Now = time_get_nanoseconds();
696 if(Now - m_LoadingState.m_LastRender < std::chrono::nanoseconds(1s) / 60l)
697 return;
698
699 // need up date this here to get correct
700 ms_GuiColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_UiColor, true));
701
702 Ui()->MapScreen();
703
704 if(GameClient()->m_MenuBackground.IsLoading())
705 {
706 // Avoid rendering while loading the menu background as this would otherwise
707 // cause the regular menu background to be rendered for a few frames while
708 // the menu background is not loaded yet.
709 return;
710 }
711 if(!GameClient()->m_MenuBackground.Render())
712 {
713 RenderBackground();
714 }
715
716 m_LoadingState.m_LastRender = Now;
717
718 CUIRect Box;
719 Ui()->Screen()->Margin(Cut: 160.0f, pOtherRect: &Box);
720
721 Graphics()->TextureClear();
722 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
723 Box.Margin(Cut: 20.0f, pOtherRect: &Box);
724
725 CUIRect Label;
726 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
727 Ui()->DoLabel(pRect: &Label, pText: pCaption, Size: 24.0f, Align: TEXTALIGN_MC);
728
729 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
730 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
731 Ui()->DoLabel(pRect: &Label, pText: pContent, Size: 20.0f, Align: TEXTALIGN_MC);
732
733 if(m_LoadingState.m_Total > 0)
734 {
735 CUIRect ProgressBar;
736 Box.HSplitBottom(Cut: 30.0f, pTop: &Box, pBottom: nullptr);
737 Box.HSplitBottom(Cut: 25.0f, pTop: &Box, pBottom: &ProgressBar);
738 ProgressBar.VMargin(Cut: 20.0f, pOtherRect: &ProgressBar);
739 Ui()->RenderProgressBar(ProgressBar, Progress: CurLoadRenderCount / (float)m_LoadingState.m_Total);
740 }
741
742 Graphics()->SetColor(r: 1.0, g: 1.0, b: 1.0, a: 1.0);
743
744 Client()->UpdateAndSwap();
745}
746
747void CMenus::FinishLoading()
748{
749 m_LoadingState.m_Current = 0;
750 m_LoadingState.m_Total = 0;
751}
752
753void CMenus::RenderNews(CUIRect MainView)
754{
755 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_NEWS);
756
757 g_Config.m_UiUnreadNews = false;
758
759 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
760
761 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
762 MainView.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &MainView);
763
764 CUIRect Label;
765
766 const char *pStr = Client()->News();
767 char aLine[256];
768 while((pStr = str_next_token(str: pStr, delim: "\n", buffer: aLine, buffer_size: sizeof(aLine))))
769 {
770 const int Len = str_length(str: aLine);
771 if(Len > 0 && aLine[0] == '|' && aLine[Len - 1] == '|')
772 {
773 MainView.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &MainView);
774 aLine[Len - 1] = '\0';
775 Ui()->DoLabel(pRect: &Label, pText: aLine + 1, Size: 20.0f, Align: TEXTALIGN_ML);
776 }
777 else
778 {
779 MainView.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &MainView);
780 Ui()->DoLabel(pRect: &Label, pText: aLine, Size: 15.f, Align: TEXTALIGN_ML);
781 }
782 }
783}
784
785void CMenus::OnInterfacesInit(CGameClient *pClient)
786{
787 CComponentInterfaces::OnInterfacesInit(pClient);
788 m_MenusIngameTouchControls.OnInterfacesInit(pClient);
789 m_MenusSettingsControls.OnInterfacesInit(pClient);
790 m_MenusStart.OnInterfacesInit(pClient);
791 m_CommunityIcons.OnInterfacesInit(pClient);
792}
793
794void CMenus::OnInit()
795{
796 if(g_Config.m_ClShowWelcome)
797 {
798 m_Popup = POPUP_LANGUAGE;
799 m_CreateDefaultFavoriteCommunities = true;
800 }
801
802 if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 &&
803 (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size())
804 {
805 // Reset page to internet when there is no favorite community for this page.
806 g_Config.m_UiPage = PAGE_INTERNET;
807 }
808
809 if(g_Config.m_ClSkipStartMenu)
810 {
811 m_ShowStart = false;
812 }
813 m_MenuPage = g_Config.m_UiPage;
814
815 m_RefreshButton.Init(pUI: Ui(), RequestedRectCount: -1);
816 m_ConnectButton.Init(pUI: Ui(), RequestedRectCount: -1);
817
818 Console()->Chain(pName: "add_favorite", pfnChainFunc: ConchainFavoritesUpdate, pUser: this);
819 Console()->Chain(pName: "remove_favorite", pfnChainFunc: ConchainFavoritesUpdate, pUser: this);
820 Console()->Chain(pName: "add_friend", pfnChainFunc: ConchainFriendlistUpdate, pUser: this);
821 Console()->Chain(pName: "remove_friend", pfnChainFunc: ConchainFriendlistUpdate, pUser: this);
822
823 Console()->Chain(pName: "add_excluded_community", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
824 Console()->Chain(pName: "remove_excluded_community", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
825 Console()->Chain(pName: "add_excluded_country", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
826 Console()->Chain(pName: "remove_excluded_country", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
827 Console()->Chain(pName: "add_excluded_type", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
828 Console()->Chain(pName: "remove_excluded_type", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
829
830 Console()->Chain(pName: "ui_page", pfnChainFunc: ConchainUiPageUpdate, pUser: this);
831
832 Console()->Chain(pName: "snd_enable", pfnChainFunc: ConchainUpdateMusicState, pUser: this);
833 Console()->Chain(pName: "snd_enable_music", pfnChainFunc: ConchainUpdateMusicState, pUser: this);
834 Console()->Chain(pName: "cl_background_entities", pfnChainFunc: ConchainBackgroundEntities, pUser: this);
835
836 Console()->Chain(pName: "cl_assets_entities", pfnChainFunc: ConchainAssetsEntities, pUser: this);
837 Console()->Chain(pName: "cl_asset_game", pfnChainFunc: ConchainAssetGame, pUser: this);
838 Console()->Chain(pName: "cl_asset_emoticons", pfnChainFunc: ConchainAssetEmoticons, pUser: this);
839 Console()->Chain(pName: "cl_asset_particles", pfnChainFunc: ConchainAssetParticles, pUser: this);
840 Console()->Chain(pName: "cl_asset_hud", pfnChainFunc: ConchainAssetHud, pUser: this);
841 Console()->Chain(pName: "cl_asset_extras", pfnChainFunc: ConchainAssetExtras, pUser: this);
842
843 Console()->Chain(pName: "demo_play", pfnChainFunc: ConchainDemoPlay, pUser: this);
844 Console()->Chain(pName: "demo_speed", pfnChainFunc: ConchainDemoSpeed, pUser: this);
845
846 m_TextureBlob = Graphics()->LoadTexture(pFilename: "blob.png", StorageType: IStorage::TYPE_ALL);
847
848 // setup load amount
849 m_LoadingState.m_Current = 0;
850 m_LoadingState.m_Total = g_pData->m_NumImages + GameClient()->ComponentCount();
851 if(!g_Config.m_ClThreadsoundloading)
852 m_LoadingState.m_Total += g_pData->m_NumSounds;
853
854 m_IsInit = true;
855
856 // load menu images
857 m_vMenuImages.clear();
858 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "menuimages", pfnCallback: MenuImageScan, pUser: this);
859
860 m_CommunityIcons.Load();
861
862 // Quad for the direction arrows above the player
863 m_DirectionQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
864 Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_DirectionQuadContainerIndex, x: 0.f, y: 0.f, Size: 22.f);
865 Graphics()->QuadContainerUpload(ContainerIndex: m_DirectionQuadContainerIndex);
866}
867
868void CMenus::ConchainBackgroundEntities(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
869{
870 pfnCallback(pResult, pCallbackUserData);
871 if(pResult->NumArguments())
872 {
873 CMenus *pSelf = (CMenus *)pUserData;
874 if(str_comp(a: g_Config.m_ClBackgroundEntities, b: pSelf->GameClient()->m_Background.MapName()) != 0)
875 pSelf->GameClient()->m_Background.LoadBackground();
876 }
877}
878
879void CMenus::ConchainUpdateMusicState(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
880{
881 pfnCallback(pResult, pCallbackUserData);
882 auto *pSelf = (CMenus *)pUserData;
883 if(pResult->NumArguments())
884 pSelf->UpdateMusicState();
885}
886
887void CMenus::UpdateMusicState()
888{
889 const bool ShouldPlay = Client()->State() == IClient::STATE_OFFLINE && g_Config.m_SndEnable && g_Config.m_SndMusic;
890 if(ShouldPlay && !GameClient()->m_Sounds.IsPlaying(SetId: SOUND_MENU))
891 GameClient()->m_Sounds.Enqueue(Channel: CSounds::CHN_MUSIC, SetId: SOUND_MENU);
892 else if(!ShouldPlay && GameClient()->m_Sounds.IsPlaying(SetId: SOUND_MENU))
893 GameClient()->m_Sounds.Stop(SetId: SOUND_MENU);
894}
895
896void CMenus::PopupMessage(const char *pTitle, const char *pMessage, const char *pButtonLabel, int NextPopup, FPopupButtonCallback pfnButtonCallback)
897{
898 // reset active item
899 Ui()->SetActiveItem(nullptr);
900
901 str_copy(dst&: m_aPopupTitle, src: pTitle);
902 str_copy(dst&: m_aPopupMessage, src: pMessage);
903 str_copy(dst&: m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, src: pButtonLabel);
904 m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup = NextPopup;
905 m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback = pfnButtonCallback;
906 m_Popup = POPUP_MESSAGE;
907}
908
909void CMenus::PopupConfirm(const char *pTitle, const char *pMessage, const char *pConfirmButtonLabel, const char *pCancelButtonLabel,
910 FPopupButtonCallback pfnConfirmButtonCallback, int ConfirmNextPopup, FPopupButtonCallback pfnCancelButtonCallback, int CancelNextPopup)
911{
912 // reset active item
913 Ui()->SetActiveItem(nullptr);
914
915 str_copy(dst&: m_aPopupTitle, src: pTitle);
916 str_copy(dst&: m_aPopupMessage, src: pMessage);
917 str_copy(dst&: m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, src: pConfirmButtonLabel);
918 m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup = ConfirmNextPopup;
919 m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback = pfnConfirmButtonCallback;
920 str_copy(dst&: m_aPopupButtons[BUTTON_CANCEL].m_aLabel, src: pCancelButtonLabel);
921 m_aPopupButtons[BUTTON_CANCEL].m_NextPopup = CancelNextPopup;
922 m_aPopupButtons[BUTTON_CANCEL].m_pfnCallback = pfnCancelButtonCallback;
923 m_Popup = POPUP_CONFIRM;
924}
925
926void CMenus::PopupWarning(const char *pTopic, const char *pBody, const char *pButton, std::chrono::nanoseconds Duration)
927{
928 // no multiline support for console
929 std::string BodyStr = pBody;
930 std::replace(first: BodyStr.begin(), last: BodyStr.end(), old_value: '\n', new_value: ' ');
931 log_warn("client", "%s: %s", pTopic, BodyStr.c_str());
932
933 Ui()->SetActiveItem(nullptr);
934
935 str_copy(dst&: m_aMessageTopic, src: pTopic);
936 str_copy(dst&: m_aMessageBody, src: pBody);
937 str_copy(dst&: m_aMessageButton, src: pButton);
938 m_Popup = POPUP_WARNING;
939 SetActive(true);
940
941 m_PopupWarningDuration = Duration;
942 m_PopupWarningLastTime = time_get_nanoseconds();
943}
944
945bool CMenus::CanDisplayWarning() const
946{
947 return m_Popup == POPUP_NONE;
948}
949
950void CMenus::Render()
951{
952 Ui()->MapScreen();
953 Ui()->SetMouseSlow(false);
954
955 static int s_Frame = 0;
956 if(s_Frame == 0)
957 {
958 RefreshBrowserTab(Force: true);
959 s_Frame++;
960 }
961 else if(s_Frame == 1)
962 {
963 UpdateMusicState();
964 s_Frame++;
965 }
966 else
967 {
968 m_CommunityIcons.Update();
969 }
970
971 // Initially add DDNet as favorite community and select its tab.
972 // This must be delayed until the DDNet info is available.
973 if(m_CreateDefaultFavoriteCommunities &&
974 ServerBrowser()->DDNetInfoAvailable())
975 {
976 m_CreateDefaultFavoriteCommunities = false;
977 if(ServerBrowser()->Community(pCommunityId: IServerBrowser::COMMUNITY_DDNET) != nullptr)
978 {
979 ServerBrowser()->FavoriteCommunitiesFilter().Clear();
980 ServerBrowser()->FavoriteCommunitiesFilter().Add(pElement: IServerBrowser::COMMUNITY_DDNET);
981 SetMenuPage(PAGE_FAVORITE_COMMUNITY_1);
982 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_FAVORITE_COMMUNITY_1);
983 }
984 }
985 if(m_JoinTutorial.m_Queued && m_Popup == POPUP_NONE)
986 {
987 const char *pAddr = ServerBrowser()->GetTutorialServer();
988 if(pAddr)
989 {
990 Client()->Connect(pAddress: pAddr);
991 }
992 else
993 {
994 m_Popup = POPUP_JOIN_TUTORIAL;
995 }
996 m_JoinTutorial.m_Queued = false;
997 }
998
999 // Determine the client state once before rendering because it can change
1000 // while rendering which causes frames with broken user interface.
1001 const IClient::EClientState ClientState = Client()->State();
1002
1003 if(ClientState == IClient::STATE_ONLINE || ClientState == IClient::STATE_DEMOPLAYBACK)
1004 {
1005 ms_ColorTabbarInactive = ms_ColorTabbarInactiveIngame;
1006 ms_ColorTabbarActive = ms_ColorTabbarActiveIngame;
1007 ms_ColorTabbarHover = ms_ColorTabbarHoverIngame;
1008 }
1009 else
1010 {
1011 if(!GameClient()->m_MenuBackground.Render())
1012 {
1013 RenderBackground();
1014 }
1015 ms_ColorTabbarInactive = ms_ColorTabbarInactiveOutgame;
1016 ms_ColorTabbarActive = ms_ColorTabbarActiveOutgame;
1017 ms_ColorTabbarHover = ms_ColorTabbarHoverOutgame;
1018 }
1019
1020 CUIRect Screen = *Ui()->Screen();
1021 if(Client()->State() != IClient::STATE_DEMOPLAYBACK || m_Popup != POPUP_NONE)
1022 {
1023 Screen.Margin(Cut: 10.0f, pOtherRect: &Screen);
1024 }
1025
1026 switch(ClientState)
1027 {
1028 case IClient::STATE_QUITTING:
1029 case IClient::STATE_RESTARTING:
1030 // Render nothing except menu background. This should not happen for more than one frame.
1031 return;
1032
1033 case IClient::STATE_CONNECTING:
1034 RenderPopupConnecting(Screen);
1035 break;
1036
1037 case IClient::STATE_LOADING:
1038 RenderPopupLoading(Screen);
1039 break;
1040
1041 case IClient::STATE_OFFLINE:
1042 if(m_Popup != POPUP_NONE)
1043 {
1044 RenderPopupFullscreen(Screen);
1045 }
1046 else if(m_ShowStart)
1047 {
1048 m_MenusStart.RenderStartMenu(MainView: Screen);
1049 }
1050 else
1051 {
1052 CUIRect TabBar, MainView;
1053 Screen.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &MainView);
1054
1055 if(m_MenuPage == PAGE_NEWS)
1056 {
1057 RenderNews(MainView);
1058 }
1059 else if(m_MenuPage >= PAGE_INTERNET && m_MenuPage <= PAGE_FAVORITE_COMMUNITY_5)
1060 {
1061 RenderServerbrowser(MainView);
1062 }
1063 else if(m_MenuPage == PAGE_DEMOS)
1064 {
1065 RenderDemoBrowser(MainView);
1066 }
1067 else if(m_MenuPage == PAGE_SETTINGS)
1068 {
1069 RenderSettings(MainView);
1070 }
1071 else
1072 {
1073 dbg_assert_failed("Invalid m_MenuPage: %d", m_MenuPage);
1074 }
1075
1076 RenderMenubar(Box: TabBar, ClientState);
1077 }
1078 break;
1079
1080 case IClient::STATE_ONLINE:
1081 if(m_Popup != POPUP_NONE)
1082 {
1083 RenderPopupFullscreen(Screen);
1084 }
1085 else
1086 {
1087 CUIRect TabBar, MainView;
1088 Screen.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &MainView);
1089
1090 if(m_GamePage == PAGE_GAME)
1091 {
1092 RenderGame(MainView);
1093 RenderIngameHint();
1094 }
1095 else if(m_GamePage == PAGE_PLAYERS)
1096 {
1097 RenderPlayers(MainView);
1098 }
1099 else if(m_GamePage == PAGE_SERVER_INFO)
1100 {
1101 RenderServerInfo(MainView);
1102 }
1103 else if(m_GamePage == PAGE_NETWORK)
1104 {
1105 RenderInGameNetwork(MainView);
1106 }
1107 else if(m_GamePage == PAGE_GHOST)
1108 {
1109 RenderGhost(MainView);
1110 }
1111 else if(m_GamePage == PAGE_CALLVOTE)
1112 {
1113 RenderServerControl(MainView);
1114 }
1115 else if(m_GamePage == PAGE_DEMOS)
1116 {
1117 RenderDemoBrowser(MainView);
1118 }
1119 else if(m_GamePage == PAGE_SETTINGS)
1120 {
1121 RenderSettings(MainView);
1122 }
1123 else
1124 {
1125 dbg_assert_failed("Invalid m_GamePage: %d", m_GamePage);
1126 }
1127
1128 RenderMenubar(Box: TabBar, ClientState);
1129 }
1130 break;
1131
1132 case IClient::STATE_DEMOPLAYBACK:
1133 if(m_Popup != POPUP_NONE)
1134 {
1135 RenderPopupFullscreen(Screen);
1136 }
1137 else
1138 {
1139 RenderDemoPlayer(MainView: Screen);
1140 }
1141 break;
1142 }
1143
1144 Ui()->RenderPopupMenus();
1145
1146 // Prevent UI elements from being hovered while a key reader is active
1147 if(GameClient()->m_KeyBinder.IsActive())
1148 {
1149 Ui()->SetHotItem(nullptr);
1150 }
1151
1152 // Handle this escape hotkey after popup menus
1153 if(!m_ShowStart && ClientState == IClient::STATE_OFFLINE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1154 {
1155 m_ShowStart = true;
1156 }
1157}
1158
1159void CMenus::RenderPopupFullscreen(CUIRect Screen)
1160{
1161 char aBuf[1536];
1162 const char *pTitle = "";
1163 const char *pExtraText = "";
1164 const char *pButtonText = "";
1165 bool TopAlign = false;
1166
1167 ColorRGBA BgColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f);
1168 if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM)
1169 {
1170 pTitle = m_aPopupTitle;
1171 pExtraText = m_aPopupMessage;
1172 TopAlign = true;
1173 }
1174 else if(m_Popup == POPUP_DISCONNECTED)
1175 {
1176 pTitle = Localize(pStr: "Disconnected");
1177 pExtraText = Client()->ErrorString();
1178 pButtonText = Localize(pStr: "Ok");
1179 if(Client()->ReconnectTime() > 0)
1180 {
1181 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Reconnect in %d sec"), (int)((Client()->ReconnectTime() - time_get()) / time_freq()) + 1);
1182 pTitle = Client()->ErrorString();
1183 pExtraText = aBuf;
1184 pButtonText = Localize(pStr: "Abort");
1185 }
1186 }
1187 else if(m_Popup == POPUP_RENAME_DEMO)
1188 {
1189 dbg_assert(m_DemolistSelectedIndex >= 0, "m_DemolistSelectedIndex invalid for POPUP_RENAME_DEMO");
1190 pTitle = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Rename folder") : Localize(pStr: "Rename demo");
1191 }
1192#if defined(CONF_VIDEORECORDER)
1193 else if(m_Popup == POPUP_RENDER_DEMO)
1194 {
1195 pTitle = Localize(pStr: "Render demo");
1196 }
1197 else if(m_Popup == POPUP_RENDER_DONE)
1198 {
1199 pTitle = Localize(pStr: "Render complete");
1200 }
1201#endif
1202 else if(m_Popup == POPUP_PASSWORD)
1203 {
1204 pTitle = Localize(pStr: "Password incorrect");
1205 pButtonText = Localize(pStr: "Try again");
1206 }
1207 else if(m_Popup == POPUP_RESTART)
1208 {
1209 pTitle = Localize(pStr: "Restart");
1210 pExtraText = Localize(pStr: "Are you sure that you want to restart?");
1211 }
1212 else if(m_Popup == POPUP_QUIT)
1213 {
1214 pTitle = Localize(pStr: "Quit");
1215 pExtraText = Localize(pStr: "Are you sure that you want to quit?");
1216 }
1217 else if(m_Popup == POPUP_FIRST_LAUNCH)
1218 {
1219 pTitle = Localize(pStr: "Welcome to DDNet");
1220 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s\n\n%s\n\n%s\n\n%s",
1221 Localize(pStr: "DDraceNetwork is a cooperative online game where the goal is for you and your group of tees to reach the finish line of the map. As a newcomer you should start on Novice servers, which host the easiest maps. Consider the ping to choose a server close to you."),
1222 Localize(pStr: "Use k key to kill (restart), q to pause and watch other players. See settings for other key binds."),
1223 Localize(pStr: "It's recommended that you check the settings to adjust them to your liking before joining a server."),
1224 Localize(pStr: "Please enter your nickname below."));
1225 pExtraText = aBuf;
1226 pButtonText = Localize(pStr: "Ok");
1227 TopAlign = true;
1228 }
1229 else if(m_Popup == POPUP_JOIN_TUTORIAL)
1230 {
1231 pTitle = Localize(pStr: "Joining Tutorial server");
1232 }
1233 else if(m_Popup == POPUP_POINTS)
1234 {
1235 pTitle = Localize(pStr: "Existing Player");
1236 if(Client()->InfoState() == IClient::EInfoState::SUCCESS && Client()->Points() > 50)
1237 {
1238 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Your nickname '%s' is already used (%d points). Do you still want to use it?"), Client()->PlayerName(), Client()->Points());
1239 pExtraText = aBuf;
1240 TopAlign = true;
1241 }
1242 else
1243 {
1244 pExtraText = Localize(pStr: "Checking for existing player with your name");
1245 }
1246 }
1247 else if(m_Popup == POPUP_WARNING)
1248 {
1249 BgColor = ColorRGBA(0.5f, 0.0f, 0.0f, 0.7f);
1250 pTitle = m_aMessageTopic;
1251 pExtraText = m_aMessageBody;
1252 pButtonText = m_aMessageButton;
1253 TopAlign = true;
1254 }
1255 else if(m_Popup == POPUP_SAVE_SKIN)
1256 {
1257 pTitle = Localize(pStr: "Save skin");
1258 pExtraText = Localize(pStr: "Are you sure you want to save your skin? If a skin with this name already exists, it will be replaced.");
1259 }
1260
1261 CUIRect Box, Part;
1262 Box = Screen;
1263 if(m_Popup != POPUP_FIRST_LAUNCH)
1264 {
1265 Box.Margin(Cut: 150.0f, pOtherRect: &Box);
1266 }
1267
1268 // Background
1269 Box.Draw(Color: BgColor, Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
1270
1271 // Title
1272 {
1273 CUIRect Title;
1274 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1275 Box.HSplitTop(Cut: 24.0f, pTop: &Title, pBottom: &Box);
1276 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1277 Title.VMargin(Cut: 20.0f, pOtherRect: &Title);
1278
1279 const float TitleFontSize = 24.0f;
1280 if(TextRender()->TextWidth(Size: TitleFontSize, pText: pTitle) > Title.w)
1281 Ui()->DoLabel(pRect: &Title, pText: pTitle, Size: TitleFontSize, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = Title.w});
1282 else
1283 Ui()->DoLabel(pRect: &Title, pText: pTitle, Size: TitleFontSize, Align: TEXTALIGN_MC);
1284 }
1285
1286 // Extra text (optional)
1287 if(m_Popup != POPUP_JOIN_TUTORIAL)
1288 {
1289 CUIRect ExtraText;
1290 Box.HSplitTop(Cut: 24.0f, pTop: &ExtraText, pBottom: &Box);
1291 ExtraText.VMargin(Cut: 20.0f, pOtherRect: &ExtraText);
1292 if(pExtraText[0] != '\0')
1293 {
1294 const float ExtraTextFontSize = m_Popup == POPUP_FIRST_LAUNCH ? 16.0f : 20.0f;
1295
1296 if(TopAlign)
1297 Ui()->DoLabel(pRect: &ExtraText, pText: pExtraText, Size: ExtraTextFontSize, Align: TEXTALIGN_TL, LabelProps: {.m_MaxWidth = ExtraText.w});
1298 else if(TextRender()->TextWidth(Size: ExtraTextFontSize, pText: pExtraText) > ExtraText.w)
1299 Ui()->DoLabel(pRect: &ExtraText, pText: pExtraText, Size: ExtraTextFontSize, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = ExtraText.w});
1300 else
1301 Ui()->DoLabel(pRect: &ExtraText, pText: pExtraText, Size: ExtraTextFontSize, Align: TEXTALIGN_MC);
1302 }
1303 }
1304
1305 if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM)
1306 {
1307 CUIRect ButtonBar;
1308 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1309 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &ButtonBar);
1310 ButtonBar.VMargin(Cut: 100.0f, pOtherRect: &ButtonBar);
1311
1312 if(m_Popup == POPUP_MESSAGE)
1313 {
1314 static CButtonContainer s_ButtonConfirm;
1315 if(DoButton_Menu(pButtonContainer: &s_ButtonConfirm, pText: m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, Checked: 0, pRect: &ButtonBar) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1316 {
1317 m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup;
1318 (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)();
1319 }
1320 }
1321 else if(m_Popup == POPUP_CONFIRM)
1322 {
1323 CUIRect CancelButton, ConfirmButton;
1324 ButtonBar.VSplitMid(pLeft: &CancelButton, pRight: &ConfirmButton, Spacing: 40.0f);
1325
1326 static CButtonContainer s_ButtonCancel;
1327 if(DoButton_Menu(pButtonContainer: &s_ButtonCancel, pText: m_aPopupButtons[BUTTON_CANCEL].m_aLabel, Checked: 0, pRect: &CancelButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1328 {
1329 m_Popup = m_aPopupButtons[BUTTON_CANCEL].m_NextPopup;
1330 (this->*m_aPopupButtons[BUTTON_CANCEL].m_pfnCallback)();
1331 }
1332
1333 static CButtonContainer s_ButtonConfirm;
1334 if(DoButton_Menu(pButtonContainer: &s_ButtonConfirm, pText: m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, Checked: 0, pRect: &ConfirmButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1335 {
1336 m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup;
1337 (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)();
1338 }
1339 }
1340 }
1341 else if(m_Popup == POPUP_QUIT || m_Popup == POPUP_RESTART)
1342 {
1343 CUIRect Yes, No;
1344 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1345 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1346
1347 // additional info
1348 Box.VMargin(Cut: 20.f, pOtherRect: &Box);
1349 if(GameClient()->Editor()->HasUnsavedData())
1350 {
1351 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s\n\n%s", Localize(pStr: "There's an unsaved map in the editor, you might want to save it."), Localize(pStr: "Continue anyway?"));
1352 Ui()->DoLabel(pRect: &Box, pText: aBuf, Size: 20.0f, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = Part.w - 20.0f});
1353 }
1354 else if(GameClient()->m_TouchControls.HasEditingChanges() || m_MenusIngameTouchControls.UnsavedChanges())
1355 {
1356 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s\n\n%s", Localize(pStr: "There's an unsaved change in the touch controls editor, you might want to save it."), Localize(pStr: "Continue anyway?"));
1357 Ui()->DoLabel(pRect: &Box, pText: aBuf, Size: 20.0f, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = Part.w - 20.0f});
1358 }
1359
1360 // buttons
1361 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1362 Part.VSplitMid(pLeft: &No, pRight: &Yes);
1363 Yes.VMargin(Cut: 20.0f, pOtherRect: &Yes);
1364 No.VMargin(Cut: 20.0f, pOtherRect: &No);
1365
1366 static CButtonContainer s_ButtonAbort;
1367 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "No"), Checked: 0, pRect: &No) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1368 m_Popup = POPUP_NONE;
1369
1370 static CButtonContainer s_ButtonTryAgain;
1371 if(DoButton_Menu(pButtonContainer: &s_ButtonTryAgain, pText: Localize(pStr: "Yes"), Checked: 0, pRect: &Yes) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1372 {
1373 if(m_Popup == POPUP_RESTART)
1374 {
1375 m_Popup = POPUP_NONE;
1376 Client()->Restart();
1377 }
1378 else
1379 {
1380 m_Popup = POPUP_NONE;
1381 Client()->Quit();
1382 }
1383 }
1384 }
1385 else if(m_Popup == POPUP_PASSWORD)
1386 {
1387 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1388 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Part);
1389 Part.VMargin(Cut: 100.0f, pOtherRect: &Part);
1390
1391 CUIRect TryAgain, Abort;
1392 Part.VSplitMid(pLeft: &Abort, pRight: &TryAgain, Spacing: 40.0f);
1393
1394 static CButtonContainer s_ButtonAbort;
1395 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Abort) ||
1396 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1397 {
1398 m_Popup = POPUP_NONE;
1399 }
1400
1401 char aAddr[NETADDR_MAXSTRSIZE];
1402 net_addr_str(addr: &Client()->ServerAddress(), string: aAddr, max_length: sizeof(aAddr), add_port: true);
1403
1404 static CButtonContainer s_ButtonTryAgain;
1405 if(DoButton_Menu(pButtonContainer: &s_ButtonTryAgain, pText: Localize(pStr: "Try again"), Checked: 0, pRect: &TryAgain) ||
1406 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1407 {
1408 Client()->Connect(pAddress: aAddr, pPassword: g_Config.m_Password);
1409 }
1410
1411 Box.VMargin(Cut: 60.0f, pOtherRect: &Box);
1412 Box.HSplitBottom(Cut: 32.0f, pTop: &Box, pBottom: nullptr);
1413 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Part);
1414
1415 CUIRect Label, TextBox;
1416 Part.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &TextBox);
1417 TextBox.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &TextBox);
1418 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Password"), Size: 18.0f, Align: TEXTALIGN_ML);
1419 Ui()->DoClearableEditBox(pLineInput: &m_PasswordInput, pRect: &TextBox, FontSize: 12.0f);
1420
1421 Box.HSplitBottom(Cut: 32.0f, pTop: &Box, pBottom: nullptr);
1422 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Part);
1423
1424 CUIRect Address;
1425 Part.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &Address);
1426 Address.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &Address);
1427 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Address"), Size: 18.0f, Align: TEXTALIGN_ML);
1428 Ui()->DoLabel(pRect: &Address, pText: aAddr, Size: 18.0f, Align: TEXTALIGN_ML);
1429
1430 const CServerBrowser::CServerEntry *pEntry = ServerBrowser()->Find(Addr: Client()->ServerAddress());
1431 if(pEntry != nullptr && pEntry->m_GotInfo)
1432 {
1433 const CCommunity *pCommunity = ServerBrowser()->Community(pCommunityId: pEntry->m_Info.m_aCommunityId);
1434 const CCommunityIcon *pIcon = pCommunity == nullptr ? nullptr : m_CommunityIcons.Find(pCommunityId: pCommunity->Id());
1435
1436 Box.HSplitBottom(Cut: 32.0f, pTop: &Box, pBottom: nullptr);
1437 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Part);
1438
1439 CUIRect Name;
1440 Part.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &Name);
1441 Name.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &Name);
1442 if(pIcon != nullptr)
1443 {
1444 CUIRect Icon;
1445 static char s_CommunityTooltipButtonId;
1446 Name.VSplitLeft(Cut: 2.5f * Name.h, pLeft: &Icon, pRight: &Name);
1447 m_CommunityIcons.Render(pIcon, Rect: Icon, Active: true);
1448 Ui()->DoButtonLogic(pId: &s_CommunityTooltipButtonId, Checked: 0, pRect: &Icon, Flags: BUTTONFLAG_NONE);
1449 GameClient()->m_Tooltips.DoToolTip(pId: &s_CommunityTooltipButtonId, pNearRect: &Icon, pText: pCommunity->Name());
1450 }
1451
1452 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Name"), Size: 18.0f, Align: TEXTALIGN_ML);
1453 Ui()->DoLabel(pRect: &Name, pText: pEntry->m_Info.m_aName, Size: 18.0f, Align: TEXTALIGN_ML);
1454 }
1455 }
1456 else if(m_Popup == POPUP_LANGUAGE)
1457 {
1458 CUIRect Button;
1459 Screen.Margin(Cut: 150.0f, pOtherRect: &Box);
1460 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1461 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1462 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Button);
1463 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1464 Box.VMargin(Cut: 20.0f, pOtherRect: &Box);
1465 const bool Activated = RenderLanguageSelection(MainView: Box);
1466 Button.VMargin(Cut: 120.0f, pOtherRect: &Button);
1467
1468 static CButtonContainer s_Button;
1469 if(DoButton_Menu(pButtonContainer: &s_Button, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &Button) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) || Activated)
1470 m_Popup = POPUP_FIRST_LAUNCH;
1471 }
1472 else if(m_Popup == POPUP_RENAME_DEMO)
1473 {
1474 CUIRect Label, TextBox, Ok, Abort;
1475
1476 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1477 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1478 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1479
1480 Part.VSplitMid(pLeft: &Abort, pRight: &Ok);
1481
1482 Ok.VMargin(Cut: 20.0f, pOtherRect: &Ok);
1483 Abort.VMargin(Cut: 20.0f, pOtherRect: &Abort);
1484
1485 static CButtonContainer s_ButtonAbort;
1486 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Abort) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1487 m_Popup = POPUP_NONE;
1488
1489 static CButtonContainer s_ButtonOk;
1490 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &Ok) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1491 {
1492 m_Popup = POPUP_NONE;
1493 // rename demo
1494 char aBufOld[IO_MAX_PATH_LENGTH];
1495 str_format(buffer: aBufOld, buffer_size: sizeof(aBufOld), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1496 char aBufNew[IO_MAX_PATH_LENGTH];
1497 str_format(buffer: aBufNew, buffer_size: sizeof(aBufNew), format: "%s/%s", m_aCurrentDemoFolder, m_DemoRenameInput.GetString());
1498 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir && !str_endswith(str: aBufNew, suffix: ".demo"))
1499 str_append(dst&: aBufNew, src: ".demo");
1500
1501 if(str_comp(a: aBufOld, b: aBufNew) == 0)
1502 {
1503 // Nothing to rename, also same capitalization
1504 }
1505 else if(!str_valid_filename(str: m_DemoRenameInput.GetString()))
1506 {
1507 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "This name cannot be used for files and folders"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENAME_DEMO);
1508 }
1509 else if(str_utf8_comp_nocase(a: aBufOld, b: aBufNew) != 0 && // Allow renaming if it only changes capitalization to support case-insensitive filesystems
1510 Storage()->FileExists(pFilename: aBufNew, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1511 {
1512 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "A demo with this name already exists"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENAME_DEMO);
1513 }
1514 else if(Storage()->FolderExists(pFilename: aBufNew, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1515 {
1516 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "A folder with this name already exists"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENAME_DEMO);
1517 }
1518 else if(Storage()->RenameFile(pOldFilename: aBufOld, pNewFilename: aBufNew, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1519 {
1520 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_DemoRenameInput.GetString());
1521 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1522 fs_split_file_extension(filename: m_DemoRenameInput.GetString(), name: m_aCurrentDemoSelectionName, name_size: sizeof(m_aCurrentDemoSelectionName));
1523 DemolistPopulate();
1524 DemolistOnUpdate(Reset: false);
1525 }
1526 else
1527 {
1528 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Unable to rename the folder") : Localize(pStr: "Unable to rename the demo"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENAME_DEMO);
1529 }
1530 }
1531
1532 Box.HSplitBottom(Cut: 60.f, pTop: &Box, pBottom: &Part);
1533 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1534
1535 Part.VSplitLeft(Cut: 60.0f, pLeft: nullptr, pRight: &Label);
1536 Label.VSplitLeft(Cut: 120.0f, pLeft: nullptr, pRight: &TextBox);
1537 TextBox.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &TextBox);
1538 TextBox.VSplitRight(Cut: 60.0f, pLeft: &TextBox, pRight: nullptr);
1539 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "New name:"), Size: 18.0f, Align: TEXTALIGN_ML);
1540 Ui()->DoEditBox(pLineInput: &m_DemoRenameInput, pRect: &TextBox, FontSize: 12.0f);
1541 }
1542#if defined(CONF_VIDEORECORDER)
1543 else if(m_Popup == POPUP_RENDER_DEMO)
1544 {
1545 CUIRect Row, Ok, Abort;
1546 Box.VMargin(Cut: 60.0f, pOtherRect: &Box);
1547 Box.HMargin(Cut: 20.0f, pOtherRect: &Box);
1548 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Row);
1549 Box.HSplitBottom(Cut: 40.0f, pTop: &Box, pBottom: nullptr);
1550 Row.VMargin(Cut: 40.0f, pOtherRect: &Row);
1551 Row.VSplitMid(pLeft: &Abort, pRight: &Ok, Spacing: 40.0f);
1552
1553 static CButtonContainer s_ButtonAbort;
1554 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Abort) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1555 {
1556 m_DemoRenderInput.Clear();
1557 m_Popup = POPUP_NONE;
1558 }
1559
1560 static CButtonContainer s_ButtonOk;
1561 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &Ok) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1562 {
1563 m_Popup = POPUP_NONE;
1564 // render video
1565 char aVideoPath[IO_MAX_PATH_LENGTH];
1566 str_format(buffer: aVideoPath, buffer_size: sizeof(aVideoPath), format: "videos/%s", m_DemoRenderInput.GetString());
1567 if(!str_endswith(str: aVideoPath, suffix: ".mp4"))
1568 str_append(dst&: aVideoPath, src: ".mp4");
1569
1570 if(!str_valid_filename(str: m_DemoRenderInput.GetString()))
1571 {
1572 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "This name cannot be used for files and folders"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENDER_DEMO);
1573 }
1574 else if(Storage()->FolderExists(pFilename: aVideoPath, Type: IStorage::TYPE_SAVE))
1575 {
1576 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "A folder with this name already exists"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENDER_DEMO);
1577 }
1578 else if(Storage()->FileExists(pFilename: aVideoPath, Type: IStorage::TYPE_SAVE))
1579 {
1580 char aMessage[128 + IO_MAX_PATH_LENGTH];
1581 str_format(buffer: aMessage, buffer_size: sizeof(aMessage), format: Localize(pStr: "File '%s' already exists, do you want to overwrite it?"), m_DemoRenderInput.GetString());
1582 PopupConfirm(pTitle: Localize(pStr: "Replace video"), pMessage: aMessage, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmDemoReplaceVideo, ConfirmNextPopup: POPUP_NONE, pfnCancelButtonCallback: &CMenus::DefaultButtonCallback, CancelNextPopup: POPUP_RENDER_DEMO);
1583 }
1584 else
1585 {
1586 PopupConfirmDemoReplaceVideo();
1587 }
1588 }
1589
1590 CUIRect ShowChatCheckbox, UseSoundsCheckbox;
1591 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: &Row);
1592 Box.HSplitBottom(Cut: 10.0f, pTop: &Box, pBottom: nullptr);
1593 Row.VSplitMid(pLeft: &ShowChatCheckbox, pRight: &UseSoundsCheckbox, Spacing: 20.0f);
1594
1595 if(DoButton_CheckBox(pId: &g_Config.m_ClVideoShowChat, pText: Localize(pStr: "Show chat"), Checked: g_Config.m_ClVideoShowChat, pRect: &ShowChatCheckbox))
1596 g_Config.m_ClVideoShowChat ^= 1;
1597
1598 if(DoButton_CheckBox(pId: &g_Config.m_ClVideoSndEnable, pText: Localize(pStr: "Use sounds"), Checked: g_Config.m_ClVideoSndEnable, pRect: &UseSoundsCheckbox))
1599 g_Config.m_ClVideoSndEnable ^= 1;
1600
1601 CUIRect ShowHudButton;
1602 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: &Row);
1603 Row.VSplitMid(pLeft: &Row, pRight: &ShowHudButton, Spacing: 20.0f);
1604
1605 if(DoButton_CheckBox(pId: &g_Config.m_ClVideoShowhud, pText: Localize(pStr: "Show ingame HUD"), Checked: g_Config.m_ClVideoShowhud, pRect: &ShowHudButton))
1606 g_Config.m_ClVideoShowhud ^= 1;
1607
1608 // slowdown
1609 CUIRect SlowDownButton;
1610 Row.VSplitLeft(Cut: 20.0f, pLeft: &SlowDownButton, pRight: &Row);
1611 Row.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Row);
1612 static CButtonContainer s_SlowDownButton;
1613 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_SlowDownButton, pText: FontIcon::BACKWARD, Checked: 0, pRect: &SlowDownButton, Flags: BUTTONFLAG_LEFT))
1614 m_Speed = std::clamp(val: m_Speed - 1, lo: 0, hi: (int)(std::size(DEMO_SPEEDS) - 1));
1615
1616 // paused
1617 CUIRect PausedButton;
1618 Row.VSplitLeft(Cut: 20.0f, pLeft: &PausedButton, pRight: &Row);
1619 Row.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Row);
1620 static CButtonContainer s_PausedButton;
1621 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_PausedButton, pText: FontIcon::PAUSE, Checked: 0, pRect: &PausedButton, Flags: BUTTONFLAG_LEFT))
1622 m_StartPaused ^= 1;
1623
1624 // fastforward
1625 CUIRect FastForwardButton;
1626 Row.VSplitLeft(Cut: 20.0f, pLeft: &FastForwardButton, pRight: &Row);
1627 Row.VSplitLeft(Cut: 8.0f, pLeft: nullptr, pRight: &Row);
1628 static CButtonContainer s_FastForwardButton;
1629 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_FastForwardButton, pText: FontIcon::FORWARD, Checked: 0, pRect: &FastForwardButton, Flags: BUTTONFLAG_LEFT))
1630 m_Speed = std::clamp(val: m_Speed + 1, lo: 0, hi: (int)(std::size(DEMO_SPEEDS) - 1));
1631
1632 // speed meter
1633 char aBuffer[128];
1634 const char *pPaused = m_StartPaused ? Localize(pStr: "(paused)") : "";
1635 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s: ×%g %s", Localize(pStr: "Speed"), DEMO_SPEEDS[m_Speed], pPaused);
1636 Ui()->DoLabel(pRect: &Row, pText: aBuffer, Size: 12.8f, Align: TEXTALIGN_ML);
1637 Box.HSplitBottom(Cut: 16.0f, pTop: &Box, pBottom: nullptr);
1638 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Row);
1639
1640 CUIRect Label, TextBox;
1641 Row.VSplitLeft(Cut: 110.0f, pLeft: &Label, pRight: &TextBox);
1642 TextBox.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &TextBox);
1643 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Video name:"), Size: 12.8f, Align: TEXTALIGN_ML);
1644 Ui()->DoEditBox(pLineInput: &m_DemoRenderInput, pRect: &TextBox, FontSize: 12.8f);
1645
1646 // Warn about disconnect if online
1647 if(Client()->State() == IClient::STATE_ONLINE)
1648 {
1649 Box.HSplitBottom(Cut: 10.0f, pTop: &Box, pBottom: nullptr);
1650 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: &Row);
1651 SLabelProperties LabelProperties;
1652 LabelProperties.SetColor(ColorRGBA(1.0f, 0.0f, 0.0f));
1653 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "You will be disconnected from the server."), Size: 12.8f, Align: TEXTALIGN_MC, LabelProps: LabelProperties);
1654 }
1655 }
1656 else if(m_Popup == POPUP_RENDER_DONE)
1657 {
1658 CUIRect Ok, OpenFolder;
1659
1660 char aFilePath[IO_MAX_PATH_LENGTH];
1661 char aSaveFolder[IO_MAX_PATH_LENGTH];
1662 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "videos", pBuffer: aSaveFolder, BufferSize: sizeof(aSaveFolder));
1663 str_format(buffer: aFilePath, buffer_size: sizeof(aFilePath), format: "%s/%s.mp4", aSaveFolder, m_DemoRenderInput.GetString());
1664
1665 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1666 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1667 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1668
1669 Part.VSplitMid(pLeft: &OpenFolder, pRight: &Ok);
1670
1671 Ok.VMargin(Cut: 20.0f, pOtherRect: &Ok);
1672 OpenFolder.VMargin(Cut: 20.0f, pOtherRect: &OpenFolder);
1673
1674 static CButtonContainer s_ButtonOpenFolder;
1675 if(DoButton_Menu(pButtonContainer: &s_ButtonOpenFolder, pText: Localize(pStr: "Videos directory"), Checked: 0, pRect: &OpenFolder))
1676 {
1677 Client()->ViewFile(pFilename: aSaveFolder);
1678 }
1679
1680 static CButtonContainer s_ButtonOk;
1681 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &Ok) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1682 {
1683 m_Popup = POPUP_NONE;
1684 m_DemoRenderInput.Clear();
1685 }
1686
1687 Box.HSplitBottom(Cut: 160.f, pTop: &Box, pBottom: &Part);
1688 Part.VMargin(Cut: 20.0f, pOtherRect: &Part);
1689
1690 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Video was saved to '%s'"), aFilePath);
1691
1692 SLabelProperties MessageProps;
1693 MessageProps.m_MaxWidth = (int)Part.w;
1694 Ui()->DoLabel(pRect: &Part, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_TL, LabelProps: MessageProps);
1695 }
1696#endif
1697 else if(m_Popup == POPUP_FIRST_LAUNCH)
1698 {
1699 CUIRect Label, TextBox, Skip, Join;
1700
1701 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1702 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1703 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1704 Part.VSplitMid(pLeft: &Skip, pRight: &Join);
1705 Skip.VMargin(Cut: 20.0f, pOtherRect: &Skip);
1706 Join.VMargin(Cut: 20.0f, pOtherRect: &Join);
1707
1708 static CButtonContainer s_JoinTutorialButton;
1709 if(DoButton_Menu(pButtonContainer: &s_JoinTutorialButton, pText: Localize(pStr: "Join Tutorial Server"), Checked: 0, pRect: &Join) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1710 {
1711 Client()->RequestDDNetInfo();
1712 m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE;
1713 JoinTutorial();
1714 }
1715
1716 static CButtonContainer s_SkipTutorialButton;
1717 if(DoButton_Menu(pButtonContainer: &s_SkipTutorialButton, pText: Localize(pStr: "Skip Tutorial"), Checked: 0, pRect: &Skip) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1718 {
1719 Client()->RequestDDNetInfo();
1720 m_JoinTutorial.m_Queued = false;
1721 m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE;
1722 }
1723
1724 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1725 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1726
1727 Part.VSplitLeft(Cut: 30.0f, pLeft: nullptr, pRight: &Part);
1728 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s\n(%s)",
1729 Localize(pStr: "Show DDNet map finishes in server browser"),
1730 Localize(pStr: "transmits your player name to info.ddnet.org"));
1731
1732 if(DoButton_CheckBox(pId: &g_Config.m_BrIndicateFinished, pText: aBuf, Checked: g_Config.m_BrIndicateFinished, pRect: &Part))
1733 g_Config.m_BrIndicateFinished ^= 1;
1734
1735 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1736 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1737
1738 Part.VSplitLeft(Cut: 60.0f, pLeft: nullptr, pRight: &Label);
1739 Label.VSplitLeft(Cut: 100.0f, pLeft: nullptr, pRight: &TextBox);
1740 TextBox.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &TextBox);
1741 TextBox.VSplitRight(Cut: 60.0f, pLeft: &TextBox, pRight: nullptr);
1742 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Nickname"), Size: 16.0f, Align: TEXTALIGN_ML);
1743 static CLineInput s_PlayerNameInput(g_Config.m_PlayerName, sizeof(g_Config.m_PlayerName));
1744 s_PlayerNameInput.SetEmptyText(Client()->PlayerName());
1745 Ui()->DoEditBox(pLineInput: &s_PlayerNameInput, pRect: &TextBox, FontSize: 12.0f);
1746 }
1747 else if(m_Popup == POPUP_JOIN_TUTORIAL)
1748 {
1749 CUIRect ButtonBar, StatusLabel, ProgressLabel, ProgressIndicator;
1750 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1751 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &ButtonBar);
1752 ButtonBar.VMargin(Cut: 120.0f, pOtherRect: &ButtonBar);
1753 Box.HSplitBottom(Cut: 20.0f, pTop: &StatusLabel, pBottom: nullptr);
1754 StatusLabel.VMargin(Cut: 20.0f, pOtherRect: &StatusLabel);
1755 StatusLabel.HSplitMid(pTop: &StatusLabel, pBottom: &ProgressLabel);
1756 ProgressLabel.VSplitLeft(Cut: 50.0f, pLeft: &ProgressIndicator, pRight: &ProgressLabel);
1757
1758 if(m_JoinTutorial.m_Status == CJoinTutorial::EStatus::REFRESHING)
1759 {
1760 if(ServerBrowser()->IsGettingServerlist() ||
1761 Client()->InfoState() == IClient::EInfoState::LOADING)
1762 {
1763 // Still refreshing
1764 }
1765 else if(ServerBrowser()->IsServerlistError() ||
1766 Client()->InfoState() == IClient::EInfoState::ERROR)
1767 {
1768 m_JoinTutorial.m_Status = CJoinTutorial::EStatus::SERVER_LIST_ERROR;
1769 }
1770 else
1771 {
1772 const char *pAddr = ServerBrowser()->GetTutorialServer();
1773 if(pAddr)
1774 {
1775 Client()->Connect(pAddress: pAddr);
1776 }
1777 else
1778 {
1779 m_JoinTutorial.m_Status = CJoinTutorial::EStatus::NO_TUTORIAL_AVAILABLE;
1780 }
1781 }
1782 }
1783
1784 const char *pStatusLabel = nullptr;
1785 switch(m_JoinTutorial.m_Status)
1786 {
1787 case CJoinTutorial::EStatus::REFRESHING:
1788 pStatusLabel = Localize(pStr: "Getting server list from master server");
1789 break;
1790 case CJoinTutorial::EStatus::SERVER_LIST_ERROR:
1791 pStatusLabel = Localize(pStr: "Could not get server list from master server");
1792 break;
1793 case CJoinTutorial::EStatus::NO_TUTORIAL_AVAILABLE:
1794 pStatusLabel = Localize(pStr: "There are no Tutorial servers available");
1795 break;
1796 }
1797 if(pStatusLabel != nullptr)
1798 {
1799 Ui()->DoLabel(pRect: &StatusLabel, pText: pStatusLabel, Size: 20.0f, Align: TEXTALIGN_ML);
1800 }
1801
1802 const char *pProgressLabel = nullptr;
1803 bool ProgressDeterminate = true;
1804 const float LastStateChangeSeconds = std::chrono::duration_cast<std::chrono::duration<float>>(d: time_get_nanoseconds() - m_JoinTutorial.m_StateChange).count();
1805 constexpr float RefreshDelay = 5.0f;
1806
1807 if(m_JoinTutorial.m_Status == CJoinTutorial::EStatus::REFRESHING)
1808 {
1809 pProgressLabel = Localize(pStr: "Please wait…");
1810 ProgressDeterminate = false;
1811 }
1812 else if(!m_JoinTutorial.m_TryRefresh)
1813 {
1814 if(!m_JoinTutorial.m_TriedRefresh)
1815 {
1816 m_JoinTutorial.m_TryRefresh = true;
1817 m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1818 }
1819 else if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::NOT_TRIED)
1820 {
1821 m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::TRY;
1822 m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1823 }
1824 }
1825
1826 if(m_JoinTutorial.m_TryRefresh)
1827 {
1828 if(LastStateChangeSeconds >= RefreshDelay)
1829 {
1830 // Activate internet tab before joining tutorial to make sure the server info
1831 // for the tutorial servers is available.
1832 GameClient()->m_Menus.SetMenuPage(CMenus::PAGE_INTERNET);
1833 GameClient()->m_Menus.RefreshBrowserTab(Force: true);
1834 m_JoinTutorial.m_Status = CJoinTutorial::EStatus::REFRESHING;
1835 m_JoinTutorial.m_TryRefresh = false;
1836 m_JoinTutorial.m_TriedRefresh = true;
1837 m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1838 }
1839 else
1840 {
1841 pProgressLabel = Localize(pStr: "Retrying…");
1842 }
1843 }
1844
1845 const auto &&ShowFinalErrorMessage = [&]() {
1846 PopupMessage(pTitle: Localize(pStr: "Error joining Tutorial server"), pMessage: Localize(pStr: "Could not find a Tutorial server. Check your internet connection."), pButtonLabel: Localize(pStr: "Ok"));
1847 };
1848 const auto &&RunServer = [&]() {
1849 char aMotd[256];
1850 str_copy(dst&: aMotd, src: "sv_motd \"");
1851 char *pDst = aMotd + str_length(str: aMotd);
1852 str_escape(dst: &pDst, src: Localize(pStr: "You're playing on a local server because no online Tutorial server could be found.\n\nYour record will only be saved locally."), end: aMotd + sizeof(aMotd) - 1);
1853 str_append(dst&: aMotd, src: "\"");
1854 if(GameClient()->m_LocalServer.RunServer(vpArguments: {"sv_register 0", "sv_map Tutorial", aMotd}))
1855 {
1856 m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::WAITING_START;
1857 m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1858 }
1859 else
1860 {
1861 ShowFinalErrorMessage();
1862 }
1863 };
1864 if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::TRY)
1865 {
1866 if(LastStateChangeSeconds >= RefreshDelay)
1867 {
1868 if(GameClient()->m_LocalServer.IsServerRunning())
1869 {
1870 GameClient()->m_LocalServer.KillServer();
1871 m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::WAITING_STOP;
1872 m_JoinTutorial.m_StateChange = time_get_nanoseconds();
1873 }
1874 else
1875 {
1876 RunServer();
1877 }
1878 }
1879 else
1880 {
1881 pProgressLabel = Localize(pStr: "Could not find online Tutorial server.\nStarting and connecting to local server…");
1882 }
1883 }
1884 else if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::WAITING_STOP)
1885 {
1886 if(LastStateChangeSeconds >= 5.0f)
1887 {
1888 ShowFinalErrorMessage();
1889 }
1890 else
1891 {
1892 if(!GameClient()->m_LocalServer.IsServerRunning())
1893 {
1894 RunServer();
1895 }
1896
1897 pProgressLabel = Localize(pStr: "Waiting for local server to stop…");
1898 ProgressDeterminate = false;
1899 }
1900 }
1901 else if(m_JoinTutorial.m_LocalServerState == CJoinTutorial::ELocalServerState::WAITING_START)
1902 {
1903 if(LastStateChangeSeconds >= 5.0f)
1904 {
1905 ShowFinalErrorMessage();
1906 }
1907 else
1908 {
1909 if(LastStateChangeSeconds >= 2.0f &&
1910 GameClient()->m_LocalServer.IsServerRunning())
1911 {
1912 Client()->Connect(pAddress: "localhost");
1913 }
1914
1915 pProgressLabel = Localize(pStr: "Waiting for local server to start…");
1916 ProgressDeterminate = false;
1917 }
1918 }
1919
1920 if(pProgressLabel != nullptr)
1921 {
1922 Ui()->RenderProgressSpinner(Center: ProgressIndicator.Center(), OuterRadius: 12.0f, Props: {.m_Progress = ProgressDeterminate ? (LastStateChangeSeconds / RefreshDelay) : -1.0f});
1923 Ui()->DoLabel(pRect: &ProgressLabel, pText: pProgressLabel, Size: 20.0f, Align: TEXTALIGN_ML);
1924 }
1925
1926 static CButtonContainer s_Button;
1927 if(DoButton_Menu(pButtonContainer: &s_Button, pText: Localize(pStr: "Cancel"), Checked: 0, pRect: &ButtonBar) ||
1928 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE) ||
1929 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1930 {
1931 m_Popup = POPUP_NONE;
1932 }
1933 }
1934 else if(m_Popup == POPUP_POINTS)
1935 {
1936 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1937 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Part);
1938 Part.VMargin(Cut: 120.0f, pOtherRect: &Part);
1939
1940 if(Client()->InfoState() == IClient::EInfoState::SUCCESS && Client()->Points() > 50)
1941 {
1942 CUIRect Yes, No;
1943 Part.VSplitMid(pLeft: &No, pRight: &Yes, Spacing: 40.0f);
1944 static CButtonContainer s_ButtonNo;
1945 if(DoButton_Menu(pButtonContainer: &s_ButtonNo, pText: Localize(pStr: "No"), Checked: 0, pRect: &No) ||
1946 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1947 {
1948 m_Popup = POPUP_FIRST_LAUNCH;
1949 }
1950
1951 static CButtonContainer s_ButtonYes;
1952 if(DoButton_Menu(pButtonContainer: &s_ButtonYes, pText: Localize(pStr: "Yes"), Checked: 0, pRect: &Yes) ||
1953 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1954 {
1955 m_Popup = POPUP_NONE;
1956 }
1957 }
1958 else
1959 {
1960 static CButtonContainer s_Button;
1961 if(DoButton_Menu(pButtonContainer: &s_Button, pText: Localize(pStr: "Cancel"), Checked: 0, pRect: &Part) ||
1962 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE) ||
1963 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) ||
1964 Client()->InfoState() == IClient::EInfoState::SUCCESS)
1965 {
1966 m_Popup = POPUP_NONE;
1967 }
1968 if(Client()->InfoState() == IClient::EInfoState::ERROR)
1969 {
1970 PopupMessage(pTitle: Localize(pStr: "Error checking player name"), pMessage: Localize(pStr: "Could not check for existing player with your name. Check your internet connection."), pButtonLabel: Localize(pStr: "Ok"));
1971 }
1972 }
1973 }
1974 else if(m_Popup == POPUP_WARNING)
1975 {
1976 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1977 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1978 Part.VMargin(Cut: 120.0f, pOtherRect: &Part);
1979
1980 static CButtonContainer s_Button;
1981 if(DoButton_Menu(pButtonContainer: &s_Button, pText: pButtonText, Checked: 0, pRect: &Part) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) || (m_PopupWarningDuration > 0s && time_get_nanoseconds() - m_PopupWarningLastTime >= m_PopupWarningDuration))
1982 {
1983 m_Popup = POPUP_NONE;
1984 SetActive(false);
1985 }
1986 }
1987 else if(m_Popup == POPUP_SAVE_SKIN)
1988 {
1989 CUIRect Label, TextBox, Yes, No;
1990
1991 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1992 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1993 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1994
1995 Part.VSplitMid(pLeft: &No, pRight: &Yes);
1996
1997 Yes.VMargin(Cut: 20.0f, pOtherRect: &Yes);
1998 No.VMargin(Cut: 20.0f, pOtherRect: &No);
1999
2000 static CButtonContainer s_ButtonNo;
2001 if(DoButton_Menu(pButtonContainer: &s_ButtonNo, pText: Localize(pStr: "No"), Checked: 0, pRect: &No) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
2002 m_Popup = POPUP_NONE;
2003
2004 static CButtonContainer s_ButtonYes;
2005 if(DoButton_Menu(pButtonContainer: &s_ButtonYes, pText: Localize(pStr: "Yes"), Checked: m_SkinNameInput.IsEmpty() ? 1 : 0, pRect: &Yes) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
2006 {
2007 if(!str_valid_filename(str: m_SkinNameInput.GetString()))
2008 {
2009 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "This name cannot be used for files and folders"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_SAVE_SKIN);
2010 }
2011 else if(CSkins7::IsSpecialSkin(pName: m_SkinNameInput.GetString()))
2012 {
2013 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "Unable to save the skin with a reserved name"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_SAVE_SKIN);
2014 }
2015 else if(!GameClient()->m_Skins7.SaveSkinfile(pName: m_SkinNameInput.GetString(), Dummy: m_Dummy))
2016 {
2017 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "Unable to save the skin"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_SAVE_SKIN);
2018 }
2019 else
2020 {
2021 m_Popup = POPUP_NONE;
2022 m_SkinList7LastRefreshTime = std::nullopt;
2023 }
2024 }
2025
2026 Box.HSplitBottom(Cut: 60.f, pTop: &Box, pBottom: &Part);
2027 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
2028
2029 Part.VMargin(Cut: 60.0f, pOtherRect: &Label);
2030 Label.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &TextBox);
2031 TextBox.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &TextBox);
2032 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Name"), Size: 18.0f, Align: TEXTALIGN_ML);
2033 Ui()->DoClearableEditBox(pLineInput: &m_SkinNameInput, pRect: &TextBox, FontSize: 12.0f);
2034 }
2035 else
2036 {
2037 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
2038 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
2039 Part.VMargin(Cut: 120.0f, pOtherRect: &Part);
2040
2041 static CButtonContainer s_Button;
2042 if(DoButton_Menu(pButtonContainer: &s_Button, pText: pButtonText, Checked: 0, pRect: &Part) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
2043 {
2044 if(m_Popup == POPUP_DISCONNECTED && Client()->ReconnectTime() > 0)
2045 Client()->SetReconnectTime(0);
2046 m_Popup = POPUP_NONE;
2047 }
2048 }
2049
2050 if(m_Popup == POPUP_NONE)
2051 Ui()->SetActiveItem(nullptr);
2052}
2053
2054void CMenus::RenderPopupConnecting(CUIRect Screen)
2055{
2056 const float FontSize = 20.0f;
2057
2058 CUIRect Box, Label;
2059 Screen.Margin(Cut: 150.0f, pOtherRect: &Box);
2060 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
2061 Box.Margin(Cut: 20.0f, pOtherRect: &Box);
2062
2063 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
2064 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Connecting to"), Size: 24.0f, Align: TEXTALIGN_MC);
2065
2066 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
2067 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
2068 SLabelProperties Props;
2069 Props.m_MaxWidth = Label.w;
2070 Props.m_EllipsisAtEnd = true;
2071 Ui()->DoLabel(pRect: &Label, pText: Client()->ConnectAddressString(), Size: FontSize, Align: TEXTALIGN_MC, LabelProps: Props);
2072
2073 if(time_get() - Client()->StateStartTime() > time_freq())
2074 {
2075 const char *pConnectivityLabel = "";
2076 switch(Client()->UdpConnectivity(NetType: Client()->ConnectNetTypes()))
2077 {
2078 case IClient::CONNECTIVITY_UNKNOWN:
2079 break;
2080 case IClient::CONNECTIVITY_CHECKING:
2081 pConnectivityLabel = Localize(pStr: "Trying to determine UDP connectivity…");
2082 break;
2083 case IClient::CONNECTIVITY_UNREACHABLE:
2084 pConnectivityLabel = Localize(pStr: "UDP seems to be filtered.");
2085 break;
2086 case IClient::CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES:
2087 pConnectivityLabel = Localize(pStr: "UDP and TCP IP addresses seem to be different. Try disabling VPN, proxy or network accelerators.");
2088 break;
2089 case IClient::CONNECTIVITY_REACHABLE:
2090 pConnectivityLabel = Localize(pStr: "No answer from server yet.");
2091 break;
2092 }
2093 if(pConnectivityLabel[0] != '\0')
2094 {
2095 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
2096 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
2097 SLabelProperties ConnectivityLabelProps;
2098 ConnectivityLabelProps.m_MaxWidth = Label.w;
2099 if(TextRender()->TextWidth(Size: FontSize, pText: pConnectivityLabel) > Label.w)
2100 Ui()->DoLabel(pRect: &Label, pText: pConnectivityLabel, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: ConnectivityLabelProps);
2101 else
2102 Ui()->DoLabel(pRect: &Label, pText: pConnectivityLabel, Size: FontSize, Align: TEXTALIGN_MC);
2103 }
2104 }
2105
2106 CUIRect Button;
2107 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Button);
2108 Button.VMargin(Cut: 100.0f, pOtherRect: &Button);
2109
2110 static CButtonContainer s_Button;
2111 if(DoButton_Menu(pButtonContainer: &s_Button, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Button) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
2112 {
2113 Client()->Disconnect();
2114 Ui()->SetActiveItem(nullptr);
2115 RefreshBrowserTab(Force: true);
2116 }
2117}
2118
2119void CMenus::RenderPopupLoading(CUIRect Screen)
2120{
2121 char aTitle[256];
2122 char aLabel1[128];
2123 char aLabel2[128];
2124 if(Client()->MapDownloadTotalsize() > 0)
2125 {
2126 const int64_t Now = time_get();
2127 if(Now - m_DownloadLastCheckTime >= time_freq())
2128 {
2129 if(m_DownloadLastCheckSize > Client()->MapDownloadAmount())
2130 {
2131 // map downloaded restarted
2132 m_DownloadLastCheckSize = 0;
2133 }
2134
2135 // update download speed
2136 const float Diff = (Client()->MapDownloadAmount() - m_DownloadLastCheckSize) / ((int)((Now - m_DownloadLastCheckTime) / time_freq()));
2137 const float StartDiff = m_DownloadLastCheckSize - 0.0f;
2138 if(StartDiff + Diff > 0.0f)
2139 m_DownloadSpeed = (Diff / (StartDiff + Diff)) * (Diff / 1.0f) + (StartDiff / (Diff + StartDiff)) * m_DownloadSpeed;
2140 else
2141 m_DownloadSpeed = 0.0f;
2142 m_DownloadLastCheckTime = Now;
2143 m_DownloadLastCheckSize = Client()->MapDownloadAmount();
2144 }
2145
2146 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: "%s: %s", Localize(pStr: "Downloading map"), Client()->MapDownloadName());
2147
2148 str_format(buffer: aLabel1, buffer_size: sizeof(aLabel1), format: Localize(pStr: "%d/%d KiB (%.1f KiB/s)"), Client()->MapDownloadAmount() / 1024, Client()->MapDownloadTotalsize() / 1024, m_DownloadSpeed / 1024.0f);
2149
2150 const int SecondsLeft = std::max(a: 1, b: m_DownloadSpeed > 0.0f ? static_cast<int>((Client()->MapDownloadTotalsize() - Client()->MapDownloadAmount()) / m_DownloadSpeed) : 1);
2151 const int MinutesLeft = SecondsLeft / 60;
2152 if(MinutesLeft > 0)
2153 {
2154 str_format(buffer: aLabel2, buffer_size: sizeof(aLabel2), format: MinutesLeft == 1 ? Localize(pStr: "%i minute left") : Localize(pStr: "%i minutes left"), MinutesLeft);
2155 }
2156 else
2157 {
2158 str_format(buffer: aLabel2, buffer_size: sizeof(aLabel2), format: SecondsLeft == 1 ? Localize(pStr: "%i second left") : Localize(pStr: "%i seconds left"), SecondsLeft);
2159 }
2160 }
2161 else
2162 {
2163 str_copy(dst&: aTitle, src: Localize(pStr: "Connected"));
2164 switch(Client()->LoadingStateDetail())
2165 {
2166 case IClient::LOADING_STATE_DETAIL_INITIAL:
2167 str_copy(dst&: aLabel1, src: Localize(pStr: "Getting game info"));
2168 break;
2169 case IClient::LOADING_STATE_DETAIL_LOADING_MAP:
2170 str_copy(dst&: aLabel1, src: Localize(pStr: "Loading map file from storage"));
2171 break;
2172 case IClient::LOADING_STATE_DETAIL_LOADING_DEMO:
2173 str_copy(dst&: aLabel1, src: Localize(pStr: "Loading demo file from storage"));
2174 break;
2175 case IClient::LOADING_STATE_DETAIL_SENDING_READY:
2176 str_copy(dst&: aLabel1, src: Localize(pStr: "Requesting to join the game"));
2177 break;
2178 case IClient::LOADING_STATE_DETAIL_GETTING_READY:
2179 str_copy(dst&: aLabel1, src: Localize(pStr: "Sending initial client info"));
2180 break;
2181 default:
2182 dbg_assert_failed("Invalid loading state %d for RenderPopupLoading", static_cast<int>(Client()->LoadingStateDetail()));
2183 }
2184 aLabel2[0] = '\0';
2185 }
2186
2187 const float FontSize = 20.0f;
2188
2189 CUIRect Box, Label;
2190 Screen.Margin(Cut: 150.0f, pOtherRect: &Box);
2191 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
2192 Box.Margin(Cut: 20.0f, pOtherRect: &Box);
2193
2194 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
2195 Ui()->DoLabel(pRect: &Label, pText: aTitle, Size: 24.0f, Align: TEXTALIGN_MC);
2196
2197 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
2198 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
2199 Ui()->DoLabel(pRect: &Label, pText: aLabel1, Size: FontSize, Align: TEXTALIGN_MC);
2200
2201 if(aLabel2[0] != '\0')
2202 {
2203 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
2204 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
2205 SLabelProperties ExtraTextProps;
2206 ExtraTextProps.m_MaxWidth = Label.w;
2207 if(TextRender()->TextWidth(Size: FontSize, pText: aLabel2) > Label.w)
2208 Ui()->DoLabel(pRect: &Label, pText: aLabel2, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: ExtraTextProps);
2209 else
2210 Ui()->DoLabel(pRect: &Label, pText: aLabel2, Size: FontSize, Align: TEXTALIGN_MC);
2211 }
2212
2213 if(Client()->MapDownloadTotalsize() > 0)
2214 {
2215 CUIRect ProgressBar;
2216 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
2217 Box.HSplitTop(Cut: 24.0f, pTop: &ProgressBar, pBottom: &Box);
2218 ProgressBar.VMargin(Cut: 20.0f, pOtherRect: &ProgressBar);
2219 Ui()->RenderProgressBar(ProgressBar, Progress: Client()->MapDownloadAmount() / (float)Client()->MapDownloadTotalsize());
2220 }
2221
2222 CUIRect Button;
2223 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Button);
2224 Button.VMargin(Cut: 100.0f, pOtherRect: &Button);
2225
2226 static CButtonContainer s_Button;
2227 if(DoButton_Menu(pButtonContainer: &s_Button, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Button) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
2228 {
2229 Client()->Disconnect();
2230 Ui()->SetActiveItem(nullptr);
2231 RefreshBrowserTab(Force: true);
2232 }
2233}
2234
2235#if defined(CONF_VIDEORECORDER)
2236void CMenus::PopupConfirmDemoReplaceVideo()
2237{
2238 char aBuf[IO_MAX_PATH_LENGTH];
2239 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s.demo", m_aCurrentDemoFolder, m_aCurrentDemoSelectionName);
2240 char aVideoName[IO_MAX_PATH_LENGTH];
2241 str_copy(dst&: aVideoName, src: m_DemoRenderInput.GetString());
2242 const char *pError = Client()->DemoPlayer_Render(pFilename: aBuf, StorageType: m_DemolistStorageType, pVideoName: aVideoName, SpeedIndex: m_Speed, StartPaused: m_StartPaused);
2243 m_Speed = DEMO_SPEED_INDEX_DEFAULT;
2244 m_StartPaused = false;
2245 m_LastPauseChange = -1.0f;
2246 m_LastSpeedChange = -1.0f;
2247 if(pError)
2248 {
2249 m_DemoRenderInput.Clear();
2250 PopupMessage(pTitle: Localize(pStr: "Error loading demo"), pMessage: pError, pButtonLabel: Localize(pStr: "Ok"));
2251 }
2252}
2253#endif
2254
2255void CMenus::SetActive(bool Active)
2256{
2257 if(Active != m_MenuActive)
2258 {
2259 Ui()->SetHotItem(nullptr);
2260 Ui()->SetActiveItem(nullptr);
2261 }
2262 m_MenuActive = Active;
2263 if(!m_MenuActive)
2264 {
2265 if(m_NeedSendinfo)
2266 {
2267 GameClient()->SendInfo(Start: false);
2268 m_NeedSendinfo = false;
2269 }
2270
2271 if(m_NeedSendDummyinfo)
2272 {
2273 GameClient()->SendDummyInfo(Start: false);
2274 m_NeedSendDummyinfo = false;
2275 }
2276
2277 if(Client()->State() == IClient::STATE_ONLINE)
2278 {
2279 GameClient()->OnRelease();
2280 }
2281 }
2282 else if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
2283 {
2284 GameClient()->OnRelease();
2285 }
2286}
2287
2288void CMenus::OnShutdown()
2289{
2290 m_CommunityIcons.Shutdown();
2291}
2292
2293bool CMenus::OnCursorMove(float x, float y, IInput::ECursorType CursorType)
2294{
2295 if(!m_MenuActive)
2296 return false;
2297
2298 Ui()->ConvertMouseMove(pX: &x, pY: &y, CursorType);
2299 Ui()->OnCursorMove(X: x, Y: y);
2300
2301 return true;
2302}
2303
2304bool CMenus::OnInput(const IInput::CEvent &Event)
2305{
2306 // Escape key is always handled to activate/deactivate menu
2307 if((Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) || IsActive())
2308 {
2309 Ui()->OnInput(Event);
2310 return true;
2311 }
2312 return false;
2313}
2314
2315void CMenus::OnStateChange(int NewState, int OldState)
2316{
2317 // reset active item
2318 Ui()->SetActiveItem(nullptr);
2319
2320 if(OldState == IClient::STATE_ONLINE || OldState == IClient::STATE_OFFLINE)
2321 TextRender()->DeleteTextContainer(TextContainerIndex&: m_MotdTextContainerIndex);
2322
2323 if(NewState == IClient::STATE_OFFLINE)
2324 {
2325 if(OldState >= IClient::STATE_ONLINE && NewState < IClient::STATE_QUITTING)
2326 UpdateMusicState();
2327 m_Popup = POPUP_NONE;
2328 if(Client()->ErrorString() && Client()->ErrorString()[0] != 0)
2329 {
2330 if(str_find(haystack: Client()->ErrorString(), needle: "password"))
2331 {
2332 m_Popup = POPUP_PASSWORD;
2333 m_PasswordInput.SelectAll();
2334 Ui()->SetActiveItem(&m_PasswordInput);
2335 }
2336 else
2337 {
2338 m_Popup = POPUP_DISCONNECTED;
2339 }
2340 }
2341 }
2342 else if(NewState == IClient::STATE_LOADING)
2343 {
2344 m_DownloadLastCheckTime = time_get();
2345 m_DownloadLastCheckSize = 0;
2346 m_DownloadSpeed = 0.0f;
2347 }
2348 else if(NewState == IClient::STATE_ONLINE || NewState == IClient::STATE_DEMOPLAYBACK)
2349 {
2350 if(m_Popup != POPUP_WARNING)
2351 {
2352 m_Popup = POPUP_NONE;
2353 SetActive(false);
2354 }
2355 }
2356}
2357
2358void CMenus::OnWindowResize()
2359{
2360 TextRender()->DeleteTextContainer(TextContainerIndex&: m_MotdTextContainerIndex);
2361}
2362
2363void CMenus::OnRender()
2364{
2365 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
2366 SetActive(true);
2367
2368 if(Client()->State() == IClient::STATE_ONLINE && GameClient()->m_ServerMode == CGameClient::SERVERMODE_PUREMOD)
2369 {
2370 Client()->Disconnect();
2371 SetActive(true);
2372 PopupMessage(pTitle: Localize(pStr: "Disconnected"), pMessage: Localize(pStr: "The server is running a non-standard tuning on a pure game type."), pButtonLabel: Localize(pStr: "Ok"));
2373 }
2374
2375 if(!IsActive())
2376 {
2377 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
2378 {
2379 SetActive(true);
2380 }
2381 else if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
2382 {
2383 Ui()->ClearHotkeys();
2384 return;
2385 }
2386 }
2387
2388 Ui()->StartCheck();
2389 UpdateColors();
2390
2391 Ui()->Update();
2392
2393 if(IsActive())
2394 Ui()->DoBackButton();
2395
2396 Render();
2397
2398 if(IsActive())
2399 {
2400 Ui()->RenderBackButton();
2401 RenderTools()->RenderCursor(Center: Ui()->MousePos(), Size: 24.0f);
2402 }
2403
2404 // render debug information
2405 if(g_Config.m_Debug)
2406 Ui()->DebugRender(X: 2.0f, Y: Ui()->Screen()->h - 12.0f);
2407
2408 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
2409 SetActive(false);
2410
2411 Ui()->FinishCheck();
2412 Ui()->ClearHotkeys();
2413}
2414
2415void CMenus::UpdateColors()
2416{
2417 ms_GuiColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_UiColor, true));
2418
2419 ms_ColorTabbarInactiveOutgame = ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f);
2420 ms_ColorTabbarActiveOutgame = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f);
2421 ms_ColorTabbarHoverOutgame = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f);
2422
2423 const float ColorIngameScaleI = 0.5f;
2424 const float ColorIngameScaleA = 0.2f;
2425
2426 ms_ColorTabbarInactiveIngame = ColorRGBA(
2427 ms_GuiColor.r * ColorIngameScaleI,
2428 ms_GuiColor.g * ColorIngameScaleI,
2429 ms_GuiColor.b * ColorIngameScaleI,
2430 ms_GuiColor.a * 0.8f);
2431
2432 ms_ColorTabbarActiveIngame = ColorRGBA(
2433 ms_GuiColor.r * ColorIngameScaleA,
2434 ms_GuiColor.g * ColorIngameScaleA,
2435 ms_GuiColor.b * ColorIngameScaleA,
2436 ms_GuiColor.a);
2437
2438 ms_ColorTabbarHoverIngame = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f);
2439}
2440
2441void CMenus::RenderBackground()
2442{
2443 const float ScreenHeight = 300.0f;
2444 const float ScreenWidth = ScreenHeight * Graphics()->ScreenAspect();
2445 Graphics()->MapScreenToSize(Width: ScreenWidth, Height: ScreenHeight);
2446
2447 // render background color
2448 Graphics()->TextureClear();
2449 Graphics()->QuadsBegin();
2450 Graphics()->SetColor(ms_GuiColor.WithAlpha(alpha: 1.0f));
2451 const IGraphics::CQuadItem BackgroundQuadItem = IGraphics::CQuadItem(0, 0, ScreenWidth, ScreenHeight);
2452 Graphics()->QuadsDrawTL(pArray: &BackgroundQuadItem, Num: 1);
2453 Graphics()->QuadsEnd();
2454
2455 // render the tiles
2456 Graphics()->TextureClear();
2457 Graphics()->QuadsBegin();
2458 Graphics()->SetColor(r: 0.0f, g: 0.0f, b: 0.0f, a: 0.045f);
2459 const float Size = 15.0f;
2460 const float OffsetTime = std::fmod(x: Client()->GlobalTime() * 0.15f, y: 2.0f);
2461 IGraphics::CQuadItem aCheckerItems[64];
2462 size_t NumCheckerItems = 0;
2463 const int NumItemsWidth = std::ceil(x: ScreenWidth / Size);
2464 const int NumItemsHeight = std::ceil(x: ScreenHeight / Size);
2465 for(int y = -2; y < NumItemsHeight; y++)
2466 {
2467 for(int x = 0; x < NumItemsWidth + 4; x += 2)
2468 {
2469 aCheckerItems[NumCheckerItems] = IGraphics::CQuadItem((x - 2 * OffsetTime + (y & 1)) * Size, (y + OffsetTime) * Size, Size, Size);
2470 NumCheckerItems++;
2471 if(NumCheckerItems == std::size(aCheckerItems))
2472 {
2473 Graphics()->QuadsDrawTL(pArray: aCheckerItems, Num: NumCheckerItems);
2474 NumCheckerItems = 0;
2475 }
2476 }
2477 }
2478 if(NumCheckerItems != 0)
2479 Graphics()->QuadsDrawTL(pArray: aCheckerItems, Num: NumCheckerItems);
2480 Graphics()->QuadsEnd();
2481
2482 // render border fade
2483 Graphics()->TextureSet(Texture: m_TextureBlob);
2484 Graphics()->QuadsBegin();
2485 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2486 const IGraphics::CQuadItem BlobQuadItem = IGraphics::CQuadItem(-100, -100, ScreenWidth + 200, ScreenHeight + 200);
2487 Graphics()->QuadsDrawTL(pArray: &BlobQuadItem, Num: 1);
2488 Graphics()->QuadsEnd();
2489
2490 // restore screen
2491 Ui()->MapScreen();
2492}
2493
2494int CMenus::DoButton_CheckBox_Tristate(const void *pId, const char *pText, TRISTATE Checked, const CUIRect *pRect)
2495{
2496 switch(Checked)
2497 {
2498 case TRISTATE::NONE:
2499 return DoButton_CheckBox_Common(pId, pText, pBoxText: "", pRect, Flags: BUTTONFLAG_LEFT);
2500 case TRISTATE::SOME:
2501 return DoButton_CheckBox_Common(pId, pText, pBoxText: "O", pRect, Flags: BUTTONFLAG_LEFT);
2502 case TRISTATE::ALL:
2503 return DoButton_CheckBox_Common(pId, pText, pBoxText: "X", pRect, Flags: BUTTONFLAG_LEFT);
2504 default:
2505 dbg_assert_failed("Invalid tristate. Checked: %d", static_cast<int>(Checked));
2506 }
2507}
2508
2509int CMenus::MenuImageScan(const char *pName, int IsDir, int DirType, void *pUser)
2510{
2511 const char *pExtension = ".png";
2512 CMenuImage MenuImage;
2513 CMenus *pSelf = static_cast<CMenus *>(pUser);
2514 if(IsDir || !str_endswith(str: pName, suffix: pExtension) || str_length(str: pName) - str_length(str: pExtension) >= (int)sizeof(MenuImage.m_aName))
2515 return 0;
2516
2517 char aPath[IO_MAX_PATH_LENGTH];
2518 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "menuimages/%s", pName);
2519
2520 CImageInfo Info;
2521 if(!pSelf->Graphics()->LoadPng(Image&: Info, pFilename: aPath, StorageType: DirType))
2522 {
2523 log_error("menus", "Failed to load menu image from '%s'", aPath);
2524 return 0;
2525 }
2526 if(Info.m_Format != CImageInfo::FORMAT_RGBA)
2527 {
2528 Info.Free();
2529 log_error("menus", "Failed to load menu image from '%s': must be an RGBA image", aPath);
2530 return 0;
2531 }
2532
2533 MenuImage.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Image: Info, Flags: 0, pTexName: aPath);
2534
2535 ConvertToGrayscale(Image: Info);
2536 MenuImage.m_GreyTexture = pSelf->Graphics()->LoadTextureRawMove(Image&: Info, Flags: 0, pTexName: aPath);
2537
2538 str_truncate(dst: MenuImage.m_aName, dst_size: sizeof(MenuImage.m_aName), src: pName, truncation_len: str_length(str: pName) - str_length(str: pExtension));
2539 pSelf->m_vMenuImages.push_back(x: MenuImage);
2540
2541 pSelf->RenderLoading(pCaption: Localize(pStr: "Loading DDNet Client"), pContent: Localize(pStr: "Loading menu images"), IncreaseCounter: 0);
2542
2543 return 0;
2544}
2545
2546const CMenus::CMenuImage *CMenus::FindMenuImage(const char *pName)
2547{
2548 for(auto &Image : m_vMenuImages)
2549 if(str_comp(a: Image.m_aName, b: pName) == 0)
2550 return &Image;
2551 return nullptr;
2552}
2553
2554void CMenus::SetMenuPage(int NewPage)
2555{
2556 const int OldPage = m_MenuPage;
2557 m_MenuPage = NewPage;
2558 if(NewPage >= PAGE_INTERNET && NewPage <= PAGE_FAVORITE_COMMUNITY_5)
2559 {
2560 g_Config.m_UiPage = NewPage;
2561 bool ForceRefresh = false;
2562 if(m_ForceRefreshLanPage && NewPage == PAGE_LAN)
2563 {
2564 ForceRefresh = true;
2565 m_ForceRefreshLanPage = false;
2566 }
2567 if(OldPage != NewPage || ForceRefresh)
2568 {
2569 RefreshBrowserTab(Force: ForceRefresh);
2570 }
2571 }
2572}
2573
2574void CMenus::RefreshBrowserTab(bool Force)
2575{
2576 if(g_Config.m_UiPage == PAGE_INTERNET)
2577 {
2578 if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET)
2579 {
2580 if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
2581 {
2582 Client()->RequestDDNetInfo();
2583 }
2584 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_INTERNET);
2585 UpdateCommunityCache(Force: true);
2586 }
2587 }
2588 else if(g_Config.m_UiPage == PAGE_LAN)
2589 {
2590 if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_LAN)
2591 {
2592 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_LAN);
2593 UpdateCommunityCache(Force: true);
2594 }
2595 }
2596 else if(g_Config.m_UiPage == PAGE_FAVORITES)
2597 {
2598 if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES)
2599 {
2600 if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
2601 {
2602 Client()->RequestDDNetInfo();
2603 }
2604 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_FAVORITES);
2605 UpdateCommunityCache(Force: true);
2606 }
2607 }
2608 else if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5)
2609 {
2610 const int BrowserType = g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + IServerBrowser::TYPE_FAVORITE_COMMUNITY_1;
2611 if(Force || ServerBrowser()->GetCurrentType() != BrowserType)
2612 {
2613 if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
2614 {
2615 Client()->RequestDDNetInfo();
2616 }
2617 ServerBrowser()->Refresh(Type: BrowserType);
2618 UpdateCommunityCache(Force: true);
2619 }
2620 }
2621}
2622
2623void CMenus::ForceRefreshLanPage()
2624{
2625 m_ForceRefreshLanPage = true;
2626}
2627
2628void CMenus::SetShowStart(bool ShowStart)
2629{
2630 m_ShowStart = ShowStart;
2631}
2632
2633void CMenus::ShowQuitPopup()
2634{
2635 m_Popup = POPUP_QUIT;
2636}
2637
2638void CMenus::JoinTutorial()
2639{
2640 m_JoinTutorial.m_Queued = true;
2641 m_JoinTutorial.m_Status = CJoinTutorial::EStatus::REFRESHING;
2642 m_JoinTutorial.m_TryRefresh = false;
2643 m_JoinTutorial.m_TriedRefresh = false;
2644 m_JoinTutorial.m_LocalServerState = CJoinTutorial::ELocalServerState::NOT_TRIED;
2645 m_JoinTutorial.m_StateChange = time_get_nanoseconds();
2646}
2647