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 <base/math.h>
4#include <base/system.h>
5
6#include <engine/demo.h>
7#include <engine/favorites.h>
8#include <engine/friends.h>
9#include <engine/ghost.h>
10#include <engine/graphics.h>
11#include <engine/serverbrowser.h>
12#include <engine/shared/config.h>
13#include <engine/shared/localization.h>
14#include <engine/textrender.h>
15
16#include <game/generated/client_data.h>
17#include <game/generated/protocol.h>
18
19#include <game/client/animstate.h>
20#include <game/client/components/countryflags.h>
21#include <game/client/gameclient.h>
22#include <game/client/render.h>
23#include <game/client/ui.h>
24#include <game/client/ui_listbox.h>
25#include <game/client/ui_scrollregion.h>
26#include <game/localization.h>
27
28#include "menus.h"
29#include "motd.h"
30#include "voting.h"
31
32#include "ghost.h"
33#include <engine/keys.h>
34#include <engine/storage.h>
35
36#include <chrono>
37
38using namespace FontIcons;
39using namespace std::chrono_literals;
40
41void CMenus::RenderGame(CUIRect MainView)
42{
43 CUIRect Button, ButtonBar, ButtonBar2;
44 bool ShowDDRaceButtons = MainView.w > 855.0f;
45 MainView.HSplitTop(Cut: 45.0f, pTop: &ButtonBar, pBottom: &MainView);
46 ButtonBar.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
47
48 // button bar
49 ButtonBar.HSplitTop(Cut: 10.0f, pTop: 0, pBottom: &ButtonBar);
50 ButtonBar.HSplitTop(Cut: 25.0f, pTop: &ButtonBar, pBottom: 0);
51 ButtonBar.VMargin(Cut: 10.0f, pOtherRect: &ButtonBar);
52
53 ButtonBar.HSplitTop(Cut: 30.0f, pTop: 0, pBottom: &ButtonBar2);
54 ButtonBar2.HSplitTop(Cut: 25.0f, pTop: &ButtonBar2, pBottom: 0);
55
56 ButtonBar.VSplitRight(Cut: 120.0f, pLeft: &ButtonBar, pRight: &Button);
57
58 static CButtonContainer s_DisconnectButton;
59 if(DoButton_Menu(pButtonContainer: &s_DisconnectButton, pText: Localize(pStr: "Disconnect"), Checked: 0, pRect: &Button))
60 {
61 if(Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
62 {
63 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);
64 }
65 else
66 {
67 Client()->Disconnect();
68 RefreshBrowserTab(Force: true);
69 }
70 }
71
72 ButtonBar.VSplitRight(Cut: 5.0f, pLeft: &ButtonBar, pRight: 0);
73 ButtonBar.VSplitRight(Cut: 170.0f, pLeft: &ButtonBar, pRight: &Button);
74
75 bool DummyConnecting = Client()->DummyConnecting();
76 static CButtonContainer s_DummyButton;
77 if(!Client()->DummyAllowed())
78 {
79 DoButton_Menu(pButtonContainer: &s_DummyButton, pText: Localize(pStr: "Connect Dummy"), Checked: 1, pRect: &Button);
80 }
81 else if(DummyConnecting)
82 {
83 DoButton_Menu(pButtonContainer: &s_DummyButton, pText: Localize(pStr: "Connecting dummy"), Checked: 1, pRect: &Button);
84 }
85 else if(DoButton_Menu(pButtonContainer: &s_DummyButton, pText: Client()->DummyConnected() ? Localize(pStr: "Disconnect Dummy") : Localize(pStr: "Connect Dummy"), Checked: 0, pRect: &Button))
86 {
87 if(!Client()->DummyConnected())
88 {
89 Client()->DummyConnect();
90 }
91 else
92 {
93 if(Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
94 {
95 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);
96 }
97 else
98 {
99 Client()->DummyDisconnect(pReason: 0);
100 SetActive(false);
101 }
102 }
103 }
104
105 ButtonBar.VSplitRight(Cut: 5.0f, pLeft: &ButtonBar, pRight: 0);
106 ButtonBar.VSplitRight(Cut: 140.0f, pLeft: &ButtonBar, pRight: &Button);
107
108 static CButtonContainer s_DemoButton;
109 const bool Recording = DemoRecorder(Recorder: RECORDER_MANUAL)->IsRecording();
110 if(DoButton_Menu(pButtonContainer: &s_DemoButton, pText: Recording ? Localize(pStr: "Stop record") : Localize(pStr: "Record demo"), Checked: 0, pRect: &Button))
111 {
112 if(!Recording)
113 Client()->DemoRecorder_Start(pFilename: Client()->GetCurrentMap(), WithTimestamp: true, Recorder: RECORDER_MANUAL);
114 else
115 Client()->DemoRecorder(Recorder: RECORDER_MANUAL)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
116 }
117
118 bool Paused = false;
119 bool Spec = false;
120 if(m_pClient->m_Snap.m_LocalClientId >= 0)
121 {
122 Paused = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_Paused;
123 Spec = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_Spec;
124 }
125
126 if(m_pClient->m_Snap.m_pLocalInfo && m_pClient->m_Snap.m_pGameInfoObj && !Paused && !Spec)
127 {
128 static CButtonContainer s_SpectateButton;
129
130 if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS)
131 {
132 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &ButtonBar);
133 ButtonBar.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &ButtonBar);
134 if(!DummyConnecting && DoButton_Menu(pButtonContainer: &s_SpectateButton, pText: Localize(pStr: "Spectate"), Checked: 0, pRect: &Button))
135 {
136 if(g_Config.m_ClDummy == 0 || Client()->DummyConnected())
137 {
138 m_pClient->SendSwitchTeam(Team: TEAM_SPECTATORS);
139 SetActive(false);
140 }
141 }
142 }
143
144 if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)
145 {
146 if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_RED)
147 {
148 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &ButtonBar);
149 ButtonBar.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &ButtonBar);
150 static CButtonContainer s_JoinRedButton;
151 if(!DummyConnecting && DoButton_Menu(pButtonContainer: &s_JoinRedButton, pText: Localize(pStr: "Join red"), Checked: 0, pRect: &Button))
152 {
153 m_pClient->SendSwitchTeam(Team: TEAM_RED);
154 SetActive(false);
155 }
156 }
157
158 if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_BLUE)
159 {
160 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &ButtonBar);
161 ButtonBar.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &ButtonBar);
162 static CButtonContainer s_JoinBlueButton;
163 if(!DummyConnecting && DoButton_Menu(pButtonContainer: &s_JoinBlueButton, pText: Localize(pStr: "Join blue"), Checked: 0, pRect: &Button))
164 {
165 m_pClient->SendSwitchTeam(Team: TEAM_BLUE);
166 SetActive(false);
167 }
168 }
169 }
170 else
171 {
172 if(m_pClient->m_Snap.m_pLocalInfo->m_Team != 0)
173 {
174 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &ButtonBar);
175 ButtonBar.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &ButtonBar);
176 if(!DummyConnecting && DoButton_Menu(pButtonContainer: &s_SpectateButton, pText: Localize(pStr: "Join game"), Checked: 0, pRect: &Button))
177 {
178 m_pClient->SendSwitchTeam(Team: 0);
179 SetActive(false);
180 }
181 }
182 }
183
184 if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS && ShowDDRaceButtons)
185 {
186 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &ButtonBar);
187 ButtonBar.VSplitLeft(Cut: 65.0f, pLeft: &Button, pRight: &ButtonBar);
188
189 static CButtonContainer s_KillButton;
190 if(DoButton_Menu(pButtonContainer: &s_KillButton, pText: Localize(pStr: "Kill"), Checked: 0, pRect: &Button))
191 {
192 m_pClient->SendKill(ClientId: -1);
193 SetActive(false);
194 }
195 }
196 }
197
198 if(m_pClient->m_ReceivedDDNetPlayer && m_pClient->m_Snap.m_pLocalInfo && m_pClient->m_Snap.m_pGameInfoObj && ShowDDRaceButtons)
199 {
200 if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS || Paused || Spec)
201 {
202 ButtonBar.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &ButtonBar);
203 ButtonBar.VSplitLeft(Cut: (!Paused && !Spec) ? 65.0f : 120.0f, pLeft: &Button, pRight: &ButtonBar);
204
205 static CButtonContainer s_PauseButton;
206 if(DoButton_Menu(pButtonContainer: &s_PauseButton, pText: (!Paused && !Spec) ? Localize(pStr: "Pause") : Localize(pStr: "Join game"), Checked: 0, pRect: &Button))
207 {
208 m_pClient->Console()->ExecuteLine(pStr: "say /pause");
209 SetActive(false);
210 }
211 }
212 }
213}
214
215void CMenus::PopupConfirmDisconnect()
216{
217 Client()->Disconnect();
218}
219
220void CMenus::PopupConfirmDisconnectDummy()
221{
222 Client()->DummyDisconnect(pReason: 0);
223 SetActive(false);
224}
225
226void CMenus::RenderPlayers(CUIRect MainView)
227{
228 CUIRect Button, Button2, ButtonBar, Options, Player;
229 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
230
231 // player options
232 MainView.Margin(Cut: 10.0f, pOtherRect: &Options);
233 Options.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
234 Options.Margin(Cut: 10.0f, pOtherRect: &Options);
235 Options.HSplitTop(Cut: 50.0f, pTop: &Button, pBottom: &Options);
236 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Player options"), Size: 34.0f, Align: TEXTALIGN_ML);
237
238 // headline
239 Options.HSplitTop(Cut: 34.0f, pTop: &ButtonBar, pBottom: &Options);
240 ButtonBar.VSplitRight(Cut: 231.0f, pLeft: &Player, pRight: &ButtonBar);
241 Ui()->DoLabel(pRect: &Player, pText: Localize(pStr: "Player"), Size: 24.0f, Align: TEXTALIGN_ML);
242
243 ButtonBar.HMargin(Cut: 1.0f, pOtherRect: &ButtonBar);
244 float Width = ButtonBar.h * 2.0f;
245 ButtonBar.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &ButtonBar);
246 RenderTools()->RenderIcon(ImageId: IMAGE_GUIICONS, SpriteId: SPRITE_GUIICON_MUTE, pRect: &Button);
247
248 ButtonBar.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &ButtonBar);
249 ButtonBar.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &ButtonBar);
250 RenderTools()->RenderIcon(ImageId: IMAGE_GUIICONS, SpriteId: SPRITE_GUIICON_EMOTICON_MUTE, pRect: &Button);
251
252 ButtonBar.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &ButtonBar);
253 ButtonBar.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &ButtonBar);
254 RenderTools()->RenderIcon(ImageId: IMAGE_GUIICONS, SpriteId: SPRITE_GUIICON_FRIEND, pRect: &Button);
255
256 int TotalPlayers = 0;
257 for(const auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName)
258 {
259 if(!pInfoByName)
260 continue;
261
262 int Index = pInfoByName->m_ClientId;
263
264 if(Index == m_pClient->m_Snap.m_LocalClientId)
265 continue;
266
267 TotalPlayers++;
268 }
269
270 static CListBox s_ListBox;
271 s_ListBox.DoStart(RowHeight: 24.0f, NumItems: TotalPlayers, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &Options);
272
273 // options
274 static char s_aPlayerIds[MAX_CLIENTS][3] = {{0}};
275
276 for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i)
277 {
278 if(!m_pClient->m_Snap.m_apInfoByName[i])
279 continue;
280
281 int Index = m_pClient->m_Snap.m_apInfoByName[i]->m_ClientId;
282 if(Index == m_pClient->m_Snap.m_LocalClientId)
283 continue;
284
285 CGameClient::CClientData &CurrentClient = m_pClient->m_aClients[Index];
286 const CListboxItem Item = s_ListBox.DoNextItem(pId: &CurrentClient);
287
288 Count++;
289
290 if(!Item.m_Visible)
291 continue;
292
293 CUIRect Row = Item.m_Rect;
294 if(Count % 2 == 1)
295 Row.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
296 Row.VSplitRight(Cut: s_ListBox.ScrollbarWidthMax() - s_ListBox.ScrollbarWidth(), pLeft: &Row, pRight: nullptr);
297 Row.VSplitRight(Cut: 300.0f, pLeft: &Player, pRight: &Row);
298
299 // player info
300 Player.VSplitLeft(Cut: 28.0f, pLeft: &Button, pRight: &Player);
301
302 CTeeRenderInfo TeeInfo = CurrentClient.m_RenderInfo;
303 TeeInfo.m_Size = Button.h;
304
305 const CAnimState *pIdleState = CAnimState::GetIdle();
306 vec2 OffsetToMid;
307 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
308 vec2 TeeRenderPos(Button.x + Button.h / 2, Button.y + Button.h / 2 + OffsetToMid.y);
309
310 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
311
312 Player.HSplitTop(Cut: 1.5f, pTop: nullptr, pBottom: &Player);
313 Player.VSplitMid(pLeft: &Player, pRight: &Button);
314 Row.VSplitRight(Cut: 210.0f, pLeft: &Button2, pRight: &Row);
315
316 Ui()->DoLabel(pRect: &Player, pText: CurrentClient.m_aName, Size: 14.0f, Align: TEXTALIGN_ML);
317 Ui()->DoLabel(pRect: &Button, pText: CurrentClient.m_aClan, Size: 14.0f, Align: TEXTALIGN_ML);
318
319 m_pClient->m_CountryFlags.Render(CountryCode: CurrentClient.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f),
320 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);
321
322 // ignore chat button
323 Row.HMargin(Cut: 2.0f, pOtherRect: &Row);
324 Row.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &Row);
325 Button.VSplitLeft(Cut: (Width - Button.h) / 4.0f, pLeft: nullptr, pRight: &Button);
326 Button.VSplitLeft(Cut: Button.h, pLeft: &Button, pRight: nullptr);
327 if(g_Config.m_ClShowChatFriends && !CurrentClient.m_Friend)
328 DoButton_Toggle(pId: &s_aPlayerIds[Index][0], Checked: 1, pRect: &Button, Active: false);
329 else if(DoButton_Toggle(pId: &s_aPlayerIds[Index][0], Checked: CurrentClient.m_ChatIgnore, pRect: &Button, Active: true))
330 CurrentClient.m_ChatIgnore ^= 1;
331
332 // ignore emoticon button
333 Row.VSplitLeft(Cut: 30.0f, pLeft: nullptr, pRight: &Row);
334 Row.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &Row);
335 Button.VSplitLeft(Cut: (Width - Button.h) / 4.0f, pLeft: nullptr, pRight: &Button);
336 Button.VSplitLeft(Cut: Button.h, pLeft: &Button, pRight: nullptr);
337 if(g_Config.m_ClShowChatFriends && !CurrentClient.m_Friend)
338 DoButton_Toggle(pId: &s_aPlayerIds[Index][1], Checked: 1, pRect: &Button, Active: false);
339 else if(DoButton_Toggle(pId: &s_aPlayerIds[Index][1], Checked: CurrentClient.m_EmoticonIgnore, pRect: &Button, Active: true))
340 CurrentClient.m_EmoticonIgnore ^= 1;
341
342 // friend button
343 Row.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Row);
344 Row.VSplitLeft(Cut: Width, pLeft: &Button, pRight: &Row);
345 Button.VSplitLeft(Cut: (Width - Button.h) / 4.0f, pLeft: nullptr, pRight: &Button);
346 Button.VSplitLeft(Cut: Button.h, pLeft: &Button, pRight: nullptr);
347 if(DoButton_Toggle(pId: &s_aPlayerIds[Index][2], Checked: CurrentClient.m_Friend, pRect: &Button, Active: true))
348 {
349 if(CurrentClient.m_Friend)
350 m_pClient->Friends()->RemoveFriend(pName: CurrentClient.m_aName, pClan: CurrentClient.m_aClan);
351 else
352 m_pClient->Friends()->AddFriend(pName: CurrentClient.m_aName, pClan: CurrentClient.m_aClan);
353
354 m_pClient->Client()->ServerBrowserUpdate();
355 }
356 }
357
358 s_ListBox.DoEnd();
359}
360
361void CMenus::RenderServerInfo(CUIRect MainView)
362{
363 if(!m_pClient->m_Snap.m_pLocalInfo)
364 return;
365
366 // fetch server info
367 CServerInfo CurrentServerInfo;
368 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
369
370 // render background
371 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
372
373 CUIRect View, ServerInfo, GameInfo, Motd;
374
375 float x = 0.0f;
376 float y = 0.0f;
377
378 char aBuf[1024];
379
380 // set view to use for all sub-modules
381 MainView.Margin(Cut: 10.0f, pOtherRect: &View);
382
383 // serverinfo
384 View.HSplitTop(Cut: View.h / 2 - 5.0f, pTop: &ServerInfo, pBottom: &Motd);
385 ServerInfo.VSplitLeft(Cut: View.w / 2 - 5.0f, pLeft: &ServerInfo, pRight: &GameInfo);
386 ServerInfo.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
387
388 ServerInfo.Margin(Cut: 5.0f, pOtherRect: &ServerInfo);
389
390 x = 5.0f;
391 y = 0.0f;
392
393 TextRender()->Text(x: ServerInfo.x + x, y: ServerInfo.y + y, Size: 32, pText: Localize(pStr: "Server info"), LineWidth: -1.0f);
394 y += 32.0f + 5.0f;
395
396 mem_zero(block: aBuf, size: sizeof(aBuf));
397 str_format(
398 buffer: aBuf,
399 buffer_size: sizeof(aBuf),
400 format: "%s\n\n"
401 "%s: %s\n"
402 "%s: %d\n"
403 "%s: %s\n"
404 "%s: %s\n",
405 CurrentServerInfo.m_aName,
406 Localize(pStr: "Address"), CurrentServerInfo.m_aAddress,
407 Localize(pStr: "Ping"), m_pClient->m_Snap.m_pLocalInfo->m_Latency,
408 Localize(pStr: "Version"), CurrentServerInfo.m_aVersion,
409 Localize(pStr: "Password"), CurrentServerInfo.m_Flags & 1 ? Localize(pStr: "Yes") : Localize(pStr: "No"));
410
411 TextRender()->Text(x: ServerInfo.x + x, y: ServerInfo.y + y, Size: 20, pText: aBuf, LineWidth: ServerInfo.w - 10.0f);
412
413 // copy info button
414 {
415 CUIRect Button;
416 ServerInfo.HSplitBottom(Cut: 20.0f, pTop: &ServerInfo, pBottom: &Button);
417 Button.VSplitRight(Cut: 200.0f, pLeft: &ServerInfo, pRight: &Button);
418 static CButtonContainer s_CopyButton;
419 if(DoButton_Menu(pButtonContainer: &s_CopyButton, pText: Localize(pStr: "Copy info"), Checked: 0, pRect: &Button))
420 {
421 char aInfo[256];
422 str_format(
423 buffer: aInfo,
424 buffer_size: sizeof(aInfo),
425 format: "%s\n"
426 "Address: ddnet://%s\n"
427 "My IGN: %s\n",
428 CurrentServerInfo.m_aName,
429 CurrentServerInfo.m_aAddress,
430 Client()->PlayerName());
431 Input()->SetClipboardText(aInfo);
432 }
433 }
434
435 // favorite checkbox
436 {
437 CUIRect Button;
438 NETADDR ServerAddr = Client()->ServerAddress();
439 TRISTATE IsFavorite = Favorites()->IsFavorite(pAddrs: &ServerAddr, NumAddrs: 1);
440 ServerInfo.HSplitBottom(Cut: 20.0f, pTop: &ServerInfo, pBottom: &Button);
441 static int s_AddFavButton = 0;
442 if(DoButton_CheckBox(pId: &s_AddFavButton, pText: Localize(pStr: "Favorite"), Checked: IsFavorite != TRISTATE::NONE, pRect: &Button))
443 {
444 if(IsFavorite != TRISTATE::NONE)
445 Favorites()->Remove(pAddrs: &ServerAddr, NumAddrs: 1);
446 else
447 Favorites()->Add(pAddrs: &ServerAddr, NumAddrs: 1);
448 }
449 }
450
451 // gameinfo
452 GameInfo.VSplitLeft(Cut: 10.0f, pLeft: 0x0, pRight: &GameInfo);
453 GameInfo.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
454
455 GameInfo.Margin(Cut: 5.0f, pOtherRect: &GameInfo);
456
457 x = 5.0f;
458 y = 0.0f;
459
460 TextRender()->Text(x: GameInfo.x + x, y: GameInfo.y + y, Size: 32, pText: Localize(pStr: "Game info"), LineWidth: -1.0f);
461 y += 32.0f + 5.0f;
462
463 if(m_pClient->m_Snap.m_pGameInfoObj)
464 {
465 mem_zero(block: aBuf, size: sizeof(aBuf));
466 str_format(
467 buffer: aBuf,
468 buffer_size: sizeof(aBuf),
469 format: "\n\n"
470 "%s: %s\n"
471 "%s: %s\n"
472 "%s: %d\n"
473 "%s: %d\n"
474 "\n"
475 "%s: %d/%d\n",
476 Localize(pStr: "Game type"), CurrentServerInfo.m_aGameType,
477 Localize(pStr: "Map"), CurrentServerInfo.m_aMap,
478 Localize(pStr: "Score limit"), m_pClient->m_Snap.m_pGameInfoObj->m_ScoreLimit,
479 Localize(pStr: "Time limit"), m_pClient->m_Snap.m_pGameInfoObj->m_TimeLimit,
480 Localize(pStr: "Players"), m_pClient->m_Snap.m_NumPlayers, CurrentServerInfo.m_MaxClients);
481 TextRender()->Text(x: GameInfo.x + x, y: GameInfo.y + y, Size: 20, pText: aBuf, LineWidth: GameInfo.w - 10.0f);
482 }
483
484 RenderServerInfoMotd(Motd);
485}
486
487void CMenus::RenderServerInfoMotd(CUIRect Motd)
488{
489 const float MotdFontSize = 16.0f;
490 Motd.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Motd);
491 Motd.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
492 Motd.HMargin(Cut: 5.0f, pOtherRect: &Motd);
493 Motd.VMargin(Cut: 10.0f, pOtherRect: &Motd);
494
495 CUIRect MotdHeader;
496 Motd.HSplitTop(Cut: 2.0f * MotdFontSize, pTop: &MotdHeader, pBottom: &Motd);
497 Motd.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Motd);
498 TextRender()->Text(x: MotdHeader.x, y: MotdHeader.y, Size: 2.0f * MotdFontSize, pText: Localize(pStr: "MOTD"), LineWidth: -1.0f);
499
500 if(!m_pClient->m_Motd.ServerMotd()[0])
501 return;
502
503 static CScrollRegion s_ScrollRegion;
504 vec2 ScrollOffset(0.0f, 0.0f);
505 CScrollRegionParams ScrollParams;
506 ScrollParams.m_ScrollUnit = 5 * MotdFontSize;
507 s_ScrollRegion.Begin(pClipRect: &Motd, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
508 Motd.y += ScrollOffset.y;
509
510 static float s_MotdHeight = 0.0f;
511 static int64_t s_MotdLastUpdateTime = -1;
512 if(!m_MotdTextContainerIndex.Valid() || s_MotdLastUpdateTime == -1 || s_MotdLastUpdateTime != m_pClient->m_Motd.ServerMotdUpdateTime())
513 {
514 CTextCursor Cursor;
515 TextRender()->SetCursor(pCursor: &Cursor, x: 0.0f, y: 0.0f, FontSize: MotdFontSize, Flags: TEXTFLAG_RENDER);
516 Cursor.m_LineWidth = Motd.w;
517 TextRender()->RecreateTextContainer(TextContainerIndex&: m_MotdTextContainerIndex, pCursor: &Cursor, pText: m_pClient->m_Motd.ServerMotd());
518 s_MotdHeight = Cursor.Height();
519 s_MotdLastUpdateTime = m_pClient->m_Motd.ServerMotdUpdateTime();
520 }
521
522 CUIRect MotdTextArea;
523 Motd.HSplitTop(Cut: s_MotdHeight, pTop: &MotdTextArea, pBottom: &Motd);
524 s_ScrollRegion.AddRect(Rect: MotdTextArea);
525
526 if(m_MotdTextContainerIndex.Valid())
527 TextRender()->RenderTextContainer(TextContainerIndex: m_MotdTextContainerIndex, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: TextRender()->DefaultTextOutlineColor(), X: MotdTextArea.x, Y: MotdTextArea.y);
528
529 s_ScrollRegion.End();
530}
531
532bool CMenus::RenderServerControlServer(CUIRect MainView)
533{
534 CUIRect List = MainView;
535 int Total = m_pClient->m_Voting.m_NumVoteOptions;
536 int NumVoteOptions = 0;
537 int aIndices[MAX_VOTE_OPTIONS];
538 static int s_CurVoteOption = 0;
539 int TotalShown = 0;
540
541 for(CVoteOptionClient *pOption = m_pClient->m_Voting.m_pFirst; pOption; pOption = pOption->m_pNext)
542 {
543 if(!m_FilterInput.IsEmpty() && !str_utf8_find_nocase(haystack: pOption->m_aDescription, needle: m_FilterInput.GetString()))
544 continue;
545 TotalShown++;
546 }
547
548 static CListBox s_ListBox;
549 s_ListBox.DoStart(RowHeight: 19.0f, NumItems: TotalShown, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_CurVoteOption, pRect: &List);
550
551 int i = -1;
552 for(CVoteOptionClient *pOption = m_pClient->m_Voting.m_pFirst; pOption; pOption = pOption->m_pNext)
553 {
554 i++;
555 if(!m_FilterInput.IsEmpty() && !str_utf8_find_nocase(haystack: pOption->m_aDescription, needle: m_FilterInput.GetString()))
556 continue;
557
558 if(NumVoteOptions < Total)
559 aIndices[NumVoteOptions] = i;
560 NumVoteOptions++;
561
562 const CListboxItem Item = s_ListBox.DoNextItem(pId: pOption);
563 if(!Item.m_Visible)
564 continue;
565
566 CUIRect Label;
567 Item.m_Rect.VMargin(Cut: 2.0f, pOtherRect: &Label);
568 Ui()->DoLabel(pRect: &Label, pText: pOption->m_aDescription, Size: 13.0f, Align: TEXTALIGN_ML);
569 }
570
571 s_CurVoteOption = s_ListBox.DoEnd();
572 if(s_CurVoteOption < Total)
573 m_CallvoteSelectedOption = aIndices[s_CurVoteOption];
574 return s_ListBox.WasItemActivated();
575}
576
577bool CMenus::RenderServerControlKick(CUIRect MainView, bool FilterSpectators)
578{
579 int NumOptions = 0;
580 int Selected = -1;
581 int aPlayerIds[MAX_CLIENTS];
582 for(const auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName)
583 {
584 if(!pInfoByName)
585 continue;
586
587 int Index = pInfoByName->m_ClientId;
588 if(Index == m_pClient->m_Snap.m_LocalClientId || (FilterSpectators && pInfoByName->m_Team == TEAM_SPECTATORS))
589 continue;
590
591 if(!str_utf8_find_nocase(haystack: m_pClient->m_aClients[Index].m_aName, needle: m_FilterInput.GetString()))
592 continue;
593
594 if(m_CallvoteSelectedPlayer == Index)
595 Selected = NumOptions;
596 aPlayerIds[NumOptions] = Index;
597 NumOptions++;
598 }
599
600 static CListBox s_ListBox;
601 s_ListBox.DoStart(RowHeight: 24.0f, NumItems: NumOptions, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: Selected, pRect: &MainView);
602
603 for(int i = 0; i < NumOptions; i++)
604 {
605 const CListboxItem Item = s_ListBox.DoNextItem(pId: &aPlayerIds[i]);
606 if(!Item.m_Visible)
607 continue;
608
609 CUIRect TeeRect, Label;
610 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h, pLeft: &TeeRect, pRight: &Label);
611
612 CTeeRenderInfo TeeInfo = m_pClient->m_aClients[aPlayerIds[i]].m_RenderInfo;
613 TeeInfo.m_Size = TeeRect.h;
614
615 const CAnimState *pIdleState = CAnimState::GetIdle();
616 vec2 OffsetToMid;
617 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
618 vec2 TeeRenderPos(TeeRect.x + TeeInfo.m_Size / 2, TeeRect.y + TeeInfo.m_Size / 2 + OffsetToMid.y);
619
620 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
621
622 Ui()->DoLabel(pRect: &Label, pText: m_pClient->m_aClients[aPlayerIds[i]].m_aName, Size: 16.0f, Align: TEXTALIGN_ML);
623 }
624
625 Selected = s_ListBox.DoEnd();
626 m_CallvoteSelectedPlayer = Selected != -1 ? aPlayerIds[Selected] : -1;
627 return s_ListBox.WasItemActivated();
628}
629
630void CMenus::RenderServerControl(CUIRect MainView)
631{
632 enum class EServerControlTab
633 {
634 SETTINGS,
635 KICKVOTE,
636 SPECVOTE,
637 };
638 static EServerControlTab s_ControlPage = EServerControlTab::SETTINGS;
639
640 // render background
641 CUIRect Bottom, RconExtension, TabBar, Button;
642 MainView.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &MainView);
643 Bottom.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
644 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
645 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
646 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
647
648 if(Client()->RconAuthed())
649 MainView.HSplitBottom(Cut: 90.0f, pTop: &MainView, pBottom: &RconExtension);
650
651 // tab bar
652 TabBar.VSplitLeft(Cut: TabBar.w / 3, pLeft: &Button, pRight: &TabBar);
653 static CButtonContainer s_Button0;
654 if(DoButton_MenuTab(pButtonContainer: &s_Button0, pText: Localize(pStr: "Change settings"), Checked: s_ControlPage == EServerControlTab::SETTINGS, pRect: &Button, Corners: IGraphics::CORNER_NONE))
655 s_ControlPage = EServerControlTab::SETTINGS;
656
657 TabBar.VSplitMid(pLeft: &Button, pRight: &TabBar);
658 static CButtonContainer s_Button1;
659 if(DoButton_MenuTab(pButtonContainer: &s_Button1, pText: Localize(pStr: "Kick player"), Checked: s_ControlPage == EServerControlTab::KICKVOTE, pRect: &Button, Corners: IGraphics::CORNER_NONE))
660 s_ControlPage = EServerControlTab::KICKVOTE;
661
662 static CButtonContainer s_Button2;
663 if(DoButton_MenuTab(pButtonContainer: &s_Button2, pText: Localize(pStr: "Move player to spectators"), Checked: s_ControlPage == EServerControlTab::SPECVOTE, pRect: &TabBar, Corners: IGraphics::CORNER_NONE))
664 s_ControlPage = EServerControlTab::SPECVOTE;
665
666 // render page
667 MainView.HSplitBottom(Cut: ms_ButtonHeight + 5 * 2, pTop: &MainView, pBottom: &Bottom);
668 Bottom.HMargin(Cut: 5.0f, pOtherRect: &Bottom);
669 Bottom.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Bottom);
670
671 bool Call = false;
672 if(s_ControlPage == EServerControlTab::SETTINGS)
673 Call = RenderServerControlServer(MainView);
674 else if(s_ControlPage == EServerControlTab::KICKVOTE)
675 Call = RenderServerControlKick(MainView, FilterSpectators: false);
676 else if(s_ControlPage == EServerControlTab::SPECVOTE)
677 Call = RenderServerControlKick(MainView, FilterSpectators: true);
678
679 // vote menu
680
681 // render quick search
682 CUIRect QuickSearch;
683 Bottom.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &Bottom);
684 Bottom.VSplitLeft(Cut: 250.0f, pLeft: &QuickSearch, pRight: &Bottom);
685 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
686 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_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
687
688 Ui()->DoLabel(pRect: &QuickSearch, pText: FONT_ICON_MAGNIFYING_GLASS, Size: 14.0f, Align: TEXTALIGN_ML);
689 float SearchWidth = TextRender()->TextWidth(Size: 14.0f, pText: FONT_ICON_MAGNIFYING_GLASS, StrLength: -1, LineWidth: -1.0f);
690 TextRender()->SetRenderFlags(0);
691 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
692 QuickSearch.VSplitLeft(Cut: SearchWidth, pLeft: 0, pRight: &QuickSearch);
693 QuickSearch.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &QuickSearch);
694
695 if(m_ControlPageOpening || (Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed()))
696 {
697 Ui()->SetActiveItem(&m_FilterInput);
698 m_ControlPageOpening = false;
699 m_FilterInput.SelectAll();
700 }
701 m_FilterInput.SetEmptyText(Localize(pStr: "Search"));
702 Ui()->DoClearableEditBox(pLineInput: &m_FilterInput, pRect: &QuickSearch, FontSize: 14.0f);
703
704 // call vote
705 Bottom.VSplitRight(Cut: 10.0f, pLeft: &Bottom, pRight: 0);
706 Bottom.VSplitRight(Cut: 120.0f, pLeft: &Bottom, pRight: &Button);
707
708 static CButtonContainer s_CallVoteButton;
709 if(DoButton_Menu(pButtonContainer: &s_CallVoteButton, pText: Localize(pStr: "Call vote"), Checked: 0, pRect: &Button) || Call)
710 {
711 if(s_ControlPage == EServerControlTab::SETTINGS)
712 {
713 m_pClient->m_Voting.CallvoteOption(OptionId: m_CallvoteSelectedOption, pReason: m_CallvoteReasonInput.GetString());
714 if(g_Config.m_UiCloseWindowAfterChangingSetting)
715 SetActive(false);
716 }
717 else if(s_ControlPage == EServerControlTab::KICKVOTE)
718 {
719 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
720 m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
721 {
722 m_pClient->m_Voting.CallvoteKick(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString());
723 SetActive(false);
724 }
725 }
726 else if(s_ControlPage == EServerControlTab::SPECVOTE)
727 {
728 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
729 m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
730 {
731 m_pClient->m_Voting.CallvoteSpectate(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString());
732 SetActive(false);
733 }
734 }
735 m_CallvoteReasonInput.Clear();
736 }
737
738 // render kick reason
739 CUIRect Reason;
740 Bottom.VSplitRight(Cut: 20.0f, pLeft: &Bottom, pRight: 0);
741 Bottom.VSplitRight(Cut: 200.0f, pLeft: &Bottom, pRight: &Reason);
742 const char *pLabel = Localize(pStr: "Reason:");
743 Ui()->DoLabel(pRect: &Reason, pText: pLabel, Size: 14.0f, Align: TEXTALIGN_ML);
744 float w = TextRender()->TextWidth(Size: 14.0f, pText: pLabel, StrLength: -1, LineWidth: -1.0f);
745 Reason.VSplitLeft(Cut: w + 10.0f, pLeft: 0, pRight: &Reason);
746 if(Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed())
747 {
748 Ui()->SetActiveItem(&m_CallvoteReasonInput);
749 m_CallvoteReasonInput.SelectAll();
750 }
751 Ui()->DoEditBox(pLineInput: &m_CallvoteReasonInput, pRect: &Reason, FontSize: 14.0f);
752
753 // vote option loading indicator
754 if(s_ControlPage == EServerControlTab::SETTINGS && m_pClient->m_Voting.IsReceivingOptions())
755 {
756 CUIRect Spinner, LoadingLabel;
757 Bottom.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &Bottom);
758 Bottom.VSplitLeft(Cut: 16.0f, pLeft: &Spinner, pRight: &Bottom);
759 Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Bottom);
760 Bottom.VSplitRight(Cut: 10.0f, pLeft: &LoadingLabel, pRight: nullptr);
761 Ui()->RenderProgressSpinner(Center: Spinner.Center(), OuterRadius: 8.0f);
762 Ui()->DoLabel(pRect: &LoadingLabel, pText: Localize(pStr: "Loading…"), Size: 14.0f, Align: TEXTALIGN_ML);
763 }
764
765 // extended features (only available when authed in rcon)
766 if(Client()->RconAuthed())
767 {
768 // background
769 RconExtension.HSplitTop(Cut: 10.0f, pTop: 0, pBottom: &RconExtension);
770 RconExtension.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &RconExtension);
771 RconExtension.HSplitTop(Cut: 5.0f, pTop: 0, pBottom: &RconExtension);
772
773 // force vote
774 Bottom.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &Bottom);
775 Bottom.VSplitLeft(Cut: 120.0f, pLeft: &Button, pRight: &Bottom);
776
777 static CButtonContainer s_ForceVoteButton;
778 if(DoButton_Menu(pButtonContainer: &s_ForceVoteButton, pText: Localize(pStr: "Force vote"), Checked: 0, pRect: &Button))
779 {
780 if(s_ControlPage == EServerControlTab::SETTINGS)
781 {
782 m_pClient->m_Voting.CallvoteOption(OptionId: m_CallvoteSelectedOption, pReason: m_CallvoteReasonInput.GetString(), ForceVote: true);
783 }
784 else if(s_ControlPage == EServerControlTab::KICKVOTE)
785 {
786 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
787 m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
788 {
789 m_pClient->m_Voting.CallvoteKick(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString(), ForceVote: true);
790 SetActive(false);
791 }
792 }
793 else if(s_ControlPage == EServerControlTab::SPECVOTE)
794 {
795 if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS &&
796 m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer])
797 {
798 m_pClient->m_Voting.CallvoteSpectate(ClientId: m_CallvoteSelectedPlayer, pReason: m_CallvoteReasonInput.GetString(), ForceVote: true);
799 SetActive(false);
800 }
801 }
802 m_CallvoteReasonInput.Clear();
803 }
804
805 if(s_ControlPage == EServerControlTab::SETTINGS)
806 {
807 // remove vote
808 Bottom.VSplitRight(Cut: 10.0f, pLeft: &Bottom, pRight: 0);
809 Bottom.VSplitRight(Cut: 120.0f, pLeft: 0, pRight: &Button);
810 static CButtonContainer s_RemoveVoteButton;
811 if(DoButton_Menu(pButtonContainer: &s_RemoveVoteButton, pText: Localize(pStr: "Remove"), Checked: 0, pRect: &Button))
812 m_pClient->m_Voting.RemovevoteOption(OptionId: m_CallvoteSelectedOption);
813
814 // add vote
815 RconExtension.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &RconExtension);
816 Bottom.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &Bottom);
817 Bottom.VSplitLeft(Cut: 250.0f, pLeft: &Button, pRight: &Bottom);
818 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Vote description:"), Size: 14.0f, Align: TEXTALIGN_ML);
819
820 Bottom.VSplitLeft(Cut: 20.0f, pLeft: 0, pRight: &Button);
821 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Vote command:"), Size: 14.0f, Align: TEXTALIGN_ML);
822
823 static CLineInputBuffered<VOTE_DESC_LENGTH> s_VoteDescriptionInput;
824 static CLineInputBuffered<VOTE_CMD_LENGTH> s_VoteCommandInput;
825 RconExtension.HSplitTop(Cut: 20.0f, pTop: &Bottom, pBottom: &RconExtension);
826 Bottom.VSplitRight(Cut: 10.0f, pLeft: &Bottom, pRight: 0);
827 Bottom.VSplitRight(Cut: 120.0f, pLeft: &Bottom, pRight: &Button);
828 static CButtonContainer s_AddVoteButton;
829 if(DoButton_Menu(pButtonContainer: &s_AddVoteButton, pText: Localize(pStr: "Add"), Checked: 0, pRect: &Button))
830 if(!s_VoteDescriptionInput.IsEmpty() && !s_VoteCommandInput.IsEmpty())
831 m_pClient->m_Voting.AddvoteOption(pDescription: s_VoteDescriptionInput.GetString(), pCommand: s_VoteCommandInput.GetString());
832
833 Bottom.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &Bottom);
834 Bottom.VSplitLeft(Cut: 250.0f, pLeft: &Button, pRight: &Bottom);
835 Ui()->DoEditBox(pLineInput: &s_VoteDescriptionInput, pRect: &Button, FontSize: 14.0f);
836
837 Bottom.VMargin(Cut: 20.0f, pOtherRect: &Button);
838 Ui()->DoEditBox(pLineInput: &s_VoteCommandInput, pRect: &Button, FontSize: 14.0f);
839 }
840 }
841}
842
843void CMenus::RenderInGameNetwork(CUIRect MainView)
844{
845 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
846
847 CUIRect TabBar, Button;
848 MainView.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &MainView);
849
850 int NewPage = g_Config.m_UiPage;
851
852 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
853 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_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
854
855 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
856 static CButtonContainer s_InternetButton;
857 if(DoButton_MenuTab(pButtonContainer: &s_InternetButton, pText: FONT_ICON_EARTH_AMERICAS, Checked: g_Config.m_UiPage == PAGE_INTERNET, pRect: &Button, Corners: IGraphics::CORNER_NONE))
858 {
859 NewPage = PAGE_INTERNET;
860 }
861 GameClient()->m_Tooltips.DoToolTip(pId: &s_InternetButton, pNearRect: &Button, pText: Localize(pStr: "Internet"));
862
863 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
864 static CButtonContainer s_LanButton;
865 if(DoButton_MenuTab(pButtonContainer: &s_LanButton, pText: FONT_ICON_NETWORK_WIRED, Checked: g_Config.m_UiPage == PAGE_LAN, pRect: &Button, Corners: IGraphics::CORNER_NONE))
866 {
867 NewPage = PAGE_LAN;
868 }
869 GameClient()->m_Tooltips.DoToolTip(pId: &s_LanButton, pNearRect: &Button, pText: Localize(pStr: "LAN"));
870
871 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
872 static CButtonContainer s_FavoritesButton;
873 if(DoButton_MenuTab(pButtonContainer: &s_FavoritesButton, pText: FONT_ICON_STAR, Checked: g_Config.m_UiPage == PAGE_FAVORITES, pRect: &Button, Corners: IGraphics::CORNER_NONE))
874 {
875 NewPage = PAGE_FAVORITES;
876 }
877 GameClient()->m_Tooltips.DoToolTip(pId: &s_FavoritesButton, pNearRect: &Button, pText: Localize(pStr: "Favorites"));
878
879 size_t FavoriteCommunityIndex = 0;
880 static CButtonContainer s_aFavoriteCommunityButtons[5];
881 static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)PAGE_FAVORITE_COMMUNITY_5 - PAGE_FAVORITE_COMMUNITY_1 + 1);
882 for(const CCommunity *pCommunity : ServerBrowser()->FavoriteCommunities())
883 {
884 TabBar.VSplitLeft(Cut: 75.0f, pLeft: &Button, pRight: &TabBar);
885 const int Page = PAGE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex;
886 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: FindCommunityIcon(pCommunityId: pCommunity->Id())))
887 {
888 NewPage = Page;
889 }
890 GameClient()->m_Tooltips.DoToolTip(pId: &s_aFavoriteCommunityButtons[FavoriteCommunityIndex], pNearRect: &Button, pText: pCommunity->Name());
891
892 ++FavoriteCommunityIndex;
893 if(FavoriteCommunityIndex >= std::size(s_aFavoriteCommunityButtons))
894 break;
895 }
896
897 TextRender()->SetRenderFlags(0);
898 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
899
900 if(NewPage != g_Config.m_UiPage)
901 {
902 SetMenuPage(NewPage);
903 }
904
905 RenderServerbrowser(MainView);
906}
907
908// ghost stuff
909int CMenus::GhostlistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
910{
911 CMenus *pSelf = (CMenus *)pUser;
912 const char *pMap = pSelf->Client()->GetCurrentMap();
913 if(IsDir || !str_endswith(str: pInfo->m_pName, suffix: ".gho") || !str_startswith(str: pInfo->m_pName, prefix: pMap))
914 return 0;
915
916 char aFilename[IO_MAX_PATH_LENGTH];
917 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "%s/%s", pSelf->m_pClient->m_Ghost.GetGhostDir(), pInfo->m_pName);
918
919 CGhostInfo Info;
920 if(!pSelf->m_pClient->m_Ghost.GhostLoader()->GetGhostInfo(pFilename: aFilename, pInfo: &Info, pMap, MapSha256: pSelf->Client()->GetCurrentMapSha256(), MapCrc: pSelf->Client()->GetCurrentMapCrc()))
921 return 0;
922
923 CGhostItem Item;
924 str_copy(dst&: Item.m_aFilename, src: aFilename);
925 str_copy(dst&: Item.m_aPlayer, src: Info.m_aOwner);
926 Item.m_Date = pInfo->m_TimeModified;
927 Item.m_Time = Info.m_Time;
928 if(Item.m_Time > 0)
929 pSelf->m_vGhosts.push_back(x: Item);
930
931 if(time_get_nanoseconds() - pSelf->m_GhostPopulateStartTime > 500ms)
932 {
933 pSelf->RenderLoading(pCaption: Localize(pStr: "Loading ghost files"), pContent: "", IncreaseCounter: 0, RenderLoadingBar: false);
934 }
935
936 return 0;
937}
938
939void CMenus::GhostlistPopulate()
940{
941 m_vGhosts.clear();
942 m_GhostPopulateStartTime = time_get_nanoseconds();
943 Storage()->ListDirectoryInfo(Type: IStorage::TYPE_ALL, pPath: m_pClient->m_Ghost.GetGhostDir(), pfnCallback: GhostlistFetchCallback, pUser: this);
944 std::sort(first: m_vGhosts.begin(), last: m_vGhosts.end());
945
946 CGhostItem *pOwnGhost = 0;
947 for(auto &Ghost : m_vGhosts)
948 {
949 Ghost.m_Failed = false;
950 if(str_comp(a: Ghost.m_aPlayer, b: Client()->PlayerName()) == 0 && (!pOwnGhost || Ghost < *pOwnGhost))
951 pOwnGhost = &Ghost;
952 }
953
954 if(pOwnGhost)
955 {
956 pOwnGhost->m_Own = true;
957 pOwnGhost->m_Slot = m_pClient->m_Ghost.Load(pFilename: pOwnGhost->m_aFilename);
958 }
959}
960
961CMenus::CGhostItem *CMenus::GetOwnGhost()
962{
963 for(auto &Ghost : m_vGhosts)
964 if(Ghost.m_Own)
965 return &Ghost;
966 return nullptr;
967}
968
969void CMenus::UpdateOwnGhost(CGhostItem Item)
970{
971 int Own = -1;
972 for(size_t i = 0; i < m_vGhosts.size(); i++)
973 if(m_vGhosts[i].m_Own)
974 Own = i;
975
976 if(Own == -1)
977 {
978 Item.m_Own = true;
979 }
980 else if(g_Config.m_ClRaceGhostSaveBest && (Item.HasFile() || !m_vGhosts[Own].HasFile()))
981 {
982 Item.m_Own = true;
983 DeleteGhostItem(Index: Own);
984 }
985 else if(m_vGhosts[Own].m_Time > Item.m_Time)
986 {
987 Item.m_Own = true;
988 m_vGhosts[Own].m_Own = false;
989 m_vGhosts[Own].m_Slot = -1;
990 }
991 else
992 {
993 Item.m_Own = false;
994 Item.m_Slot = -1;
995 }
996
997 Item.m_Date = std::time(timer: 0);
998 Item.m_Failed = false;
999 m_vGhosts.insert(position: std::lower_bound(first: m_vGhosts.begin(), last: m_vGhosts.end(), val: Item), x: Item);
1000}
1001
1002void CMenus::DeleteGhostItem(int Index)
1003{
1004 if(m_vGhosts[Index].HasFile())
1005 Storage()->RemoveFile(pFilename: m_vGhosts[Index].m_aFilename, Type: IStorage::TYPE_SAVE);
1006 m_vGhosts.erase(position: m_vGhosts.begin() + Index);
1007}
1008
1009void CMenus::RenderGhost(CUIRect MainView)
1010{
1011 // render background
1012 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1013
1014 MainView.HSplitTop(Cut: 10.0f, pTop: 0, pBottom: &MainView);
1015 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: 0);
1016 MainView.VSplitLeft(Cut: 5.0f, pLeft: 0, pRight: &MainView);
1017 MainView.VSplitRight(Cut: 5.0f, pLeft: &MainView, pRight: 0);
1018
1019 CUIRect Headers, Status;
1020 CUIRect View = MainView;
1021
1022 View.HSplitTop(Cut: 17.0f, pTop: &Headers, pBottom: &View);
1023 View.HSplitBottom(Cut: 28.0f, pTop: &View, pBottom: &Status);
1024
1025 // split of the scrollbar
1026 Headers.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1027 Headers.VSplitRight(Cut: 20.0f, pLeft: &Headers, pRight: 0);
1028
1029 struct CColumn
1030 {
1031 const char *m_pCaption;
1032 int m_Id;
1033 float m_Width;
1034 CUIRect m_Rect;
1035 };
1036
1037 enum
1038 {
1039 COL_ACTIVE = 0,
1040 COL_NAME,
1041 COL_TIME,
1042 COL_DATE,
1043 };
1044
1045 static CColumn s_aCols[] = {
1046 {.m_pCaption: "", .m_Id: -1, .m_Width: 2.0f, .m_Rect: {.x: 0}},
1047 {.m_pCaption: "", .m_Id: COL_ACTIVE, .m_Width: 30.0f, .m_Rect: {.x: 0}},
1048 {.m_pCaption: Localizable(pStr: "Name"), .m_Id: COL_NAME, .m_Width: 200.0f, .m_Rect: {.x: 0}},
1049 {.m_pCaption: Localizable(pStr: "Time"), .m_Id: COL_TIME, .m_Width: 90.0f, .m_Rect: {.x: 0}},
1050 {.m_pCaption: Localizable(pStr: "Date"), .m_Id: COL_DATE, .m_Width: 150.0f, .m_Rect: {.x: 0}},
1051 };
1052
1053 int NumCols = std::size(s_aCols);
1054
1055 // do layout
1056 for(int i = 0; i < NumCols; i++)
1057 {
1058 Headers.VSplitLeft(Cut: s_aCols[i].m_Width, pLeft: &s_aCols[i].m_Rect, pRight: &Headers);
1059
1060 if(i + 1 < NumCols)
1061 Headers.VSplitLeft(Cut: 2, pLeft: nullptr, pRight: &Headers);
1062 }
1063
1064 // do headers
1065 for(int i = 0; i < NumCols; i++)
1066 DoButton_GridHeader(pId: &s_aCols[i].m_Id, pText: Localize(pStr: s_aCols[i].m_pCaption), Checked: 0, pRect: &s_aCols[i].m_Rect);
1067
1068 View.Draw(Color: ColorRGBA(0, 0, 0, 0.15f), Corners: 0, Rounding: 0);
1069
1070 const int NumGhosts = m_vGhosts.size();
1071 int NumFailed = 0;
1072 int NumActivated = 0;
1073 static int s_SelectedIndex = 0;
1074 static CListBox s_ListBox;
1075 s_ListBox.DoStart(RowHeight: 17.0f, NumItems: NumGhosts, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_SelectedIndex, pRect: &View, Background: false);
1076
1077 for(int i = 0; i < NumGhosts; i++)
1078 {
1079 const CGhostItem *pGhost = &m_vGhosts[i];
1080 const CListboxItem Item = s_ListBox.DoNextItem(pId: pGhost);
1081
1082 if(pGhost->m_Failed)
1083 NumFailed++;
1084 if(pGhost->Active())
1085 NumActivated++;
1086
1087 if(!Item.m_Visible)
1088 continue;
1089
1090 ColorRGBA rgb = ColorRGBA(1.0f, 1.0f, 1.0f);
1091 if(pGhost->m_Own)
1092 rgb = color_cast<ColorRGBA>(hsl: ColorHSLA(0.33f, 1.0f, 0.75f));
1093
1094 if(pGhost->m_Failed)
1095 rgb = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f);
1096
1097 TextRender()->TextColor(rgb: rgb.WithAlpha(alpha: pGhost->HasFile() ? 1.0f : 0.5f));
1098
1099 for(int c = 0; c < NumCols; c++)
1100 {
1101 CUIRect Button;
1102 Button.x = s_aCols[c].m_Rect.x;
1103 Button.y = Item.m_Rect.y;
1104 Button.h = Item.m_Rect.h;
1105 Button.w = s_aCols[c].m_Rect.w;
1106
1107 int Id = s_aCols[c].m_Id;
1108
1109 if(Id == COL_ACTIVE)
1110 {
1111 if(pGhost->Active())
1112 {
1113 Graphics()->WrapClamp();
1114 Graphics()->TextureSet(Texture: GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[(SPRITE_OOP + 7) - SPRITE_OOP]);
1115 Graphics()->QuadsBegin();
1116 IGraphics::CQuadItem QuadItem(Button.x + Button.w / 2, Button.y + Button.h / 2, 20.0f, 20.0f);
1117 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1118
1119 Graphics()->QuadsEnd();
1120 Graphics()->WrapNormal();
1121 }
1122 }
1123 else if(Id == COL_NAME)
1124 {
1125 Ui()->DoLabel(pRect: &Button, pText: pGhost->m_aPlayer, Size: 12.0f, Align: TEXTALIGN_ML);
1126 }
1127 else if(Id == COL_TIME)
1128 {
1129 char aBuf[64];
1130 str_time(centisecs: pGhost->m_Time / 10, format: TIME_HOURS_CENTISECS, buffer: aBuf, buffer_size: sizeof(aBuf));
1131 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_ML);
1132 }
1133 else if(Id == COL_DATE)
1134 {
1135 char aBuf[64];
1136 str_timestamp_ex(time: pGhost->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), FORMAT_SPACE);
1137 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_ML);
1138 }
1139 }
1140
1141 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1142 }
1143
1144 s_SelectedIndex = s_ListBox.DoEnd();
1145
1146 Status.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1147 Status.Margin(Cut: 5.0f, pOtherRect: &Status);
1148
1149 CUIRect Button;
1150 Status.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &Status);
1151
1152 static CButtonContainer s_ReloadButton;
1153 static CButtonContainer s_DirectoryButton;
1154 static CButtonContainer s_ActivateAll;
1155
1156 if(DoButton_FontIcon(pButtonContainer: &s_ReloadButton, pText: FONT_ICON_ARROW_ROTATE_RIGHT, Checked: 0, pRect: &Button) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
1157 {
1158 m_pClient->m_Ghost.UnloadAll();
1159 GhostlistPopulate();
1160 }
1161
1162 Status.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &Status);
1163 Status.VSplitLeft(Cut: 175.0f, pLeft: &Button, pRight: &Status);
1164 if(DoButton_Menu(pButtonContainer: &s_DirectoryButton, pText: Localize(pStr: "Ghosts directory"), Checked: 0, pRect: &Button))
1165 {
1166 char aBuf[IO_MAX_PATH_LENGTH];
1167 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "ghosts", pBuffer: aBuf, BufferSize: sizeof(aBuf));
1168 Storage()->CreateFolder(pFoldername: "ghosts", Type: IStorage::TYPE_SAVE);
1169 Client()->ViewFile(pFilename: aBuf);
1170 }
1171
1172 Status.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &Status);
1173 if(NumGhosts - NumFailed > 0)
1174 {
1175 Status.VSplitLeft(Cut: 175.0f, pLeft: &Button, pRight: &Status);
1176 bool ActivateAll = ((NumGhosts - NumFailed) != NumActivated) && m_pClient->m_Ghost.FreeSlots();
1177
1178 const char *pActionText = ActivateAll ? Localize(pStr: "Activate all") : Localize(pStr: "Deactivate all");
1179 if(DoButton_Menu(pButtonContainer: &s_ActivateAll, pText: pActionText, Checked: 0, pRect: &Button))
1180 {
1181 for(int i = 0; i < NumGhosts; i++)
1182 {
1183 CGhostItem *pGhost = &m_vGhosts[i];
1184 if(pGhost->m_Failed || (ActivateAll && pGhost->m_Slot != -1))
1185 continue;
1186
1187 if(ActivateAll)
1188 {
1189 if(!m_pClient->m_Ghost.FreeSlots())
1190 break;
1191
1192 pGhost->m_Slot = m_pClient->m_Ghost.Load(pFilename: pGhost->m_aFilename);
1193 if(pGhost->m_Slot == -1)
1194 pGhost->m_Failed = true;
1195 }
1196 else
1197 {
1198 m_pClient->m_Ghost.UnloadAll();
1199 pGhost->m_Slot = -1;
1200 }
1201 }
1202 }
1203 }
1204
1205 if(s_SelectedIndex == -1 || s_SelectedIndex >= (int)m_vGhosts.size())
1206 return;
1207
1208 CGhostItem *pGhost = &m_vGhosts[s_SelectedIndex];
1209
1210 CGhostItem *pOwnGhost = GetOwnGhost();
1211 int ReservedSlots = !pGhost->m_Own && !(pOwnGhost && pOwnGhost->Active());
1212 if(!pGhost->m_Failed && pGhost->HasFile() && (pGhost->Active() || m_pClient->m_Ghost.FreeSlots() > ReservedSlots))
1213 {
1214 Status.VSplitRight(Cut: 120.0f, pLeft: &Status, pRight: &Button);
1215
1216 static CButtonContainer s_GhostButton;
1217 const char *pText = pGhost->Active() ? Localize(pStr: "Deactivate") : Localize(pStr: "Activate");
1218 if(DoButton_Menu(pButtonContainer: &s_GhostButton, pText, Checked: 0, pRect: &Button) || s_ListBox.WasItemActivated())
1219 {
1220 if(pGhost->Active())
1221 {
1222 m_pClient->m_Ghost.Unload(Slot: pGhost->m_Slot);
1223 pGhost->m_Slot = -1;
1224 }
1225 else
1226 {
1227 pGhost->m_Slot = m_pClient->m_Ghost.Load(pFilename: pGhost->m_aFilename);
1228 if(pGhost->m_Slot == -1)
1229 pGhost->m_Failed = true;
1230 }
1231 }
1232 Status.VSplitRight(Cut: 5.0f, pLeft: &Status, pRight: 0);
1233 }
1234
1235 Status.VSplitRight(Cut: 120.0f, pLeft: &Status, pRight: &Button);
1236
1237 static CButtonContainer s_DeleteButton;
1238 if(DoButton_Menu(pButtonContainer: &s_DeleteButton, pText: Localize(pStr: "Delete"), Checked: 0, pRect: &Button))
1239 {
1240 if(pGhost->Active())
1241 m_pClient->m_Ghost.Unload(Slot: pGhost->m_Slot);
1242 DeleteGhostItem(Index: s_SelectedIndex);
1243 }
1244
1245 Status.VSplitRight(Cut: 5.0f, pLeft: &Status, pRight: 0);
1246
1247 bool Recording = m_pClient->m_Ghost.GhostRecorder()->IsRecording();
1248 if(!pGhost->HasFile() && !Recording && pGhost->Active())
1249 {
1250 static CButtonContainer s_SaveButton;
1251 Status.VSplitRight(Cut: 120.0f, pLeft: &Status, pRight: &Button);
1252 if(DoButton_Menu(pButtonContainer: &s_SaveButton, pText: Localize(pStr: "Save"), Checked: 0, pRect: &Button))
1253 m_pClient->m_Ghost.SaveGhost(pItem: pGhost);
1254 }
1255}
1256
1257void CMenus::RenderIngameHint()
1258{
1259 float Width = 300 * Graphics()->ScreenAspect();
1260 Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: Width, BottomRightY: 300);
1261 TextRender()->TextColor(r: 1, g: 1, b: 1, a: 1);
1262 TextRender()->Text(x: 5, y: 280, Size: 5, pText: Localize(pStr: "Menu opened. Press Esc key again to close menu."), LineWidth: -1.0f);
1263 Ui()->MapScreen();
1264}
1265