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/demo.h>
6#include <engine/graphics.h>
7#include <engine/shared/config.h>
8#include <engine/textrender.h>
9
10#include <game/generated/protocol.h>
11
12#include <game/client/animstate.h>
13#include <game/client/components/countryflags.h>
14#include <game/client/components/motd.h>
15#include <game/client/components/statboard.h>
16#include <game/client/gameclient.h>
17#include <game/client/render.h>
18#include <game/client/ui.h>
19#include <game/localization.h>
20
21CScoreboard::CScoreboard()
22{
23 OnReset();
24}
25
26void CScoreboard::ConKeyScoreboard(IConsole::IResult *pResult, void *pUserData)
27{
28 CScoreboard *pSelf = static_cast<CScoreboard *>(pUserData);
29 pSelf->m_Active = pResult->GetInteger(Index: 0) != 0;
30}
31
32void CScoreboard::OnConsoleInit()
33{
34 Console()->Register(pName: "+scoreboard", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyScoreboard, pUser: this, pHelp: "Show scoreboard");
35}
36
37void CScoreboard::OnReset()
38{
39 m_Active = false;
40 m_ServerRecord = -1.0f;
41}
42
43void CScoreboard::OnRelease()
44{
45 m_Active = false;
46}
47
48void CScoreboard::OnMessage(int MsgType, void *pRawMsg)
49{
50 if(MsgType == NETMSGTYPE_SV_RECORD)
51 {
52 CNetMsg_Sv_Record *pMsg = static_cast<CNetMsg_Sv_Record *>(pRawMsg);
53 m_ServerRecord = pMsg->m_ServerTimeBest / 100.0f;
54 }
55 else if(MsgType == NETMSGTYPE_SV_RECORDLEGACY)
56 {
57 CNetMsg_Sv_RecordLegacy *pMsg = static_cast<CNetMsg_Sv_RecordLegacy *>(pRawMsg);
58 m_ServerRecord = pMsg->m_ServerTimeBest / 100.0f;
59 }
60}
61
62void CScoreboard::RenderTitle(CUIRect TitleBar, int Team, const char *pTitle)
63{
64 dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid");
65
66 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
67
68 char aScore[128] = "";
69 if(GameClient()->m_GameInfo.m_TimeScore)
70 {
71 if(m_ServerRecord > 0)
72 {
73 str_time_float(secs: m_ServerRecord, format: TIME_HOURS, buffer: aScore, buffer_size: sizeof(aScore));
74 }
75 }
76 else if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS))
77 {
78 const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj;
79 if(pGameDataObj)
80 {
81 str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d", Team == TEAM_RED ? pGameDataObj->m_TeamscoreRed : pGameDataObj->m_TeamscoreBlue);
82 }
83 }
84 else
85 {
86 if(GameClient()->m_Snap.m_SpecInfo.m_Active &&
87 GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW &&
88 GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId])
89 {
90 str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d", GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId]->m_Score);
91 }
92 else if(GameClient()->m_Snap.m_pLocalInfo)
93 {
94 str_format(buffer: aScore, buffer_size: sizeof(aScore), format: "%d", GameClient()->m_Snap.m_pLocalInfo->m_Score);
95 }
96 }
97
98 const float TitleFontSize = 40.0f;
99 const float ScoreTextWidth = TextRender()->TextWidth(Size: TitleFontSize, pText: aScore);
100
101 TitleBar.VMargin(Cut: 20.0f, pOtherRect: &TitleBar);
102 CUIRect TitleLabel, ScoreLabel;
103 if(Team == TEAM_RED)
104 {
105 TitleBar.VSplitRight(Cut: ScoreTextWidth, pLeft: &TitleLabel, pRight: &ScoreLabel);
106 TitleLabel.VSplitRight(Cut: 10.0f, pLeft: &TitleLabel, pRight: nullptr);
107 }
108 else
109 {
110 TitleBar.VSplitLeft(Cut: ScoreTextWidth, pLeft: &ScoreLabel, pRight: &TitleLabel);
111 TitleLabel.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &TitleLabel);
112 }
113
114 {
115 SLabelProperties Props;
116 Props.m_MaxWidth = TitleLabel.w;
117 Props.m_EllipsisAtEnd = true;
118 Ui()->DoLabel(pRect: &TitleLabel, pText: pTitle, Size: TitleFontSize, Align: Team == TEAM_RED ? TEXTALIGN_ML : TEXTALIGN_MR, LabelProps: Props);
119 }
120
121 if(aScore[0] != '\0')
122 {
123 Ui()->DoLabel(pRect: &ScoreLabel, pText: aScore, Size: TitleFontSize, Align: Team == TEAM_RED ? TEXTALIGN_MR : TEXTALIGN_ML);
124 }
125}
126
127void CScoreboard::RenderGoals(CUIRect Goals)
128{
129 Goals.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
130 Goals.VMargin(Cut: 10.0f, pOtherRect: &Goals);
131
132 const float FontSize = 20.0f;
133 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
134 char aBuf[64];
135
136 if(pGameInfoObj->m_ScoreLimit)
137 {
138 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %d", Localize(pStr: "Score limit"), pGameInfoObj->m_ScoreLimit);
139 Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
140 }
141
142 if(pGameInfoObj->m_TimeLimit)
143 {
144 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Time limit: %d min"), pGameInfoObj->m_TimeLimit);
145 Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_MC);
146 }
147
148 if(pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent)
149 {
150 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Round %d/%d"), pGameInfoObj->m_RoundCurrent, pGameInfoObj->m_RoundNum);
151 Ui()->DoLabel(pRect: &Goals, pText: aBuf, Size: FontSize, Align: TEXTALIGN_MR);
152 }
153}
154
155void CScoreboard::RenderSpectators(CUIRect Spectators)
156{
157 Spectators.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
158 Spectators.Margin(Cut: 10.0f, pOtherRect: &Spectators);
159
160 CTextCursor Cursor;
161 TextRender()->SetCursor(pCursor: &Cursor, x: Spectators.x, y: Spectators.y, FontSize: 22.0f, Flags: TEXTFLAG_RENDER);
162 Cursor.m_LineWidth = Spectators.w;
163 Cursor.m_MaxLines = round_truncate(f: Spectators.h / Cursor.m_FontSize);
164
165 int RemainingSpectators = 0;
166 for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName)
167 {
168 if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS)
169 continue;
170 ++RemainingSpectators;
171 }
172
173 TextRender()->TextEx(pCursor: &Cursor, pText: Localize(pStr: "Spectators"));
174
175 if(RemainingSpectators > 0)
176 {
177 TextRender()->TextEx(pCursor: &Cursor, pText: ": ");
178 }
179
180 bool CommaNeeded = false;
181 for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName)
182 {
183 if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS)
184 continue;
185
186 if(CommaNeeded)
187 {
188 TextRender()->TextEx(pCursor: &Cursor, pText: ", ");
189 }
190
191 if(Cursor.m_LineCount == Cursor.m_MaxLines && RemainingSpectators >= 2)
192 {
193 // This is less expensive than checking with a separate invisible
194 // text cursor though we waste some space at the end of the line.
195 char aRemaining[64];
196 str_format(buffer: aRemaining, buffer_size: sizeof(aRemaining), format: Localize(pStr: "%d others…", pContext: "Spectators"), RemainingSpectators);
197 TextRender()->TextEx(pCursor: &Cursor, pText: aRemaining);
198 break;
199 }
200
201 if(GameClient()->m_aClients[pInfo->m_ClientId].m_AuthLevel)
202 {
203 TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClAuthedPlayerColor)));
204 }
205
206 if(g_Config.m_ClShowIds)
207 {
208 char aClientId[5];
209 str_format(buffer: aClientId, buffer_size: sizeof(aClientId), format: "%d: ", pInfo->m_ClientId);
210 TextRender()->TextEx(pCursor: &Cursor, pText: aClientId);
211 }
212 TextRender()->TextEx(pCursor: &Cursor, pText: GameClient()->m_aClients[pInfo->m_ClientId].m_aName);
213 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
214
215 CommaNeeded = true;
216 --RemainingSpectators;
217 }
218}
219
220void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, int CountEnd)
221{
222 dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid");
223
224 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
225 const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj;
226 const bool TimeScore = GameClient()->m_GameInfo.m_TimeScore;
227 const int NumPlayers = CountEnd - CountStart;
228
229 // calculate measurements
230 float LineHeight;
231 float TeeSizeMod;
232 float Spacing;
233 float RoundRadius;
234 float FontSize;
235 if(NumPlayers <= 8)
236 {
237 LineHeight = 60.0f;
238 TeeSizeMod = 1.0f;
239 Spacing = 16.0f;
240 RoundRadius = 10.0f;
241 FontSize = 24.0f;
242 }
243 else if(NumPlayers <= 12)
244 {
245 LineHeight = 50.0f;
246 TeeSizeMod = 0.9f;
247 Spacing = 5.0f;
248 RoundRadius = 10.0f;
249 FontSize = 24.0f;
250 }
251 else if(NumPlayers <= 16)
252 {
253 LineHeight = 40.0f;
254 TeeSizeMod = 0.8f;
255 Spacing = 0.0f;
256 RoundRadius = 5.0f;
257 FontSize = 24.0f;
258 }
259 else if(NumPlayers <= 24)
260 {
261 LineHeight = 27.0f;
262 TeeSizeMod = 0.6f;
263 Spacing = 0.0f;
264 RoundRadius = 5.0f;
265 FontSize = 20.0f;
266 }
267 else
268 {
269 LineHeight = 20.0f;
270 TeeSizeMod = 0.4f;
271 Spacing = 0.0f;
272 RoundRadius = 5.0f;
273 FontSize = 16.0f;
274 }
275
276 const float ScoreOffset = Scoreboard.x + 10.0f + 10.0f;
277 const float ScoreLength = TextRender()->TextWidth(Size: FontSize, pText: TimeScore ? "00:00:00" : "99999");
278 const float TeeOffset = ScoreOffset + ScoreLength + 15.0f;
279 const float TeeLength = 60.0f * TeeSizeMod;
280 const float NameOffset = TeeOffset + TeeLength;
281 const float NameLength = 300.0f - TeeLength;
282 const float CountryLength = (LineHeight - Spacing - TeeSizeMod * 5.0f) * 2.0f;
283 const float PingLength = 65.0f;
284 const float PingOffset = Scoreboard.x + Scoreboard.w - PingLength - 10.0f - 10.0f;
285 const float CountryOffset = PingOffset - CountryLength;
286 const float ClanLength = Scoreboard.w - ((NameOffset - Scoreboard.x) + NameLength) - (Scoreboard.w - (CountryOffset - Scoreboard.x));
287 const float ClanOffset = CountryOffset - ClanLength;
288
289 // render headlines
290 const float HeadlineFontsize = 22.0f;
291 CUIRect Headline;
292 Scoreboard.HSplitTop(Cut: HeadlineFontsize * 2.0f, pTop: &Headline, pBottom: &Scoreboard);
293 const float HeadlineY = Headline.y + Headline.h / 2.0f - HeadlineFontsize / 2.0f;
294 const char *pScore = TimeScore ? Localize(pStr: "Time") : Localize(pStr: "Score");
295 TextRender()->Text(x: ScoreOffset + ScoreLength - TextRender()->TextWidth(Size: HeadlineFontsize, pText: pScore), y: HeadlineY, Size: HeadlineFontsize, pText: pScore);
296 TextRender()->Text(x: NameOffset, y: HeadlineY, Size: HeadlineFontsize, pText: Localize(pStr: "Name"));
297 const char *pClanLabel = Localize(pStr: "Clan");
298 TextRender()->Text(x: ClanOffset + (ClanLength - TextRender()->TextWidth(Size: HeadlineFontsize, pText: pClanLabel)) / 2.0f, y: HeadlineY, Size: HeadlineFontsize, pText: pClanLabel);
299 const char *pPingLabel = Localize(pStr: "Ping");
300 TextRender()->Text(x: PingOffset + PingLength - TextRender()->TextWidth(Size: HeadlineFontsize, pText: pPingLabel), y: HeadlineY, Size: HeadlineFontsize, pText: pPingLabel);
301
302 // render player entries
303 int CountRendered = 0;
304 int PrevDDTeam = -1;
305
306 char aBuf[64];
307 for(int i = 0; i < MAX_CLIENTS; i++)
308 {
309 // make sure that we render the correct team
310 const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_apInfoByDDTeamScore[i];
311 if(!pInfo || pInfo->m_Team != Team)
312 continue;
313
314 if(CountRendered++ < CountStart)
315 continue;
316
317 int DDTeam = GameClient()->m_Teams.Team(ClientId: pInfo->m_ClientId);
318 int NextDDTeam = 0;
319
320 for(int j = i + 1; j < MAX_CLIENTS; j++)
321 {
322 const CNetObj_PlayerInfo *pInfoNext = GameClient()->m_Snap.m_apInfoByDDTeamScore[j];
323 if(!pInfoNext || pInfoNext->m_Team != Team)
324 continue;
325
326 NextDDTeam = GameClient()->m_Teams.Team(ClientId: pInfoNext->m_ClientId);
327 break;
328 }
329
330 if(PrevDDTeam == -1)
331 {
332 for(int j = i - 1; j >= 0; j--)
333 {
334 const CNetObj_PlayerInfo *pInfoPrev = GameClient()->m_Snap.m_apInfoByDDTeamScore[j];
335 if(!pInfoPrev || pInfoPrev->m_Team != Team)
336 continue;
337
338 PrevDDTeam = GameClient()->m_Teams.Team(ClientId: pInfoPrev->m_ClientId);
339 break;
340 }
341 }
342
343 CUIRect RowAndSpacing, Row;
344 Scoreboard.HSplitTop(Cut: LineHeight + Spacing, pTop: &RowAndSpacing, pBottom: &Scoreboard);
345 RowAndSpacing.HSplitTop(Cut: LineHeight, pTop: &Row, pBottom: nullptr);
346
347 // team background
348 if(DDTeam != TEAM_FLOCK)
349 {
350 const ColorRGBA Color = GameClient()->GetDDTeamColor(DDTeam).WithAlpha(alpha: 0.5f);
351 int TeamRectCorners = 0;
352 if(PrevDDTeam != DDTeam)
353 TeamRectCorners |= IGraphics::CORNER_T;
354 if(NextDDTeam != DDTeam)
355 TeamRectCorners |= IGraphics::CORNER_B;
356 RowAndSpacing.Draw(Color, Corners: TeamRectCorners, Rounding: RoundRadius);
357
358 if(NextDDTeam != DDTeam)
359 {
360 const float TeamFontSize = FontSize / 1.5f;
361 if(NumPlayers > 8)
362 {
363 if(DDTeam == TEAM_SUPER)
364 str_copy(dst&: aBuf, src: Localize(pStr: "Super"));
365 else
366 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", DDTeam);
367 TextRender()->Text(x: Row.x, y: Row.y + Row.h / 2.0f - TeamFontSize / 2.0f, Size: TeamFontSize, pText: aBuf);
368 }
369 else
370 {
371 if(DDTeam == TEAM_SUPER)
372 str_copy(dst&: aBuf, src: Localize(pStr: "Super"));
373 else
374 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Team %d"), DDTeam);
375 TextRender()->Text(x: Row.x + Row.w / 2.0f - TextRender()->TextWidth(Size: TeamFontSize, pText: aBuf) / 2.0f + 10.0f, y: Row.y + Row.h, Size: TeamFontSize, pText: aBuf);
376 }
377 }
378 }
379 PrevDDTeam = DDTeam;
380
381 // background so it's easy to find the local player or the followed one in spectator mode
382 if((!GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_Local) ||
383 (GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW && pInfo->m_Local) ||
384 (GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == GameClient()->m_Snap.m_SpecInfo.m_SpectatorId))
385 {
386 CUIRect Highlight;
387 Row.VMargin(Cut: 10.0f, pOtherRect: &Highlight);
388 Highlight.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: RoundRadius);
389 }
390
391 // score
392 if(TimeScore)
393 {
394 if(pInfo->m_Score == -9999)
395 {
396 aBuf[0] = '\0';
397 }
398 else
399 {
400 str_time(centisecs: (int64_t)absolute(a: pInfo->m_Score) * 100, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
401 }
402 }
403 else
404 {
405 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", clamp(val: pInfo->m_Score, lo: -999, hi: 99999));
406 }
407 TextRender()->Text(x: ScoreOffset + ScoreLength - TextRender()->TextWidth(Size: FontSize, pText: aBuf), y: Row.y + (Row.h - FontSize) / 2.0f, Size: FontSize, pText: aBuf);
408
409 // CTF flag
410 if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) &&
411 pGameDataObj && (pGameDataObj->m_FlagCarrierRed == pInfo->m_ClientId || pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId))
412 {
413 Graphics()->BlendNormal();
414 Graphics()->TextureSet(Texture: pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId ? GameClient()->m_GameSkin.m_SpriteFlagBlue : GameClient()->m_GameSkin.m_SpriteFlagRed);
415 Graphics()->QuadsBegin();
416 Graphics()->QuadsSetSubset(TopLeftU: 1.0f, TopLeftV: 0.0f, BottomRightU: 0.0f, BottomRightV: 1.0f);
417 IGraphics::CQuadItem QuadItem(TeeOffset, Row.y - 5.0f - Spacing / 2.0f, Row.h / 2.0f, Row.h);
418 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
419 Graphics()->QuadsEnd();
420 }
421
422 const CGameClient::CClientData &ClientData = GameClient()->m_aClients[pInfo->m_ClientId];
423
424 // skin
425 {
426 CTeeRenderInfo TeeInfo = ClientData.m_RenderInfo;
427 TeeInfo.m_Size *= TeeSizeMod;
428 vec2 OffsetToMid;
429 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
430 const vec2 TeeRenderPos = vec2(TeeOffset + TeeLength / 2, Row.y + Row.h / 2.0f + OffsetToMid.y);
431 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &TeeInfo, Emote: EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
432 }
433
434 // name
435 {
436 CTextCursor Cursor;
437 TextRender()->SetCursor(pCursor: &Cursor, x: NameOffset, y: Row.y + (Row.h - FontSize) / 2.0f, FontSize, Flags: TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END);
438 Cursor.m_LineWidth = NameLength;
439 if(ClientData.m_AuthLevel)
440 {
441 TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClAuthedPlayerColor)));
442 }
443 if(g_Config.m_ClShowIds)
444 {
445 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s%d: %s", pInfo->m_ClientId < 10 ? " " : "", pInfo->m_ClientId, ClientData.m_aName);
446 TextRender()->TextEx(pCursor: &Cursor, pText: aBuf);
447 }
448 else
449 {
450 TextRender()->TextEx(pCursor: &Cursor, pText: ClientData.m_aName);
451 }
452 }
453
454 // clan
455 {
456 if(str_comp(a: ClientData.m_aClan, b: GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_aClan) == 0)
457 {
458 TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClSameClanColor)));
459 }
460 else
461 {
462 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
463 }
464 CTextCursor Cursor;
465 TextRender()->SetCursor(pCursor: &Cursor, x: ClanOffset + (ClanLength - minimum(a: TextRender()->TextWidth(Size: FontSize, pText: ClientData.m_aClan), b: ClanLength)) / 2.0f, y: Row.y + (Row.h - FontSize) / 2.0f, FontSize, Flags: TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END);
466 Cursor.m_LineWidth = ClanLength;
467 TextRender()->TextEx(pCursor: &Cursor, pText: ClientData.m_aClan);
468 }
469
470 // country flag
471 GameClient()->m_CountryFlags.Render(CountryCode: ClientData.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f),
472 x: CountryOffset, y: Row.y + (Spacing + TeeSizeMod * 5.0f) / 2.0f, w: CountryLength, h: Row.h - Spacing - TeeSizeMod * 5.0f);
473
474 // ping
475 if(g_Config.m_ClEnablePingColor)
476 {
477 TextRender()->TextColor(rgb: color_cast<ColorRGBA>(hsl: ColorHSLA((300.0f - clamp(val: pInfo->m_Latency, lo: 0, hi: 300)) / 1000.0f, 1.0f, 0.5f)));
478 }
479 else
480 {
481 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
482 }
483 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", clamp(val: pInfo->m_Latency, lo: 0, hi: 999));
484 TextRender()->Text(x: PingOffset + PingLength - TextRender()->TextWidth(Size: FontSize, pText: aBuf), y: Row.y + (Row.h - FontSize) / 2.0f, Size: FontSize, pText: aBuf);
485 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
486
487 if(CountRendered == CountEnd)
488 break;
489 }
490}
491
492void CScoreboard::RenderRecordingNotification(float x)
493{
494 char aBuf[512] = "";
495
496 const auto &&AppendRecorderInfo = [&](int Recorder, const char *pName) {
497 if(GameClient()->DemoRecorder(Recorder)->IsRecording())
498 {
499 char aTime[32];
500 str_time(centisecs: (int64_t)GameClient()->DemoRecorder(Recorder)->Length() * 100, format: TIME_HOURS, buffer: aTime, buffer_size: sizeof(aTime));
501 str_append(dst&: aBuf, src: pName);
502 str_append(dst&: aBuf, src: " ");
503 str_append(dst&: aBuf, src: aTime);
504 str_append(dst&: aBuf, src: " ");
505 }
506 };
507
508 AppendRecorderInfo(RECORDER_MANUAL, Localize(pStr: "Manual"));
509 AppendRecorderInfo(RECORDER_RACE, Localize(pStr: "Race"));
510 AppendRecorderInfo(RECORDER_AUTO, Localize(pStr: "Auto"));
511 AppendRecorderInfo(RECORDER_REPLAYS, Localize(pStr: "Replay"));
512
513 if(aBuf[0] == '\0')
514 return;
515
516 const float FontSize = 20.0f;
517
518 CUIRect Rect = {.x: x, .y: 0.0f, .w: TextRender()->TextWidth(Size: FontSize, pText: aBuf) + 60.0f, .h: 50.0f};
519 Rect.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), Corners: IGraphics::CORNER_B, Rounding: 15.0f);
520 Rect.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &Rect);
521 Rect.VSplitRight(Cut: 10.0f, pLeft: &Rect, pRight: nullptr);
522
523 CUIRect Circle;
524 Rect.VSplitLeft(Cut: 20.0f, pLeft: &Circle, pRight: &Rect);
525 Circle.HMargin(Cut: (Circle.h - Circle.w) / 2.0f, pOtherRect: &Circle);
526 Circle.Draw(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: Circle.h / 2.0f);
527
528 Rect.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Rect);
529 Ui()->DoLabel(pRect: &Rect, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
530}
531
532void CScoreboard::OnRender()
533{
534 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
535 return;
536
537 if(!Active())
538 return;
539
540 // if the score board is active, then we should clear the motd message as well
541 if(GameClient()->m_Motd.IsActive())
542 GameClient()->m_Motd.Clear();
543
544 const float Height = 400.0f * 3.0f;
545 const float Width = Height * Graphics()->ScreenAspect();
546 Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: Width, BottomRightY: Height);
547
548 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
549 const bool Teams = pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS);
550 const int NumPlayers = maximum(a: GameClient()->m_Snap.m_aTeamSize[TEAM_RED], b: GameClient()->m_Snap.m_aTeamSize[TEAM_BLUE]);
551
552 const float ScoreboardSmallWidth = 750.0f + 20.0f;
553 const float ScoreboardWidth = !Teams && NumPlayers <= 16 ? ScoreboardSmallWidth : 1500.0f;
554 const float TitleHeight = 60.0f;
555
556 CUIRect Scoreboard = {.x: (Width - ScoreboardWidth) / 2.0f, .y: 150.0f, .w: ScoreboardWidth, .h: 710.0f + TitleHeight};
557
558 if(Teams)
559 {
560 const char *pRedTeamName = GetTeamName(Team: TEAM_RED);
561 const char *pBlueTeamName = GetTeamName(Team: TEAM_BLUE);
562
563 // Game over title
564 const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj;
565 if((pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) && pGameDataObj)
566 {
567 char aTitle[256];
568 if(pGameDataObj->m_TeamscoreRed > pGameDataObj->m_TeamscoreBlue)
569 {
570 TextRender()->TextColor(rgb: ColorRGBA(0.975f, 0.17f, 0.17f, 1.0f));
571 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!"), pRedTeamName);
572 }
573 else if(pGameDataObj->m_TeamscoreBlue > pGameDataObj->m_TeamscoreRed)
574 {
575 TextRender()->TextColor(rgb: ColorRGBA(0.17f, 0.46f, 0.975f, 1.0f));
576 str_format(buffer: aTitle, buffer_size: sizeof(aTitle), format: Localize(pStr: "%s wins!"), pBlueTeamName);
577 }
578 else
579 {
580 TextRender()->TextColor(rgb: ColorRGBA(0.91f, 0.78f, 0.33f, 1.0f));
581 str_copy(dst&: aTitle, src: Localize(pStr: "Draw!"));
582 }
583
584 const float TitleFontSize = 72.0f;
585 CUIRect GameOverTitle = {.x: Scoreboard.x, .y: Scoreboard.y - TitleFontSize - 12.0f, .w: Scoreboard.w, .h: TitleFontSize};
586 Ui()->DoLabel(pRect: &GameOverTitle, pText: aTitle, Size: TitleFontSize, Align: TEXTALIGN_MC);
587 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
588 }
589
590 CUIRect RedScoreboard, BlueScoreboard, RedTitle, BlueTitle;
591 Scoreboard.VSplitMid(pLeft: &RedScoreboard, pRight: &BlueScoreboard, Spacing: 15.0f);
592 RedScoreboard.HSplitTop(Cut: TitleHeight, pTop: &RedTitle, pBottom: &RedScoreboard);
593 BlueScoreboard.HSplitTop(Cut: TitleHeight, pTop: &BlueTitle, pBottom: &BlueScoreboard);
594
595 RedTitle.Draw(Color: ColorRGBA(0.975f, 0.17f, 0.17f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 15.0f);
596 BlueTitle.Draw(Color: ColorRGBA(0.17f, 0.46f, 0.975f, 0.5f), Corners: IGraphics::CORNER_T, Rounding: 15.0f);
597 RedScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 15.0f);
598 BlueScoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_B, Rounding: 15.0f);
599
600 RenderTitle(TitleBar: RedTitle, Team: TEAM_RED, pTitle: pRedTeamName);
601 RenderTitle(TitleBar: BlueTitle, Team: TEAM_BLUE, pTitle: pBlueTeamName);
602 RenderScoreboard(Scoreboard: RedScoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: NumPlayers);
603 RenderScoreboard(Scoreboard: BlueScoreboard, Team: TEAM_BLUE, CountStart: 0, CountEnd: NumPlayers);
604 }
605 else
606 {
607 Scoreboard.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
608
609 const char *pTitle;
610 if(pGameInfoObj && (pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER))
611 {
612 pTitle = Localize(pStr: "Game over");
613 }
614 else
615 {
616 pTitle = Client()->GetCurrentMap();
617 }
618
619 CUIRect Title;
620 Scoreboard.HSplitTop(Cut: TitleHeight, pTop: &Title, pBottom: &Scoreboard);
621 RenderTitle(TitleBar: Title, Team: TEAM_RED, pTitle);
622
623 if(NumPlayers <= 16)
624 {
625 RenderScoreboard(Scoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: NumPlayers);
626 }
627 else
628 {
629 int PlayersPerSide;
630 if(NumPlayers <= 24)
631 PlayersPerSide = 12;
632 else if(NumPlayers <= 32)
633 PlayersPerSide = 16;
634 else if(NumPlayers <= 48)
635 PlayersPerSide = 24;
636 else
637 PlayersPerSide = 32;
638
639 CUIRect LeftScoreboard, RightScoreboard;
640 Scoreboard.VSplitMid(pLeft: &LeftScoreboard, pRight: &RightScoreboard);
641 RenderScoreboard(Scoreboard: LeftScoreboard, Team: TEAM_RED, CountStart: 0, CountEnd: PlayersPerSide);
642 RenderScoreboard(Scoreboard: RightScoreboard, Team: TEAM_RED, CountStart: PlayersPerSide, CountEnd: 2 * PlayersPerSide);
643 }
644 }
645
646 CUIRect Spectators = {.x: (Width - ScoreboardSmallWidth) / 2.0f, .y: Scoreboard.y + Scoreboard.h + 10.0f, .w: ScoreboardSmallWidth, .h: 200.0f};
647 if(pGameInfoObj && (pGameInfoObj->m_ScoreLimit || pGameInfoObj->m_TimeLimit || (pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent)))
648 {
649 CUIRect Goals;
650 Spectators.HSplitTop(Cut: 50.0f, pTop: &Goals, pBottom: &Spectators);
651 Spectators.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Spectators);
652 RenderGoals(Goals);
653 }
654 RenderSpectators(Spectators);
655
656 RenderRecordingNotification(x: (Width / 7) * 4 + 20);
657}
658
659bool CScoreboard::Active() const
660{
661 // if statboard is active don't show scoreboard
662 if(GameClient()->m_Statboard.IsActive())
663 return false;
664
665 if(m_Active)
666 return true;
667
668 if(GameClient()->m_Snap.m_pLocalInfo && !GameClient()->m_Snap.m_SpecInfo.m_Active)
669 {
670 // we are not a spectator, check if we are dead
671 if(!GameClient()->m_Snap.m_pLocalCharacter && g_Config.m_ClScoreboardOnDeath)
672 return true;
673 }
674
675 // if the game is over
676 const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj;
677 if(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)
678 return true;
679
680 return false;
681}
682
683const char *CScoreboard::GetTeamName(int Team) const
684{
685 dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid");
686
687 int ClanPlayers = 0;
688 const char *pClanName = nullptr;
689 const char *pDefaultTeamName = Team == TEAM_RED ? Localize(pStr: "Red team") : Localize(pStr: "Blue team");
690 for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByScore)
691 {
692 if(!pInfo || pInfo->m_Team != Team)
693 continue;
694
695 if(!pClanName)
696 {
697 pClanName = GameClient()->m_aClients[pInfo->m_ClientId].m_aClan;
698 ClanPlayers++;
699 }
700 else
701 {
702 if(str_comp(a: GameClient()->m_aClients[pInfo->m_ClientId].m_aClan, b: pClanName) == 0)
703 ClanPlayers++;
704 else
705 return pDefaultTeamName;
706 }
707 }
708
709 if(ClanPlayers > 1 && pClanName[0] != '\0')
710 return pClanName;
711 else
712 return pDefaultTeamName;
713}
714