1#include <engine/graphics.h>
2#include <engine/serverbrowser.h>
3#include <engine/shared/config.h>
4#include <engine/storage.h>
5#include <engine/textrender.h>
6
7#include <generated/client_data.h>
8
9#include <game/client/animstate.h>
10#include <game/client/components/motd.h>
11#include <game/client/components/statboard.h>
12#include <game/client/gameclient.h>
13#include <game/localization.h>
14
15CStatboard::CStatboard()
16{
17 m_Active = false;
18 m_ScreenshotTaken = false;
19 m_ScreenshotTime = -1;
20}
21
22void CStatboard::OnReset()
23{
24 for(auto &Stat : GameClient()->m_aStats)
25 Stat.Reset();
26 m_Active = false;
27 m_ScreenshotTaken = false;
28 m_ScreenshotTime = -1;
29}
30
31void CStatboard::OnRelease()
32{
33 m_Active = false;
34}
35
36void CStatboard::ConKeyStats(IConsole::IResult *pResult, void *pUserData)
37{
38 ((CStatboard *)pUserData)->m_Active = pResult->GetInteger(Index: 0) != 0;
39}
40
41void CStatboard::OnConsoleInit()
42{
43 Console()->Register(pName: "+statboard", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConKeyStats, pUser: this, pHelp: "Show stats");
44}
45
46bool CStatboard::IsActive() const
47{
48 return m_Active;
49}
50
51void CStatboard::OnMessage(int MsgType, void *pRawMsg)
52{
53 if(GameClient()->m_SuppressEvents)
54 return;
55
56 if(MsgType == NETMSGTYPE_SV_KILLMSG)
57 {
58 CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg;
59 CGameClient::CClientStats *pStats = GameClient()->m_aStats;
60
61 pStats[pMsg->m_Victim].m_Deaths++;
62 pStats[pMsg->m_Victim].m_CurrentSpree = 0;
63 if(pMsg->m_Weapon >= 0)
64 pStats[pMsg->m_Victim].m_aDeathsFrom[pMsg->m_Weapon]++;
65 if(pMsg->m_Victim != pMsg->m_Killer)
66 {
67 pStats[pMsg->m_Killer].m_Frags++;
68 pStats[pMsg->m_Killer].m_CurrentSpree++;
69
70 if(pStats[pMsg->m_Killer].m_CurrentSpree > pStats[pMsg->m_Killer].m_BestSpree)
71 pStats[pMsg->m_Killer].m_BestSpree = pStats[pMsg->m_Killer].m_CurrentSpree;
72 if(pMsg->m_Weapon >= 0)
73 pStats[pMsg->m_Killer].m_aFragsWith[pMsg->m_Weapon]++;
74 }
75 else
76 pStats[pMsg->m_Victim].m_Suicides++;
77 }
78 else if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM)
79 {
80 CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg;
81 CGameClient::CClientStats *pStats = GameClient()->m_aStats;
82
83 for(int i = 0; i < MAX_CLIENTS; i++)
84 {
85 if(GameClient()->m_Teams.Team(ClientId: i) == pMsg->m_Team)
86 {
87 pStats[i].m_Deaths++;
88 pStats[i].m_Suicides++;
89 }
90 }
91 }
92 else if(MsgType == NETMSGTYPE_SV_CHAT)
93 {
94 CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
95 if(pMsg->m_ClientId < 0)
96 {
97 const char *p, *t;
98 const char *pLookFor = "flag was captured by '";
99 if((p = str_find(haystack: pMsg->m_pMessage, needle: pLookFor)))
100 {
101 char aName[MAX_NAME_LENGTH];
102 p += str_length(str: pLookFor);
103 t = str_rchr(haystack: p, needle: '\'');
104
105 if(t <= p)
106 return;
107 str_truncate(dst: aName, dst_size: sizeof(aName), src: p, truncation_len: t - p);
108
109 for(int i = 0; i < MAX_CLIENTS; i++)
110 {
111 if(!GameClient()->m_aStats[i].IsActive())
112 continue;
113
114 if(str_comp(a: GameClient()->m_aClients[i].m_aName, b: aName) == 0)
115 {
116 GameClient()->m_aStats[i].m_FlagCaptures++;
117 break;
118 }
119 }
120 }
121 }
122 }
123}
124
125void CStatboard::OnRender()
126{
127 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
128 return;
129
130 if((g_Config.m_ClAutoStatboardScreenshot || g_Config.m_ClAutoCSV) && Client()->State() != IClient::STATE_DEMOPLAYBACK)
131 {
132 if(m_ScreenshotTime < 0 && GameClient()->m_Snap.m_pGameInfoObj && GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)
133 m_ScreenshotTime = time_get() + time_freq() * 3;
134 if(m_ScreenshotTime > -1 && m_ScreenshotTime < time_get())
135 m_Active = true;
136 if(!m_ScreenshotTaken && m_ScreenshotTime > -1 && m_ScreenshotTime + time_freq() / 5 < time_get())
137 {
138 if(g_Config.m_ClAutoStatboardScreenshot)
139 AutoStatScreenshot();
140 if(g_Config.m_ClAutoCSV)
141 AutoStatCSV();
142 m_ScreenshotTaken = true;
143 }
144 }
145
146 if(IsActive())
147 RenderGlobalStats();
148}
149
150void CStatboard::RenderGlobalStats()
151{
152 const float StatboardWidth = 400 * 3.0f * Graphics()->ScreenAspect();
153 const float StatboardHeight = 400 * 3.0f;
154 float StatboardContentWidth = 260.0f;
155 float StatboardContentHeight = 750.0f;
156
157 const CNetObj_PlayerInfo *apPlayers[MAX_CLIENTS] = {nullptr};
158 int NumPlayers = 0;
159
160 // sort red or dm players by score
161 for(const auto *pInfo : GameClient()->m_Snap.m_apInfoByScore)
162 {
163 if(!pInfo || !GameClient()->m_aStats[pInfo->m_ClientId].IsActive() || GameClient()->m_aClients[pInfo->m_ClientId].m_Team != TEAM_RED)
164 continue;
165 apPlayers[NumPlayers] = pInfo;
166 NumPlayers++;
167 }
168
169 // sort blue players by score after
170 if(GameClient()->IsTeamPlay())
171 {
172 for(const auto *pInfo : GameClient()->m_Snap.m_apInfoByScore)
173 {
174 if(!pInfo || !GameClient()->m_aStats[pInfo->m_ClientId].IsActive() || GameClient()->m_aClients[pInfo->m_ClientId].m_Team != TEAM_BLUE)
175 continue;
176 apPlayers[NumPlayers] = pInfo;
177 NumPlayers++;
178 }
179 }
180
181 // Dirty hack. Do not show scoreboard if there are more than 32 players
182 // remove as soon as support of more than 32 players is required
183 if(NumPlayers > 32)
184 return;
185
186 //clear motd if it is active
187 if(GameClient()->m_Motd.IsActive())
188 GameClient()->m_Motd.Clear();
189
190 bool GameWithFlags = GameClient()->m_Snap.m_pGameInfoObj &&
191 GameClient()->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS;
192
193 StatboardContentWidth += 7 * 85 + 95; // Suicides 95; other labels 85
194
195 if(GameWithFlags)
196 StatboardContentWidth += 150; // Grabs & Flags
197
198 bool aDisplayWeapon[NUM_WEAPONS] = {false};
199 for(int i = 0; i < NumPlayers; i++)
200 {
201 const CGameClient::CClientStats *pStats = &GameClient()->m_aStats[apPlayers[i]->m_ClientId];
202 for(int j = 0; j < NUM_WEAPONS; j++)
203 aDisplayWeapon[j] = aDisplayWeapon[j] || pStats->m_aFragsWith[j] || pStats->m_aDeathsFrom[j];
204 }
205 for(bool DisplayWeapon : aDisplayWeapon)
206 if(DisplayWeapon)
207 StatboardContentWidth += 80;
208
209 float x = StatboardWidth / 2 - StatboardContentWidth / 2;
210 float y = 200.0f;
211
212 Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: StatboardWidth, BottomRightY: StatboardHeight);
213
214 Graphics()->DrawRect(x: x - 10.f, y: y - 10.f, w: StatboardContentWidth, h: StatboardContentHeight, Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 17.0f);
215
216 int px = 325;
217
218 TextRender()->Text(x: x + 10, y: y - 5, Size: 22.0f, pText: Localize(pStr: "Name"), LineWidth: -1.0f);
219 const char *apHeaders[] = {
220 Localize(pStr: "Frags"), Localize(pStr: "Deaths"), Localize(pStr: "Suicides"),
221 Localize(pStr: "Ratio"), Localize(pStr: "Net"), Localize(pStr: "FPM"),
222 Localize(pStr: "Spree"), Localize(pStr: "Best"), Localize(pStr: "Grabs")};
223 for(int i = 0; i < 9; i++)
224 {
225 if(i == 2)
226 px += 10.0f; // Suicides
227 if(i == 8 && !GameWithFlags) // Don't draw "Grabs" in game with no flag
228 continue;
229 const float TextWidth = TextRender()->TextWidth(Size: 22.0f, pText: apHeaders[i], StrLength: -1, LineWidth: -1.0f);
230 TextRender()->Text(x: x + px - TextWidth, y: y - 5, Size: 22.0f, pText: apHeaders[i], LineWidth: -1.0f);
231 px += 85;
232 }
233
234 px -= 40;
235 for(int i = 0; i < NUM_WEAPONS; i++)
236 {
237 if(!aDisplayWeapon[i])
238 continue;
239 float ScaleX, ScaleY;
240 Graphics()->GetSpriteScale(pSprite: g_pData->m_Weapons.m_aId[i].m_pSpriteBody, ScaleX, ScaleY);
241 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpriteWeapons[i]);
242 Graphics()->QuadsBegin();
243 if(i == 0)
244 Graphics()->DrawSprite(x: x + px, y: y + 10, ScaledWidth: g_pData->m_Weapons.m_aId[i].m_VisualSize * 0.8f * ScaleX, ScaledHeight: g_pData->m_Weapons.m_aId[i].m_VisualSize * 0.8f * ScaleY);
245 else
246 Graphics()->DrawSprite(x: x + px, y: y + 10, ScaledWidth: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX, ScaledHeight: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY);
247 px += 80;
248 Graphics()->QuadsEnd();
249 }
250
251 if(GameWithFlags)
252 {
253 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteFlagRed);
254 float ScaleX, ScaleY;
255 Graphics()->GetSpriteScale(Id: SPRITE_FLAG_RED, ScaleX, ScaleY);
256 Graphics()->QuadsBegin();
257 Graphics()->QuadsSetRotation(Angle: 0.78f);
258 Graphics()->DrawSprite(x: x + px, y: y + 15, ScaledWidth: 48 * ScaleX, ScaledHeight: 48 * ScaleY);
259 Graphics()->QuadsEnd();
260 }
261
262 y += 29.0f;
263
264 float FontSize = 24.0f;
265 float LineHeight = 50.0f;
266 float TeeSizemod = 0.8f;
267 float ContentLineOffset = LineHeight * 0.05f;
268
269 if(NumPlayers > 16)
270 {
271 FontSize = 20.0f;
272 LineHeight = 22.0f;
273 TeeSizemod = 0.34f;
274 ContentLineOffset = 0;
275 }
276 else if(NumPlayers > 14)
277 {
278 FontSize = 24.0f;
279 LineHeight = 40.0f;
280 TeeSizemod = 0.7f;
281 }
282
283 for(int j = 0; j < NumPlayers; j++)
284 {
285 const CNetObj_PlayerInfo *pInfo = apPlayers[j];
286 const CGameClient::CClientStats *pStats = &GameClient()->m_aStats[pInfo->m_ClientId];
287
288 if(GameClient()->m_Snap.m_LocalClientId == pInfo->m_ClientId || (GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == GameClient()->m_Snap.m_SpecInfo.m_SpectatorId))
289 {
290 // background so it's easy to find the local player
291 Graphics()->DrawRect(x: x - 10, y: y + ContentLineOffset / 2, w: StatboardContentWidth, h: LineHeight - ContentLineOffset, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
292 }
293
294 CTeeRenderInfo Teeinfo = GameClient()->m_aClients[pInfo->m_ClientId].m_RenderInfo;
295 Teeinfo.m_Size *= TeeSizemod;
296
297 const CAnimState *pIdleState = CAnimState::GetIdle();
298 vec2 OffsetToMid;
299 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &Teeinfo, TeeOffsetToMid&: OffsetToMid);
300 vec2 TeeRenderPos(x + Teeinfo.m_Size / 2, y + LineHeight / 2.0f + OffsetToMid.y);
301
302 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &Teeinfo, Emote: EMOTE_NORMAL, Dir: vec2(1, 0), Pos: TeeRenderPos);
303
304 char aBuf[128];
305 CTextCursor Cursor;
306 Cursor.SetPosition(vec2(x + 64, y + (LineHeight * 0.95f - FontSize) / 2.f));
307 Cursor.m_FontSize = FontSize;
308 Cursor.m_Flags |= TEXTFLAG_STOP_AT_END;
309 Cursor.m_LineWidth = 220;
310 TextRender()->TextEx(pCursor: &Cursor, pText: GameClient()->m_aClients[pInfo->m_ClientId].m_aName, Length: -1);
311
312 px = 325;
313
314 // FRAGS
315 {
316 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pStats->m_Frags);
317 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
318 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
319 px += 85;
320 }
321 // DEATHS
322 {
323 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pStats->m_Deaths);
324 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
325 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
326 px += 85;
327 }
328 // SUICIDES
329 {
330 px += 10;
331 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pStats->m_Suicides);
332 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
333 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
334 px += 85;
335 }
336 // RATIO
337 {
338 if(pStats->m_Deaths == 0)
339 str_copy(dst&: aBuf, src: "--");
340 else
341 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%.2f", (float)(pStats->m_Frags) / pStats->m_Deaths);
342 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
343 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
344 px += 85;
345 }
346 // NET
347 {
348 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%+d", pStats->m_Frags - pStats->m_Deaths);
349 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
350 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
351 px += 85;
352 }
353 // FPM
354 {
355 float Fpm = pStats->GetFPM(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy), TickSpeed: Client()->GameTickSpeed());
356 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%.1f", Fpm);
357 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
358 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
359 px += 85;
360 }
361 // SPREE
362 {
363 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pStats->m_CurrentSpree);
364 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
365 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
366 px += 85;
367 }
368 // BEST SPREE
369 {
370 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pStats->m_BestSpree);
371 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
372 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
373 px += 85;
374 }
375 // GRABS
376 if(GameWithFlags)
377 {
378 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pStats->m_FlagGrabs);
379 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
380 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
381 px += 85;
382 }
383 // WEAPONS
384 px -= 40;
385 for(int i = 0; i < NUM_WEAPONS; i++)
386 {
387 if(!aDisplayWeapon[i])
388 continue;
389
390 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d/%d", pStats->m_aFragsWith[i], pStats->m_aDeathsFrom[i]);
391 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
392 TextRender()->Text(x: x + px - TextWidth / 2, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
393 px += 80;
394 }
395 // FLAGS
396 if(GameWithFlags)
397 {
398 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pStats->m_FlagCaptures);
399 const float TextWidth = TextRender()->TextWidth(Size: FontSize, pText: aBuf, StrLength: -1, LineWidth: -1.0f);
400 TextRender()->Text(x: x - TextWidth + px, y: y + (LineHeight * 0.95f - FontSize) / 2.f, Size: FontSize, pText: aBuf, LineWidth: -1.0f);
401 }
402 y += LineHeight;
403 }
404}
405
406void CStatboard::AutoStatScreenshot()
407{
408 if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
409 Client()->AutoStatScreenshot_Start();
410}
411
412void CStatboard::AutoStatCSV()
413{
414 if(Client()->State() != IClient::STATE_DEMOPLAYBACK)
415 {
416 char aDate[20], aFilename[IO_MAX_PATH_LENGTH];
417 str_timestamp(buffer: aDate, buffer_size: sizeof(aDate));
418 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "screenshots/auto/stats_%s.csv", aDate);
419 IOHANDLE File = Storage()->OpenFile(pFilename: aFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
420
421 if(File)
422 {
423 char aStats[1024 * (VANILLA_MAX_CLIENTS + 1)];
424 FormatStats(pDest: aStats, DestSize: sizeof(aStats));
425 io_write(io: File, buffer: aStats, size: str_length(str: aStats));
426 io_close(io: File);
427 }
428
429 Client()->AutoCSV_Start();
430 }
431}
432
433std::string CStatboard::ReplaceCommata(char *pStr)
434{
435 if(!str_find(haystack: pStr, needle: ","))
436 return pStr;
437
438 char aOutbuf[256] = "";
439 for(int i = 0, Skip = 0; i < 64; i++)
440 {
441 if(pStr[i] == ',')
442 {
443 aOutbuf[i + Skip++] = '%';
444 aOutbuf[i + Skip++] = '2';
445 aOutbuf[i + Skip] = 'C';
446 }
447 else
448 aOutbuf[i + Skip] = pStr[i];
449 }
450 return aOutbuf;
451}
452
453void CStatboard::FormatStats(char *pDest, size_t DestSize)
454{
455 // server stats
456 CServerInfo CurrentServerInfo;
457 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
458 char aServerStats[1024];
459 str_format(buffer: aServerStats, buffer_size: sizeof(aServerStats), format: "Servername,Game-type,Map\n%s,%s,%s", ReplaceCommata(pStr: CurrentServerInfo.m_aName).c_str(), ReplaceCommata(pStr: CurrentServerInfo.m_aGameType).c_str(), ReplaceCommata(pStr: CurrentServerInfo.m_aMap).c_str());
460
461 // player stats
462
463 // sort players
464 const CNetObj_PlayerInfo *apPlayers[MAX_CLIENTS] = {nullptr};
465 int NumPlayers = 0;
466
467 // sort red or dm players by score
468 for(const auto *pInfo : GameClient()->m_Snap.m_apInfoByScore)
469 {
470 if(!pInfo || !GameClient()->m_aStats[pInfo->m_ClientId].IsActive() || GameClient()->m_aClients[pInfo->m_ClientId].m_Team != TEAM_RED)
471 continue;
472 apPlayers[NumPlayers] = pInfo;
473 NumPlayers++;
474 }
475
476 // sort blue players by score after
477 if(GameClient()->IsTeamPlay())
478 {
479 for(const auto *pInfo : GameClient()->m_Snap.m_apInfoByScore)
480 {
481 if(!pInfo || !GameClient()->m_aStats[pInfo->m_ClientId].IsActive() || GameClient()->m_aClients[pInfo->m_ClientId].m_Team != TEAM_BLUE)
482 continue;
483 apPlayers[NumPlayers] = pInfo;
484 NumPlayers++;
485 }
486 }
487
488 char aPlayerStats[1024 * VANILLA_MAX_CLIENTS] = "Local-player,Team,Name,Clan,Score,Frags,Deaths,Suicides,F/D-ratio,Net,FPM,Spree,Best,Hammer-F/D,Gun-F/D,Shotgun-F/D,Grenade-F/D,Laser-F/D,Ninja-F/D,GameWithFlags,Flag-grabs,Flag-captures\n";
489 for(int i = 0; i < NumPlayers; i++)
490 {
491 const CNetObj_PlayerInfo *pInfo = apPlayers[i];
492 const CGameClient::CClientStats *pStats = &GameClient()->m_aStats[pInfo->m_ClientId];
493
494 // Pre-formatting
495
496 // Weapons frags/deaths
497 char aWeaponFD[64 * NUM_WEAPONS];
498 for(int j = 0; j < NUM_WEAPONS; j++)
499 {
500 if(j == 0)
501 str_format(buffer: aWeaponFD, buffer_size: sizeof(aWeaponFD), format: "%d/%d", pStats->m_aFragsWith[j], pStats->m_aDeathsFrom[j]);
502 else
503 str_format(buffer: aWeaponFD, buffer_size: sizeof(aWeaponFD), format: "%s,%d/%d", aWeaponFD, pStats->m_aFragsWith[j], pStats->m_aDeathsFrom[j]);
504 }
505
506 // Frag/Death ratio
507 float KillRatio = 0.0f;
508 if(pStats->m_Deaths != 0)
509 KillRatio = (float)(pStats->m_Frags) / pStats->m_Deaths;
510
511 // Local player
512 bool LocalPlayer = (GameClient()->m_Snap.m_LocalClientId == pInfo->m_ClientId || (GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == GameClient()->m_Snap.m_SpecInfo.m_SpectatorId));
513
514 // Game with flags
515 bool GameWithFlags = (GameClient()->m_Snap.m_pGameInfoObj && GameClient()->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS);
516
517 char aBuf[1024];
518 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d,%d,%s,%s,%d,%d,%d,%d,%.2f,%i,%.1f,%d,%d,%s,%d,%d,%d\n",
519 LocalPlayer ? 1 : 0, // Local player
520 GameClient()->m_aClients[pInfo->m_ClientId].m_Team, // Team
521 ReplaceCommata(pStr: GameClient()->m_aClients[pInfo->m_ClientId].m_aName).c_str(), // Name
522 ReplaceCommata(pStr: GameClient()->m_aClients[pInfo->m_ClientId].m_aClan).c_str(), // Clan
523 std::clamp(val: pInfo->m_Score, lo: -999, hi: 999), // Score
524 pStats->m_Frags, // Frags
525 pStats->m_Deaths, // Deaths
526 pStats->m_Suicides, // Suicides
527 KillRatio, // Kill ratio
528 pStats->m_Frags - pStats->m_Deaths, // Net
529 pStats->GetFPM(Tick: Client()->GameTick(Conn: g_Config.m_ClDummy), TickSpeed: Client()->GameTickSpeed()), // FPM
530 pStats->m_CurrentSpree, // CurSpree
531 pStats->m_BestSpree, // BestSpree
532 aWeaponFD, // WeaponFD
533 GameWithFlags ? 1 : 0, // GameWithFlags
534 pStats->m_FlagGrabs, // Flag grabs
535 pStats->m_FlagCaptures); // Flag captures
536
537 str_append(dst&: aPlayerStats, src: aBuf);
538 }
539
540 str_format(buffer: pDest, buffer_size: DestSize, format: "%s\n\n%s", aServerStats, aPlayerStats);
541}
542