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#include "ghost.h"
4#include "menus.h"
5#include "motd.h"
6#include "voting.h"
7
8#include <base/color.h>
9#include <base/dbg.h>
10#include <base/math.h>
11#include <base/str.h>
12#include <base/time.h>
13
14#include <engine/console.h>
15#include <engine/demo.h>
16#include <engine/favorites.h>
17#include <engine/font_icons.h>
18#include <engine/friends.h>
19#include <engine/ghost.h>
20#include <engine/graphics.h>
21#include <engine/keys.h>
22#include <engine/serverbrowser.h>
23#include <engine/shared/config.h>
24#include <engine/shared/localization.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/countryflags.h>
33#include <game/client/components/touch_controls.h>
34#include <game/client/gameclient.h>
35#include <game/client/ui.h>
36#include <game/client/ui_listbox.h>
37#include <game/client/ui_scrollregion.h>
38#include <game/localization.h>
39
40#include <chrono>
41
42using namespace std::chrono_literals;
43
44void CMenus::RenderGame(CUIRect MainView)
45{
46 CUIRect Button, ButtonBars, ButtonBar, ButtonBar2;
47 bool ShowDDRaceButtons = MainView.w > 855.0f;
48 MainView.HSplitTop(Cut: 45.0f + (g_Config.m_ClTouchControls ? 35.0f : 0.0f), pTop: &ButtonBars, pBottom: &MainView);
49 ButtonBars.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
50 ButtonBars.Margin(Cut: 10.0f, pOtherRect: &ButtonBars);
51 ButtonBars.HSplitTop(Cut: 25.0f, pTop: &ButtonBar, pBottom: &ButtonBars);
52 if(g_Config.m_ClTouchControls)
53 {
54 ButtonBars.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &ButtonBars);
55 ButtonBars.HSplitTop(Cut: 25.0f, pTop: &ButtonBar2, pBottom: &ButtonBars);
56 }
57
58 ButtonBar.VSplitRight(Cut: 120.0f, pLeft: &ButtonBar, pRight: &Button);
59 static CButtonContainer s_DisconnectButton;
60 if(DoButton_Menu(pButtonContainer: &s_DisconnectButton, pText: Localize(pStr: "Disconnect"), Checked: 0, pRect: &Button))
61 {
62 if((GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0) ||
63 GameClient()->m_TouchControls.HasEditingChanges() ||
64 GameClient()->m_Menus.m_MenusIngameTouchControls.UnsavedChanges())
65 {
66 char aBuf[256] = {'\0'};
67 if(GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
68 {
69 str_copy(dst&: aBuf, src: Localize(pStr: "Are you sure that you want to disconnect?"));
70 }
71 if(GameClient()->m_TouchControls.HasEditingChanges() ||
72 GameClient()->m_Menus.m_MenusIngameTouchControls.UnsavedChanges())
73 {
74 if(aBuf[0] != '\0')
75 {
76 str_append(dst&: aBuf, src: "\n\n");
77 }
78 str_append(dst&: aBuf, src: Localize(pStr: "There's an unsaved change in the touch controls editor, you might want to save it."));
79 }
80 PopupConfirm(pTitle: Localize(pStr: "Disconnect"), pMessage: aBuf, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmDisconnect);
81 }
82 else
83 {
84 Client()->Disconnect();
85 RefreshBrowserTab(Force: true);
86 }
87 }
88
89 ButtonBar.VSplitRight(Cut: 5.0f, pLeft: &ButtonBar, pRight: nullptr);
90 ButtonBar.VSplitRight(Cut: 170.0f, pLeft: &ButtonBar, pRight: &Button);
91
92 static CButtonContainer s_DummyButton;
93 if(!Client()->DummyAllowed())
94 {
95 DoButton_Menu(pButtonContainer: &s_DummyButton, pText: Localize(pStr: "Connect Dummy"), Checked: 1, pRect: &Button);
96 GameClient()->m_Tooltips.DoToolTip(pId: &s_DummyButton, pNearRect: &Button, pText: Localize(pStr: "Dummy is not allowed on this server"));
97 }
98 else if(Client()->DummyConnectingDelayed())
99 {
100 DoButton_Menu(pButtonContainer: &s_DummyButton, pText: Localize(pStr: "Connect Dummy"), Checked: 1, pRect: &Button);
101 GameClient()->m_Tooltips.DoToolTip(pId: &s_DummyButton, pNearRect: &Button, pText: Localize(pStr: "Please wait…"));
102 }
103 else if(Client()->DummyConnecting())
104 {
105 DoButton_Menu(pButtonContainer: &s_DummyButton, pText: Localize(pStr: "Connecting dummy"), Checked: 1, pRect: &Button);
106 }
107 else if(DoButton_Menu(pButtonContainer: &s_DummyButton, pText: Client()->DummyConnected() ? Localize(pStr: "Disconnect Dummy") : Localize(pStr: "Connect Dummy"), Checked: 0, pRect: &Button))
108 {
109 if(!Client()->DummyConnected())
110 {
111 Client()->DummyConnect();
112 }
113 else
114 {
115 if(GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
116 {
117 PopupConfirm(pTitle: Localize(pStr: "Disconnect Dummy"), pMessage: Localize(pStr: "Are you sure that you want to disconnect your dummy?"), pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmDisconnectDummy);
118 }
119 else
120 {
121 Client()->DummyDisconnect(pReason: nullptr);
122 SetActive(false);
123 }
124 }
125 }
126
127 ButtonBar.VSplitRight(Cut: 5.0f, pLeft: &ButtonBar, pRight: nullptr);
128 ButtonBar.VSplitRight(Cut: 140.0f, pLeft: &ButtonBar, pRight: &Button);
129 static CButtonContainer s_DemoButton;
130 const bool Recording = DemoRecorder(Recorder: RECORDER_MANUAL)->IsRecording();
131 if(DoButton_Menu(pButtonContainer: &s_DemoButton, pText: Recording ? Localize(pStr: "Stop record") : Localize(pStr: "Record demo"), Checked: 0, pRect: &Button))
132 {
133 if(!Recording)
134 Client()->DemoRecorder_Start(pFilename: GameClient()->Map()->BaseName(), WithTimestamp: true, Recorder: RECORDER_MANUAL);
135 else
136 Client()->DemoRecorder(Recorder: RECORDER_MANUAL)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
137 }
138
139 bool Paused = false;
140 bool Spec = false;
141 if(GameClient()->m_Snap.m_LocalClientId >= 0)
142 {
143 Paused = GameClient()->m_aClients[GameClient()->m_Snap.m_LocalClientId].m_Paused;
144 Spec = GameClient()->m_aClients[GameClient()->m_Snap.m_LocalClientId].m_Spec;
145 }
146
147 if(GameClient()->m_Snap.m_pLocalInfo && GameClient()->m_Snap.m_pGameInfoObj && !Paused && !Spec)
148 {
149 if(GameClient()->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS)
150 {
151 ButtonBar.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &ButtonBar);
152 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ButtonBar);
153 static CButtonContainer s_SpectateButton;
154 if(!Client()->DummyConnecting() && DoButton_Menu(pButtonContainer: &s_SpectateButton, pText: Localize(pStr: "Spectate"), Checked: 0, pRect: &Button))
155 {
156 if(g_Config.m_ClDummy == 0 || Client()->DummyConnected())
157 {
158 GameClient()->SendSwitchTeam(Team: TEAM_SPECTATORS);
159 SetActive(false);
160 }
161 }
162 }
163
164 if(GameClient()->IsTeamPlay())
165 {
166 if(GameClient()->m_Snap.m_pLocalInfo->m_Team != TEAM_RED)
167 {
168 ButtonBar.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: &ButtonBar);
169 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ButtonBar);
170 static CButtonContainer s_JoinRedButton;
171 if(!Client()->DummyConnecting() && DoButton_Menu(pButtonContainer: &s_JoinRedButton, pText: Localize(pStr: "Join red"), Checked: 0, pRect: &Button))
172 {
173 GameClient()->SendSwitchTeam(Team: TEAM_RED);
174 SetActive(false);
175 }
176 }
177
178 if(GameClient()->m_Snap.m_pLocalInfo->m_Team != TEAM_BLUE)
179 {
180 ButtonBar.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: &ButtonBar);
181 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ButtonBar);
182 static CButtonContainer s_JoinBlueButton;
183 if(!Client()->DummyConnecting() && DoButton_Menu(pButtonContainer: &s_JoinBlueButton, pText: Localize(pStr: "Join blue"), Checked: 0, pRect: &Button))
184 {
185 GameClient()->SendSwitchTeam(Team: TEAM_BLUE);
186 SetActive(false);
187 }
188 }
189 }
190 else
191 {
192 if(GameClient()->m_Snap.m_pLocalInfo->m_Team != TEAM_GAME)
193 {
194 ButtonBar.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &ButtonBar);
195 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ButtonBar);
196 static CButtonContainer s_JoinGameButton;
197 if(!Client()->DummyConnecting() && DoButton_Menu(pButtonContainer: &s_JoinGameButton, pText: Localize(pStr: "Join game"), Checked: 0, pRect: &Button))
198 {
199 GameClient()->SendSwitchTeam(Team: TEAM_GAME);
200 SetActive(false);
201 }
202 }
203 }
204
205 if(GameClient()->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS && (ShowDDRaceButtons || !GameClient()->IsTeamPlay()))
206 {
207 ButtonBar.VSplitLeft(Cut: 65.0f, pLeft: &Button, pRight: &ButtonBar);
208 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ButtonBar);
209
210 static CButtonContainer s_KillButton;
211 if(DoButton_Menu(pButtonContainer: &s_KillButton, pText: Localize(pStr: "Kill"), Checked: 0, pRect: &Button))
212 {
213 GameClient()->SendKill();
214 SetActive(false);
215 }
216 }
217 }
218
219 if(GameClient()->m_ReceivedDDNetPlayer && GameClient()->m_Snap.m_pLocalInfo && (ShowDDRaceButtons || !GameClient()->IsTeamPlay()))
220 {
221 if(GameClient()->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS || Paused || Spec)
222 {
223 ButtonBar.VSplitLeft(Cut: (!Paused && !Spec) ? 65.0f : 120.0f, pLeft: &Button, pRight: &ButtonBar);
224 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ButtonBar);
225
226 static CButtonContainer s_PauseButton;
227 if(DoButton_Menu(pButtonContainer: &s_PauseButton, pText: (!Paused && !Spec) ? Localize(pStr: "Pause") : Localize(pStr: "Join game"), Checked: 0, pRect: &Button))
228 {
229 Console()->ExecuteLine(pStr: "say /pause", ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
230 SetActive(false);
231 }
232 }
233 }
234
235 if(GameClient()->m_Snap.m_pLocalInfo && (GameClient()->m_Snap.m_pLocalInfo->m_Team == TEAM_SPECTATORS || Paused || Spec))
236 {
237 ButtonBar.VSplitLeft(Cut: 32.0f, pLeft: &Button, pRight: &ButtonBar);
238 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ButtonBar);
239
240 static CButtonContainer s_AutoCameraButton;
241
242 bool Active = GameClient()->m_Camera.m_AutoSpecCamera && GameClient()->m_Camera.SpectatingPlayer() && GameClient()->m_Camera.CanUseAutoSpecCamera();
243 bool Enabled = g_Config.m_ClSpecAutoSync;
244 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_AutoCameraButton, pText: FontIcon::CAMERA, Checked: !Active, pRect: &Button, Flags: BUTTONFLAG_LEFT, Corners: IGraphics::CORNER_ALL, Enabled))
245 {
246 GameClient()->m_Camera.ToggleAutoSpecCamera();
247 }
248 GameClient()->m_Camera.UpdateAutoSpecCameraTooltip();
249 GameClient()->m_Tooltips.DoToolTip(pId: &s_AutoCameraButton, pNearRect: &Button, pText: GameClient()->m_Camera.AutoSpecCameraTooltip());
250 }
251
252 if(g_Config.m_ClTouchControls)
253 {
254 ButtonBar2.VSplitLeft(Cut: 200.0f, pLeft: &Button, pRight: &ButtonBar2);
255 static char s_TouchControlsEditCheckbox;
256 if(DoButton_CheckBox(pId: &s_TouchControlsEditCheckbox, pText: Localize(pStr: "Edit touch controls"), Checked: GameClient()->m_TouchControls.IsEditingActive(), pRect: &Button))
257 {
258 if(GameClient()->m_TouchControls.IsEditingActive() && m_MenusIngameTouchControls.UnsavedChanges())
259 {
260 m_MenusIngameTouchControls.m_pOldSelectedButton = GameClient()->m_TouchControls.SelectedButton();
261 m_MenusIngameTouchControls.m_pNewSelectedButton = nullptr;
262 PopupConfirm(pTitle: Localize(pStr: "Unsaved changes"), pMessage: Localize(pStr: "Save all changes before turning off the editor?"), pConfirmButtonLabel: Localize(pStr: "Save"), pCancelButtonLabel: Localize(pStr: "Cancel"), pfnConfirmButtonCallback: &CMenus::PopupConfirmTurnOffEditor);
263 }
264 else
265 {
266 GameClient()->m_TouchControls.SetEditingActive(!GameClient()->m_TouchControls.IsEditingActive());
267 if(GameClient()->m_TouchControls.IsEditingActive())
268 {
269 GameClient()->m_TouchControls.ResetVirtualVisibilities();
270 m_MenusIngameTouchControls.m_EditElement = CMenusIngameTouchControls::EElementType::LAYOUT;
271 }
272 else
273 {
274 m_MenusIngameTouchControls.ResetButtonPointers();
275 }
276 }
277 }
278
279 ButtonBar2.VSplitRight(Cut: 80.0f, pLeft: &ButtonBar2, pRight: &Button);
280 static CButtonContainer s_CloseButton;
281 if(DoButton_Menu(pButtonContainer: &s_CloseButton, pText: Localize(pStr: "Close"), Checked: 0, pRect: &Button))
282 {
283 SetActive(false);
284 }
285
286 ButtonBar2.VSplitRight(Cut: 5.0f, pLeft: &ButtonBar2, pRight: nullptr);
287 ButtonBar2.VSplitRight(Cut: 160.0f, pLeft: &ButtonBar2, pRight: &Button);
288 static CButtonContainer s_RemoveConsoleButton;
289 if(DoButton_Menu(pButtonContainer: &s_RemoveConsoleButton, pText: Localize(pStr: "Remote console"), Checked: 0, pRect: &Button))
290 {
291 Console()->ExecuteLine(pStr: "toggle_remote_console", ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
292 }
293
294 ButtonBar2.VSplitRight(Cut: 5.0f, pLeft: &ButtonBar2, pRight: nullptr);
295 ButtonBar2.VSplitRight(Cut: 120.0f, pLeft: &ButtonBar2, pRight: &Button);
296 static CButtonContainer s_LocalConsoleButton;
297 if(DoButton_Menu(pButtonContainer: &s_LocalConsoleButton, pText: Localize(pStr: "Console"), Checked: 0, pRect: &Button))
298 {
299 Console()->ExecuteLine(pStr: "toggle_local_console", ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
300 }
301 // Only when these are all false, the preview page is rendered. Once the page is not rendered, update is needed upon next rendering.
302 if(!GameClient()->m_TouchControls.IsEditingActive() || m_MenusIngameTouchControls.m_CurrentMenu != CMenusIngameTouchControls::EMenuType::MENU_BUTTONS || GameClient()->m_TouchControls.IsButtonEditing())
303 m_MenusIngameTouchControls.m_NeedUpdatePreview = true;
304 // Quit preview all buttons automatically.
305 if(!GameClient()->m_TouchControls.IsEditingActive() || m_MenusIngameTouchControls.m_CurrentMenu != CMenusIngameTouchControls::EMenuType::MENU_PREVIEW)
306 GameClient()->m_TouchControls.SetPreviewAllButtons(false);
307 if(GameClient()->m_TouchControls.IsEditingActive())
308 {
309 // Resolve issues if needed before rendering, so the elements could have a correct value on this frame.
310 // Issues need to be resolved before popup. So CheckCachedSettings could not be bad.
311 m_MenusIngameTouchControls.ResolveIssues();
312 // Do Popups if needed.
313 CTouchControls::CPopupParam PopupParam = GameClient()->m_TouchControls.RequiredPopup();
314 if(PopupParam.m_PopupType != CTouchControls::EPopupType::NUM_POPUPS)
315 {
316 m_MenusIngameTouchControls.DoPopupType(PopupParam);
317 return;
318 }
319 if(m_MenusIngameTouchControls.m_FirstEnter)
320 {
321 m_MenusIngameTouchControls.m_aCachedVisibilities[(int)CTouchControls::EButtonVisibility::DEMO_PLAYER] = CMenusIngameTouchControls::EVisibilityType::EXCLUDE;
322 m_MenusIngameTouchControls.m_ColorActive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorActive()).Pack(Alpha: true);
323 m_MenusIngameTouchControls.m_ColorInactive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorInactive()).Pack(Alpha: true);
324 m_MenusIngameTouchControls.m_FirstEnter = false;
325 }
326 // Their width is all 505.0f, height is adjustable, you can directly change its h value, so no need for changing where tab is.
327 CUIRect SelectingTab;
328 MainView.HSplitTop(Cut: 40.0f, pTop: nullptr, pBottom: &MainView);
329 MainView.VMargin(Cut: (MainView.w - CMenusIngameTouchControls::BUTTON_EDITOR_WIDTH) / 2.0f, pOtherRect: &MainView);
330 MainView.HSplitTop(Cut: 25.0f, pTop: &SelectingTab, pBottom: &MainView);
331
332 m_MenusIngameTouchControls.RenderSelectingTab(SelectingTab);
333 switch(m_MenusIngameTouchControls.m_CurrentMenu)
334 {
335 case CMenusIngameTouchControls::EMenuType::MENU_FILE: m_MenusIngameTouchControls.RenderTouchControlsEditor(MainView); break;
336 case CMenusIngameTouchControls::EMenuType::MENU_BUTTONS: m_MenusIngameTouchControls.RenderTouchButtonEditor(MainView); break;
337 case CMenusIngameTouchControls::EMenuType::MENU_SETTINGS: m_MenusIngameTouchControls.RenderConfigSettings(MainView); break;
338 case CMenusIngameTouchControls::EMenuType::MENU_PREVIEW: m_MenusIngameTouchControls.RenderPreviewSettings(MainView); break;
339 default: dbg_assert_failed("Unknown selected tab value = %d.", (int)m_MenusIngameTouchControls.m_CurrentMenu);
340 }
341 }
342 }
343}
344
345void CMenus::PopupConfirmDisconnect()
346{
347 Client()->Disconnect();
348}
349
350void CMenus::PopupConfirmDisconnectDummy()
351{
352 Client()->DummyDisconnect(pReason: nullptr);
353 SetActive(false);
354}
355
356void CMenus::PopupConfirmDiscardTouchControlsChanges()
357{
358 if(GameClient()->m_TouchControls.LoadConfigurationFromFile(StorageType: IStorage::TYPE_ALL))
359 {
360 m_MenusIngameTouchControls.m_ColorActive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorActive()).Pack(Alpha: true);
361 m_MenusIngameTouchControls.m_ColorInactive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorInactive()).Pack(Alpha: true);
362 GameClient()->m_TouchControls.SetEditingChanges(false);
363 }
364 else
365 {
366 SWarning Warning(Localize(pStr: "Error loading touch controls"), Localize(pStr: "Could not load touch controls from file. See local console for details."));
367 Warning.m_AutoHide = false;
368 Client()->AddWarning(Warning);
369 }
370}
371
372void CMenus::PopupConfirmResetTouchControls()
373{
374 bool Success = false;
375 for(int StorageType = IStorage::TYPE_SAVE + 1; StorageType < Storage()->NumPaths(); ++StorageType)
376 {
377 if(GameClient()->m_TouchControls.LoadConfigurationFromFile(StorageType))
378 {
379 Success = true;
380 break;
381 }
382 }
383 if(Success)
384 {
385 m_MenusIngameTouchControls.m_ColorActive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorActive()).Pack(Alpha: true);
386 m_MenusIngameTouchControls.m_ColorInactive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorInactive()).Pack(Alpha: true);
387 GameClient()->m_TouchControls.SetEditingChanges(true);
388 }
389 else
390 {
391 SWarning Warning(Localize(pStr: "Error loading touch controls"), Localize(pStr: "Could not load default touch controls from file. See local console for details."));
392 Warning.m_AutoHide = false;
393 Client()->AddWarning(Warning);
394 }
395}
396
397void CMenus::PopupConfirmImportTouchControlsClipboard()
398{
399 if(GameClient()->m_TouchControls.LoadConfigurationFromClipboard())
400 {
401 m_MenusIngameTouchControls.m_ColorActive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorActive()).Pack(Alpha: true);
402 m_MenusIngameTouchControls.m_ColorInactive = color_cast<ColorHSLA>(rgb: GameClient()->m_TouchControls.BackgroundColorInactive()).Pack(Alpha: true);
403 GameClient()->m_TouchControls.SetEditingChanges(true);
404 }
405 else
406 {
407 SWarning Warning(Localize(pStr: "Error loading touch controls"), Localize(pStr: "Could not load touch controls from clipboard. See local console for details."));
408 Warning.m_AutoHide = false;
409 Client()->AddWarning(Warning);
410 }
411}
412
413void CMenus::PopupConfirmDeleteButton()
414{
415 GameClient()->m_TouchControls.DeleteSelectedButton();
416 m_MenusIngameTouchControls.ResetCachedSettings();
417 GameClient()->m_TouchControls.SetEditingChanges(true);
418}
419
420void CMenus::PopupCancelDeselectButton()
421{
422 m_MenusIngameTouchControls.ResetButtonPointers();
423 m_MenusIngameTouchControls.SetUnsavedChanges(false);
424 m_MenusIngameTouchControls.ResetCachedSettings();
425}
426
427void CMenus::PopupConfirmSelectedNotVisible()
428{
429 if(m_MenusIngameTouchControls.UnsavedChanges())
430 {
431 // The m_pSelectedButton can't nullptr, because this function is triggered when selected button not visible.
432 m_MenusIngameTouchControls.m_pOldSelectedButton = GameClient()->m_TouchControls.SelectedButton();
433 m_MenusIngameTouchControls.m_pNewSelectedButton = nullptr;
434 m_MenusIngameTouchControls.m_CloseMenu = true;
435 m_MenusIngameTouchControls.ChangeSelectedButtonWhileHavingUnsavedChanges();
436 }
437 else
438 {
439 m_MenusIngameTouchControls.ResetButtonPointers();
440 GameClient()->m_Menus.SetActive(false);
441 }
442}
443
444void CMenus::PopupConfirmChangeSelectedButton()
445{
446 if(m_MenusIngameTouchControls.CheckCachedSettings())
447 {
448 GameClient()->m_TouchControls.SetSelectedButton(m_MenusIngameTouchControls.m_pNewSelectedButton);
449 m_MenusIngameTouchControls.SaveCachedSettingsToTarget(pTargetButton: m_MenusIngameTouchControls.m_pOldSelectedButton);
450 // Update wild pointer.
451 if(m_MenusIngameTouchControls.m_pNewSelectedButton != nullptr)
452 m_MenusIngameTouchControls.m_pNewSelectedButton = GameClient()->m_TouchControls.SelectedButton();
453 GameClient()->m_TouchControls.SetEditingChanges(true);
454 m_MenusIngameTouchControls.SetUnsavedChanges(false);
455 PopupCancelChangeSelectedButton();
456 }
457}
458
459void CMenus::PopupCancelChangeSelectedButton()
460{
461 GameClient()->m_TouchControls.SetSelectedButton(m_MenusIngameTouchControls.m_pNewSelectedButton);
462 m_MenusIngameTouchControls.CacheAllSettingsFromTarget(pTargetButton: m_MenusIngameTouchControls.m_pNewSelectedButton);
463 m_MenusIngameTouchControls.SetUnsavedChanges(false);
464 if(m_MenusIngameTouchControls.m_pNewSelectedButton != nullptr)
465 {
466 m_MenusIngameTouchControls.UpdateSampleButton();
467 }
468 else
469 {
470 m_MenusIngameTouchControls.ResetButtonPointers();
471 }
472 if(m_MenusIngameTouchControls.m_CloseMenu)
473 GameClient()->m_Menus.SetActive(false);
474}
475
476void CMenus::PopupConfirmTurnOffEditor()
477{
478 if(m_MenusIngameTouchControls.CheckCachedSettings())
479 {
480 m_MenusIngameTouchControls.SaveCachedSettingsToTarget(pTargetButton: m_MenusIngameTouchControls.m_pOldSelectedButton);
481 GameClient()->m_TouchControls.SetEditingActive(!GameClient()->m_TouchControls.IsEditingActive());
482 m_MenusIngameTouchControls.ResetButtonPointers();
483 }
484}
485
486void CMenus::PopupConfirmOpenWiki()
487{
488 Client()->ViewLink(pLink: Localize(pStr: "https://wiki.ddnet.org/wiki/Touch_controls"));
489}
490
491void CMenus::RenderPlayers(CUIRect MainView)
492{
493 CUIRect Button, Button2, ButtonBar, PlayerList, Player;
494 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
495
496 // list background color
497 MainView.Margin(Cut: 10.0f, pOtherRect: &PlayerList);
498 PlayerList.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
499 PlayerList.Margin(Cut: 10.0f, pOtherRect: &PlayerList);
500
501 // headline
502 PlayerList.HSplitTop(Cut: 34.0f, pTop: &ButtonBar, pBottom: &PlayerList);
503 ButtonBar.VSplitRight(Cut: 231.0f, pLeft: &Player, pRight: &ButtonBar);
504 Ui()->DoLabel(pRect: &Player, pText: Localize(pStr: "Player"), Size: 24.0f, Align: TEXTALIGN_ML);
505
506 ButtonBar.HMargin(Cut: 1.0f, pOtherRect: &ButtonBar);
507 float Width = ButtonBar.h * 2.0f;
508 ButtonBar.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &ButtonBar);
509 RenderTools()->RenderIcon(ImageId: IMAGE_GUIICONS, SpriteId: SPRITE_GUIICON_MUTE, pRect: &Button);
510
511 ButtonBar.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &ButtonBar);
512 ButtonBar.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &ButtonBar);
513 RenderTools()->RenderIcon(ImageId: IMAGE_GUIICONS, SpriteId: SPRITE_GUIICON_EMOTICON_MUTE, pRect: &Button);
514
515 ButtonBar.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &ButtonBar);
516 ButtonBar.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &ButtonBar);
517 RenderTools()->RenderIcon(ImageId: IMAGE_GUIICONS, SpriteId: SPRITE_GUIICON_FRIEND, pRect: &Button);
518
519 int TotalPlayers = 0;
520 for(const auto &pInfoByName : GameClient()->m_Snap.m_apInfoByName)
521 {
522 if(!pInfoByName)
523 continue;
524
525 int Index = pInfoByName->m_ClientId;
526
527 if(Index == GameClient()->m_Snap.m_LocalClientId)
528 continue;
529
530 TotalPlayers++;
531 }
532
533 static CListBox s_ListBox;
534 s_ListBox.DoStart(RowHeight: 24.0f, NumItems: TotalPlayers, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &PlayerList);
535
536 // options
537 static char s_aPlayerIds[MAX_CLIENTS][4] = {{0}};
538
539 for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i)
540 {
541 if(!GameClient()->m_Snap.m_apInfoByName[i])
542 continue;
543
544 int Index = GameClient()->m_Snap.m_apInfoByName[i]->m_ClientId;
545 if(Index == GameClient()->m_Snap.m_LocalClientId)
546 continue;
547
548 CGameClient::CClientData &CurrentClient = GameClient()->m_aClients[Index];
549 const CListboxItem Item = s_ListBox.DoNextItem(pId: &CurrentClient);
550
551 Count++;
552
553 if(!Item.m_Visible)
554 continue;
555
556 CUIRect Row = Item.m_Rect;
557 if(Count % 2 == 1)
558 Row.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
559 Row.VSplitRight(Cut: s_ListBox.ScrollbarWidthMax() - s_ListBox.ScrollbarWidth(), pLeft: &Row, pRight: nullptr);
560 Row.VSplitRight(Cut: 300.0f, pLeft: &Player, pRight: &Row);
561
562 // player info
563 Player.VSplitLeft(Cut: 28.0f, pLeft: &Button, pRight: &Player);
564
565 CTeeRenderInfo TeeInfo = CurrentClient.m_RenderInfo;
566 TeeInfo.m_Size = Button.h;
567
568 const CAnimState *pIdleState = CAnimState::GetIdle();
569 vec2 OffsetToMid;
570 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
571 vec2 TeeRenderPos(Button.x + Button.h / 2, Button.y + Button.h / 2 + OffsetToMid.y);
572 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
573 Ui()->DoButtonLogic(pId: &s_aPlayerIds[Index][3], Checked: 0, pRect: &Button, Flags: BUTTONFLAG_NONE);
574 GameClient()->m_Tooltips.DoToolTip(pId: &s_aPlayerIds[Index][3], pNearRect: &Button, pText: CurrentClient.m_aSkinName);
575
576 Player.HSplitTop(Cut: 1.5f, pTop: nullptr, pBottom: &Player);
577 Player.VSplitMid(pLeft: &Player, pRight: &Button);
578 Row.VSplitRight(Cut: 210.0f, pLeft: &Button2, pRight: &Row);
579
580 Ui()->DoLabel(pRect: &Player, pText: CurrentClient.m_aName, Size: 14.0f, Align: TEXTALIGN_ML);
581 Ui()->DoLabel(pRect: &Button, pText: CurrentClient.m_aClan, Size: 14.0f, Align: TEXTALIGN_ML);
582
583 GameClient()->m_CountryFlags.Render(CountryCode: CurrentClient.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f),
584 x: Button2.x, y: Button2.y + Button2.h / 2.0f - 0.75f * Button2.h / 2.0f, w: 1.5f * Button2.h, h: 0.75f * Button2.h);
585
586 // ignore chat button
587 Row.HMargin(Cut: 2.0f, pOtherRect: &Row);
588 Row.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &Row);
589 Button.VSplitLeft(Cut: (Width - Button.h) / 4.0f, pLeft: nullptr, pRight: &Button);
590 Button.VSplitLeft(Cut: Button.h, pLeft: &Button, pRight: nullptr);
591 if(g_Config.m_ClShowChatFriends && !CurrentClient.m_Friend)
592 DoButton_Toggle(pId: &s_aPlayerIds[Index][0], Checked: 1, pRect: &Button, Active: false);
593 else if(DoButton_Toggle(pId: &s_aPlayerIds[Index][0], Checked: CurrentClient.m_ChatIgnore, pRect: &Button, Active: true))
594 CurrentClient.m_ChatIgnore ^= 1;
595
596 // ignore emoticon button
597 Row.VSplitLeft(Cut: 30.0f, pLeft: nullptr, pRight: &Row);
598 Row.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &Row);
599 Button.VSplitLeft(Cut: (Width - Button.h) / 4.0f, pLeft: nullptr, pRight: &Button);
600 Button.VSplitLeft(Cut: Button.h, pLeft: &Button, pRight: nullptr);
601 if(g_Config.m_ClShowChatFriends && !CurrentClient.m_Friend)
602 DoButton_Toggle(pId: &s_aPlayerIds[Index][1], Checked: 1, pRect: &Button, Active: false);
603 else if(DoButton_Toggle(pId: &s_aPlayerIds[Index][1], Checked: CurrentClient.m_EmoticonIgnore, pRect: &Button, Active: true))
604 CurrentClient.m_EmoticonIgnore ^= 1;
605
606 // friend button
607 Row.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Row);
608 Row.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &Row);
609 Button.VSplitLeft(Cut: (Width - Button.h) / 4.0f, pLeft: nullptr, pRight: &Button);
610 Button.VSplitLeft(Cut: Button.h, pLeft: &Button, pRight: nullptr);
611 if(DoButton_Toggle(pId: &s_aPlayerIds[Index][2], Checked: CurrentClient.m_Friend, pRect: &Button, Active: true))
612 {
613 if(CurrentClient.m_Friend)
614 GameClient()->Friends()->RemoveFriend(pName: CurrentClient.m_aName, pClan: CurrentClient.m_aClan);
615 else
616 GameClient()->Friends()->AddFriend(pName: CurrentClient.m_aName, pClan: CurrentClient.m_aClan);
617
618 GameClient()->Client()->ServerBrowserUpdate();
619 }
620 }
621
622 s_ListBox.DoEnd();
623}
624
625void CMenus::RenderServerInfo(CUIRect MainView)
626{
627 const float FontSizeTitle = 32.0f;
628 const float FontSizeBody = 20.0f;
629
630 const CServerInfo &CurrentServerInfo = Client()->ServerInfo();
631
632 CUIRect ServerInfo, GameInfo, Motd;
633 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
634 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
635 MainView.HSplitMid(pTop: &ServerInfo, pBottom: &Motd, Spacing: 10.0f);
636 ServerInfo.VSplitMid(pLeft: &ServerInfo, pRight: &GameInfo, Spacing: 10.0f);
637
638 ServerInfo.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
639 ServerInfo.Margin(Cut: 10.0f, pOtherRect: &ServerInfo);
640
641 CUIRect Label;
642 ServerInfo.HSplitTop(Cut: FontSizeTitle, pTop: &Label, pBottom: &ServerInfo);
643 ServerInfo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ServerInfo);
644 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Server info"), Size: FontSizeTitle, Align: TEXTALIGN_ML);
645
646 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
647 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: nullptr, pBottom: &ServerInfo);
648 Ui()->DoLabel(pRect: &Label, pText: CurrentServerInfo.m_aName, Size: FontSizeBody, Align: TEXTALIGN_ML);
649
650 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
651 char aBuf[256];
652 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Address"), CurrentServerInfo.m_aAddress);
653 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
654
655 if(GameClient()->m_Snap.m_pLocalInfo)
656 {
657 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
658 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d", Localize(pStr: "Ping"), GameClient()->m_Snap.m_pLocalInfo->m_Latency);
659 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
660 }
661
662 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
663 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Version"), CurrentServerInfo.m_aVersion);
664 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
665
666 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
667 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Password"), CurrentServerInfo.m_Flags & SERVER_FLAG_PASSWORD ? Localize(pStr: "Yes") : Localize(pStr: "No"));
668 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
669
670 const CCommunity *pCommunity = ServerBrowser()->Community(pCommunityId: CurrentServerInfo.m_aCommunityId);
671 if(pCommunity != nullptr)
672 {
673 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
674 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Community"));
675 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
676
677 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pCommunity->Id());
678 if(pIcon != nullptr)
679 {
680 Label.VSplitLeft(Cut: TextRender()->TextWidth(Size: FontSizeBody, pText: aBuf) + 8.0f, pLeft: nullptr, pRight: &Label);
681 Label.VSplitLeft(Cut: 2.0f * Label.h, pLeft: &Label, pRight: nullptr);
682 m_CommunityIcons.Render(pIcon, Rect: Label, Active: true);
683 static char s_CommunityTooltipButtonId;
684 Ui()->DoButtonLogic(pId: &s_CommunityTooltipButtonId, Checked: 0, pRect: &Label, Flags: BUTTONFLAG_NONE);
685 GameClient()->m_Tooltips.DoToolTip(pId: &s_CommunityTooltipButtonId, pNearRect: &Label, pText: pCommunity->Name());
686 }
687 }
688
689 // copy info button
690 {
691 CUIRect Button;
692 ServerInfo.HSplitBottom(Cut: 20.0f, pTop: &ServerInfo, pBottom: &Button);
693 Button.VSplitRight(Cut: 200.0f, pLeft: &ServerInfo, pRight: &Button);
694 static CButtonContainer s_CopyButton;
695 if(DoButton_Menu(pButtonContainer: &s_CopyButton, pText: Localize(pStr: "Copy info"), Checked: 0, pRect: &Button))
696 {
697 char aInfo[256];
698 str_format(
699 buffer: aInfo,
700 buffer_size: sizeof(aInfo),
701 format: "%s\n"
702 "Address: ddnet://%s\n"
703 "My IGN: %s\n",
704 CurrentServerInfo.m_aName,
705 CurrentServerInfo.m_aAddress,
706 Client()->PlayerName());
707 Input()->SetClipboardText(aInfo);
708 }
709 }
710
711 // favorite checkbox
712 {
713 CUIRect Button;
714 TRISTATE IsFavorite = Favorites()->IsFavorite(pAddrs: CurrentServerInfo.m_aAddresses, NumAddrs: CurrentServerInfo.m_NumAddresses);
715 ServerInfo.HSplitBottom(Cut: 20.0f, pTop: &ServerInfo, pBottom: &Button);
716 static int s_AddFavButton = 0;
717 if(DoButton_CheckBox(pId: &s_AddFavButton, pText: Localize(pStr: "Favorite"), Checked: IsFavorite != TRISTATE::NONE, pRect: &Button))
718 {
719 if(IsFavorite != TRISTATE::NONE)
720 Favorites()->Remove(pAddrs: CurrentServerInfo.m_aAddresses, NumAddrs: CurrentServerInfo.m_NumAddresses);
721 else
722 Favorites()->Add(pAddrs: CurrentServerInfo.m_aAddresses, NumAddrs: CurrentServerInfo.m_NumAddresses);
723 }
724 }
725
726 GameInfo.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
727 GameInfo.Margin(Cut: 10.0f, pOtherRect: &GameInfo);
728
729 GameInfo.HSplitTop(Cut: FontSizeTitle, pTop: &Label, pBottom: &GameInfo);
730 GameInfo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &GameInfo);
731 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Game info"), Size: FontSizeTitle, Align: TEXTALIGN_ML);
732
733 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
734 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Game type"), CurrentServerInfo.m_aGameType);
735 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
736
737 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
738 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Map"), CurrentServerInfo.m_aMap);
739 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
740
741 const auto *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
742 if(pGameInfoObj)
743 {
744 if(pGameInfoObj->m_ScoreLimit)
745 {
746 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
747 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d", Localize(pStr: "Score limit"), pGameInfoObj->m_ScoreLimit);
748 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
749 }
750
751 if(pGameInfoObj->m_TimeLimit)
752 {
753 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
754 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Time limit: %d min"), pGameInfoObj->m_TimeLimit);
755 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
756 }
757
758 if(pGameInfoObj->m_RoundCurrent && pGameInfoObj->m_RoundNum)
759 {
760 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
761 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Round %d/%d"), pGameInfoObj->m_RoundCurrent, pGameInfoObj->m_RoundNum);
762 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
763 }
764 }
765
766 if(GameClient()->m_GameInfo.m_DDRaceTeam)
767 {
768 const char *pTeamMode = nullptr;
769 switch(Config()->m_SvTeam)
770 {
771 case SV_TEAM_FORBIDDEN:
772 pTeamMode = Localize(pStr: "forbidden", pContext: "Team status");
773 break;
774 case SV_TEAM_ALLOWED:
775 if(g_Config.m_SvSoloServer)
776 pTeamMode = Localize(pStr: "solo", pContext: "Team status");
777 else
778 pTeamMode = Localize(pStr: "allowed", pContext: "Team status");
779 break;
780 case SV_TEAM_MANDATORY:
781 pTeamMode = Localize(pStr: "required", pContext: "Team status");
782 break;
783 case SV_TEAM_FORCED_SOLO:
784 pTeamMode = Localize(pStr: "solo", pContext: "Team status");
785 break;
786 default:
787 dbg_assert_failed("unknown team mode");
788 }
789 if((Config()->m_SvTeam == SV_TEAM_ALLOWED || Config()->m_SvTeam == SV_TEAM_MANDATORY) && (Config()->m_SvMinTeamSize != DefaultConfig::SvMinTeamSize || Config()->m_SvMaxTeamSize != DefaultConfig::SvMaxTeamSize))
790 {
791 if(Config()->m_SvMinTeamSize != DefaultConfig::SvMinTeamSize && Config()->m_SvMaxTeamSize != DefaultConfig::SvMaxTeamSize)
792 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s (%s %d, %s %d)", Localize(pStr: "Teams"), pTeamMode, Localize(pStr: "minimum", pContext: "Team size"), Config()->m_SvMinTeamSize, Localize(pStr: "maximum", pContext: "Team size"), Config()->m_SvMaxTeamSize);
793 else if(Config()->m_SvMinTeamSize != DefaultConfig::SvMinTeamSize)
794 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s (%s %d)", Localize(pStr: "Teams"), pTeamMode, Localize(pStr: "minimum", pContext: "Team size"), Config()->m_SvMinTeamSize);
795 else
796 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s (%s %d)", Localize(pStr: "Teams"), pTeamMode, Localize(pStr: "maximum", pContext: "Team size"), Config()->m_SvMaxTeamSize);
797 }
798 else
799 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Teams"), pTeamMode);
800 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
801 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
802 }
803
804 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
805 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d/%d", Localize(pStr: "Players"), GameClient()->m_Snap.m_NumPlayers, CurrentServerInfo.m_MaxClients);
806 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
807
808 RenderServerInfoMotd(Motd);
809}
810
811void CMenus::RenderServerInfoMotd(CUIRect Motd)
812{
813 const float MotdFontSize = 16.0f;
814 Motd.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
815 Motd.Margin(Cut: 10.0f, pOtherRect: &Motd);
816
817 CUIRect MotdHeader;
818 Motd.HSplitTop(Cut: 2.0f * MotdFontSize, pTop: &MotdHeader, pBottom: &Motd);
819 Motd.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Motd);
820 Ui()->DoLabel(pRect: &MotdHeader, pText: Localize(pStr: "MOTD"), Size: 2.0f * MotdFontSize, Align: TEXTALIGN_ML);
821
822 if(!GameClient()->m_Motd.ServerMotd()[0])
823 return;
824
825 static CScrollRegion s_ScrollRegion;
826 CScrollRegionParams ScrollParams;
827 ScrollParams.m_ScrollUnit = 5 * MotdFontSize;
828 s_ScrollRegion.Begin(pClipRect: &Motd, pParams: &ScrollParams);
829
830 static float s_MotdHeight = 0.0f;
831 static int64_t s_MotdLastUpdateTime = -1;
832 if(!m_MotdTextContainerIndex.Valid() || s_MotdLastUpdateTime == -1 || s_MotdLastUpdateTime != GameClient()->m_Motd.ServerMotdUpdateTime())
833 {
834 CTextCursor Cursor;
835 Cursor.m_FontSize = MotdFontSize;
836 Cursor.m_LineWidth = Motd.w;
837 TextRender()->RecreateTextContainer(TextContainerIndex&: m_MotdTextContainerIndex, pCursor: &Cursor, pText: GameClient()->m_Motd.ServerMotd());
838 s_MotdHeight = Cursor.Height();
839 s_MotdLastUpdateTime = GameClient()->m_Motd.ServerMotdUpdateTime();
840 }
841
842 CUIRect MotdTextArea;
843 Motd.HSplitTop(Cut: s_MotdHeight, pTop: &MotdTextArea, pBottom: &Motd);
844 s_ScrollRegion.AddRect(Rect: MotdTextArea);
845
846 if(m_MotdTextContainerIndex.Valid())
847 TextRender()->RenderTextContainer(TextContainerIndex: m_MotdTextContainerIndex, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: TextRender()->DefaultTextOutlineColor(), X: MotdTextArea.x, Y: MotdTextArea.y);
848
849 s_ScrollRegion.End();
850}
851
852bool CMenus::RenderServerControlServer(CUIRect MainView, bool UpdateScroll)
853{
854 CUIRect List = MainView;
855 int NumVoteOptions = 0;
856 int aIndices[MAX_VOTE_OPTIONS];
857 int Selected = -1;
858 int TotalShown = 0;
859
860 int i = 0;
861 for(const CVoteOptionClient *pOption = GameClient()->m_Voting.FirstOption(); pOption; pOption = pOption->m_pNext, i++)
862 {
863 if(!m_FilterInput.IsEmpty() && !str_utf8_find_nocase(haystack: pOption->m_aDescription, needle: m_FilterInput.GetString()))
864 continue;
865 if(i == m_CallvoteSelectedOption)
866 Selected = TotalShown;
867 TotalShown++;
868 }
869
870 static CListBox s_ListBox;
871 s_ListBox.DoStart(RowHeight: 19.0f, NumItems: TotalShown, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: Selected, pRect: &List);
872
873 i = 0;
874 for(const CVoteOptionClient *pOption = GameClient()->m_Voting.FirstOption(); pOption; pOption = pOption->m_pNext, i++)
875 {
876 if(!m_FilterInput.IsEmpty() && !str_utf8_find_nocase(haystack: pOption->m_aDescription, needle: m_FilterInput.GetString()))
877 continue;
878 aIndices[NumVoteOptions] = i;
879 NumVoteOptions++;
880
881 const CListboxItem Item = s_ListBox.DoNextItem(pId: pOption);
882 if(!Item.m_Visible)
883 continue;
884
885 CUIRect Label;
886 Item.m_Rect.VMargin(Cut: 2.0f, pOtherRect: &Label);
887 Ui()->DoLabel(pRect: &Label, pText: pOption->m_aDescription, Size: 13.0f, Align: TEXTALIGN_ML);
888 }
889
890 Selected = s_ListBox.DoEnd();
891 if(UpdateScroll)
892 s_ListBox.ScrollToSelected();
893 m_CallvoteSelectedOption = Selected != -1 ? aIndices[Selected] : -1;
894 return s_ListBox.WasItemActivated();
895}
896
897bool CMenus::RenderServerControlKick(CUIRect MainView, bool FilterSpectators, bool UpdateScroll)
898{
899 int NumOptions = 0;
900 int Selected = -1;
901 int aPlayerIds[MAX_CLIENTS];
902 for(const auto &pInfoByName : GameClient()->m_Snap.m_apInfoByName)
903 {
904 if(!pInfoByName)
905 continue;
906
907 int Index = pInfoByName->m_ClientId;
908 if(Index == GameClient()->m_Snap.m_LocalClientId || (FilterSpectators && pInfoByName->m_Team == TEAM_SPECTATORS))
909 continue;
910
911 if(!str_utf8_find_nocase(haystack: GameClient()->m_aClients[Index].m_aName, needle: m_FilterInput.GetString()))
912 continue;
913
914 if(m_CallvoteSelectedPlayer == Index)
915 Selected = NumOptions;
916 aPlayerIds[NumOptions] = Index;
917 NumOptions++;
918 }
919
920 static CListBox s_ListBox;
921 s_ListBox.DoStart(RowHeight: 24.0f, NumItems: NumOptions, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: Selected, pRect: &MainView);
922
923 for(int i = 0; i < NumOptions; i++)
924 {
925 const CListboxItem Item = s_ListBox.DoNextItem(pId: &aPlayerIds[i]);
926 if(!Item.m_Visible)
927 continue;
928
929 CUIRect TeeRect, Label;
930 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h, pLeft: &TeeRect, pRight: &Label);
931
932 CTeeRenderInfo TeeInfo = GameClient()->m_aClients[aPlayerIds[i]].m_RenderInfo;
933 TeeInfo.m_Size = TeeRect.h;
934
935 const CAnimState *pIdleState = CAnimState::GetIdle();
936 vec2 OffsetToMid;
937 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
938 vec2 TeeRenderPos(TeeRect.x + TeeInfo.m_Size / 2, TeeRect.y + TeeInfo.m_Size / 2 + OffsetToMid.y);
939
940 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
941
942 Ui()->DoLabel(pRect: &Label, pText: GameClient()->m_aClients[aPlayerIds[i]].m_aName, Size: 16.0f, Align: TEXTALIGN_ML);
943 }
944
945 Selected = s_ListBox.DoEnd();
946 if(UpdateScroll)
947 s_ListBox.ScrollToSelected();
948 m_CallvoteSelectedPlayer = Selected != -1 ? aPlayerIds[Selected] : -1;
949 return s_ListBox.WasItemActivated();
950}
951
952void CMenus::RenderServerControl(CUIRect MainView)
953{
954 enum class EServerControlTab
955 {
956 SETTINGS,
957 KICKVOTE,
958 SPECVOTE,
959 };
960 static EServerControlTab s_ControlPage = EServerControlTab::SETTINGS;
961
962 // render background
963 CUIRect Bottom, RconExtension, TabBar, Button;
964 MainView.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &MainView);
965 Bottom.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
966 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
967 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
968 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
969
970 if(Client()->RconAuthed())
971 MainView.HSplitBottom(Cut: 90.0f, pTop: &MainView, pBottom: &RconExtension);
972
973 // tab bar
974 TabBar.VSplitLeft(Cut: TabBar.w / 3, pLeft: &Button, pRight: &TabBar);
975 static CButtonContainer s_Button0;
976 if(DoButton_MenuTab(pButtonContainer: &s_Button0, pText: Localize(pStr: "Change settings"), Checked: s_ControlPage == EServerControlTab::SETTINGS, pRect: &Button, Corners: IGraphics::CORNER_NONE))
977 s_ControlPage = EServerControlTab::SETTINGS;
978
979 TabBar.VSplitMid(pLeft: &Button, pRight: &TabBar);
980 static CButtonContainer s_Button1;
981 if(DoButton_MenuTab(pButtonContainer: &s_Button1, pText: Localize(pStr: "Kick player"), Checked: s_ControlPage == EServerControlTab::KICKVOTE, pRect: &Button, Corners: IGraphics::CORNER_NONE))
982 s_ControlPage = EServerControlTab::KICKVOTE;
983
984 static CButtonContainer s_Button2;
985 if(DoButton_MenuTab(pButtonContainer: &s_Button2, pText: Localize(pStr: "Move player to spectators"), Checked: s_ControlPage == EServerControlTab::SPECVOTE, pRect: &TabBar, Corners: IGraphics::CORNER_NONE))
986 s_ControlPage = EServerControlTab::SPECVOTE;
987
988 // render page
989 MainView.HSplitBottom(Cut: ms_ButtonHeight + 5 * 2, pTop: &MainView, pBottom: &Bottom);
990 Bottom.HMargin(Cut: 5.0f, pOtherRect: &Bottom);
991 Bottom.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Bottom);
992
993 // render quick search
994 CUIRect QuickSearch;
995 Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Bottom);
996 Bottom.VSplitLeft(Cut: 250.0f, pLeft: &QuickSearch, pRight: &Bottom);
997 if(m_ControlPageOpening)
998 {
999 m_ControlPageOpening = false;
1000 Ui()->SetActiveItem(&m_FilterInput);
1001 m_FilterInput.SelectAll();
1002 }
1003 bool Searching = Ui()->DoEditBox_Search(pLineInput: &m_FilterInput, pRect: &QuickSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive());
1004
1005 // vote menu
1006 bool Call = false;
1007 if(s_ControlPage == EServerControlTab::SETTINGS)
1008 Call = RenderServerControlServer(MainView, UpdateScroll: Searching);
1009 else if(s_ControlPage == EServerControlTab::KICKVOTE)
1010 Call = RenderServerControlKick(MainView, FilterSpectators: false, UpdateScroll: Searching);
1011 else if(s_ControlPage == EServerControlTab::SPECVOTE)
1012 Call = RenderServerControlKick(MainView, FilterSpectators: true, UpdateScroll: Searching);
1013
1014 // call vote
1015 Bottom.VSplitRight(Cut: 10.0f, pLeft: &Bottom, pRight: nullptr);
1016 Bottom.VSplitRight(Cut: 120.0f, pLeft: &Bottom, pRight: &Button);
1017
1018 static CButtonContainer s_CallVoteButton;
1019 if(DoButton_Menu(pButtonContainer: &s_CallVoteButton, pText: Localize(pStr: "Call vote"), Checked: 0, pRect: &Button) || Call)
1020 {
1021 if(s_ControlPage == EServerControlTab::SETTINGS)
1022 {
1023 if(0 <= m_CallvoteSelectedOption && m_CallvoteSelectedOption < GameClient()->m_Voting.NumOptions())
1024 {
1025 GameClient()->m_Voting.CallvoteOption(OptionId: m_CallvoteSelectedOption, pReason: m_CallvoteReasonInput.GetString());
1026 if(g_Config.m_UiCloseWindowAfterChangingSetting)
1027 SetActive(false);
1028 }
1029 }
1030 else if(s_ControlPage == EServerControlTab::KICKVOTE)
1031 {
1032 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
1033 GameClient()->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
1034 {
1035 GameClient()->m_Voting.CallvoteKick(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString());
1036 SetActive(false);
1037 }
1038 }
1039 else if(s_ControlPage == EServerControlTab::SPECVOTE)
1040 {
1041 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
1042 GameClient()->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
1043 {
1044 GameClient()->m_Voting.CallvoteSpectate(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString());
1045 SetActive(false);
1046 }
1047 }
1048 m_CallvoteReasonInput.Clear();
1049 }
1050
1051 // render kick reason
1052 CUIRect Reason;
1053 Bottom.VSplitRight(Cut: 20.0f, pLeft: &Bottom, pRight: nullptr);
1054 Bottom.VSplitRight(Cut: 200.0f, pLeft: &Bottom, pRight: &Reason);
1055 const char *pLabel = Localize(pStr: "Reason:");
1056 Ui()->DoLabel(pRect: &Reason, pText: pLabel, Size: 14.0f, Align: TEXTALIGN_ML);
1057 float w = TextRender()->TextWidth(Size: 14.0f, pText: pLabel, StrLength: -1, LineWidth: -1.0f);
1058 Reason.VSplitLeft(Cut: w + 10.0f, pLeft: nullptr, pRight: &Reason);
1059 if(Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed())
1060 {
1061 Ui()->SetActiveItem(&m_CallvoteReasonInput);
1062 m_CallvoteReasonInput.SelectAll();
1063 }
1064 Ui()->DoEditBox(pLineInput: &m_CallvoteReasonInput, pRect: &Reason, FontSize: 14.0f);
1065
1066 // vote option loading indicator
1067 if(s_ControlPage == EServerControlTab::SETTINGS && GameClient()->m_Voting.IsReceivingOptions())
1068 {
1069 CUIRect Spinner, LoadingLabel;
1070 Bottom.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &Bottom);
1071 Bottom.VSplitLeft(Cut: 16.0f, pLeft: &Spinner, pRight: &Bottom);
1072 Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Bottom);
1073 Bottom.VSplitRight(Cut: 10.0f, pLeft: &LoadingLabel, pRight: nullptr);
1074 Ui()->RenderProgressSpinner(Center: Spinner.Center(), OuterRadius: 8.0f);
1075 Ui()->DoLabel(pRect: &LoadingLabel, pText: Localize(pStr: "Loading…"), Size: 14.0f, Align: TEXTALIGN_ML);
1076 }
1077
1078 // extended features (only available when authed in rcon)
1079 if(Client()->RconAuthed())
1080 {
1081 // background
1082 RconExtension.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &RconExtension);
1083 RconExtension.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &RconExtension);
1084 RconExtension.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &RconExtension);
1085
1086 // force vote
1087 Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Bottom);
1088 Bottom.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &Bottom);
1089
1090 static CButtonContainer s_ForceVoteButton;
1091 if(DoButton_Menu(pButtonContainer: &s_ForceVoteButton, pText: Localize(pStr: "Force vote"), Checked: 0, pRect: &Button))
1092 {
1093 if(s_ControlPage == EServerControlTab::SETTINGS)
1094 {
1095 GameClient()->m_Voting.CallvoteOption(OptionId: m_CallvoteSelectedOption, pReason: m_CallvoteReasonInput.GetString(), ForceVote: true);
1096 }
1097 else if(s_ControlPage == EServerControlTab::KICKVOTE)
1098 {
1099 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
1100 GameClient()->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
1101 {
1102 GameClient()->m_Voting.CallvoteKick(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString(), ForceVote: true);
1103 SetActive(false);
1104 }
1105 }
1106 else if(s_ControlPage == EServerControlTab::SPECVOTE)
1107 {
1108 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
1109 GameClient()->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
1110 {
1111 GameClient()->m_Voting.CallvoteSpectate(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString(), ForceVote: true);
1112 SetActive(false);
1113 }
1114 }
1115 m_CallvoteReasonInput.Clear();
1116 }
1117
1118 if(s_ControlPage == EServerControlTab::SETTINGS)
1119 {
1120 // remove vote
1121 Bottom.VSplitRight(Cut: 10.0f, pLeft: &Bottom, pRight: nullptr);
1122 Bottom.VSplitRight(Cut: 120.0f, pLeft: nullptr, pRight: &Button);
1123 static CButtonContainer s_RemoveVoteButton;
1124 if(DoButton_Menu(pButtonContainer: &s_RemoveVoteButton, pText: Localize(pStr: "Remove"), Checked: 0, pRect: &Button))
1125 GameClient()->m_Voting.RemovevoteOption(OptionId: m_CallvoteSelectedOption);
1126
1127 // add vote
1128 RconExtension.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &RconExtension);
1129 Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Bottom);
1130 Bottom.VSplitLeft(Cut: 250.0f, pLeft: &Button, pRight: &Bottom);
1131 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Vote description:"), Size: 14.0f, Align: TEXTALIGN_ML);
1132
1133 Bottom.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &Button);
1134 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Vote command:"), Size: 14.0f, Align: TEXTALIGN_ML);
1135
1136 static CLineInputBuffered<VOTE_DESC_LENGTH> s_VoteDescriptionInput;
1137 static CLineInputBuffered<VOTE_CMD_LENGTH> s_VoteCommandInput;
1138 RconExtension.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &RconExtension);
1139 Bottom.VSplitRight(Cut: 10.0f, pLeft: &Bottom, pRight: nullptr);
1140 Bottom.VSplitRight(Cut: 120.0f, pLeft: &Bottom, pRight: &Button);
1141 static CButtonContainer s_AddVoteButton;
1142 if(DoButton_Menu(pButtonContainer: &s_AddVoteButton, pText: Localize(pStr: "Add"), Checked: 0, pRect: &Button))
1143 if(!s_VoteDescriptionInput.IsEmpty() && !s_VoteCommandInput.IsEmpty())
1144 GameClient()->m_Voting.AddvoteOption(pDescription: s_VoteDescriptionInput.GetString(), pCommand: s_VoteCommandInput.GetString());
1145
1146 Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Bottom);
1147 Bottom.VSplitLeft(Cut: 250.0f, pLeft: &Button, pRight: &Bottom);
1148 Ui()->DoEditBox(pLineInput: &s_VoteDescriptionInput, pRect: &Button, FontSize: 14.0f);
1149
1150 Bottom.VMargin(Cut: 20.0f, pOtherRect: &Button);
1151 Ui()->DoEditBox(pLineInput: &s_VoteCommandInput, pRect: &Button, FontSize: 14.0f);
1152 }
1153 }
1154}
1155
1156void CMenus::RenderInGameNetwork(CUIRect MainView)
1157{
1158 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1159
1160 CUIRect TabBar, Button;
1161 MainView.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &MainView);
1162
1163 int NewPage = g_Config.m_UiPage;
1164
1165 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1166 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);
1167
1168 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
1169 static CButtonContainer s_InternetButton;
1170 if(DoButton_MenuTab(pButtonContainer: &s_InternetButton, pText: FontIcon::EARTH_AMERICAS, Checked: g_Config.m_UiPage == PAGE_INTERNET, pRect: &Button, Corners: IGraphics::CORNER_NONE))
1171 {
1172 NewPage = PAGE_INTERNET;
1173 }
1174 GameClient()->m_Tooltips.DoToolTip(pId: &s_InternetButton, pNearRect: &Button, pText: Localize(pStr: "Internet"));
1175
1176 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
1177 static CButtonContainer s_LanButton;
1178 if(DoButton_MenuTab(pButtonContainer: &s_LanButton, pText: FontIcon::NETWORK_WIRED, Checked: g_Config.m_UiPage == PAGE_LAN, pRect: &Button, Corners: IGraphics::CORNER_NONE))
1179 {
1180 NewPage = PAGE_LAN;
1181 }
1182 GameClient()->m_Tooltips.DoToolTip(pId: &s_LanButton, pNearRect: &Button, pText: Localize(pStr: "LAN"));
1183
1184 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
1185 static CButtonContainer s_FavoritesButton;
1186 if(DoButton_MenuTab(pButtonContainer: &s_FavoritesButton, pText: FontIcon::STAR, Checked: g_Config.m_UiPage == PAGE_FAVORITES, pRect: &Button, Corners: IGraphics::CORNER_NONE))
1187 {
1188 NewPage = PAGE_FAVORITES;
1189 }
1190 GameClient()->m_Tooltips.DoToolTip(pId: &s_FavoritesButton, pNearRect: &Button, pText: Localize(pStr: "Favorites"));
1191
1192 const int MaxPage = PAGE_FAVORITES + ServerBrowser()->FavoriteCommunities().size();
1193 if(
1194 !Ui()->IsPopupOpen() &&
1195 CLineInput::GetActiveInput() == nullptr &&
1196 (g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= MaxPage) &&
1197 (m_MenuPage >= PAGE_INTERNET && m_MenuPage <= PAGE_FAVORITE_COMMUNITY_5))
1198 {
1199 if(Input()->KeyPress(Key: KEY_RIGHT))
1200 {
1201 NewPage = g_Config.m_UiPage + 1;
1202 if(NewPage > MaxPage)
1203 NewPage = PAGE_INTERNET;
1204 }
1205 if(Input()->KeyPress(Key: KEY_LEFT))
1206 {
1207 NewPage = g_Config.m_UiPage - 1;
1208 if(NewPage < PAGE_INTERNET)
1209 NewPage = MaxPage;
1210 }
1211 }
1212
1213 size_t FavoriteCommunityIndex = 0;
1214 static CButtonContainer s_aFavoriteCommunityButtons[5];
1215 static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)PAGE_FAVORITE_COMMUNITY_5 - PAGE_FAVORITE_COMMUNITY_1 + 1);
1216 for(const CCommunity *pCommunity : ServerBrowser()->FavoriteCommunities())
1217 {
1218 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
1219 const int Page = PAGE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex;
1220 if(DoButton_MenuTab(pButtonContainer: &s_aFavoriteCommunityButtons[FavoriteCommunityIndex], pText: FontIcon::ELLIPSIS, Checked: g_Config.m_UiPage == Page, pRect: &Button, Corners: IGraphics::CORNER_NONE, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 10.0f, pCommunityIcon: m_CommunityIcons.Find(pCommunityId: pCommunity->Id())))
1221 {
1222 NewPage = Page;
1223 }
1224 GameClient()->m_Tooltips.DoToolTip(pId: &s_aFavoriteCommunityButtons[FavoriteCommunityIndex], pNearRect: &Button, pText: pCommunity->Name());
1225
1226 ++FavoriteCommunityIndex;
1227 if(FavoriteCommunityIndex >= std::size(s_aFavoriteCommunityButtons))
1228 break;
1229 }
1230
1231 TextRender()->SetRenderFlags(0);
1232 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1233
1234 if(NewPage != g_Config.m_UiPage)
1235 {
1236 SetMenuPage(NewPage);
1237 }
1238
1239 RenderServerbrowser(MainView);
1240}
1241
1242// ghost stuff
1243int CMenus::GhostlistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
1244{
1245 CMenus *pSelf = (CMenus *)pUser;
1246 const char *pMap = pSelf->GameClient()->Map()->BaseName();
1247 if(IsDir || !str_endswith(str: pInfo->m_pName, suffix: ".gho") || !str_startswith(str: pInfo->m_pName, prefix: pMap))
1248 return 0;
1249
1250 char aFilename[IO_MAX_PATH_LENGTH];
1251 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "%s/%s", pSelf->GameClient()->m_Ghost.GetGhostDir(), pInfo->m_pName);
1252
1253 CGhostInfo Info;
1254 if(!pSelf->GameClient()->m_Ghost.GhostLoader()->GetGhostInfo(pFilename: aFilename, pInfo: &Info, pMap, MapSha256: pSelf->GameClient()->Map()->Sha256(), MapCrc: pSelf->GameClient()->Map()->Crc()))
1255 return 0;
1256
1257 CGhostItem Item;
1258 str_copy(dst&: Item.m_aFilename, src: aFilename);
1259 str_copy(dst&: Item.m_aPlayer, src: Info.m_aOwner);
1260 Item.m_Date = pInfo->m_TimeModified;
1261 Item.m_Time = Info.m_Time;
1262 if(Item.m_Time > 0)
1263 pSelf->m_vGhosts.push_back(x: Item);
1264
1265 if(time_get_nanoseconds() - pSelf->m_GhostPopulateStartTime > 500ms)
1266 {
1267 pSelf->RenderLoading(pCaption: Localize(pStr: "Loading ghost files"), pContent: "", IncreaseCounter: 0);
1268 }
1269
1270 return 0;
1271}
1272
1273void CMenus::GhostlistPopulate()
1274{
1275 m_vGhosts.clear();
1276 m_GhostPopulateStartTime = time_get_nanoseconds();
1277 Storage()->ListDirectoryInfo(Type: IStorage::TYPE_ALL, pPath: GameClient()->m_Ghost.GetGhostDir(), pfnCallback: GhostlistFetchCallback, pUser: this);
1278 SortGhostlist();
1279
1280 CGhostItem *pOwnGhost = nullptr;
1281 for(auto &Ghost : m_vGhosts)
1282 {
1283 Ghost.m_Failed = false;
1284 if(str_comp(a: Ghost.m_aPlayer, b: Client()->PlayerName()) == 0 && (!pOwnGhost || Ghost < *pOwnGhost))
1285 pOwnGhost = &Ghost;
1286 }
1287
1288 if(pOwnGhost)
1289 {
1290 pOwnGhost->m_Own = true;
1291 pOwnGhost->m_Slot = GameClient()->m_Ghost.Load(pFilename: pOwnGhost->m_aFilename);
1292 }
1293}
1294
1295CMenus::CGhostItem *CMenus::GetOwnGhost()
1296{
1297 for(auto &Ghost : m_vGhosts)
1298 if(Ghost.m_Own)
1299 return &Ghost;
1300 return nullptr;
1301}
1302
1303void CMenus::UpdateOwnGhost(CGhostItem Item)
1304{
1305 int Own = -1;
1306 for(size_t i = 0; i < m_vGhosts.size(); i++)
1307 if(m_vGhosts[i].m_Own)
1308 Own = i;
1309
1310 if(Own == -1)
1311 {
1312 Item.m_Own = true;
1313 }
1314 else if(g_Config.m_ClRaceGhostSaveBest && (Item.HasFile() || !m_vGhosts[Own].HasFile()))
1315 {
1316 Item.m_Own = true;
1317 DeleteGhostItem(Index: Own);
1318 }
1319 else if(m_vGhosts[Own].m_Time > Item.m_Time)
1320 {
1321 Item.m_Own = true;
1322 m_vGhosts[Own].m_Own = false;
1323 m_vGhosts[Own].m_Slot = -1;
1324 }
1325 else
1326 {
1327 Item.m_Own = false;
1328 Item.m_Slot = -1;
1329 }
1330
1331 Item.m_Date = std::time(timer: nullptr);
1332 Item.m_Failed = false;
1333 m_vGhosts.insert(position: std::lower_bound(first: m_vGhosts.begin(), last: m_vGhosts.end(), val: Item), x: Item);
1334 SortGhostlist();
1335}
1336
1337void CMenus::DeleteGhostItem(int Index)
1338{
1339 if(m_vGhosts[Index].HasFile())
1340 Storage()->RemoveFile(pFilename: m_vGhosts[Index].m_aFilename, Type: IStorage::TYPE_SAVE);
1341 m_vGhosts.erase(position: m_vGhosts.begin() + Index);
1342}
1343
1344void CMenus::SortGhostlist()
1345{
1346 if(g_Config.m_GhSort == GHOST_SORT_NAME)
1347 std::stable_sort(first: m_vGhosts.begin(), last: m_vGhosts.end(), comp: [](const CGhostItem &Left, const CGhostItem &Right) {
1348 return g_Config.m_GhSortOrder ? (str_comp(a: Left.m_aPlayer, b: Right.m_aPlayer) > 0) : (str_comp(a: Left.m_aPlayer, b: Right.m_aPlayer) < 0);
1349 });
1350 else if(g_Config.m_GhSort == GHOST_SORT_TIME)
1351 std::stable_sort(first: m_vGhosts.begin(), last: m_vGhosts.end(), comp: [](const CGhostItem &Left, const CGhostItem &Right) {
1352 return g_Config.m_GhSortOrder ? (Left.m_Time > Right.m_Time) : (Left.m_Time < Right.m_Time);
1353 });
1354 else if(g_Config.m_GhSort == GHOST_SORT_DATE)
1355 std::stable_sort(first: m_vGhosts.begin(), last: m_vGhosts.end(), comp: [](const CGhostItem &Left, const CGhostItem &Right) {
1356 return g_Config.m_GhSortOrder ? (Left.m_Date > Right.m_Date) : (Left.m_Date < Right.m_Date);
1357 });
1358}
1359
1360void CMenus::RenderGhost(CUIRect MainView)
1361{
1362 // render background
1363 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1364
1365 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1366 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
1367 MainView.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MainView);
1368 MainView.VSplitRight(Cut: 5.0f, pLeft: &MainView, pRight: nullptr);
1369
1370 CUIRect Headers, Status;
1371 CUIRect View = MainView;
1372
1373 View.HSplitTop(Cut: 17.0f, pTop: &Headers, pBottom: &View);
1374 View.HSplitBottom(Cut: 28.0f, pTop: &View, pBottom: &Status);
1375
1376 // split of the scrollbar
1377 Headers.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1378 Headers.VSplitRight(Cut: 20.0f, pLeft: &Headers, pRight: nullptr);
1379
1380 class CColumn
1381 {
1382 public:
1383 const char *m_pCaption;
1384 int m_Id;
1385 int m_Sort;
1386 float m_Width;
1387 CUIRect m_Rect;
1388 };
1389
1390 enum
1391 {
1392 COL_ACTIVE = 0,
1393 COL_NAME,
1394 COL_TIME,
1395 COL_DATE,
1396 };
1397
1398 static CColumn s_aCols[] = {
1399 {.m_pCaption: "", .m_Id: -1, .m_Sort: GHOST_SORT_NONE, .m_Width: 2.0f, .m_Rect: {.x: 0}},
1400 {.m_pCaption: "", .m_Id: COL_ACTIVE, .m_Sort: GHOST_SORT_NONE, .m_Width: 30.0f, .m_Rect: {.x: 0}},
1401 {.m_pCaption: Localizable(pStr: "Name"), .m_Id: COL_NAME, .m_Sort: GHOST_SORT_NAME, .m_Width: 200.0f, .m_Rect: {.x: 0}},
1402 {.m_pCaption: Localizable(pStr: "Time"), .m_Id: COL_TIME, .m_Sort: GHOST_SORT_TIME, .m_Width: 90.0f, .m_Rect: {.x: 0}},
1403 {.m_pCaption: Localizable(pStr: "Date"), .m_Id: COL_DATE, .m_Sort: GHOST_SORT_DATE, .m_Width: 150.0f, .m_Rect: {.x: 0}},
1404 };
1405
1406 int NumCols = std::size(s_aCols);
1407
1408 // do layout
1409 for(int i = 0; i < NumCols; i++)
1410 {
1411 Headers.VSplitLeft(Cut: s_aCols[i].m_Width, pLeft: &s_aCols[i].m_Rect, pRight: &Headers);
1412
1413 if(i + 1 < NumCols)
1414 Headers.VSplitLeft(Cut: 2, pLeft: nullptr, pRight: &Headers);
1415 }
1416
1417 // do headers
1418 for(const auto &Col : s_aCols)
1419 {
1420 if(DoButton_GridHeader(pId: &Col.m_Id, pText: Localize(pStr: Col.m_pCaption), Checked: g_Config.m_GhSort == Col.m_Sort, pRect: &Col.m_Rect))
1421 {
1422 if(Col.m_Sort != GHOST_SORT_NONE)
1423 {
1424 if(g_Config.m_GhSort == Col.m_Sort)
1425 g_Config.m_GhSortOrder ^= 1;
1426 else
1427 g_Config.m_GhSortOrder = 0;
1428 g_Config.m_GhSort = Col.m_Sort;
1429
1430 SortGhostlist();
1431 }
1432 }
1433 }
1434
1435 View.Draw(Color: ColorRGBA(0, 0, 0, 0.15f), Corners: 0, Rounding: 0);
1436
1437 const int NumGhosts = m_vGhosts.size();
1438 int NumFailed = 0;
1439 int NumActivated = 0;
1440 static int s_SelectedIndex = 0;
1441 static CListBox s_ListBox;
1442 s_ListBox.DoStart(RowHeight: 17.0f, NumItems: NumGhosts, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_SelectedIndex, pRect: &View, Background: false);
1443
1444 for(int i = 0; i < NumGhosts; i++)
1445 {
1446 const CGhostItem *pGhost = &m_vGhosts[i];
1447 const CListboxItem Item = s_ListBox.DoNextItem(pId: pGhost);
1448
1449 if(pGhost->m_Failed)
1450 NumFailed++;
1451 if(pGhost->Active())
1452 NumActivated++;
1453
1454 if(!Item.m_Visible)
1455 continue;
1456
1457 ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f);
1458 if(pGhost->m_Own)
1459 Color = color_cast<ColorRGBA>(hsl: ColorHSLA(0.33f, 1.0f, 0.75f));
1460
1461 if(pGhost->m_Failed)
1462 Color = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f);
1463
1464 TextRender()->TextColor(Color: Color.WithAlpha(alpha: pGhost->HasFile() ? 1.0f : 0.5f));
1465
1466 for(int c = 0; c < NumCols; c++)
1467 {
1468 CUIRect Button;
1469 Button.x = s_aCols[c].m_Rect.x;
1470 Button.y = Item.m_Rect.y;
1471 Button.h = Item.m_Rect.h;
1472 Button.w = s_aCols[c].m_Rect.w;
1473
1474 int Id = s_aCols[c].m_Id;
1475
1476 if(Id == COL_ACTIVE)
1477 {
1478 if(pGhost->Active())
1479 {
1480 Graphics()->WrapClamp();
1481 Graphics()->TextureSet(Texture: GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[(SPRITE_OOP + 7) - SPRITE_OOP]);
1482 Graphics()->QuadsBegin();
1483 IGraphics::CQuadItem QuadItem(Button.x + Button.w / 2, Button.y + Button.h / 2, 20.0f, 20.0f);
1484 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1485
1486 Graphics()->QuadsEnd();
1487 Graphics()->WrapNormal();
1488 }
1489 }
1490 else if(Id == COL_NAME)
1491 {
1492 Ui()->DoLabel(pRect: &Button, pText: pGhost->m_aPlayer, Size: 12.0f, Align: TEXTALIGN_ML);
1493 }
1494 else if(Id == COL_TIME)
1495 {
1496 char aBuf[64];
1497 str_time(centisecs: pGhost->m_Time / 10, format: ETimeFormat::HOURS_CENTISECS, buffer: aBuf, buffer_size: sizeof(aBuf));
1498 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_ML);
1499 }
1500 else if(Id == COL_DATE)
1501 {
1502 char aBuf[64];
1503 str_timestamp_ex(time: pGhost->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), format: TimestampFormat::SPACE);
1504 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_ML);
1505 }
1506 }
1507
1508 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1509 }
1510
1511 s_SelectedIndex = s_ListBox.DoEnd();
1512
1513 Status.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1514 Status.Margin(Cut: 5.0f, pOtherRect: &Status);
1515
1516 CUIRect Button;
1517 Status.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &Status);
1518
1519 static CButtonContainer s_ReloadButton;
1520 static CButtonContainer s_DirectoryButton;
1521 static CButtonContainer s_ActivateAll;
1522
1523 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_ReloadButton, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
1524 {
1525 GameClient()->m_Ghost.UnloadAll();
1526 GhostlistPopulate();
1527 }
1528
1529 Status.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &Status);
1530 Status.VSplitLeft(Cut: 175.0f, pLeft: &Button, pRight: &Status);
1531 if(DoButton_Menu(pButtonContainer: &s_DirectoryButton, pText: Localize(pStr: "Ghosts directory"), Checked: 0, pRect: &Button))
1532 {
1533 char aBuf[IO_MAX_PATH_LENGTH];
1534 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "ghosts", pBuffer: aBuf, BufferSize: sizeof(aBuf));
1535 Storage()->CreateFolder(pFoldername: "ghosts", Type: IStorage::TYPE_SAVE);
1536 Client()->ViewFile(pFilename: aBuf);
1537 }
1538
1539 Status.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &Status);
1540 if(NumGhosts - NumFailed > 0)
1541 {
1542 Status.VSplitLeft(Cut: 175.0f, pLeft: &Button, pRight: &Status);
1543 bool ActivateAll = ((NumGhosts - NumFailed) != NumActivated) && GameClient()->m_Ghost.FreeSlots();
1544
1545 const char *pActionText = ActivateAll ? Localize(pStr: "Activate all") : Localize(pStr: "Deactivate all");
1546 if(DoButton_Menu(pButtonContainer: &s_ActivateAll, pText: pActionText, Checked: 0, pRect: &Button))
1547 {
1548 for(int i = 0; i < NumGhosts; i++)
1549 {
1550 CGhostItem *pGhost = &m_vGhosts[i];
1551 if(pGhost->m_Failed || (ActivateAll && pGhost->m_Slot != -1))
1552 continue;
1553
1554 if(ActivateAll)
1555 {
1556 if(!GameClient()->m_Ghost.FreeSlots())
1557 break;
1558
1559 pGhost->m_Slot = GameClient()->m_Ghost.Load(pFilename: pGhost->m_aFilename);
1560 if(pGhost->m_Slot == -1)
1561 pGhost->m_Failed = true;
1562 }
1563 else
1564 {
1565 GameClient()->m_Ghost.UnloadAll();
1566 pGhost->m_Slot = -1;
1567 }
1568 }
1569 }
1570 }
1571
1572 if(s_SelectedIndex == -1 || s_SelectedIndex >= (int)m_vGhosts.size())
1573 return;
1574
1575 CGhostItem *pGhost = &m_vGhosts[s_SelectedIndex];
1576
1577 CGhostItem *pOwnGhost = GetOwnGhost();
1578 int ReservedSlots = !pGhost->m_Own && !(pOwnGhost && pOwnGhost->Active());
1579 if(!pGhost->m_Failed && pGhost->HasFile() && (pGhost->Active() || GameClient()->m_Ghost.FreeSlots() > ReservedSlots))
1580 {
1581 Status.VSplitRight(Cut: 120.0f, pLeft: &Status, pRight: &Button);
1582
1583 static CButtonContainer s_GhostButton;
1584 const char *pText = pGhost->Active() ? Localize(pStr: "Deactivate") : Localize(pStr: "Activate");
1585 if(DoButton_Menu(pButtonContainer: &s_GhostButton, pText, Checked: 0, pRect: &Button) || s_ListBox.WasItemActivated())
1586 {
1587 if(pGhost->Active())
1588 {
1589 GameClient()->m_Ghost.Unload(Slot: pGhost->m_Slot);
1590 pGhost->m_Slot = -1;
1591 }
1592 else
1593 {
1594 pGhost->m_Slot = GameClient()->m_Ghost.Load(pFilename: pGhost->m_aFilename);
1595 if(pGhost->m_Slot == -1)
1596 pGhost->m_Failed = true;
1597 }
1598 }
1599 Status.VSplitRight(Cut: 5.0f, pLeft: &Status, pRight: nullptr);
1600 }
1601
1602 Status.VSplitRight(Cut: 120.0f, pLeft: &Status, pRight: &Button);
1603
1604 static CButtonContainer s_DeleteButton;
1605 if(DoButton_Menu(pButtonContainer: &s_DeleteButton, pText: Localize(pStr: "Delete"), Checked: 0, pRect: &Button))
1606 {
1607 if(pGhost->Active())
1608 GameClient()->m_Ghost.Unload(Slot: pGhost->m_Slot);
1609 DeleteGhostItem(Index: s_SelectedIndex);
1610 }
1611
1612 Status.VSplitRight(Cut: 5.0f, pLeft: &Status, pRight: nullptr);
1613
1614 bool Recording = GameClient()->m_Ghost.GhostRecorder()->IsRecording();
1615 if(!pGhost->HasFile() && !Recording && pGhost->Active())
1616 {
1617 static CButtonContainer s_SaveButton;
1618 Status.VSplitRight(Cut: 120.0f, pLeft: &Status, pRight: &Button);
1619 if(DoButton_Menu(pButtonContainer: &s_SaveButton, pText: Localize(pStr: "Save"), Checked: 0, pRect: &Button))
1620 GameClient()->m_Ghost.SaveGhost(pItem: pGhost);
1621 }
1622}
1623
1624void CMenus::RenderIngameHint()
1625{
1626 // With touch controls enabled there is a Close button in the menu and usually no Escape key available.
1627 if(g_Config.m_ClTouchControls)
1628 return;
1629
1630 float Width = 300 * Graphics()->ScreenAspect();
1631 Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: Width, BottomRightY: 300);
1632 TextRender()->TextColor(r: 1, g: 1, b: 1, a: 1);
1633 TextRender()->Text(x: 5, y: 280, Size: 5, pText: Localize(pStr: "Menu opened. Press Esc key again to close menu."), LineWidth: -1.0f);
1634 Ui()->MapScreen();
1635}
1636