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_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Checked);
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 const int CurLoadRenderCount = m_LoadingState.m_Current;
738 m_LoadingState.m_Current += IncreaseCounter;
739
740 // make sure that we don't render for each little thing we load
741 // because that will slow down loading if we have vsync
742 const std::chrono::nanoseconds Now = time_get_nanoseconds();
743 if(Now - m_LoadingState.m_LastRender < std::chrono::nanoseconds(1s) / 60l)
744 return;
745
746 m_LoadingState.m_LastRender = Now;
747
748 // need up date this here to get correct
749 ms_GuiColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_UiColor, true));
750
751 Ui()->MapScreen();
752
753 if(!RenderMenuBackgroundMap || !GameClient()->m_MenuBackground.Render())
754 {
755 RenderBackground();
756 }
757
758 CUIRect Box;
759 Ui()->Screen()->Margin(Cut: 160.0f, pOtherRect: &Box);
760
761 Graphics()->BlendNormal();
762 Graphics()->TextureClear();
763 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
764 Box.Margin(Cut: 20.0f, pOtherRect: &Box);
765
766 CUIRect Label;
767 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
768 Ui()->DoLabel(pRect: &Label, pText: pCaption, Size: 24.0f, Align: TEXTALIGN_MC);
769
770 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
771 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
772 Ui()->DoLabel(pRect: &Label, pText: pContent, Size: 20.0f, Align: TEXTALIGN_MC);
773
774 if(RenderLoadingBar)
775 {
776 CUIRect ProgressBar;
777 Box.HSplitBottom(Cut: 30.0f, pTop: &Box, pBottom: nullptr);
778 Box.HSplitBottom(Cut: 25.0f, pTop: &Box, pBottom: &ProgressBar);
779 ProgressBar.VMargin(Cut: 20.0f, pOtherRect: &ProgressBar);
780 Ui()->RenderProgressBar(ProgressBar, Progress: CurLoadRenderCount / (float)m_LoadingState.m_Total);
781 }
782
783 Client()->UpdateAndSwap();
784}
785
786void CMenus::RenderNews(CUIRect MainView)
787{
788 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_NEWS);
789
790 g_Config.m_UiUnreadNews = false;
791
792 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
793
794 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
795 MainView.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &MainView);
796
797 CUIRect Label;
798
799 const char *pStr = Client()->News();
800 char aLine[256];
801 while((pStr = str_next_token(str: pStr, delim: "\n", buffer: aLine, buffer_size: sizeof(aLine))))
802 {
803 const int Len = str_length(str: aLine);
804 if(Len > 0 && aLine[0] == '|' && aLine[Len - 1] == '|')
805 {
806 MainView.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &MainView);
807 aLine[Len - 1] = '\0';
808 Ui()->DoLabel(pRect: &Label, pText: aLine + 1, Size: 20.0f, Align: TEXTALIGN_ML);
809 }
810 else
811 {
812 MainView.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &MainView);
813 Ui()->DoLabel(pRect: &Label, pText: aLine, Size: 15.f, Align: TEXTALIGN_ML);
814 }
815 }
816}
817
818void CMenus::OnInit()
819{
820 if(g_Config.m_ClShowWelcome)
821 {
822 m_Popup = POPUP_LANGUAGE;
823 m_CreateDefaultFavoriteCommunities = true;
824 }
825
826 if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 &&
827 (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size())
828 {
829 // Reset page to internet when there is no favorite community for this page.
830 g_Config.m_UiPage = PAGE_INTERNET;
831 }
832
833 if(g_Config.m_ClSkipStartMenu)
834 {
835 m_ShowStart = false;
836 }
837
838 SetMenuPage(g_Config.m_UiPage);
839
840 m_RefreshButton.Init(pUI: Ui(), RequestedRectCount: -1);
841 m_ConnectButton.Init(pUI: Ui(), RequestedRectCount: -1);
842
843 Console()->Chain(pName: "add_favorite", pfnChainFunc: ConchainFavoritesUpdate, pUser: this);
844 Console()->Chain(pName: "remove_favorite", pfnChainFunc: ConchainFavoritesUpdate, pUser: this);
845 Console()->Chain(pName: "add_friend", pfnChainFunc: ConchainFriendlistUpdate, pUser: this);
846 Console()->Chain(pName: "remove_friend", pfnChainFunc: ConchainFriendlistUpdate, pUser: this);
847
848 Console()->Chain(pName: "add_excluded_community", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
849 Console()->Chain(pName: "remove_excluded_community", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
850 Console()->Chain(pName: "add_excluded_country", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
851 Console()->Chain(pName: "remove_excluded_country", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
852 Console()->Chain(pName: "add_excluded_type", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
853 Console()->Chain(pName: "remove_excluded_type", pfnChainFunc: ConchainCommunitiesUpdate, pUser: this);
854
855 Console()->Chain(pName: "ui_page", pfnChainFunc: ConchainUiPageUpdate, pUser: this);
856
857 Console()->Chain(pName: "snd_enable", pfnChainFunc: ConchainUpdateMusicState, pUser: this);
858 Console()->Chain(pName: "snd_enable_music", pfnChainFunc: ConchainUpdateMusicState, pUser: this);
859 Console()->Chain(pName: "cl_background_entities", pfnChainFunc: ConchainBackgroundEntities, pUser: this);
860
861 Console()->Chain(pName: "cl_assets_entities", pfnChainFunc: ConchainAssetsEntities, pUser: this);
862 Console()->Chain(pName: "cl_asset_game", pfnChainFunc: ConchainAssetGame, pUser: this);
863 Console()->Chain(pName: "cl_asset_emoticons", pfnChainFunc: ConchainAssetEmoticons, pUser: this);
864 Console()->Chain(pName: "cl_asset_particles", pfnChainFunc: ConchainAssetParticles, pUser: this);
865 Console()->Chain(pName: "cl_asset_hud", pfnChainFunc: ConchainAssetHud, pUser: this);
866 Console()->Chain(pName: "cl_asset_extras", pfnChainFunc: ConchainAssetExtras, pUser: this);
867
868 m_TextureBlob = Graphics()->LoadTexture(pFilename: "blob.png", StorageType: IStorage::TYPE_ALL);
869
870 // setup load amount
871 const int NumMenuImages = 5;
872 m_LoadingState.m_Current = 0;
873 m_LoadingState.m_Total = g_pData->m_NumImages + NumMenuImages + GameClient()->ComponentCount();
874 if(!g_Config.m_ClThreadsoundloading)
875 m_LoadingState.m_Total += g_pData->m_NumSounds;
876
877 m_IsInit = true;
878
879 // load menu images
880 m_vMenuImages.clear();
881 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "menuimages", pfnCallback: MenuImageScan, pUser: this);
882
883 // load community icons
884 m_vCommunityIcons.clear();
885 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "communityicons", pfnCallback: CommunityIconScan, pUser: this);
886}
887
888void CMenus::OnConsoleInit()
889{
890 ConfigManager()->RegisterCallback(pfnFunc: CMenus::ConfigSaveCallback, pUserData: this);
891 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");
892 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");
893}
894
895void CMenus::ConchainBackgroundEntities(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
896{
897 pfnCallback(pResult, pCallbackUserData);
898 if(pResult->NumArguments())
899 {
900 CMenus *pSelf = (CMenus *)pUserData;
901 pSelf->UpdateBackgroundEntities();
902 }
903}
904
905void CMenus::UpdateBackgroundEntities()
906{
907 if(str_comp(a: g_Config.m_ClBackgroundEntities, b: m_pClient->m_Background.MapName()) != 0)
908 m_pClient->m_Background.LoadBackground();
909}
910
911void CMenus::ConchainUpdateMusicState(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
912{
913 pfnCallback(pResult, pCallbackUserData);
914 auto *pSelf = (CMenus *)pUserData;
915 if(pResult->NumArguments())
916 pSelf->UpdateMusicState();
917}
918
919void CMenus::UpdateMusicState()
920{
921 const bool ShouldPlay = Client()->State() == IClient::STATE_OFFLINE && g_Config.m_SndEnable && g_Config.m_SndMusic;
922 if(ShouldPlay && !m_pClient->m_Sounds.IsPlaying(SetId: SOUND_MENU))
923 m_pClient->m_Sounds.Enqueue(Channel: CSounds::CHN_MUSIC, SetId: SOUND_MENU);
924 else if(!ShouldPlay && m_pClient->m_Sounds.IsPlaying(SetId: SOUND_MENU))
925 m_pClient->m_Sounds.Stop(SetId: SOUND_MENU);
926}
927
928void CMenus::PopupMessage(const char *pTitle, const char *pMessage, const char *pButtonLabel, int NextPopup, FPopupButtonCallback pfnButtonCallback)
929{
930 // reset active item
931 Ui()->SetActiveItem(nullptr);
932
933 str_copy(dst&: m_aPopupTitle, src: pTitle);
934 str_copy(dst&: m_aPopupMessage, src: pMessage);
935 str_copy(dst&: m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, src: pButtonLabel);
936 m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup = NextPopup;
937 m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback = pfnButtonCallback;
938 m_Popup = POPUP_MESSAGE;
939}
940
941void CMenus::PopupConfirm(const char *pTitle, const char *pMessage, const char *pConfirmButtonLabel, const char *pCancelButtonLabel,
942 FPopupButtonCallback pfnConfirmButtonCallback, int ConfirmNextPopup, FPopupButtonCallback pfnCancelButtonCallback, int CancelNextPopup)
943{
944 // reset active item
945 Ui()->SetActiveItem(nullptr);
946
947 str_copy(dst&: m_aPopupTitle, src: pTitle);
948 str_copy(dst&: m_aPopupMessage, src: pMessage);
949 str_copy(dst&: m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, src: pConfirmButtonLabel);
950 m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup = ConfirmNextPopup;
951 m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback = pfnConfirmButtonCallback;
952 str_copy(dst&: m_aPopupButtons[BUTTON_CANCEL].m_aLabel, src: pCancelButtonLabel);
953 m_aPopupButtons[BUTTON_CANCEL].m_NextPopup = CancelNextPopup;
954 m_aPopupButtons[BUTTON_CANCEL].m_pfnCallback = pfnCancelButtonCallback;
955 m_Popup = POPUP_CONFIRM;
956}
957
958void CMenus::PopupWarning(const char *pTopic, const char *pBody, const char *pButton, std::chrono::nanoseconds Duration)
959{
960 // no multiline support for console
961 std::string BodyStr = pBody;
962 while(BodyStr.find(c: '\n') != std::string::npos)
963 BodyStr.replace(pos: BodyStr.find(c: '\n'), n1: 1, s: " ");
964 dbg_msg(sys: pTopic, fmt: "%s", BodyStr.c_str());
965
966 // reset active item
967 Ui()->SetActiveItem(nullptr);
968
969 str_copy(dst&: m_aMessageTopic, src: pTopic);
970 str_copy(dst&: m_aMessageBody, src: pBody);
971 str_copy(dst&: m_aMessageButton, src: pButton);
972 m_Popup = POPUP_WARNING;
973 SetActive(true);
974
975 m_PopupWarningDuration = Duration;
976 m_PopupWarningLastTime = time_get_nanoseconds();
977}
978
979bool CMenus::CanDisplayWarning() const
980{
981 return m_Popup == POPUP_NONE;
982}
983
984void CMenus::Render()
985{
986 Ui()->MapScreen();
987 Ui()->ResetMouseSlow();
988
989 static int s_Frame = 0;
990 if(s_Frame == 0)
991 {
992 RefreshBrowserTab(Force: true);
993 s_Frame++;
994 }
995 else if(s_Frame == 1)
996 {
997 UpdateMusicState();
998 s_Frame++;
999 }
1000 else
1001 {
1002 UpdateCommunityIcons();
1003 }
1004
1005 if(ServerBrowser()->DDNetInfoAvailable())
1006 {
1007 // Initially add DDNet as favorite community and select its tab.
1008 // This must be delayed until the DDNet info is available.
1009 if(m_CreateDefaultFavoriteCommunities)
1010 {
1011 m_CreateDefaultFavoriteCommunities = false;
1012 if(ServerBrowser()->Community(pCommunityId: IServerBrowser::COMMUNITY_DDNET) != nullptr)
1013 {
1014 ServerBrowser()->FavoriteCommunitiesFilter().Clear();
1015 ServerBrowser()->FavoriteCommunitiesFilter().Add(pElement: IServerBrowser::COMMUNITY_DDNET);
1016 SetMenuPage(PAGE_FAVORITE_COMMUNITY_1);
1017 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_FAVORITE_COMMUNITY_1);
1018 }
1019 }
1020
1021 if(m_JoinTutorial && m_Popup == POPUP_NONE && !ServerBrowser()->IsGettingServerlist())
1022 {
1023 m_JoinTutorial = false;
1024 // This is only reached on first launch, when the DDNet community tab has been created and
1025 // activated by default, so the server info for the tutorial server should be available.
1026 const char *pAddr = ServerBrowser()->GetTutorialServer();
1027 if(pAddr)
1028 {
1029 Client()->Connect(pAddress: pAddr);
1030 }
1031 }
1032 }
1033
1034 // Determine the client state once before rendering because it can change
1035 // while rendering which causes frames with broken user interface.
1036 const IClient::EClientState ClientState = Client()->State();
1037
1038 if(ClientState == IClient::STATE_ONLINE || ClientState == IClient::STATE_DEMOPLAYBACK)
1039 {
1040 ms_ColorTabbarInactive = ms_ColorTabbarInactiveIngame;
1041 ms_ColorTabbarActive = ms_ColorTabbarActiveIngame;
1042 ms_ColorTabbarHover = ms_ColorTabbarHoverIngame;
1043 }
1044 else
1045 {
1046 if(!GameClient()->m_MenuBackground.Render())
1047 {
1048 RenderBackground();
1049 }
1050 ms_ColorTabbarInactive = ms_ColorTabbarInactiveOutgame;
1051 ms_ColorTabbarActive = ms_ColorTabbarActiveOutgame;
1052 ms_ColorTabbarHover = ms_ColorTabbarHoverOutgame;
1053 }
1054
1055 CUIRect Screen = *Ui()->Screen();
1056 if(Client()->State() != IClient::STATE_DEMOPLAYBACK || m_Popup != POPUP_NONE)
1057 {
1058 Screen.Margin(Cut: 10.0f, pOtherRect: &Screen);
1059 }
1060
1061 switch(ClientState)
1062 {
1063 case IClient::STATE_QUITTING:
1064 case IClient::STATE_RESTARTING:
1065 // Render nothing except menu background. This should not happen for more than one frame.
1066 return;
1067
1068 case IClient::STATE_CONNECTING:
1069 RenderPopupConnecting(Screen);
1070 break;
1071
1072 case IClient::STATE_LOADING:
1073 RenderPopupLoading(Screen);
1074 break;
1075
1076 case IClient::STATE_OFFLINE:
1077 if(m_Popup != POPUP_NONE)
1078 {
1079 RenderPopupFullscreen(Screen);
1080 }
1081 else if(m_ShowStart)
1082 {
1083 RenderStartMenu(MainView: Screen);
1084 }
1085 else
1086 {
1087 CUIRect TabBar, MainView;
1088 Screen.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &MainView);
1089
1090 if(m_MenuPage == PAGE_NEWS)
1091 {
1092 RenderNews(MainView);
1093 }
1094 else if(m_MenuPage >= PAGE_INTERNET && m_MenuPage <= PAGE_FAVORITE_COMMUNITY_5)
1095 {
1096 RenderServerbrowser(MainView);
1097 }
1098 else if(m_MenuPage == PAGE_DEMOS)
1099 {
1100 RenderDemoBrowser(MainView);
1101 }
1102 else if(m_MenuPage == PAGE_SETTINGS)
1103 {
1104 RenderSettings(MainView);
1105 }
1106 else
1107 {
1108 dbg_assert(false, "m_MenuPage invalid");
1109 }
1110
1111 RenderMenubar(Box: TabBar, ClientState);
1112 }
1113 break;
1114
1115 case IClient::STATE_ONLINE:
1116 if(m_Popup != POPUP_NONE)
1117 {
1118 RenderPopupFullscreen(Screen);
1119 }
1120 else
1121 {
1122 CUIRect TabBar, MainView;
1123 Screen.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &MainView);
1124
1125 if(m_GamePage == PAGE_GAME)
1126 {
1127 RenderGame(MainView);
1128 RenderIngameHint();
1129 }
1130 else if(m_GamePage == PAGE_PLAYERS)
1131 {
1132 RenderPlayers(MainView);
1133 }
1134 else if(m_GamePage == PAGE_SERVER_INFO)
1135 {
1136 RenderServerInfo(MainView);
1137 }
1138 else if(m_GamePage == PAGE_NETWORK)
1139 {
1140 RenderInGameNetwork(MainView);
1141 }
1142 else if(m_GamePage == PAGE_GHOST)
1143 {
1144 RenderGhost(MainView);
1145 }
1146 else if(m_GamePage == PAGE_CALLVOTE)
1147 {
1148 RenderServerControl(MainView);
1149 }
1150 else if(m_GamePage == PAGE_SETTINGS)
1151 {
1152 RenderSettings(MainView);
1153 }
1154 else
1155 {
1156 dbg_assert(false, "m_GamePage invalid");
1157 }
1158
1159 RenderMenubar(Box: TabBar, ClientState);
1160 }
1161 break;
1162
1163 case IClient::STATE_DEMOPLAYBACK:
1164 if(m_Popup != POPUP_NONE)
1165 {
1166 RenderPopupFullscreen(Screen);
1167 }
1168 else
1169 {
1170 RenderDemoPlayer(MainView: Screen);
1171 }
1172 break;
1173 }
1174
1175 Ui()->RenderPopupMenus();
1176
1177 // Prevent UI elements from being hovered while a key reader is active
1178 if(m_Binder.m_TakeKey)
1179 {
1180 Ui()->SetHotItem(nullptr);
1181 }
1182
1183 // Handle this escape hotkey after popup menus
1184 if(!m_ShowStart && ClientState == IClient::STATE_OFFLINE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1185 {
1186 m_ShowStart = true;
1187 }
1188}
1189
1190void CMenus::RenderPopupFullscreen(CUIRect Screen)
1191{
1192 char aBuf[1536];
1193 const char *pTitle = "";
1194 const char *pExtraText = "";
1195 const char *pButtonText = "";
1196 bool TopAlign = false;
1197
1198 ColorRGBA BgColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f);
1199 if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM)
1200 {
1201 pTitle = m_aPopupTitle;
1202 pExtraText = m_aPopupMessage;
1203 TopAlign = true;
1204 }
1205 else if(m_Popup == POPUP_DISCONNECTED)
1206 {
1207 pTitle = Localize(pStr: "Disconnected");
1208 pExtraText = Client()->ErrorString();
1209 pButtonText = Localize(pStr: "Ok");
1210 if(Client()->ReconnectTime() > 0)
1211 {
1212 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Reconnect in %d sec"), (int)((Client()->ReconnectTime() - time_get()) / time_freq()));
1213 pTitle = Client()->ErrorString();
1214 pExtraText = aBuf;
1215 pButtonText = Localize(pStr: "Abort");
1216 }
1217 }
1218 else if(m_Popup == POPUP_RENAME_DEMO)
1219 {
1220 dbg_assert(m_DemolistSelectedIndex >= 0, "m_DemolistSelectedIndex invalid for POPUP_RENAME_DEMO");
1221 pTitle = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Rename folder") : Localize(pStr: "Rename demo");
1222 }
1223#if defined(CONF_VIDEORECORDER)
1224 else if(m_Popup == POPUP_RENDER_DEMO)
1225 {
1226 pTitle = Localize(pStr: "Render demo");
1227 }
1228 else if(m_Popup == POPUP_RENDER_DONE)
1229 {
1230 pTitle = Localize(pStr: "Render complete");
1231 }
1232#endif
1233 else if(m_Popup == POPUP_PASSWORD)
1234 {
1235 pTitle = Localize(pStr: "Password incorrect");
1236 pButtonText = Localize(pStr: "Try again");
1237 }
1238 else if(m_Popup == POPUP_RESTART)
1239 {
1240 pTitle = Localize(pStr: "Restart");
1241 pExtraText = Localize(pStr: "Are you sure that you want to restart?");
1242 }
1243 else if(m_Popup == POPUP_QUIT)
1244 {
1245 pTitle = Localize(pStr: "Quit");
1246 pExtraText = Localize(pStr: "Are you sure that you want to quit?");
1247 }
1248 else if(m_Popup == POPUP_FIRST_LAUNCH)
1249 {
1250 pTitle = Localize(pStr: "Welcome to DDNet");
1251 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s\n\n%s\n\n%s\n\n%s",
1252 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."),
1253 Localize(pStr: "Use k key to kill (restart), q to pause and watch other players. See settings for other key binds."),
1254 Localize(pStr: "It's recommended that you check the settings to adjust them to your liking before joining a server."),
1255 Localize(pStr: "Please enter your nickname below."));
1256 pExtraText = aBuf;
1257 pButtonText = Localize(pStr: "Ok");
1258 TopAlign = true;
1259 }
1260 else if(m_Popup == POPUP_POINTS)
1261 {
1262 pTitle = Localize(pStr: "Existing Player");
1263 if(Client()->Points() > 50)
1264 {
1265 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());
1266 pExtraText = aBuf;
1267 TopAlign = true;
1268 }
1269 else if(Client()->Points() >= 0)
1270 {
1271 m_Popup = POPUP_NONE;
1272 }
1273 else
1274 {
1275 pExtraText = Localize(pStr: "Checking for existing player with your name");
1276 }
1277 }
1278 else if(m_Popup == POPUP_WARNING)
1279 {
1280 BgColor = ColorRGBA(0.5f, 0.0f, 0.0f, 0.7f);
1281 pTitle = m_aMessageTopic;
1282 pExtraText = m_aMessageBody;
1283 pButtonText = m_aMessageButton;
1284 TopAlign = true;
1285 }
1286
1287 CUIRect Box, Part;
1288 Box = Screen;
1289 if(m_Popup != POPUP_FIRST_LAUNCH)
1290 Box.Margin(Cut: 150.0f, pOtherRect: &Box);
1291
1292 // render the box
1293 Box.Draw(Color: BgColor, Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
1294
1295 Box.HSplitTop(Cut: 20.f, pTop: &Part, pBottom: &Box);
1296 Box.HSplitTop(Cut: 24.f, pTop: &Part, pBottom: &Box);
1297 Part.VMargin(Cut: 20.f, pOtherRect: &Part);
1298 SLabelProperties Props;
1299 Props.m_MaxWidth = (int)Part.w;
1300
1301 if(TextRender()->TextWidth(Size: 24.f, pText: pTitle, StrLength: -1, LineWidth: -1.0f) > Part.w)
1302 Ui()->DoLabel(pRect: &Part, pText: pTitle, Size: 24.f, Align: TEXTALIGN_ML, LabelProps: Props);
1303 else
1304 Ui()->DoLabel(pRect: &Part, pText: pTitle, Size: 24.f, Align: TEXTALIGN_MC);
1305
1306 Box.HSplitTop(Cut: 20.f, pTop: &Part, pBottom: &Box);
1307 Box.HSplitTop(Cut: 24.f, pTop: &Part, pBottom: &Box);
1308 Part.VMargin(Cut: 20.f, pOtherRect: &Part);
1309
1310 float FontSize = m_Popup == POPUP_FIRST_LAUNCH ? 16.0f : 20.f;
1311
1312 Props.m_MaxWidth = (int)Part.w;
1313 if(TopAlign)
1314 Ui()->DoLabel(pRect: &Part, pText: pExtraText, Size: FontSize, Align: TEXTALIGN_TL, LabelProps: Props);
1315 else if(TextRender()->TextWidth(Size: FontSize, pText: pExtraText, StrLength: -1, LineWidth: -1.0f) > Part.w)
1316 Ui()->DoLabel(pRect: &Part, pText: pExtraText, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props);
1317 else
1318 Ui()->DoLabel(pRect: &Part, pText: pExtraText, Size: FontSize, Align: TEXTALIGN_MC);
1319
1320 if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM)
1321 {
1322 CUIRect ButtonBar;
1323 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1324 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &ButtonBar);
1325 ButtonBar.VMargin(Cut: 100.0f, pOtherRect: &ButtonBar);
1326
1327 if(m_Popup == POPUP_MESSAGE)
1328 {
1329 static CButtonContainer s_ButtonConfirm;
1330 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))
1331 {
1332 m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup;
1333 (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)();
1334 }
1335 }
1336 else if(m_Popup == POPUP_CONFIRM)
1337 {
1338 CUIRect CancelButton, ConfirmButton;
1339 ButtonBar.VSplitMid(pLeft: &CancelButton, pRight: &ConfirmButton, Spacing: 40.0f);
1340
1341 static CButtonContainer s_ButtonCancel;
1342 if(DoButton_Menu(pButtonContainer: &s_ButtonCancel, pText: m_aPopupButtons[BUTTON_CANCEL].m_aLabel, Checked: 0, pRect: &CancelButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1343 {
1344 m_Popup = m_aPopupButtons[BUTTON_CANCEL].m_NextPopup;
1345 (this->*m_aPopupButtons[BUTTON_CANCEL].m_pfnCallback)();
1346 }
1347
1348 static CButtonContainer s_ButtonConfirm;
1349 if(DoButton_Menu(pButtonContainer: &s_ButtonConfirm, pText: m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, Checked: 0, pRect: &ConfirmButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1350 {
1351 m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup;
1352 (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)();
1353 }
1354 }
1355 }
1356 else if(m_Popup == POPUP_QUIT || m_Popup == POPUP_RESTART)
1357 {
1358 CUIRect Yes, No;
1359 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1360 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1361
1362 // additional info
1363 Box.VMargin(Cut: 20.f, pOtherRect: &Box);
1364 if(m_pClient->Editor()->HasUnsavedData())
1365 {
1366 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?"));
1367 Props.m_MaxWidth = Part.w - 20.0f;
1368 Ui()->DoLabel(pRect: &Box, pText: aBuf, Size: 20.f, Align: TEXTALIGN_ML, LabelProps: Props);
1369 }
1370
1371 // buttons
1372 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1373 Part.VSplitMid(pLeft: &No, pRight: &Yes);
1374 Yes.VMargin(Cut: 20.0f, pOtherRect: &Yes);
1375 No.VMargin(Cut: 20.0f, pOtherRect: &No);
1376
1377 static CButtonContainer s_ButtonAbort;
1378 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "No"), Checked: 0, pRect: &No) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1379 m_Popup = POPUP_NONE;
1380
1381 static CButtonContainer s_ButtonTryAgain;
1382 if(DoButton_Menu(pButtonContainer: &s_ButtonTryAgain, pText: Localize(pStr: "Yes"), Checked: 0, pRect: &Yes) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1383 {
1384 if(m_Popup == POPUP_RESTART)
1385 {
1386 m_Popup = POPUP_NONE;
1387 Client()->Restart();
1388 }
1389 else
1390 {
1391 m_Popup = POPUP_NONE;
1392 Client()->Quit();
1393 }
1394 }
1395 }
1396 else if(m_Popup == POPUP_PASSWORD)
1397 {
1398 CUIRect Label, TextBox, TryAgain, Abort;
1399
1400 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1401 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1402 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1403
1404 Part.VSplitMid(pLeft: &Abort, pRight: &TryAgain);
1405
1406 TryAgain.VMargin(Cut: 20.0f, pOtherRect: &TryAgain);
1407 Abort.VMargin(Cut: 20.0f, pOtherRect: &Abort);
1408
1409 static CButtonContainer s_ButtonAbort;
1410 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Abort) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1411 m_Popup = POPUP_NONE;
1412
1413 static CButtonContainer s_ButtonTryAgain;
1414 if(DoButton_Menu(pButtonContainer: &s_ButtonTryAgain, pText: Localize(pStr: "Try again"), Checked: 0, pRect: &TryAgain) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1415 {
1416 char aAddr[NETADDR_MAXSTRSIZE];
1417 net_addr_str(addr: &Client()->ServerAddress(), string: aAddr, max_length: sizeof(aAddr), add_port: true);
1418 Client()->Connect(pAddress: aAddr, pPassword: g_Config.m_Password);
1419 }
1420
1421 Box.HSplitBottom(Cut: 60.f, pTop: &Box, pBottom: &Part);
1422 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1423
1424 Part.VSplitLeft(Cut: 60.0f, pLeft: 0, pRight: &Label);
1425 Label.VSplitLeft(Cut: 100.0f, pLeft: 0, pRight: &TextBox);
1426 TextBox.VSplitLeft(Cut: 20.0f, pLeft: 0, pRight: &TextBox);
1427 TextBox.VSplitRight(Cut: 60.0f, pLeft: &TextBox, pRight: 0);
1428 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Password"), Size: 18.0f, Align: TEXTALIGN_ML);
1429 Ui()->DoClearableEditBox(pLineInput: &m_PasswordInput, pRect: &TextBox, FontSize: 12.0f);
1430 }
1431 else if(m_Popup == POPUP_LANGUAGE)
1432 {
1433 CUIRect Button;
1434 Screen.Margin(Cut: 150.0f, pOtherRect: &Box);
1435 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1436 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1437 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Button);
1438 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: nullptr);
1439 Box.VMargin(Cut: 20.0f, pOtherRect: &Box);
1440 const bool Activated = RenderLanguageSelection(MainView: Box);
1441 Button.VMargin(Cut: 120.0f, pOtherRect: &Button);
1442
1443 static CButtonContainer s_Button;
1444 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)
1445 m_Popup = POPUP_FIRST_LAUNCH;
1446 }
1447 else if(m_Popup == POPUP_RENAME_DEMO)
1448 {
1449 CUIRect Label, TextBox, Ok, Abort;
1450
1451 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1452 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1453 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1454
1455 Part.VSplitMid(pLeft: &Abort, pRight: &Ok);
1456
1457 Ok.VMargin(Cut: 20.0f, pOtherRect: &Ok);
1458 Abort.VMargin(Cut: 20.0f, pOtherRect: &Abort);
1459
1460 static CButtonContainer s_ButtonAbort;
1461 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Abort) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1462 m_Popup = POPUP_NONE;
1463
1464 static CButtonContainer s_ButtonOk;
1465 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &Ok) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1466 {
1467 m_Popup = POPUP_NONE;
1468 // rename demo
1469 char aBufOld[IO_MAX_PATH_LENGTH];
1470 str_format(buffer: aBufOld, buffer_size: sizeof(aBufOld), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1471 char aBufNew[IO_MAX_PATH_LENGTH];
1472 str_format(buffer: aBufNew, buffer_size: sizeof(aBufNew), format: "%s/%s", m_aCurrentDemoFolder, m_DemoRenameInput.GetString());
1473 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir && !str_endswith(str: aBufNew, suffix: ".demo"))
1474 str_append(dst&: aBufNew, src: ".demo");
1475
1476 if(Storage()->FileExists(pFilename: aBufNew, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1477 {
1478 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "A demo with this name already exists"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENAME_DEMO);
1479 }
1480 else if(Storage()->FolderExists(pFilename: aBufNew, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1481 {
1482 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "A folder with this name already exists"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENAME_DEMO);
1483 }
1484 else if(Storage()->RenameFile(pOldFilename: aBufOld, pNewFilename: aBufNew, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1485 {
1486 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_DemoRenameInput.GetString());
1487 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1488 fs_split_file_extension(filename: m_DemoRenameInput.GetString(), name: m_aCurrentDemoSelectionName, name_size: sizeof(m_aCurrentDemoSelectionName));
1489 DemolistPopulate();
1490 DemolistOnUpdate(Reset: false);
1491 }
1492 else
1493 {
1494 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);
1495 }
1496 }
1497
1498 Box.HSplitBottom(Cut: 60.f, pTop: &Box, pBottom: &Part);
1499 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1500
1501 Part.VSplitLeft(Cut: 60.0f, pLeft: 0, pRight: &Label);
1502 Label.VSplitLeft(Cut: 120.0f, pLeft: 0, pRight: &TextBox);
1503 TextBox.VSplitLeft(Cut: 20.0f, pLeft: 0, pRight: &TextBox);
1504 TextBox.VSplitRight(Cut: 60.0f, pLeft: &TextBox, pRight: 0);
1505 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "New name:"), Size: 18.0f, Align: TEXTALIGN_ML);
1506 Ui()->DoEditBox(pLineInput: &m_DemoRenameInput, pRect: &TextBox, FontSize: 12.0f);
1507 }
1508#if defined(CONF_VIDEORECORDER)
1509 else if(m_Popup == POPUP_RENDER_DEMO)
1510 {
1511 CUIRect Row, Ok, Abort;
1512 Box.VMargin(Cut: 60.0f, pOtherRect: &Box);
1513 Box.HMargin(Cut: 20.0f, pOtherRect: &Box);
1514 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Row);
1515 Box.HSplitBottom(Cut: 40.0f, pTop: &Box, pBottom: nullptr);
1516 Row.VMargin(Cut: 40.0f, pOtherRect: &Row);
1517 Row.VSplitMid(pLeft: &Abort, pRight: &Ok, Spacing: 40.0f);
1518
1519 static CButtonContainer s_ButtonAbort;
1520 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Abort) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1521 {
1522 m_DemoRenderInput.Clear();
1523 m_Popup = POPUP_NONE;
1524 }
1525
1526 static CButtonContainer s_ButtonOk;
1527 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &Ok) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1528 {
1529 m_Popup = POPUP_NONE;
1530 // render video
1531 char aVideoPath[IO_MAX_PATH_LENGTH];
1532 str_format(buffer: aVideoPath, buffer_size: sizeof(aVideoPath), format: "videos/%s", m_DemoRenderInput.GetString());
1533 if(!str_endswith(str: aVideoPath, suffix: ".mp4"))
1534 str_append(dst&: aVideoPath, src: ".mp4");
1535 if(Storage()->FolderExists(pFilename: aVideoPath, Type: IStorage::TYPE_SAVE))
1536 {
1537 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: Localize(pStr: "A folder with this name already exists"), pButtonLabel: Localize(pStr: "Ok"), NextPopup: POPUP_RENDER_DEMO);
1538 }
1539 else if(Storage()->FileExists(pFilename: aVideoPath, Type: IStorage::TYPE_SAVE))
1540 {
1541 char aMessage[128 + IO_MAX_PATH_LENGTH];
1542 str_format(buffer: aMessage, buffer_size: sizeof(aMessage), format: Localize(pStr: "File '%s' already exists, do you want to overwrite it?"), m_DemoRenderInput.GetString());
1543 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);
1544 }
1545 else
1546 {
1547 PopupConfirmDemoReplaceVideo();
1548 }
1549 }
1550
1551 CUIRect ShowChatCheckbox, UseSoundsCheckbox;
1552 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: &Row);
1553 Box.HSplitBottom(Cut: 10.0f, pTop: &Box, pBottom: nullptr);
1554 Row.VSplitMid(pLeft: &ShowChatCheckbox, pRight: &UseSoundsCheckbox, Spacing: 20.0f);
1555
1556 if(DoButton_CheckBox(pId: &g_Config.m_ClVideoShowChat, pText: Localize(pStr: "Show chat"), Checked: g_Config.m_ClVideoShowChat, pRect: &ShowChatCheckbox))
1557 g_Config.m_ClVideoShowChat ^= 1;
1558
1559 if(DoButton_CheckBox(pId: &g_Config.m_ClVideoSndEnable, pText: Localize(pStr: "Use sounds"), Checked: g_Config.m_ClVideoSndEnable, pRect: &UseSoundsCheckbox))
1560 g_Config.m_ClVideoSndEnable ^= 1;
1561
1562 CUIRect ShowHudButton;
1563 Box.HSplitBottom(Cut: 20.0f, pTop: &Box, pBottom: &Row);
1564 Row.VSplitMid(pLeft: &Row, pRight: &ShowHudButton, Spacing: 20.0f);
1565
1566 if(DoButton_CheckBox(pId: &g_Config.m_ClVideoShowhud, pText: Localize(pStr: "Show ingame HUD"), Checked: g_Config.m_ClVideoShowhud, pRect: &ShowHudButton))
1567 g_Config.m_ClVideoShowhud ^= 1;
1568
1569 // slowdown
1570 CUIRect SlowDownButton;
1571 Row.VSplitLeft(Cut: 20.0f, pLeft: &SlowDownButton, pRight: &Row);
1572 Row.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Row);
1573 static CButtonContainer s_SlowDownButton;
1574 if(DoButton_FontIcon(pButtonContainer: &s_SlowDownButton, pText: FONT_ICON_BACKWARD, Checked: 0, pRect: &SlowDownButton, Corners: IGraphics::CORNER_ALL))
1575 m_Speed = clamp(val: m_Speed - 1, lo: 0, hi: (int)(g_DemoSpeeds - 1));
1576
1577 // paused
1578 CUIRect PausedButton;
1579 Row.VSplitLeft(Cut: 20.0f, pLeft: &PausedButton, pRight: &Row);
1580 Row.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Row);
1581 static CButtonContainer s_PausedButton;
1582 if(DoButton_FontIcon(pButtonContainer: &s_PausedButton, pText: FONT_ICON_PAUSE, Checked: 0, pRect: &PausedButton, Corners: IGraphics::CORNER_ALL))
1583 m_StartPaused ^= 1;
1584
1585 // fastforward
1586 CUIRect FastForwardButton;
1587 Row.VSplitLeft(Cut: 20.0f, pLeft: &FastForwardButton, pRight: &Row);
1588 Row.VSplitLeft(Cut: 8.0f, pLeft: nullptr, pRight: &Row);
1589 static CButtonContainer s_FastForwardButton;
1590 if(DoButton_FontIcon(pButtonContainer: &s_FastForwardButton, pText: FONT_ICON_FORWARD, Checked: 0, pRect: &FastForwardButton, Corners: IGraphics::CORNER_ALL))
1591 m_Speed = clamp(val: m_Speed + 1, lo: 0, hi: (int)(g_DemoSpeeds - 1));
1592
1593 // speed meter
1594 char aBuffer[128];
1595 const char *pPaused = m_StartPaused ? Localize(pStr: "(paused)") : "";
1596 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s: ×%g %s", Localize(pStr: "Speed"), g_aSpeeds[m_Speed], pPaused);
1597 Ui()->DoLabel(pRect: &Row, pText: aBuffer, Size: 12.8f, Align: TEXTALIGN_ML);
1598 Box.HSplitBottom(Cut: 16.0f, pTop: &Box, pBottom: nullptr);
1599 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Row);
1600
1601 CUIRect Label, TextBox;
1602 Row.VSplitLeft(Cut: 110.0f, pLeft: &Label, pRight: &TextBox);
1603 TextBox.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &TextBox);
1604 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Video name:"), Size: 12.8f, Align: TEXTALIGN_ML);
1605 Ui()->DoEditBox(pLineInput: &m_DemoRenderInput, pRect: &TextBox, FontSize: 12.8f);
1606 }
1607 else if(m_Popup == POPUP_RENDER_DONE)
1608 {
1609 CUIRect Ok, OpenFolder;
1610
1611 char aFilePath[IO_MAX_PATH_LENGTH];
1612 char aSaveFolder[IO_MAX_PATH_LENGTH];
1613 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "videos", pBuffer: aSaveFolder, BufferSize: sizeof(aSaveFolder));
1614 str_format(buffer: aFilePath, buffer_size: sizeof(aFilePath), format: "%s/%s.mp4", aSaveFolder, m_DemoRenderInput.GetString());
1615
1616 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1617 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1618 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1619
1620 Part.VSplitMid(pLeft: &OpenFolder, pRight: &Ok);
1621
1622 Ok.VMargin(Cut: 20.0f, pOtherRect: &Ok);
1623 OpenFolder.VMargin(Cut: 20.0f, pOtherRect: &OpenFolder);
1624
1625 static CButtonContainer s_ButtonOpenFolder;
1626 if(DoButton_Menu(pButtonContainer: &s_ButtonOpenFolder, pText: Localize(pStr: "Videos directory"), Checked: 0, pRect: &OpenFolder))
1627 {
1628 Client()->ViewFile(pFilename: aSaveFolder);
1629 }
1630
1631 static CButtonContainer s_ButtonOk;
1632 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &Ok) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1633 {
1634 m_Popup = POPUP_NONE;
1635 m_DemoRenderInput.Clear();
1636 }
1637
1638 Box.HSplitBottom(Cut: 160.f, pTop: &Box, pBottom: &Part);
1639 Part.VMargin(Cut: 20.0f, pOtherRect: &Part);
1640
1641 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Video was saved to '%s'"), aFilePath);
1642
1643 SLabelProperties MessageProps;
1644 MessageProps.m_MaxWidth = (int)Part.w;
1645 Ui()->DoLabel(pRect: &Part, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_TL, LabelProps: MessageProps);
1646 }
1647#endif
1648 else if(m_Popup == POPUP_FIRST_LAUNCH)
1649 {
1650 CUIRect Label, TextBox, Skip, Join;
1651
1652 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1653 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1654 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1655 Part.VSplitMid(pLeft: &Skip, pRight: &Join);
1656 Skip.VMargin(Cut: 20.0f, pOtherRect: &Skip);
1657 Join.VMargin(Cut: 20.0f, pOtherRect: &Join);
1658
1659 static CButtonContainer s_JoinTutorialButton;
1660 if(DoButton_Menu(pButtonContainer: &s_JoinTutorialButton, pText: Localize(pStr: "Join Tutorial Server"), Checked: 0, pRect: &Join) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1661 {
1662 m_JoinTutorial = true;
1663 Client()->RequestDDNetInfo();
1664 m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE;
1665 }
1666
1667 static CButtonContainer s_SkipTutorialButton;
1668 if(DoButton_Menu(pButtonContainer: &s_SkipTutorialButton, pText: Localize(pStr: "Skip Tutorial"), Checked: 0, pRect: &Skip) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1669 {
1670 m_JoinTutorial = false;
1671 Client()->RequestDDNetInfo();
1672 m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE;
1673 }
1674
1675 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1676 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1677
1678 Part.VSplitLeft(Cut: 30.0f, pLeft: 0, pRight: &Part);
1679 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s\n(%s)",
1680 Localize(pStr: "Show DDNet map finishes in server browser"),
1681 Localize(pStr: "transmits your player name to info.ddnet.org"));
1682
1683 if(DoButton_CheckBox(pId: &g_Config.m_BrIndicateFinished, pText: aBuf, Checked: g_Config.m_BrIndicateFinished, pRect: &Part))
1684 g_Config.m_BrIndicateFinished ^= 1;
1685
1686 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1687 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1688
1689 Part.VSplitLeft(Cut: 60.0f, pLeft: 0, pRight: &Label);
1690 Label.VSplitLeft(Cut: 100.0f, pLeft: 0, pRight: &TextBox);
1691 TextBox.VSplitLeft(Cut: 20.0f, pLeft: 0, pRight: &TextBox);
1692 TextBox.VSplitRight(Cut: 60.0f, pLeft: &TextBox, pRight: 0);
1693 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Nickname"), Size: 16.0f, Align: TEXTALIGN_ML);
1694 static CLineInput s_PlayerNameInput(g_Config.m_PlayerName, sizeof(g_Config.m_PlayerName));
1695 s_PlayerNameInput.SetEmptyText(Client()->PlayerName());
1696 Ui()->DoEditBox(pLineInput: &s_PlayerNameInput, pRect: &TextBox, FontSize: 12.0f);
1697 }
1698 else if(m_Popup == POPUP_POINTS)
1699 {
1700 CUIRect Yes, No;
1701
1702 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1703 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1704 Part.VMargin(Cut: 80.0f, pOtherRect: &Part);
1705
1706 Part.VSplitMid(pLeft: &No, pRight: &Yes);
1707
1708 Yes.VMargin(Cut: 20.0f, pOtherRect: &Yes);
1709 No.VMargin(Cut: 20.0f, pOtherRect: &No);
1710
1711 static CButtonContainer s_ButtonNo;
1712 if(DoButton_Menu(pButtonContainer: &s_ButtonNo, pText: Localize(pStr: "No"), Checked: 0, pRect: &No) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1713 m_Popup = POPUP_FIRST_LAUNCH;
1714
1715 static CButtonContainer s_ButtonYes;
1716 if(DoButton_Menu(pButtonContainer: &s_ButtonYes, pText: Localize(pStr: "Yes"), Checked: 0, pRect: &Yes) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1717 m_Popup = POPUP_NONE;
1718 }
1719 else if(m_Popup == POPUP_WARNING)
1720 {
1721 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1722 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1723 Part.VMargin(Cut: 120.0f, pOtherRect: &Part);
1724
1725 static CButtonContainer s_Button;
1726 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))
1727 {
1728 m_Popup = POPUP_NONE;
1729 SetActive(false);
1730 }
1731 }
1732 else
1733 {
1734 Box.HSplitBottom(Cut: 20.f, pTop: &Box, pBottom: &Part);
1735 Box.HSplitBottom(Cut: 24.f, pTop: &Box, pBottom: &Part);
1736 Part.VMargin(Cut: 120.0f, pOtherRect: &Part);
1737
1738 static CButtonContainer s_Button;
1739 if(DoButton_Menu(pButtonContainer: &s_Button, pText: pButtonText, Checked: 0, pRect: &Part) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))
1740 {
1741 if(m_Popup == POPUP_DISCONNECTED && Client()->ReconnectTime() > 0)
1742 Client()->SetReconnectTime(0);
1743 m_Popup = POPUP_NONE;
1744 }
1745 }
1746
1747 if(m_Popup == POPUP_NONE)
1748 Ui()->SetActiveItem(nullptr);
1749}
1750
1751void CMenus::RenderPopupConnecting(CUIRect Screen)
1752{
1753 const float FontSize = 20.0f;
1754
1755 CUIRect Box, Label;
1756 Screen.Margin(Cut: 150.0f, pOtherRect: &Box);
1757 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
1758 Box.Margin(Cut: 20.0f, pOtherRect: &Box);
1759
1760 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
1761 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Connecting to"), Size: 24.0f, Align: TEXTALIGN_MC);
1762
1763 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1764 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
1765 SLabelProperties Props;
1766 Props.m_MaxWidth = Label.w;
1767 Props.m_EllipsisAtEnd = true;
1768 Ui()->DoLabel(pRect: &Label, pText: Client()->ConnectAddressString(), Size: FontSize, Align: TEXTALIGN_MC, LabelProps: Props);
1769
1770 if(time_get() - Client()->StateStartTime() > time_freq())
1771 {
1772 const char *pConnectivityLabel = "";
1773 switch(Client()->UdpConnectivity(NetType: Client()->ConnectNetTypes()))
1774 {
1775 case IClient::CONNECTIVITY_UNKNOWN:
1776 break;
1777 case IClient::CONNECTIVITY_CHECKING:
1778 pConnectivityLabel = Localize(pStr: "Trying to determine UDP connectivity…");
1779 break;
1780 case IClient::CONNECTIVITY_UNREACHABLE:
1781 pConnectivityLabel = Localize(pStr: "UDP seems to be filtered.");
1782 break;
1783 case IClient::CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES:
1784 pConnectivityLabel = Localize(pStr: "UDP and TCP IP addresses seem to be different. Try disabling VPN, proxy or network accelerators.");
1785 break;
1786 case IClient::CONNECTIVITY_REACHABLE:
1787 pConnectivityLabel = Localize(pStr: "No answer from server yet.");
1788 break;
1789 }
1790 if(pConnectivityLabel[0] != '\0')
1791 {
1792 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1793 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
1794 SLabelProperties ConnectivityLabelProps;
1795 ConnectivityLabelProps.m_MaxWidth = Label.w;
1796 if(TextRender()->TextWidth(Size: FontSize, pText: pConnectivityLabel) > Label.w)
1797 Ui()->DoLabel(pRect: &Label, pText: pConnectivityLabel, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: ConnectivityLabelProps);
1798 else
1799 Ui()->DoLabel(pRect: &Label, pText: pConnectivityLabel, Size: FontSize, Align: TEXTALIGN_MC);
1800 }
1801 }
1802
1803 CUIRect Button;
1804 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Button);
1805 Button.VMargin(Cut: 100.0f, pOtherRect: &Button);
1806
1807 static CButtonContainer s_Button;
1808 if(DoButton_Menu(pButtonContainer: &s_Button, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Button) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1809 {
1810 Client()->Disconnect();
1811 Ui()->SetActiveItem(nullptr);
1812 RefreshBrowserTab(Force: true);
1813 }
1814}
1815
1816void CMenus::RenderPopupLoading(CUIRect Screen)
1817{
1818 char aTitle[256];
1819 char aLabel1[128];
1820 char aLabel2[128];
1821 if(Client()->MapDownloadTotalsize() > 0)
1822 {
1823 const int64_t Now = time_get();
1824 if(Now - m_DownloadLastCheckTime >= time_freq())
1825 {
1826 if(m_DownloadLastCheckSize > Client()->MapDownloadAmount())
1827 {
1828 // map downloaded restarted
1829 m_DownloadLastCheckSize = 0;
1830 }
1831
1832 // update download speed
1833 const float Diff = (Client()->MapDownloadAmount() - m_DownloadLastCheckSize) / ((int)((Now - m_DownloadLastCheckTime) / time_freq()));
1834 const float StartDiff = m_DownloadLastCheckSize - 0.0f;
1835 if(StartDiff + Diff > 0.0f)
1836 m_DownloadSpeed = (Diff / (StartDiff + Diff)) * (Diff / 1.0f) + (StartDiff / (Diff + StartDiff)) * m_DownloadSpeed;
1837 else
1838 m_DownloadSpeed = 0.0f;
1839 m_DownloadLastCheckTime = Now;
1840 m_DownloadLastCheckSize = Client()->MapDownloadAmount();
1841 }
1842
1843 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: "%s: %s", Localize(pStr: "Downloading map"), Client()->MapDownloadName());
1844
1845 str_format(buffer: aLabel1, buffer_size: sizeof(aLabel1), format: Localize(pStr: "%d/%d KiB (%.1f KiB/s)"), Client()->MapDownloadAmount() / 1024, Client()->MapDownloadTotalsize() / 1024, m_DownloadSpeed / 1024.0f);
1846
1847 const int SecondsLeft = maximum(a: 1, b: m_DownloadSpeed > 0.0f ? static_cast<int>((Client()->MapDownloadTotalsize() - Client()->MapDownloadAmount()) / m_DownloadSpeed) : 1);
1848 const int MinutesLeft = SecondsLeft / 60;
1849 if(MinutesLeft > 0)
1850 {
1851 str_format(buffer: aLabel2, buffer_size: sizeof(aLabel2), format: MinutesLeft == 1 ? Localize(pStr: "%i minute left") : Localize(pStr: "%i minutes left"), MinutesLeft);
1852 }
1853 else
1854 {
1855 str_format(buffer: aLabel2, buffer_size: sizeof(aLabel2), format: SecondsLeft == 1 ? Localize(pStr: "%i second left") : Localize(pStr: "%i seconds left"), SecondsLeft);
1856 }
1857 }
1858 else
1859 {
1860 str_copy(dst&: aTitle, src: Localize(pStr: "Connected"));
1861 switch(Client()->LoadingStateDetail())
1862 {
1863 case IClient::LOADING_STATE_DETAIL_INITIAL:
1864 str_copy(dst&: aLabel1, src: Localize(pStr: "Getting game info"));
1865 break;
1866 case IClient::LOADING_STATE_DETAIL_LOADING_MAP:
1867 str_copy(dst&: aLabel1, src: Localize(pStr: "Loading map file from storage"));
1868 break;
1869 case IClient::LOADING_STATE_DETAIL_LOADING_DEMO:
1870 str_copy(dst&: aLabel1, src: Localize(pStr: "Loading demo file from storage"));
1871 break;
1872 case IClient::LOADING_STATE_DETAIL_SENDING_READY:
1873 str_copy(dst&: aLabel1, src: Localize(pStr: "Requesting to join the game"));
1874 break;
1875 case IClient::LOADING_STATE_DETAIL_GETTING_READY:
1876 str_copy(dst&: aLabel1, src: Localize(pStr: "Sending initial client info"));
1877 break;
1878 default:
1879 dbg_assert(false, "Invalid loading state for RenderPopupLoading");
1880 break;
1881 }
1882 aLabel2[0] = '\0';
1883 }
1884
1885 const float FontSize = 20.0f;
1886
1887 CUIRect Box, Label;
1888 Screen.Margin(Cut: 150.0f, pOtherRect: &Box);
1889 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
1890 Box.Margin(Cut: 20.0f, pOtherRect: &Box);
1891
1892 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
1893 Ui()->DoLabel(pRect: &Label, pText: aTitle, Size: 24.0f, Align: TEXTALIGN_MC);
1894
1895 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1896 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
1897 Ui()->DoLabel(pRect: &Label, pText: aLabel1, Size: FontSize, Align: TEXTALIGN_MC);
1898
1899 if(aLabel2[0] != '\0')
1900 {
1901 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1902 Box.HSplitTop(Cut: 24.0f, pTop: &Label, pBottom: &Box);
1903 SLabelProperties ExtraTextProps;
1904 ExtraTextProps.m_MaxWidth = Label.w;
1905 if(TextRender()->TextWidth(Size: FontSize, pText: aLabel2) > Label.w)
1906 Ui()->DoLabel(pRect: &Label, pText: aLabel2, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: ExtraTextProps);
1907 else
1908 Ui()->DoLabel(pRect: &Label, pText: aLabel2, Size: FontSize, Align: TEXTALIGN_MC);
1909 }
1910
1911 if(Client()->MapDownloadTotalsize() > 0)
1912 {
1913 CUIRect ProgressBar;
1914 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
1915 Box.HSplitTop(Cut: 24.0f, pTop: &ProgressBar, pBottom: &Box);
1916 ProgressBar.VMargin(Cut: 20.0f, pOtherRect: &ProgressBar);
1917 Ui()->RenderProgressBar(ProgressBar, Progress: Client()->MapDownloadAmount() / (float)Client()->MapDownloadTotalsize());
1918 }
1919
1920 CUIRect Button;
1921 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &Button);
1922 Button.VMargin(Cut: 100.0f, pOtherRect: &Button);
1923
1924 static CButtonContainer s_Button;
1925 if(DoButton_Menu(pButtonContainer: &s_Button, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &Button) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
1926 {
1927 Client()->Disconnect();
1928 Ui()->SetActiveItem(nullptr);
1929 RefreshBrowserTab(Force: true);
1930 }
1931}
1932
1933#if defined(CONF_VIDEORECORDER)
1934void CMenus::PopupConfirmDemoReplaceVideo()
1935{
1936 char aBuf[IO_MAX_PATH_LENGTH];
1937 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s.demo", m_aCurrentDemoFolder, m_aCurrentDemoSelectionName);
1938 char aVideoName[IO_MAX_PATH_LENGTH];
1939 str_copy(dst&: aVideoName, src: m_DemoRenderInput.GetString());
1940 const char *pError = Client()->DemoPlayer_Render(pFilename: aBuf, StorageType: m_DemolistStorageType, pVideoName: aVideoName, SpeedIndex: m_Speed, StartPaused: m_StartPaused);
1941 m_Speed = 4;
1942 m_StartPaused = false;
1943 m_LastPauseChange = -1.0f;
1944 m_LastSpeedChange = -1.0f;
1945 if(pError)
1946 {
1947 m_DemoRenderInput.Clear();
1948 PopupMessage(pTitle: Localize(pStr: "Error loading demo"), pMessage: pError, pButtonLabel: Localize(pStr: "Ok"));
1949 }
1950}
1951#endif
1952
1953void CMenus::RenderThemeSelection(CUIRect MainView)
1954{
1955 const std::vector<CTheme> &vThemes = GameClient()->m_MenuBackground.GetThemes();
1956
1957 int SelectedTheme = -1;
1958 for(int i = 0; i < (int)vThemes.size(); i++)
1959 {
1960 if(str_comp(a: vThemes[i].m_Name.c_str(), b: g_Config.m_ClMenuMap) == 0)
1961 {
1962 SelectedTheme = i;
1963 break;
1964 }
1965 }
1966 const int OldSelected = SelectedTheme;
1967
1968 static CListBox s_ListBox;
1969 s_ListBox.DoHeader(pRect: &MainView, pTitle: Localize(pStr: "Theme"), HeaderHeight: 20.0f);
1970 s_ListBox.DoStart(RowHeight: 20.0f, NumItems: vThemes.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: SelectedTheme);
1971
1972 for(int i = 0; i < (int)vThemes.size(); i++)
1973 {
1974 const CTheme &Theme = vThemes[i];
1975 const CListboxItem Item = s_ListBox.DoNextItem(pId: &Theme.m_Name, Selected: i == SelectedTheme);
1976
1977 if(!Item.m_Visible)
1978 continue;
1979
1980 CUIRect Icon, Label;
1981 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h * 2.0f, pLeft: &Icon, pRight: &Label);
1982
1983 // draw icon if it exists
1984 if(Theme.m_IconTexture.IsValid())
1985 {
1986 Icon.VMargin(Cut: 6.0f, pOtherRect: &Icon);
1987 Icon.HMargin(Cut: 3.0f, pOtherRect: &Icon);
1988 Graphics()->TextureSet(Texture: Theme.m_IconTexture);
1989 Graphics()->QuadsBegin();
1990 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1991 IGraphics::CQuadItem QuadItem(Icon.x, Icon.y, Icon.w, Icon.h);
1992 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
1993 Graphics()->QuadsEnd();
1994 }
1995
1996 char aName[128];
1997 if(Theme.m_Name.empty())
1998 str_copy(dst&: aName, src: "(none)");
1999 else if(str_comp(a: Theme.m_Name.c_str(), b: "auto") == 0)
2000 str_copy(dst&: aName, src: "(seasons)");
2001 else if(str_comp(a: Theme.m_Name.c_str(), b: "rand") == 0)
2002 str_copy(dst&: aName, src: "(random)");
2003 else if(Theme.m_HasDay && Theme.m_HasNight)
2004 str_copy(dst&: aName, src: Theme.m_Name.c_str());
2005 else if(Theme.m_HasDay && !Theme.m_HasNight)
2006 str_format(buffer: aName, buffer_size: sizeof(aName), format: "%s (day)", Theme.m_Name.c_str());
2007 else if(!Theme.m_HasDay && Theme.m_HasNight)
2008 str_format(buffer: aName, buffer_size: sizeof(aName), format: "%s (night)", Theme.m_Name.c_str());
2009 else // generic
2010 str_copy(dst&: aName, src: Theme.m_Name.c_str());
2011
2012 Ui()->DoLabel(pRect: &Label, pText: aName, Size: 16.0f * CUi::ms_FontmodHeight, Align: TEXTALIGN_ML);
2013 }
2014
2015 SelectedTheme = s_ListBox.DoEnd();
2016
2017 if(OldSelected != SelectedTheme)
2018 {
2019 const CTheme &Theme = vThemes[SelectedTheme];
2020 str_copy(dst&: g_Config.m_ClMenuMap, src: Theme.m_Name.c_str());
2021 GameClient()->m_MenuBackground.LoadMenuBackground(HasDayHint: Theme.m_HasDay, HasNightHint: Theme.m_HasNight);
2022 }
2023}
2024
2025void CMenus::SetActive(bool Active)
2026{
2027 if(Active != m_MenuActive)
2028 {
2029 Ui()->SetHotItem(nullptr);
2030 Ui()->SetActiveItem(nullptr);
2031 }
2032 m_MenuActive = Active;
2033 if(!m_MenuActive)
2034 {
2035 if(m_NeedSendinfo)
2036 {
2037 m_pClient->SendInfo(Start: false);
2038 m_NeedSendinfo = false;
2039 }
2040
2041 if(m_NeedSendDummyinfo)
2042 {
2043 m_pClient->SendDummyInfo(Start: false);
2044 m_NeedSendDummyinfo = false;
2045 }
2046
2047 if(Client()->State() == IClient::STATE_ONLINE)
2048 {
2049 m_pClient->OnRelease();
2050 }
2051 }
2052 else if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
2053 {
2054 m_pClient->OnRelease();
2055 }
2056}
2057
2058void CMenus::OnReset()
2059{
2060}
2061
2062void CMenus::OnShutdown()
2063{
2064 KillServer();
2065 m_CommunityIconLoadJobs.clear();
2066 m_CommunityIconDownloadJobs.clear();
2067}
2068
2069bool CMenus::OnCursorMove(float x, float y, IInput::ECursorType CursorType)
2070{
2071 if(!m_MenuActive)
2072 return false;
2073
2074 Ui()->ConvertMouseMove(pX: &x, pY: &y, CursorType);
2075 Ui()->OnCursorMove(X: x, Y: y);
2076
2077 return true;
2078}
2079
2080bool CMenus::OnInput(const IInput::CEvent &Event)
2081{
2082 // Escape key is always handled to activate/deactivate menu
2083 if((Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) || IsActive())
2084 {
2085 Ui()->OnInput(Event);
2086 return true;
2087 }
2088 return false;
2089}
2090
2091void CMenus::OnStateChange(int NewState, int OldState)
2092{
2093 // reset active item
2094 Ui()->SetActiveItem(nullptr);
2095
2096 if(OldState == IClient::STATE_ONLINE || OldState == IClient::STATE_OFFLINE)
2097 TextRender()->DeleteTextContainer(TextContainerIndex&: m_MotdTextContainerIndex);
2098
2099 if(NewState == IClient::STATE_OFFLINE)
2100 {
2101 if(OldState >= IClient::STATE_ONLINE && NewState < IClient::STATE_QUITTING)
2102 UpdateMusicState();
2103 m_Popup = POPUP_NONE;
2104 if(Client()->ErrorString() && Client()->ErrorString()[0] != 0)
2105 {
2106 if(str_find(haystack: Client()->ErrorString(), needle: "password"))
2107 {
2108 m_Popup = POPUP_PASSWORD;
2109 m_PasswordInput.SelectAll();
2110 Ui()->SetActiveItem(&m_PasswordInput);
2111 }
2112 else
2113 m_Popup = POPUP_DISCONNECTED;
2114 }
2115 }
2116 else if(NewState == IClient::STATE_LOADING)
2117 {
2118 m_DownloadLastCheckTime = time_get();
2119 m_DownloadLastCheckSize = 0;
2120 m_DownloadSpeed = 0.0f;
2121 }
2122 else if(NewState == IClient::STATE_ONLINE || NewState == IClient::STATE_DEMOPLAYBACK)
2123 {
2124 if(m_Popup != POPUP_WARNING)
2125 {
2126 m_Popup = POPUP_NONE;
2127 SetActive(false);
2128 }
2129 }
2130}
2131
2132void CMenus::OnWindowResize()
2133{
2134 TextRender()->DeleteTextContainer(TextContainerIndex&: m_MotdTextContainerIndex);
2135}
2136
2137void CMenus::OnRender()
2138{
2139 Ui()->StartCheck();
2140
2141 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
2142 SetActive(true);
2143
2144 if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_ServerMode == CGameClient::SERVERMODE_PUREMOD)
2145 {
2146 Client()->Disconnect();
2147 SetActive(true);
2148 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"));
2149 }
2150
2151 if(!IsActive())
2152 {
2153 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
2154 {
2155 SetActive(true);
2156 }
2157 else if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
2158 {
2159 Ui()->FinishCheck();
2160 Ui()->ClearHotkeys();
2161 return;
2162 }
2163 }
2164
2165 UpdateColors();
2166
2167 Ui()->Update();
2168
2169 Render();
2170
2171 if(IsActive())
2172 {
2173 RenderTools()->RenderCursor(Center: Ui()->MousePos(), Size: 24.0f);
2174 }
2175
2176 // render debug information
2177 if(g_Config.m_Debug)
2178 Ui()->DebugRender();
2179
2180 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
2181 SetActive(false);
2182
2183 Ui()->FinishCheck();
2184 Ui()->ClearHotkeys();
2185}
2186
2187void CMenus::UpdateColors()
2188{
2189 ms_GuiColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_UiColor, true));
2190
2191 ms_ColorTabbarInactiveOutgame = ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f);
2192 ms_ColorTabbarActiveOutgame = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f);
2193 ms_ColorTabbarHoverOutgame = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f);
2194
2195 const float ColorIngameScaleI = 0.5f;
2196 const float ColorIngameAcaleA = 0.2f;
2197
2198 ms_ColorTabbarInactiveIngame = ColorRGBA(
2199 ms_GuiColor.r * ColorIngameScaleI,
2200 ms_GuiColor.g * ColorIngameScaleI,
2201 ms_GuiColor.b * ColorIngameScaleI,
2202 ms_GuiColor.a * 0.8f);
2203
2204 ms_ColorTabbarActiveIngame = ColorRGBA(
2205 ms_GuiColor.r * ColorIngameAcaleA,
2206 ms_GuiColor.g * ColorIngameAcaleA,
2207 ms_GuiColor.b * ColorIngameAcaleA,
2208 ms_GuiColor.a);
2209
2210 ms_ColorTabbarHoverIngame = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f);
2211}
2212
2213void CMenus::RenderBackground()
2214{
2215 Graphics()->BlendNormal();
2216
2217 const float ScreenHeight = 300.0f;
2218 const float ScreenWidth = ScreenHeight * Graphics()->ScreenAspect();
2219 Graphics()->MapScreen(TopLeftX: 0.0f, TopLeftY: 0.0f, BottomRightX: ScreenWidth, BottomRightY: ScreenHeight);
2220
2221 // render background color
2222 Graphics()->TextureClear();
2223 Graphics()->QuadsBegin();
2224 Graphics()->SetColor(ms_GuiColor.WithAlpha(alpha: 1.0f));
2225 const IGraphics::CQuadItem BackgroundQuadItem = IGraphics::CQuadItem(0, 0, ScreenWidth, ScreenHeight);
2226 Graphics()->QuadsDrawTL(pArray: &BackgroundQuadItem, Num: 1);
2227 Graphics()->QuadsEnd();
2228
2229 // render the tiles
2230 Graphics()->TextureClear();
2231 Graphics()->QuadsBegin();
2232 Graphics()->SetColor(r: 0.0f, g: 0.0f, b: 0.0f, a: 0.045f);
2233 const float Size = 15.0f;
2234 const float OffsetTime = std::fmod(x: LocalTime() * 0.15f, y: 2.0f);
2235 IGraphics::CQuadItem aCheckerItems[64];
2236 size_t NumCheckerItems = 0;
2237 for(int y = -2; y < (int)(ScreenWidth / Size); y++)
2238 {
2239 for(int x = -2; x < (int)(ScreenHeight / Size); x++)
2240 {
2241 aCheckerItems[NumCheckerItems] = IGraphics::CQuadItem((x - OffsetTime) * Size * 2 + (y & 1) * Size, (y + OffsetTime) * Size, Size, Size);
2242 NumCheckerItems++;
2243 if(NumCheckerItems == std::size(aCheckerItems))
2244 {
2245 Graphics()->QuadsDrawTL(pArray: aCheckerItems, Num: NumCheckerItems);
2246 NumCheckerItems = 0;
2247 }
2248 }
2249 }
2250 if(NumCheckerItems != 0)
2251 Graphics()->QuadsDrawTL(pArray: aCheckerItems, Num: NumCheckerItems);
2252 Graphics()->QuadsEnd();
2253
2254 // render border fade
2255 Graphics()->TextureSet(Texture: m_TextureBlob);
2256 Graphics()->QuadsBegin();
2257 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2258 const IGraphics::CQuadItem BlobQuadItem = IGraphics::CQuadItem(-100, -100, ScreenWidth + 200, ScreenHeight + 200);
2259 Graphics()->QuadsDrawTL(pArray: &BlobQuadItem, Num: 1);
2260 Graphics()->QuadsEnd();
2261
2262 // restore screen
2263 Ui()->MapScreen();
2264}
2265
2266bool CMenus::CheckHotKey(int Key) const
2267{
2268 return m_Popup == POPUP_NONE &&
2269 !Input()->ShiftIsPressed() && !Input()->ModifierIsPressed() && // no modifier
2270 Input()->KeyIsPressed(Key) && m_pClient->m_GameConsole.IsClosed();
2271}
2272
2273int CMenus::DoButton_CheckBox_Tristate(const void *pId, const char *pText, TRISTATE Checked, const CUIRect *pRect)
2274{
2275 switch(Checked)
2276 {
2277 case TRISTATE::NONE:
2278 return DoButton_CheckBox_Common(pId, pText, pBoxText: "", pRect);
2279 case TRISTATE::SOME:
2280 return DoButton_CheckBox_Common(pId, pText, pBoxText: "O", pRect);
2281 case TRISTATE::ALL:
2282 return DoButton_CheckBox_Common(pId, pText, pBoxText: "X", pRect);
2283 default:
2284 dbg_assert(false, "invalid tristate");
2285 }
2286 dbg_break();
2287}
2288
2289int CMenus::MenuImageScan(const char *pName, int IsDir, int DirType, void *pUser)
2290{
2291 const char *pExtension = ".png";
2292 CMenuImage MenuImage;
2293 CMenus *pSelf = static_cast<CMenus *>(pUser);
2294 if(IsDir || !str_endswith(str: pName, suffix: pExtension) || str_length(str: pName) - str_length(str: pExtension) >= (int)sizeof(MenuImage.m_aName))
2295 return 0;
2296
2297 char aPath[IO_MAX_PATH_LENGTH];
2298 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "menuimages/%s", pName);
2299
2300 CImageInfo Info;
2301 if(!pSelf->Graphics()->LoadPng(Image&: Info, pFilename: aPath, StorageType: DirType))
2302 {
2303 char aError[IO_MAX_PATH_LENGTH + 64];
2304 str_format(buffer: aError, buffer_size: sizeof(aError), format: "Failed to load menu image from '%s'", aPath);
2305 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "menus", pStr: aError);
2306 return 0;
2307 }
2308 if(Info.m_Format != CImageInfo::FORMAT_RGBA)
2309 {
2310 Info.Free();
2311 char aError[IO_MAX_PATH_LENGTH + 64];
2312 str_format(buffer: aError, buffer_size: sizeof(aError), format: "Failed to load menu image from '%s': must be an RGBA image", aPath);
2313 pSelf->Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "menus", pStr: aError);
2314 return 0;
2315 }
2316
2317 MenuImage.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Image: Info, Flags: 0, pTexName: aPath);
2318
2319 // create gray scale version
2320 unsigned char *pData = static_cast<unsigned char *>(Info.m_pData);
2321 const size_t Step = Info.PixelSize();
2322 for(size_t i = 0; i < Info.m_Width * Info.m_Height; i++)
2323 {
2324 int v = (pData[i * Step] + pData[i * Step + 1] + pData[i * Step + 2]) / 3;
2325 pData[i * Step] = v;
2326 pData[i * Step + 1] = v;
2327 pData[i * Step + 2] = v;
2328 }
2329 MenuImage.m_GreyTexture = pSelf->Graphics()->LoadTextureRawMove(Image&: Info, Flags: 0, pTexName: aPath);
2330
2331 str_truncate(dst: MenuImage.m_aName, dst_size: sizeof(MenuImage.m_aName), src: pName, truncation_len: str_length(str: pName) - str_length(str: pExtension));
2332 pSelf->m_vMenuImages.push_back(x: MenuImage);
2333
2334 pSelf->RenderLoading(pCaption: Localize(pStr: "Loading DDNet Client"), pContent: Localize(pStr: "Loading menu images"), IncreaseCounter: 1);
2335
2336 return 0;
2337}
2338
2339const CMenus::CMenuImage *CMenus::FindMenuImage(const char *pName)
2340{
2341 for(auto &Image : m_vMenuImages)
2342 if(str_comp(a: Image.m_aName, b: pName) == 0)
2343 return &Image;
2344 return nullptr;
2345}
2346
2347void CMenus::SetMenuPage(int NewPage)
2348{
2349 const int OldPage = m_MenuPage;
2350 m_MenuPage = NewPage;
2351 if(NewPage >= PAGE_INTERNET && NewPage <= PAGE_FAVORITE_COMMUNITY_5)
2352 {
2353 g_Config.m_UiPage = NewPage;
2354 bool ForceRefresh = false;
2355 if(m_ForceRefreshLanPage)
2356 {
2357 ForceRefresh = NewPage == PAGE_LAN;
2358 m_ForceRefreshLanPage = false;
2359 }
2360 if(OldPage != NewPage || ForceRefresh)
2361 {
2362 RefreshBrowserTab(Force: ForceRefresh);
2363 }
2364 }
2365}
2366
2367void CMenus::RefreshBrowserTab(bool Force)
2368{
2369 if(g_Config.m_UiPage == PAGE_INTERNET)
2370 {
2371 if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET)
2372 {
2373 if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
2374 {
2375 Client()->RequestDDNetInfo();
2376 }
2377 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_INTERNET);
2378 UpdateCommunityCache(Force: true);
2379 }
2380 }
2381 else if(g_Config.m_UiPage == PAGE_LAN)
2382 {
2383 if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_LAN)
2384 {
2385 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_LAN);
2386 UpdateCommunityCache(Force: true);
2387 }
2388 }
2389 else if(g_Config.m_UiPage == PAGE_FAVORITES)
2390 {
2391 if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES)
2392 {
2393 if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
2394 {
2395 Client()->RequestDDNetInfo();
2396 }
2397 ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_FAVORITES);
2398 UpdateCommunityCache(Force: true);
2399 }
2400 }
2401 else if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5)
2402 {
2403 const int BrowserType = g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + IServerBrowser::TYPE_FAVORITE_COMMUNITY_1;
2404 if(Force || ServerBrowser()->GetCurrentType() != BrowserType)
2405 {
2406 if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
2407 {
2408 Client()->RequestDDNetInfo();
2409 }
2410 ServerBrowser()->Refresh(Type: BrowserType);
2411 UpdateCommunityCache(Force: true);
2412 }
2413 }
2414}
2415