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