Scoreboard
Scoreboard
#include <engine/demo.h>
#include <engine/graphics.h>
#include <engine/shared/config.h>
#include <engine/textrender.h>
#include <game/generated/protocol.h>
#include <game/client/animstate.h>
#include <game/client/components/countryflags.h>
#include <game/client/components/motd.h>
#include <game/client/components/statboard.h>
#include <game/client/gameclient.h>
#include <game/client/render.h>
#include <game/client/ui.h>
#include <game/generated/client_data7.h>
#include <game/localization.h>
CScoreboard::CScoreboard()
{
OnReset();
}
void CScoreboard::OnConsoleInit()
{
Console()->Register("+scoreboard", "", CFGFLAG_CLIENT, ConKeyScoreboard,
this, "Show scoreboard");
}
void CScoreboard::OnInit()
{
m_DeadTeeTexture = Graphics()->LoadTexture("deadtee.png",
IStorage::TYPE_ALL);
}
void CScoreboard::OnReset()
{
m_Active = false;
m_ServerRecord = -1.0f;
}
void CScoreboard::OnRelease()
{
m_Active = false;
}
TitleBar.VMargin(20.0f, &TitleBar);
CUIRect TitleLabel, ScoreLabel;
if(Team == TEAM_RED)
{
TitleBar.VSplitRight(ScoreTextWidth, &TitleLabel, &ScoreLabel);
TitleLabel.VSplitRight(10.0f, &TitleLabel, nullptr);
}
else
{
TitleBar.VSplitLeft(ScoreTextWidth, &ScoreLabel, &TitleLabel);
TitleLabel.VSplitLeft(10.0f, nullptr, &TitleLabel);
}
{
SLabelProperties Props;
Props.m_MaxWidth = TitleLabel.w;
Props.m_EllipsisAtEnd = true;
Ui()->DoLabel(&TitleLabel, pTitle, TitleFontSize, Team == TEAM_RED ?
TEXTALIGN_ML : TEXTALIGN_MR, Props);
}
if(aScore[0] != '\0')
{
Ui()->DoLabel(&ScoreLabel, aScore, TitleFontSize, Team == TEAM_RED ?
TEXTALIGN_MR : TEXTALIGN_ML);
}
}
if(pGameInfoObj->m_ScoreLimit)
{
str_format(aBuf, sizeof(aBuf), "%s: %d", Localize("Score limit"),
pGameInfoObj->m_ScoreLimit);
Ui()->DoLabel(&Goals, aBuf, FontSize, TEXTALIGN_ML);
}
if(pGameInfoObj->m_TimeLimit)
{
str_format(aBuf, sizeof(aBuf), Localize("Time limit: %d min"),
pGameInfoObj->m_TimeLimit);
Ui()->DoLabel(&Goals, aBuf, FontSize, TEXTALIGN_MC);
}
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, Spectators.x, Spectators.y, 22.0f,
TEXTFLAG_RENDER);
Cursor.m_LineWidth = Spectators.w;
Cursor.m_MaxLines = round_truncate(Spectators.h / Cursor.m_FontSize);
TextRender()->TextEx(&Cursor, pClanName);
TextRender()->TextEx(&Cursor, " ");
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
CommaNeeded = true;
}
}
// calculate measurements
float LineHeight;
float TeeSizeMod;
float Spacing;
float RoundRadius;
float FontSize;
if(NumPlayers <= 8)
{
LineHeight = 60.0f;
TeeSizeMod = 1.0f;
Spacing = 16.0f;
RoundRadius = 10.0f;
FontSize = 24.0f;
}
else if(NumPlayers <= 12)
{
LineHeight = 50.0f;
TeeSizeMod = 0.9f;
Spacing = 5.0f;
RoundRadius = 10.0f;
FontSize = 24.0f;
}
else if(NumPlayers <= 16)
{
LineHeight = 40.0f;
TeeSizeMod = 0.8f;
Spacing = 0.0f;
RoundRadius = 5.0f;
FontSize = 24.0f;
}
else if(NumPlayers <= 24)
{
LineHeight = 27.0f;
TeeSizeMod = 0.6f;
Spacing = 0.0f;
RoundRadius = 5.0f;
FontSize = 20.0f;
}
else if(NumPlayers <= 32)
{
LineHeight = 20.0f;
TeeSizeMod = 0.4f;
Spacing = 0.0f;
RoundRadius = 5.0f;
FontSize = 16.0f;
}
else if(LowScoreboardWidth)
{
LineHeight = 15.0f;
TeeSizeMod = 0.25f;
Spacing = 0.0f;
RoundRadius = 2.0f;
FontSize = 14.0f;
}
else
{
LineHeight = 10.0f;
TeeSizeMod = 0.2f;
Spacing = 0.0f;
RoundRadius = 2.0f;
FontSize = 10.0f;
}
// render headlines
const float HeadlineFontsize = 22.0f;
CUIRect Headline;
Scoreboard.HSplitTop(HeadlineFontsize * 2.0f, &Headline, &Scoreboard);
const float HeadlineY = Headline.y + Headline.h / 2.0f - HeadlineFontsize /
2.0f;
const char *pScore = TimeScore ? Localize("Time") : Localize("Score");
TextRender()->Text(ScoreOffset + ScoreLength - TextRender()-
>TextWidth(HeadlineFontsize, pScore), HeadlineY, HeadlineFontsize, pScore);
TextRender()->Text(NameOffset, HeadlineY, HeadlineFontsize,
Localize("Name"));
const char *pClanLabel = Localize("Clan");
TextRender()->Text(ClanOffset + (ClanLength - TextRender()-
>TextWidth(HeadlineFontsize, pClanLabel)) / 2.0f, HeadlineY, HeadlineFontsize,
pClanLabel);
const char *pPingLabel = Localize("Ping");
TextRender()->Text(PingOffset + PingLength - TextRender()-
>TextWidth(HeadlineFontsize, pPingLabel), HeadlineY, HeadlineFontsize, pPingLabel);
char aBuf[64];
int MaxTeamSize = Config()->m_SvMaxTeamSize;
NextDDTeam = GameClient()->m_Teams.Team(pInfoNext-
>m_ClientId);
break;
}
if(PrevDDTeam == -1)
{
for(int j = i - 1; j >= 0; j--)
{
const CNetObj_PlayerInfo *pInfoPrev = GameClient()-
>m_Snap.m_apInfoByDDTeamScore[j];
if(!pInfoPrev || pInfoPrev->m_Team != Team)
continue;
PrevDDTeam = GameClient()->m_Teams.Team(pInfoPrev-
>m_ClientId);
break;
}
}
// team background
if(DDTeam != TEAM_FLOCK)
{
const ColorRGBA Color = GameClient()-
>GetDDTeamColor(DDTeam).WithAlpha(0.5f);
int TeamRectCorners = 0;
if(PrevDDTeam != DDTeam)
{
TeamRectCorners |= IGraphics::CORNER_T;
State.m_TeamStartX = Row.x;
State.m_TeamStartY = Row.y;
}
if(NextDDTeam != DDTeam)
TeamRectCorners |= IGraphics::CORNER_B;
RowAndSpacing.Draw(Color, TeamRectCorners, RoundRadius);
CurrentDDTeamSize++;
if(NextDDTeam != DDTeam)
{
const float TeamFontSize = FontSize / 1.5f;
if(NumPlayers > 8)
{
if(DDTeam == TEAM_SUPER)
str_copy(aBuf, Localize("Super"));
else if(CurrentDDTeamSize <= 1)
str_format(aBuf, sizeof(aBuf), "%d",
DDTeam);
else
str_format(aBuf, sizeof(aBuf),
Localize("%d\n(%d/%d)", "Team and size"), DDTeam, CurrentDDTeamSize, MaxTeamSize);
TextRender()->Text(State.m_TeamStartX,
maximum(State.m_TeamStartY + Row.h / 2.0f - TeamFontSize, State.m_TeamStartY + 3.0f
/* padding top */), TeamFontSize, aBuf);
}
else
{
if(DDTeam == TEAM_SUPER)
str_copy(aBuf, Localize("Super"));
else if(CurrentDDTeamSize > 1)
str_format(aBuf, sizeof(aBuf),
Localize("Team %d (%d/%d)"), DDTeam, CurrentDDTeamSize, MaxTeamSize);
else
str_format(aBuf, sizeof(aBuf),
Localize("Team %d"), DDTeam);
TextRender()->Text(Row.x + Row.w / 2.0f -
TextRender()->TextWidth(TeamFontSize, aBuf) / 2.0f + 10.0f, Row.y + Row.h,
TeamFontSize, aBuf);
}
CurrentDDTeamSize = 0;
}
}
PrevDDTeam = DDTeam;
// score
if(Race7)
{
if(pInfo->m_Score == -1)
{
aBuf[0] = '\0';
}
else
{
// 0.7 uses milliseconds and ddnets str_time wants
centiseconds
// 0.7 servers can also send the amount of precision
the client should use
// we ignore that and always show 3 digit precision
str_time((int64_t)absolute(pInfo->m_Score / 10),
TIME_MINS_CENTISECS, aBuf, sizeof(aBuf));
}
}
else if(TimeScore)
{
if(pInfo->m_Score == -9999)
{
aBuf[0] = '\0';
}
else
{
str_time((int64_t)absolute(pInfo->m_Score) * 100,
TIME_HOURS, aBuf, sizeof(aBuf));
}
}
else
{
str_format(aBuf, sizeof(aBuf), "%d", clamp(pInfo->m_Score,
-999, 99999));
}
TextRender()->Text(ScoreOffset + ScoreLength - TextRender()-
>TextWidth(FontSize, aBuf), Row.y + (Row.h - FontSize) / 2.0f, FontSize, aBuf);
// CTF flag
if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS)
&&
pGameDataObj && (pGameDataObj->m_FlagCarrierRed == pInfo-
>m_ClientId || pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId))
{
Graphics()->BlendNormal();
Graphics()->TextureSet(pGameDataObj->m_FlagCarrierBlue ==
pInfo->m_ClientId ? GameClient()->m_GameSkin.m_SpriteFlagBlue : GameClient()-
>m_GameSkin.m_SpriteFlagRed);
Graphics()->QuadsBegin();
Graphics()->QuadsSetSubset(1.0f, 0.0f, 0.0f, 1.0f);
IGraphics::CQuadItem QuadItem(TeeOffset, Row.y - 5.0f -
Spacing / 2.0f, Row.h / 2.0f, Row.h);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
// skin
if(RenderDead)
{
Graphics()->BlendNormal();
Graphics()->TextureSet(m_DeadTeeTexture);
Graphics()->QuadsBegin();
if(m_pClient->IsTeamPlay())
{
Graphics()->SetColor(m_pClient-
>m_Skins7.GetTeamColor(true, 0, m_pClient->m_aClients[pInfo->m_ClientId].m_Team,
protocol7::SKINPART_BODY));
}
CTeeRenderInfo TeeInfo = m_pClient->m_aClients[pInfo-
>m_ClientId].m_RenderInfo;
TeeInfo.m_Size *= TeeSizeMod;
IGraphics::CQuadItem QuadItem(TeeOffset, Row.y,
TeeInfo.m_Size, TeeInfo.m_Size);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
else
{
CTeeRenderInfo TeeInfo = ClientData.m_RenderInfo;
TeeInfo.m_Size *= TeeSizeMod;
vec2 OffsetToMid;
CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(),
&TeeInfo, OffsetToMid);
const vec2 TeeRenderPos = vec2(TeeOffset + TeeLength / 2,
Row.y + Row.h / 2.0f + OffsetToMid.y);
RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeInfo,
EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos);
}
// name
{
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, NameOffset, Row.y + (Row.h
- FontSize) / 2.0f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END);
Cursor.m_LineWidth = NameLength;
if(ClientData.m_AuthLevel)
{
TextRender()-
>TextColor(color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClAuthedPlayerColor)));
}
if(g_Config.m_ClShowIds)
{
char aClientId[16];
GameClient()->FormatClientId(pInfo->m_ClientId,
aClientId, EClientIdFormat::INDENT_AUTO);
TextRender()->TextEx(&Cursor, aClientId);
}
TextRender()->TextEx(&Cursor, ClientData.m_aName);
// ready / watching
if(Client()->IsSixup() && Client()-
>m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 &
protocol7::PLAYERFLAG_READY)
{
TextRender()->TextColor(0.1f, 1.0f, 0.1f,
TextColor.a);
TextRender()->TextEx(&Cursor, "✓");
}
}
// clan
{
if(GameClient()->m_aLocalIds[g_Config.m_ClDummy] >= 0 &&
str_comp(ClientData.m_aClan, GameClient()->m_aClients[GameClient()-
>m_aLocalIds[g_Config.m_ClDummy]].m_aClan) == 0)
{
TextRender()-
>TextColor(color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClSameClanColor)));
}
else
{
TextRender()->TextColor(TextColor);
}
CTextCursor Cursor;
TextRender()->SetCursor(&Cursor, ClanOffset + (ClanLength -
minimum(TextRender()->TextWidth(FontSize, ClientData.m_aClan), ClanLength)) / 2.0f,
Row.y + (Row.h - FontSize) / 2.0f, FontSize, TEXTFLAG_RENDER |
TEXTFLAG_ELLIPSIS_AT_END);
Cursor.m_LineWidth = ClanLength;
TextRender()->TextEx(&Cursor, ClientData.m_aClan);
}
// country flag
GameClient()->m_CountryFlags.Render(ClientData.m_Country,
ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f),
CountryOffset, Row.y + (Spacing + TeeSizeMod * 5.0f) /
2.0f, CountryLength, Row.h - Spacing - TeeSizeMod * 5.0f);
// ping
if(g_Config.m_ClEnablePingColor)
{
TextRender()-
>TextColor(color_cast<ColorRGBA>(ColorHSLA((300.0f - clamp(pInfo->m_Latency, 0,
300)) / 1000.0f, 1.0f, 0.5f)));
}
else
{
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
str_format(aBuf, sizeof(aBuf), "%d", clamp(pInfo->m_Latency, 0,
999));
TextRender()->Text(PingOffset + PingLength - TextRender()-
>TextWidth(FontSize, aBuf), Row.y + (Row.h - FontSize) / 2.0f, FontSize, aBuf);
TextRender()->TextColor(TextRender()->DefaultTextColor());
if(CountRendered == CountEnd)
break;
}
}
}
void CScoreboard::RenderRecordingNotification(float x)
{
char aBuf[512] = "";
AppendRecorderInfo(RECORDER_MANUAL, Localize("Manual"));
AppendRecorderInfo(RECORDER_RACE, Localize("Race"));
AppendRecorderInfo(RECORDER_AUTO, Localize("Auto"));
AppendRecorderInfo(RECORDER_REPLAYS, Localize("Replay"));
if(aBuf[0] == '\0')
return;
CUIRect Circle;
Rect.VSplitLeft(20.0f, &Circle, &Rect);
Circle.HMargin((Circle.h - Circle.w) / 2.0f, &Circle);
Circle.Draw(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), IGraphics::CORNER_ALL,
Circle.h / 2.0f);
if(!IsActive())
return;
// if the score board is active, then we should clear the motd message as
well
if(GameClient()->m_Motd.IsActive())
GameClient()->m_Motd.Clear();
if(Teams)
{
const char *pRedTeamName = GetTeamName(TEAM_RED);
const char *pBlueTeamName = GetTeamName(TEAM_BLUE);
CUIRect Title;
Scoreboard.HSplitTop(TitleHeight, &Title, &Scoreboard);
RenderTitle(Title, TEAM_RED, pTitle);
RenderRecordingNotification((Width / 7) * 4 + 20);
}
if(m_Active)
return true;
return false;
}
int ClanPlayers = 0;
const char *pClanName = nullptr;
for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByScore)
{
if(!pInfo || pInfo->m_Team != Team)
continue;
if(!pClanName)
{
pClanName = GameClient()->m_aClients[pInfo->m_ClientId].m_aClan;
ClanPlayers++;
}
else
{
if(str_comp(GameClient()->m_aClients[pInfo->m_ClientId].m_aClan,
pClanName) == 0)
ClanPlayers++;
else
return nullptr;
}
}