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