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