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