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