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