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 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 = minimum(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: minimum(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: minimum(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 + 20.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: maximum(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
739 // name
740 {
741 CTextCursor Cursor;
742 Cursor.SetPosition(vec2(NameOffset, Row.y + (Row.h - FontSize) / 2.0f));
743 Cursor.m_FontSize = FontSize;
744 Cursor.m_Flags |= TEXTFLAG_ELLIPSIS_AT_END;
745 Cursor.m_LineWidth = NameLength;
746 if(ClientData.m_AuthLevel)
747 {
748 TextRender()->TextColor(Color: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClAuthedPlayerColor)));
749 }
750 if(g_Config.m_ClShowIds)
751 {
752 char aClientId[16];
753 GameClient()->FormatClientId(ClientId: pInfo->m_ClientId, aClientId, Format: EClientIdFormat::INDENT_AUTO);
754 TextRender()->TextEx(pCursor: &Cursor, pText: aClientId);
755 }
756 TextRender()->TextEx(pCursor: &Cursor, pText: ClientData.m_aName);
757
758 // ready / watching
759 if(Client()->IsSixup() && Client()->m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 & protocol7::PLAYERFLAG_READY)
760 {
761 TextRender()->TextColor(r: 0.1f, g: 1.0f, b: 0.1f, a: TextColor.a);
762 TextRender()->TextEx(pCursor: &Cursor, pText: "✓");
763 }
764 }
765
766 // clan
767 {
768 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)
769 {
770 TextRender()->TextColor(Color: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClSameClanColor)));
771 }
772 else
773 {
774 TextRender()->TextColor(Color: TextColor);
775 }
776 CTextCursor Cursor;
777 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));
778 Cursor.m_FontSize = FontSize;
779 Cursor.m_Flags |= TEXTFLAG_ELLIPSIS_AT_END;
780 Cursor.m_LineWidth = ClanLength;
781 TextRender()->TextEx(pCursor: &Cursor, pText: ClientData.m_aClan);
782 }
783
784 // country flag
785 GameClient()->m_CountryFlags.Render(CountryCode: ClientData.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f),
786 x: CountryOffset, y: Row.y + (Spacing + TeeSizeMod * 5.0f) / 2.0f, w: CountryLength, h: Row.h - Spacing - TeeSizeMod * 5.0f);
787
788 // ping
789 if(g_Config.m_ClEnablePingColor)
790 {
791 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)));
792 }
793 else
794 {
795 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
796 }
797 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", std::clamp(val: pInfo->m_Latency, lo: 0, hi: 999));
798 TextRender()->Text(x: PingOffset + PingLength - TextRender()->TextWidth(Size: FontSize, pText: aBuf), y: Row.y + (Row.h - FontSize) / 2.0f, Size: FontSize, pText: aBuf);
799 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
800
801 if(CountRendered == CountEnd)
802 break;
803 }
804 if(CountRendered == CountEnd)
805 break;
806 }
807}
808
809void CScoreboard::RenderRecordingNotification(float x)
810{
811 char aBuf[512] = "";
812
813 const auto &&AppendRecorderInfo = [&](int Recorder, const char *pName) {
814 if(GameClient()->DemoRecorder(Recorder)->IsRecording())
815 {
816 char aTime[32];
817 str_time(centisecs: (int64_t)GameClient()->DemoRecorder(Recorder)->Length() * 100, format: ETimeFormat::HOURS, buffer: aTime, buffer_size: sizeof(aTime));
818 str_append(dst&: aBuf, src: pName);
819 str_append(dst&: aBuf, src: " ");
820 str_append(dst&: aBuf, src: aTime);
821 str_append(dst&: aBuf, src: " ");
822 }
823 };
824
825 AppendRecorderInfo(RECORDER_MANUAL, Localize(pStr: "Manual"));
826 AppendRecorderInfo(RECORDER_RACE, Localize(pStr: "Race"));
827 AppendRecorderInfo(RECORDER_AUTO, Localize(pStr: "Auto"));
828 AppendRecorderInfo(RECORDER_REPLAYS, Localize(pStr: "Replay"));
829
830 if(aBuf[0] == '\0')
831 return;
832
833 const float FontSize = 10.0f;
834
835 CUIRect Rect = {.x: x, .y: 0.0f, .w: TextRender()->TextWidth(Size: FontSize, pText: aBuf) + 30.0f, .h: 25.0f};
836 Rect.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_B, Rounding: 7.5f);
837 Rect.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Rect);
838 Rect.VSplitRight(Cut: 5.0f, pLeft: &Rect, pRight: nullptr);
839
840 CUIRect Circle;
841 Rect.VSplitLeft(Cut: 10.0f, pLeft: &Circle, pRight: &Rect);
842 Circle.HMargin(Cut: (Circle.h - Circle.w) / 2.0f, pOtherRect: &Circle);
843 Circle.Draw(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: Circle.h / 2.0f);
844
845 Rect.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Rect);
846 Ui()->DoLabel(pRect: &Rect, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
847}
848
849void CScoreboard::OnRender()
850{
851 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
852 return;
853
854 if(!IsActive())
855 {
856 // lock mouse if scoreboard was opened by being dead or game pause
857 if(m_MouseUnlocked)
858 {
859 LockMouse();
860 }
861 return;
862 }
863
864 if(!GameClient()->m_Menus.IsActive() && !GameClient()->m_Chat.IsActive())
865 {
866 Ui()->StartCheck();
867 Ui()->Update();
868 }
869
870 // if the score board is active, then we should clear the motd message as well
871 if(GameClient()->m_Motd.IsActive())
872 GameClient()->m_Motd.Clear();
873
874 const CUIRect Screen = *Ui()->Screen();
875 Ui()->MapScreen();
876
877 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
878 const bool Teams = GameClient()->IsTeamPlay();
879 const auto &aTeamSize = GameClient()->m_Snap.m_aTeamSize;
880 const int NumPlayers = Teams ? maximum(a: aTeamSize[TEAM_RED], b: aTeamSize[TEAM_BLUE]) : aTeamSize[TEAM_RED];
881
882 const float ScoreboardSmallWidth = 375.0f + 10.0f;
883 const float ScoreboardWidth = !Teams && NumPlayers <= 16 ? ScoreboardSmallWidth : 750.0f;
884 const float TitleHeight = 30.0f;
885
886 CUIRect Scoreboard = {.x: (Screen.w - ScoreboardWidth) / 2.0f, .y: 75.0f, .w: ScoreboardWidth, .h: 355.0f + TitleHeight};
887 CScoreboardRenderState RenderState{};
888
889 if(Teams)
890 {
891 const char *pRedTeamName = GetTeamName(Team: TEAM_RED);
892 const char *pBlueTeamName = GetTeamName(Team: TEAM_BLUE);
893
894 // Game over title
895 const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj;
896 if((pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) && pGameDataObj)
897 {
898 char aTitle[256];
899 if(pGameDataObj->m_TeamscoreRed > pGameDataObj->m_TeamscoreBlue)
900 {
901 TextRender()->TextColor(Color: ColorRGBA(0.975f, 0.17f, 0.17f, 1.0f));
902 if(pRedTeamName == nullptr)
903 {
904 str_copy(dst&: aTitle, src: Localize(pStr: "Red team wins!"));
905 }
906 else
907 {
908 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!"), pRedTeamName);
909 }
910 }
911 else if(pGameDataObj->m_TeamscoreBlue > pGameDataObj->m_TeamscoreRed)
912 {
913 TextRender()->TextColor(Color: ColorRGBA(0.17f, 0.46f, 0.975f, 1.0f));
914 if(pBlueTeamName == nullptr)
915 {
916 str_copy(dst&: aTitle, src: Localize(pStr: "Blue team wins!"));
917 }
918 else
919 {
920 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!"), pBlueTeamName);
921 }
922 }
923 else
924 {
925 TextRender()->TextColor(Color: ColorRGBA(0.91f, 0.78f, 0.33f, 1.0f));
926 str_copy(dst&: aTitle, src: Localize(pStr: "Draw!"));
927 }
928
929 const float TitleFontSize = 36.0f;
930 CUIRect GameOverTitle = {.x: Scoreboard.x, .y: Scoreboard.y - TitleFontSize - 6.0f, .w: Scoreboard.w, .h: TitleFontSize};
931 Ui()->DoLabel(pRect: &GameOverTitle, pText: aTitle, Size: TitleFontSize, Align: TEXTALIGN_MC);
932 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
933 }
934
935 CUIRect RedScoreboard, BlueScoreboard, RedTitle, BlueTitle;
936 Scoreboard.VSplitMid(pLeft: &RedScoreboard, pRight: &BlueScoreboard, Spacing: 7.5f);
937 RedScoreboard.HSplitTop(Cut: TitleHeight, pTop: &RedTitle, pBottom: &RedScoreboard);
938 BlueScoreboard.HSplitTop(Cut: TitleHeight, pTop: &BlueTitle, pBottom: &BlueScoreboard);
939
940 RedTitle.Draw(Color: ColorRGBA(0.975f, 0.17f, 0.17f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 7.5f);
941 BlueTitle.Draw(Color: ColorRGBA(0.17f, 0.46f, 0.975f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 7.5f);
942 RedScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 7.5f);
943 BlueScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 7.5f);
944
945 RenderTitleBar(TitleBar: RedTitle, Team: TEAM_RED, pTitle: pRedTeamName == nullptr ? Localize(pStr: "Red team") : pRedTeamName);
946 RenderTitleBar(TitleBar: BlueTitle, Team: TEAM_BLUE, pTitle: pBlueTeamName == nullptr ? Localize(pStr: "Blue team") : pBlueTeamName);
947 RenderScoreboard(Scoreboard: RedScoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: NumPlayers, State&: RenderState);
948 RenderScoreboard(Scoreboard: BlueScoreboard, Team: TEAM_BLUE, CountStart: 0, CountEnd: NumPlayers, State&: RenderState);
949 }
950 else
951 {
952 Scoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 7.5f);
953
954 const char *pTitle;
955 if(pGameInfoObj && (pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
956 {
957 pTitle = Localize(pStr: "Game over");
958 }
959 else
960 {
961 pTitle = GameClient()->Map()->BaseName();
962 }
963
964 CUIRect Title;
965 Scoreboard.HSplitTop(Cut: TitleHeight, pTop: &Title, pBottom: &Scoreboard);
966 RenderTitleBar(TitleBar: Title, Team: TEAM_GAME, pTitle);
967
968 if(NumPlayers <= 16)
969 {
970 RenderScoreboard(Scoreboard, Team: TEAM_GAME, CountStart: 0, CountEnd: NumPlayers, State&: RenderState);
971 }
972 else if(NumPlayers <= 64)
973 {
974 int PlayersPerSide;
975 if(NumPlayers <= 24)
976 PlayersPerSide = 12;
977 else if(NumPlayers <= 32)
978 PlayersPerSide = 16;
979 else if(NumPlayers <= 48)
980 PlayersPerSide = 24;
981 else
982 PlayersPerSide = 32;
983
984 CUIRect LeftScoreboard, RightScoreboard;
985 Scoreboard.VSplitMid(pLeft: &LeftScoreboard, pRight: &RightScoreboard);
986 RenderScoreboard(Scoreboard: LeftScoreboard, Team: TEAM_GAME, CountStart: 0, CountEnd: PlayersPerSide, State&: RenderState);
987 RenderScoreboard(Scoreboard: RightScoreboard, Team: TEAM_GAME, CountStart: PlayersPerSide, CountEnd: 2 * PlayersPerSide, State&: RenderState);
988 }
989 else
990 {
991 const int NumColumns = 3;
992 const int PlayersPerColumn = std::ceil(x: 128.0f / NumColumns);
993 CUIRect RemainingScoreboard = Scoreboard;
994 for(int i = 0; i < NumColumns; ++i)
995 {
996 CUIRect Column;
997 RemainingScoreboard.VSplitLeft(Cut: Scoreboard.w / NumColumns, pLeft: &Column, pRight: &RemainingScoreboard);
998 RenderScoreboard(Scoreboard: Column, Team: TEAM_GAME, CountStart: i * PlayersPerColumn, CountEnd: (i + 1) * PlayersPerColumn, State&: RenderState);
999 }
1000 }
1001 }
1002
1003 CUIRect Spectators = {.x: (Screen.w - ScoreboardSmallWidth) / 2.0f, .y: Scoreboard.y + Scoreboard.h + 5.0f, .w: ScoreboardSmallWidth, .h: 100.0f};
1004 if(pGameInfoObj && (pGameInfoObj->m_ScoreLimit || pGameInfoObj->m_TimeLimit || (pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent)))
1005 {
1006 CUIRect Goals;
1007 Spectators.HSplitTop(Cut: 25.0f, pTop: &Goals, pBottom: &Spectators);
1008 Spectators.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Spectators);
1009 RenderGoals(Goals);
1010 }
1011 RenderSpectators(Spectators);
1012
1013 RenderRecordingNotification(x: (Screen.w / 7) * 4 + 10);
1014
1015 if(!GameClient()->m_Menus.IsActive() && !GameClient()->m_Chat.IsActive())
1016 {
1017 Ui()->RenderPopupMenus();
1018
1019 if(m_MouseUnlocked)
1020 RenderTools()->RenderCursor(Center: Ui()->MousePos(), Size: 24.0f);
1021
1022 Ui()->FinishCheck();
1023 }
1024}
1025
1026bool CScoreboard::IsActive() const
1027{
1028 // if statboard is active don't show scoreboard
1029 if(GameClient()->m_Statboard.IsActive())
1030 return false;
1031
1032 if(m_Active)
1033 return true;
1034
1035 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
1036 if(GameClient()->m_Snap.m_pLocalInfo && !GameClient()->m_Snap.m_SpecInfo.m_Active)
1037 {
1038 // we are not a spectator, check if we are dead and the game isn't paused
1039 if(!GameClient()->m_Snap.m_pLocalCharacter && g_Config.m_ClScoreboardOnDeath &&
1040 !(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED))
1041 return true;
1042 }
1043
1044 // if the game is over
1045 if(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)
1046 return true;
1047
1048 return false;
1049}
1050
1051const char *CScoreboard::GetTeamName(int Team) const
1052{
1053 dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid");
1054
1055 int ClanPlayers = 0;
1056 const char *pClanName = nullptr;
1057 for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByScore)
1058 {
1059 if(!pInfo || pInfo->m_Team != Team)
1060 continue;
1061
1062 if(!pClanName)
1063 {
1064 pClanName = GameClient()->m_aClients[pInfo->m_ClientId].m_aClan;
1065 ClanPlayers++;
1066 }
1067 else
1068 {
1069 if(str_comp(a: GameClient()->m_aClients[pInfo->m_ClientId].m_aClan, b: pClanName) == 0)
1070 ClanPlayers++;
1071 else
1072 return nullptr;
1073 }
1074 }
1075
1076 if(ClanPlayers > 1 && pClanName[0] != '\0')
1077 return pClanName;
1078 else
1079 return nullptr;
1080}
1081
1082CUi::EPopupMenuFunctionResult CScoreboard::CScoreboardPopupContext::Render(void *pContext, CUIRect View, bool Active)
1083{
1084 CScoreboardPopupContext *pPopupContext = static_cast<CScoreboardPopupContext *>(pContext);
1085 CScoreboard *pScoreboard = pPopupContext->m_pScoreboard;
1086 CUi *pUi = pPopupContext->m_pScoreboard->Ui();
1087
1088 CGameClient::CClientData &Client = pScoreboard->GameClient()->m_aClients[pPopupContext->m_ClientId];
1089
1090 if(!Client.m_Active)
1091 return CUi::POPUP_CLOSE_CURRENT;
1092
1093 const float Margin = 5.0f;
1094 View.Margin(Cut: Margin, pOtherRect: &View);
1095
1096 CUIRect Label, Container, Action;
1097 const float ItemSpacing = 2.0f;
1098 const float FontSize = 12.0f;
1099
1100 View.HSplitTop(Cut: FontSize, pTop: &Label, pBottom: &View);
1101 pUi->DoLabel(pRect: &Label, pText: Client.m_aName, Size: FontSize, Align: TEXTALIGN_ML);
1102
1103 if(!pPopupContext->m_IsLocal)
1104 {
1105 const int ActionsNum = 3;
1106 const float ActionSize = 25.0f;
1107 const float ActionSpacing = (View.w - (ActionsNum * ActionSize)) / 2;
1108 int ActionCorners = IGraphics::CORNER_ALL;
1109
1110 View.HSplitTop(Cut: ItemSpacing * 2, pTop: nullptr, pBottom: &View);
1111 View.HSplitTop(Cut: ActionSize, pTop: &Container, pBottom: &View);
1112
1113 Container.VSplitLeft(Cut: ActionSize, pLeft: &Action, pRight: &Container);
1114
1115 ColorRGBA FriendActionColor = Client.m_Friend ? ColorRGBA(0.95f, 0.3f, 0.3f, 0.85f * pUi->ButtonColorMul(pId: &pPopupContext->m_FriendAction)) :
1116 ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * pUi->ButtonColorMul(pId: &pPopupContext->m_FriendAction));
1117 const char *pFriendActionIcon = pUi->HotItem() == &pPopupContext->m_FriendAction && Client.m_Friend ? FontIcon::HEART_CRACK : FontIcon::HEART;
1118 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))
1119 {
1120 if(Client.m_Friend)
1121 {
1122 pScoreboard->GameClient()->Friends()->RemoveFriend(pName: Client.m_aName, pClan: Client.m_aClan);
1123 }
1124 else
1125 {
1126 pScoreboard->GameClient()->Friends()->AddFriend(pName: Client.m_aName, pClan: Client.m_aClan);
1127 }
1128 }
1129
1130 pScoreboard->GameClient()->m_Tooltips.DoToolTip(pId: &pPopupContext->m_FriendAction, pNearRect: &Action, pText: Client.m_Friend ? Localize(pStr: "Remove friend") : Localize(pStr: "Add friend"));
1131
1132 Container.VSplitLeft(Cut: ActionSpacing, pLeft: nullptr, pRight: &Container);
1133 Container.VSplitLeft(Cut: ActionSize, pLeft: &Action, pRight: &Container);
1134
1135 if(pUi->DoButton_FontIcon(pButtonContainer: &pPopupContext->m_MuteAction, pText: FontIcon::BAN, Checked: Client.m_ChatIgnore, pRect: &Action, Flags: BUTTONFLAG_LEFT, Corners: ActionCorners))
1136 {
1137 Client.m_ChatIgnore ^= 1;
1138 }
1139 pScoreboard->GameClient()->m_Tooltips.DoToolTip(pId: &pPopupContext->m_MuteAction, pNearRect: &Action, pText: Client.m_ChatIgnore ? Localize(pStr: "Unmute") : Localize(pStr: "Mute"));
1140
1141 Container.VSplitLeft(Cut: ActionSpacing, pLeft: nullptr, pRight: &Container);
1142 Container.VSplitLeft(Cut: ActionSize, pLeft: &Action, pRight: &Container);
1143
1144 const char *EmoticonActionIcon = Client.m_EmoticonIgnore ? FontIcon::COMMENT_SLASH : FontIcon::COMMENT;
1145 if(pUi->DoButton_FontIcon(pButtonContainer: &pPopupContext->m_EmoticonAction, pText: EmoticonActionIcon, Checked: Client.m_EmoticonIgnore, pRect: &Action, Flags: BUTTONFLAG_LEFT, Corners: ActionCorners))
1146 {
1147 Client.m_EmoticonIgnore ^= 1;
1148 }
1149 pScoreboard->GameClient()->m_Tooltips.DoToolTip(pId: &pPopupContext->m_EmoticonAction, pNearRect: &Action, pText: Client.m_EmoticonIgnore ? Localize(pStr: "Unmute emoticons") : Localize(pStr: "Mute emoticons"));
1150 }
1151
1152 const float ButtonSize = 17.5f;
1153 View.HSplitTop(Cut: ItemSpacing * 2, pTop: nullptr, pBottom: &View);
1154 View.HSplitTop(Cut: ButtonSize, pTop: &Container, pBottom: &View);
1155
1156 bool IsSpectating = pScoreboard->GameClient()->m_Snap.m_SpecInfo.m_Active && pScoreboard->GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == pPopupContext->m_ClientId;
1157 ColorRGBA SpectateButtonColor = ColorRGBA(1.0f, 1.0f, 1.0f, (IsSpectating ? 0.25f : 0.5f) * pUi->ButtonColorMul(pId: &pPopupContext->m_SpectateButton));
1158 if(!pPopupContext->m_IsSpectating)
1159 {
1160 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))
1161 {
1162 if(IsSpectating)
1163 {
1164 pScoreboard->GameClient()->m_Spectator.Spectate(SpectatorId: SPEC_FREEVIEW);
1165 pScoreboard->Console()->ExecuteLine(pStr: "say /spec", ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
1166 }
1167 else
1168 {
1169 if(pScoreboard->GameClient()->m_Snap.m_SpecInfo.m_Active)
1170 {
1171 pScoreboard->GameClient()->m_Spectator.Spectate(SpectatorId: pPopupContext->m_ClientId);
1172 }
1173 else
1174 {
1175 // escape the name
1176 char aEscapedCommand[2 * MAX_NAME_LENGTH + 32];
1177 str_copy(dst&: aEscapedCommand, src: "say /spec \"");
1178 char *pDst = aEscapedCommand + str_length(str: aEscapedCommand);
1179 str_escape(dst: &pDst, src: Client.m_aName, end: aEscapedCommand + sizeof(aEscapedCommand));
1180 str_append(dst&: aEscapedCommand, src: "\"");
1181
1182 pScoreboard->Console()->ExecuteLine(pStr: aEscapedCommand, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
1183 }
1184 }
1185 }
1186 }
1187
1188 return CUi::POPUP_KEEP_OPEN;
1189}
1190
1191CUi::EPopupMenuFunctionResult CScoreboard::CMapTitlePopupContext::Render(void *pContext, CUIRect View, bool Active)
1192{
1193 CMapTitlePopupContext *pPopupContext = static_cast<CMapTitlePopupContext *>(pContext);
1194 CScoreboard *pScoreboard = pPopupContext->m_pScoreboard;
1195
1196 pScoreboard->TextRender()->Text(x: View.x, y: View.y, Size: pPopupContext->m_FontSize, pText: pScoreboard->GameClient()->m_aMapDescription, LineWidth: View.w);
1197
1198 return CUi::POPUP_KEEP_OPEN;
1199}
1200