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