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