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