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 "scoreboard.h"
4
5#include <base/dbg.h>
6#include <base/time.h>
7
8#include <engine/console.h>
9#include <engine/demo.h>
10#include <engine/font_icons.h>
11#include <engine/graphics.h>
12#include <engine/shared/config.h>
13#include <engine/textrender.h>
14
15#include <generated/client_data7.h>
16#include <generated/protocol.h>
17
18#include <game/client/animstate.h>
19#include <game/client/components/countryflags.h>
20#include <game/client/components/motd.h>
21#include <game/client/components/statboard.h>
22#include <game/client/gameclient.h>
23#include <game/client/ui.h>
24#include <game/localization.h>
25
26// Horizontal spacing of the scoreboard contents, both to its edges and between columns
27static constexpr float MARGIN = 10.0f;
28
29CScoreboard::CScoreboard()
30{
31 CScoreboard::OnReset();
32}
33
34void CScoreboard::SetUiMousePos(vec2 Pos)
35{
36 const vec2 WindowSize = vec2(Graphics()->WindowWidth(), Graphics()->WindowHeight());
37 const CUIRect *pScreen = Ui()->Screen();
38
39 const vec2 UpdatedMousePos = Ui()->UpdatedMousePos();
40 Pos = Pos / vec2(pScreen->w, pScreen->h) * WindowSize;
41 Ui()->OnCursorMove(X: Pos.x - UpdatedMousePos.x, Y: Pos.y - UpdatedMousePos.y);
42}
43
44void CScoreboard::LockMouse()
45{
46 Ui()->ClosePopupMenus();
47 m_MouseUnlocked = false;
48 SetUiMousePos(m_LastMousePos.value());
49 m_LastMousePos = Ui()->MousePos();
50}
51
52void CScoreboard::ConKeyScoreboard(IConsole::IResult *pResult, void *pUserData)
53{
54 CScoreboard *pSelf = static_cast<CScoreboard *>(pUserData);
55
56 pSelf->GameClient()->m_Spectator.OnRelease();
57 pSelf->GameClient()->m_Emoticon.OnRelease();
58
59 pSelf->m_Active = pResult->GetInteger(Index: 0) != 0;
60
61 if(!pSelf->IsActive() && pSelf->m_MouseUnlocked)
62 {
63 pSelf->LockMouse();
64 }
65}
66
67void CScoreboard::ConToggleScoreboardCursor(IConsole::IResult *pResult, void *pUserData)
68{
69 CScoreboard *pSelf = static_cast<CScoreboard *>(pUserData);
70
71 if(!pSelf->IsActive() ||
72 pSelf->GameClient()->m_Menus.IsActive() ||
73 pSelf->GameClient()->m_Chat.IsActive() ||
74 pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK)
75 {
76 return;
77 }
78
79 pSelf->m_MouseUnlocked = !pSelf->m_MouseUnlocked;
80
81 if(!pSelf->m_MouseUnlocked)
82 {
83 pSelf->Ui()->ClosePopupMenus();
84 }
85
86 vec2 OldMousePos = pSelf->Ui()->MousePos();
87
88 if(pSelf->m_LastMousePos == std::nullopt)
89 {
90 pSelf->SetUiMousePos(pSelf->Ui()->Screen()->Center());
91 }
92 else
93 {
94 pSelf->SetUiMousePos(pSelf->m_LastMousePos.value());
95 }
96
97 // save pos, so moving the mouse in esc menu doesn't change the position
98 pSelf->m_LastMousePos = OldMousePos;
99}
100
101void CScoreboard::OnConsoleInit()
102{
103 Console()->Register(pName: "+scoreboard", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyScoreboard, pUser: this, pHelp: "Show scoreboard");
104 Console()->Register(pName: "toggle_scoreboard_cursor", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConToggleScoreboardCursor, pUser: this, pHelp: "Toggle scoreboard cursor");
105}
106
107void CScoreboard::OnInit()
108{
109 m_DeadTeeTexture = Graphics()->LoadTexture(pFilename: "deadtee.png", StorageType: IStorage::TYPE_ALL);
110}
111
112void CScoreboard::OnReset()
113{
114 m_Active = false;
115 m_MouseUnlocked = false;
116 m_LastMousePos = std::nullopt;
117}
118
119void CScoreboard::ResetTexts()
120{
121 for(CPlayerElement &Player : m_aPlayers)
122 {
123 Player.m_Score.Reset(pTextRender: TextRender());
124 Player.m_ScoreMillis.Reset(pTextRender: TextRender());
125 Player.m_Name.Reset(pTextRender: TextRender());
126 Player.m_ReadyMark.Reset(pTextRender: TextRender());
127 Player.m_Clan.Reset(pTextRender: TextRender());
128 Player.m_Ping.Reset(pTextRender: TextRender());
129 }
130 m_TitleScore.Reset(pTextRender: TextRender());
131 m_TitleScoreMillis.Reset(pTextRender: TextRender());
132 m_HeadlineScore.Reset(pTextRender: TextRender());
133 m_HeadlineName.Reset(pTextRender: TextRender());
134 m_HeadlineClan.Reset(pTextRender: TextRender());
135 m_HeadlinePing.Reset(pTextRender: TextRender());
136}
137
138void CScoreboard::OnShutdown()
139{
140 ResetTexts();
141}
142
143void CScoreboard::OnWindowResize()
144{
145 ResetTexts();
146}
147
148void CScoreboard::OnRelease()
149{
150 m_Active = false;
151
152 if(m_MouseUnlocked)
153 {
154 LockMouse();
155 }
156}
157
158bool CScoreboard::OnCursorMove(float x, float y, IInput::ECursorType CursorType)
159{
160 if(!IsActive() || !m_MouseUnlocked)
161 return false;
162
163 Ui()->ConvertMouseMove(pX: &x, pY: &y, CursorType);
164 Ui()->OnCursorMove(X: x, Y: y);
165
166 return true;
167}
168
169bool CScoreboard::OnInput(const IInput::CEvent &Event)
170{
171 if(m_MouseUnlocked && Event.m_Key == KEY_ESCAPE && (Event.m_Flags & IInput::FLAG_PRESS))
172 {
173 LockMouse();
174 return true;
175 }
176
177 return IsActive() && m_MouseUnlocked;
178}
179
180void CScoreboard::RenderTitle(CUIRect TitleLabel, int Team, const char *pTitle, float TitleFontSize)
181{
182 const bool IsMapTitle = !GameClient()->IsTeamPlay();
183 if(IsMapTitle && m_MouseUnlocked && GameClient()->m_aMapDescription[0] != '\0')
184 {
185 const int ButtonResult = Ui()->DoButtonLogic(pId: &m_MapTitleButtonId, Checked: 0, pRect: &TitleLabel, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
186 if(ButtonResult != 0)
187 {
188 m_MapTitlePopupContext.m_pScoreboard = this;
189
190 m_MapTitlePopupContext.m_FontSize = 12.0f;
191 const float MaxWidth = 300.0f;
192 const float Margin = 5.0f;
193 const char *pDescription = GameClient()->m_aMapDescription;
194 const float TextWidth = std::min(a: std::ceil(x: TextRender()->TextWidth(Size: m_MapTitlePopupContext.m_FontSize, pText: pDescription) + 0.5f), b: MaxWidth);
195 float TextHeight = 0.0f;
196 STextSizeProperties TextSizeProps{};
197 TextSizeProps.m_pHeight = &TextHeight;
198 TextRender()->TextWidth(Size: m_MapTitlePopupContext.m_FontSize, pText: pDescription, StrLength: -1, LineWidth: TextWidth, Flags: 0, TextSizeProps);
199
200 Ui()->DoPopupMenu(pId: &m_MapTitlePopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: TextWidth + Margin * 2, Height: TextHeight + Margin * 2, pContext: &m_MapTitlePopupContext, pfnFunc: CMapTitlePopupContext::Render);
201 }
202 if(Ui()->HotItem() == &m_MapTitleButtonId)
203 {
204 TitleLabel.Draw(Color: ColorRGBA(0.7f, 0.7f, 0.7f, 0.3f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
205 }
206 }
207
208 SLabelProperties Props;
209 Props.m_MaxWidth = TitleLabel.w;
210 Props.m_EllipsisAtEnd = true;
211 Ui()->DoLabel(pRect: &TitleLabel, pText: pTitle, Size: TitleFontSize, Align: Team == TEAM_RED ? TEXTALIGN_ML : TEXTALIGN_MR, LabelProps: Props);
212}
213
214void CScoreboard::RenderTitleScore(CUIRect ScoreLabel, int Team, float TitleFontSize)
215{
216 // map best
217 char aScore[128] = "";
218 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
219 const bool TimeScore = GameClient()->m_GameInfo.m_TimeScore;
220 const bool Race7 = Client()->IsSixup() && pGameInfoObj && pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE;
221 if(GameClient()->m_ReceivedDDNetPlayerFinishTimes || TimeScore || Race7)
222 {
223 if(GameClient()->m_MapBestTimeSeconds != FinishTime::UNSET)
224 {
225 Ui()->RenderTime(TimeRect: ScoreLabel,
226 FontSize: TitleFontSize,
227 Seconds: GameClient()->m_MapBestTimeSeconds,
228 NotFinished: GameClient()->m_MapBestTimeSeconds == FinishTime::NOT_FINISHED_MILLIS,
229 Millis: GameClient()->m_MapBestTimeMillis,
230 TrueMilliseconds: GameClient()->m_ReceivedDDNetPlayerFinishTimesMillis,
231 SecondsText&: m_TitleScore, MillisText&: m_TitleScoreMillis, Color: TextRender()->DefaultTextColor());
232 return;
233 }
234 }
235 else if(GameClient()->IsTeamPlay()) // normal score
236 {
237 const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj;
238 if(pGameDataObj)
239 {
240 str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d", Team == TEAM_RED ? pGameDataObj->m_TeamscoreRed : pGameDataObj->m_TeamscoreBlue);
241 }
242 }
243 else
244 {
245 if(GameClient()->m_Snap.m_SpecInfo.m_Active &&
246 GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW &&
247 GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId])
248 {
249 str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d", GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId]->m_Score);
250 }
251 else if(GameClient()->m_Snap.m_pLocalInfo)
252 {
253 str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d", GameClient()->m_Snap.m_pLocalInfo->m_Score);
254 }
255 }
256
257 const float ScoreTextWidth = aScore[0] != '\0' ? TextRender()->TextWidth(Size: TitleFontSize, pText: aScore, StrLength: -1, LineWidth: -1.0f, Flags: 0) : 0.0f;
258 if(ScoreTextWidth != 0.0f)
259 {
260 Ui()->DoLabel(pRect: &ScoreLabel, pText: aScore, Size: TitleFontSize, Align: Team == TEAM_RED ? TEXTALIGN_MR : TEXTALIGN_ML);
261 }
262}
263
264void CScoreboard::RenderTitleBar(CUIRect TitleBar, int Team, const char *pTitle)
265{
266 dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid");
267
268 const float TitleFontSize = 20.0f;
269 const float ScoreTextWidth = TextRender()->TextWidth(Size: TitleFontSize, pText: "00:00:00");
270 const float TitleTextWidth = TextRender()->TextWidth(Size: TitleFontSize, pText: pTitle);
271
272 TitleBar.VMargin(Cut: MARGIN, pOtherRect: &TitleBar);
273 CUIRect TitleLabel, ScoreLabel;
274 if(Team == TEAM_RED)
275 {
276 TitleBar.VSplitRight(Cut: ScoreTextWidth, pLeft: &TitleLabel, pRight: &ScoreLabel);
277 TitleLabel.VSplitRight(Cut: 5.0f, pLeft: &TitleLabel, pRight: nullptr);
278 TitleLabel.VSplitLeft(Cut: std::min(a: TitleTextWidth + 2.0f, b: TitleLabel.w), pLeft: &TitleLabel, pRight: nullptr);
279 }
280 else
281 {
282 TitleBar.VSplitLeft(Cut: ScoreTextWidth, pLeft: &ScoreLabel, pRight: &TitleLabel);
283 TitleLabel.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TitleLabel);
284 TitleLabel.VSplitRight(Cut: std::min(a: TitleTextWidth + 2.0f, b: TitleLabel.w), pLeft: nullptr, pRight: &TitleLabel);
285 }
286
287 RenderTitle(TitleLabel, Team, pTitle, TitleFontSize);
288 RenderTitleScore(ScoreLabel, Team, TitleFontSize);
289}
290
291void CScoreboard::RenderGoals(CUIRect Goals)
292{
293 Goals.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 7.5f);
294 Goals.VMargin(Cut: 5.0f, pOtherRect: &Goals);
295
296 const float FontSize = 10.0f;
297 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
298 char aBuf[64];
299
300 if(pGameInfoObj->m_ScoreLimit)
301 {
302 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d", Localize(pStr: "Score limit"), pGameInfoObj->m_ScoreLimit);
303 Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
304 }
305
306 if(pGameInfoObj->m_TimeLimit)
307 {
308 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Time limit: %d min"), pGameInfoObj->m_TimeLimit);
309 Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_MC);
310 }
311
312 if(pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent)
313 {
314 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Round %d/%d"), pGameInfoObj->m_RoundCurrent, pGameInfoObj->m_RoundNum);
315 Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_MR);
316 }
317}
318
319void CScoreboard::RenderSpectators(CUIRect Spectators)
320{
321 Spectators.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 7.5f);
322 constexpr float SpectatorCut = 5.0f;
323 Spectators.Margin(Cut: SpectatorCut, pOtherRect: &Spectators);
324
325 CTextCursor Cursor;
326 Cursor.SetPosition(Spectators.TopLeft());
327 Cursor.m_FontSize = 11.0f;
328 Cursor.m_LineWidth = Spectators.w;
329 Cursor.m_MaxLines = round_truncate(f: Spectators.h / Cursor.m_FontSize);
330
331 int RemainingSpectators = 0;
332 for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName)
333 {
334 if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS)
335 continue;
336 ++RemainingSpectators;
337 }
338
339 TextRender()->TextEx(pCursor: &Cursor, pText: Localize(pStr: "Spectators"));
340
341 if(RemainingSpectators > 0)
342 {
343 TextRender()->TextEx(pCursor: &Cursor, pText: ": ");
344 }
345
346 bool CommaNeeded = false;
347 for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName)
348 {
349 if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS)
350 continue;
351
352 if(CommaNeeded)
353 {
354 TextRender()->TextEx(pCursor: &Cursor, pText: ", ");
355 }
356
357 if(Cursor.m_LineCount == Cursor.m_MaxLines && RemainingSpectators >= 2)
358 {
359 // This is less expensive than checking with a separate invisible
360 // text cursor though we waste some space at the end of the line.
361 char aRemaining[64];
362 str_format(buffer: aRemaining, buffer_size: sizeof(aRemaining), format: Localize(pStr: "%d others…", pContext: "Spectators"), RemainingSpectators);
363 TextRender()->TextEx(pCursor: &Cursor, pText: aRemaining);
364 break;
365 }
366
367 CUIRect SpectatorRect, SpectatorRectLineBreak;
368 float Margin = 1.0f;
369 SpectatorRect.x = Cursor.m_X - Margin;
370 SpectatorRect.y = Cursor.m_Y;
371
372 if(g_Config.m_ClShowIds)
373 {
374 char aClientId[16];
375 GameClient()->FormatClientId(ClientId: pInfo->m_ClientId, aClientId, Format: EClientIdFormat::NO_INDENT);
376 TextRender()->TextEx(pCursor: &Cursor, pText: aClientId);
377 }
378
379 const CGameClient::CClientData &ClientData = GameClient()->m_aClients[pInfo->m_ClientId];
380 {
381 const char *pClanName = ClientData.m_aClan;
382 if(pClanName[0] != '\0')
383 {
384 if(GameClient()->m_aLocalIds[g_Config.m_ClDummy] >= 0 && str_comp(a: pClanName, b: GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_aClan) == 0)
385 {
386 TextRender()->TextColor(Color: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClSameClanColor)));
387 }
388 else
389 {
390 TextRender()->TextColor(Color: ColorRGBA(0.7f, 0.7f, 0.7f));
391 }
392
393 TextRender()->TextEx(pCursor: &Cursor, pText: pClanName);
394 TextRender()->TextEx(pCursor: &Cursor, pText: " ");
395
396 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
397 }
398 }
399
400 if(GameClient()->m_aClients[pInfo->m_ClientId].m_AuthLevel)
401 {
402 TextRender()->TextColor(Color: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClAuthedPlayerColor)));
403 }
404
405 TextRender()->TextEx(pCursor: &Cursor, pText: GameClient()->m_aClients[pInfo->m_ClientId].m_aName);
406 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
407
408 CommaNeeded = true;
409 --RemainingSpectators;
410
411 bool LineBreakDetected = false;
412 SpectatorRect.h = Cursor.m_FontSize;
413
414 // detect line breaks
415 if(Cursor.m_Y != SpectatorRect.y)
416 {
417 LineBreakDetected = true;
418 SpectatorRectLineBreak.x = Spectators.x - SpectatorCut;
419 SpectatorRectLineBreak.y = Cursor.m_Y;
420 SpectatorRectLineBreak.h = Cursor.m_FontSize;
421 SpectatorRectLineBreak.w = Cursor.m_X - Spectators.x + SpectatorCut + 2 * Margin;
422
423 SpectatorRect.w = Spectators.x + Spectators.w + SpectatorCut - SpectatorRect.x;
424 }
425 else
426 {
427 SpectatorRect.w = Cursor.m_X - SpectatorRect.x + 2 * Margin;
428 }
429
430 if(m_MouseUnlocked)
431 {
432 int ButtonResult = Ui()->DoButtonLogic(pId: &m_aPlayers[pInfo->m_ClientId].m_PlayerButtonId, Checked: 0, pRect: &SpectatorRect, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
433
434 if(LineBreakDetected && ButtonResult == 0)
435 {
436 ButtonResult = Ui()->DoButtonLogic(pId: &m_aPlayers[pInfo->m_ClientId].m_SpectatorSecondLineButtonId, Checked: 0, pRect: &SpectatorRectLineBreak, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
437 }
438 if(ButtonResult != 0)
439 {
440 m_ScoreboardPopupContext.m_pScoreboard = this;
441 m_ScoreboardPopupContext.m_ClientId = pInfo->m_ClientId;
442 m_ScoreboardPopupContext.m_IsLocal = GameClient()->m_aLocalIds[0] == pInfo->m_ClientId ||
443 (Client()->DummyConnected() && GameClient()->m_aLocalIds[1] == pInfo->m_ClientId);
444 m_ScoreboardPopupContext.m_IsSpectating = true;
445
446 Ui()->DoPopupMenu(pId: &m_ScoreboardPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 110.0f,
447 Height: m_ScoreboardPopupContext.m_IsLocal ? 30.0f : 60.0f, pContext: &m_ScoreboardPopupContext, pfnFunc: CScoreboardPopupContext::Render);
448 }
449
450 if(Ui()->HotItem() == &m_aPlayers[pInfo->m_ClientId].m_PlayerButtonId ||
451 Ui()->HotItem() == &m_aPlayers[pInfo->m_ClientId].m_SpectatorSecondLineButtonId ||
452 (Ui()->IsPopupOpen(pId: &m_ScoreboardPopupContext) && m_ScoreboardPopupContext.m_ClientId == pInfo->m_ClientId))
453 {
454 if(!LineBreakDetected)
455 {
456 SpectatorRect.Draw(Color: TextRender()->DefaultTextSelectionColor(), Corners: IGraphics::CORNER_ALL, Rounding: 2.5f);
457 }
458 else
459 {
460 SpectatorRect.Draw(Color: TextRender()->DefaultTextSelectionColor(), Corners: IGraphics::CORNER_L, Rounding: 2.5f);
461 SpectatorRectLineBreak.Draw(Color: TextRender()->DefaultTextSelectionColor(), Corners: IGraphics::CORNER_R, Rounding: 2.5f);
462 }
463 }
464 }
465 }
466}
467
468void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, int CountEnd, CScoreboardRenderState &State)
469{
470 dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid");
471
472 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
473 const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj;
474 const bool TimeScore = GameClient()->m_GameInfo.m_TimeScore;
475 const bool MillisecondScore = GameClient()->m_ReceivedDDNetPlayerFinishTimes;
476 const bool TrueMilliseconds = GameClient()->m_ReceivedDDNetPlayerFinishTimesMillis;
477 const int NumPlayers = CountEnd - CountStart;
478 const bool LowScoreboardWidth = Scoreboard.w < 350.0f;
479
480 bool Race7 = Client()->IsSixup() && pGameInfoObj && pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE;
481
482 const bool UseTime = Race7 || TimeScore || MillisecondScore;
483
484 // calculate measurements
485 float LineHeight;
486 float TeeSizeMod;
487 float Spacing;
488 float RoundRadius;
489 float FontSize;
490 if(NumPlayers <= 8)
491 {
492 LineHeight = 30.0f;
493 TeeSizeMod = 0.5f;
494 Spacing = 8.0f;
495 RoundRadius = 5.0f;
496 FontSize = 12.0f;
497 }
498 else if(NumPlayers <= 12)
499 {
500 LineHeight = 25.0f;
501 TeeSizeMod = 0.45f;
502 Spacing = 2.5f;
503 RoundRadius = 5.0f;
504 FontSize = 12.0f;
505 }
506 else if(NumPlayers <= 16)
507 {
508 LineHeight = 20.0f;
509 TeeSizeMod = 0.4f;
510 Spacing = 0.0f;
511 RoundRadius = 2.5f;
512 FontSize = 12.0f;
513 }
514 else if(NumPlayers <= 24)
515 {
516 LineHeight = 13.5f;
517 TeeSizeMod = 0.3f;
518 Spacing = 0.0f;
519 RoundRadius = 2.5f;
520 FontSize = 10.0f;
521 }
522 else if(NumPlayers <= 32)
523 {
524 LineHeight = 10.0f;
525 TeeSizeMod = 0.2f;
526 Spacing = 0.0f;
527 RoundRadius = 2.5f;
528 FontSize = 8.0f;
529 }
530 else if(LowScoreboardWidth)
531 {
532 LineHeight = 7.5f;
533 TeeSizeMod = 0.125f;
534 Spacing = 0.0f;
535 RoundRadius = 1.0f;
536 FontSize = 7.0f;
537 }
538 else
539 {
540 LineHeight = 5.0f;
541 TeeSizeMod = 0.1f;
542 Spacing = 0.0f;
543 RoundRadius = 1.0f;
544 FontSize = 5.0f;
545 }
546
547 const float ScoreOffset = Scoreboard.x + MARGIN;
548 const float ScoreLength = TextRender()->TextWidth(Size: FontSize, pText: UseTime ? "00:00:00" : "99999");
549 const float TeeOffset = ScoreOffset + ScoreLength + MARGIN;
550 const float TeeLength = 60.0f * TeeSizeMod;
551 const float NameOffset = TeeOffset + TeeLength;
552 const float NameLength = (LowScoreboardWidth ? 90.0f : 150.0f) - TeeLength;
553 const float CountryLength = (LineHeight - Spacing - TeeSizeMod * 5.0f) * 2.0f;
554 const float PingLength = 27.5f;
555 const float PingOffset = Scoreboard.x + Scoreboard.w - PingLength - MARGIN;
556 const float CountryOffset = PingOffset - CountryLength;
557 const float ClanOffset = NameOffset + NameLength + 2.5f;
558 const float ClanLength = CountryOffset - ClanOffset - 2.5f;
559
560 // render headlines
561 const float HeadlineFontsize = 11.0f;
562 CUIRect Headline;
563 Scoreboard.HSplitTop(Cut: HeadlineFontsize * 2.0f, pTop: &Headline, pBottom: &Scoreboard);
564 const float HeadlineY = Headline.y + Headline.h / 2.0f - HeadlineFontsize / 2.0f;
565 const ColorRGBA HeadlineColor = TextRender()->DefaultTextColor();
566 m_HeadlineScore.Update(pTextRender: TextRender(), pText: UseTime ? Localize(pStr: "Time") : Localize(pStr: "Score"), FontSize: HeadlineFontsize);
567 m_HeadlineName.Update(pTextRender: TextRender(), pText: Localize(pStr: "Name"), FontSize: HeadlineFontsize);
568 m_HeadlineClan.Update(pTextRender: TextRender(), pText: Localize(pStr: "Clan"), FontSize: HeadlineFontsize);
569 m_HeadlinePing.Update(pTextRender: TextRender(), pText: Localize(pStr: "Ping"), FontSize: HeadlineFontsize);
570 m_HeadlineScore.Render(pTextRender: TextRender(), Pos: vec2(ScoreOffset + ScoreLength - m_HeadlineScore.Width(), HeadlineY), Color: HeadlineColor);
571 m_HeadlineName.Render(pTextRender: TextRender(), Pos: vec2(NameOffset, HeadlineY), Color: HeadlineColor);
572 m_HeadlineClan.Render(pTextRender: TextRender(), Pos: vec2(ClanOffset + (ClanLength - m_HeadlineClan.Width()) / 2.0f, HeadlineY), Color: HeadlineColor);
573 m_HeadlinePing.Render(pTextRender: TextRender(), Pos: vec2(PingOffset + PingLength - m_HeadlinePing.Width(), HeadlineY), Color: HeadlineColor);
574
575 // render player entries
576 int CountRendered = 0;
577 int PrevDDTeam = -1;
578 int &CurrentDDTeamSize = State.m_CurrentDDTeamSize;
579
580 char aBuf[64];
581 int MaxTeamSize = Config()->m_SvMaxTeamSize;
582
583 for(int RenderDead = 0; RenderDead < 2; RenderDead++)
584 {
585 for(int i = 0; i < MAX_CLIENTS; i++)
586 {
587 // make sure that we render the correct team
588 const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_apInfoByDDTeamScore[i];
589 if(!pInfo || pInfo->m_Team != Team)
590 continue;
591 bool IsDead = Client()->m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 & protocol7::PLAYERFLAG_DEAD;
592 if(!RenderDead && IsDead)
593 continue;
594 if(RenderDead && !IsDead)
595 continue;
596 if(CountRendered++ < CountStart)
597 continue;
598
599 int DDTeam = GameClient()->m_Teams.Team(ClientId: pInfo->m_ClientId);
600 int NextDDTeam = 0;
601
602 ColorRGBA TextColor = TextRender()->DefaultTextColor();
603 TextColor.a = RenderDead ? 0.5f : 1.0f;
604 TextRender()->TextColor(Color: TextColor);
605
606 for(int j = i + 1; j < MAX_CLIENTS; j++)
607 {
608 const CNetObj_PlayerInfo *pInfoNext = GameClient()->m_Snap.m_apInfoByDDTeamScore[j];
609 if(!pInfoNext || pInfoNext->m_Team != Team)
610 continue;
611
612 NextDDTeam = GameClient()->m_Teams.Team(ClientId: pInfoNext->m_ClientId);
613 break;
614 }
615
616 if(PrevDDTeam == -1)
617 {
618 for(int j = i - 1; j >= 0; j--)
619 {
620 const CNetObj_PlayerInfo *pInfoPrev = GameClient()->m_Snap.m_apInfoByDDTeamScore[j];
621 if(!pInfoPrev || pInfoPrev->m_Team != Team)
622 continue;
623
624 PrevDDTeam = GameClient()->m_Teams.Team(ClientId: pInfoPrev->m_ClientId);
625 break;
626 }
627 }
628
629 CUIRect RowAndSpacing, Row;
630 Scoreboard.HSplitTop(Cut: LineHeight + Spacing, pTop: &RowAndSpacing, pBottom: &Scoreboard);
631 RowAndSpacing.HSplitTop(Cut: LineHeight, pTop: &Row, pBottom: nullptr);
632
633 // team background
634 if(DDTeam != TEAM_FLOCK)
635 {
636 const ColorRGBA Color = GameClient()->GetDDTeamColor(DDTeam).WithAlpha(alpha: 0.5f);
637 int TeamRectCorners = 0;
638 if(PrevDDTeam != DDTeam)
639 {
640 TeamRectCorners |= IGraphics::CORNER_T;
641 State.m_TeamStartX = Row.x;
642 State.m_TeamStartY = Row.y;
643 }
644 if(NextDDTeam != DDTeam)
645 TeamRectCorners |= IGraphics::CORNER_B;
646 RowAndSpacing.Draw(Color, Corners: TeamRectCorners, Rounding: RoundRadius);
647
648 CurrentDDTeamSize++;
649
650 if(NextDDTeam != DDTeam)
651 {
652 const float TeamFontSize = FontSize / 1.5f;
653
654 if(NumPlayers > 8)
655 {
656 if(DDTeam == GameClient()->m_Teams.TeamSuper())
657 str_copy(dst&: aBuf, src: Localize(pStr: "Super"));
658 else if(CurrentDDTeamSize <= 1)
659 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", DDTeam);
660 else
661 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d\n(%d/%d)", pContext: "Team and size"), DDTeam, CurrentDDTeamSize, MaxTeamSize);
662 TextRender()->Text(x: State.m_TeamStartX, y: std::max(a: State.m_TeamStartY + Row.h / 2.0f - TeamFontSize, b: State.m_TeamStartY + 1.5f /* padding top */), Size: TeamFontSize, pText: aBuf);
663 }
664 else
665 {
666 if(DDTeam == GameClient()->m_Teams.TeamSuper())
667 str_copy(dst&: aBuf, src: Localize(pStr: "Super"));
668 else if(CurrentDDTeamSize > 1)
669 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Team %d (%d/%d)"), DDTeam, CurrentDDTeamSize, MaxTeamSize);
670 else
671 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Team %d"), DDTeam);
672 TextRender()->Text(x: Row.x + Row.w / 2.0f - TextRender()->TextWidth(Size: TeamFontSize, pText: aBuf) / 2.0f + 5.0f, y: Row.y + Row.h, Size: TeamFontSize, pText: aBuf);
673 }
674
675 CurrentDDTeamSize = 0;
676 }
677 }
678 PrevDDTeam = DDTeam;
679
680 // background so it's easy to find the local player or the followed one in spectator mode
681 if((!GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_Local) ||
682 (GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW && pInfo->m_Local) ||
683 (GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == GameClient()->m_Snap.m_SpecInfo.m_SpectatorId))
684 {
685 Row.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: RoundRadius);
686 }
687
688 const CGameClient::CClientData &ClientData = GameClient()->m_aClients[pInfo->m_ClientId];
689 CPlayerElement &Player = m_aPlayers[pInfo->m_ClientId];
690
691 if(m_MouseUnlocked)
692 {
693 const int ButtonResult = Ui()->DoButtonLogic(pId: &Player.m_PlayerButtonId, Checked: 0, pRect: &Row, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
694 if(ButtonResult != 0)
695 {
696 m_ScoreboardPopupContext.m_pScoreboard = this;
697 m_ScoreboardPopupContext.m_ClientId = pInfo->m_ClientId;
698 m_ScoreboardPopupContext.m_IsLocal = GameClient()->m_aLocalIds[0] == pInfo->m_ClientId ||
699 (Client()->DummyConnected() && GameClient()->m_aLocalIds[1] == pInfo->m_ClientId);
700 m_ScoreboardPopupContext.m_IsSpectating = false;
701
702 Ui()->DoPopupMenu(pId: &m_ScoreboardPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 110.0f,
703 Height: m_ScoreboardPopupContext.m_IsLocal ? 58.5f : 87.5f, pContext: &m_ScoreboardPopupContext, pfnFunc: CScoreboardPopupContext::Render);
704 }
705
706 if(Ui()->HotItem() == &Player.m_PlayerButtonId ||
707 (Ui()->IsPopupOpen(pId: &m_ScoreboardPopupContext) && m_ScoreboardPopupContext.m_ClientId == pInfo->m_ClientId))
708 {
709 Row.Draw(Color: ColorRGBA(0.7f, 0.7f, 0.7f, 0.7f), Corners: IGraphics::CORNER_ALL, Rounding: RoundRadius);
710 }
711 }
712
713 // score
714 CUIRect ScorePosition;
715 ScorePosition.x = ScoreOffset;
716 ScorePosition.w = ScoreLength;
717 ScorePosition.y = Row.y;
718 ScorePosition.h = Row.h;
719
720 if(Race7)
721 {
722 Ui()->RenderTime(TimeRect: ScorePosition, FontSize, Seconds: pInfo->m_Score / 1000, NotFinished: pInfo->m_Score == protocol7::FinishTime::NOT_FINISHED, Millis: pInfo->m_Score % 1000, TrueMilliseconds: true,
723 SecondsText&: Player.m_Score, MillisText&: Player.m_ScoreMillis, Color: TextColor);
724 }
725 else if(MillisecondScore)
726 {
727 Ui()->RenderTime(TimeRect: ScorePosition, FontSize, Seconds: ClientData.m_FinishTimeSeconds, NotFinished: ClientData.m_FinishTimeSeconds == FinishTime::NOT_FINISHED_MILLIS, Millis: ClientData.m_FinishTimeMillis, TrueMilliseconds,
728 SecondsText&: Player.m_Score, MillisText&: Player.m_ScoreMillis, Color: TextColor);
729 }
730 else if(TimeScore)
731 {
732 Ui()->RenderTime(TimeRect: ScorePosition, FontSize, Seconds: pInfo->m_Score, NotFinished: pInfo->m_Score == FinishTime::NOT_FINISHED_TIMESCORE, Millis: -1, TrueMilliseconds: false,
733 SecondsText&: Player.m_Score, MillisText&: Player.m_ScoreMillis, Color: TextColor);
734 }
735 else
736 {
737 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", std::clamp(val: pInfo->m_Score, lo: -999, hi: 99999));
738 Player.m_Score.Update(pTextRender: TextRender(), pText: aBuf, FontSize);
739 Player.m_Score.Render(pTextRender: TextRender(), Pos: vec2(ScoreOffset + ScoreLength - Player.m_Score.Width(), ScorePosition.y + (Row.h - FontSize) / 2.0f), Color: TextColor);
740 }
741
742 // CTF flag
743 if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) &&
744 pGameDataObj && (pGameDataObj->m_FlagCarrierRed == pInfo->m_ClientId || pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId))
745 {
746 Graphics()->TextureSet(Texture: pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId ? GameClient()->m_GameSkin.m_SpriteFlagBlue : GameClient()->m_GameSkin.m_SpriteFlagRed);
747 Graphics()->QuadsBegin();
748 Graphics()->QuadsSetSubset(TopLeftU: 1.0f, TopLeftV: 0.0f, BottomRightU: 0.0f, BottomRightV: 1.0f);
749 IGraphics::CQuadItem QuadItem(TeeOffset, Row.y - 2.5f - Spacing / 2.0f, Row.h / 2.0f, Row.h);
750 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
751 Graphics()->QuadsEnd();
752 }
753
754 // skin
755 if(RenderDead)
756 {
757 Graphics()->TextureSet(Texture: m_DeadTeeTexture);
758 Graphics()->QuadsBegin();
759 if(GameClient()->IsTeamPlay())
760 {
761 Graphics()->SetColor(GameClient()->m_Skins7.GetTeamColor(UseCustomColors: true, PartColor: 0, Team: GameClient()->m_aClients[pInfo->m_ClientId].m_Team, Part: protocol7::SKINPART_BODY));
762 }
763 CTeeRenderInfo TeeInfo = GameClient()->m_aClients[pInfo->m_ClientId].m_RenderInfo;
764 TeeInfo.m_Size *= TeeSizeMod;
765 IGraphics::CQuadItem QuadItem(TeeOffset, Row.y, TeeInfo.m_Size, TeeInfo.m_Size);
766 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
767 Graphics()->QuadsEnd();
768 }
769 else
770 {
771 CTeeRenderInfo TeeInfo = ClientData.m_RenderInfo;
772 TeeInfo.m_Size *= TeeSizeMod;
773 vec2 OffsetToMid;
774 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
775 const vec2 TeeRenderPos = vec2(TeeOffset + TeeLength / 2, Row.y + Row.h / 2.0f + OffsetToMid.y);
776 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
777
778 if(m_MouseUnlocked)
779 {
780 const CUIRect SkinRect = {.x: TeeOffset, .y: Row.y, .w: TeeLength, .h: Row.h};
781 GameClient()->m_Tooltips.DoToolTip(pId: &m_aPlayers[pInfo->m_ClientId].m_PlayerButtonId, pNearRect: &SkinRect, pText: ClientData.m_aSkinName);
782 }
783 }
784
785 const float TextY = Row.y + (Row.h - FontSize) / 2.0f;
786
787 // name
788 {
789 if(g_Config.m_ClShowIds)
790 {
791 char aClientId[16];
792 GameClient()->FormatClientId(ClientId: pInfo->m_ClientId, aClientId, Format: EClientIdFormat::INDENT_AUTO);
793 str_copy(dst&: aBuf, src: aClientId);
794 str_append(dst&: aBuf, src: ClientData.m_aName);
795 }
796 else
797 {
798 str_copy(dst&: aBuf, src: ClientData.m_aName);
799 }
800 Player.m_Name.Update(pTextRender: TextRender(), pText: aBuf, FontSize, LineWidth: NameLength, CursorFlags: TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END);
801
802 ColorRGBA NameColor = TextColor;
803 if(ClientData.m_AuthLevel)
804 {
805 NameColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClAuthedPlayerColor));
806 }
807 Player.m_Name.Render(pTextRender: TextRender(), Pos: vec2(NameOffset, TextY), Color: NameColor);
808
809 // ready / watching
810 if(Client()->IsSixup() && Client()->m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 & protocol7::PLAYERFLAG_READY)
811 {
812 Player.m_ReadyMark.Update(pTextRender: TextRender(), pText: "✓", FontSize);
813 Player.m_ReadyMark.Render(pTextRender: TextRender(), Pos: vec2(NameOffset + Player.m_Name.Width(), TextY), Color: ColorRGBA(0.1f, 1.0f, 0.1f, TextColor.a));
814 }
815 }
816
817 // clan
818 {
819 ColorRGBA ClanColor = TextColor;
820 if(GameClient()->m_aLocalIds[g_Config.m_ClDummy] >= 0 && str_comp(a: ClientData.m_aClan, b: GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_aClan) == 0)
821 {
822 ClanColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClSameClanColor));
823 }
824 Player.m_Clan.Update(pTextRender: TextRender(), pText: ClientData.m_aClan, FontSize, LineWidth: ClanLength, CursorFlags: TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END);
825 Player.m_Clan.Render(pTextRender: TextRender(), Pos: vec2(ClanOffset + (ClanLength - std::min(a: Player.m_Clan.Width(), b: ClanLength)) / 2.0f, TextY), Color: ClanColor);
826 }
827
828 // country flag
829 GameClient()->m_CountryFlags.Render(CountryCode: ClientData.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f),
830 x: CountryOffset, y: Row.y + (Spacing + TeeSizeMod * 5.0f) / 2.0f, w: CountryLength, h: Row.h - Spacing - TeeSizeMod * 5.0f);
831
832 // ping
833 ColorRGBA PingColor = TextRender()->DefaultTextColor();
834 if(g_Config.m_ClEnablePingColor)
835 {
836 PingColor = color_cast<ColorRGBA>(hsl: ColorHSLA((300.0f - std::clamp(val: pInfo->m_Latency, lo: 0, hi: 300)) / 1000.0f, 1.0f, 0.5f));
837 }
838 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", std::clamp(val: pInfo->m_Latency, lo: 0, hi: 999));
839 Player.m_Ping.Update(pTextRender: TextRender(), pText: aBuf, FontSize);
840 Player.m_Ping.Render(pTextRender: TextRender(), Pos: vec2(PingOffset + PingLength - Player.m_Ping.Width(), TextY), Color: PingColor);
841 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
842
843 if(CountRendered == CountEnd)
844 break;
845 }
846 if(CountRendered == CountEnd)
847 break;
848 }
849}
850
851void CScoreboard::RenderRecordingNotification(float x)
852{
853 char aBuf[512] = "";
854
855 const auto &&AppendRecorderInfo = [&](int Recorder, const char *pName) {
856 if(GameClient()->DemoRecorder(Recorder)->IsRecording())
857 {
858 char aTime[32];
859 str_time(centisecs: (int64_t)GameClient()->DemoRecorder(Recorder)->Length() * 100, format: ETimeFormat::HOURS, buffer: aTime, buffer_size: sizeof(aTime));
860 str_append(dst&: aBuf, src: pName);
861 str_append(dst&: aBuf, src: " ");
862 str_append(dst&: aBuf, src: aTime);
863 str_append(dst&: aBuf, src: " ");
864 }
865 };
866
867 AppendRecorderInfo(RECORDER_MANUAL, Localize(pStr: "Manual"));
868 AppendRecorderInfo(RECORDER_RACE, Localize(pStr: "Race"));
869 AppendRecorderInfo(RECORDER_AUTO, Localize(pStr: "Auto"));
870 AppendRecorderInfo(RECORDER_REPLAYS, Localize(pStr: "Replay"));
871
872 if(aBuf[0] == '\0')
873 return;
874
875 const float FontSize = 10.0f;
876
877 CUIRect Rect = {.x: x, .y: 0.0f, .w: TextRender()->TextWidth(Size: FontSize, pText: aBuf) + 30.0f, .h: 25.0f};
878 Rect.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_B, Rounding: 7.5f);
879 Rect.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Rect);
880 Rect.VSplitRight(Cut: 5.0f, pLeft: &Rect, pRight: nullptr);
881
882 CUIRect Circle;
883 Rect.VSplitLeft(Cut: 10.0f, pLeft: &Circle, pRight: &Rect);
884 Circle.HMargin(Cut: (Circle.h - Circle.w) / 2.0f, pOtherRect: &Circle);
885 Circle.Draw(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: Circle.h / 2.0f);
886
887 Rect.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Rect);
888 Ui()->DoLabel(pRect: &Rect, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
889}
890
891void CScoreboard::OnRender()
892{
893 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
894 return;
895
896 if(!IsActive())
897 {
898 // lock mouse if scoreboard was opened by being dead or game pause
899 if(m_MouseUnlocked)
900 {
901 LockMouse();
902 }
903 return;
904 }
905
906 if(!GameClient()->m_Menus.IsActive() && !GameClient()->m_Chat.IsActive())
907 {
908 Ui()->StartCheck();
909 Ui()->Update();
910 }
911
912 // if the score board is active, then we should clear the motd message as well
913 if(GameClient()->m_Motd.IsActive())
914 GameClient()->m_Motd.Clear();
915
916 const CUIRect Screen = *Ui()->Screen();
917 Ui()->MapScreen();
918
919 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
920 const bool Teams = GameClient()->IsTeamPlay();
921 const auto &aTeamSize = GameClient()->m_Snap.m_aTeamSize;
922 const int NumPlayers = Teams ? std::max(a: aTeamSize[TEAM_RED], b: aTeamSize[TEAM_BLUE]) : aTeamSize[TEAM_RED];
923
924 const float ScoreboardSmallWidth = 375.0f + 10.0f;
925 const float ScoreboardWidth = !Teams && NumPlayers <= 16 ? ScoreboardSmallWidth : 750.0f;
926 const float TitleHeight = 30.0f;
927
928 CUIRect Scoreboard = {.x: (Screen.w - ScoreboardWidth) / 2.0f, .y: 75.0f, .w: ScoreboardWidth, .h: 355.0f + TitleHeight};
929 CScoreboardRenderState RenderState{};
930
931 if(Teams)
932 {
933 const char *pRedTeamName = GetTeamName(Team: TEAM_RED);
934 const char *pBlueTeamName = GetTeamName(Team: TEAM_BLUE);
935
936 // Game over title
937 const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj;
938 if((pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) && pGameDataObj)
939 {
940 char aTitle[256];
941 if(pGameDataObj->m_TeamscoreRed > pGameDataObj->m_TeamscoreBlue)
942 {
943 TextRender()->TextColor(Color: ColorRGBA(0.975f, 0.17f, 0.17f, 1.0f));
944 if(pRedTeamName == nullptr)
945 {
946 str_copy(dst&: aTitle, src: Localize(pStr: "Red team wins!"));
947 }
948 else
949 {
950 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!"), pRedTeamName);
951 }
952 }
953 else if(pGameDataObj->m_TeamscoreBlue > pGameDataObj->m_TeamscoreRed)
954 {
955 TextRender()->TextColor(Color: ColorRGBA(0.17f, 0.46f, 0.975f, 1.0f));
956 if(pBlueTeamName == nullptr)
957 {
958 str_copy(dst&: aTitle, src: Localize(pStr: "Blue team wins!"));
959 }
960 else
961 {
962 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!"), pBlueTeamName);
963 }
964 }
965 else
966 {
967 TextRender()->TextColor(Color: ColorRGBA(0.91f, 0.78f, 0.33f, 1.0f));
968 str_copy(dst&: aTitle, src: Localize(pStr: "Draw!"));
969 }
970
971 const float TitleFontSize = 36.0f;
972 CUIRect GameOverTitle = {.x: Scoreboard.x, .y: Scoreboard.y - TitleFontSize - 6.0f, .w: Scoreboard.w, .h: TitleFontSize};
973 Ui()->DoLabel(pRect: &GameOverTitle, pText: aTitle, Size: TitleFontSize, Align: TEXTALIGN_MC);
974 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
975 }
976
977 CUIRect RedScoreboard, BlueScoreboard, RedTitle, BlueTitle;
978 Scoreboard.VSplitMid(pLeft: &RedScoreboard, pRight: &BlueScoreboard, Spacing: 7.5f);
979 RedScoreboard.HSplitTop(Cut: TitleHeight, pTop: &RedTitle, pBottom: &RedScoreboard);
980 BlueScoreboard.HSplitTop(Cut: TitleHeight, pTop: &BlueTitle, pBottom: &BlueScoreboard);
981
982 RedTitle.Draw(Color: ColorRGBA(0.975f, 0.17f, 0.17f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 7.5f);
983 BlueTitle.Draw(Color: ColorRGBA(0.17f, 0.46f, 0.975f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 7.5f);
984 RedScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 7.5f);
985 BlueScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 7.5f);
986
987 RenderTitleBar(TitleBar: RedTitle, Team: TEAM_RED, pTitle: pRedTeamName == nullptr ? Localize(pStr: "Red team") : pRedTeamName);
988 RenderTitleBar(TitleBar: BlueTitle, Team: TEAM_BLUE, pTitle: pBlueTeamName == nullptr ? Localize(pStr: "Blue team") : pBlueTeamName);
989 RenderScoreboard(Scoreboard: RedScoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: NumPlayers, State&: RenderState);
990 RenderScoreboard(Scoreboard: BlueScoreboard, Team: TEAM_BLUE, CountStart: 0, CountEnd: NumPlayers, State&: RenderState);
991 }
992 else
993 {
994 Scoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 7.5f);
995
996 const char *pTitle;
997 if(pGameInfoObj && (pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
998 {
999 pTitle = Localize(pStr: "Game over");
1000 }
1001 else
1002 {
1003 pTitle = GameClient()->Map()->BaseName();
1004 }
1005
1006 CUIRect Title;
1007 Scoreboard.HSplitTop(Cut: TitleHeight, pTop: &Title, pBottom: &Scoreboard);
1008 RenderTitleBar(TitleBar: Title, Team: TEAM_GAME, pTitle);
1009
1010 if(NumPlayers <= 16)
1011 {
1012 RenderScoreboard(Scoreboard, Team: TEAM_GAME, CountStart: 0, CountEnd: NumPlayers, State&: RenderState);
1013 }
1014 else if(NumPlayers <= 64)
1015 {
1016 int PlayersPerSide;
1017 if(NumPlayers <= 24)
1018 PlayersPerSide = 12;
1019 else if(NumPlayers <= 32)
1020 PlayersPerSide = 16;
1021 else if(NumPlayers <= 48)
1022 PlayersPerSide = 24;
1023 else
1024 PlayersPerSide = 32;
1025
1026 CUIRect LeftScoreboard, RightScoreboard;
1027 Scoreboard.VSplitMid(pLeft: &LeftScoreboard, pRight: &RightScoreboard);
1028 RenderScoreboard(Scoreboard: LeftScoreboard, Team: TEAM_GAME, CountStart: 0, CountEnd: PlayersPerSide, State&: RenderState);
1029 RenderScoreboard(Scoreboard: RightScoreboard, Team: TEAM_GAME, CountStart: PlayersPerSide, CountEnd: 2 * PlayersPerSide, State&: RenderState);
1030 }
1031 else
1032 {
1033 const int NumColumns = 3;
1034 const int PlayersPerColumn = std::ceil(x: 128.0f / NumColumns);
1035 CUIRect RemainingScoreboard = Scoreboard;
1036 for(int i = 0; i < NumColumns; ++i)
1037 {
1038 CUIRect Column;
1039 RemainingScoreboard.VSplitLeft(Cut: Scoreboard.w / NumColumns, pLeft: &Column, pRight: &RemainingScoreboard);
1040 RenderScoreboard(Scoreboard: Column, Team: TEAM_GAME, CountStart: i * PlayersPerColumn, CountEnd: (i + 1) * PlayersPerColumn, State&: RenderState);
1041 }
1042 }
1043 }
1044
1045 CUIRect Spectators = {.x: (Screen.w - ScoreboardSmallWidth) / 2.0f, .y: Scoreboard.y + Scoreboard.h + 5.0f, .w: ScoreboardSmallWidth, .h: 100.0f};
1046 if(pGameInfoObj && (pGameInfoObj->m_ScoreLimit || pGameInfoObj->m_TimeLimit || (pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent)))
1047 {
1048 CUIRect Goals;
1049 Spectators.HSplitTop(Cut: 25.0f, pTop: &Goals, pBottom: &Spectators);
1050 Spectators.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Spectators);
1051 RenderGoals(Goals);
1052 }
1053 RenderSpectators(Spectators);
1054
1055 RenderRecordingNotification(x: (Screen.w / 7) * 4 + 10);
1056
1057 if(!GameClient()->m_Menus.IsActive() && !GameClient()->m_Chat.IsActive())
1058 {
1059 Ui()->RenderPopupMenus();
1060
1061 if(m_MouseUnlocked)
1062 RenderTools()->RenderCursor(Center: Ui()->MousePos(), Size: 24.0f);
1063
1064 Ui()->FinishCheck();
1065 }
1066}
1067
1068bool CScoreboard::IsActive() const
1069{
1070 // if statboard is active don't show scoreboard
1071 if(GameClient()->m_Statboard.IsActive())
1072 return false;
1073
1074 if(m_Active)
1075 return true;
1076
1077 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
1078 if(GameClient()->m_Snap.m_pLocalInfo && !GameClient()->m_Snap.m_SpecInfo.m_Active)
1079 {
1080 // we are not a spectator, check if we are dead and the game isn't paused
1081 if(!GameClient()->m_Snap.m_pLocalCharacter && g_Config.m_ClScoreboardOnDeath &&
1082 !(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED))
1083 return true;
1084 }
1085
1086 // if the game is over
1087 if(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)
1088 return true;
1089
1090 return false;
1091}
1092
1093const char *CScoreboard::GetTeamName(int Team) const
1094{
1095 dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid");
1096
1097 int ClanPlayers = 0;
1098 const char *pClanName = nullptr;
1099 for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByScore)
1100 {
1101 if(!pInfo || pInfo->m_Team != Team)
1102 continue;
1103
1104 if(!pClanName)
1105 {
1106 pClanName = GameClient()->m_aClients[pInfo->m_ClientId].m_aClan;
1107 ClanPlayers++;
1108 }
1109 else
1110 {
1111 if(str_comp(a: GameClient()->m_aClients[pInfo->m_ClientId].m_aClan, b: pClanName) == 0)
1112 ClanPlayers++;
1113 else
1114 return nullptr;
1115 }
1116 }
1117
1118 if(ClanPlayers > 1 && pClanName[0] != '\0')
1119 return pClanName;
1120 else
1121 return nullptr;
1122}
1123
1124CUi::EPopupMenuFunctionResult CScoreboard::CScoreboardPopupContext::Render(void *pContext, CUIRect View, bool Active)
1125{
1126 CScoreboardPopupContext *pPopupContext = static_cast<CScoreboardPopupContext *>(pContext);
1127 CScoreboard *pScoreboard = pPopupContext->m_pScoreboard;
1128 CUi *pUi = pPopupContext->m_pScoreboard->Ui();
1129
1130 CGameClient::CClientData &Client = pScoreboard->GameClient()->m_aClients[pPopupContext->m_ClientId];
1131
1132 if(!Client.m_Active)
1133 return CUi::POPUP_CLOSE_CURRENT;
1134
1135 const float Margin = 5.0f;
1136 View.Margin(Cut: Margin, pOtherRect: &View);
1137
1138 CUIRect Label, Container, Action;
1139 const float ItemSpacing = 2.0f;
1140 const float FontSize = 12.0f;
1141
1142 View.HSplitTop(Cut: FontSize, pTop: &Label, pBottom: &View);
1143 pUi->DoLabel(pRect: &Label, pText: Client.m_aName, Size: FontSize, Align: TEXTALIGN_ML);
1144
1145 if(!pPopupContext->m_IsLocal)
1146 {
1147 const int ActionsNum = 3;
1148 const float ActionSize = 25.0f;
1149 const float ActionSpacing = (View.w - (ActionsNum * ActionSize)) / 2;
1150 int ActionCorners = IGraphics::CORNER_ALL;
1151
1152 View.HSplitTop(Cut: ItemSpacing * 2, pTop: nullptr, pBottom: &View);
1153 View.HSplitTop(Cut: ActionSize, pTop: &Container, pBottom: &View);
1154
1155 Container.VSplitLeft(Cut: ActionSize, pLeft: &Action, pRight: &Container);
1156
1157 ColorRGBA FriendActionColor = Client.m_Friend ? ColorRGBA(0.95f, 0.3f, 0.3f, 0.85f * pUi->ButtonColorMul(pId: &pPopupContext->m_FriendAction)) :
1158 ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * pUi->ButtonColorMul(pId: &pPopupContext->m_FriendAction));
1159 const char *pFriendActionIcon = pUi->HotItem() == &pPopupContext->m_FriendAction && Client.m_Friend ? FontIcon::HEART_CRACK : FontIcon::HEART;
1160 if(pUi->DoButton_FontIcon(pButtonContainer: &pPopupContext->m_FriendAction, pText: pFriendActionIcon, Checked: Client.m_Friend, pRect: &Action, Flags: BUTTONFLAG_LEFT, Corners: ActionCorners, Enabled: true, ButtonColor: FriendActionColor))
1161 {
1162 if(Client.m_Friend)
1163 {
1164 pScoreboard->GameClient()->Friends()->RemoveFriend(pName: Client.m_aName, pClan: Client.m_aClan);
1165 }
1166 else
1167 {
1168 pScoreboard->GameClient()->Friends()->AddFriend(pName: Client.m_aName, pClan: Client.m_aClan);
1169 }
1170 }
1171
1172 pScoreboard->GameClient()->m_Tooltips.DoToolTip(pId: &pPopupContext->m_FriendAction, pNearRect: &Action, pText: Client.m_Friend ? Localize(pStr: "Remove friend") : Localize(pStr: "Add friend"));
1173
1174 Container.VSplitLeft(Cut: ActionSpacing, pLeft: nullptr, pRight: &Container);
1175 Container.VSplitLeft(Cut: ActionSize, pLeft: &Action, pRight: &Container);
1176
1177 if(pUi->DoButton_FontIcon(pButtonContainer: &pPopupContext->m_MuteAction, pText: FontIcon::BAN, Checked: Client.m_ChatIgnore, pRect: &Action, Flags: BUTTONFLAG_LEFT, Corners: ActionCorners))
1178 {
1179 Client.m_ChatIgnore ^= 1;
1180 }
1181 pScoreboard->GameClient()->m_Tooltips.DoToolTip(pId: &pPopupContext->m_MuteAction, pNearRect: &Action, pText: Client.m_ChatIgnore ? Localize(pStr: "Unmute") : Localize(pStr: "Mute"));
1182
1183 Container.VSplitLeft(Cut: ActionSpacing, pLeft: nullptr, pRight: &Container);
1184 Container.VSplitLeft(Cut: ActionSize, pLeft: &Action, pRight: &Container);
1185
1186 const char *EmoticonActionIcon = Client.m_EmoticonIgnore ? FontIcon::COMMENT_SLASH : FontIcon::COMMENT;
1187 if(pUi->DoButton_FontIcon(pButtonContainer: &pPopupContext->m_EmoticonAction, pText: EmoticonActionIcon, Checked: Client.m_EmoticonIgnore, pRect: &Action, Flags: BUTTONFLAG_LEFT, Corners: ActionCorners))
1188 {
1189 Client.m_EmoticonIgnore ^= 1;
1190 }
1191 pScoreboard->GameClient()->m_Tooltips.DoToolTip(pId: &pPopupContext->m_EmoticonAction, pNearRect: &Action, pText: Client.m_EmoticonIgnore ? Localize(pStr: "Unmute emoticons") : Localize(pStr: "Mute emoticons"));
1192 }
1193
1194 const float ButtonSize = 17.5f;
1195 View.HSplitTop(Cut: ItemSpacing * 2, pTop: nullptr, pBottom: &View);
1196 View.HSplitTop(Cut: ButtonSize, pTop: &Container, pBottom: &View);
1197
1198 bool IsSpectating = pScoreboard->GameClient()->m_Snap.m_SpecInfo.m_Active && pScoreboard->GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == pPopupContext->m_ClientId;
1199 ColorRGBA SpectateButtonColor = ColorRGBA(1.0f, 1.0f, 1.0f, (IsSpectating ? 0.25f : 0.5f) * pUi->ButtonColorMul(pId: &pPopupContext->m_SpectateButton));
1200 if(!pPopupContext->m_IsSpectating)
1201 {
1202 if(pUi->DoButton_PopupMenu(pButtonContainer: &pPopupContext->m_SpectateButton, pText: Localize(pStr: "Spectate"), pRect: &Container, Size: FontSize, Align: TEXTALIGN_MC, Padding: 0.0f, TransparentInactive: false, Enabled: true, ButtonColor: SpectateButtonColor))
1203 {
1204 if(IsSpectating)
1205 {
1206 pScoreboard->GameClient()->m_Spectator.Spectate(SpectatorId: SPEC_FREEVIEW);
1207 pScoreboard->Console()->ExecuteLine(pStr: "say /spec", ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
1208 }
1209 else
1210 {
1211 if(pScoreboard->GameClient()->m_Snap.m_SpecInfo.m_Active)
1212 {
1213 pScoreboard->GameClient()->m_Spectator.Spectate(SpectatorId: pPopupContext->m_ClientId);
1214 }
1215 else
1216 {
1217 // escape the name
1218 char aEscapedCommand[2 * MAX_NAME_LENGTH + 32];
1219 str_copy(dst&: aEscapedCommand, src: "say /spec \"");
1220 char *pDst = aEscapedCommand + str_length(str: aEscapedCommand);
1221 str_escape(dst: &pDst, src: Client.m_aName, end: aEscapedCommand + sizeof(aEscapedCommand));
1222 str_append(dst&: aEscapedCommand, src: "\"");
1223
1224 pScoreboard->Console()->ExecuteLine(pStr: aEscapedCommand, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
1225 }
1226 }
1227 }
1228 }
1229
1230 return CUi::POPUP_KEEP_OPEN;
1231}
1232
1233CUi::EPopupMenuFunctionResult CScoreboard::CMapTitlePopupContext::Render(void *pContext, CUIRect View, bool Active)
1234{
1235 CMapTitlePopupContext *pPopupContext = static_cast<CMapTitlePopupContext *>(pContext);
1236 CScoreboard *pScoreboard = pPopupContext->m_pScoreboard;
1237
1238 pScoreboard->TextRender()->Text(x: View.x, y: View.y, Size: pPopupContext->m_FontSize, pText: pScoreboard->GameClient()->m_aMapDescription, LineWidth: View.w);
1239
1240 return CUi::POPUP_KEEP_OPEN;
1241}
1242