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