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