1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "client.h"
5
6#include "demoedit.h"
7#include "friends.h"
8#include "serverbrowser.h"
9
10#include <base/hash.h>
11#include <base/hash_ctxt.h>
12#include <base/log.h>
13#include <base/logger.h>
14#include <base/math.h>
15#include <base/str.h>
16#include <base/system.h>
17#include <base/windows.h>
18
19#include <engine/config.h>
20#include <engine/console.h>
21#include <engine/discord.h>
22#include <engine/editor.h>
23#include <engine/engine.h>
24#include <engine/external/json-parser/json.h>
25#include <engine/favorites.h>
26#include <engine/graphics.h>
27#include <engine/input.h>
28#include <engine/keys.h>
29#include <engine/map.h>
30#include <engine/notifications.h>
31#include <engine/serverbrowser.h>
32#include <engine/shared/assertion_logger.h>
33#include <engine/shared/compression.h>
34#include <engine/shared/config.h>
35#include <engine/shared/demo.h>
36#include <engine/shared/fifo.h>
37#include <engine/shared/filecollection.h>
38#include <engine/shared/http.h>
39#include <engine/shared/masterserver.h>
40#include <engine/shared/network.h>
41#include <engine/shared/packer.h>
42#include <engine/shared/protocol.h>
43#include <engine/shared/protocol7.h>
44#include <engine/shared/protocol_ex.h>
45#include <engine/shared/protocolglue.h>
46#include <engine/shared/rust_version.h>
47#include <engine/shared/snapshot.h>
48#include <engine/shared/uuid_manager.h>
49#include <engine/sound.h>
50#include <engine/steam.h>
51#include <engine/storage.h>
52#include <engine/textrender.h>
53
54#include <generated/protocol.h>
55#include <generated/protocol7.h>
56#include <generated/protocolglue.h>
57
58#include <game/localization.h>
59#include <game/version.h>
60
61#if defined(CONF_VIDEORECORDER)
62#include "video.h"
63#endif
64
65#if defined(CONF_PLATFORM_ANDROID)
66#include <android/android_main.h>
67#elif defined(CONF_PLATFORM_EMSCRIPTEN)
68#include <emscripten/emscripten.h>
69#endif
70
71#include "SDL.h"
72#ifdef main
73#undef main
74#endif
75
76#include <chrono>
77#include <limits>
78#include <stack>
79#include <thread>
80#include <tuple>
81
82using namespace std::chrono_literals;
83
84static constexpr ColorRGBA gs_ClientNetworkPrintColor{0.7f, 1, 0.7f, 1.0f};
85static constexpr ColorRGBA gs_ClientNetworkErrPrintColor{1.0f, 0.25f, 0.25f, 1.0f};
86
87CClient::CClient() :
88 m_DemoPlayer(&m_SnapshotDelta, true, [&]() { UpdateDemoIntraTimers(); }),
89 m_InputtimeMarginGraph(128, 2, true),
90 m_aGametimeMarginGraphs{{128, 2, true}, {128, 2, true}},
91 m_FpsGraph(4096, 0, true)
92{
93 m_StateStartTime = time_get();
94 for(auto &DemoRecorder : m_aDemoRecorder)
95 DemoRecorder = CDemoRecorder(&m_SnapshotDelta);
96 m_LastRenderTime = time_get();
97 mem_zero(block: m_aInputs, size: sizeof(m_aInputs));
98 mem_zero(block: m_aapSnapshots, size: sizeof(m_aapSnapshots));
99 for(auto &SnapshotStorage : m_aSnapshotStorage)
100 SnapshotStorage.Init();
101 mem_zero(block: m_aDemorecSnapshotHolders, size: sizeof(m_aDemorecSnapshotHolders));
102 mem_zero(block: &m_CurrentServerInfo, size: sizeof(m_CurrentServerInfo));
103 mem_zero(block: &m_Checksum, size: sizeof(m_Checksum));
104 for(auto &GameTime : m_aGameTime)
105 GameTime.Init(Target: 0);
106 m_PredictedTime.Init(Target: 0);
107
108 m_Sixup = false;
109}
110
111// ----- send functions -----
112static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer, bool Sixup)
113{
114 int MsgId = pMsg->m_MsgId;
115 Packer.Reset();
116
117 if(Sixup && !pMsg->m_NoTranslate)
118 {
119 if(pMsg->m_System)
120 {
121 if(MsgId >= OFFSET_UUID)
122 ;
123 else if(MsgId == NETMSG_INFO || MsgId == NETMSG_REQUEST_MAP_DATA)
124 ;
125 else if(MsgId == NETMSG_READY)
126 MsgId = protocol7::NETMSG_READY;
127 else if(MsgId == NETMSG_RCON_CMD)
128 MsgId = protocol7::NETMSG_RCON_CMD;
129 else if(MsgId == NETMSG_ENTERGAME)
130 MsgId = protocol7::NETMSG_ENTERGAME;
131 else if(MsgId == NETMSG_INPUT)
132 MsgId = protocol7::NETMSG_INPUT;
133 else if(MsgId == NETMSG_RCON_AUTH)
134 MsgId = protocol7::NETMSG_RCON_AUTH;
135 else if(MsgId == NETMSG_PING)
136 MsgId = protocol7::NETMSG_PING;
137 else
138 {
139 log_error("net", "0.7 DROP send sys %d", MsgId);
140 return false;
141 }
142 }
143 else
144 {
145 if(MsgId >= 0 && MsgId < OFFSET_UUID)
146 MsgId = Msg_SixToSeven(a: MsgId);
147
148 if(MsgId < 0)
149 return false;
150 }
151 }
152
153 if(pMsg->m_MsgId < OFFSET_UUID)
154 {
155 Packer.AddInt(i: (MsgId << 1) | (pMsg->m_System ? 1 : 0));
156 }
157 else
158 {
159 Packer.AddInt(i: pMsg->m_System ? 1 : 0); // NETMSG_EX, NETMSGTYPE_EX
160 g_UuidManager.PackUuid(Id: pMsg->m_MsgId, pPacker: &Packer);
161 }
162 Packer.AddRaw(pData: pMsg->Data(), Size: pMsg->Size());
163
164 return true;
165}
166
167int CClient::SendMsg(int Conn, CMsgPacker *pMsg, int Flags)
168{
169 CNetChunk Packet;
170
171 if(State() == IClient::STATE_OFFLINE)
172 return 0;
173
174 // repack message (inefficient)
175 CPacker Pack;
176 if(!RepackMsg(pMsg, Packer&: Pack, Sixup: IsSixup()))
177 return 0;
178
179 mem_zero(block: &Packet, size: sizeof(CNetChunk));
180 Packet.m_ClientId = 0;
181 Packet.m_pData = Pack.Data();
182 Packet.m_DataSize = Pack.Size();
183
184 if(Flags & MSGFLAG_VITAL)
185 Packet.m_Flags |= NETSENDFLAG_VITAL;
186 if(Flags & MSGFLAG_FLUSH)
187 Packet.m_Flags |= NETSENDFLAG_FLUSH;
188
189 if((Flags & MSGFLAG_RECORD) && Conn == g_Config.m_ClDummy)
190 {
191 for(auto &i : m_aDemoRecorder)
192 if(i.IsRecording())
193 i.RecordMessage(pData: Packet.m_pData, Size: Packet.m_DataSize);
194 }
195
196 if(!(Flags & MSGFLAG_NOSEND))
197 {
198 m_aNetClient[Conn].Send(pChunk: &Packet);
199 }
200
201 return 0;
202}
203
204int CClient::SendMsgActive(CMsgPacker *pMsg, int Flags)
205{
206 return SendMsg(Conn: g_Config.m_ClDummy, pMsg, Flags);
207}
208
209void CClient::SendInfo(int Conn)
210{
211 CMsgPacker MsgVer(NETMSG_CLIENTVER, true);
212 MsgVer.AddRaw(pData: &m_ConnectionId, Size: sizeof(m_ConnectionId));
213 MsgVer.AddInt(i: GameClient()->DDNetVersion());
214 MsgVer.AddString(pStr: GameClient()->DDNetVersionStr());
215 SendMsg(Conn, pMsg: &MsgVer, Flags: MSGFLAG_VITAL);
216
217 if(IsSixup())
218 {
219 CMsgPacker Msg(NETMSG_INFO, true);
220 Msg.AddString(GAME_NETVERSION7, Limit: 128);
221 Msg.AddString(pStr: Config()->m_Password);
222 Msg.AddInt(i: GameClient()->ClientVersion7());
223 SendMsg(Conn, pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
224 return;
225 }
226
227 CMsgPacker Msg(NETMSG_INFO, true);
228 Msg.AddString(pStr: GameClient()->NetVersion());
229 Msg.AddString(pStr: m_aPassword);
230 SendMsg(Conn, pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
231}
232
233void CClient::SendEnterGame(int Conn)
234{
235 CMsgPacker Msg(NETMSG_ENTERGAME, true);
236 SendMsg(Conn, pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
237}
238
239void CClient::SendReady(int Conn)
240{
241 CMsgPacker Msg(NETMSG_READY, true);
242 SendMsg(Conn, pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
243}
244
245void CClient::SendMapRequest()
246{
247 dbg_assert(!m_MapdownloadFileTemp, "Map download already in progress");
248 m_MapdownloadFileTemp = Storage()->OpenFile(pFilename: m_aMapdownloadFilenameTemp, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
249 if(IsSixup())
250 {
251 CMsgPacker MsgP(protocol7::NETMSG_REQUEST_MAP_DATA, true, true);
252 SendMsg(Conn: CONN_MAIN, pMsg: &MsgP, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
253 }
254 else
255 {
256 CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true);
257 Msg.AddInt(i: m_MapdownloadChunk);
258 SendMsg(Conn: CONN_MAIN, pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
259 }
260}
261
262void CClient::RconAuth(const char *pName, const char *pPassword, bool Dummy)
263{
264 if(m_aRconAuthed[Dummy] != 0)
265 return;
266
267 if(pName != m_aRconUsername)
268 str_copy(dst&: m_aRconUsername, src: pName);
269 if(pPassword != m_aRconPassword)
270 str_copy(dst&: m_aRconPassword, src: pPassword);
271
272 if(IsSixup())
273 {
274 CMsgPacker Msg7(protocol7::NETMSG_RCON_AUTH, true, true);
275 Msg7.AddString(pStr: pPassword);
276 SendMsg(Conn: Dummy, pMsg: &Msg7, Flags: MSGFLAG_VITAL);
277 return;
278 }
279
280 CMsgPacker Msg(NETMSG_RCON_AUTH, true);
281 Msg.AddString(pStr: pName);
282 Msg.AddString(pStr: pPassword);
283 Msg.AddInt(i: 1);
284 SendMsg(Conn: Dummy, pMsg: &Msg, Flags: MSGFLAG_VITAL);
285}
286
287void CClient::Rcon(const char *pCmd)
288{
289 CMsgPacker Msg(NETMSG_RCON_CMD, true);
290 Msg.AddString(pStr: pCmd);
291 SendMsgActive(pMsg: &Msg, Flags: MSGFLAG_VITAL);
292}
293
294float CClient::GotRconCommandsPercentage() const
295{
296 if(m_ExpectedRconCommands <= 0)
297 return -1.0f;
298 if(m_GotRconCommands > m_ExpectedRconCommands)
299 return -1.0f;
300
301 return (float)m_GotRconCommands / (float)m_ExpectedRconCommands;
302}
303
304float CClient::GotMaplistPercentage() const
305{
306 if(m_ExpectedMaplistEntries <= 0)
307 return -1.0f;
308 if(m_vMaplistEntries.size() > (size_t)m_ExpectedMaplistEntries)
309 return -1.0f;
310
311 return (float)m_vMaplistEntries.size() / (float)m_ExpectedMaplistEntries;
312}
313
314bool CClient::ConnectionProblems() const
315{
316 return m_aNetClient[g_Config.m_ClDummy].GotProblems(MaxLatency: MaxLatencyTicks() * time_freq() / GameTickSpeed());
317}
318
319void CClient::SendInput()
320{
321 int64_t Now = time_get();
322
323 if(m_aPredTick[g_Config.m_ClDummy] <= 0)
324 return;
325
326 bool Force = false;
327 // fetch input
328 for(int Dummy = 0; Dummy < NUM_DUMMIES; Dummy++)
329 {
330 if(!DummyConnected() && Dummy != 0)
331 {
332 break;
333 }
334 int i = g_Config.m_ClDummy ^ Dummy;
335 int Size = GameClient()->OnSnapInput(pData: m_aInputs[i][m_aCurrentInput[i]].m_aData, Dummy, Force);
336
337 if(Size)
338 {
339 // pack input
340 CMsgPacker Msg(NETMSG_INPUT, true);
341 Msg.AddInt(i: m_aAckGameTick[i]);
342 Msg.AddInt(i: m_aPredTick[g_Config.m_ClDummy]);
343 Msg.AddInt(i: Size);
344
345 m_aInputs[i][m_aCurrentInput[i]].m_Tick = m_aPredTick[g_Config.m_ClDummy];
346 m_aInputs[i][m_aCurrentInput[i]].m_PredictedTime = m_PredictedTime.Get(Now);
347 m_aInputs[i][m_aCurrentInput[i]].m_PredictionMargin = PredictionMargin() * time_freq() / 1000;
348 m_aInputs[i][m_aCurrentInput[i]].m_Time = Now;
349
350 // pack it
351 for(int k = 0; k < Size / 4; k++)
352 {
353 static const int FlagsOffset = offsetof(CNetObj_PlayerInput, m_PlayerFlags) / sizeof(int);
354 if(k == FlagsOffset && IsSixup())
355 {
356 int PlayerFlags = m_aInputs[i][m_aCurrentInput[i]].m_aData[k];
357 Msg.AddInt(i: PlayerFlags_SixToSeven(Flags: PlayerFlags));
358 }
359 else
360 {
361 Msg.AddInt(i: m_aInputs[i][m_aCurrentInput[i]].m_aData[k]);
362 }
363 }
364
365 m_aCurrentInput[i]++;
366 m_aCurrentInput[i] %= 200;
367
368 SendMsg(Conn: i, pMsg: &Msg, Flags: MSGFLAG_FLUSH);
369 // ugly workaround for dummy. we need to send input with dummy to prevent
370 // prediction time resets. but if we do it too often, then it's
371 // impossible to use grenade with frozen dummy that gets hammered...
372 if(g_Config.m_ClDummyCopyMoves || m_aCurrentInput[i] % 2)
373 Force = true;
374 }
375 }
376}
377
378const char *CClient::LatestVersion() const
379{
380 return m_aVersionStr;
381}
382
383// TODO: OPT: do this a lot smarter!
384int *CClient::GetInput(int Tick, int IsDummy) const
385{
386 int Best = -1;
387 const int d = IsDummy ^ g_Config.m_ClDummy;
388 for(int i = 0; i < 200; i++)
389 {
390 if(m_aInputs[d][i].m_Tick != -1 && m_aInputs[d][i].m_Tick <= Tick && (Best == -1 || m_aInputs[d][Best].m_Tick < m_aInputs[d][i].m_Tick))
391 Best = i;
392 }
393
394 if(Best != -1)
395 return (int *)m_aInputs[d][Best].m_aData;
396 return nullptr;
397}
398
399// ------ state handling -----
400void CClient::SetState(EClientState State)
401{
402 if(m_State == IClient::STATE_QUITTING || m_State == IClient::STATE_RESTARTING)
403 return;
404 if(m_State == State)
405 return;
406
407 if(g_Config.m_Debug)
408 {
409 char aBuf[64];
410 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "state change. last=%d current=%d", m_State, State);
411 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client", pStr: aBuf);
412 }
413
414 const EClientState OldState = m_State;
415 m_State = State;
416
417 m_StateStartTime = time_get();
418 GameClient()->OnStateChange(NewState: m_State, OldState);
419
420 if(State == IClient::STATE_OFFLINE && m_ReconnectTime == 0)
421 {
422 if(g_Config.m_ClReconnectFull > 0 && (str_find_nocase(haystack: ErrorString(), needle: "full") || str_find_nocase(haystack: ErrorString(), needle: "reserved")))
423 m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectFull;
424 else if(g_Config.m_ClReconnectTimeout > 0 && (str_find_nocase(haystack: ErrorString(), needle: "Timeout") || str_find_nocase(haystack: ErrorString(), needle: "Too weak connection")))
425 m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectTimeout;
426 }
427
428 if(State == IClient::STATE_ONLINE)
429 {
430 const bool Registered = m_ServerBrowser.IsRegistered(Addr: ServerAddress());
431 CServerInfo CurrentServerInfo;
432 GetServerInfo(pServerInfo: &CurrentServerInfo);
433
434 Discord()->SetGameInfo(ServerInfo: CurrentServerInfo, pMapName: m_aCurrentMap, Registered);
435 Steam()->SetGameInfo(ServerAddr: ServerAddress(), pMapName: m_aCurrentMap, AnnounceAddr: Registered);
436 }
437 else if(OldState == IClient::STATE_ONLINE)
438 {
439 Discord()->ClearGameInfo();
440 Steam()->ClearGameInfo();
441 }
442}
443
444// called when the map is loaded and we should init for a new round
445void CClient::OnEnterGame(bool Dummy)
446{
447 // reset input
448 for(int i = 0; i < 200; i++)
449 {
450 m_aInputs[Dummy][i].m_Tick = -1;
451 }
452 m_aCurrentInput[Dummy] = 0;
453
454 // reset snapshots
455 m_aapSnapshots[Dummy][SNAP_CURRENT] = nullptr;
456 m_aapSnapshots[Dummy][SNAP_PREV] = nullptr;
457 m_aSnapshotStorage[Dummy].PurgeAll();
458 m_aReceivedSnapshots[Dummy] = 0;
459 m_aSnapshotParts[Dummy] = 0;
460 m_aSnapshotIncomingDataSize[Dummy] = 0;
461 m_SnapCrcErrors = 0;
462 // Also make gameclient aware that snapshots have been purged
463 GameClient()->InvalidateSnapshot();
464
465 // reset times
466 m_aAckGameTick[Dummy] = -1;
467 m_aCurrentRecvTick[Dummy] = 0;
468 m_aPrevGameTick[Dummy] = 0;
469 m_aCurGameTick[Dummy] = 0;
470 m_aGameIntraTick[Dummy] = 0.0f;
471 m_aGameTickTime[Dummy] = 0.0f;
472 m_aGameIntraTickSincePrev[Dummy] = 0.0f;
473 m_aPredTick[Dummy] = 0;
474 m_aPredIntraTick[Dummy] = 0.0f;
475 m_aGameTime[Dummy].Init(Target: 0);
476 m_PredictedTime.Init(Target: 0);
477
478 if(!Dummy)
479 {
480 m_LastDummyConnectTime = 0.0f;
481 }
482
483 GameClient()->OnEnterGame();
484}
485
486void CClient::EnterGame(int Conn)
487{
488 if(State() == IClient::STATE_DEMOPLAYBACK)
489 return;
490
491 m_aDidPostConnect[Conn] = false;
492
493 // now we will wait for two snapshots
494 // to finish the connection
495 SendEnterGame(Conn);
496 OnEnterGame(Dummy: Conn);
497
498 ServerInfoRequest(); // fresh one for timeout protection
499 m_CurrentServerNextPingTime = time_get() + time_freq() / 2;
500}
501
502void CClient::OnPostConnect(int Conn)
503{
504 if(!m_ServerCapabilities.m_ChatTimeoutCode)
505 return;
506
507 char aBufMsg[256];
508 if(!g_Config.m_ClRunOnJoin[0] && !g_Config.m_ClDummyDefaultEyes && !g_Config.m_ClPlayerDefaultEyes)
509 str_format(buffer: aBufMsg, buffer_size: sizeof(aBufMsg), format: "/timeout %s", m_aTimeoutCodes[Conn]);
510 else
511 str_format(buffer: aBufMsg, buffer_size: sizeof(aBufMsg), format: "/mc;timeout %s", m_aTimeoutCodes[Conn]);
512
513 if(g_Config.m_ClDummyDefaultEyes || g_Config.m_ClPlayerDefaultEyes)
514 {
515 int Emote = Conn == CONN_DUMMY ? g_Config.m_ClDummyDefaultEyes : g_Config.m_ClPlayerDefaultEyes;
516
517 if(Emote != EMOTE_NORMAL)
518 {
519 char aBuf[32];
520 static const char *s_EMOTE_NAMES[] = {
521 "pain",
522 "happy",
523 "surprise",
524 "angry",
525 "blink",
526 };
527 static_assert(std::size(s_EMOTE_NAMES) == NUM_EMOTES - 1, "The size of EMOTE_NAMES must match NUM_EMOTES - 1");
528
529 str_append(dst&: aBufMsg, src: ";");
530 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "emote %s %d", s_EMOTE_NAMES[Emote - 1], g_Config.m_ClEyeDuration);
531 str_append(dst&: aBufMsg, src: aBuf);
532 }
533 }
534 if(g_Config.m_ClRunOnJoin[0])
535 {
536 str_append(dst&: aBufMsg, src: ";");
537 str_append(dst&: aBufMsg, src: g_Config.m_ClRunOnJoin);
538 }
539 if(IsSixup())
540 {
541 protocol7::CNetMsg_Cl_Say Msg7;
542 Msg7.m_Mode = protocol7::CHAT_ALL;
543 Msg7.m_Target = -1;
544 Msg7.m_pMessage = aBufMsg;
545 SendPackMsg(Conn, pMsg: &Msg7, Flags: MSGFLAG_VITAL, NoTranslate: true);
546 }
547 else
548 {
549 CNetMsg_Cl_Say MsgP;
550 MsgP.m_Team = 0;
551 MsgP.m_pMessage = aBufMsg;
552 CMsgPacker PackerTimeout(&MsgP);
553 MsgP.Pack(pPacker: &PackerTimeout);
554 SendMsg(Conn, pMsg: &PackerTimeout, Flags: MSGFLAG_VITAL);
555 }
556}
557
558static void GenerateTimeoutCode(char *pBuffer, unsigned Size, char *pSeed, const NETADDR *pAddrs, int NumAddrs, bool Dummy)
559{
560 MD5_CTX Md5;
561 md5_init(ctxt: &Md5);
562 const char *pDummy = Dummy ? "dummy" : "normal";
563 md5_update(ctxt: &Md5, data: (unsigned char *)pDummy, data_len: str_length(str: pDummy) + 1);
564 md5_update(ctxt: &Md5, data: (unsigned char *)pSeed, data_len: str_length(str: pSeed) + 1);
565 for(int i = 0; i < NumAddrs; i++)
566 {
567 md5_update(ctxt: &Md5, data: (unsigned char *)&pAddrs[i], data_len: sizeof(pAddrs[i]));
568 }
569 MD5_DIGEST Digest = md5_finish(ctxt: &Md5);
570
571 unsigned short aRandom[8];
572 mem_copy(dest: aRandom, source: Digest.data, size: sizeof(aRandom));
573 generate_password(buffer: pBuffer, length: Size, random: aRandom, random_length: 8);
574}
575
576void CClient::GenerateTimeoutSeed()
577{
578 secure_random_password(buffer: g_Config.m_ClTimeoutSeed, length: sizeof(g_Config.m_ClTimeoutSeed), pw_length: 16);
579}
580
581void CClient::GenerateTimeoutCodes(const NETADDR *pAddrs, int NumAddrs)
582{
583 if(g_Config.m_ClTimeoutSeed[0])
584 {
585 for(int i = 0; i < 2; i++)
586 {
587 GenerateTimeoutCode(pBuffer: m_aTimeoutCodes[i], Size: sizeof(m_aTimeoutCodes[i]), pSeed: g_Config.m_ClTimeoutSeed, pAddrs, NumAddrs, Dummy: i);
588
589 char aBuf[64];
590 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "timeout code '%s' (%s)", m_aTimeoutCodes[i], i == 0 ? "normal" : "dummy");
591 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
592 }
593 }
594 else
595 {
596 str_copy(dst&: m_aTimeoutCodes[0], src: g_Config.m_ClTimeoutCode);
597 str_copy(dst&: m_aTimeoutCodes[1], src: g_Config.m_ClDummyTimeoutCode);
598 }
599}
600
601void CClient::Connect(const char *pAddress, const char *pPassword)
602{
603 // Disconnect will not change the state if we are already quitting/restarting
604 if(m_State == IClient::STATE_QUITTING || m_State == IClient::STATE_RESTARTING)
605 return;
606 Disconnect();
607 dbg_assert(m_State == IClient::STATE_OFFLINE, "Disconnect must ensure that client is offline");
608
609 const NETADDR LastAddr = ServerAddress();
610
611 if(pAddress != m_aConnectAddressStr)
612 str_copy(dst&: m_aConnectAddressStr, src: pAddress);
613
614 char aMsg[512];
615 str_format(buffer: aMsg, buffer_size: sizeof(aMsg), format: "connecting to '%s'", m_aConnectAddressStr);
616 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aMsg, PrintColor: gs_ClientNetworkPrintColor);
617
618 int NumConnectAddrs = 0;
619 NETADDR aConnectAddrs[MAX_SERVER_ADDRESSES];
620 mem_zero(block: aConnectAddrs, size: sizeof(aConnectAddrs));
621 const char *pNextAddr = pAddress;
622 char aBuffer[128];
623 bool OnlySixup = true;
624 while((pNextAddr = str_next_token(str: pNextAddr, delim: ",", buffer: aBuffer, buffer_size: sizeof(aBuffer))))
625 {
626 NETADDR NextAddr;
627 char aHost[128];
628 const int UrlParseResult = net_addr_from_url(addr: &NextAddr, string: aBuffer, host_buf: aHost, host_buf_size: sizeof(aHost));
629 bool Sixup = NextAddr.type & NETTYPE_TW7;
630 if(UrlParseResult > 0)
631 str_copy(dst&: aHost, src: aBuffer);
632
633 if(net_host_lookup(hostname: aHost, addr: &NextAddr, types: m_aNetClient[CONN_MAIN].NetType()) != 0)
634 {
635 log_error("client", "could not find address of %s", aHost);
636 continue;
637 }
638 if(NumConnectAddrs == (int)std::size(aConnectAddrs))
639 {
640 log_warn("client", "too many connect addresses, ignoring %s", aHost);
641 continue;
642 }
643 if(NextAddr.port == 0)
644 {
645 NextAddr.port = 8303;
646 }
647 if(Sixup)
648 NextAddr.type |= NETTYPE_TW7;
649 else
650 OnlySixup = false;
651
652 char aNextAddr[NETADDR_MAXSTRSIZE];
653 net_addr_str(addr: &NextAddr, string: aNextAddr, max_length: sizeof(aNextAddr), add_port: true);
654 log_debug("client", "resolved connect address '%s' to %s", aBuffer, aNextAddr);
655
656 if(NextAddr == LastAddr)
657 {
658 m_SendPassword = true;
659 }
660
661 aConnectAddrs[NumConnectAddrs] = NextAddr;
662 NumConnectAddrs += 1;
663 }
664
665 if(NumConnectAddrs == 0)
666 {
667 log_error("client", "could not find any connect address");
668 char aWarning[256];
669 str_format(buffer: aWarning, buffer_size: sizeof(aWarning), format: Localize(pStr: "Could not resolve connect address '%s'. See local console for details."), m_aConnectAddressStr);
670 SWarning Warning(Localize(pStr: "Connect address error"), aWarning);
671 Warning.m_AutoHide = false;
672 AddWarning(Warning);
673 return;
674 }
675
676 m_ConnectionId = RandomUuid();
677 ServerInfoRequest();
678
679 if(m_SendPassword)
680 {
681 str_copy(dst&: m_aPassword, src: g_Config.m_Password);
682 m_SendPassword = false;
683 }
684 else if(!pPassword)
685 m_aPassword[0] = 0;
686 else
687 str_copy(dst&: m_aPassword, src: pPassword);
688
689 m_CanReceiveServerCapabilities = true;
690
691 m_Sixup = OnlySixup;
692 if(m_Sixup)
693 {
694 m_aNetClient[CONN_MAIN].Connect7(pAddr: aConnectAddrs, NumAddrs: NumConnectAddrs);
695 }
696 else
697 m_aNetClient[CONN_MAIN].Connect(pAddr: aConnectAddrs, NumAddrs: NumConnectAddrs);
698
699 m_aNetClient[CONN_MAIN].RefreshStun();
700 SetState(IClient::STATE_CONNECTING);
701
702 m_InputtimeMarginGraph.Init(Min: -150.0f, Max: 150.0f);
703 m_aGametimeMarginGraphs[CONN_MAIN].Init(Min: -150.0f, Max: 150.0f);
704
705 GenerateTimeoutCodes(pAddrs: aConnectAddrs, NumAddrs: NumConnectAddrs);
706}
707
708void CClient::DisconnectWithReason(const char *pReason)
709{
710 if(pReason != nullptr && pReason[0] == '\0')
711 pReason = nullptr;
712
713 DummyDisconnect(pReason);
714
715 char aBuf[512];
716 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "disconnecting. reason='%s'", pReason ? pReason : "unknown");
717 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf, PrintColor: gs_ClientNetworkPrintColor);
718
719 // stop demo playback and recorder
720 // make sure to remove replay tmp demo
721 m_DemoPlayer.Stop();
722 for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++)
723 {
724 DemoRecorder(Recorder)->Stop(Mode: Recorder == RECORDER_REPLAYS ? IDemoRecorder::EStopMode::REMOVE_FILE : IDemoRecorder::EStopMode::KEEP_FILE);
725 }
726
727 m_aRconAuthed[0] = 0;
728 // Make sure to clear credentials completely from memory
729 mem_zero(block: m_aRconUsername, size: sizeof(m_aRconUsername));
730 mem_zero(block: m_aRconPassword, size: sizeof(m_aRconPassword));
731 m_MapDetailsPresent = false;
732 m_ServerSentCapabilities = false;
733 m_UseTempRconCommands = 0;
734 m_ExpectedRconCommands = -1;
735 m_GotRconCommands = 0;
736 m_pConsole->DeregisterTempAll();
737 m_ExpectedMaplistEntries = -1;
738 m_vMaplistEntries.clear();
739 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
740 m_aNetClient[CONN_MAIN].Disconnect(pReason);
741 SetState(IClient::STATE_OFFLINE);
742 m_pMap->Unload();
743 m_CurrentServerPingInfoType = -1;
744 m_CurrentServerPingBasicToken = -1;
745 m_CurrentServerPingToken = -1;
746 mem_zero(block: &m_CurrentServerPingUuid, size: sizeof(m_CurrentServerPingUuid));
747 m_CurrentServerCurrentPingTime = -1;
748 m_CurrentServerNextPingTime = -1;
749
750 ResetMapDownload(ResetActive: true);
751
752 // clear the current server info
753 mem_zero(block: &m_CurrentServerInfo, size: sizeof(m_CurrentServerInfo));
754
755 // clear snapshots
756 m_aapSnapshots[0][SNAP_CURRENT] = nullptr;
757 m_aapSnapshots[0][SNAP_PREV] = nullptr;
758 m_aReceivedSnapshots[0] = 0;
759 m_LastDummy = false;
760
761 // 0.7
762 m_TranslationContext.Reset();
763 m_Sixup = false;
764}
765
766void CClient::Disconnect()
767{
768 if(m_State != IClient::STATE_OFFLINE)
769 {
770 DisconnectWithReason(pReason: nullptr);
771 }
772}
773
774bool CClient::DummyConnected() const
775{
776 return m_DummyConnected;
777}
778
779bool CClient::DummyConnecting() const
780{
781 return m_DummyConnecting;
782}
783
784bool CClient::DummyConnectingDelayed() const
785{
786 return !DummyConnected() && !DummyConnecting() && m_LastDummyConnectTime > 0.0f && m_LastDummyConnectTime + 5.0f > GlobalTime();
787}
788
789void CClient::DummyConnect()
790{
791 if(m_aNetClient[CONN_MAIN].State() != NETSTATE_ONLINE)
792 {
793 log_info("client", "Not online.");
794 return;
795 }
796
797 if(!DummyAllowed())
798 {
799 log_info("client", "Dummy is not allowed on this server.");
800 return;
801 }
802 if(DummyConnecting())
803 {
804 log_info("client", "Dummy is already connecting.");
805 return;
806 }
807 if(DummyConnected())
808 {
809 // causes log spam with connect+swap binds
810 // https://github.com/ddnet/ddnet/issues/9426
811 // log_info("client", "Dummy is already connected.");
812 return;
813 }
814 if(DummyConnectingDelayed())
815 {
816 log_info("client", "Wait before connecting dummy again.");
817 return;
818 }
819
820 m_LastDummyConnectTime = GlobalTime();
821 m_aRconAuthed[1] = 0;
822 m_DummySendConnInfo = true;
823
824 g_Config.m_ClDummyCopyMoves = 0;
825 g_Config.m_ClDummyHammer = 0;
826
827 m_DummyConnecting = true;
828 // connect to the server
829 if(IsSixup())
830 m_aNetClient[CONN_DUMMY].Connect7(pAddr: m_aNetClient[CONN_MAIN].ServerAddress(), NumAddrs: 1);
831 else
832 m_aNetClient[CONN_DUMMY].Connect(pAddr: m_aNetClient[CONN_MAIN].ServerAddress(), NumAddrs: 1);
833
834 m_aGametimeMarginGraphs[CONN_DUMMY].Init(Min: -150.0f, Max: 150.0f);
835}
836
837void CClient::DummyDisconnect(const char *pReason)
838{
839 m_aNetClient[CONN_DUMMY].Disconnect(pReason);
840 g_Config.m_ClDummy = 0;
841
842 m_aRconAuthed[1] = 0;
843 m_aapSnapshots[1][SNAP_CURRENT] = nullptr;
844 m_aapSnapshots[1][SNAP_PREV] = nullptr;
845 m_aReceivedSnapshots[1] = 0;
846 m_DummyConnected = false;
847 m_DummyConnecting = false;
848 m_DummyReconnectOnReload = false;
849 m_DummyDeactivateOnReconnect = false;
850 GameClient()->OnDummyDisconnect();
851}
852
853bool CClient::DummyAllowed() const
854{
855 return m_ServerCapabilities.m_AllowDummy;
856}
857
858void CClient::GetServerInfo(CServerInfo *pServerInfo) const
859{
860 mem_copy(dest: pServerInfo, source: &m_CurrentServerInfo, size: sizeof(m_CurrentServerInfo));
861}
862
863void CClient::ServerInfoRequest()
864{
865 mem_zero(block: &m_CurrentServerInfo, size: sizeof(m_CurrentServerInfo));
866 m_CurrentServerInfoRequestTime = 0;
867}
868
869void CClient::LoadDebugFont()
870{
871 m_DebugFont = Graphics()->LoadTexture(pFilename: "debug_font.png", StorageType: IStorage::TYPE_ALL);
872}
873
874// ---
875
876IClient::CSnapItem CClient::SnapGetItem(int SnapId, int Index) const
877{
878 dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
879 const CSnapshot *pSnapshot = m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap;
880 const CSnapshotItem *pSnapshotItem = pSnapshot->GetItem(Index);
881 CSnapItem Item;
882 Item.m_Type = pSnapshot->GetItemType(Index);
883 Item.m_Id = pSnapshotItem->Id();
884 Item.m_pData = pSnapshotItem->Data();
885 Item.m_DataSize = pSnapshot->GetItemSize(Index);
886 return Item;
887}
888
889const void *CClient::SnapFindItem(int SnapId, int Type, int Id) const
890{
891 if(!m_aapSnapshots[g_Config.m_ClDummy][SnapId])
892 return nullptr;
893
894 return m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap->FindItem(Type, Id);
895}
896
897int CClient::SnapNumItems(int SnapId) const
898{
899 dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
900 if(!m_aapSnapshots[g_Config.m_ClDummy][SnapId])
901 return 0;
902 return m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap->NumItems();
903}
904
905void CClient::SnapSetStaticsize(int ItemType, int Size)
906{
907 m_SnapshotDelta.SetStaticsize(ItemType, Size);
908}
909
910void CClient::SnapSetStaticsize7(int ItemType, int Size)
911{
912 m_SnapshotDelta.SetStaticsize7(ItemType, Size);
913}
914
915void CClient::RenderDebug()
916{
917 if(!g_Config.m_Debug)
918 {
919 return;
920 }
921
922 const std::chrono::nanoseconds Now = time_get_nanoseconds();
923 if(Now - m_NetstatsLastUpdate > 1s)
924 {
925 m_NetstatsLastUpdate = Now;
926 m_NetstatsPrev = m_NetstatsCurrent;
927 net_stats(stats: &m_NetstatsCurrent);
928 }
929
930 char aBuffer[512];
931 const float FontSize = 16.0f;
932
933 Graphics()->TextureSet(Texture: m_DebugFont);
934 Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: Graphics()->ScreenWidth(), BottomRightY: Graphics()->ScreenHeight());
935 Graphics()->QuadsBegin();
936
937 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "Game/predicted tick: %d/%d", m_aCurGameTick[g_Config.m_ClDummy], m_aPredTick[g_Config.m_ClDummy]);
938 Graphics()->QuadsText(x: 2, y: 2, Size: FontSize, pText: aBuffer);
939
940 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "Prediction time: %d ms", GetPredictionTime());
941 Graphics()->QuadsText(x: 2, y: 2 + FontSize, Size: FontSize, pText: aBuffer);
942
943 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "FPS: %3d", round_to_int(f: 1.0f / m_FrameTimeAverage));
944 Graphics()->QuadsText(x: 20.0f * FontSize, y: 2, Size: FontSize, pText: aBuffer);
945
946 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "Frametime: %4d us", round_to_int(f: m_FrameTimeAverage * 1000000.0f));
947 Graphics()->QuadsText(x: 20.0f * FontSize, y: 2 + FontSize, Size: FontSize, pText: aBuffer);
948
949 str_format(aBuffer, sizeof(aBuffer), "%16s: %" PRIu64 " KiB", "Texture memory", Graphics()->TextureMemoryUsage() / 1024);
950 Graphics()->QuadsText(x: 32.0f * FontSize, y: 2, Size: FontSize, pText: aBuffer);
951
952 str_format(aBuffer, sizeof(aBuffer), "%16s: %" PRIu64 " KiB", "Buffer memory", Graphics()->BufferMemoryUsage() / 1024);
953 Graphics()->QuadsText(x: 32.0f * FontSize, y: 2 + FontSize, Size: FontSize, pText: aBuffer);
954
955 str_format(aBuffer, sizeof(aBuffer), "%16s: %" PRIu64 " KiB", "Streamed memory", Graphics()->StreamedMemoryUsage() / 1024);
956 Graphics()->QuadsText(x: 32.0f * FontSize, y: 2 + 2 * FontSize, Size: FontSize, pText: aBuffer);
957
958 str_format(aBuffer, sizeof(aBuffer), "%16s: %" PRIu64 " KiB", "Staging memory", Graphics()->StagingMemoryUsage() / 1024);
959 Graphics()->QuadsText(x: 32.0f * FontSize, y: 2 + 3 * FontSize, Size: FontSize, pText: aBuffer);
960
961 // Network
962 {
963 const uint64_t OverheadSize = 14 + 20 + 8; // ETH + IP + UDP
964 const uint64_t SendPackets = m_NetstatsCurrent.sent_packets - m_NetstatsPrev.sent_packets;
965 const uint64_t SendBytes = m_NetstatsCurrent.sent_bytes - m_NetstatsPrev.sent_bytes;
966 const uint64_t SendTotal = SendBytes + SendPackets * OverheadSize;
967 const uint64_t RecvPackets = m_NetstatsCurrent.recv_packets - m_NetstatsPrev.recv_packets;
968 const uint64_t RecvBytes = m_NetstatsCurrent.recv_bytes - m_NetstatsPrev.recv_bytes;
969 const uint64_t RecvTotal = RecvBytes + RecvPackets * OverheadSize;
970
971 str_format(aBuffer, sizeof(aBuffer), "Send: %3" PRIu64 " %5" PRIu64 "+%4" PRIu64 "=%5" PRIu64 " (%3" PRIu64 " Kibit/s) average: %5" PRIu64,
972 SendPackets, SendBytes, SendPackets * OverheadSize, SendTotal, (SendTotal * 8) / 1024, SendPackets == 0 ? 0 : SendBytes / SendPackets);
973 Graphics()->QuadsText(x: 2, y: 2 + 3 * FontSize, Size: FontSize, pText: aBuffer);
974 str_format(aBuffer, sizeof(aBuffer), "Recv: %3" PRIu64 " %5" PRIu64 "+%4" PRIu64 "=%5" PRIu64 " (%3" PRIu64 " Kibit/s) average: %5" PRIu64,
975 RecvPackets, RecvBytes, RecvPackets * OverheadSize, RecvTotal, (RecvTotal * 8) / 1024, RecvPackets == 0 ? 0 : RecvBytes / RecvPackets);
976 Graphics()->QuadsText(x: 2, y: 2 + 4 * FontSize, Size: FontSize, pText: aBuffer);
977 }
978
979 // Snapshots
980 {
981 const float OffsetY = 2 + 6 * FontSize;
982 int Row = 0;
983 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%5s %20s: %8s %8s %8s", "ID", "Name", "Rate", "Updates", "R/U");
984 Graphics()->QuadsText(x: 2, y: OffsetY + Row * 12, Size: FontSize, pText: aBuffer);
985 Row++;
986 for(int i = 0; i < NUM_NETOBJTYPES; i++)
987 {
988 if(m_SnapshotDelta.GetDataRate(Index: i))
989 {
990 str_format(
991 aBuffer,
992 sizeof(aBuffer),
993 "%5d %20s: %8" PRIu64 " %8" PRIu64 " %8" PRIu64,
994 i,
995 GameClient()->GetItemName(i),
996 m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i),
997 (m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8);
998 Graphics()->QuadsText(x: 2, y: OffsetY + Row * 12, Size: FontSize, pText: aBuffer);
999 Row++;
1000 }
1001 }
1002 for(int i = CSnapshot::MAX_TYPE; i > (CSnapshot::MAX_TYPE - 64); i--)
1003 {
1004 if(m_SnapshotDelta.GetDataRate(Index: i) && m_aapSnapshots[g_Config.m_ClDummy][IClient::SNAP_CURRENT])
1005 {
1006 const int Type = m_aapSnapshots[g_Config.m_ClDummy][IClient::SNAP_CURRENT]->m_pAltSnap->GetExternalItemType(InternalType: i);
1007 if(Type == UUID_INVALID)
1008 {
1009 str_format(
1010 aBuffer,
1011 sizeof(aBuffer),
1012 "%5d %20s: %8" PRIu64 " %8" PRIu64 " %8" PRIu64,
1013 i,
1014 "Unknown UUID",
1015 m_SnapshotDelta.GetDataRate(i) / 8,
1016 m_SnapshotDelta.GetDataUpdates(i),
1017 (m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8);
1018 Graphics()->QuadsText(x: 2, y: OffsetY + Row * 12, Size: FontSize, pText: aBuffer);
1019 Row++;
1020 }
1021 else if(Type != i)
1022 {
1023 str_format(
1024 aBuffer,
1025 sizeof(aBuffer),
1026 "%5d %20s: %8" PRIu64 " %8" PRIu64 " %8" PRIu64,
1027 Type,
1028 GameClient()->GetItemName(Type),
1029 m_SnapshotDelta.GetDataRate(i) / 8,
1030 m_SnapshotDelta.GetDataUpdates(i),
1031 (m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8);
1032 Graphics()->QuadsText(x: 2, y: OffsetY + Row * 12, Size: FontSize, pText: aBuffer);
1033 Row++;
1034 }
1035 }
1036 }
1037 }
1038
1039 Graphics()->QuadsEnd();
1040}
1041
1042void CClient::RenderGraphs()
1043{
1044 if(!g_Config.m_DbgGraphs)
1045 return;
1046
1047 // Make sure graph positions and sizes are aligned with pixels to avoid lines overlapping graph edges
1048 Graphics()->MapScreen(TopLeftX: 0, TopLeftY: 0, BottomRightX: Graphics()->ScreenWidth(), BottomRightY: Graphics()->ScreenHeight());
1049 const float GraphW = std::round(x: Graphics()->ScreenWidth() / 4.0f);
1050 const float GraphH = std::round(x: Graphics()->ScreenHeight() / 6.0f);
1051 const float GraphSpacing = std::round(x: Graphics()->ScreenWidth() / 100.0f);
1052 const float GraphX = Graphics()->ScreenWidth() - GraphW - GraphSpacing;
1053
1054 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1055 TextRender()->Text(x: GraphX, y: GraphSpacing * 5 - 12.0f - 10.0f, Size: 12.0f, pText: Localize(pStr: "Press Ctrl+Shift+G to disable debug graphs."));
1056
1057 m_FpsGraph.Scale(WantedTotalTime: time_freq());
1058 m_FpsGraph.Render(pGraphics: Graphics(), pTextRender: TextRender(), x: GraphX, y: GraphSpacing * 5, w: GraphW, h: GraphH, pDescription: "FPS");
1059 m_InputtimeMarginGraph.Scale(WantedTotalTime: 5 * time_freq());
1060 m_InputtimeMarginGraph.Render(pGraphics: Graphics(), pTextRender: TextRender(), x: GraphX, y: GraphSpacing * 6 + GraphH, w: GraphW, h: GraphH, pDescription: "Prediction Margin");
1061 m_aGametimeMarginGraphs[g_Config.m_ClDummy].Scale(WantedTotalTime: 5 * time_freq());
1062 m_aGametimeMarginGraphs[g_Config.m_ClDummy].Render(pGraphics: Graphics(), pTextRender: TextRender(), x: GraphX, y: GraphSpacing * 7 + GraphH * 2, w: GraphW, h: GraphH, pDescription: "Gametime Margin");
1063}
1064
1065void CClient::Restart()
1066{
1067 SetState(IClient::STATE_RESTARTING);
1068}
1069
1070void CClient::Quit()
1071{
1072 SetState(IClient::STATE_QUITTING);
1073}
1074
1075void CClient::ResetSocket()
1076{
1077 NETADDR BindAddr;
1078 if(g_Config.m_Bindaddr[0] == '\0')
1079 {
1080 mem_zero(block: &BindAddr, size: sizeof(BindAddr));
1081 }
1082 else if(net_host_lookup(hostname: g_Config.m_Bindaddr, addr: &BindAddr, types: NETTYPE_ALL) != 0)
1083 {
1084 log_error("client", "The configured bindaddr '%s' cannot be resolved.", g_Config.m_Bindaddr);
1085 return;
1086 }
1087 BindAddr.type = NETTYPE_ALL;
1088 for(size_t Conn = 0; Conn < std::size(m_aNetClient); Conn++)
1089 {
1090 char aError[256];
1091 if(!InitNetworkClientImpl(BindAddr, Conn, pError: aError, ErrorSize: sizeof(aError)))
1092 log_error("client", "%s", aError);
1093 }
1094}
1095const char *CClient::PlayerName() const
1096{
1097 if(g_Config.m_PlayerName[0])
1098 {
1099 return g_Config.m_PlayerName;
1100 }
1101 if(g_Config.m_SteamName[0])
1102 {
1103 return g_Config.m_SteamName;
1104 }
1105 return "nameless tee";
1106}
1107
1108const char *CClient::DummyName()
1109{
1110 if(g_Config.m_ClDummyName[0])
1111 {
1112 return g_Config.m_ClDummyName;
1113 }
1114 const char *pBase = nullptr;
1115 if(g_Config.m_PlayerName[0])
1116 {
1117 pBase = g_Config.m_PlayerName;
1118 }
1119 else if(g_Config.m_SteamName[0])
1120 {
1121 pBase = g_Config.m_SteamName;
1122 }
1123 if(pBase)
1124 {
1125 str_format(buffer: m_aAutomaticDummyName, buffer_size: sizeof(m_aAutomaticDummyName), format: "[D] %s", pBase);
1126 return m_aAutomaticDummyName;
1127 }
1128 return "brainless tee";
1129}
1130
1131const char *CClient::ErrorString() const
1132{
1133 return m_aNetClient[CONN_MAIN].ErrorString();
1134}
1135
1136void CClient::Render()
1137{
1138 if(m_EditorActive)
1139 {
1140 m_pEditor->OnRender();
1141 }
1142 else
1143 {
1144 GameClient()->OnRender();
1145 }
1146
1147 RenderDebug();
1148 RenderGraphs();
1149}
1150
1151const char *CClient::LoadMap(const char *pName, const char *pFilename, const std::optional<SHA256_DIGEST> &WantedSha256, unsigned WantedCrc)
1152{
1153 static char s_aErrorMsg[128];
1154
1155 SetState(IClient::STATE_LOADING);
1156 SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_MAP);
1157 if((bool)m_LoadingCallback)
1158 m_LoadingCallback(IClient::LOADING_CALLBACK_DETAIL_MAP);
1159
1160 if(!m_pMap->Load(pMapName: pFilename, StorageType: IStorage::TYPE_ALL))
1161 {
1162 str_format(buffer: s_aErrorMsg, buffer_size: sizeof(s_aErrorMsg), format: "map '%s' not found", pFilename);
1163 return s_aErrorMsg;
1164 }
1165
1166 if(WantedSha256.has_value() && m_pMap->Sha256() != WantedSha256.value())
1167 {
1168 char aWanted[SHA256_MAXSTRSIZE];
1169 char aGot[SHA256_MAXSTRSIZE];
1170 sha256_str(digest: WantedSha256.value(), str: aWanted, max_len: sizeof(aWanted));
1171 sha256_str(digest: m_pMap->Sha256(), str: aGot, max_len: sizeof(aWanted));
1172 str_format(buffer: s_aErrorMsg, buffer_size: sizeof(s_aErrorMsg), format: "map differs from the server. %s != %s", aGot, aWanted);
1173 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: s_aErrorMsg);
1174 m_pMap->Unload();
1175 return s_aErrorMsg;
1176 }
1177
1178 // Only check CRC if we don't have the secure SHA256.
1179 if(!WantedSha256.has_value() && m_pMap->Crc() != WantedCrc)
1180 {
1181 str_format(buffer: s_aErrorMsg, buffer_size: sizeof(s_aErrorMsg), format: "map differs from the server. %08x != %08x", m_pMap->Crc(), WantedCrc);
1182 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: s_aErrorMsg);
1183 m_pMap->Unload();
1184 return s_aErrorMsg;
1185 }
1186
1187 // stop demo recording if we loaded a new map
1188 for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++)
1189 {
1190 DemoRecorder(Recorder)->Stop(Mode: Recorder == RECORDER_REPLAYS ? IDemoRecorder::EStopMode::REMOVE_FILE : IDemoRecorder::EStopMode::KEEP_FILE);
1191 }
1192
1193 char aBuf[256];
1194 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "loaded map '%s'", pFilename);
1195 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
1196
1197 str_copy(dst&: m_aCurrentMap, src: pName);
1198 str_copy(dst&: m_aCurrentMapPath, src: pFilename);
1199
1200 return nullptr;
1201}
1202
1203static void FormatMapDownloadFilename(const char *pName, const std::optional<SHA256_DIGEST> &Sha256, int Crc, bool Temp, char *pBuffer, int BufferSize)
1204{
1205 char aSuffix[32];
1206 if(Temp)
1207 {
1208 IStorage::FormatTmpPath(aBuf: aSuffix, BufSize: sizeof(aSuffix), pPath: "");
1209 }
1210 else
1211 {
1212 str_copy(dst&: aSuffix, src: ".map");
1213 }
1214
1215 if(Sha256.has_value())
1216 {
1217 char aSha256[SHA256_MAXSTRSIZE];
1218 sha256_str(digest: Sha256.value(), str: aSha256, max_len: sizeof(aSha256));
1219 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "downloadedmaps/%s_%s%s", pName, aSha256, aSuffix);
1220 }
1221 else
1222 {
1223 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "downloadedmaps/%s_%08x%s", pName, Crc, aSuffix);
1224 }
1225}
1226
1227const char *CClient::LoadMapSearch(const char *pMapName, const std::optional<SHA256_DIGEST> &WantedSha256, int WantedCrc)
1228{
1229 char aBuf[512];
1230 char aWanted[SHA256_MAXSTRSIZE + 16];
1231 aWanted[0] = 0;
1232 if(WantedSha256.has_value())
1233 {
1234 char aWantedSha256[SHA256_MAXSTRSIZE];
1235 sha256_str(digest: WantedSha256.value(), str: aWantedSha256, max_len: sizeof(aWantedSha256));
1236 str_format(buffer: aWanted, buffer_size: sizeof(aWanted), format: "sha256=%s ", aWantedSha256);
1237 }
1238 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "loading map, map=%s wanted %scrc=%08x", pMapName, aWanted, WantedCrc);
1239 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
1240
1241 // try the normal maps folder
1242 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "maps/%s.map", pMapName);
1243 const char *pError = LoadMap(pName: pMapName, pFilename: aBuf, WantedSha256, WantedCrc);
1244 if(!pError)
1245 return nullptr;
1246
1247 // try the downloaded maps
1248 FormatMapDownloadFilename(pName: pMapName, Sha256: WantedSha256, Crc: WantedCrc, Temp: false, pBuffer: aBuf, BufferSize: sizeof(aBuf));
1249 pError = LoadMap(pName: pMapName, pFilename: aBuf, WantedSha256, WantedCrc);
1250 if(!pError)
1251 return nullptr;
1252
1253 // backward compatibility with old names
1254 if(WantedSha256.has_value())
1255 {
1256 FormatMapDownloadFilename(pName: pMapName, Sha256: std::nullopt, Crc: WantedCrc, Temp: false, pBuffer: aBuf, BufferSize: sizeof(aBuf));
1257 pError = LoadMap(pName: pMapName, pFilename: aBuf, WantedSha256, WantedCrc);
1258 if(!pError)
1259 return nullptr;
1260 }
1261
1262 // search for the map within subfolders
1263 char aFilename[IO_MAX_PATH_LENGTH];
1264 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "%s.map", pMapName);
1265 if(Storage()->FindFile(pFilename: aFilename, pPath: "maps", Type: IStorage::TYPE_ALL, pBuffer: aBuf, BufferSize: sizeof(aBuf)))
1266 {
1267 pError = LoadMap(pName: pMapName, pFilename: aBuf, WantedSha256, WantedCrc);
1268 if(!pError)
1269 return nullptr;
1270 }
1271
1272 static char s_aErrorMsg[256];
1273 str_format(buffer: s_aErrorMsg, buffer_size: sizeof(s_aErrorMsg), format: "Could not find map '%s'", pMapName);
1274 return s_aErrorMsg;
1275}
1276
1277void CClient::ProcessConnlessPacket(CNetChunk *pPacket)
1278{
1279 // server info
1280 if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_INFO))
1281 {
1282 int Type = -1;
1283 if(mem_comp(a: pPacket->m_pData, b: SERVERBROWSE_INFO, size: sizeof(SERVERBROWSE_INFO)) == 0)
1284 Type = SERVERINFO_VANILLA;
1285 else if(mem_comp(a: pPacket->m_pData, b: SERVERBROWSE_INFO_EXTENDED, size: sizeof(SERVERBROWSE_INFO_EXTENDED)) == 0)
1286 Type = SERVERINFO_EXTENDED;
1287 else if(mem_comp(a: pPacket->m_pData, b: SERVERBROWSE_INFO_EXTENDED_MORE, size: sizeof(SERVERBROWSE_INFO_EXTENDED_MORE)) == 0)
1288 Type = SERVERINFO_EXTENDED_MORE;
1289
1290 if(Type != -1)
1291 {
1292 void *pData = (unsigned char *)pPacket->m_pData + sizeof(SERVERBROWSE_INFO);
1293 int DataSize = pPacket->m_DataSize - sizeof(SERVERBROWSE_INFO);
1294 ProcessServerInfo(Type, pFrom: &pPacket->m_Address, pData, DataSize);
1295 }
1296 }
1297}
1298
1299static int SavedServerInfoType(int Type)
1300{
1301 if(Type == SERVERINFO_EXTENDED_MORE)
1302 return SERVERINFO_EXTENDED;
1303
1304 return Type;
1305}
1306
1307void CClient::ProcessServerInfo(int RawType, NETADDR *pFrom, const void *pData, int DataSize)
1308{
1309 CServerBrowser::CServerEntry *pEntry = m_ServerBrowser.Find(Addr: *pFrom);
1310
1311 CServerInfo Info = {.m_ServerIndex: 0};
1312 int SavedType = SavedServerInfoType(Type: RawType);
1313 if(SavedType == SERVERINFO_EXTENDED && pEntry && pEntry->m_GotInfo && SavedType == pEntry->m_Info.m_Type)
1314 {
1315 Info = pEntry->m_Info;
1316 }
1317 else
1318 {
1319 Info.m_NumAddresses = 1;
1320 Info.m_aAddresses[0] = *pFrom;
1321 }
1322
1323 Info.m_Type = SavedType;
1324
1325 net_addr_str(addr: pFrom, string: Info.m_aAddress, max_length: sizeof(Info.m_aAddress), add_port: true);
1326
1327 CUnpacker Up;
1328 Up.Reset(pData, Size: DataSize);
1329
1330#define GET_STRING(array) str_copy(array, Up.GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES), sizeof(array))
1331#define GET_INT(integer) (integer) = str_toint(Up.GetString())
1332
1333 int Token;
1334 int PacketNo = 0; // Only used if SavedType == SERVERINFO_EXTENDED
1335
1336 GET_INT(Token);
1337 if(RawType != SERVERINFO_EXTENDED_MORE)
1338 {
1339 GET_STRING(Info.m_aVersion);
1340 GET_STRING(Info.m_aName);
1341 GET_STRING(Info.m_aMap);
1342
1343 if(SavedType == SERVERINFO_EXTENDED)
1344 {
1345 GET_INT(Info.m_MapCrc);
1346 GET_INT(Info.m_MapSize);
1347 }
1348
1349 GET_STRING(Info.m_aGameType);
1350 GET_INT(Info.m_Flags);
1351 GET_INT(Info.m_NumPlayers);
1352 GET_INT(Info.m_MaxPlayers);
1353 GET_INT(Info.m_NumClients);
1354 GET_INT(Info.m_MaxClients);
1355
1356 // don't add invalid info to the server browser list
1357 if(Info.m_NumClients < 0 || Info.m_MaxClients < 0 ||
1358 Info.m_NumPlayers < 0 || Info.m_MaxPlayers < 0 ||
1359 Info.m_NumPlayers > Info.m_NumClients || Info.m_MaxPlayers > Info.m_MaxClients)
1360 {
1361 return;
1362 }
1363
1364 m_ServerBrowser.UpdateServerCommunity(pInfo: &Info);
1365 m_ServerBrowser.UpdateServerRank(pInfo: &Info);
1366
1367 switch(SavedType)
1368 {
1369 case SERVERINFO_VANILLA:
1370 if(Info.m_MaxPlayers > VANILLA_MAX_CLIENTS ||
1371 Info.m_MaxClients > VANILLA_MAX_CLIENTS)
1372 {
1373 return;
1374 }
1375 break;
1376 case SERVERINFO_64_LEGACY:
1377 if(Info.m_MaxPlayers > MAX_CLIENTS ||
1378 Info.m_MaxClients > MAX_CLIENTS)
1379 {
1380 return;
1381 }
1382 break;
1383 case SERVERINFO_EXTENDED:
1384 if(Info.m_NumPlayers > Info.m_NumClients)
1385 return;
1386 break;
1387 default:
1388 dbg_assert_failed("unknown serverinfo type");
1389 }
1390
1391 if(SavedType == SERVERINFO_EXTENDED)
1392 PacketNo = 0;
1393 }
1394 else
1395 {
1396 GET_INT(PacketNo);
1397 // 0 needs to be excluded because that's reserved for the main packet.
1398 if(PacketNo <= 0 || PacketNo >= 64)
1399 return;
1400 }
1401
1402 bool DuplicatedPacket = false;
1403 if(SavedType == SERVERINFO_EXTENDED)
1404 {
1405 Up.GetString(); // extra info, reserved
1406
1407 uint64_t Flag = (uint64_t)1 << PacketNo;
1408 DuplicatedPacket = Info.m_ReceivedPackets & Flag;
1409 Info.m_ReceivedPackets |= Flag;
1410 }
1411
1412 bool IgnoreError = false;
1413 for(int i = 0; i < MAX_CLIENTS && Info.m_NumReceivedClients < MAX_CLIENTS && !Up.Error(); i++)
1414 {
1415 CServerInfo::CClient *pClient = &Info.m_aClients[Info.m_NumReceivedClients];
1416 GET_STRING(pClient->m_aName);
1417 if(Up.Error())
1418 {
1419 // Packet end, no problem unless it happens during one
1420 // player info, so ignore the error.
1421 IgnoreError = true;
1422 break;
1423 }
1424 GET_STRING(pClient->m_aClan);
1425 GET_INT(pClient->m_Country);
1426 GET_INT(pClient->m_Score);
1427 GET_INT(pClient->m_Player);
1428 if(SavedType == SERVERINFO_EXTENDED)
1429 {
1430 Up.GetString(); // extra info, reserved
1431 }
1432 if(!Up.Error())
1433 {
1434 if(SavedType == SERVERINFO_64_LEGACY)
1435 {
1436 uint64_t Flag = (uint64_t)1 << i;
1437 if(!(Info.m_ReceivedPackets & Flag))
1438 {
1439 Info.m_ReceivedPackets |= Flag;
1440 Info.m_NumReceivedClients++;
1441 }
1442 }
1443 else
1444 {
1445 Info.m_NumReceivedClients++;
1446 }
1447 }
1448 }
1449
1450 str_clean_whitespaces(str: Info.m_aName);
1451
1452 if(!Up.Error() || IgnoreError)
1453 {
1454 if(!DuplicatedPacket && (!pEntry || !pEntry->m_GotInfo || SavedType >= pEntry->m_Info.m_Type))
1455 {
1456 m_ServerBrowser.OnServerInfoUpdate(Addr: *pFrom, Token, pInfo: &Info);
1457 }
1458
1459 // Player info is irrelevant for the client (while connected),
1460 // it gets its info from elsewhere.
1461 //
1462 // SERVERINFO_EXTENDED_MORE doesn't carry any server
1463 // information, so just skip it.
1464 if(m_aNetClient[CONN_MAIN].State() == NETSTATE_ONLINE &&
1465 ServerAddress() == *pFrom &&
1466 RawType != SERVERINFO_EXTENDED_MORE)
1467 {
1468 // Only accept server info that has a type that is
1469 // newer or equal to something the server already sent
1470 // us.
1471 if(SavedType >= m_CurrentServerInfo.m_Type)
1472 {
1473 m_CurrentServerInfo = Info;
1474 m_CurrentServerInfoRequestTime = -1;
1475 Discord()->UpdateServerInfo(ServerInfo: Info, pMapName: m_aCurrentMap);
1476 }
1477
1478 bool ValidPong = false;
1479 if(!m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && SavedType >= m_CurrentServerPingInfoType)
1480 {
1481 if(RawType == SERVERINFO_VANILLA)
1482 {
1483 ValidPong = Token == m_CurrentServerPingBasicToken;
1484 }
1485 else if(RawType == SERVERINFO_EXTENDED)
1486 {
1487 ValidPong = Token == m_CurrentServerPingToken;
1488 }
1489 }
1490 if(ValidPong)
1491 {
1492 int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
1493 m_ServerBrowser.SetCurrentServerPing(Addr: ServerAddress(), Ping: LatencyMs);
1494 m_CurrentServerPingInfoType = SavedType;
1495 m_CurrentServerCurrentPingTime = -1;
1496
1497 char aBuf[64];
1498 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "got pong from current server, latency=%dms", LatencyMs);
1499 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf);
1500 }
1501 }
1502 }
1503
1504#undef GET_STRING
1505#undef GET_INT
1506}
1507
1508static CServerCapabilities GetServerCapabilities(int Version, int Flags, bool Sixup)
1509{
1510 CServerCapabilities Result;
1511 bool DDNet = false;
1512 if(Version >= 1)
1513 {
1514 DDNet = Flags & SERVERCAPFLAG_DDNET;
1515 }
1516 Result.m_ChatTimeoutCode = DDNet;
1517 Result.m_AnyPlayerFlag = !Sixup;
1518 Result.m_PingEx = false;
1519 Result.m_AllowDummy = true;
1520 Result.m_SyncWeaponInput = false;
1521 if(Version >= 1)
1522 {
1523 Result.m_ChatTimeoutCode = Flags & SERVERCAPFLAG_CHATTIMEOUTCODE;
1524 }
1525 if(Version >= 2)
1526 {
1527 Result.m_AnyPlayerFlag = Flags & SERVERCAPFLAG_ANYPLAYERFLAG;
1528 }
1529 if(Version >= 3)
1530 {
1531 Result.m_PingEx = Flags & SERVERCAPFLAG_PINGEX;
1532 }
1533 if(Version >= 4)
1534 {
1535 Result.m_AllowDummy = Flags & SERVERCAPFLAG_ALLOWDUMMY;
1536 }
1537 if(Version >= 5)
1538 {
1539 Result.m_SyncWeaponInput = Flags & SERVERCAPFLAG_SYNCWEAPONINPUT;
1540 }
1541 return Result;
1542}
1543
1544void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy)
1545{
1546 CUnpacker Unpacker;
1547 Unpacker.Reset(pData: pPacket->m_pData, Size: pPacket->m_DataSize);
1548 CMsgPacker Packer(NETMSG_EX, true);
1549
1550 // unpack msgid and system flag
1551 int Msg;
1552 bool Sys;
1553 CUuid Uuid;
1554
1555 int Result = UnpackMessageId(pId: &Msg, pSys: &Sys, pUuid: &Uuid, pUnpacker: &Unpacker, pPacker: &Packer);
1556 if(Result == UNPACKMESSAGE_ERROR)
1557 {
1558 return;
1559 }
1560 else if(Result == UNPACKMESSAGE_ANSWER)
1561 {
1562 SendMsg(Conn, pMsg: &Packer, Flags: MSGFLAG_VITAL);
1563 }
1564
1565 // allocates the memory for the translated data
1566 CPacker Packer6;
1567 if(IsSixup())
1568 {
1569 bool IsExMsg = false;
1570 int Success = !TranslateSysMsg(pMsgId: &Msg, System: Sys, pUnpacker: &Unpacker, pPacker: &Packer6, pPacket, pIsExMsg: &IsExMsg);
1571 if(Msg < 0)
1572 return;
1573 if(Success && !IsExMsg)
1574 {
1575 Unpacker.Reset(pData: Packer6.Data(), Size: Packer6.Size());
1576 }
1577 }
1578
1579 if(Sys)
1580 {
1581 // system message
1582 if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_DETAILS)
1583 {
1584 const char *pMap = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
1585 SHA256_DIGEST *pMapSha256 = (SHA256_DIGEST *)Unpacker.GetRaw(Size: sizeof(*pMapSha256));
1586 int MapCrc = Unpacker.GetInt();
1587 int MapSize = Unpacker.GetInt();
1588 if(Unpacker.Error())
1589 {
1590 return;
1591 }
1592
1593 const char *pMapUrl = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1594 if(Unpacker.Error())
1595 {
1596 pMapUrl = "";
1597 }
1598
1599 m_MapDetailsPresent = true;
1600 (void)MapSize;
1601 str_copy(dst&: m_aMapDetailsName, src: pMap);
1602 m_MapDetailsSha256 = *pMapSha256;
1603 m_MapDetailsCrc = MapCrc;
1604 str_copy(dst&: m_aMapDetailsUrl, src: pMapUrl);
1605 }
1606 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CAPABILITIES)
1607 {
1608 if(!m_CanReceiveServerCapabilities)
1609 {
1610 return;
1611 }
1612 int Version = Unpacker.GetInt();
1613 int Flags = Unpacker.GetInt();
1614 if(Unpacker.Error() || Version <= 0)
1615 {
1616 return;
1617 }
1618 m_ServerCapabilities = GetServerCapabilities(Version, Flags, Sixup: IsSixup());
1619 m_CanReceiveServerCapabilities = false;
1620 m_ServerSentCapabilities = true;
1621 }
1622 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_CHANGE)
1623 {
1624 if(m_CanReceiveServerCapabilities)
1625 {
1626 m_ServerCapabilities = GetServerCapabilities(Version: 0, Flags: 0, Sixup: IsSixup());
1627 m_CanReceiveServerCapabilities = false;
1628 }
1629 bool MapDetailsWerePresent = m_MapDetailsPresent;
1630 m_MapDetailsPresent = false;
1631
1632 const char *pMap = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
1633 int MapCrc = Unpacker.GetInt();
1634 int MapSize = Unpacker.GetInt();
1635 if(Unpacker.Error())
1636 {
1637 return;
1638 }
1639 if(MapSize < 0 || MapSize > 1024 * 1024 * 1024) // 1 GiB
1640 {
1641 DisconnectWithReason(pReason: "invalid map size");
1642 return;
1643 }
1644
1645 if(!str_valid_filename(str: pMap))
1646 {
1647 DisconnectWithReason(pReason: "map name is not a valid filename");
1648 return;
1649 }
1650
1651 if(m_DummyConnected && !m_DummyReconnectOnReload)
1652 {
1653 DummyDisconnect(pReason: nullptr);
1654 }
1655
1656 ResetMapDownload(ResetActive: true);
1657
1658 std::optional<SHA256_DIGEST> MapSha256;
1659 const char *pMapUrl = nullptr;
1660 if(MapDetailsWerePresent && str_comp(a: m_aMapDetailsName, b: pMap) == 0 && m_MapDetailsCrc == MapCrc)
1661 {
1662 MapSha256 = m_MapDetailsSha256;
1663 pMapUrl = m_aMapDetailsUrl[0] ? m_aMapDetailsUrl : nullptr;
1664 }
1665
1666 if(LoadMapSearch(pMapName: pMap, WantedSha256: MapSha256, WantedCrc: MapCrc) == nullptr)
1667 {
1668 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client/network", pStr: "loading done");
1669 SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_SENDING_READY);
1670 SendReady(Conn: CONN_MAIN);
1671 }
1672 else
1673 {
1674 // start map download
1675 FormatMapDownloadFilename(pName: pMap, Sha256: MapSha256, Crc: MapCrc, Temp: false, pBuffer: m_aMapdownloadFilename, BufferSize: sizeof(m_aMapdownloadFilename));
1676 FormatMapDownloadFilename(pName: pMap, Sha256: MapSha256, Crc: MapCrc, Temp: true, pBuffer: m_aMapdownloadFilenameTemp, BufferSize: sizeof(m_aMapdownloadFilenameTemp));
1677
1678 char aBuf[256];
1679 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "starting to download map to '%s'", m_aMapdownloadFilenameTemp);
1680 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client/network", pStr: aBuf);
1681
1682 str_copy(dst&: m_aMapdownloadName, src: pMap);
1683 m_MapdownloadSha256 = MapSha256;
1684 m_MapdownloadCrc = MapCrc;
1685 m_MapdownloadTotalsize = MapSize;
1686
1687 if(MapSha256.has_value())
1688 {
1689 char aUrl[256];
1690 char aEscaped[256];
1691 EscapeUrl(aBuf&: aEscaped, pStr: m_aMapdownloadFilename + 15); // cut off downloadedmaps/
1692 bool UseConfigUrl = str_comp(a: g_Config.m_ClMapDownloadUrl, b: "https://maps.ddnet.org") != 0 || m_aMapDownloadUrl[0] == '\0';
1693 str_format(buffer: aUrl, buffer_size: sizeof(aUrl), format: "%s/%s", UseConfigUrl ? g_Config.m_ClMapDownloadUrl : m_aMapDownloadUrl, aEscaped);
1694
1695 m_pMapdownloadTask = HttpGetFile(pUrl: pMapUrl ? pMapUrl : aUrl, pStorage: Storage(), pOutputFile: m_aMapdownloadFilenameTemp, StorageType: IStorage::TYPE_SAVE);
1696 m_pMapdownloadTask->Timeout(Timeout: CTimeout{.m_ConnectTimeoutMs: g_Config.m_ClMapDownloadConnectTimeoutMs, .m_TimeoutMs: 0, .m_LowSpeedLimit: g_Config.m_ClMapDownloadLowSpeedLimit, .m_LowSpeedTime: g_Config.m_ClMapDownloadLowSpeedTime});
1697 m_pMapdownloadTask->MaxResponseSize(MaxResponseSize: MapSize);
1698 m_pMapdownloadTask->ExpectSha256(Sha256: MapSha256.value());
1699 Http()->Run(pRequest: m_pMapdownloadTask);
1700 }
1701 else
1702 {
1703 SendMapRequest();
1704 }
1705 }
1706 }
1707 else if(Conn == CONN_MAIN && Msg == NETMSG_MAP_DATA)
1708 {
1709 if(!m_MapdownloadFileTemp)
1710 {
1711 return;
1712 }
1713 int Last = -1;
1714 int MapCRC = -1;
1715 int Chunk = -1;
1716 int Size = -1;
1717
1718 if(IsSixup())
1719 {
1720 MapCRC = m_MapdownloadCrc;
1721 Chunk = m_MapdownloadChunk;
1722 Size = minimum(a: m_TranslationContext.m_MapDownloadChunkSize, b: m_TranslationContext.m_MapdownloadTotalsize - m_MapdownloadAmount);
1723 }
1724 else
1725 {
1726 Last = Unpacker.GetInt();
1727 MapCRC = Unpacker.GetInt();
1728 Chunk = Unpacker.GetInt();
1729 Size = Unpacker.GetInt();
1730 }
1731
1732 const unsigned char *pData = Unpacker.GetRaw(Size);
1733 if(Unpacker.Error() || Size <= 0 || MapCRC != m_MapdownloadCrc || Chunk != m_MapdownloadChunk)
1734 {
1735 return;
1736 }
1737
1738 io_write(io: m_MapdownloadFileTemp, buffer: pData, size: Size);
1739
1740 m_MapdownloadAmount += Size;
1741
1742 if(IsSixup())
1743 Last = m_MapdownloadAmount == m_TranslationContext.m_MapdownloadTotalsize;
1744
1745 if(Last)
1746 {
1747 if(m_MapdownloadFileTemp)
1748 {
1749 io_close(io: m_MapdownloadFileTemp);
1750 m_MapdownloadFileTemp = nullptr;
1751 }
1752 FinishMapDownload();
1753 }
1754 else
1755 {
1756 // request new chunk
1757 m_MapdownloadChunk++;
1758
1759 if(IsSixup() && (m_MapdownloadChunk % m_TranslationContext.m_MapDownloadChunksPerRequest == 0))
1760 {
1761 CMsgPacker MsgP(protocol7::NETMSG_REQUEST_MAP_DATA, true, true);
1762 SendMsg(Conn: CONN_MAIN, pMsg: &MsgP, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
1763 }
1764 else
1765 {
1766 CMsgPacker MsgP(NETMSG_REQUEST_MAP_DATA, true);
1767 MsgP.AddInt(i: m_MapdownloadChunk);
1768 SendMsg(Conn: CONN_MAIN, pMsg: &MsgP, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
1769 }
1770
1771 if(g_Config.m_Debug)
1772 {
1773 char aBuf[256];
1774 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "requested chunk %d", m_MapdownloadChunk);
1775 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client/network", pStr: aBuf);
1776 }
1777 }
1778 }
1779 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_RELOAD)
1780 {
1781 if(m_DummyConnected)
1782 {
1783 m_DummyReconnectOnReload = true;
1784 m_DummyDeactivateOnReconnect = g_Config.m_ClDummy == 0;
1785 g_Config.m_ClDummy = 0;
1786 }
1787 else
1788 m_DummyDeactivateOnReconnect = false;
1789 }
1790 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CON_READY)
1791 {
1792 GameClient()->OnConnected();
1793 if(m_DummyReconnectOnReload)
1794 {
1795 m_DummySendConnInfo = true;
1796 m_DummyReconnectOnReload = false;
1797 }
1798 }
1799 else if(Conn == CONN_DUMMY && Msg == NETMSG_CON_READY)
1800 {
1801 m_DummyConnected = true;
1802 m_DummyConnecting = false;
1803 g_Config.m_ClDummy = 1;
1804 Rcon(pCmd: "crashmeplx");
1805 if(m_aRconAuthed[0] && !m_aRconAuthed[1])
1806 RconAuth(pName: m_aRconUsername, pPassword: m_aRconPassword);
1807 }
1808 else if(Msg == NETMSG_PING)
1809 {
1810 CMsgPacker MsgP(NETMSG_PING_REPLY, true);
1811 int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0;
1812 SendMsg(Conn, pMsg: &MsgP, Flags: MSGFLAG_FLUSH | Vital);
1813 }
1814 else if(Msg == NETMSG_PINGEX)
1815 {
1816 CUuid *pId = (CUuid *)Unpacker.GetRaw(Size: sizeof(*pId));
1817 if(Unpacker.Error())
1818 {
1819 return;
1820 }
1821 CMsgPacker MsgP(NETMSG_PONGEX, true);
1822 MsgP.AddRaw(pData: pId, Size: sizeof(*pId));
1823 int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0;
1824 SendMsg(Conn, pMsg: &MsgP, Flags: MSGFLAG_FLUSH | Vital);
1825 }
1826 else if(Conn == CONN_MAIN && Msg == NETMSG_PONGEX)
1827 {
1828 CUuid *pId = (CUuid *)Unpacker.GetRaw(Size: sizeof(*pId));
1829 if(Unpacker.Error())
1830 {
1831 return;
1832 }
1833 if(m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && *pId == m_CurrentServerPingUuid)
1834 {
1835 int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
1836 m_ServerBrowser.SetCurrentServerPing(Addr: ServerAddress(), Ping: LatencyMs);
1837 m_CurrentServerCurrentPingTime = -1;
1838
1839 char aBuf[64];
1840 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "got pong from current server, latency=%dms", LatencyMs);
1841 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf);
1842 }
1843 }
1844 else if(Msg == NETMSG_CHECKSUM_REQUEST)
1845 {
1846 CUuid *pUuid = (CUuid *)Unpacker.GetRaw(Size: sizeof(*pUuid));
1847 if(Unpacker.Error())
1848 {
1849 return;
1850 }
1851 int ResultCheck = HandleChecksum(Conn, Uuid: *pUuid, pUnpacker: &Unpacker);
1852 if(ResultCheck)
1853 {
1854 CMsgPacker MsgP(NETMSG_CHECKSUM_ERROR, true);
1855 MsgP.AddRaw(pData: pUuid, Size: sizeof(*pUuid));
1856 MsgP.AddInt(i: ResultCheck);
1857 SendMsg(Conn, pMsg: &MsgP, Flags: MSGFLAG_VITAL);
1858 }
1859 }
1860 else if(Msg == NETMSG_RECONNECT)
1861 {
1862 if(Conn == CONN_MAIN)
1863 {
1864 Connect(pAddress: m_aConnectAddressStr);
1865 }
1866 else
1867 {
1868 DummyDisconnect(pReason: "reconnect");
1869 // Reset dummy connect time to allow immediate reconnect
1870 m_LastDummyConnectTime = 0.0f;
1871 DummyConnect();
1872 }
1873 }
1874 else if(Msg == NETMSG_REDIRECT)
1875 {
1876 int RedirectPort = Unpacker.GetInt();
1877 if(Unpacker.Error())
1878 {
1879 return;
1880 }
1881 if(Conn == CONN_MAIN)
1882 {
1883 NETADDR ServerAddr = ServerAddress();
1884 ServerAddr.port = RedirectPort;
1885 char aAddr[NETADDR_MAXSTRSIZE];
1886 net_addr_str(addr: &ServerAddr, string: aAddr, max_length: sizeof(aAddr), add_port: true);
1887 Connect(pAddress: aAddr);
1888 }
1889 else
1890 {
1891 DummyDisconnect(pReason: "redirect");
1892 if(ServerAddress().port != RedirectPort)
1893 {
1894 // Only allow redirecting to the same port to reconnect. The dummy
1895 // should not be connected to a different server than the main, as
1896 // the client assumes that main and dummy use the same map.
1897 return;
1898 }
1899 // Reset dummy connect time to allow immediate reconnect
1900 m_LastDummyConnectTime = 0.0f;
1901 DummyConnect();
1902 }
1903 }
1904 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_ADD)
1905 {
1906 const char *pName = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1907 const char *pHelp = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1908 const char *pParams = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1909 if(!Unpacker.Error())
1910 {
1911 m_pConsole->RegisterTemp(pName, pParams, Flags: CFGFLAG_SERVER, pHelp);
1912 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
1913 }
1914 m_GotRconCommands++;
1915 }
1916 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_REM)
1917 {
1918 const char *pName = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1919 if(!Unpacker.Error())
1920 {
1921 m_pConsole->DeregisterTemp(pName);
1922 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
1923 }
1924 }
1925 else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_AUTH_STATUS)
1926 {
1927 int ResultInt = Unpacker.GetInt();
1928 if(!Unpacker.Error())
1929 {
1930 m_aRconAuthed[Conn] = ResultInt;
1931
1932 if(m_aRconAuthed[Conn])
1933 RconAuth(pName: m_aRconUsername, pPassword: m_aRconPassword, Dummy: g_Config.m_ClDummy ^ 1);
1934 }
1935 if(Conn == CONN_MAIN)
1936 {
1937 int Old = m_UseTempRconCommands;
1938 m_UseTempRconCommands = Unpacker.GetInt();
1939 if(Unpacker.Error())
1940 {
1941 m_UseTempRconCommands = 0;
1942 }
1943 if(Old != 0 && m_UseTempRconCommands == 0)
1944 {
1945 m_pConsole->DeregisterTempAll();
1946 m_ExpectedRconCommands = -1;
1947 m_vMaplistEntries.clear();
1948 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
1949 m_ExpectedMaplistEntries = -1;
1950 }
1951 }
1952 }
1953 else if(!Dummy && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_LINE)
1954 {
1955 const char *pLine = Unpacker.GetString();
1956 if(!Unpacker.Error())
1957 {
1958 GameClient()->OnRconLine(pLine);
1959 }
1960 }
1961 else if(Conn == CONN_MAIN && Msg == NETMSG_PING_REPLY)
1962 {
1963 char aBuf[256];
1964 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "latency %.2f", (time_get() - m_PingStartTime) * 1000 / (float)time_freq());
1965 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client/network", pStr: aBuf);
1966 }
1967 else if(Msg == NETMSG_INPUTTIMING)
1968 {
1969 int InputPredTick = Unpacker.GetInt();
1970 int TimeLeft = Unpacker.GetInt();
1971 if(Unpacker.Error())
1972 {
1973 return;
1974 }
1975
1976 int64_t Now = time_get();
1977
1978 // adjust our prediction time
1979 int64_t Target = 0;
1980 for(int k = 0; k < 200; k++)
1981 {
1982 if(m_aInputs[Conn][k].m_Tick == InputPredTick)
1983 {
1984 Target = m_aInputs[Conn][k].m_PredictedTime + (Now - m_aInputs[Conn][k].m_Time);
1985 Target = Target - (int64_t)((TimeLeft / 1000.0f) * time_freq());
1986 break;
1987 }
1988 }
1989
1990 if(Target)
1991 m_PredictedTime.Update(pGraph: &m_InputtimeMarginGraph, Target, TimeLeft, AdjustDirection: CSmoothTime::ADJUSTDIRECTION_UP);
1992 }
1993 else if(Msg == NETMSG_SNAP || Msg == NETMSG_SNAPSINGLE || Msg == NETMSG_SNAPEMPTY)
1994 {
1995 // we are not allowed to process snapshot yet
1996 if(State() < IClient::STATE_LOADING)
1997 {
1998 return;
1999 }
2000
2001 int GameTick = Unpacker.GetInt();
2002 int DeltaTick = GameTick - Unpacker.GetInt();
2003
2004 int NumParts = 1;
2005 int Part = 0;
2006 if(Msg == NETMSG_SNAP)
2007 {
2008 NumParts = Unpacker.GetInt();
2009 Part = Unpacker.GetInt();
2010 }
2011
2012 unsigned int Crc = 0;
2013 int PartSize = 0;
2014 if(Msg != NETMSG_SNAPEMPTY)
2015 {
2016 Crc = Unpacker.GetInt();
2017 PartSize = Unpacker.GetInt();
2018 }
2019
2020 const char *pData = (const char *)Unpacker.GetRaw(Size: PartSize);
2021 if(Unpacker.Error() || NumParts < 1 || NumParts > CSnapshot::MAX_PARTS || Part < 0 || Part >= NumParts || PartSize < 0 || PartSize > MAX_SNAPSHOT_PACKSIZE)
2022 {
2023 return;
2024 }
2025
2026 // Check m_aAckGameTick to see if we already got a snapshot for that tick
2027 if(GameTick >= m_aCurrentRecvTick[Conn] && GameTick > m_aAckGameTick[Conn])
2028 {
2029 if(GameTick != m_aCurrentRecvTick[Conn])
2030 {
2031 m_aSnapshotParts[Conn] = 0;
2032 m_aCurrentRecvTick[Conn] = GameTick;
2033 m_aSnapshotIncomingDataSize[Conn] = 0;
2034 }
2035
2036 mem_copy(dest: (char *)m_aaSnapshotIncomingData[Conn] + Part * MAX_SNAPSHOT_PACKSIZE, source: pData, size: std::clamp(val: PartSize, lo: 0, hi: (int)sizeof(m_aaSnapshotIncomingData[Conn]) - Part * MAX_SNAPSHOT_PACKSIZE));
2037 m_aSnapshotParts[Conn] |= (uint64_t)(1) << Part;
2038
2039 if(Part == NumParts - 1)
2040 {
2041 m_aSnapshotIncomingDataSize[Conn] = (NumParts - 1) * MAX_SNAPSHOT_PACKSIZE + PartSize;
2042 }
2043
2044 if((NumParts < CSnapshot::MAX_PARTS && m_aSnapshotParts[Conn] == (((uint64_t)(1) << NumParts) - 1)) ||
2045 (NumParts == CSnapshot::MAX_PARTS && m_aSnapshotParts[Conn] == std::numeric_limits<uint64_t>::max()))
2046 {
2047 unsigned char aTmpBuffer2[CSnapshot::MAX_SIZE];
2048 unsigned char aTmpBuffer3[CSnapshot::MAX_SIZE];
2049 CSnapshot *pTmpBuffer3 = (CSnapshot *)aTmpBuffer3; // Fix compiler warning for strict-aliasing
2050
2051 // reset snapshotting
2052 m_aSnapshotParts[Conn] = 0;
2053
2054 // find snapshot that we should use as delta
2055 const CSnapshot *pDeltaShot = CSnapshot::EmptySnapshot();
2056 if(DeltaTick >= 0)
2057 {
2058 int DeltashotSize = m_aSnapshotStorage[Conn].Get(Tick: DeltaTick, pTagtime: nullptr, ppData: &pDeltaShot, ppAltData: nullptr);
2059
2060 if(DeltashotSize < 0)
2061 {
2062 // couldn't find the delta snapshots that the server used
2063 // to compress this snapshot. force the server to resync
2064 if(g_Config.m_Debug)
2065 {
2066 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client", pStr: "error, couldn't find the delta snapshot");
2067 }
2068
2069 // ack snapshot
2070 m_aAckGameTick[Conn] = -1;
2071 SendInput();
2072 return;
2073 }
2074 }
2075
2076 // decompress snapshot
2077 const void *pDeltaData = m_SnapshotDelta.EmptyDelta();
2078 int DeltaSize = sizeof(int) * 3;
2079
2080 if(m_aSnapshotIncomingDataSize[Conn])
2081 {
2082 int IntSize = CVariableInt::Decompress(pSrc: m_aaSnapshotIncomingData[Conn], SrcSize: m_aSnapshotIncomingDataSize[Conn], pDst: aTmpBuffer2, DstSize: sizeof(aTmpBuffer2));
2083
2084 if(IntSize < 0) // failure during decompression
2085 return;
2086
2087 pDeltaData = aTmpBuffer2;
2088 DeltaSize = IntSize;
2089 }
2090
2091 // unpack delta
2092 const int SnapSize = m_SnapshotDelta.UnpackDelta(pFrom: pDeltaShot, pTo: pTmpBuffer3, pSrcData: pDeltaData, DataSize: DeltaSize, Sixup: IsSixup());
2093 if(SnapSize < 0)
2094 {
2095 dbg_msg(sys: "client", fmt: "delta unpack failed. error=%d", SnapSize);
2096 return;
2097 }
2098 if(!pTmpBuffer3->IsValid(ActualSize: SnapSize))
2099 {
2100 dbg_msg(sys: "client", fmt: "snapshot invalid. SnapSize=%d, DeltaSize=%d", SnapSize, DeltaSize);
2101 return;
2102 }
2103
2104 if(Msg != NETMSG_SNAPEMPTY && pTmpBuffer3->Crc() != Crc)
2105 {
2106 log_error("client", "snapshot crc error #%d - tick=%d wantedcrc=%d gotcrc=%d compressed_size=%d delta_tick=%d",
2107 m_SnapCrcErrors, GameTick, Crc, pTmpBuffer3->Crc(), m_aSnapshotIncomingDataSize[Conn], DeltaTick);
2108
2109 m_SnapCrcErrors++;
2110 if(m_SnapCrcErrors > 10)
2111 {
2112 // to many errors, send reset
2113 m_aAckGameTick[Conn] = -1;
2114 SendInput();
2115 m_SnapCrcErrors = 0;
2116 }
2117 return;
2118 }
2119 else
2120 {
2121 if(m_SnapCrcErrors)
2122 m_SnapCrcErrors--;
2123 }
2124
2125 // purge old snapshots
2126 int PurgeTick = DeltaTick;
2127 if(m_aapSnapshots[Conn][SNAP_PREV] && m_aapSnapshots[Conn][SNAP_PREV]->m_Tick < PurgeTick)
2128 PurgeTick = m_aapSnapshots[Conn][SNAP_PREV]->m_Tick;
2129 if(m_aapSnapshots[Conn][SNAP_CURRENT] && m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick < PurgeTick)
2130 PurgeTick = m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick;
2131 m_aSnapshotStorage[Conn].PurgeUntil(Tick: PurgeTick);
2132
2133 // create a verified and unpacked snapshot
2134 int AltSnapSize = -1;
2135 unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
2136 CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
2137
2138 if(IsSixup())
2139 {
2140 unsigned char aTmpTransSnapBuffer[CSnapshot::MAX_SIZE];
2141 CSnapshot *pTmpTransSnapBuffer = (CSnapshot *)aTmpTransSnapBuffer;
2142 mem_copy(dest: pTmpTransSnapBuffer, source: pTmpBuffer3, size: CSnapshot::MAX_SIZE);
2143 AltSnapSize = GameClient()->TranslateSnap(pSnapDstSix: pAltSnapBuffer, pSnapSrcSeven: pTmpTransSnapBuffer, Conn, Dummy);
2144 }
2145 else
2146 {
2147 AltSnapSize = UnpackAndValidateSnapshot(pFrom: pTmpBuffer3, pTo: pAltSnapBuffer);
2148 }
2149
2150 if(AltSnapSize < 0)
2151 {
2152 dbg_msg(sys: "client", fmt: "unpack snapshot and validate failed. error=%d", AltSnapSize);
2153 return;
2154 }
2155
2156 // add new
2157 m_aSnapshotStorage[Conn].Add(Tick: GameTick, Tagtime: time_get(), DataSize: SnapSize, pData: pTmpBuffer3, AltDataSize: AltSnapSize, pAltData: pAltSnapBuffer);
2158
2159 if(!Dummy)
2160 {
2161 GameClient()->ProcessDemoSnapshot(pSnap: pTmpBuffer3);
2162
2163 unsigned char aSnapSeven[CSnapshot::MAX_SIZE];
2164 CSnapshot *pSnapSeven = (CSnapshot *)aSnapSeven;
2165 int DemoSnapSize = SnapSize;
2166 if(IsSixup())
2167 {
2168 DemoSnapSize = GameClient()->OnDemoRecSnap7(pFrom: pTmpBuffer3, pTo: pSnapSeven, Conn);
2169 if(DemoSnapSize < 0)
2170 {
2171 dbg_msg(sys: "sixup", fmt: "demo snapshot failed. error=%d", DemoSnapSize);
2172 }
2173 }
2174
2175 if(DemoSnapSize >= 0)
2176 {
2177 // add snapshot to demo
2178 for(auto &DemoRecorder : m_aDemoRecorder)
2179 {
2180 if(DemoRecorder.IsRecording())
2181 {
2182 // write snapshot
2183 DemoRecorder.RecordSnapshot(Tick: GameTick, pData: IsSixup() ? pSnapSeven : pTmpBuffer3, Size: DemoSnapSize);
2184 }
2185 }
2186 }
2187 }
2188
2189 // apply snapshot, cycle pointers
2190 m_aReceivedSnapshots[Conn]++;
2191
2192 // we got two snapshots until we see us self as connected
2193 if(m_aReceivedSnapshots[Conn] == 2)
2194 {
2195 // start at 200ms and work from there
2196 if(!Dummy)
2197 {
2198 m_PredictedTime.Init(Target: GameTick * time_freq() / GameTickSpeed());
2199 m_PredictedTime.SetAdjustSpeed(Direction: CSmoothTime::ADJUSTDIRECTION_UP, Value: 1000.0f);
2200 m_PredictedTime.UpdateMargin(Margin: PredictionMargin() * time_freq() / 1000);
2201 }
2202 m_aGameTime[Conn].Init(Target: (GameTick - 1) * time_freq() / GameTickSpeed());
2203 m_aapSnapshots[Conn][SNAP_PREV] = m_aSnapshotStorage[Conn].m_pFirst;
2204 m_aapSnapshots[Conn][SNAP_CURRENT] = m_aSnapshotStorage[Conn].m_pLast;
2205 m_aPrevGameTick[Conn] = m_aapSnapshots[Conn][SNAP_PREV]->m_Tick;
2206 m_aCurGameTick[Conn] = m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick;
2207 if(Conn == CONN_MAIN)
2208 {
2209 m_LocalStartTime = time_get();
2210#if defined(CONF_VIDEORECORDER)
2211 IVideo::SetLocalStartTime(m_LocalStartTime);
2212#endif
2213 }
2214 if(!Dummy)
2215 {
2216 GameClient()->OnNewSnapshot();
2217 }
2218 SetState(IClient::STATE_ONLINE);
2219 if(Conn == CONN_MAIN)
2220 {
2221 DemoRecorder_HandleAutoStart();
2222 }
2223 }
2224
2225 // adjust game time
2226 if(m_aReceivedSnapshots[Conn] > 2)
2227 {
2228 int64_t Now = m_aGameTime[Conn].Get(Now: time_get());
2229 int64_t TickStart = GameTick * time_freq() / GameTickSpeed();
2230 int64_t TimeLeft = (TickStart - Now) * 1000 / time_freq();
2231 m_aGameTime[Conn].Update(pGraph: &m_aGametimeMarginGraphs[Conn], Target: (GameTick - 1) * time_freq() / GameTickSpeed(), TimeLeft, AdjustDirection: CSmoothTime::ADJUSTDIRECTION_DOWN);
2232 }
2233
2234 if(m_aReceivedSnapshots[Conn] > GameTickSpeed() && !m_aDidPostConnect[Conn])
2235 {
2236 OnPostConnect(Conn);
2237 m_aDidPostConnect[Conn] = true;
2238 }
2239
2240 // ack snapshot
2241 m_aAckGameTick[Conn] = GameTick;
2242 }
2243 }
2244 }
2245 else if(Conn == CONN_MAIN && Msg == NETMSG_RCONTYPE)
2246 {
2247 bool UsernameReq = Unpacker.GetInt() & 1;
2248 if(!Unpacker.Error())
2249 {
2250 GameClient()->OnRconType(UsernameReq);
2251 }
2252 }
2253 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_START)
2254 {
2255 const int ExpectedRconCommands = Unpacker.GetInt();
2256 if(Unpacker.Error() || ExpectedRconCommands < 0)
2257 return;
2258
2259 m_ExpectedRconCommands = ExpectedRconCommands;
2260 m_GotRconCommands = 0;
2261 }
2262 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_END)
2263 {
2264 m_ExpectedRconCommands = -1;
2265 }
2266 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_ADD)
2267 {
2268 while(true)
2269 {
2270 const char *pMapName = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
2271 if(Unpacker.Error())
2272 {
2273 return;
2274 }
2275 if(pMapName[0] != '\0')
2276 {
2277 m_vMaplistEntries.emplace_back(args&: pMapName);
2278 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
2279 }
2280 }
2281 }
2282 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_GROUP_START)
2283 {
2284 const int ExpectedMaplistEntries = Unpacker.GetInt();
2285 if(Unpacker.Error() || ExpectedMaplistEntries < 0)
2286 return;
2287
2288 m_vMaplistEntries.clear();
2289 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
2290 m_ExpectedMaplistEntries = ExpectedMaplistEntries;
2291 }
2292 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_GROUP_END)
2293 {
2294 m_ExpectedMaplistEntries = -1;
2295 }
2296 }
2297 // the client handles only vital messages https://github.com/ddnet/ddnet/issues/11178
2298 else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 || Msg == NETMSGTYPE_SV_PREINPUT)
2299 {
2300 // game message
2301 if(!Dummy)
2302 {
2303 for(auto &DemoRecorder : m_aDemoRecorder)
2304 if(DemoRecorder.IsRecording())
2305 DemoRecorder.RecordMessage(pData: pPacket->m_pData, Size: pPacket->m_DataSize);
2306 }
2307
2308 GameClient()->OnMessage(MsgId: Msg, pUnpacker: &Unpacker, Conn, Dummy);
2309 }
2310}
2311
2312int CClient::UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo)
2313{
2314 CUnpacker Unpacker;
2315 CSnapshotBuilder Builder;
2316 Builder.Init();
2317 CNetObjHandler *pNetObjHandler = GameClient()->GetNetObjHandler();
2318
2319 int Num = pFrom->NumItems();
2320 for(int Index = 0; Index < Num; Index++)
2321 {
2322 const CSnapshotItem *pFromItem = pFrom->GetItem(Index);
2323 const int FromItemSize = pFrom->GetItemSize(Index);
2324 const int ItemType = pFrom->GetItemType(Index);
2325 const void *pData = pFromItem->Data();
2326 Unpacker.Reset(pData, Size: FromItemSize);
2327
2328 void *pRawObj = pNetObjHandler->SecureUnpackObj(Type: ItemType, pUnpacker: &Unpacker);
2329 if(!pRawObj)
2330 {
2331 if(g_Config.m_Debug && ItemType != UUID_UNKNOWN)
2332 {
2333 char aBuf[256];
2334 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "dropped weird object '%s' (%d), failed on '%s'", pNetObjHandler->GetObjName(Type: ItemType), ItemType, pNetObjHandler->FailedObjOn());
2335 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
2336 }
2337 continue;
2338 }
2339 const int ItemSize = pNetObjHandler->GetUnpackedObjSize(Type: ItemType);
2340
2341 void *pObj = Builder.NewItem(Type: pFromItem->Type(), Id: pFromItem->Id(), Size: ItemSize);
2342 if(!pObj)
2343 return -4;
2344
2345 mem_copy(dest: pObj, source: pRawObj, size: ItemSize);
2346 }
2347
2348 return Builder.Finish(pSnapdata: pTo);
2349}
2350
2351void CClient::ResetMapDownload(bool ResetActive)
2352{
2353 if(m_pMapdownloadTask)
2354 {
2355 m_pMapdownloadTask->Abort();
2356 m_pMapdownloadTask = nullptr;
2357 }
2358
2359 if(m_MapdownloadFileTemp)
2360 {
2361 io_close(io: m_MapdownloadFileTemp);
2362 m_MapdownloadFileTemp = nullptr;
2363 }
2364
2365 if(Storage()->FileExists(pFilename: m_aMapdownloadFilenameTemp, Type: IStorage::TYPE_SAVE))
2366 {
2367 Storage()->RemoveFile(pFilename: m_aMapdownloadFilenameTemp, Type: IStorage::TYPE_SAVE);
2368 }
2369
2370 if(ResetActive)
2371 {
2372 m_MapdownloadChunk = 0;
2373 m_MapdownloadSha256 = std::nullopt;
2374 m_MapdownloadCrc = 0;
2375 m_MapdownloadTotalsize = -1;
2376 m_MapdownloadAmount = 0;
2377 m_aMapdownloadFilename[0] = '\0';
2378 m_aMapdownloadFilenameTemp[0] = '\0';
2379 m_aMapdownloadName[0] = '\0';
2380 }
2381}
2382
2383void CClient::FinishMapDownload()
2384{
2385 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client/network", pStr: "download complete, loading map");
2386
2387 bool FileSuccess = true;
2388 FileSuccess &= Storage()->RemoveFile(pFilename: m_aMapdownloadFilename, Type: IStorage::TYPE_SAVE);
2389 FileSuccess &= Storage()->RenameFile(pOldFilename: m_aMapdownloadFilenameTemp, pNewFilename: m_aMapdownloadFilename, Type: IStorage::TYPE_SAVE);
2390 if(!FileSuccess)
2391 {
2392 char aError[128 + IO_MAX_PATH_LENGTH];
2393 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Could not save downloaded map. Try manually deleting this file: %s"), m_aMapdownloadFilename);
2394 DisconnectWithReason(pReason: aError);
2395 return;
2396 }
2397
2398 const char *pError = LoadMap(pName: m_aMapdownloadName, pFilename: m_aMapdownloadFilename, WantedSha256: m_MapdownloadSha256, WantedCrc: m_MapdownloadCrc);
2399 if(!pError)
2400 {
2401 ResetMapDownload(ResetActive: true);
2402 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client/network", pStr: "loading done");
2403 SendReady(Conn: CONN_MAIN);
2404 }
2405 else if(m_pMapdownloadTask) // fallback
2406 {
2407 ResetMapDownload(ResetActive: false);
2408 SendMapRequest();
2409 }
2410 else
2411 {
2412 DisconnectWithReason(pReason: pError);
2413 }
2414}
2415
2416void CClient::ResetDDNetInfoTask()
2417{
2418 if(m_pDDNetInfoTask)
2419 {
2420 m_pDDNetInfoTask->Abort();
2421 m_pDDNetInfoTask = nullptr;
2422 }
2423}
2424
2425typedef std::tuple<int, int, int> TVersion;
2426static const TVersion gs_InvalidVersion = std::make_tuple(args: -1, args: -1, args: -1);
2427
2428static TVersion ToVersion(char *pStr)
2429{
2430 int aVersion[3] = {0, 0, 0};
2431 const char *p = strtok(s: pStr, delim: ".");
2432
2433 for(int i = 0; i < 3 && p; ++i)
2434 {
2435 if(!str_isallnum(str: p))
2436 return gs_InvalidVersion;
2437
2438 aVersion[i] = str_toint(str: p);
2439 p = strtok(s: nullptr, delim: ".");
2440 }
2441
2442 if(p)
2443 return gs_InvalidVersion;
2444
2445 return std::make_tuple(args&: aVersion[0], args&: aVersion[1], args&: aVersion[2]);
2446}
2447
2448void CClient::LoadDDNetInfo()
2449{
2450 const json_value *pDDNetInfo = m_ServerBrowser.LoadDDNetInfo();
2451
2452 if(!pDDNetInfo)
2453 {
2454 m_InfoState = EInfoState::ERROR;
2455 return;
2456 }
2457
2458 const json_value &DDNetInfo = *pDDNetInfo;
2459 const json_value &CurrentVersion = DDNetInfo["version"];
2460 if(CurrentVersion.type == json_string)
2461 {
2462 char aNewVersionStr[64];
2463 str_copy(dst&: aNewVersionStr, src: CurrentVersion);
2464 char aCurVersionStr[64];
2465 str_copy(dst&: aCurVersionStr, GAME_RELEASE_VERSION);
2466 if(ToVersion(pStr: aNewVersionStr) > ToVersion(pStr: aCurVersionStr))
2467 {
2468 str_copy(dst&: m_aVersionStr, src: CurrentVersion);
2469 }
2470 else
2471 {
2472 m_aVersionStr[0] = '0';
2473 m_aVersionStr[1] = '\0';
2474 }
2475 }
2476
2477 const json_value &News = DDNetInfo["news"];
2478 if(News.type == json_string)
2479 {
2480 // Only mark news button if something new was added to the news
2481 if(m_aNews[0] && str_find(haystack: m_aNews, needle: News) == nullptr)
2482 g_Config.m_UiUnreadNews = true;
2483
2484 str_copy(dst&: m_aNews, src: News);
2485 }
2486
2487 const json_value &MapDownloadUrl = DDNetInfo["map-download-url"];
2488 if(MapDownloadUrl.type == json_string)
2489 {
2490 str_copy(dst&: m_aMapDownloadUrl, src: MapDownloadUrl);
2491 }
2492
2493 const json_value &Points = DDNetInfo["points"];
2494 if(Points.type == json_integer)
2495 {
2496 m_Points = Points.u.integer;
2497 }
2498
2499 const json_value &StunServersIpv6 = DDNetInfo["stun-servers-ipv6"];
2500 if(StunServersIpv6.type == json_array && StunServersIpv6[0].type == json_string)
2501 {
2502 NETADDR Addr;
2503 if(!net_addr_from_str(addr: &Addr, string: StunServersIpv6[0]))
2504 {
2505 m_aNetClient[CONN_MAIN].FeedStunServer(StunServer: Addr);
2506 }
2507 }
2508 const json_value &StunServersIpv4 = DDNetInfo["stun-servers-ipv4"];
2509 if(StunServersIpv4.type == json_array && StunServersIpv4[0].type == json_string)
2510 {
2511 NETADDR Addr;
2512 if(!net_addr_from_str(addr: &Addr, string: StunServersIpv4[0]))
2513 {
2514 m_aNetClient[CONN_MAIN].FeedStunServer(StunServer: Addr);
2515 }
2516 }
2517 const json_value &ConnectingIp = DDNetInfo["connecting-ip"];
2518 if(ConnectingIp.type == json_string)
2519 {
2520 NETADDR Addr;
2521 if(!net_addr_from_str(addr: &Addr, string: ConnectingIp))
2522 {
2523 m_HaveGlobalTcpAddr = true;
2524 m_GlobalTcpAddr = Addr;
2525 log_debug("info", "got global tcp ip address: %s", (const char *)ConnectingIp);
2526 }
2527 }
2528 const json_value &WarnPngliteIncompatibleImages = DDNetInfo["warn-pnglite-incompatible-images"];
2529 Graphics()->WarnPngliteIncompatibleImages(Warn: WarnPngliteIncompatibleImages.type == json_boolean && (bool)WarnPngliteIncompatibleImages);
2530 m_InfoState = EInfoState::SUCCESS;
2531}
2532
2533int CClient::ConnectNetTypes() const
2534{
2535 const NETADDR *pConnectAddrs;
2536 int NumConnectAddrs;
2537 m_aNetClient[CONN_MAIN].ConnectAddresses(ppAddrs: &pConnectAddrs, pNumAddrs: &NumConnectAddrs);
2538 int NetType = 0;
2539 for(int i = 0; i < NumConnectAddrs; i++)
2540 {
2541 NetType |= pConnectAddrs[i].type;
2542 }
2543 return NetType;
2544}
2545
2546void CClient::PumpNetwork()
2547{
2548 for(auto &NetClient : m_aNetClient)
2549 {
2550 NetClient.Update();
2551 }
2552
2553 if(State() != IClient::STATE_DEMOPLAYBACK)
2554 {
2555 // check for errors of main and dummy
2556 if(State() != IClient::STATE_OFFLINE && State() < IClient::STATE_QUITTING)
2557 {
2558 if(m_aNetClient[CONN_MAIN].State() == NETSTATE_OFFLINE)
2559 {
2560 // This will also disconnect the dummy, so the branch below is an `else if`
2561 Disconnect();
2562 char aBuf[256];
2563 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "offline error='%s'", m_aNetClient[CONN_MAIN].ErrorString());
2564 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf, PrintColor: gs_ClientNetworkErrPrintColor);
2565 }
2566 else if((DummyConnecting() || DummyConnected()) && m_aNetClient[CONN_DUMMY].State() == NETSTATE_OFFLINE)
2567 {
2568 const bool WasConnecting = DummyConnecting();
2569 DummyDisconnect(pReason: nullptr);
2570 char aBuf[256];
2571 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "offline dummy error='%s'", m_aNetClient[CONN_DUMMY].ErrorString());
2572 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf, PrintColor: gs_ClientNetworkErrPrintColor);
2573 if(WasConnecting)
2574 {
2575 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Could not connect dummy"), m_aNetClient[CONN_DUMMY].ErrorString());
2576 GameClient()->Echo(pString: aBuf);
2577 }
2578 }
2579 }
2580
2581 // check if main was connected
2582 if(State() == IClient::STATE_CONNECTING && m_aNetClient[CONN_MAIN].State() == NETSTATE_ONLINE)
2583 {
2584 // we switched to online
2585 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: "connected, sending info", PrintColor: gs_ClientNetworkPrintColor);
2586 SetState(IClient::STATE_LOADING);
2587 SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_INITIAL);
2588 SendInfo(Conn: CONN_MAIN);
2589 }
2590
2591 // progress on dummy connect when the connection is online
2592 if(m_DummySendConnInfo && m_aNetClient[CONN_DUMMY].State() == NETSTATE_ONLINE)
2593 {
2594 m_DummySendConnInfo = false;
2595 SendInfo(Conn: CONN_DUMMY);
2596 m_aNetClient[CONN_DUMMY].Update();
2597 SendReady(Conn: CONN_DUMMY);
2598 GameClient()->SendDummyInfo(Start: true);
2599 SendEnterGame(Conn: CONN_DUMMY);
2600 }
2601 }
2602
2603 // process packets
2604 CNetChunk Packet;
2605 SECURITY_TOKEN ResponseToken;
2606 for(int Conn = 0; Conn < NUM_CONNS; Conn++)
2607 {
2608 while(m_aNetClient[Conn].Recv(pChunk: &Packet, pResponseToken: &ResponseToken, Sixup: IsSixup()))
2609 {
2610 if(Packet.m_ClientId == -1)
2611 {
2612 if(ResponseToken != NET_SECURITY_TOKEN_UNKNOWN)
2613 PreprocessConnlessPacket7(pPacket: &Packet);
2614
2615 ProcessConnlessPacket(pPacket: &Packet);
2616 continue;
2617 }
2618 if(Conn == CONN_MAIN || Conn == CONN_DUMMY)
2619 {
2620 ProcessServerPacket(pPacket: &Packet, Conn, Dummy: g_Config.m_ClDummy ^ Conn);
2621 }
2622 }
2623 }
2624}
2625
2626void CClient::OnDemoPlayerSnapshot(void *pData, int Size)
2627{
2628 // update ticks, they could have changed
2629 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
2630 m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
2631 m_aPrevGameTick[0] = pInfo->m_PreviousTick;
2632
2633 // create a verified and unpacked snapshot
2634 unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
2635 CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
2636 int AltSnapSize;
2637
2638 if(IsSixup())
2639 {
2640 AltSnapSize = GameClient()->TranslateSnap(pSnapDstSix: pAltSnapBuffer, pSnapSrcSeven: (CSnapshot *)pData, Conn: CONN_MAIN, Dummy: false);
2641 if(AltSnapSize < 0)
2642 {
2643 dbg_msg(sys: "sixup", fmt: "failed to translate snapshot. error=%d", AltSnapSize);
2644 return;
2645 }
2646 }
2647 else
2648 {
2649 AltSnapSize = UnpackAndValidateSnapshot(pFrom: (CSnapshot *)pData, pTo: pAltSnapBuffer);
2650 if(AltSnapSize < 0)
2651 {
2652 dbg_msg(sys: "client", fmt: "unpack snapshot and validate failed. error=%d", AltSnapSize);
2653 return;
2654 }
2655 }
2656
2657 // handle snapshots after validation
2658 std::swap(a&: m_aapSnapshots[0][SNAP_PREV], b&: m_aapSnapshots[0][SNAP_CURRENT]);
2659 mem_copy(dest: m_aapSnapshots[0][SNAP_CURRENT]->m_pSnap, source: pData, size: Size);
2660 mem_copy(dest: m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, source: pAltSnapBuffer, size: AltSnapSize);
2661
2662 GameClient()->OnNewSnapshot();
2663}
2664
2665void CClient::OnDemoPlayerMessage(void *pData, int Size)
2666{
2667 CUnpacker Unpacker;
2668 Unpacker.Reset(pData, Size);
2669 CMsgPacker Packer(NETMSG_EX, true);
2670
2671 // unpack msgid and system flag
2672 int Msg;
2673 bool Sys;
2674 CUuid Uuid;
2675
2676 int Result = UnpackMessageId(pId: &Msg, pSys: &Sys, pUuid: &Uuid, pUnpacker: &Unpacker, pPacker: &Packer);
2677 if(Result == UNPACKMESSAGE_ERROR)
2678 {
2679 return;
2680 }
2681
2682 if(!Sys)
2683 GameClient()->OnMessage(MsgId: Msg, pUnpacker: &Unpacker, Conn: CONN_MAIN, Dummy: false);
2684}
2685
2686void CClient::UpdateDemoIntraTimers()
2687{
2688 // update timers
2689 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
2690 m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
2691 m_aPrevGameTick[0] = pInfo->m_PreviousTick;
2692 m_aGameIntraTick[0] = pInfo->m_IntraTick;
2693 m_aGameTickTime[0] = pInfo->m_TickTime;
2694 m_aGameIntraTickSincePrev[0] = pInfo->m_IntraTickSincePrev;
2695}
2696
2697void CClient::Update()
2698{
2699 PumpNetwork();
2700
2701 if(State() == IClient::STATE_DEMOPLAYBACK)
2702 {
2703 if(m_DemoPlayer.IsPlaying())
2704 {
2705#if defined(CONF_VIDEORECORDER)
2706 if(IVideo::Current())
2707 {
2708 IVideo::Current()->NextVideoFrame();
2709 IVideo::Current()->NextAudioFrameTimeline(Mix: [this](short *pFinalOut, unsigned Frames) {
2710 Sound()->Mix(pFinalOut, Frames);
2711 });
2712 }
2713#endif
2714
2715 m_DemoPlayer.Update();
2716
2717 // update timers
2718 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
2719 m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
2720 m_aPrevGameTick[0] = pInfo->m_PreviousTick;
2721 m_aGameIntraTick[0] = pInfo->m_IntraTick;
2722 m_aGameTickTime[0] = pInfo->m_TickTime;
2723 }
2724 else
2725 {
2726 // Disconnect when demo playback stopped, either due to playback error
2727 // or because the end of the demo was reached when rendering it.
2728 DisconnectWithReason(pReason: m_DemoPlayer.ErrorMessage());
2729 if(m_DemoPlayer.ErrorMessage()[0] != '\0')
2730 {
2731 SWarning Warning(Localize(pStr: "Error playing demo"), m_DemoPlayer.ErrorMessage());
2732 Warning.m_AutoHide = false;
2733 AddWarning(Warning);
2734 }
2735 }
2736 }
2737 else if(State() == IClient::STATE_ONLINE)
2738 {
2739 if(m_LastDummy != (bool)g_Config.m_ClDummy)
2740 {
2741 // Invalidate references to !m_ClDummy snapshots
2742 GameClient()->InvalidateSnapshot();
2743 GameClient()->OnDummySwap();
2744 }
2745
2746 if(m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT])
2747 {
2748 // switch dummy snapshot
2749 int64_t Now = m_aGameTime[!g_Config.m_ClDummy].Get(Now: time_get());
2750 while(true)
2751 {
2752 if(!m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext)
2753 break;
2754 int64_t TickStart = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
2755 if(TickStart >= Now)
2756 break;
2757
2758 m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT];
2759 m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext;
2760
2761 // set ticks
2762 m_aCurGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick;
2763 m_aPrevGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV]->m_Tick;
2764 }
2765 }
2766
2767 if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT])
2768 {
2769 // switch snapshot
2770 bool Repredict = false;
2771 int64_t Now = m_aGameTime[g_Config.m_ClDummy].Get(Now: time_get());
2772 int64_t PredNow = m_PredictedTime.Get(Now: time_get());
2773
2774 if(m_LastDummy != (bool)g_Config.m_ClDummy && m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV])
2775 {
2776 // Load snapshot for m_ClDummy
2777 GameClient()->OnNewSnapshot();
2778 Repredict = true;
2779 }
2780
2781 while(true)
2782 {
2783 if(!m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext)
2784 break;
2785 int64_t TickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
2786 if(TickStart >= Now)
2787 break;
2788
2789 m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT];
2790 m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext;
2791
2792 // set ticks
2793 m_aCurGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick;
2794 m_aPrevGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick;
2795
2796 GameClient()->OnNewSnapshot();
2797 Repredict = true;
2798 }
2799
2800 if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV])
2801 {
2802 int64_t CurTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
2803 int64_t PrevTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick * time_freq() / GameTickSpeed();
2804 int PrevPredTick = (int)(PredNow * GameTickSpeed() / time_freq());
2805 int NewPredTick = PrevPredTick + 1;
2806
2807 m_aGameIntraTick[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)(CurTickStart - PrevTickStart);
2808 m_aGameTickTime[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)time_freq();
2809 m_aGameIntraTickSincePrev[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)(time_freq() / GameTickSpeed());
2810
2811 int64_t CurPredTickStart = NewPredTick * time_freq() / GameTickSpeed();
2812 int64_t PrevPredTickStart = PrevPredTick * time_freq() / GameTickSpeed();
2813 m_aPredIntraTick[g_Config.m_ClDummy] = (PredNow - PrevPredTickStart) / (float)(CurPredTickStart - PrevPredTickStart);
2814
2815 if(absolute(a: NewPredTick - m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick) > MaxLatencyTicks())
2816 {
2817 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: "prediction time reset!");
2818 m_PredictedTime.Init(Target: CurTickStart + 2 * time_freq() / GameTickSpeed());
2819 }
2820
2821 if(NewPredTick > m_aPredTick[g_Config.m_ClDummy])
2822 {
2823 m_aPredTick[g_Config.m_ClDummy] = NewPredTick;
2824 Repredict = true;
2825
2826 // send input
2827 SendInput();
2828 }
2829 }
2830
2831 // only do sane predictions
2832 if(Repredict)
2833 {
2834 if(m_aPredTick[g_Config.m_ClDummy] > m_aCurGameTick[g_Config.m_ClDummy] && m_aPredTick[g_Config.m_ClDummy] < m_aCurGameTick[g_Config.m_ClDummy] + MaxLatencyTicks())
2835 GameClient()->OnPredict();
2836 }
2837
2838 // fetch server info if we don't have it
2839 if(m_CurrentServerInfoRequestTime >= 0 &&
2840 time_get() > m_CurrentServerInfoRequestTime)
2841 {
2842 m_ServerBrowser.RequestCurrentServer(Addr: ServerAddress());
2843 m_CurrentServerInfoRequestTime = time_get() + time_freq() * 2;
2844 }
2845
2846 // periodically ping server
2847 if(m_CurrentServerNextPingTime >= 0 &&
2848 time_get() > m_CurrentServerNextPingTime)
2849 {
2850 int64_t NowPing = time_get();
2851 int64_t Freq = time_freq();
2852
2853 char aBuf[64];
2854 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "pinging current server%s", !m_ServerCapabilities.m_PingEx ? ", using fallback via server info" : "");
2855 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
2856
2857 m_CurrentServerPingUuid = RandomUuid();
2858 if(!m_ServerCapabilities.m_PingEx)
2859 {
2860 m_ServerBrowser.RequestCurrentServerWithRandomToken(Addr: ServerAddress(), pBasicToken: &m_CurrentServerPingBasicToken, pToken: &m_CurrentServerPingToken);
2861 }
2862 else
2863 {
2864 CMsgPacker Msg(NETMSG_PINGEX, true);
2865 Msg.AddRaw(pData: &m_CurrentServerPingUuid, Size: sizeof(m_CurrentServerPingUuid));
2866 SendMsg(Conn: CONN_MAIN, pMsg: &Msg, Flags: MSGFLAG_FLUSH);
2867 }
2868 m_CurrentServerCurrentPingTime = NowPing;
2869 m_CurrentServerNextPingTime = NowPing + 600 * Freq; // ping every 10 minutes
2870 }
2871 }
2872
2873 if(m_DummyDeactivateOnReconnect && g_Config.m_ClDummy == 1)
2874 {
2875 m_DummyDeactivateOnReconnect = false;
2876 g_Config.m_ClDummy = 0;
2877 }
2878 else if(!m_DummyConnected && m_DummyDeactivateOnReconnect)
2879 {
2880 m_DummyDeactivateOnReconnect = false;
2881 }
2882
2883 m_LastDummy = (bool)g_Config.m_ClDummy;
2884 }
2885
2886 // STRESS TEST: join the server again
2887#ifdef CONF_DEBUG
2888 if(g_Config.m_DbgStress)
2889 {
2890 static int64_t s_ActionTaken = 0;
2891 int64_t Now = time_get();
2892 if(State() == IClient::STATE_OFFLINE)
2893 {
2894 if(Now > s_ActionTaken + time_freq() * 2)
2895 {
2896 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "stress", pStr: "reconnecting!");
2897 Connect(pAddress: g_Config.m_DbgStressServer);
2898 s_ActionTaken = Now;
2899 }
2900 }
2901 else
2902 {
2903 if(Now > s_ActionTaken + time_freq() * (10 + g_Config.m_DbgStress))
2904 {
2905 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "stress", pStr: "disconnecting!");
2906 Disconnect();
2907 s_ActionTaken = Now;
2908 }
2909 }
2910 }
2911#endif
2912
2913 if(m_pMapdownloadTask)
2914 {
2915 if(m_pMapdownloadTask->State() == EHttpState::DONE)
2916 FinishMapDownload();
2917 else if(m_pMapdownloadTask->State() == EHttpState::ERROR || m_pMapdownloadTask->State() == EHttpState::ABORTED)
2918 {
2919 dbg_msg(sys: "webdl", fmt: "http failed, falling back to gameserver");
2920 ResetMapDownload(ResetActive: false);
2921 SendMapRequest();
2922 }
2923 }
2924
2925 if(m_pDDNetInfoTask)
2926 {
2927 if(m_pDDNetInfoTask->State() == EHttpState::DONE)
2928 {
2929 if(m_ServerBrowser.DDNetInfoSha256() == m_pDDNetInfoTask->ResultSha256())
2930 {
2931 log_debug("client/info", "DDNet info already up-to-date");
2932 m_InfoState = EInfoState::SUCCESS;
2933 }
2934 else
2935 {
2936 log_debug("client/info", "Loading new DDNet info");
2937 LoadDDNetInfo();
2938 }
2939
2940 ResetDDNetInfoTask();
2941 }
2942 else if(m_pDDNetInfoTask->State() == EHttpState::ERROR || m_pDDNetInfoTask->State() == EHttpState::ABORTED)
2943 {
2944 ResetDDNetInfoTask();
2945 m_InfoState = EInfoState::ERROR;
2946 }
2947 }
2948
2949 if(State() == IClient::STATE_ONLINE)
2950 {
2951 if(!m_EditJobs.empty())
2952 {
2953 std::shared_ptr<CDemoEdit> pJob = m_EditJobs.front();
2954 if(pJob->State() == IJob::STATE_DONE)
2955 {
2956 char aBuf[IO_MAX_PATH_LENGTH + 64];
2957 if(pJob->Success())
2958 {
2959 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Successfully saved the replay to '%s'!", pJob->Destination());
2960 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: aBuf);
2961
2962 GameClient()->Echo(pString: Localize(pStr: "Successfully saved the replay!"));
2963 }
2964 else
2965 {
2966 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Failed saving the replay to '%s'...", pJob->Destination());
2967 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: aBuf);
2968
2969 GameClient()->Echo(pString: Localize(pStr: "Failed saving the replay!"));
2970 }
2971 m_EditJobs.pop_front();
2972 }
2973 }
2974 }
2975
2976 // update the server browser
2977 m_ServerBrowser.Update();
2978
2979 // update editor/gameclient
2980 if(m_EditorActive)
2981 m_pEditor->OnUpdate();
2982 else
2983 GameClient()->OnUpdate();
2984
2985 Discord()->Update();
2986 Steam()->Update();
2987 if(Steam()->GetConnectAddress())
2988 {
2989 HandleConnectAddress(pAddr: Steam()->GetConnectAddress());
2990 Steam()->ClearConnectAddress();
2991 }
2992
2993 if(m_ReconnectTime > 0 && time_get() > m_ReconnectTime)
2994 {
2995 if(State() != STATE_ONLINE)
2996 Connect(pAddress: m_aConnectAddressStr);
2997 m_ReconnectTime = 0;
2998 }
2999
3000 m_PredictedTime.UpdateMargin(Margin: PredictionMargin() * time_freq() / 1000);
3001}
3002
3003void CClient::RegisterInterfaces()
3004{
3005 Kernel()->RegisterInterface(pInterface: static_cast<IDemoRecorder *>(&m_aDemoRecorder[RECORDER_MANUAL]), Destroy: false);
3006 Kernel()->RegisterInterface(pInterface: static_cast<IDemoPlayer *>(&m_DemoPlayer), Destroy: false);
3007 Kernel()->RegisterInterface(pInterface: static_cast<IGhostRecorder *>(&m_GhostRecorder), Destroy: false);
3008 Kernel()->RegisterInterface(pInterface: static_cast<IGhostLoader *>(&m_GhostLoader), Destroy: false);
3009 Kernel()->RegisterInterface(pInterface: static_cast<IServerBrowser *>(&m_ServerBrowser), Destroy: false);
3010#if defined(CONF_AUTOUPDATE)
3011 Kernel()->RegisterInterface(pInterface: static_cast<IUpdater *>(&m_Updater), Destroy: false);
3012#endif
3013 Kernel()->RegisterInterface(pInterface: static_cast<IFriends *>(&m_Friends), Destroy: false);
3014 Kernel()->ReregisterInterface(pInterface: static_cast<IFriends *>(&m_Foes));
3015 Kernel()->RegisterInterface(pInterface: static_cast<IHttp *>(&m_Http), Destroy: false);
3016}
3017
3018void CClient::InitInterfaces()
3019{
3020 // fetch interfaces
3021 m_pEngine = Kernel()->RequestInterface<IEngine>();
3022 m_pEditor = Kernel()->RequestInterface<IEditor>();
3023 m_pFavorites = Kernel()->RequestInterface<IFavorites>();
3024 m_pSound = Kernel()->RequestInterface<IEngineSound>();
3025 m_pGameClient = Kernel()->RequestInterface<IGameClient>();
3026 m_pInput = Kernel()->RequestInterface<IEngineInput>();
3027 m_pMap = Kernel()->RequestInterface<IEngineMap>();
3028 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
3029 m_pConfig = m_pConfigManager->Values();
3030#if defined(CONF_AUTOUPDATE)
3031 m_pUpdater = Kernel()->RequestInterface<IUpdater>();
3032#endif
3033 m_pDiscord = Kernel()->RequestInterface<IDiscord>();
3034 m_pSteam = Kernel()->RequestInterface<ISteam>();
3035 m_pNotifications = Kernel()->RequestInterface<INotifications>();
3036 m_pStorage = Kernel()->RequestInterface<IStorage>();
3037
3038 m_DemoEditor.Init(pSnapshotDelta: &m_SnapshotDelta, pConsole: m_pConsole, pStorage: m_pStorage);
3039
3040 m_ServerBrowser.SetBaseInfo(pClient: &m_aNetClient[CONN_CONTACT], pNetVersion: m_pGameClient->NetVersion());
3041
3042#if defined(CONF_AUTOUPDATE)
3043 m_Updater.Init(pHttp: &m_Http);
3044#endif
3045
3046 m_pConfigManager->RegisterCallback(pfnFunc: IFavorites::ConfigSaveCallback, pUserData: m_pFavorites);
3047 m_Friends.Init();
3048 m_Foes.Init(Foes: true);
3049
3050 m_GhostRecorder.Init();
3051 m_GhostLoader.Init();
3052}
3053
3054void CClient::Run()
3055{
3056 m_LocalStartTime = m_GlobalStartTime = time_get();
3057#if defined(CONF_VIDEORECORDER)
3058 IVideo::SetLocalStartTime(m_LocalStartTime);
3059#endif
3060 m_aSnapshotParts[0] = 0;
3061 m_aSnapshotParts[1] = 0;
3062
3063 if(m_GenerateTimeoutSeed)
3064 {
3065 GenerateTimeoutSeed();
3066 }
3067
3068 unsigned int Seed;
3069 secure_random_fill(bytes: &Seed, length: sizeof(Seed));
3070 srand(seed: Seed);
3071
3072 if(g_Config.m_Debug)
3073 {
3074 g_UuidManager.DebugDump();
3075 }
3076
3077 char aNetworkError[256];
3078 if(!InitNetworkClient(pError: aNetworkError, ErrorSize: sizeof(aNetworkError)))
3079 {
3080 log_error("client", "%s", aNetworkError);
3081 ShowMessageBox(MessageBox: {.m_pTitle = "Network Error", .m_pMessage = aNetworkError});
3082 return;
3083 }
3084
3085 if(!m_Http.Init(ShutdownDelay: std::chrono::seconds{1}))
3086 {
3087 const char *pErrorMessage = "Failed to initialize the HTTP client.";
3088 log_error("client", "%s", pErrorMessage);
3089 ShowMessageBox(MessageBox: {.m_pTitle = "HTTP Error", .m_pMessage = pErrorMessage});
3090 return;
3091 }
3092
3093 // init graphics
3094 m_pGraphics = CreateEngineGraphicsThreaded();
3095 Kernel()->RegisterInterface(pInterface: m_pGraphics); // IEngineGraphics
3096 Kernel()->RegisterInterface(pInterface: static_cast<IGraphics *>(m_pGraphics), Destroy: false);
3097 {
3098 CMemoryLogger MemoryLogger;
3099 MemoryLogger.SetParent(log_get_scope_logger());
3100 bool Success;
3101 {
3102 CLogScope LogScope(&MemoryLogger);
3103 Success = m_pGraphics->Init() == 0;
3104 }
3105 if(!Success)
3106 {
3107 log_error("client", "Failed to initialize the graphics (see details above)");
3108 std::string Message = std::string("Failed to initialize the graphics. See details below.\n\n") + MemoryLogger.ConcatenatedLines();
3109 ShowMessageBox(MessageBox: {.m_pTitle = "Graphics Error", .m_pMessage = Message.c_str()});
3110 return;
3111 }
3112 }
3113
3114 // make sure the first frame just clears everything to prevent undesired colors when waiting for io
3115 Graphics()->Clear(r: 0, g: 0, b: 0);
3116 Graphics()->Swap();
3117
3118 // init localization first, making sure all errors during init can be localized
3119 GameClient()->InitializeLanguage();
3120
3121 // init sound, allowed to fail
3122 const bool SoundInitFailed = Sound()->Init() != 0;
3123
3124#if defined(CONF_VIDEORECORDER)
3125 // init video recorder aka ffmpeg
3126 CVideo::Init();
3127#endif
3128
3129 // init text render
3130 m_pTextRender = Kernel()->RequestInterface<IEngineTextRender>();
3131 m_pTextRender->Init();
3132
3133 // init the input
3134 Input()->Init();
3135
3136 // init the editor
3137 m_pEditor->Init();
3138
3139 m_ServerBrowser.OnInit();
3140 // loads the existing ddnet info file if it exists
3141 LoadDDNetInfo();
3142
3143 LoadDebugFont();
3144
3145 if(Steam()->GetPlayerName())
3146 {
3147 str_copy(dst&: g_Config.m_SteamName, src: Steam()->GetPlayerName());
3148 }
3149
3150 Graphics()->AddWindowResizeListener(pFunc: [this] { OnWindowResize(); });
3151
3152 GameClient()->OnInit();
3153
3154 m_Fifo.Init(pConsole: m_pConsole, pFifoFile: g_Config.m_ClInputFifo, Flag: CFGFLAG_CLIENT);
3155
3156 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: "version " GAME_RELEASE_VERSION " on " CONF_PLATFORM_STRING " " CONF_ARCH_STRING, PrintColor: ColorRGBA(0.7f, 0.7f, 1.0f, 1.0f));
3157 if(GIT_SHORTREV_HASH)
3158 {
3159 char aBuf[64];
3160 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "git revision hash: %s", GIT_SHORTREV_HASH);
3161 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf, PrintColor: ColorRGBA(0.7f, 0.7f, 1.0f, 1.0f));
3162 }
3163
3164 //
3165 m_FpsGraph.Init(Min: 0.0f, Max: 120.0f);
3166
3167 // never start with the editor
3168 g_Config.m_ClEditor = 0;
3169
3170 // process pending commands
3171 m_pConsole->StoreCommands(Store: false);
3172
3173 InitChecksum();
3174 m_pConsole->InitChecksum(pData: ChecksumData());
3175
3176 // request the new ddnet info from server if already past the welcome dialog
3177 if(g_Config.m_ClShowWelcome)
3178 g_Config.m_ClShowWelcome = 0;
3179 else
3180 RequestDDNetInfo();
3181
3182 if(SoundInitFailed)
3183 {
3184 SWarning Warning(Localize(pStr: "Sound error"), Localize(pStr: "The audio device couldn't be initialised."));
3185 Warning.m_AutoHide = false;
3186 AddWarning(Warning);
3187 }
3188
3189 bool LastD = false;
3190 bool LastE = false;
3191 bool LastG = false;
3192
3193 auto LastTime = time_get_nanoseconds();
3194 int64_t LastRenderTime = time_get();
3195
3196 while(true)
3197 {
3198 set_new_tick();
3199
3200 // handle pending connects
3201 if(m_aCmdConnect[0])
3202 {
3203 str_copy(dst&: g_Config.m_UiServerAddress, src: m_aCmdConnect);
3204 Connect(pAddress: m_aCmdConnect);
3205 m_aCmdConnect[0] = 0;
3206 }
3207
3208 // handle pending demo play
3209 if(m_aCmdPlayDemo[0])
3210 {
3211 const char *pError = DemoPlayer_Play(pFilename: m_aCmdPlayDemo, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
3212 if(pError)
3213 log_error("demo_player", "playing passed demo file '%s' failed: %s", m_aCmdPlayDemo, pError);
3214 m_aCmdPlayDemo[0] = 0;
3215 }
3216
3217 // handle pending map edits
3218 if(m_aCmdEditMap[0])
3219 {
3220 int Result = m_pEditor->HandleMapDrop(pFilename: m_aCmdEditMap, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
3221 if(Result)
3222 g_Config.m_ClEditor = true;
3223 else
3224 log_error("editor", "editing passed map file '%s' failed", m_aCmdEditMap);
3225 m_aCmdEditMap[0] = 0;
3226 }
3227
3228 // update input
3229 if(Input()->Update())
3230 {
3231 if(State() == IClient::STATE_QUITTING)
3232 break;
3233 else
3234 SetState(IClient::STATE_QUITTING); // SDL_QUIT
3235 }
3236
3237 char aFile[IO_MAX_PATH_LENGTH];
3238 if(Input()->GetDropFile(aBuf: aFile, Len: sizeof(aFile)))
3239 {
3240 if(str_startswith(str: aFile, CONNECTLINK_NO_SLASH))
3241 HandleConnectLink(pLink: aFile);
3242 else if(str_endswith(str: aFile, suffix: ".demo"))
3243 HandleDemoPath(pPath: aFile);
3244 else if(str_endswith(str: aFile, suffix: ".map"))
3245 HandleMapPath(pPath: aFile);
3246 }
3247
3248#if defined(CONF_AUTOUPDATE)
3249 Updater()->Update();
3250#endif
3251
3252 // update sound
3253 Sound()->Update();
3254
3255 if(CtrlShiftKey(Key: KEY_D, Last&: LastD))
3256 g_Config.m_Debug ^= 1;
3257
3258 if(CtrlShiftKey(Key: KEY_G, Last&: LastG))
3259 g_Config.m_DbgGraphs ^= 1;
3260
3261 if(CtrlShiftKey(Key: KEY_E, Last&: LastE))
3262 {
3263 if(g_Config.m_ClEditor)
3264 m_pEditor->OnClose();
3265 g_Config.m_ClEditor = g_Config.m_ClEditor ^ 1;
3266 }
3267
3268 // render
3269 {
3270 if(g_Config.m_ClEditor)
3271 {
3272 if(!m_EditorActive)
3273 {
3274 Input()->MouseModeRelative();
3275 GameClient()->OnActivateEditor();
3276 m_pEditor->OnActivate();
3277 m_EditorActive = true;
3278 }
3279 }
3280 else if(m_EditorActive)
3281 {
3282 m_EditorActive = false;
3283 }
3284
3285 Update();
3286 int64_t Now = time_get();
3287
3288 bool IsRenderActive = (g_Config.m_GfxBackgroundRender || m_pGraphics->WindowOpen());
3289
3290 bool AsyncRenderOld = g_Config.m_GfxAsyncRenderOld;
3291
3292 int GfxRefreshRate = g_Config.m_GfxRefreshRate;
3293
3294#if defined(CONF_VIDEORECORDER)
3295 // keep rendering synced
3296 if(IVideo::Current())
3297 {
3298 AsyncRenderOld = false;
3299 GfxRefreshRate = 0;
3300 }
3301#endif
3302
3303 if(IsRenderActive &&
3304 (!AsyncRenderOld || m_pGraphics->IsIdle()) &&
3305 (!GfxRefreshRate || (time_freq() / (int64_t)g_Config.m_GfxRefreshRate) <= Now - LastRenderTime))
3306 {
3307 // update frametime
3308 m_RenderFrameTime = (Now - m_LastRenderTime) / (float)time_freq();
3309 m_FpsGraph.Add(Value: 1.0f / m_RenderFrameTime);
3310
3311 if(m_BenchmarkFile)
3312 {
3313 char aBuf[64];
3314 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Frametime %d us\n", (int)(m_RenderFrameTime * 1000000));
3315 io_write(io: m_BenchmarkFile, buffer: aBuf, size: str_length(str: aBuf));
3316 if(time_get() > m_BenchmarkStopTime)
3317 {
3318 io_close(io: m_BenchmarkFile);
3319 m_BenchmarkFile = nullptr;
3320 Quit();
3321 }
3322 }
3323
3324 m_FrameTimeAverage = m_FrameTimeAverage * 0.9f + m_RenderFrameTime * 0.1f;
3325
3326 // keep the overflow time - it's used to make sure the gfx refreshrate is reached
3327 int64_t AdditionalTime = g_Config.m_GfxRefreshRate ? ((Now - LastRenderTime) - (time_freq() / (int64_t)g_Config.m_GfxRefreshRate)) : 0;
3328 // if the value is over the frametime of a 60 fps frame, reset the additional time (drop the frames, that are lost already)
3329 if(AdditionalTime > (time_freq() / 60))
3330 AdditionalTime = (time_freq() / 60);
3331 LastRenderTime = Now - AdditionalTime;
3332 m_LastRenderTime = Now;
3333
3334 Render();
3335 m_pGraphics->Swap();
3336 }
3337 else if(!IsRenderActive)
3338 {
3339 // if the client does not render, it should reset its render time to a time where it would render the first frame, when it wakes up again
3340 LastRenderTime = g_Config.m_GfxRefreshRate ? (Now - (time_freq() / (int64_t)g_Config.m_GfxRefreshRate)) : Now;
3341 }
3342 }
3343
3344 AutoScreenshot_Cleanup();
3345 AutoStatScreenshot_Cleanup();
3346 AutoCSV_Cleanup();
3347
3348 m_Fifo.Update();
3349
3350 if(State() == IClient::STATE_QUITTING || State() == IClient::STATE_RESTARTING)
3351 break;
3352
3353 // beNice
3354 auto Now = time_get_nanoseconds();
3355 decltype(Now) SleepTimeInNanoSeconds{0};
3356 bool Slept = false;
3357 if(g_Config.m_ClRefreshRateInactive && !m_pGraphics->WindowActive())
3358 {
3359 SleepTimeInNanoSeconds = (std::chrono::nanoseconds(1s) / (int64_t)g_Config.m_ClRefreshRateInactive) - (Now - LastTime);
3360 std::this_thread::sleep_for(rtime: SleepTimeInNanoSeconds);
3361 Slept = true;
3362 }
3363 else if(g_Config.m_ClRefreshRate)
3364 {
3365 SleepTimeInNanoSeconds = (std::chrono::nanoseconds(1s) / (int64_t)g_Config.m_ClRefreshRate) - (Now - LastTime);
3366 auto SleepTimeInNanoSecondsInner = SleepTimeInNanoSeconds;
3367 auto NowInner = Now;
3368 while(std::chrono::duration_cast<std::chrono::microseconds>(d: SleepTimeInNanoSecondsInner) > 0us)
3369 {
3370 net_socket_read_wait(sock: m_aNetClient[CONN_MAIN].m_Socket, nanoseconds: SleepTimeInNanoSecondsInner);
3371 auto NowInnerCalc = time_get_nanoseconds();
3372 SleepTimeInNanoSecondsInner -= (NowInnerCalc - NowInner);
3373 NowInner = NowInnerCalc;
3374 }
3375 Slept = true;
3376 }
3377 if(Slept)
3378 {
3379 // if the diff gets too small it shouldn't get even smaller (drop the updates, that could not be handled)
3380 if(SleepTimeInNanoSeconds < -16666666ns)
3381 SleepTimeInNanoSeconds = -16666666ns;
3382 // don't go higher than the frametime of a 60 fps frame
3383 else if(SleepTimeInNanoSeconds > 16666666ns)
3384 SleepTimeInNanoSeconds = 16666666ns;
3385 // the time diff between the time that was used actually used and the time the thread should sleep/wait
3386 // will be calculated in the sleep time of the next update tick by faking the time it should have slept/wait.
3387 // so two cases (and the case it slept exactly the time it should):
3388 // - the thread slept/waited too long, then it adjust the time to sleep/wait less in the next update tick
3389 // - the thread slept/waited too less, then it adjust the time to sleep/wait more in the next update tick
3390 LastTime = Now + SleepTimeInNanoSeconds;
3391 }
3392 else
3393 LastTime = Now;
3394
3395 // update local and global time
3396 m_LocalTime = (time_get() - m_LocalStartTime) / (float)time_freq();
3397 m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq();
3398 }
3399
3400 GameClient()->RenderShutdownMessage();
3401 Disconnect();
3402
3403 if(!m_pConfigManager->Save())
3404 {
3405 char aError[128];
3406 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Saving settings to '%s' failed"), CONFIG_FILE);
3407 m_vQuittingWarnings.emplace_back(args: Localize(pStr: "Error saving settings"), args&: aError);
3408 }
3409
3410 m_Fifo.Shutdown();
3411 m_Http.Shutdown();
3412 Engine()->ShutdownJobs();
3413
3414 GameClient()->RenderShutdownMessage();
3415 GameClient()->OnShutdown();
3416 delete m_pEditor;
3417
3418 // close sockets
3419 for(unsigned int i = 0; i < std::size(m_aNetClient); i++)
3420 m_aNetClient[i].Close();
3421
3422 // shutdown text render while graphics are still available
3423 m_pTextRender->Shutdown();
3424}
3425
3426bool CClient::InitNetworkClient(char *pError, size_t ErrorSize)
3427{
3428 NETADDR BindAddr;
3429 if(g_Config.m_Bindaddr[0] == '\0')
3430 {
3431 mem_zero(block: &BindAddr, size: sizeof(BindAddr));
3432 }
3433 else if(net_host_lookup(hostname: g_Config.m_Bindaddr, addr: &BindAddr, types: NETTYPE_ALL) != 0)
3434 {
3435 str_format(buffer: pError, buffer_size: ErrorSize, format: "The configured bindaddr '%s' cannot be resolved.", g_Config.m_Bindaddr);
3436 return false;
3437 }
3438 BindAddr.type = NETTYPE_ALL;
3439 for(size_t i = 0; i < std::size(m_aNetClient); i++)
3440 {
3441 if(!InitNetworkClientImpl(BindAddr, Conn: i, pError, ErrorSize))
3442 {
3443 return false;
3444 }
3445 }
3446 return true;
3447}
3448
3449bool CClient::InitNetworkClientImpl(NETADDR BindAddr, int Conn, char *pError, size_t ErrorSize)
3450{
3451 int *pPort;
3452 const char *pName;
3453 switch(Conn)
3454 {
3455 case CONN_MAIN:
3456 pPort = &g_Config.m_ClPort;
3457 pName = "main";
3458 break;
3459 case CONN_DUMMY:
3460 pPort = &g_Config.m_ClDummyPort;
3461 pName = "dummy";
3462 break;
3463 case CONN_CONTACT:
3464 pPort = &g_Config.m_ClContactPort;
3465 pName = "contact";
3466 break;
3467 default:
3468 dbg_assert_failed("unreachable");
3469 }
3470 if(m_aNetClient[Conn].State() != NETSTATE_OFFLINE)
3471 {
3472 str_format(buffer: pError, buffer_size: ErrorSize, format: "Could not open network client %s while already connected.", pName);
3473 return false;
3474 }
3475 if(*pPort < 1024) // Reject users setting ports that we don't want to use
3476 *pPort = 0;
3477 BindAddr.port = *pPort;
3478
3479 unsigned RemainingAttempts = 25;
3480 while(!m_aNetClient[Conn].Open(BindAddr))
3481 {
3482 --RemainingAttempts;
3483 if(RemainingAttempts == 0)
3484 {
3485 if(g_Config.m_Bindaddr[0])
3486 str_format(buffer: pError, buffer_size: ErrorSize, format: "Could not open network client %s, try changing or unsetting the bindaddr '%s'.", pName, g_Config.m_Bindaddr);
3487 else
3488 str_format(buffer: pError, buffer_size: ErrorSize, format: "Could not open network client %s.", pName);
3489 return false;
3490 }
3491 if(BindAddr.port != 0)
3492 BindAddr.port = 0;
3493 }
3494 return true;
3495}
3496
3497bool CClient::CtrlShiftKey(int Key, bool &Last)
3498{
3499 if(Input()->ModifierIsPressed() && Input()->ShiftIsPressed() && !Last && Input()->KeyIsPressed(Key))
3500 {
3501 Last = true;
3502 return true;
3503 }
3504 else if(Last && !Input()->KeyIsPressed(Key))
3505 Last = false;
3506
3507 return false;
3508}
3509
3510void CClient::Con_Connect(IConsole::IResult *pResult, void *pUserData)
3511{
3512 CClient *pSelf = (CClient *)pUserData;
3513 pSelf->HandleConnectLink(pLink: pResult->GetString(Index: 0));
3514}
3515
3516void CClient::Con_Disconnect(IConsole::IResult *pResult, void *pUserData)
3517{
3518 CClient *pSelf = (CClient *)pUserData;
3519 pSelf->Disconnect();
3520}
3521
3522void CClient::Con_DummyConnect(IConsole::IResult *pResult, void *pUserData)
3523{
3524 CClient *pSelf = (CClient *)pUserData;
3525 pSelf->DummyConnect();
3526}
3527
3528void CClient::Con_DummyDisconnect(IConsole::IResult *pResult, void *pUserData)
3529{
3530 CClient *pSelf = (CClient *)pUserData;
3531 pSelf->DummyDisconnect(pReason: nullptr);
3532}
3533
3534void CClient::Con_DummyResetInput(IConsole::IResult *pResult, void *pUserData)
3535{
3536 CClient *pSelf = (CClient *)pUserData;
3537 pSelf->GameClient()->DummyResetInput();
3538}
3539
3540void CClient::Con_Quit(IConsole::IResult *pResult, void *pUserData)
3541{
3542 CClient *pSelf = (CClient *)pUserData;
3543 pSelf->Quit();
3544}
3545
3546void CClient::Con_Restart(IConsole::IResult *pResult, void *pUserData)
3547{
3548 CClient *pSelf = (CClient *)pUserData;
3549 pSelf->Restart();
3550}
3551
3552void CClient::Con_Minimize(IConsole::IResult *pResult, void *pUserData)
3553{
3554 CClient *pSelf = (CClient *)pUserData;
3555 pSelf->Graphics()->Minimize();
3556}
3557
3558void CClient::Con_Ping(IConsole::IResult *pResult, void *pUserData)
3559{
3560 CClient *pSelf = (CClient *)pUserData;
3561
3562 CMsgPacker Msg(NETMSG_PING, true);
3563 pSelf->SendMsg(Conn: CONN_MAIN, pMsg: &Msg, Flags: MSGFLAG_FLUSH);
3564 pSelf->m_PingStartTime = time_get();
3565}
3566
3567void CClient::ConNetReset(IConsole::IResult *pResult, void *pUserData)
3568{
3569 CClient *pSelf = (CClient *)pUserData;
3570 pSelf->ResetSocket();
3571}
3572
3573void CClient::AutoScreenshot_Start()
3574{
3575 if(g_Config.m_ClAutoScreenshot)
3576 {
3577 Graphics()->TakeScreenshot(pFilename: "auto/autoscreen");
3578 m_AutoScreenshotRecycle = true;
3579 }
3580}
3581
3582void CClient::AutoStatScreenshot_Start()
3583{
3584 if(g_Config.m_ClAutoStatboardScreenshot)
3585 {
3586 Graphics()->TakeScreenshot(pFilename: "auto/stats/autoscreen");
3587 m_AutoStatScreenshotRecycle = true;
3588 }
3589}
3590
3591void CClient::AutoScreenshot_Cleanup()
3592{
3593 if(m_AutoScreenshotRecycle)
3594 {
3595 if(g_Config.m_ClAutoScreenshotMax)
3596 {
3597 // clean up auto taken screens
3598 CFileCollection AutoScreens;
3599 AutoScreens.Init(pStorage: Storage(), pPath: "screenshots/auto", pFileDesc: "autoscreen", pFileExt: ".png", MaxEntries: g_Config.m_ClAutoScreenshotMax);
3600 }
3601 m_AutoScreenshotRecycle = false;
3602 }
3603}
3604
3605void CClient::AutoStatScreenshot_Cleanup()
3606{
3607 if(m_AutoStatScreenshotRecycle)
3608 {
3609 if(g_Config.m_ClAutoStatboardScreenshotMax)
3610 {
3611 // clean up auto taken screens
3612 CFileCollection AutoScreens;
3613 AutoScreens.Init(pStorage: Storage(), pPath: "screenshots/auto/stats", pFileDesc: "autoscreen", pFileExt: ".png", MaxEntries: g_Config.m_ClAutoStatboardScreenshotMax);
3614 }
3615 m_AutoStatScreenshotRecycle = false;
3616 }
3617}
3618
3619void CClient::AutoCSV_Start()
3620{
3621 if(g_Config.m_ClAutoCSV)
3622 m_AutoCSVRecycle = true;
3623}
3624
3625void CClient::AutoCSV_Cleanup()
3626{
3627 if(m_AutoCSVRecycle)
3628 {
3629 if(g_Config.m_ClAutoCSVMax)
3630 {
3631 // clean up auto csvs
3632 CFileCollection AutoRecord;
3633 AutoRecord.Init(pStorage: Storage(), pPath: "record/csv", pFileDesc: "autorecord", pFileExt: ".csv", MaxEntries: g_Config.m_ClAutoCSVMax);
3634 }
3635 m_AutoCSVRecycle = false;
3636 }
3637}
3638
3639void CClient::Con_Screenshot(IConsole::IResult *pResult, void *pUserData)
3640{
3641 CClient *pSelf = (CClient *)pUserData;
3642 pSelf->Graphics()->TakeScreenshot(pFilename: nullptr);
3643}
3644
3645#if defined(CONF_VIDEORECORDER)
3646
3647void CClient::Con_StartVideo(IConsole::IResult *pResult, void *pUserData)
3648{
3649 CClient *pSelf = static_cast<CClient *>(pUserData);
3650
3651 if(pResult->NumArguments())
3652 {
3653 pSelf->StartVideo(pFilename: pResult->GetString(Index: 0), WithTimestamp: false);
3654 }
3655 else
3656 {
3657 pSelf->StartVideo(pFilename: "video", WithTimestamp: true);
3658 }
3659}
3660
3661void CClient::StartVideo(const char *pFilename, bool WithTimestamp)
3662{
3663 if(State() != IClient::STATE_DEMOPLAYBACK)
3664 {
3665 log_error("videorecorder", "Video can only be recorded in demo player.");
3666 return;
3667 }
3668
3669 if(IVideo::Current())
3670 {
3671 log_error("videorecorder", "Already recording.");
3672 return;
3673 }
3674
3675 char aFilename[IO_MAX_PATH_LENGTH];
3676 if(WithTimestamp)
3677 {
3678 char aTimestamp[20];
3679 str_timestamp(buffer: aTimestamp, buffer_size: sizeof(aTimestamp));
3680 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "videos/%s_%s.mp4", pFilename, aTimestamp);
3681 }
3682 else
3683 {
3684 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "videos/%s.mp4", pFilename);
3685 }
3686
3687 // wait for idle, so there is no data race
3688 Graphics()->WaitForIdle();
3689 // pause the sound device while creating the video instance
3690 Sound()->PauseAudioDevice();
3691 new CVideo(Graphics(), Sound(), Storage(), Graphics()->ScreenWidth(), Graphics()->ScreenHeight(), aFilename);
3692 Sound()->UnpauseAudioDevice();
3693 if(!IVideo::Current()->Start())
3694 {
3695 log_error("videorecorder", "Failed to start recording to '%s'", aFilename);
3696 m_DemoPlayer.Stop(pErrorMessage: "Failed to start video recording. See local console for details.");
3697 return;
3698 }
3699 if(m_DemoPlayer.Info()->m_Info.m_Paused)
3700 {
3701 IVideo::Current()->Pause(Pause: true);
3702 }
3703 log_info("videorecorder", "Recording to '%s'", aFilename);
3704}
3705
3706void CClient::Con_StopVideo(IConsole::IResult *pResult, void *pUserData)
3707{
3708 if(!IVideo::Current())
3709 {
3710 log_error("videorecorder", "Not recording.");
3711 return;
3712 }
3713
3714 IVideo::Current()->Stop();
3715 log_info("videorecorder", "Stopped recording.");
3716}
3717
3718#endif
3719
3720void CClient::Con_Rcon(IConsole::IResult *pResult, void *pUserData)
3721{
3722 CClient *pSelf = (CClient *)pUserData;
3723 pSelf->Rcon(pCmd: pResult->GetString(Index: 0));
3724}
3725
3726void CClient::Con_RconAuth(IConsole::IResult *pResult, void *pUserData)
3727{
3728 CClient *pSelf = (CClient *)pUserData;
3729 pSelf->RconAuth(pName: "", pPassword: pResult->GetString(Index: 0));
3730}
3731
3732void CClient::Con_RconLogin(IConsole::IResult *pResult, void *pUserData)
3733{
3734 CClient *pSelf = (CClient *)pUserData;
3735 pSelf->RconAuth(pName: pResult->GetString(Index: 0), pPassword: pResult->GetString(Index: 1));
3736}
3737
3738void CClient::Con_BeginFavoriteGroup(IConsole::IResult *pResult, void *pUserData)
3739{
3740 CClient *pSelf = (CClient *)pUserData;
3741 if(pSelf->m_FavoritesGroup)
3742 {
3743 log_error("client", "opening favorites group while there is already one, discarding old one");
3744 for(int i = 0; i < pSelf->m_FavoritesGroupNum; i++)
3745 {
3746 char aAddr[NETADDR_MAXSTRSIZE];
3747 net_addr_str(addr: &pSelf->m_aFavoritesGroupAddresses[i], string: aAddr, max_length: sizeof(aAddr), add_port: true);
3748 log_warn("client", "discarding %s", aAddr);
3749 }
3750 }
3751 pSelf->m_FavoritesGroup = true;
3752 pSelf->m_FavoritesGroupAllowPing = false;
3753 pSelf->m_FavoritesGroupNum = 0;
3754}
3755
3756void CClient::Con_EndFavoriteGroup(IConsole::IResult *pResult, void *pUserData)
3757{
3758 CClient *pSelf = (CClient *)pUserData;
3759 if(!pSelf->m_FavoritesGroup)
3760 {
3761 log_error("client", "closing favorites group while there is none, ignoring");
3762 return;
3763 }
3764 log_info("client", "adding group of %d favorites", pSelf->m_FavoritesGroupNum);
3765 pSelf->m_pFavorites->Add(pAddrs: pSelf->m_aFavoritesGroupAddresses, NumAddrs: pSelf->m_FavoritesGroupNum);
3766 if(pSelf->m_FavoritesGroupAllowPing)
3767 {
3768 pSelf->m_pFavorites->AllowPing(pAddrs: pSelf->m_aFavoritesGroupAddresses, NumAddrs: pSelf->m_FavoritesGroupNum, AllowPing: true);
3769 }
3770 pSelf->m_FavoritesGroup = false;
3771}
3772
3773void CClient::Con_AddFavorite(IConsole::IResult *pResult, void *pUserData)
3774{
3775 CClient *pSelf = (CClient *)pUserData;
3776 NETADDR Addr;
3777
3778 if(net_addr_from_url(addr: &Addr, string: pResult->GetString(Index: 0), host_buf: nullptr, host_buf_size: 0) != 0 && net_addr_from_str(addr: &Addr, string: pResult->GetString(Index: 0)) != 0)
3779 {
3780 char aBuf[128];
3781 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "invalid address '%s'", pResult->GetString(Index: 0));
3782 pSelf->m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf);
3783 return;
3784 }
3785 bool AllowPing = pResult->NumArguments() > 1 && str_find(haystack: pResult->GetString(Index: 1), needle: "allow_ping");
3786 char aAddr[NETADDR_MAXSTRSIZE];
3787 net_addr_str(addr: &Addr, string: aAddr, max_length: sizeof(aAddr), add_port: true);
3788 if(pSelf->m_FavoritesGroup)
3789 {
3790 if(pSelf->m_FavoritesGroupNum == (int)std::size(pSelf->m_aFavoritesGroupAddresses))
3791 {
3792 log_error("client", "discarding %s because groups can have at most a size of %d", aAddr, pSelf->m_FavoritesGroupNum);
3793 return;
3794 }
3795 log_info("client", "adding %s to favorites group", aAddr);
3796 pSelf->m_aFavoritesGroupAddresses[pSelf->m_FavoritesGroupNum] = Addr;
3797 pSelf->m_FavoritesGroupAllowPing = pSelf->m_FavoritesGroupAllowPing || AllowPing;
3798 pSelf->m_FavoritesGroupNum += 1;
3799 }
3800 else
3801 {
3802 log_info("client", "adding %s to favorites", aAddr);
3803 pSelf->m_pFavorites->Add(pAddrs: &Addr, NumAddrs: 1);
3804 if(AllowPing)
3805 {
3806 pSelf->m_pFavorites->AllowPing(pAddrs: &Addr, NumAddrs: 1, AllowPing: true);
3807 }
3808 }
3809}
3810
3811void CClient::Con_RemoveFavorite(IConsole::IResult *pResult, void *pUserData)
3812{
3813 CClient *pSelf = (CClient *)pUserData;
3814 NETADDR Addr;
3815 if(net_addr_from_str(addr: &Addr, string: pResult->GetString(Index: 0)) == 0)
3816 pSelf->m_pFavorites->Remove(pAddrs: &Addr, NumAddrs: 1);
3817}
3818
3819void CClient::DemoSliceBegin()
3820{
3821 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
3822 g_Config.m_ClDemoSliceBegin = pInfo->m_Info.m_CurrentTick;
3823}
3824
3825void CClient::DemoSliceEnd()
3826{
3827 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
3828 g_Config.m_ClDemoSliceEnd = pInfo->m_Info.m_CurrentTick;
3829}
3830
3831void CClient::Con_DemoSliceBegin(IConsole::IResult *pResult, void *pUserData)
3832{
3833 CClient *pSelf = (CClient *)pUserData;
3834 pSelf->DemoSliceBegin();
3835}
3836
3837void CClient::Con_DemoSliceEnd(IConsole::IResult *pResult, void *pUserData)
3838{
3839 CClient *pSelf = (CClient *)pUserData;
3840 pSelf->DemoSliceEnd();
3841}
3842
3843void CClient::Con_SaveReplay(IConsole::IResult *pResult, void *pUserData)
3844{
3845 CClient *pSelf = (CClient *)pUserData;
3846 if(pResult->NumArguments())
3847 {
3848 int Length = pResult->GetInteger(Index: 0);
3849 if(Length <= 0)
3850 pSelf->m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: length must be greater than 0 second.");
3851 else
3852 {
3853 if(pResult->NumArguments() >= 2)
3854 pSelf->SaveReplay(Length, pFilename: pResult->GetString(Index: 1));
3855 else
3856 pSelf->SaveReplay(Length);
3857 }
3858 }
3859 else
3860 pSelf->SaveReplay(Length: g_Config.m_ClReplayLength);
3861}
3862
3863void CClient::SaveReplay(const int Length, const char *pFilename)
3864{
3865 if(!g_Config.m_ClReplays)
3866 {
3867 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "Feature is disabled. Please enable it via configuration.");
3868 GameClient()->Echo(pString: Localize(pStr: "Replay feature is disabled!"));
3869 return;
3870 }
3871
3872 if(!DemoRecorder(Recorder: RECORDER_REPLAYS)->IsRecording())
3873 {
3874 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: demorecorder isn't recording. Try to rejoin to fix that.");
3875 }
3876 else if(DemoRecorder(Recorder: RECORDER_REPLAYS)->Length() < 1)
3877 {
3878 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: demorecorder isn't recording for at least 1 second.");
3879 }
3880 else
3881 {
3882 char aFilename[IO_MAX_PATH_LENGTH];
3883 if(pFilename[0] == '\0')
3884 {
3885 char aTimestamp[20];
3886 str_timestamp(buffer: aTimestamp, buffer_size: sizeof(aTimestamp));
3887 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/replays/%s_%s_(replay).demo", m_aCurrentMap, aTimestamp);
3888 }
3889 else
3890 {
3891 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/replays/%s.demo", pFilename);
3892 IOHANDLE Handle = m_pStorage->OpenFile(pFilename: aFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
3893 if(!Handle)
3894 {
3895 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: invalid filename. Try a different one!");
3896 return;
3897 }
3898 io_close(io: Handle);
3899 m_pStorage->RemoveFile(pFilename: aFilename, Type: IStorage::TYPE_SAVE);
3900 }
3901
3902 // Stop the recorder to correctly slice the demo after
3903 DemoRecorder(Recorder: RECORDER_REPLAYS)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
3904
3905 // Slice the demo to get only the last cl_replay_length seconds
3906 const char *pSrc = m_aDemoRecorder[RECORDER_REPLAYS].CurrentFilename();
3907 const int EndTick = GameTick(Conn: g_Config.m_ClDummy);
3908 const int StartTick = EndTick - Length * GameTickSpeed();
3909
3910 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "Saving replay...");
3911
3912 // Create a job to do this slicing in background because it can be a bit long depending on the file size
3913 std::shared_ptr<CDemoEdit> pDemoEditTask = std::make_shared<CDemoEdit>(args: GameClient()->NetVersion(), args: &m_SnapshotDelta, args&: m_pStorage, args&: pSrc, args&: aFilename, args: StartTick, args: EndTick);
3914 Engine()->AddJob(pJob: pDemoEditTask);
3915 m_EditJobs.push_back(x: pDemoEditTask);
3916
3917 // And we restart the recorder
3918 DemoRecorder_UpdateReplayRecorder();
3919 }
3920}
3921
3922void CClient::DemoSlice(const char *pDstPath, CLIENTFUNC_FILTER pfnFilter, void *pUser)
3923{
3924 if(m_DemoPlayer.IsPlaying())
3925 {
3926 m_DemoEditor.Slice(pDemo: m_DemoPlayer.Filename(), pDst: pDstPath, StartTick: g_Config.m_ClDemoSliceBegin, EndTick: g_Config.m_ClDemoSliceEnd, pfnFilter, pUser);
3927 }
3928}
3929
3930const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType)
3931{
3932 // Don't disconnect unless the file exists (only for play command)
3933 if(!Storage()->FileExists(pFilename, Type: StorageType))
3934 return Localize(pStr: "No demo with this filename exists");
3935
3936 Disconnect();
3937 m_aNetClient[CONN_MAIN].ResetErrorString();
3938
3939 SetState(IClient::STATE_LOADING);
3940 SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_DEMO);
3941 if((bool)m_LoadingCallback)
3942 m_LoadingCallback(IClient::LOADING_CALLBACK_DETAIL_DEMO);
3943
3944 // try to start playback
3945 m_DemoPlayer.SetListener(this);
3946 if(m_DemoPlayer.Load(pStorage: Storage(), pConsole: m_pConsole, pFilename, StorageType))
3947 {
3948 DisconnectWithReason(pReason: m_DemoPlayer.ErrorMessage());
3949 return m_DemoPlayer.ErrorMessage();
3950 }
3951
3952 m_Sixup = m_DemoPlayer.IsSixup();
3953
3954 // load map
3955 const CMapInfo *pMapInfo = m_DemoPlayer.GetMapInfo();
3956 const char *pError = LoadMapSearch(pMapName: pMapInfo->m_aName, WantedSha256: pMapInfo->m_Sha256, WantedCrc: pMapInfo->m_Crc);
3957 if(pError)
3958 {
3959 if(!m_DemoPlayer.ExtractMap(pStorage: Storage()))
3960 {
3961 DisconnectWithReason(pReason: pError);
3962 return pError;
3963 }
3964
3965 pError = LoadMapSearch(pMapName: pMapInfo->m_aName, WantedSha256: pMapInfo->m_Sha256, WantedCrc: pMapInfo->m_Crc);
3966 if(pError)
3967 {
3968 DisconnectWithReason(pReason: pError);
3969 return pError;
3970 }
3971 }
3972
3973 // setup current server info
3974 mem_zero(block: &m_CurrentServerInfo, size: sizeof(m_CurrentServerInfo));
3975 str_copy(dst&: m_CurrentServerInfo.m_aMap, src: pMapInfo->m_aName);
3976 m_CurrentServerInfo.m_MapCrc = pMapInfo->m_Crc;
3977 m_CurrentServerInfo.m_MapSize = pMapInfo->m_Size;
3978
3979 // enter demo playback state
3980 SetState(IClient::STATE_DEMOPLAYBACK);
3981
3982 GameClient()->OnConnected();
3983
3984 // setup buffers
3985 mem_zero(block: m_aaaDemorecSnapshotData, size: sizeof(m_aaaDemorecSnapshotData));
3986
3987 for(int SnapshotType = 0; SnapshotType < NUM_SNAPSHOT_TYPES; SnapshotType++)
3988 {
3989 m_aapSnapshots[0][SnapshotType] = &m_aDemorecSnapshotHolders[SnapshotType];
3990 m_aapSnapshots[0][SnapshotType]->m_pSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][0];
3991 m_aapSnapshots[0][SnapshotType]->m_pAltSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][1];
3992 m_aapSnapshots[0][SnapshotType]->m_SnapSize = 0;
3993 m_aapSnapshots[0][SnapshotType]->m_AltSnapSize = 0;
3994 m_aapSnapshots[0][SnapshotType]->m_Tick = -1;
3995 }
3996
3997 m_DemoPlayer.Play();
3998 GameClient()->OnEnterGame();
3999
4000 return nullptr;
4001}
4002
4003#if defined(CONF_VIDEORECORDER)
4004const char *CClient::DemoPlayer_Render(const char *pFilename, int StorageType, const char *pVideoName, int SpeedIndex, bool StartPaused)
4005{
4006 const char *pError = DemoPlayer_Play(pFilename, StorageType);
4007 if(pError)
4008 return pError;
4009
4010 StartVideo(pFilename: pVideoName, WithTimestamp: false);
4011 m_DemoPlayer.SetSpeedIndex(SpeedIndex);
4012 if(StartPaused)
4013 {
4014 m_DemoPlayer.Pause();
4015 }
4016 return nullptr;
4017}
4018#endif
4019
4020void CClient::Con_Play(IConsole::IResult *pResult, void *pUserData)
4021{
4022 CClient *pSelf = (CClient *)pUserData;
4023 pSelf->HandleDemoPath(pPath: pResult->GetString(Index: 0));
4024}
4025
4026void CClient::Con_DemoPlay(IConsole::IResult *pResult, void *pUserData)
4027{
4028 CClient *pSelf = (CClient *)pUserData;
4029 if(pSelf->m_DemoPlayer.IsPlaying())
4030 {
4031 if(pSelf->m_DemoPlayer.BaseInfo()->m_Paused)
4032 {
4033 pSelf->m_DemoPlayer.Unpause();
4034 }
4035 else
4036 {
4037 pSelf->m_DemoPlayer.Pause();
4038 }
4039 }
4040}
4041
4042void CClient::Con_DemoSpeed(IConsole::IResult *pResult, void *pUserData)
4043{
4044 CClient *pSelf = (CClient *)pUserData;
4045 pSelf->m_DemoPlayer.SetSpeed(pResult->GetFloat(Index: 0));
4046}
4047
4048void CClient::DemoRecorder_Start(const char *pFilename, bool WithTimestamp, int Recorder)
4049{
4050 dbg_assert(State() == IClient::STATE_ONLINE, "Client must be online to record demo");
4051
4052 char aFilename[IO_MAX_PATH_LENGTH];
4053 if(WithTimestamp)
4054 {
4055 char aTimestamp[20];
4056 str_timestamp(buffer: aTimestamp, buffer_size: sizeof(aTimestamp));
4057 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/%s_%s.demo", pFilename, aTimestamp);
4058 }
4059 else
4060 {
4061 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/%s.demo", pFilename);
4062 }
4063
4064 m_aDemoRecorder[Recorder].Start(
4065 pStorage: Storage(),
4066 pConsole: m_pConsole,
4067 pFilename: aFilename,
4068 pNetversion: IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(),
4069 pMap: m_aCurrentMap,
4070 Sha256: m_pMap->Sha256(),
4071 MapCrc: m_pMap->Crc(),
4072 pType: "client",
4073 MapSize: m_pMap->Size(),
4074 pMapData: nullptr,
4075 MapFile: m_pMap->File(),
4076 pfnFilter: nullptr,
4077 pUser: nullptr);
4078}
4079
4080void CClient::DemoRecorder_HandleAutoStart()
4081{
4082 if(g_Config.m_ClAutoDemoRecord)
4083 {
4084 DemoRecorder(Recorder: RECORDER_AUTO)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
4085
4086 char aFilename[IO_MAX_PATH_LENGTH];
4087 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "auto/%s", m_aCurrentMap);
4088 DemoRecorder_Start(pFilename: aFilename, WithTimestamp: true, Recorder: RECORDER_AUTO);
4089
4090 if(g_Config.m_ClAutoDemoMax)
4091 {
4092 // clean up auto recorded demos
4093 CFileCollection AutoDemos;
4094 AutoDemos.Init(pStorage: Storage(), pPath: "demos/auto", pFileDesc: "" /* empty for wild card */, pFileExt: ".demo", MaxEntries: g_Config.m_ClAutoDemoMax);
4095 }
4096 }
4097
4098 DemoRecorder_UpdateReplayRecorder();
4099}
4100
4101void CClient::DemoRecorder_UpdateReplayRecorder()
4102{
4103 if(!g_Config.m_ClReplays && DemoRecorder(Recorder: RECORDER_REPLAYS)->IsRecording())
4104 {
4105 DemoRecorder(Recorder: RECORDER_REPLAYS)->Stop(Mode: IDemoRecorder::EStopMode::REMOVE_FILE);
4106 }
4107
4108 if(g_Config.m_ClReplays && !DemoRecorder(Recorder: RECORDER_REPLAYS)->IsRecording())
4109 {
4110 char aFilename[IO_MAX_PATH_LENGTH];
4111 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "replays/replay_tmp_%s", m_aCurrentMap);
4112 DemoRecorder_Start(pFilename: aFilename, WithTimestamp: true, Recorder: RECORDER_REPLAYS);
4113 }
4114}
4115
4116void CClient::DemoRecorder_AddDemoMarker(int Recorder)
4117{
4118 m_aDemoRecorder[Recorder].AddDemoMarker();
4119}
4120
4121class IDemoRecorder *CClient::DemoRecorder(int Recorder)
4122{
4123 return &m_aDemoRecorder[Recorder];
4124}
4125
4126void CClient::Con_Record(IConsole::IResult *pResult, void *pUserData)
4127{
4128 CClient *pSelf = (CClient *)pUserData;
4129
4130 if(pSelf->State() != IClient::STATE_ONLINE)
4131 {
4132 log_error("demo_recorder", "Client is not online.");
4133 return;
4134 }
4135 if(pSelf->m_aDemoRecorder[RECORDER_MANUAL].IsRecording())
4136 {
4137 log_error("demo_recorder", "Demo recorder already recording to '%s'.", pSelf->m_aDemoRecorder[RECORDER_MANUAL].CurrentFilename());
4138 return;
4139 }
4140
4141 if(pResult->NumArguments())
4142 pSelf->DemoRecorder_Start(pFilename: pResult->GetString(Index: 0), WithTimestamp: false, Recorder: RECORDER_MANUAL);
4143 else
4144 pSelf->DemoRecorder_Start(pFilename: pSelf->m_aCurrentMap, WithTimestamp: true, Recorder: RECORDER_MANUAL);
4145}
4146
4147void CClient::Con_StopRecord(IConsole::IResult *pResult, void *pUserData)
4148{
4149 CClient *pSelf = (CClient *)pUserData;
4150 pSelf->DemoRecorder(Recorder: RECORDER_MANUAL)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
4151}
4152
4153void CClient::Con_AddDemoMarker(IConsole::IResult *pResult, void *pUserData)
4154{
4155 CClient *pSelf = (CClient *)pUserData;
4156 for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++)
4157 pSelf->DemoRecorder_AddDemoMarker(Recorder);
4158}
4159
4160void CClient::Con_BenchmarkQuit(IConsole::IResult *pResult, void *pUserData)
4161{
4162 CClient *pSelf = (CClient *)pUserData;
4163 int Seconds = pResult->GetInteger(Index: 0);
4164 const char *pFilename = pResult->GetString(Index: 1);
4165 pSelf->BenchmarkQuit(Seconds, pFilename);
4166}
4167
4168void CClient::BenchmarkQuit(int Seconds, const char *pFilename)
4169{
4170 m_BenchmarkFile = Storage()->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_ABSOLUTE);
4171 m_BenchmarkStopTime = time_get() + time_freq() * Seconds;
4172}
4173
4174void CClient::UpdateAndSwap()
4175{
4176 Input()->Update();
4177 Graphics()->Swap();
4178 Graphics()->Clear(r: 0, g: 0, b: 0);
4179 m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq();
4180}
4181
4182void CClient::ServerBrowserUpdate()
4183{
4184 m_ServerBrowser.RequestResort();
4185}
4186
4187void CClient::ConchainServerBrowserUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4188{
4189 pfnCallback(pResult, pCallbackUserData);
4190 if(pResult->NumArguments())
4191 ((CClient *)pUserData)->ServerBrowserUpdate();
4192}
4193
4194void CClient::InitChecksum()
4195{
4196 CChecksumData *pData = &m_Checksum.m_Data;
4197 pData->m_SizeofData = sizeof(*pData);
4198 str_copy(dst&: pData->m_aVersionStr, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")");
4199 pData->m_Start = time_get();
4200 os_version_str(version: pData->m_aOsVersion, length: sizeof(pData->m_aOsVersion));
4201 secure_random_fill(bytes: &pData->m_Random, length: sizeof(pData->m_Random));
4202 pData->m_Version = GameClient()->DDNetVersion();
4203 pData->m_SizeofClient = sizeof(*this);
4204 pData->m_SizeofConfig = sizeof(pData->m_Config);
4205 pData->InitFiles();
4206}
4207
4208#ifndef DDNET_CHECKSUM_SALT
4209// salt@checksum.ddnet.tw: db877f2b-2ddb-3ba6-9f67-a6d169ec671d
4210#define DDNET_CHECKSUM_SALT \
4211 { \
4212 { \
4213 0xdb, 0x87, 0x7f, 0x2b, 0x2d, 0xdb, 0x3b, 0xa6, \
4214 0x9f, 0x67, 0xa6, 0xd1, 0x69, 0xec, 0x67, 0x1d, \
4215 } \
4216 }
4217#endif
4218
4219int CClient::HandleChecksum(int Conn, CUuid Uuid, CUnpacker *pUnpacker)
4220{
4221 int Start = pUnpacker->GetInt();
4222 int Length = pUnpacker->GetInt();
4223 if(pUnpacker->Error())
4224 {
4225 return 1;
4226 }
4227 if(Start < 0 || Length < 0 || Start > std::numeric_limits<int>::max() - Length)
4228 {
4229 return 2;
4230 }
4231 int End = Start + Length;
4232 int ChecksumBytesEnd = minimum(a: End, b: (int)sizeof(m_Checksum.m_aBytes));
4233 int FileStart = maximum(a: Start, b: (int)sizeof(m_Checksum.m_aBytes));
4234 unsigned char aStartBytes[sizeof(int32_t)];
4235 unsigned char aEndBytes[sizeof(int32_t)];
4236 uint_to_bytes_be(bytes: aStartBytes, value: Start);
4237 uint_to_bytes_be(bytes: aEndBytes, value: End);
4238
4239 if(Start <= (int)sizeof(m_Checksum.m_aBytes))
4240 {
4241 mem_zero(block: &m_Checksum.m_Data.m_Config, size: sizeof(m_Checksum.m_Data.m_Config));
4242#define CHECKSUM_RECORD(Flags) (((Flags) & CFGFLAG_CLIENT) == 0 || ((Flags) & CFGFLAG_INSENSITIVE) != 0)
4243#define MACRO_CONFIG_INT(Name, ScriptName, Def, Min, Max, Flags, Desc) \
4244 if(CHECKSUM_RECORD(Flags)) \
4245 { \
4246 m_Checksum.m_Data.m_Config.m_##Name = g_Config.m_##Name; \
4247 }
4248#define MACRO_CONFIG_COL(Name, ScriptName, Def, Flags, Desc) \
4249 if(CHECKSUM_RECORD(Flags)) \
4250 { \
4251 m_Checksum.m_Data.m_Config.m_##Name = g_Config.m_##Name; \
4252 }
4253#define MACRO_CONFIG_STR(Name, ScriptName, Len, Def, Flags, Desc) \
4254 if(CHECKSUM_RECORD(Flags)) \
4255 { \
4256 str_copy(m_Checksum.m_Data.m_Config.m_##Name, g_Config.m_##Name, sizeof(m_Checksum.m_Data.m_Config.m_##Name)); \
4257 }
4258#include <engine/shared/config_variables.h>
4259#undef CHECKSUM_RECORD
4260#undef MACRO_CONFIG_INT
4261#undef MACRO_CONFIG_COL
4262#undef MACRO_CONFIG_STR
4263 }
4264 if(End > (int)sizeof(m_Checksum.m_aBytes))
4265 {
4266 if(m_OwnExecutableSize == 0)
4267 {
4268 m_OwnExecutable = io_current_exe();
4269 // io_length returns -1 on error.
4270 m_OwnExecutableSize = m_OwnExecutable ? io_length(io: m_OwnExecutable) : -1;
4271 }
4272 // Own executable not available.
4273 if(m_OwnExecutableSize < 0)
4274 {
4275 return 3;
4276 }
4277 if(End - (int)sizeof(m_Checksum.m_aBytes) > m_OwnExecutableSize)
4278 {
4279 return 4;
4280 }
4281 }
4282
4283 SHA256_CTX Sha256Ctxt;
4284 sha256_init(ctxt: &Sha256Ctxt);
4285 CUuid Salt = DDNET_CHECKSUM_SALT;
4286 sha256_update(ctxt: &Sha256Ctxt, data: &Salt, data_len: sizeof(Salt));
4287 sha256_update(ctxt: &Sha256Ctxt, data: &Uuid, data_len: sizeof(Uuid));
4288 sha256_update(ctxt: &Sha256Ctxt, data: aStartBytes, data_len: sizeof(aStartBytes));
4289 sha256_update(ctxt: &Sha256Ctxt, data: aEndBytes, data_len: sizeof(aEndBytes));
4290 if(Start < (int)sizeof(m_Checksum.m_aBytes))
4291 {
4292 sha256_update(ctxt: &Sha256Ctxt, data: m_Checksum.m_aBytes + Start, data_len: ChecksumBytesEnd - Start);
4293 }
4294 if(End > (int)sizeof(m_Checksum.m_aBytes))
4295 {
4296 unsigned char aBuf[2048];
4297 if(io_seek(io: m_OwnExecutable, offset: FileStart - sizeof(m_Checksum.m_aBytes), origin: IOSEEK_START))
4298 {
4299 return 5;
4300 }
4301 for(int i = FileStart; i < End; i += sizeof(aBuf))
4302 {
4303 int Read = io_read(io: m_OwnExecutable, buffer: aBuf, size: minimum(a: (int)sizeof(aBuf), b: End - i));
4304 sha256_update(ctxt: &Sha256Ctxt, data: aBuf, data_len: Read);
4305 }
4306 }
4307 SHA256_DIGEST Sha256 = sha256_finish(ctxt: &Sha256Ctxt);
4308
4309 CMsgPacker Msg(NETMSG_CHECKSUM_RESPONSE, true);
4310 Msg.AddRaw(pData: &Uuid, Size: sizeof(Uuid));
4311 Msg.AddRaw(pData: &Sha256, Size: sizeof(Sha256));
4312 SendMsg(Conn, pMsg: &Msg, Flags: MSGFLAG_VITAL);
4313
4314 return 0;
4315}
4316
4317void CClient::ConchainWindowScreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4318{
4319 CClient *pSelf = (CClient *)pUserData;
4320 if(pSelf->Graphics() && pResult->NumArguments())
4321 {
4322 if(g_Config.m_GfxScreen != pResult->GetInteger(Index: 0))
4323 pSelf->Graphics()->SwitchWindowScreen(Index: pResult->GetInteger(Index: 0), MoveToCenter: true);
4324 }
4325 else
4326 pfnCallback(pResult, pCallbackUserData);
4327}
4328
4329void CClient::ConchainFullscreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4330{
4331 CClient *pSelf = (CClient *)pUserData;
4332 if(pSelf->Graphics() && pResult->NumArguments())
4333 {
4334 if(g_Config.m_GfxFullscreen != pResult->GetInteger(Index: 0))
4335 pSelf->Graphics()->SetWindowParams(FullscreenMode: pResult->GetInteger(Index: 0), IsBorderless: g_Config.m_GfxBorderless);
4336 }
4337 else
4338 pfnCallback(pResult, pCallbackUserData);
4339}
4340
4341void CClient::ConchainWindowBordered(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4342{
4343 CClient *pSelf = (CClient *)pUserData;
4344 if(pSelf->Graphics() && pResult->NumArguments())
4345 {
4346 if(!g_Config.m_GfxFullscreen && (g_Config.m_GfxBorderless != pResult->GetInteger(Index: 0)))
4347 pSelf->Graphics()->SetWindowParams(FullscreenMode: g_Config.m_GfxFullscreen, IsBorderless: !g_Config.m_GfxBorderless);
4348 }
4349 else
4350 pfnCallback(pResult, pCallbackUserData);
4351}
4352
4353void CClient::Notify(const char *pTitle, const char *pMessage)
4354{
4355 if(m_pGraphics->WindowActive() || !g_Config.m_ClShowNotifications)
4356 return;
4357
4358 Notifications()->Notify(pTitle, pMessage);
4359 Graphics()->NotifyWindow();
4360}
4361
4362void CClient::OnWindowResize()
4363{
4364 TextRender()->OnPreWindowResize();
4365 GameClient()->OnWindowResize();
4366 m_pEditor->OnWindowResize();
4367 TextRender()->OnWindowResize();
4368}
4369
4370void CClient::ConchainWindowVSync(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4371{
4372 CClient *pSelf = (CClient *)pUserData;
4373 if(pSelf->Graphics() && pResult->NumArguments())
4374 {
4375 if(g_Config.m_GfxVsync != pResult->GetInteger(Index: 0))
4376 pSelf->Graphics()->SetVSync(pResult->GetInteger(Index: 0));
4377 }
4378 else
4379 pfnCallback(pResult, pCallbackUserData);
4380}
4381
4382void CClient::ConchainWindowResize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4383{
4384 CClient *pSelf = (CClient *)pUserData;
4385 pfnCallback(pResult, pCallbackUserData);
4386 if(pSelf->Graphics() && pResult->NumArguments())
4387 {
4388 pSelf->Graphics()->ResizeToScreen();
4389 }
4390}
4391
4392void CClient::ConchainTimeoutSeed(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4393{
4394 CClient *pSelf = (CClient *)pUserData;
4395 pfnCallback(pResult, pCallbackUserData);
4396 if(pResult->NumArguments())
4397 pSelf->m_GenerateTimeoutSeed = false;
4398}
4399
4400void CClient::ConchainPassword(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4401{
4402 CClient *pSelf = (CClient *)pUserData;
4403 pfnCallback(pResult, pCallbackUserData);
4404 if(pResult->NumArguments() && pSelf->m_LocalStartTime) //won't set m_SendPassword before game has started
4405 pSelf->m_SendPassword = true;
4406}
4407
4408void CClient::ConchainReplays(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4409{
4410 CClient *pSelf = (CClient *)pUserData;
4411 pfnCallback(pResult, pCallbackUserData);
4412 if(pResult->NumArguments() && pSelf->State() == IClient::STATE_ONLINE)
4413 {
4414 pSelf->DemoRecorder_UpdateReplayRecorder();
4415 }
4416}
4417
4418void CClient::ConchainInputFifo(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4419{
4420 CClient *pSelf = (CClient *)pUserData;
4421 pfnCallback(pResult, pCallbackUserData);
4422 if(pSelf->m_Fifo.IsInit())
4423 {
4424 pSelf->m_Fifo.Shutdown();
4425 pSelf->m_Fifo.Init(pConsole: pSelf->m_pConsole, pFifoFile: pSelf->Config()->m_ClInputFifo, Flag: CFGFLAG_CLIENT);
4426 }
4427}
4428
4429void CClient::ConchainNetReset(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4430{
4431 CClient *pSelf = (CClient *)pUserData;
4432 pfnCallback(pResult, pCallbackUserData);
4433 if(pResult->NumArguments())
4434 pSelf->ResetSocket();
4435}
4436
4437void CClient::ConchainLoglevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4438{
4439 CClient *pSelf = (CClient *)pUserData;
4440 pfnCallback(pResult, pCallbackUserData);
4441 if(pResult->NumArguments())
4442 {
4443 pSelf->m_pFileLogger->SetFilter(CLogFilter{.m_MaxLevel: IConsole::ToLogLevelFilter(ConsoleLevel: g_Config.m_Loglevel)});
4444 }
4445}
4446
4447void CClient::ConchainStdoutOutputLevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4448{
4449 CClient *pSelf = (CClient *)pUserData;
4450 pfnCallback(pResult, pCallbackUserData);
4451 if(pResult->NumArguments() && pSelf->m_pStdoutLogger)
4452 {
4453 pSelf->m_pStdoutLogger->SetFilter(CLogFilter{.m_MaxLevel: IConsole::ToLogLevelFilter(ConsoleLevel: g_Config.m_StdoutOutputLevel)});
4454 }
4455}
4456
4457void CClient::RegisterCommands()
4458{
4459 m_pConsole = Kernel()->RequestInterface<IConsole>();
4460
4461 m_pConsole->Register(pName: "dummy_connect", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DummyConnect, pUser: this, pHelp: "Connect dummy");
4462 m_pConsole->Register(pName: "dummy_disconnect", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DummyDisconnect, pUser: this, pHelp: "Disconnect dummy");
4463 m_pConsole->Register(pName: "dummy_reset", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DummyResetInput, pUser: this, pHelp: "Reset dummy");
4464
4465 m_pConsole->Register(pName: "quit", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Quit, pUser: this, pHelp: "Quit the client");
4466 m_pConsole->Register(pName: "exit", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Quit, pUser: this, pHelp: "Quit the client");
4467 m_pConsole->Register(pName: "restart", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Restart, pUser: this, pHelp: "Restart the client");
4468 m_pConsole->Register(pName: "minimize", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Minimize, pUser: this, pHelp: "Minimize the client");
4469 m_pConsole->Register(pName: "connect", pParams: "r[host|ip]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Connect, pUser: this, pHelp: "Connect to the specified host/ip");
4470 m_pConsole->Register(pName: "disconnect", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Disconnect, pUser: this, pHelp: "Disconnect from the server");
4471 m_pConsole->Register(pName: "ping", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Ping, pUser: this, pHelp: "Ping the current server");
4472 m_pConsole->Register(pName: "screenshot", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Screenshot, pUser: this, pHelp: "Take a screenshot");
4473 m_pConsole->Register(pName: "net_reset", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConNetReset, pUser: this, pHelp: "Rebinds the client's listening address and port");
4474
4475#if defined(CONF_VIDEORECORDER)
4476 m_pConsole->Register(pName: "start_video", pParams: "?r[file]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_StartVideo, pUser: this, pHelp: "Start recording a video");
4477 m_pConsole->Register(pName: "stop_video", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_StopVideo, pUser: this, pHelp: "Stop recording a video");
4478#endif
4479
4480 m_pConsole->Register(pName: "rcon", pParams: "r[rcon-command]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Rcon, pUser: this, pHelp: "Send specified command to rcon");
4481 m_pConsole->Register(pName: "rcon_auth", pParams: "r[password]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RconAuth, pUser: this, pHelp: "Authenticate to rcon");
4482 m_pConsole->Register(pName: "rcon_login", pParams: "s[username] r[password]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RconLogin, pUser: this, pHelp: "Authenticate to rcon with a username");
4483 m_pConsole->Register(pName: "play", pParams: "r[file]", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Play, pUser: this, pHelp: "Play back a demo");
4484 m_pConsole->Register(pName: "record", pParams: "?r[file]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Record, pUser: this, pHelp: "Start recording a demo");
4485 m_pConsole->Register(pName: "stoprecord", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_StopRecord, pUser: this, pHelp: "Stop recording a demo");
4486 m_pConsole->Register(pName: "add_demomarker", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_AddDemoMarker, pUser: this, pHelp: "Add demo timeline marker");
4487 m_pConsole->Register(pName: "begin_favorite_group", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_BeginFavoriteGroup, pUser: this, pHelp: "Use this before `add_favorite` to group favorites. End with `end_favorite_group`");
4488 m_pConsole->Register(pName: "end_favorite_group", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_EndFavoriteGroup, pUser: this, pHelp: "Use this after `add_favorite` to group favorites. Start with `begin_favorite_group`");
4489 m_pConsole->Register(pName: "add_favorite", pParams: "s[host|ip] ?s['allow_ping']", Flags: CFGFLAG_CLIENT, pfnFunc: Con_AddFavorite, pUser: this, pHelp: "Add a server as a favorite");
4490 m_pConsole->Register(pName: "remove_favorite", pParams: "r[host|ip]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RemoveFavorite, pUser: this, pHelp: "Remove a server from favorites");
4491 m_pConsole->Register(pName: "demo_slice_start", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoSliceBegin, pUser: this, pHelp: "Mark the beginning of a demo cut");
4492 m_pConsole->Register(pName: "demo_slice_end", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoSliceEnd, pUser: this, pHelp: "Mark the end of a demo cut");
4493 m_pConsole->Register(pName: "demo_play", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoPlay, pUser: this, pHelp: "Play/pause the current demo");
4494 m_pConsole->Register(pName: "demo_speed", pParams: "f[speed]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoSpeed, pUser: this, pHelp: "Set current demo speed");
4495
4496 m_pConsole->Register(pName: "save_replay", pParams: "?i[length] ?r[filename]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_SaveReplay, pUser: this, pHelp: "Save a replay of the last defined amount of seconds");
4497 m_pConsole->Register(pName: "benchmark_quit", pParams: "i[seconds] r[file]", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_BenchmarkQuit, pUser: this, pHelp: "Benchmark frame times for number of seconds to file, then quit");
4498
4499 RustVersionRegister(console&: *m_pConsole);
4500
4501 m_pConsole->Chain(pName: "cl_timeout_seed", pfnChainFunc: ConchainTimeoutSeed, pUser: this);
4502 m_pConsole->Chain(pName: "cl_replays", pfnChainFunc: ConchainReplays, pUser: this);
4503 m_pConsole->Chain(pName: "cl_input_fifo", pfnChainFunc: ConchainInputFifo, pUser: this);
4504 m_pConsole->Chain(pName: "cl_port", pfnChainFunc: ConchainNetReset, pUser: this);
4505 m_pConsole->Chain(pName: "cl_dummy_port", pfnChainFunc: ConchainNetReset, pUser: this);
4506 m_pConsole->Chain(pName: "cl_contact_port", pfnChainFunc: ConchainNetReset, pUser: this);
4507 m_pConsole->Chain(pName: "bindaddr", pfnChainFunc: ConchainNetReset, pUser: this);
4508
4509 m_pConsole->Chain(pName: "password", pfnChainFunc: ConchainPassword, pUser: this);
4510
4511 // used for server browser update
4512 m_pConsole->Chain(pName: "br_filter_string", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4513 m_pConsole->Chain(pName: "br_exclude_string", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4514 m_pConsole->Chain(pName: "br_filter_full", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4515 m_pConsole->Chain(pName: "br_filter_empty", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4516 m_pConsole->Chain(pName: "br_filter_spectators", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4517 m_pConsole->Chain(pName: "br_filter_friends", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4518 m_pConsole->Chain(pName: "br_filter_country", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4519 m_pConsole->Chain(pName: "br_filter_country_index", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4520 m_pConsole->Chain(pName: "br_filter_pw", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4521 m_pConsole->Chain(pName: "br_filter_gametype", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4522 m_pConsole->Chain(pName: "br_filter_gametype_strict", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4523 m_pConsole->Chain(pName: "br_filter_connecting_players", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4524 m_pConsole->Chain(pName: "br_filter_serveraddress", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4525 m_pConsole->Chain(pName: "br_filter_unfinished_map", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4526 m_pConsole->Chain(pName: "br_filter_login", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4527 m_pConsole->Chain(pName: "add_favorite", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4528 m_pConsole->Chain(pName: "remove_favorite", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4529 m_pConsole->Chain(pName: "end_favorite_group", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4530
4531 m_pConsole->Chain(pName: "gfx_screen", pfnChainFunc: ConchainWindowScreen, pUser: this);
4532 m_pConsole->Chain(pName: "gfx_screen_width", pfnChainFunc: ConchainWindowResize, pUser: this);
4533 m_pConsole->Chain(pName: "gfx_screen_height", pfnChainFunc: ConchainWindowResize, pUser: this);
4534 m_pConsole->Chain(pName: "gfx_screen_refresh_rate", pfnChainFunc: ConchainWindowResize, pUser: this);
4535 m_pConsole->Chain(pName: "gfx_fullscreen", pfnChainFunc: ConchainFullscreen, pUser: this);
4536 m_pConsole->Chain(pName: "gfx_borderless", pfnChainFunc: ConchainWindowBordered, pUser: this);
4537 m_pConsole->Chain(pName: "gfx_vsync", pfnChainFunc: ConchainWindowVSync, pUser: this);
4538
4539 m_pConsole->Chain(pName: "loglevel", pfnChainFunc: ConchainLoglevel, pUser: this);
4540 m_pConsole->Chain(pName: "stdout_output_level", pfnChainFunc: ConchainStdoutOutputLevel, pUser: this);
4541}
4542
4543static CClient *CreateClient()
4544{
4545 return new CClient;
4546}
4547
4548void CClient::HandleConnectAddress(const NETADDR *pAddr)
4549{
4550 net_addr_str(addr: pAddr, string: m_aCmdConnect, max_length: sizeof(m_aCmdConnect), add_port: true);
4551}
4552
4553void CClient::HandleConnectLink(const char *pLink)
4554{
4555 // Chrome works fine with ddnet:// but not with ddnet:
4556 // Check ddnet:// before ddnet: because we don't want the // as part of connect command
4557 const char *pConnectLink = nullptr;
4558 if((pConnectLink = str_startswith(str: pLink, CONNECTLINK_DOUBLE_SLASH)))
4559 str_copy(dst&: m_aCmdConnect, src: pConnectLink);
4560 else if((pConnectLink = str_startswith(str: pLink, CONNECTLINK_NO_SLASH)))
4561 str_copy(dst&: m_aCmdConnect, src: pConnectLink);
4562 else
4563 str_copy(dst&: m_aCmdConnect, src: pLink);
4564 // Edge appends / to the URL
4565 const int Length = str_length(str: m_aCmdConnect);
4566 if(m_aCmdConnect[Length - 1] == '/')
4567 m_aCmdConnect[Length - 1] = '\0';
4568}
4569
4570void CClient::HandleDemoPath(const char *pPath)
4571{
4572 str_copy(dst&: m_aCmdPlayDemo, src: pPath);
4573}
4574
4575void CClient::HandleMapPath(const char *pPath)
4576{
4577 str_copy(dst&: m_aCmdEditMap, src: pPath);
4578}
4579
4580static bool UnknownArgumentCallback(const char *pCommand, void *pUser)
4581{
4582 CClient *pClient = static_cast<CClient *>(pUser);
4583 if(str_startswith(str: pCommand, CONNECTLINK_NO_SLASH))
4584 {
4585 pClient->HandleConnectLink(pLink: pCommand);
4586 return true;
4587 }
4588 else if(str_endswith(str: pCommand, suffix: ".demo"))
4589 {
4590 pClient->HandleDemoPath(pPath: pCommand);
4591 return true;
4592 }
4593 else if(str_endswith(str: pCommand, suffix: ".map"))
4594 {
4595 pClient->HandleMapPath(pPath: pCommand);
4596 return true;
4597 }
4598 return false;
4599}
4600
4601static bool SaveUnknownCommandCallback(const char *pCommand, void *pUser)
4602{
4603 CClient *pClient = static_cast<CClient *>(pUser);
4604 pClient->ConfigManager()->StoreUnknownCommand(pCommand);
4605 return true;
4606}
4607
4608/*
4609 Server Time
4610 Client Mirror Time
4611 Client Predicted Time
4612
4613 Snapshot Latency
4614 Downstream latency
4615
4616 Prediction Latency
4617 Upstream latency
4618*/
4619
4620#if defined(CONF_PLATFORM_MACOS)
4621extern "C" int TWMain(int argc, const char **argv)
4622#elif defined(CONF_PLATFORM_ANDROID)
4623static int gs_AndroidStarted = false;
4624extern "C" [[gnu::visibility("default")]] int SDL_main(int argc, char *argv[]);
4625int SDL_main(int argc, char *argv2[])
4626#else
4627int main(int argc, const char **argv)
4628#endif
4629{
4630 const int64_t MainStart = time_get();
4631
4632#if defined(CONF_PLATFORM_ANDROID)
4633 const char **argv = const_cast<const char **>(argv2);
4634 // Android might not unload the library from memory, causing globals like gs_AndroidStarted
4635 // not to be initialized correctly when starting the app again.
4636 if(gs_AndroidStarted)
4637 {
4638 ShowMessageBoxWithoutGraphics({.m_pTitle = "Android Error", .m_pMessage = "The app was started, but not closed properly, this causes bugs. Please restart or manually close this task."});
4639 std::exit(0);
4640 }
4641 gs_AndroidStarted = true;
4642#elif defined(CONF_FAMILY_WINDOWS)
4643 CWindowsComLifecycle WindowsComLifecycle(true);
4644#endif
4645 CCmdlineFix CmdlineFix(&argc, &argv);
4646
4647 std::vector<std::shared_ptr<ILogger>> vpLoggers;
4648 std::shared_ptr<ILogger> pStdoutLogger = nullptr;
4649#if defined(CONF_PLATFORM_ANDROID)
4650 pStdoutLogger = std::shared_ptr<ILogger>(log_logger_android());
4651#else
4652 bool Silent = false;
4653 for(int i = 1; i < argc; i++)
4654 {
4655 if(str_comp(a: "-s", b: argv[i]) == 0 || str_comp(a: "--silent", b: argv[i]) == 0)
4656 {
4657 Silent = true;
4658 }
4659 }
4660 if(!Silent)
4661 {
4662 pStdoutLogger = std::shared_ptr<ILogger>(log_logger_stdout());
4663 }
4664#endif
4665 if(pStdoutLogger)
4666 {
4667 vpLoggers.push_back(x: pStdoutLogger);
4668 }
4669 std::shared_ptr<CFutureLogger> pFutureFileLogger = std::make_shared<CFutureLogger>();
4670 vpLoggers.push_back(x: pFutureFileLogger);
4671 std::shared_ptr<CFutureLogger> pFutureConsoleLogger = std::make_shared<CFutureLogger>();
4672 vpLoggers.push_back(x: pFutureConsoleLogger);
4673 std::shared_ptr<CFutureLogger> pFutureAssertionLogger = std::make_shared<CFutureLogger>();
4674 vpLoggers.push_back(x: pFutureAssertionLogger);
4675 log_set_global_logger(logger: log_logger_collection(vpLoggers: std::move(vpLoggers)).release());
4676
4677#if defined(CONF_PLATFORM_ANDROID)
4678 // Initialize Android after logger is available
4679 const char *pAndroidInitError = InitAndroid();
4680 if(pAndroidInitError != nullptr)
4681 {
4682 log_error("android", "%s", pAndroidInitError);
4683 ShowMessageBoxWithoutGraphics({.m_pTitle = "Android Error", .m_pMessage = pAndroidInitError});
4684 std::exit(0);
4685 }
4686#endif
4687
4688 std::stack<std::function<void()>> CleanerFunctions;
4689 std::function<void()> PerformCleanup = [&CleanerFunctions]() mutable {
4690 while(!CleanerFunctions.empty())
4691 {
4692 CleanerFunctions.top()();
4693 CleanerFunctions.pop();
4694 }
4695 };
4696 std::function<void()> PerformFinalCleanup = []() {
4697#if defined(CONF_PLATFORM_ANDROID)
4698 // Forcefully terminate the entire process, to ensure that static variables
4699 // will be initialized correctly when the app is started again after quitting.
4700 // Returning from the main function is not enough, as this only results in the
4701 // native thread terminating, but the Java thread will continue. Java does not
4702 // support unloading libraries once they have been loaded, so all static
4703 // variables will not have their expected initial values anymore when the app
4704 // is started again after quitting. The variable gs_AndroidStarted above is
4705 // used to check that static variables have been initialized properly.
4706 // TODO: This is not the correct way to close an activity on Android, as it
4707 // ignores the activity lifecycle entirely, which may cause issues if
4708 // we ever used any global resources like the camera.
4709 std::exit(0);
4710#elif defined(CONF_PLATFORM_EMSCRIPTEN)
4711 // Hide canvas after client quit as it will be entirely black without visible
4712 // cursor, also blocking view of the console.
4713 EM_ASM({
4714 document.querySelector('#canvas').style.display = 'none';
4715 });
4716#endif
4717 };
4718 std::function<void()> PerformAllCleanup = [PerformCleanup, PerformFinalCleanup]() mutable {
4719 PerformCleanup();
4720 PerformFinalCleanup();
4721 };
4722
4723 // Register SDL for cleanup before creating the kernel and client,
4724 // so SDL is shutdown after kernel and client. Otherwise the client
4725 // may crash when shutting down after SDL is already shutdown.
4726 CleanerFunctions.emplace(args: []() { SDL_Quit(); });
4727
4728 CClient *pClient = CreateClient();
4729 pClient->SetLoggers(pFileLogger: pFutureFileLogger, pStdoutLogger: std::move(pStdoutLogger));
4730
4731 IKernel *pKernel = IKernel::Create();
4732 pKernel->RegisterInterface(pInterface: pClient, Destroy: false);
4733 pClient->RegisterInterfaces();
4734 CleanerFunctions.emplace(args: [pKernel, pClient]() {
4735 // Ensure that the assert handler doesn't use the client/graphics after they've been destroyed
4736 dbg_assert_set_handler(handler: nullptr);
4737 pKernel->Shutdown();
4738 delete pKernel;
4739 delete pClient;
4740 });
4741
4742 const std::thread::id MainThreadId = std::this_thread::get_id();
4743 dbg_assert_set_handler(handler: [MainThreadId, pClient](const char *pMsg) {
4744 if(MainThreadId != std::this_thread::get_id())
4745 return;
4746 char aOsVersionString[128];
4747 if(!os_version_str(version: aOsVersionString, length: sizeof(aOsVersionString)))
4748 {
4749 str_copy(dst&: aOsVersionString, src: "unknown");
4750 }
4751 char aGpuInfo[512];
4752 pClient->GetGpuInfoString(aGpuInfo);
4753 char aMessage[2048];
4754 str_format(buffer: aMessage, buffer_size: sizeof(aMessage),
4755 format: "An assertion error occurred. Please write down or take a screenshot of the following information and report this error.\n"
4756 "Please also share the assert log"
4757#if defined(CONF_CRASHDUMP)
4758 " and crash log"
4759#endif
4760 " which you should find in the 'dumps' folder in your config directory.\n\n"
4761 "%s\n\n"
4762 "Platform: %s (%s)\n"
4763 "Configuration: base"
4764#if defined(CONF_AUTOUPDATE)
4765 " + autoupdate"
4766#endif
4767#if defined(CONF_CRASHDUMP)
4768 " + crashdump"
4769#endif
4770#if defined(CONF_DEBUG)
4771 " + debug"
4772#endif
4773#if defined(CONF_DISCORD)
4774 " + discord"
4775#endif
4776#if defined(CONF_VIDEORECORDER)
4777 " + videorecorder"
4778#endif
4779#if defined(CONF_WEBSOCKETS)
4780 " + websockets"
4781#endif
4782 "\n"
4783 "Game version: %s %s %s\n"
4784 "OS version: %s\n\n"
4785 "%s", // GPU info
4786 pMsg,
4787 CONF_PLATFORM_STRING, CONF_ARCH_ENDIAN_STRING,
4788 GAME_NAME, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH != nullptr ? GIT_SHORTREV_HASH : "",
4789 aOsVersionString,
4790 aGpuInfo);
4791 // Also log all of this information to the assertion log file
4792 log_error("assertion", "%s", aMessage);
4793 std::vector<IGraphics::CMessageBoxButton> vButtons;
4794 // Storage may not have been initialized yet and viewing files is not supported on Android yet
4795#if !defined(CONF_PLATFORM_ANDROID)
4796 if(pClient->Storage() != nullptr)
4797 {
4798 vButtons.push_back(x: {.m_pLabel = "Show dumps"});
4799 }
4800#endif
4801 vButtons.push_back(x: {.m_pLabel = "OK", .m_Confirm = true, .m_Cancel = true});
4802 const std::optional<int> MessageResult = pClient->ShowMessageBox(MessageBox: {.m_pTitle = "Assertion Error", .m_pMessage = aMessage, .m_vButtons = vButtons});
4803#if !defined(CONF_PLATFORM_ANDROID)
4804 if(pClient->Storage() != nullptr && MessageResult && *MessageResult == 0)
4805 {
4806 char aDumpsPath[IO_MAX_PATH_LENGTH];
4807 pClient->Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "dumps", pBuffer: aDumpsPath, BufferSize: sizeof(aDumpsPath));
4808 pClient->ViewFile(pFilename: aDumpsPath);
4809 }
4810#else
4811 (void)MessageResult;
4812#endif
4813 // Client will crash due to assertion, don't call PerformAllCleanup in this inconsistent state
4814 });
4815
4816 // create the components
4817 IEngine *pEngine = CreateEngine(GAME_NAME, pFutureLogger: pFutureConsoleLogger);
4818 pKernel->RegisterInterface(pInterface: pEngine, Destroy: false);
4819 CleanerFunctions.emplace(args: [pEngine]() {
4820 // Engine has to be destroyed before the graphics so that skin download thread can finish
4821 delete pEngine;
4822 });
4823
4824 IStorage *pStorage;
4825 {
4826 CMemoryLogger MemoryLogger;
4827 MemoryLogger.SetParent(log_get_scope_logger());
4828 {
4829 CLogScope LogScope(&MemoryLogger);
4830 pStorage = CreateStorage(InitializationType: IStorage::EInitializationType::CLIENT, NumArgs: argc, ppArguments: argv);
4831 }
4832 if(!pStorage)
4833 {
4834 log_error("client", "Failed to initialize the storage location (see details above)");
4835 std::string Message = std::string("Failed to initialize the storage location. See details below.\n\n") + MemoryLogger.ConcatenatedLines();
4836 pClient->ShowMessageBox(MessageBox: {.m_pTitle = "Storage Error", .m_pMessage = Message.c_str()});
4837 PerformAllCleanup();
4838 return -1;
4839 }
4840 }
4841 pKernel->RegisterInterface(pInterface: pStorage);
4842
4843 pFutureAssertionLogger->Set(CreateAssertionLogger(pStorage, GAME_NAME));
4844
4845 {
4846 char aBufPath[IO_MAX_PATH_LENGTH];
4847 char aBufName[IO_MAX_PATH_LENGTH];
4848 char aDate[64];
4849 str_timestamp(buffer: aDate, buffer_size: sizeof(aDate));
4850 str_format(buffer: aBufName, buffer_size: sizeof(aBufName), format: "dumps/" GAME_NAME "_%s_crash_log_%s_%d_%s.RTP", CONF_PLATFORM_STRING, aDate, pid(), GIT_SHORTREV_HASH != nullptr ? GIT_SHORTREV_HASH : "");
4851 pStorage->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: aBufName, pBuffer: aBufPath, BufferSize: sizeof(aBufPath));
4852 crashdump_init_if_available(log_file_path: aBufPath);
4853 }
4854
4855 IConsole *pConsole = CreateConsole(FlagMask: CFGFLAG_CLIENT).release();
4856 pKernel->RegisterInterface(pInterface: pConsole);
4857
4858 IConfigManager *pConfigManager = CreateConfigManager();
4859 pKernel->RegisterInterface(pInterface: pConfigManager);
4860
4861 IEngineSound *pEngineSound = CreateEngineSound();
4862 pKernel->RegisterInterface(pInterface: pEngineSound); // IEngineSound
4863 pKernel->RegisterInterface(pInterface: static_cast<ISound *>(pEngineSound), Destroy: false);
4864
4865 IEngineInput *pEngineInput = CreateEngineInput();
4866 pKernel->RegisterInterface(pInterface: pEngineInput); // IEngineInput
4867 pKernel->RegisterInterface(pInterface: static_cast<IInput *>(pEngineInput), Destroy: false);
4868
4869 IEngineTextRender *pEngineTextRender = CreateEngineTextRender();
4870 pKernel->RegisterInterface(pInterface: pEngineTextRender); // IEngineTextRender
4871 pKernel->RegisterInterface(pInterface: static_cast<ITextRender *>(pEngineTextRender), Destroy: false);
4872
4873 IEngineMap *pEngineMap = CreateEngineMap();
4874 pKernel->RegisterInterface(pInterface: pEngineMap); // IEngineMap
4875 pKernel->RegisterInterface(pInterface: static_cast<IMap *>(pEngineMap), Destroy: false);
4876
4877 IDiscord *pDiscord = CreateDiscord();
4878 pKernel->RegisterInterface(pInterface: pDiscord);
4879
4880 ISteam *pSteam = CreateSteam();
4881 pKernel->RegisterInterface(pInterface: pSteam);
4882
4883 INotifications *pNotifications = CreateNotifications();
4884 pKernel->RegisterInterface(pInterface: pNotifications);
4885
4886 pKernel->RegisterInterface(pInterface: CreateEditor(), Destroy: false);
4887 pKernel->RegisterInterface(pInterface: CreateFavorites().release());
4888 pKernel->RegisterInterface(pInterface: CreateGameClient());
4889
4890 pEngine->Init();
4891 pConsole->Init();
4892 pConfigManager->Init();
4893 pNotifications->Init(GAME_NAME " Client");
4894
4895 // register all console commands
4896 pClient->RegisterCommands();
4897
4898 pKernel->RequestInterface<IGameClient>()->OnConsoleInit();
4899
4900 // init client's interfaces
4901 pClient->InitInterfaces();
4902
4903 // execute config file
4904 if(pStorage->FileExists(CONFIG_FILE, Type: IStorage::TYPE_ALL))
4905 {
4906 pConsole->SetUnknownCommandCallback(pfnCallback: SaveUnknownCommandCallback, pUser: pClient);
4907 if(!pConsole->ExecuteFile(CONFIG_FILE, ClientId: IConsole::CLIENT_ID_UNSPECIFIED))
4908 {
4909 const char *pError = "Failed to load config from '" CONFIG_FILE "'.";
4910 log_error("client", "%s", pError);
4911 pClient->ShowMessageBox(MessageBox: {.m_pTitle = "Config File Error", .m_pMessage = pError});
4912 PerformAllCleanup();
4913 return -1;
4914 }
4915 pConsole->SetUnknownCommandCallback(pfnCallback: IConsole::EmptyUnknownCommandCallback, pUser: nullptr);
4916 }
4917
4918 // execute autoexec file
4919 if(pStorage->FileExists(AUTOEXEC_CLIENT_FILE, Type: IStorage::TYPE_ALL))
4920 {
4921 pConsole->ExecuteFile(AUTOEXEC_CLIENT_FILE, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
4922 }
4923 else // fallback
4924 {
4925 pConsole->ExecuteFile(AUTOEXEC_FILE, ClientId: IConsole::CLIENT_ID_UNSPECIFIED);
4926 }
4927
4928 if(g_Config.m_ClConfigVersion < 1)
4929 {
4930 if(g_Config.m_ClAntiPing == 0)
4931 {
4932 g_Config.m_ClAntiPingPlayers = 1;
4933 g_Config.m_ClAntiPingGrenade = 1;
4934 g_Config.m_ClAntiPingWeapons = 1;
4935 }
4936 }
4937 g_Config.m_ClConfigVersion = 1;
4938
4939 // parse the command line arguments
4940 pConsole->SetUnknownCommandCallback(pfnCallback: UnknownArgumentCallback, pUser: pClient);
4941 pConsole->ParseArguments(NumArgs: argc - 1, ppArguments: &argv[1]);
4942 pConsole->SetUnknownCommandCallback(pfnCallback: IConsole::EmptyUnknownCommandCallback, pUser: nullptr);
4943
4944 if(pSteam->GetConnectAddress())
4945 {
4946 pClient->HandleConnectAddress(pAddr: pSteam->GetConnectAddress());
4947 pSteam->ClearConnectAddress();
4948 }
4949
4950 if(g_Config.m_Logfile[0])
4951 {
4952 const int Mode = g_Config.m_Logappend ? IOFLAG_APPEND : IOFLAG_WRITE;
4953 IOHANDLE Logfile = pStorage->OpenFile(pFilename: g_Config.m_Logfile, Flags: Mode, Type: IStorage::TYPE_SAVE_OR_ABSOLUTE);
4954 if(Logfile)
4955 {
4956 pFutureFileLogger->Set(log_logger_file(file: Logfile));
4957 }
4958 else
4959 {
4960 log_error("client", "failed to open '%s' for logging", g_Config.m_Logfile);
4961 pFutureFileLogger->Set(log_logger_noop());
4962 }
4963 }
4964 else
4965 {
4966 pFutureFileLogger->Set(log_logger_noop());
4967 }
4968
4969 // Register protocol and file extensions
4970#if defined(CONF_FAMILY_WINDOWS)
4971 pClient->ShellRegister();
4972#endif
4973
4974 // Do not automatically translate touch events to mouse events and vice versa.
4975 SDL_SetHint(name: "SDL_TOUCH_MOUSE_EVENTS", value: "0");
4976 SDL_SetHint(name: "SDL_MOUSE_TOUCH_EVENTS", value: "0");
4977
4978 // Support longer IME composition strings (enables SDL_TEXTEDITING_EXT).
4979#if SDL_VERSION_ATLEAST(2, 0, 22)
4980 SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, value: "1");
4981#endif
4982
4983#if defined(CONF_PLATFORM_MACOS)
4984 // Hints will not be set if there is an existing override hint or environment variable that takes precedence.
4985 // So this respects cli environment overrides.
4986 SDL_SetHint("SDL_MAC_OPENGL_ASYNC_DISPATCH", "1");
4987#endif
4988
4989#if defined(CONF_FAMILY_WINDOWS)
4990 SDL_SetHint("SDL_IME_SHOW_UI", g_Config.m_InpImeNativeUi ? "1" : "0");
4991#else
4992 SDL_SetHint(name: "SDL_IME_SHOW_UI", value: "1");
4993#endif
4994
4995#if defined(CONF_PLATFORM_ANDROID)
4996 // Trap the Android back button so it can be handled in our code reliably
4997 // instead of letting the system handle it.
4998 SDL_SetHint("SDL_ANDROID_TRAP_BACK_BUTTON", "1");
4999 // Force landscape screen orientation.
5000 SDL_SetHint("SDL_IOS_ORIENTATIONS", "LandscapeLeft LandscapeRight");
5001#endif
5002
5003 // init SDL
5004 if(SDL_Init(flags: 0) < 0)
5005 {
5006 char aError[256];
5007 str_format(buffer: aError, buffer_size: sizeof(aError), format: "Unable to initialize SDL base: %s", SDL_GetError());
5008 log_error("client", "%s", aError);
5009 pClient->ShowMessageBox(MessageBox: {.m_pTitle = "SDL Error", .m_pMessage = aError});
5010 PerformAllCleanup();
5011 return -1;
5012 }
5013
5014 // run the client
5015 log_trace("client", "initialization finished after %.2fms, starting...", (time_get() - MainStart) * 1000.0f / (float)time_freq());
5016 pClient->Run();
5017
5018 const bool Restarting = pClient->State() == CClient::STATE_RESTARTING;
5019#if !defined(CONF_PLATFORM_ANDROID)
5020 char aRestartBinaryPath[IO_MAX_PATH_LENGTH];
5021 if(Restarting)
5022 {
5023 pStorage->GetBinaryPath(PLAT_CLIENT_EXEC, pBuffer: aRestartBinaryPath, BufferSize: sizeof(aRestartBinaryPath));
5024 }
5025#endif
5026
5027 std::vector<SWarning> vQuittingWarnings = pClient->QuittingWarnings();
5028
5029 PerformCleanup();
5030
5031 for(const SWarning &Warning : vQuittingWarnings)
5032 {
5033 ShowMessageBoxWithoutGraphics(MessageBox: {.m_pTitle = Warning.m_aWarningTitle, .m_pMessage = Warning.m_aWarningMsg});
5034 }
5035
5036 if(Restarting)
5037 {
5038#if defined(CONF_PLATFORM_ANDROID)
5039 RestartAndroidApp();
5040#else
5041 shell_execute(file: aRestartBinaryPath, window_state: EShellExecuteWindowState::FOREGROUND);
5042#endif
5043 }
5044
5045 PerformFinalCleanup();
5046
5047 return 0;
5048}
5049
5050// DDRace
5051
5052const char *CClient::GetCurrentMap() const
5053{
5054 return m_aCurrentMap;
5055}
5056
5057const char *CClient::GetCurrentMapPath() const
5058{
5059 return m_aCurrentMapPath;
5060}
5061
5062SHA256_DIGEST CClient::GetCurrentMapSha256() const
5063{
5064 return m_pMap->Sha256();
5065}
5066
5067unsigned CClient::GetCurrentMapCrc() const
5068{
5069 return m_pMap->Crc();
5070}
5071
5072void CClient::RaceRecord_Start(const char *pFilename)
5073{
5074 dbg_assert(State() == IClient::STATE_ONLINE, "Client must be online to record demo");
5075
5076 m_aDemoRecorder[RECORDER_RACE].Start(
5077 pStorage: Storage(),
5078 pConsole: m_pConsole,
5079 pFilename,
5080 pNetversion: IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(),
5081 pMap: m_aCurrentMap,
5082 Sha256: m_pMap->Sha256(),
5083 MapCrc: m_pMap->Crc(),
5084 pType: "client",
5085 MapSize: m_pMap->Size(),
5086 pMapData: nullptr,
5087 MapFile: m_pMap->File(),
5088 pfnFilter: nullptr,
5089 pUser: nullptr);
5090}
5091
5092void CClient::RaceRecord_Stop()
5093{
5094 if(m_aDemoRecorder[RECORDER_RACE].IsRecording())
5095 {
5096 m_aDemoRecorder[RECORDER_RACE].Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
5097 }
5098}
5099
5100bool CClient::RaceRecord_IsRecording()
5101{
5102 return m_aDemoRecorder[RECORDER_RACE].IsRecording();
5103}
5104
5105void CClient::RequestDDNetInfo()
5106{
5107 if(m_pDDNetInfoTask && !m_pDDNetInfoTask->Done())
5108 return;
5109
5110 char aUrl[256];
5111 str_copy(dst&: aUrl, src: DDNET_INFO_URL);
5112
5113 if(g_Config.m_BrIndicateFinished)
5114 {
5115 char aEscaped[128];
5116 EscapeUrl(pBuf: aEscaped, Size: sizeof(aEscaped), pStr: PlayerName());
5117 str_append(dst&: aUrl, src: "?name=");
5118 str_append(dst&: aUrl, src: aEscaped);
5119 }
5120
5121 m_pDDNetInfoTask = HttpGetFile(pUrl: aUrl, pStorage: Storage(), pOutputFile: DDNET_INFO_FILE, StorageType: IStorage::TYPE_SAVE);
5122 m_pDDNetInfoTask->Timeout(Timeout: CTimeout{.m_ConnectTimeoutMs: 10000, .m_TimeoutMs: 0, .m_LowSpeedLimit: 500, .m_LowSpeedTime: 10});
5123 m_pDDNetInfoTask->SkipByFileTime(SkipByFileTime: false); // Always re-download.
5124 // Use ipv4 so we can know the ingame ip addresses of players before they join game servers
5125 m_pDDNetInfoTask->IpResolve(IpResolve: IPRESOLVE::V4);
5126 Http()->Run(pRequest: m_pDDNetInfoTask);
5127 m_InfoState = EInfoState::LOADING;
5128}
5129
5130int CClient::GetPredictionTime()
5131{
5132 int64_t Now = time_get();
5133 return (int)((m_PredictedTime.Get(Now) - m_aGameTime[g_Config.m_ClDummy].Get(Now)) * 1000 / (float)time_freq());
5134}
5135
5136int CClient::GetPredictionTick()
5137{
5138 int PredictionTick = GetPredictionTime() * GameTickSpeed() / 1000.0f;
5139
5140 int PredictionMin = g_Config.m_ClAntiPingLimit * GameTickSpeed() / 1000.0f;
5141
5142 if(g_Config.m_ClAntiPingLimit == 0)
5143 {
5144 float PredictionPercentage = 1 - g_Config.m_ClAntiPingPercent / 100.0f;
5145 PredictionMin = std::floor(x: PredictionTick * PredictionPercentage);
5146 }
5147
5148 if(PredictionMin > PredictionTick - 1)
5149 {
5150 PredictionMin = PredictionTick - 1;
5151 }
5152
5153 if(PredictionMin <= 0)
5154 return PredGameTick(Conn: g_Config.m_ClDummy);
5155
5156 PredictionTick = PredGameTick(Conn: g_Config.m_ClDummy) - PredictionMin;
5157
5158 if(PredictionTick < GameTick(Conn: g_Config.m_ClDummy) + 1)
5159 {
5160 PredictionTick = GameTick(Conn: g_Config.m_ClDummy) + 1;
5161 }
5162 return PredictionTick;
5163}
5164
5165void CClient::GetSmoothTick(int *pSmoothTick, float *pSmoothIntraTick, float MixAmount)
5166{
5167 int64_t GameTime = m_aGameTime[g_Config.m_ClDummy].Get(Now: time_get());
5168 int64_t PredTime = m_PredictedTime.Get(Now: time_get());
5169 int64_t SmoothTime = std::clamp(val: GameTime + (int64_t)(MixAmount * (PredTime - GameTime)), lo: GameTime, hi: PredTime);
5170
5171 *pSmoothTick = (int)(SmoothTime * GameTickSpeed() / time_freq()) + 1;
5172 *pSmoothIntraTick = (SmoothTime - (*pSmoothTick - 1) * time_freq() / GameTickSpeed()) / (float)(time_freq() / GameTickSpeed());
5173}
5174
5175void CClient::AddWarning(const SWarning &Warning)
5176{
5177 const std::unique_lock<std::mutex> Lock(m_WarningsMutex);
5178 m_vWarnings.emplace_back(args: Warning);
5179}
5180
5181std::optional<SWarning> CClient::CurrentWarning()
5182{
5183 const std::unique_lock<std::mutex> Lock(m_WarningsMutex);
5184 if(m_vWarnings.empty())
5185 {
5186 return std::nullopt;
5187 }
5188 else
5189 {
5190 std::optional<SWarning> Result = std::make_optional(t&: m_vWarnings[0]);
5191 m_vWarnings.erase(position: m_vWarnings.begin());
5192 return Result;
5193 }
5194}
5195
5196int CClient::MaxLatencyTicks() const
5197{
5198 return GameTickSpeed() + (PredictionMargin() * GameTickSpeed()) / 1000;
5199}
5200
5201int CClient::PredictionMargin() const
5202{
5203 return m_ServerCapabilities.m_SyncWeaponInput ? g_Config.m_ClPredictionMargin : 10;
5204}
5205
5206int CClient::UdpConnectivity(int NetType)
5207{
5208 static const int NETTYPES[2] = {NETTYPE_IPV6, NETTYPE_IPV4};
5209 int Connectivity = CONNECTIVITY_UNKNOWN;
5210 for(int PossibleNetType : NETTYPES)
5211 {
5212 if((NetType & PossibleNetType) == 0)
5213 {
5214 continue;
5215 }
5216 NETADDR GlobalUdpAddr;
5217 int NewConnectivity;
5218 switch(m_aNetClient[CONN_MAIN].GetConnectivity(NetType: PossibleNetType, pGlobalAddr: &GlobalUdpAddr))
5219 {
5220 case CONNECTIVITY::UNKNOWN:
5221 NewConnectivity = CONNECTIVITY_UNKNOWN;
5222 break;
5223 case CONNECTIVITY::CHECKING:
5224 NewConnectivity = CONNECTIVITY_CHECKING;
5225 break;
5226 case CONNECTIVITY::UNREACHABLE:
5227 NewConnectivity = CONNECTIVITY_UNREACHABLE;
5228 break;
5229 case CONNECTIVITY::REACHABLE:
5230 NewConnectivity = CONNECTIVITY_REACHABLE;
5231 break;
5232 case CONNECTIVITY::ADDRESS_KNOWN:
5233 GlobalUdpAddr.port = 0;
5234 if(m_HaveGlobalTcpAddr && NetType == (int)m_GlobalTcpAddr.type && net_addr_comp(a: &m_GlobalTcpAddr, b: &GlobalUdpAddr) != 0)
5235 {
5236 NewConnectivity = CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES;
5237 break;
5238 }
5239 NewConnectivity = CONNECTIVITY_REACHABLE;
5240 break;
5241 default:
5242 dbg_assert(0, "invalid connectivity value");
5243 return CONNECTIVITY_UNKNOWN;
5244 }
5245 Connectivity = std::max(a: Connectivity, b: NewConnectivity);
5246 }
5247 return Connectivity;
5248}
5249
5250static bool ViewLinkImpl(const char *pLink)
5251{
5252#if defined(CONF_PLATFORM_ANDROID)
5253 if(SDL_OpenURL(pLink) == 0)
5254 {
5255 return true;
5256 }
5257 log_error("client", "Failed to open link '%s' (%s)", pLink, SDL_GetError());
5258 return false;
5259#else
5260 if(open_link(link: pLink))
5261 {
5262 return true;
5263 }
5264 log_error("client", "Failed to open link '%s'", pLink);
5265 return false;
5266#endif
5267}
5268
5269bool CClient::ViewLink(const char *pLink)
5270{
5271 if(!str_startswith(str: pLink, prefix: "https://"))
5272 {
5273 log_error("client", "Failed to open link '%s': only https-links are allowed", pLink);
5274 return false;
5275 }
5276 return ViewLinkImpl(pLink);
5277}
5278
5279bool CClient::ViewFile(const char *pFilename)
5280{
5281#if defined(CONF_PLATFORM_MACOS)
5282 return ViewLinkImpl(pFilename);
5283#else
5284 // Create a file link so the path can contain forward and
5285 // backward slashes. But the file link must be absolute.
5286 char aWorkingDir[IO_MAX_PATH_LENGTH];
5287 if(fs_is_relative_path(path: pFilename))
5288 {
5289 if(!fs_getcwd(buffer: aWorkingDir, buffer_size: sizeof(aWorkingDir)))
5290 {
5291 log_error("client", "Failed to open file '%s' (failed to get working directory)", pFilename);
5292 return false;
5293 }
5294 str_append(dst&: aWorkingDir, src: "/");
5295 }
5296 else
5297 aWorkingDir[0] = '\0';
5298
5299 char aFileLink[IO_MAX_PATH_LENGTH];
5300 str_format(buffer: aFileLink, buffer_size: sizeof(aFileLink), format: "file://%s%s", aWorkingDir, pFilename);
5301 return ViewLinkImpl(pLink: aFileLink);
5302#endif
5303}
5304
5305#if defined(CONF_FAMILY_WINDOWS)
5306void CClient::ShellRegister()
5307{
5308 char aFullPath[IO_MAX_PATH_LENGTH];
5309 Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
5310 if(!aFullPath[0])
5311 {
5312 log_error("client", "Failed to register protocol and file extensions: could not determine absolute path");
5313 return;
5314 }
5315
5316 bool Updated = false;
5317 if(!windows_shell_register_protocol("ddnet", aFullPath, &Updated))
5318 log_error("client", "Failed to register ddnet protocol");
5319 if(!windows_shell_register_extension(".map", "Map File", GAME_NAME, aFullPath, &Updated))
5320 log_error("client", "Failed to register .map file extension");
5321 if(!windows_shell_register_extension(".demo", "Demo File", GAME_NAME, aFullPath, &Updated))
5322 log_error("client", "Failed to register .demo file extension");
5323 if(!windows_shell_register_application(GAME_NAME, aFullPath, &Updated))
5324 log_error("client", "Failed to register application");
5325 if(Updated)
5326 windows_shell_update();
5327}
5328
5329void CClient::ShellUnregister()
5330{
5331 char aFullPath[IO_MAX_PATH_LENGTH];
5332 Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
5333 if(!aFullPath[0])
5334 {
5335 log_error("client", "Failed to unregister protocol and file extensions: could not determine absolute path");
5336 return;
5337 }
5338
5339 bool Updated = false;
5340 if(!windows_shell_unregister_class("ddnet", &Updated))
5341 log_error("client", "Failed to unregister ddnet protocol");
5342 if(!windows_shell_unregister_class(GAME_NAME ".map", &Updated))
5343 log_error("client", "Failed to unregister .map file extension");
5344 if(!windows_shell_unregister_class(GAME_NAME ".demo", &Updated))
5345 log_error("client", "Failed to unregister .demo file extension");
5346 if(!windows_shell_unregister_application(aFullPath, &Updated))
5347 log_error("client", "Failed to unregister application");
5348 if(Updated)
5349 windows_shell_update();
5350}
5351#endif
5352
5353std::optional<int> CClient::ShowMessageBox(const IGraphics::CMessageBox &MessageBox)
5354{
5355 std::optional<int> Result = m_pGraphics == nullptr ? std::nullopt : m_pGraphics->ShowMessageBox(MessageBox);
5356 if(!Result)
5357 {
5358 Result = ShowMessageBoxWithoutGraphics(MessageBox);
5359 }
5360 return Result;
5361}
5362
5363void CClient::GetGpuInfoString(char (&aGpuInfo)[512])
5364{
5365 if(m_pGraphics == nullptr || !m_pGraphics->IsBackendInitialized())
5366 {
5367 str_format(buffer: aGpuInfo, buffer_size: std::size(aGpuInfo),
5368 format: "Graphics backend: %s %d.%d.%d\n"
5369 "Graphics %s not yet initialized.",
5370 g_Config.m_GfxBackend, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch,
5371 m_pGraphics == nullptr ? "were" : "backend was");
5372 }
5373 else
5374 {
5375 // TODO: Even better would be if the backend could return its name and version, because the config variables can be outdated when the client was not restarted.
5376 str_format(aGpuInfo, std::size(aGpuInfo),
5377 "Graphics backend: %s %d.%d.%d\n"
5378 "GPU: %s - %s - %s\n"
5379 "Texture: %" PRIu64 " MiB, "
5380 "Buffer: %" PRIu64 " MiB, "
5381 "Streamed: %" PRIu64 " MiB, "
5382 "Staging: %" PRIu64 " MiB",
5383 g_Config.m_GfxBackend, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch,
5384 m_pGraphics->GetVendorString(), m_pGraphics->GetRendererString(), m_pGraphics->GetVersionString(),
5385 m_pGraphics->TextureMemoryUsage() / 1024 / 1024,
5386 m_pGraphics->BufferMemoryUsage() / 1024 / 1024,
5387 m_pGraphics->StreamedMemoryUsage() / 1024 / 1024,
5388 m_pGraphics->StagingMemoryUsage() / 1024 / 1024);
5389 }
5390}
5391
5392void CClient::SetLoggers(std::shared_ptr<ILogger> &&pFileLogger, std::shared_ptr<ILogger> &&pStdoutLogger)
5393{
5394 m_pFileLogger = pFileLogger;
5395 m_pStdoutLogger = pStdoutLogger;
5396}
5397