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