1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, Sven2
6 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// network statistics and information dialogs
19
20#include <C4Include.h>
21#include <C4Network2Stats.h>
22
23#include <C4Game.h>
24#include <C4Player.h>
25
26#include <format>
27
28C4Graph::C4Graph()
29 : szTitle(LoadResStr(id: C4ResStrTableKey::IDS_NET_GRAPH), false), dwColor(0x7fff0000) {}
30
31C4TableGraph::C4TableGraph(int iBackLogLength, int iStartTime)
32 : iBackLogLength(iBackLogLength), pValues(nullptr), iBackLogPos(0), fWrapped(false)
33 , iTime(iStartTime), iInitialStartTime(iStartTime), pAveragedValues(nullptr), iAveragedTime(iStartTime), iAvgRange(1)
34 , fMultiplier(1)
35{
36 // create value buffer
37 assert(iBackLogLength);
38 pValues = pAveragedValues = new ValueType[iBackLogLength];
39 *pValues = 0;
40}
41
42C4TableGraph::~C4TableGraph()
43{
44 // flush stuff
45 Reset(iToTime: -1);
46 // free value buffer(s)
47 if (pValues != pAveragedValues) delete[] pAveragedValues;
48 delete[] pValues;
49}
50
51void C4TableGraph::Reset(TimeType iToTime)
52{
53 // flush buffer
54 if (!!szDumpFile) DumpToFile(rszFilename: szDumpFile, fAppend: fWrapped);
55 // reset stuff
56 iInitialStartTime = iTime = iToTime;
57 fWrapped = false;
58 iBackLogPos = 0;
59 *pValues = 0;
60}
61
62// retrieve timeframe covered by backlog
63C4Graph::TimeType C4TableGraph::GetStartTime() const
64{
65 // wrap? -> whole buffer used
66 if (fWrapped) return iTime - iBackLogLength;
67 // otherwise, just buffer to the start used
68 return iTime - iBackLogPos;
69}
70
71C4Graph::TimeType C4TableGraph::GetEndTime() const
72{
73 // end time is current
74 return iTime;
75}
76
77C4Graph::ValueType C4TableGraph::GetValue(TimeType iAtTime) const
78{
79 // must be inside buffer
80 assert(Inside(iAtTime, GetStartTime(), GetEndTime() - 1));
81 // query it - can't be negative if inside start/end-time
82 return pAveragedValues[(iAtTime - iInitialStartTime) % iBackLogLength] * fMultiplier;
83}
84
85C4Graph::ValueType C4TableGraph::GetAtValue(TimeType iAtTime) const
86{
87 // must be inside buffer
88 assert(Inside(iAtTime, GetStartTime(), GetEndTime() - 1));
89 // query it - can't be negative if inside start/end-time
90 return pValues[(iAtTime - iInitialStartTime) % iBackLogLength];
91}
92
93void C4TableGraph::SetAvgValue(TimeType iAtTime, ValueType iValue) const
94{
95 // must be inside buffer
96 assert(Inside(iAtTime, GetStartTime(), GetEndTime() - 1));
97 // set it - can't be negative if inside start/end-time
98 pAveragedValues[(iAtTime - iInitialStartTime) % iBackLogLength] = iValue;
99}
100
101C4Graph::ValueType C4TableGraph::GetMedianValue(TimeType iStartTime, TimeType iEndTime) const
102{
103 assert(iStartTime < iEndTime);
104 // safety: Never build median if no values are recorded
105 if (!iBackLogPos && !fWrapped) return 0;
106 // sum up and divide in the end - let's hope this will never be called for really large values that could overflow ValueType
107 ValueType iSum = GetValue(iAtTime: iStartTime), iNum = 1;
108 for (; ++iStartTime < iEndTime; ++iNum) iSum += GetValue(iAtTime: iStartTime);
109 return iSum / iNum;
110}
111
112C4Graph::ValueType C4TableGraph::GetMinValue() const
113{
114 int iPos0 = iBackLogPos ? iBackLogPos - 1 : iBackLogPos;
115 ValueType iMinVal = pAveragedValues[iPos0];
116 int i = iPos0; ValueType *p = pAveragedValues;
117 while (i--) iMinVal = (std::min)(a: iMinVal, b: *p++);
118 if (fWrapped)
119 {
120 i = iBackLogLength - iPos0;
121 while (--i) iMinVal = (std::min)(a: iMinVal, b: *++p);
122 }
123 return iMinVal * fMultiplier;
124}
125
126C4Graph::ValueType C4TableGraph::GetMaxValue() const
127{
128 int iPos0 = iBackLogPos ? iBackLogPos - 1 : iBackLogPos;
129 ValueType iMaxVal = pAveragedValues[iPos0];
130 int i = iPos0; ValueType *p = pAveragedValues;
131 while (i--) iMaxVal = (std::max)(a: iMaxVal, b: *p++);
132 if (fWrapped)
133 {
134 i = iBackLogLength - iPos0;
135 while (--i) iMaxVal = (std::max)(a: iMaxVal, b: *++p);
136 }
137 return iMaxVal * fMultiplier;
138}
139
140void C4TableGraph::RecordValue(ValueType iValue)
141{
142 // rec value
143 pValues[iBackLogPos] = iValue;
144 // calc time
145 ++iTime;
146 if (++iBackLogPos >= iBackLogLength)
147 {
148 // create dump before overwriting last buffer
149 if (!!szDumpFile) DumpToFile(rszFilename: szDumpFile, fAppend: fWrapped);
150 // restart buffer
151 fWrapped = true;
152 iBackLogPos = 0;
153 }
154}
155
156bool C4TableGraph::DumpToFile(const StdStrBuf &rszFilename, bool fAppend) const
157{
158 assert(!!rszFilename);
159 // nothing to write?
160 if (!fWrapped && !iBackLogPos) return false;
161 // try append if desired; create if unsuccessful
162 CStdFile out;
163 if (fAppend) if (!out.Append(szFilename: rszFilename.getData())) fAppend = false;
164 if (!fAppend)
165 {
166 if (!out.Create(szFileName: rszFilename.getData())) return false;
167 // print header
168 out.WriteString(szStr: "t\tv\r\n");
169 }
170 // write out current timeframe
171 int iEndTime = GetEndTime();
172 for (int iWriteTime = GetStartTime(); iWriteTime < iEndTime; ++iWriteTime)
173 {
174 out.WriteString(szStr: std::format(fmt: "{}\t{}\r\n", args&: iWriteTime, args: GetValue(iAtTime: iWriteTime)).c_str());
175 }
176 return true;
177}
178
179void C4TableGraph::SetAverageTime(int iToTime)
180{
181 // set new time; resetting valid, averaged range
182 if (iAveragedTime == iToTime) return;
183 assert(iToTime > 0);
184 iAvgRange = iToTime;
185 iAveragedTime = iInitialStartTime;
186}
187
188#define FORWARD_AVERAGE
189#define FORWARD_AVERAGE_FACTOR 4
190
191void C4TableGraph::Update() const
192{
193 // no averaging necessary?
194 if (pAveragedValues == pValues)
195 {
196 if (iAvgRange == 1) return;
197 // averaging necessary, but buffer not yet created: Create it!
198 pAveragedValues = new ValueType[iBackLogLength];
199 }
200 // up-to-date?
201 if (iAveragedTime == iTime) return;
202 assert(iAveragedTime < iTime); // must not have gone back!
203 // update it
204 int iStartTime = GetStartTime();
205#ifdef FORWARD_AVERAGE
206 int iAvgFwRange = iAvgRange / FORWARD_AVERAGE_FACTOR;
207#else
208 int iAvgFwRange = 0;
209#endif
210 for (int iUpdateTime = (std::max)(a: iAveragedTime - iAvgFwRange - 1, b: iStartTime); iUpdateTime < iTime; ++iUpdateTime)
211 {
212 ValueType iSum = 0, iSumWeight = 0, iWeight;
213 for (int iSumTime = (std::max)(a: iUpdateTime - iAvgRange, b: iStartTime); iSumTime < (std::min)(a: iUpdateTime + iAvgFwRange + 1, b: iTime); ++iSumTime)
214 {
215 iWeight = static_cast<ValueType>(iAvgRange) - Abs(val: iUpdateTime - iSumTime) + 1;
216 iSum += GetAtValue(iAtTime: iSumTime) * iWeight;
217 iSumWeight += iWeight;
218 }
219 SetAvgValue(iAtTime: iUpdateTime, iValue: iSum / iSumWeight);
220 }
221 // now it's all up-to-date
222 iAveragedTime = iTime;
223}
224
225C4Graph::TimeType C4GraphCollection::GetStartTime() const
226{
227 const_iterator i = begin(); if (i == end()) return 0;
228 C4Graph::TimeType iTime = (*i)->GetStartTime();
229 while (++i != end()) iTime = (std::min)(a: iTime, b: (*i)->GetStartTime());
230 return iTime;
231}
232
233C4Graph::TimeType C4GraphCollection::GetEndTime() const
234{
235 const_iterator i = begin(); if (i == end()) return 0;
236 C4Graph::TimeType iTime = (*i)->GetEndTime();
237 while (++i != end()) iTime = (std::max)(a: iTime, b: (*i)->GetEndTime());
238 return iTime;
239}
240
241C4Graph::ValueType C4GraphCollection::GetMinValue() const
242{
243 const_iterator i = begin(); if (i == end()) return 0;
244 C4Graph::ValueType iVal = (*i)->GetMinValue();
245 while (++i != end()) iVal = (std::min)(a: iVal, b: (*i)->GetMinValue());
246 return iVal;
247}
248
249C4Graph::ValueType C4GraphCollection::GetMaxValue() const
250{
251 const_iterator i = begin(); if (i == end()) return 0;
252 C4Graph::ValueType iVal = (*i)->GetMaxValue();
253 while (++i != end()) iVal = (std::max)(a: iVal, b: (*i)->GetMaxValue());
254 return iVal;
255}
256
257int C4GraphCollection::GetSeriesCount() const
258{
259 int iCount = 0;
260 for (const_iterator i = begin(); i != end(); ++i) iCount += (*i)->GetSeriesCount();
261 return iCount;
262}
263
264const C4Graph *C4GraphCollection::GetSeries(int iIndex) const
265{
266 for (const_iterator i = begin(); i != end(); ++i)
267 {
268 int iCnt = (*i)->GetSeriesCount();
269 if (iIndex < iCnt) return (*i)->GetSeries(iIndex);
270 iIndex -= iCnt;
271 }
272 return nullptr;
273}
274
275void C4GraphCollection::Update() const
276{
277 // update all child graphs
278 for (const_iterator i = begin(); i != end(); ++i)(*i)->Update();
279}
280
281void C4GraphCollection::SetAverageTime(int iToTime)
282{
283 if (iCommonAvgTime = iToTime)
284 for (iterator i = begin(); i != end(); ++i)(*i)->SetAverageTime(iToTime);
285}
286
287void C4GraphCollection::SetMultiplier(ValueType fToVal)
288{
289 if (fMultiplier = fToVal)
290 for (iterator i = begin(); i != end(); ++i)(*i)->SetMultiplier(fToVal);
291}
292
293C4Network2Stats::C4Network2Stats() : pSec1Timer(nullptr)
294{
295 // set self (needed in CreateGraph-fns)
296 Game.pNetworkStatistics = this;
297 // init callback timer
298 pSec1Timer = new C4Sec1TimerCallback<C4Network2Stats>(this);
299 SecondCounter = 0;
300 ControlCounter = 0;
301 // init graphs
302 statObjCount.SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_MSG_OBJCOUNT));
303 statFPS.SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_MSG_FPS));
304 statNetI.SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_NET_INPUT));
305 statNetI.SetColorDw(0x00ff00);
306 statNetO.SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_NET_OUTPUT));
307 statNetO.SetColorDw(0xff0000);
308 graphNetIO.AddGraph(pAdd: &statNetI); graphNetIO.AddGraph(pAdd: &statNetO);
309 statControls.SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_NET_CONTROL));
310 statControls.SetAverageTime(100);
311 statActions.SetTitle(LoadResStr(id: C4ResStrTableKey::IDS_NET_APM));
312 statActions.SetAverageTime(100);
313 for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next) pPlr->CreateGraphs();
314 C4Network2Client *pClient = nullptr;
315 while (pClient = Game.Network.Clients.GetNextClient(pClient)) pClient->CreateGraphs();
316}
317
318C4Network2Stats::~C4Network2Stats()
319{
320 for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next) pPlr->ClearGraphs();
321 C4Network2Client *pClient = nullptr;
322 while (pClient = Game.Network.Clients.GetNextClient(pClient)) pClient->ClearGraphs();
323 pSec1Timer->Release();
324}
325
326void C4Network2Stats::ExecuteFrame()
327{
328 statObjCount.RecordValue(iValue: C4Graph::ValueType(Game.Objects.ObjectCount()));
329}
330
331void C4Network2Stats::ExecuteSecond()
332{
333 statFPS.RecordValue(iValue: C4Graph::ValueType(Game.FPS));
334 statNetI.RecordValue(iValue: C4Graph::ValueType(Game.Network.NetIO.getProtIRate(eProt: P_TCP) + Game.Network.NetIO.getProtIRate(eProt: P_UDP)));
335 statNetO.RecordValue(iValue: C4Graph::ValueType(Game.Network.NetIO.getProtORate(eProt: P_TCP) + Game.Network.NetIO.getProtORate(eProt: P_UDP)));
336 // pings for all clients
337 C4Network2Client *pClient = nullptr;
338 while (pClient = Game.Network.Clients.GetNextClient(pClient)) if (pClient->getStatPing())
339 {
340 int iPing = 0;
341 C4Network2IOConnection *pConn = pClient->getMsgConn();
342 if (pConn) iPing = pConn->getLag();
343 pClient->getStatPing()->RecordValue(iValue: C4Graph::ValueType(iPing));
344 }
345 ++SecondCounter;
346}
347
348void C4Network2Stats::ExecuteControlFrame()
349{
350 // control rate may have updated: always convert values to actions per minute
351 statControls.SetMultiplier(static_cast<C4Graph::ValueType>(1000) / 38 / Game.Control.ControlRate);
352 statActions.SetMultiplier(static_cast<C4Graph::ValueType>(1000) / 38 * 60 / Game.Control.ControlRate);
353 // register and reset control counts for all players
354 for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next)
355 {
356 if (pPlr->pstatControls)
357 {
358 pPlr->pstatControls->RecordValue(iValue: C4Graph::ValueType(pPlr->ControlCount));
359 pPlr->ControlCount = 0;
360 }
361 if (pPlr->pstatActions)
362 {
363 pPlr->pstatActions->RecordValue(iValue: C4Graph::ValueType(pPlr->ActionCount));
364 pPlr->ActionCount = 0;
365 }
366 }
367 ++ControlCounter;
368}
369
370C4Graph *C4Network2Stats::GetGraphByName(const StdStrBuf &rszName, bool &rfIsTemp)
371{
372 // compare against default graph names
373 rfIsTemp = false;
374 if (SEqualNoCase(szStr1: rszName.getData(), szStr2: "oc")) return &statObjCount;
375 if (SEqualNoCase(szStr1: rszName.getData(), szStr2: "fps")) return &statFPS;
376 if (SEqualNoCase(szStr1: rszName.getData(), szStr2: "netio")) return &graphNetIO;
377 if (SEqualNoCase(szStr1: rszName.getData(), szStr2: "pings")) return &statPings;
378 if (SEqualNoCase(szStr1: rszName.getData(), szStr2: "control")) return &statControls;
379 if (SEqualNoCase(szStr1: rszName.getData(), szStr2: "apm")) return &statActions;
380 // no match
381 return nullptr;
382}
383