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 CServerInfo CurrentServerInfo;
631 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
632
633 CUIRect ServerInfo, GameInfo, Motd;
634 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
635 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
636 MainView.HSplitMid(pTop: &ServerInfo, pBottom: &Motd, Spacing: 10.0f);
637 ServerInfo.VSplitMid(pLeft: &ServerInfo, pRight: &GameInfo, Spacing: 10.0f);
638
639 ServerInfo.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
640 ServerInfo.Margin(Cut: 10.0f, pOtherRect: &ServerInfo);
641
642 CUIRect Label;
643 ServerInfo.HSplitTop(Cut: FontSizeTitle, pTop: &Label, pBottom: &ServerInfo);
644 ServerInfo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ServerInfo);
645 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Server info"), Size: FontSizeTitle, Align: TEXTALIGN_ML);
646
647 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
648 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: nullptr, pBottom: &ServerInfo);
649 Ui()->DoLabel(pRect: &Label, pText: CurrentServerInfo.m_aName, Size: FontSizeBody, Align: TEXTALIGN_ML);
650
651 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
652 char aBuf[256];
653 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Address"), CurrentServerInfo.m_aAddress);
654 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
655
656 if(GameClient()->m_Snap.m_pLocalInfo)
657 {
658 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
659 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d", Localize(pStr: "Ping"), GameClient()->m_Snap.m_pLocalInfo->m_Latency);
660 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
661 }
662
663 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
664 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Version"), CurrentServerInfo.m_aVersion);
665 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
666
667 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
668 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"));
669 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
670
671 const CCommunity *pCommunity = ServerBrowser()->Community(pCommunityId: CurrentServerInfo.m_aCommunityId);
672 if(pCommunity != nullptr)
673 {
674 ServerInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &ServerInfo);
675 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Community"));
676 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
677
678 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pCommunity->Id());
679 if(pIcon != nullptr)
680 {
681 Label.VSplitLeft(Cut: TextRender()->TextWidth(Size: FontSizeBody, pText: aBuf) + 8.0f, pLeft: nullptr, pRight: &Label);
682 Label.VSplitLeft(Cut: 2.0f * Label.h, pLeft: &Label, pRight: nullptr);
683 m_CommunityIcons.Render(pIcon, Rect: Label, Active: true);
684 static char s_CommunityTooltipButtonId;
685 Ui()->DoButtonLogic(pId: &s_CommunityTooltipButtonId, Checked: 0, pRect: &Label, Flags: BUTTONFLAG_NONE);
686 GameClient()->m_Tooltips.DoToolTip(pId: &s_CommunityTooltipButtonId, pNearRect: &Label, pText: pCommunity->Name());
687 }
688 }
689
690 // copy info button
691 {
692 CUIRect Button;
693 ServerInfo.HSplitBottom(Cut: 20.0f, pTop: &ServerInfo, pBottom: &Button);
694 Button.VSplitRight(Cut: 200.0f, pLeft: &ServerInfo, pRight: &Button);
695 static CButtonContainer s_CopyButton;
696 if(DoButton_Menu(pButtonContainer: &s_CopyButton, pText: Localize(pStr: "Copy info"), Checked: 0, pRect: &Button))
697 {
698 char aInfo[256];
699 str_format(
700 buffer: aInfo,
701 buffer_size: sizeof(aInfo),
702 format: "%s\n"
703 "Address: ddnet://%s\n"
704 "My IGN: %s\n",
705 CurrentServerInfo.m_aName,
706 CurrentServerInfo.m_aAddress,
707 Client()->PlayerName());
708 Input()->SetClipboardText(aInfo);
709 }
710 }
711
712 // favorite checkbox
713 {
714 CUIRect Button;
715 TRISTATE IsFavorite = Favorites()->IsFavorite(pAddrs: CurrentServerInfo.m_aAddresses, NumAddrs: CurrentServerInfo.m_NumAddresses);
716 ServerInfo.HSplitBottom(Cut: 20.0f, pTop: &ServerInfo, pBottom: &Button);
717 static int s_AddFavButton = 0;
718 if(DoButton_CheckBox(pId: &s_AddFavButton, pText: Localize(pStr: "Favorite"), Checked: IsFavorite != TRISTATE::NONE, pRect: &Button))
719 {
720 if(IsFavorite != TRISTATE::NONE)
721 Favorites()->Remove(pAddrs: CurrentServerInfo.m_aAddresses, NumAddrs: CurrentServerInfo.m_NumAddresses);
722 else
723 Favorites()->Add(pAddrs: CurrentServerInfo.m_aAddresses, NumAddrs: CurrentServerInfo.m_NumAddresses);
724 }
725 }
726
727 GameInfo.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
728 GameInfo.Margin(Cut: 10.0f, pOtherRect: &GameInfo);
729
730 GameInfo.HSplitTop(Cut: FontSizeTitle, pTop: &Label, pBottom: &GameInfo);
731 GameInfo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &GameInfo);
732 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Game info"), Size: FontSizeTitle, Align: TEXTALIGN_ML);
733
734 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
735 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Game type"), CurrentServerInfo.m_aGameType);
736 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
737
738 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
739 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Map"), CurrentServerInfo.m_aMap);
740 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
741
742 const auto *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
743 if(pGameInfoObj)
744 {
745 if(pGameInfoObj->m_ScoreLimit)
746 {
747 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
748 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d", Localize(pStr: "Score limit"), pGameInfoObj->m_ScoreLimit);
749 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
750 }
751
752 if(pGameInfoObj->m_TimeLimit)
753 {
754 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
755 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Time limit: %d min"), pGameInfoObj->m_TimeLimit);
756 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
757 }
758
759 if(pGameInfoObj->m_RoundCurrent && pGameInfoObj->m_RoundNum)
760 {
761 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
762 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Round %d/%d"), pGameInfoObj->m_RoundCurrent, pGameInfoObj->m_RoundNum);
763 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
764 }
765 }
766
767 if(GameClient()->m_GameInfo.m_DDRaceTeam)
768 {
769 const char *pTeamMode = nullptr;
770 switch(Config()->m_SvTeam)
771 {
772 case SV_TEAM_FORBIDDEN:
773 pTeamMode = Localize(pStr: "forbidden", pContext: "Team status");
774 break;
775 case SV_TEAM_ALLOWED:
776 if(g_Config.m_SvSoloServer)
777 pTeamMode = Localize(pStr: "solo", pContext: "Team status");
778 else
779 pTeamMode = Localize(pStr: "allowed", pContext: "Team status");
780 break;
781 case SV_TEAM_MANDATORY:
782 pTeamMode = Localize(pStr: "required", pContext: "Team status");
783 break;
784 case SV_TEAM_FORCED_SOLO:
785 pTeamMode = Localize(pStr: "solo", pContext: "Team status");
786 break;
787 default:
788 dbg_assert_failed("unknown team mode");
789 }
790 if((Config()->m_SvTeam == SV_TEAM_ALLOWED || Config()->m_SvTeam == SV_TEAM_MANDATORY) && (Config()->m_SvMinTeamSize != DefaultConfig::SvMinTeamSize || Config()->m_SvMaxTeamSize != DefaultConfig::SvMaxTeamSize))
791 {
792 if(Config()->m_SvMinTeamSize != DefaultConfig::SvMinTeamSize && Config()->m_SvMaxTeamSize != DefaultConfig::SvMaxTeamSize)
793 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);
794 else if(Config()->m_SvMinTeamSize != DefaultConfig::SvMinTeamSize)
795 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);
796 else
797 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);
798 }
799 else
800 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Teams"), pTeamMode);
801 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
802 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
803 }
804
805 GameInfo.HSplitTop(Cut: FontSizeBody, pTop: &Label, pBottom: &GameInfo);
806 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d/%d", Localize(pStr: "Players"), GameClient()->m_Snap.m_NumPlayers, CurrentServerInfo.m_MaxClients);
807 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FontSizeBody, Align: TEXTALIGN_ML);
808
809 RenderServerInfoMotd(Motd);
810}
811
812void CMenus::RenderServerInfoMotd(CUIRect Motd)
813{
814 const float MotdFontSize = 16.0f;
815 Motd.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
816 Motd.Margin(Cut: 10.0f, pOtherRect: &Motd);
817
818 CUIRect MotdHeader;
819 Motd.HSplitTop(Cut: 2.0f * MotdFontSize, pTop: &MotdHeader, pBottom: &Motd);
820 Motd.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Motd);
821 Ui()->DoLabel(pRect: &MotdHeader, pText: Localize(pStr: "MOTD"), Size: 2.0f * MotdFontSize, Align: TEXTALIGN_ML);
822
823 if(!GameClient()->m_Motd.ServerMotd()[0])
824 return;
825
826 static CScrollRegion s_ScrollRegion;
827 CScrollRegionParams ScrollParams;
828 ScrollParams.m_ScrollUnit = 5 * MotdFontSize;
829 s_ScrollRegion.Begin(pClipRect: &Motd, pParams: &ScrollParams);
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: FontIcon::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: FontIcon::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: FontIcon::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: 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())))
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->GameClient()->Map()->BaseName();
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->GameClient()->Map()->Sha256(), MapCrc: pSelf->GameClient()->Map()->Crc()))
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: ETimeFormat::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: TimestampFormat::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: FontIcon::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