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