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