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, SHA256_DIGEST *pWantedSha256, 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))
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(pWantedSha256 && m_pMap->Sha256() != *pWantedSha256)
1167 {
1168 char aWanted[SHA256_MAXSTRSIZE];
1169 char aGot[SHA256_MAXSTRSIZE];
1170 sha256_str(digest: *pWantedSha256, 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(!pWantedSha256 && 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 SHA256_DIGEST *pSha256, 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(pSha256)
1216 {
1217 char aSha256[SHA256_MAXSTRSIZE];
1218 sha256_str(digest: *pSha256, 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, SHA256_DIGEST *pWantedSha256, int WantedCrc)
1228{
1229 char aBuf[512];
1230 char aWanted[SHA256_MAXSTRSIZE + 16];
1231 aWanted[0] = 0;
1232 if(pWantedSha256)
1233 {
1234 char aWantedSha256[SHA256_MAXSTRSIZE];
1235 sha256_str(digest: *pWantedSha256, 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, pWantedSha256, WantedCrc);
1244 if(!pError)
1245 return nullptr;
1246
1247 // try the downloaded maps
1248 FormatMapDownloadFilename(pName: pMapName, pSha256: pWantedSha256, Crc: WantedCrc, Temp: false, pBuffer: aBuf, BufferSize: sizeof(aBuf));
1249 pError = LoadMap(pName: pMapName, pFilename: aBuf, pWantedSha256, WantedCrc);
1250 if(!pError)
1251 return nullptr;
1252
1253 // backward compatibility with old names
1254 if(pWantedSha256)
1255 {
1256 FormatMapDownloadFilename(pName: pMapName, pSha256: nullptr, Crc: WantedCrc, Temp: false, pBuffer: aBuf, BufferSize: sizeof(aBuf));
1257 pError = LoadMap(pName: pMapName, pFilename: aBuf, pWantedSha256, 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, pWantedSha256, 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(net_addr_comp(a: &ServerAddress(), b: pFrom) == 0 && RawType != SERVERINFO_EXTENDED_MORE)
1465 {
1466 // Only accept server info that has a type that is
1467 // newer or equal to something the server already sent
1468 // us.
1469 if(SavedType >= m_CurrentServerInfo.m_Type)
1470 {
1471 m_CurrentServerInfo = Info;
1472 m_CurrentServerInfoRequestTime = -1;
1473 Discord()->UpdateServerInfo(ServerInfo: Info, pMapName: m_aCurrentMap);
1474 }
1475
1476 bool ValidPong = false;
1477 if(!m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && SavedType >= m_CurrentServerPingInfoType)
1478 {
1479 if(RawType == SERVERINFO_VANILLA)
1480 {
1481 ValidPong = Token == m_CurrentServerPingBasicToken;
1482 }
1483 else if(RawType == SERVERINFO_EXTENDED)
1484 {
1485 ValidPong = Token == m_CurrentServerPingToken;
1486 }
1487 }
1488 if(ValidPong)
1489 {
1490 int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
1491 m_ServerBrowser.SetCurrentServerPing(Addr: ServerAddress(), Ping: LatencyMs);
1492 m_CurrentServerPingInfoType = SavedType;
1493 m_CurrentServerCurrentPingTime = -1;
1494
1495 char aBuf[64];
1496 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "got pong from current server, latency=%dms", LatencyMs);
1497 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf);
1498 }
1499 }
1500 }
1501
1502#undef GET_STRING
1503#undef GET_INT
1504}
1505
1506static CServerCapabilities GetServerCapabilities(int Version, int Flags, bool Sixup)
1507{
1508 CServerCapabilities Result;
1509 bool DDNet = false;
1510 if(Version >= 1)
1511 {
1512 DDNet = Flags & SERVERCAPFLAG_DDNET;
1513 }
1514 Result.m_ChatTimeoutCode = DDNet;
1515 Result.m_AnyPlayerFlag = !Sixup;
1516 Result.m_PingEx = false;
1517 Result.m_AllowDummy = true;
1518 Result.m_SyncWeaponInput = false;
1519 if(Version >= 1)
1520 {
1521 Result.m_ChatTimeoutCode = Flags & SERVERCAPFLAG_CHATTIMEOUTCODE;
1522 }
1523 if(Version >= 2)
1524 {
1525 Result.m_AnyPlayerFlag = Flags & SERVERCAPFLAG_ANYPLAYERFLAG;
1526 }
1527 if(Version >= 3)
1528 {
1529 Result.m_PingEx = Flags & SERVERCAPFLAG_PINGEX;
1530 }
1531 if(Version >= 4)
1532 {
1533 Result.m_AllowDummy = Flags & SERVERCAPFLAG_ALLOWDUMMY;
1534 }
1535 if(Version >= 5)
1536 {
1537 Result.m_SyncWeaponInput = Flags & SERVERCAPFLAG_SYNCWEAPONINPUT;
1538 }
1539 return Result;
1540}
1541
1542void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy)
1543{
1544 CUnpacker Unpacker;
1545 Unpacker.Reset(pData: pPacket->m_pData, Size: pPacket->m_DataSize);
1546 CMsgPacker Packer(NETMSG_EX, true);
1547
1548 // unpack msgid and system flag
1549 int Msg;
1550 bool Sys;
1551 CUuid Uuid;
1552
1553 int Result = UnpackMessageId(pId: &Msg, pSys: &Sys, pUuid: &Uuid, pUnpacker: &Unpacker, pPacker: &Packer);
1554 if(Result == UNPACKMESSAGE_ERROR)
1555 {
1556 return;
1557 }
1558 else if(Result == UNPACKMESSAGE_ANSWER)
1559 {
1560 SendMsg(Conn, pMsg: &Packer, Flags: MSGFLAG_VITAL);
1561 }
1562
1563 // allocates the memory for the translated data
1564 CPacker Packer6;
1565 if(IsSixup())
1566 {
1567 bool IsExMsg = false;
1568 int Success = !TranslateSysMsg(pMsgId: &Msg, System: Sys, pUnpacker: &Unpacker, pPacker: &Packer6, pPacket, pIsExMsg: &IsExMsg);
1569 if(Msg < 0)
1570 return;
1571 if(Success && !IsExMsg)
1572 {
1573 Unpacker.Reset(pData: Packer6.Data(), Size: Packer6.Size());
1574 }
1575 }
1576
1577 if(Sys)
1578 {
1579 // system message
1580 if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_DETAILS)
1581 {
1582 const char *pMap = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
1583 SHA256_DIGEST *pMapSha256 = (SHA256_DIGEST *)Unpacker.GetRaw(Size: sizeof(*pMapSha256));
1584 int MapCrc = Unpacker.GetInt();
1585 int MapSize = Unpacker.GetInt();
1586 if(Unpacker.Error())
1587 {
1588 return;
1589 }
1590
1591 const char *pMapUrl = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1592 if(Unpacker.Error())
1593 {
1594 pMapUrl = "";
1595 }
1596
1597 m_MapDetailsPresent = true;
1598 (void)MapSize;
1599 str_copy(dst&: m_aMapDetailsName, src: pMap);
1600 m_MapDetailsSha256 = *pMapSha256;
1601 m_MapDetailsCrc = MapCrc;
1602 str_copy(dst&: m_aMapDetailsUrl, src: pMapUrl);
1603 }
1604 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CAPABILITIES)
1605 {
1606 if(!m_CanReceiveServerCapabilities)
1607 {
1608 return;
1609 }
1610 int Version = Unpacker.GetInt();
1611 int Flags = Unpacker.GetInt();
1612 if(Unpacker.Error() || Version <= 0)
1613 {
1614 return;
1615 }
1616 m_ServerCapabilities = GetServerCapabilities(Version, Flags, Sixup: IsSixup());
1617 m_CanReceiveServerCapabilities = false;
1618 m_ServerSentCapabilities = true;
1619 }
1620 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_CHANGE)
1621 {
1622 if(m_CanReceiveServerCapabilities)
1623 {
1624 m_ServerCapabilities = GetServerCapabilities(Version: 0, Flags: 0, Sixup: IsSixup());
1625 m_CanReceiveServerCapabilities = false;
1626 }
1627 bool MapDetailsWerePresent = m_MapDetailsPresent;
1628 m_MapDetailsPresent = false;
1629
1630 const char *pMap = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
1631 int MapCrc = Unpacker.GetInt();
1632 int MapSize = Unpacker.GetInt();
1633 if(Unpacker.Error())
1634 {
1635 return;
1636 }
1637 if(MapSize < 0 || MapSize > 1024 * 1024 * 1024) // 1 GiB
1638 {
1639 DisconnectWithReason(pReason: "invalid map size");
1640 return;
1641 }
1642
1643 if(!str_valid_filename(str: pMap))
1644 {
1645 DisconnectWithReason(pReason: "map name is not a valid filename");
1646 return;
1647 }
1648
1649 if(m_DummyConnected && !m_DummyReconnectOnReload)
1650 {
1651 DummyDisconnect(pReason: nullptr);
1652 }
1653
1654 ResetMapDownload(ResetActive: true);
1655
1656 SHA256_DIGEST *pMapSha256 = nullptr;
1657 const char *pMapUrl = nullptr;
1658 if(MapDetailsWerePresent && str_comp(a: m_aMapDetailsName, b: pMap) == 0 && m_MapDetailsCrc == MapCrc)
1659 {
1660 pMapSha256 = &m_MapDetailsSha256;
1661 pMapUrl = m_aMapDetailsUrl[0] ? m_aMapDetailsUrl : nullptr;
1662 }
1663
1664 if(LoadMapSearch(pMapName: pMap, pWantedSha256: pMapSha256, WantedCrc: MapCrc) == nullptr)
1665 {
1666 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client/network", pStr: "loading done");
1667 SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_SENDING_READY);
1668 SendReady(Conn: CONN_MAIN);
1669 }
1670 else
1671 {
1672 // start map download
1673 FormatMapDownloadFilename(pName: pMap, pSha256: pMapSha256, Crc: MapCrc, Temp: false, pBuffer: m_aMapdownloadFilename, BufferSize: sizeof(m_aMapdownloadFilename));
1674 FormatMapDownloadFilename(pName: pMap, pSha256: pMapSha256, Crc: MapCrc, Temp: true, pBuffer: m_aMapdownloadFilenameTemp, BufferSize: sizeof(m_aMapdownloadFilenameTemp));
1675
1676 char aBuf[256];
1677 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "starting to download map to '%s'", m_aMapdownloadFilenameTemp);
1678 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client/network", pStr: aBuf);
1679
1680 str_copy(dst&: m_aMapdownloadName, src: pMap);
1681 m_MapdownloadSha256Present = (bool)pMapSha256;
1682 m_MapdownloadSha256 = pMapSha256 ? *pMapSha256 : SHA256_ZEROED;
1683 m_MapdownloadCrc = MapCrc;
1684 m_MapdownloadTotalsize = MapSize;
1685
1686 if(pMapSha256)
1687 {
1688 char aUrl[256];
1689 char aEscaped[256];
1690 EscapeUrl(aBuf&: aEscaped, pStr: m_aMapdownloadFilename + 15); // cut off downloadedmaps/
1691 bool UseConfigUrl = str_comp(a: g_Config.m_ClMapDownloadUrl, b: "https://maps.ddnet.org") != 0 || m_aMapDownloadUrl[0] == '\0';
1692 str_format(buffer: aUrl, buffer_size: sizeof(aUrl), format: "%s/%s", UseConfigUrl ? g_Config.m_ClMapDownloadUrl : m_aMapDownloadUrl, aEscaped);
1693
1694 m_pMapdownloadTask = HttpGetFile(pUrl: pMapUrl ? pMapUrl : aUrl, pStorage: Storage(), pOutputFile: m_aMapdownloadFilenameTemp, StorageType: IStorage::TYPE_SAVE);
1695 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});
1696 m_pMapdownloadTask->MaxResponseSize(MaxResponseSize: MapSize);
1697 m_pMapdownloadTask->ExpectSha256(Sha256: *pMapSha256);
1698 Http()->Run(pRequest: m_pMapdownloadTask);
1699 }
1700 else
1701 {
1702 SendMapRequest();
1703 }
1704 }
1705 }
1706 else if(Conn == CONN_MAIN && Msg == NETMSG_MAP_DATA)
1707 {
1708 if(!m_MapdownloadFileTemp)
1709 {
1710 return;
1711 }
1712 int Last = -1;
1713 int MapCRC = -1;
1714 int Chunk = -1;
1715 int Size = -1;
1716
1717 if(IsSixup())
1718 {
1719 MapCRC = m_MapdownloadCrc;
1720 Chunk = m_MapdownloadChunk;
1721 Size = minimum(a: m_TranslationContext.m_MapDownloadChunkSize, b: m_TranslationContext.m_MapdownloadTotalsize - m_MapdownloadAmount);
1722 }
1723 else
1724 {
1725 Last = Unpacker.GetInt();
1726 MapCRC = Unpacker.GetInt();
1727 Chunk = Unpacker.GetInt();
1728 Size = Unpacker.GetInt();
1729 }
1730
1731 const unsigned char *pData = Unpacker.GetRaw(Size);
1732 if(Unpacker.Error() || Size <= 0 || MapCRC != m_MapdownloadCrc || Chunk != m_MapdownloadChunk)
1733 {
1734 return;
1735 }
1736
1737 io_write(io: m_MapdownloadFileTemp, buffer: pData, size: Size);
1738
1739 m_MapdownloadAmount += Size;
1740
1741 if(IsSixup())
1742 Last = m_MapdownloadAmount == m_TranslationContext.m_MapdownloadTotalsize;
1743
1744 if(Last)
1745 {
1746 if(m_MapdownloadFileTemp)
1747 {
1748 io_close(io: m_MapdownloadFileTemp);
1749 m_MapdownloadFileTemp = nullptr;
1750 }
1751 FinishMapDownload();
1752 }
1753 else
1754 {
1755 // request new chunk
1756 m_MapdownloadChunk++;
1757
1758 if(IsSixup() && (m_MapdownloadChunk % m_TranslationContext.m_MapDownloadChunksPerRequest == 0))
1759 {
1760 CMsgPacker MsgP(protocol7::NETMSG_REQUEST_MAP_DATA, true, true);
1761 SendMsg(Conn: CONN_MAIN, pMsg: &MsgP, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
1762 }
1763 else
1764 {
1765 CMsgPacker MsgP(NETMSG_REQUEST_MAP_DATA, true);
1766 MsgP.AddInt(i: m_MapdownloadChunk);
1767 SendMsg(Conn: CONN_MAIN, pMsg: &MsgP, Flags: MSGFLAG_VITAL | MSGFLAG_FLUSH);
1768 }
1769
1770 if(g_Config.m_Debug)
1771 {
1772 char aBuf[256];
1773 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "requested chunk %d", m_MapdownloadChunk);
1774 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client/network", pStr: aBuf);
1775 }
1776 }
1777 }
1778 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_RELOAD)
1779 {
1780 if(m_DummyConnected)
1781 {
1782 m_DummyReconnectOnReload = true;
1783 m_DummyDeactivateOnReconnect = g_Config.m_ClDummy == 0;
1784 g_Config.m_ClDummy = 0;
1785 }
1786 else
1787 m_DummyDeactivateOnReconnect = false;
1788 }
1789 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CON_READY)
1790 {
1791 GameClient()->OnConnected();
1792 if(m_DummyReconnectOnReload)
1793 {
1794 m_DummySendConnInfo = true;
1795 m_DummyReconnectOnReload = false;
1796 }
1797 }
1798 else if(Conn == CONN_DUMMY && Msg == NETMSG_CON_READY)
1799 {
1800 m_DummyConnected = true;
1801 m_DummyConnecting = false;
1802 g_Config.m_ClDummy = 1;
1803 Rcon(pCmd: "crashmeplx");
1804 if(m_aRconAuthed[0] && !m_aRconAuthed[1])
1805 RconAuth(pName: m_aRconUsername, pPassword: m_aRconPassword);
1806 }
1807 else if(Msg == NETMSG_PING)
1808 {
1809 CMsgPacker MsgP(NETMSG_PING_REPLY, true);
1810 int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0;
1811 SendMsg(Conn, pMsg: &MsgP, Flags: MSGFLAG_FLUSH | Vital);
1812 }
1813 else if(Msg == NETMSG_PINGEX)
1814 {
1815 CUuid *pId = (CUuid *)Unpacker.GetRaw(Size: sizeof(*pId));
1816 if(Unpacker.Error())
1817 {
1818 return;
1819 }
1820 CMsgPacker MsgP(NETMSG_PONGEX, true);
1821 MsgP.AddRaw(pData: pId, Size: sizeof(*pId));
1822 int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0;
1823 SendMsg(Conn, pMsg: &MsgP, Flags: MSGFLAG_FLUSH | Vital);
1824 }
1825 else if(Conn == CONN_MAIN && Msg == NETMSG_PONGEX)
1826 {
1827 CUuid *pId = (CUuid *)Unpacker.GetRaw(Size: sizeof(*pId));
1828 if(Unpacker.Error())
1829 {
1830 return;
1831 }
1832 if(m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && *pId == m_CurrentServerPingUuid)
1833 {
1834 int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
1835 m_ServerBrowser.SetCurrentServerPing(Addr: ServerAddress(), Ping: LatencyMs);
1836 m_CurrentServerCurrentPingTime = -1;
1837
1838 char aBuf[64];
1839 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "got pong from current server, latency=%dms", LatencyMs);
1840 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf);
1841 }
1842 }
1843 else if(Msg == NETMSG_CHECKSUM_REQUEST)
1844 {
1845 CUuid *pUuid = (CUuid *)Unpacker.GetRaw(Size: sizeof(*pUuid));
1846 if(Unpacker.Error())
1847 {
1848 return;
1849 }
1850 int ResultCheck = HandleChecksum(Conn, Uuid: *pUuid, pUnpacker: &Unpacker);
1851 if(ResultCheck)
1852 {
1853 CMsgPacker MsgP(NETMSG_CHECKSUM_ERROR, true);
1854 MsgP.AddRaw(pData: pUuid, Size: sizeof(*pUuid));
1855 MsgP.AddInt(i: ResultCheck);
1856 SendMsg(Conn, pMsg: &MsgP, Flags: MSGFLAG_VITAL);
1857 }
1858 }
1859 else if(Msg == NETMSG_RECONNECT)
1860 {
1861 if(Conn == CONN_MAIN)
1862 {
1863 Connect(pAddress: m_aConnectAddressStr);
1864 }
1865 else
1866 {
1867 DummyDisconnect(pReason: "reconnect");
1868 // Reset dummy connect time to allow immediate reconnect
1869 m_LastDummyConnectTime = 0.0f;
1870 DummyConnect();
1871 }
1872 }
1873 else if(Msg == NETMSG_REDIRECT)
1874 {
1875 int RedirectPort = Unpacker.GetInt();
1876 if(Unpacker.Error())
1877 {
1878 return;
1879 }
1880 if(Conn == CONN_MAIN)
1881 {
1882 NETADDR ServerAddr = ServerAddress();
1883 ServerAddr.port = RedirectPort;
1884 char aAddr[NETADDR_MAXSTRSIZE];
1885 net_addr_str(addr: &ServerAddr, string: aAddr, max_length: sizeof(aAddr), add_port: true);
1886 Connect(pAddress: aAddr);
1887 }
1888 else
1889 {
1890 DummyDisconnect(pReason: "redirect");
1891 if(ServerAddress().port != RedirectPort)
1892 {
1893 // Only allow redirecting to the same port to reconnect. The dummy
1894 // should not be connected to a different server than the main, as
1895 // the client assumes that main and dummy use the same map.
1896 return;
1897 }
1898 // Reset dummy connect time to allow immediate reconnect
1899 m_LastDummyConnectTime = 0.0f;
1900 DummyConnect();
1901 }
1902 }
1903 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_ADD)
1904 {
1905 const char *pName = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1906 const char *pHelp = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1907 const char *pParams = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1908 if(!Unpacker.Error())
1909 {
1910 m_pConsole->RegisterTemp(pName, pParams, Flags: CFGFLAG_SERVER, pHelp);
1911 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
1912 }
1913 m_GotRconCommands++;
1914 }
1915 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_REM)
1916 {
1917 const char *pName = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC);
1918 if(!Unpacker.Error())
1919 {
1920 m_pConsole->DeregisterTemp(pName);
1921 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
1922 }
1923 }
1924 else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_AUTH_STATUS)
1925 {
1926 int ResultInt = Unpacker.GetInt();
1927 if(!Unpacker.Error())
1928 {
1929 m_aRconAuthed[Conn] = ResultInt;
1930
1931 if(m_aRconAuthed[Conn])
1932 RconAuth(pName: m_aRconUsername, pPassword: m_aRconPassword, Dummy: g_Config.m_ClDummy ^ 1);
1933 }
1934 if(Conn == CONN_MAIN)
1935 {
1936 int Old = m_UseTempRconCommands;
1937 m_UseTempRconCommands = Unpacker.GetInt();
1938 if(Unpacker.Error())
1939 {
1940 m_UseTempRconCommands = 0;
1941 }
1942 if(Old != 0 && m_UseTempRconCommands == 0)
1943 {
1944 m_pConsole->DeregisterTempAll();
1945 m_ExpectedRconCommands = -1;
1946 m_vMaplistEntries.clear();
1947 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
1948 m_ExpectedMaplistEntries = -1;
1949 }
1950 }
1951 }
1952 else if(!Dummy && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_LINE)
1953 {
1954 const char *pLine = Unpacker.GetString();
1955 if(!Unpacker.Error())
1956 {
1957 GameClient()->OnRconLine(pLine);
1958 }
1959 }
1960 else if(Conn == CONN_MAIN && Msg == NETMSG_PING_REPLY)
1961 {
1962 char aBuf[256];
1963 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "latency %.2f", (time_get() - m_PingStartTime) * 1000 / (float)time_freq());
1964 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client/network", pStr: aBuf);
1965 }
1966 else if(Msg == NETMSG_INPUTTIMING)
1967 {
1968 int InputPredTick = Unpacker.GetInt();
1969 int TimeLeft = Unpacker.GetInt();
1970 if(Unpacker.Error())
1971 {
1972 return;
1973 }
1974
1975 int64_t Now = time_get();
1976
1977 // adjust our prediction time
1978 int64_t Target = 0;
1979 for(int k = 0; k < 200; k++)
1980 {
1981 if(m_aInputs[Conn][k].m_Tick == InputPredTick)
1982 {
1983 Target = m_aInputs[Conn][k].m_PredictedTime + (Now - m_aInputs[Conn][k].m_Time);
1984 Target = Target - (int64_t)((TimeLeft / 1000.0f) * time_freq());
1985 break;
1986 }
1987 }
1988
1989 if(Target)
1990 m_PredictedTime.Update(pGraph: &m_InputtimeMarginGraph, Target, TimeLeft, AdjustDirection: CSmoothTime::ADJUSTDIRECTION_UP);
1991 }
1992 else if(Msg == NETMSG_SNAP || Msg == NETMSG_SNAPSINGLE || Msg == NETMSG_SNAPEMPTY)
1993 {
1994 // we are not allowed to process snapshot yet
1995 if(State() < IClient::STATE_LOADING)
1996 {
1997 return;
1998 }
1999
2000 int GameTick = Unpacker.GetInt();
2001 int DeltaTick = GameTick - Unpacker.GetInt();
2002
2003 int NumParts = 1;
2004 int Part = 0;
2005 if(Msg == NETMSG_SNAP)
2006 {
2007 NumParts = Unpacker.GetInt();
2008 Part = Unpacker.GetInt();
2009 }
2010
2011 unsigned int Crc = 0;
2012 int PartSize = 0;
2013 if(Msg != NETMSG_SNAPEMPTY)
2014 {
2015 Crc = Unpacker.GetInt();
2016 PartSize = Unpacker.GetInt();
2017 }
2018
2019 const char *pData = (const char *)Unpacker.GetRaw(Size: PartSize);
2020 if(Unpacker.Error() || NumParts < 1 || NumParts > CSnapshot::MAX_PARTS || Part < 0 || Part >= NumParts || PartSize < 0 || PartSize > MAX_SNAPSHOT_PACKSIZE)
2021 {
2022 return;
2023 }
2024
2025 // Check m_aAckGameTick to see if we already got a snapshot for that tick
2026 if(GameTick >= m_aCurrentRecvTick[Conn] && GameTick > m_aAckGameTick[Conn])
2027 {
2028 if(GameTick != m_aCurrentRecvTick[Conn])
2029 {
2030 m_aSnapshotParts[Conn] = 0;
2031 m_aCurrentRecvTick[Conn] = GameTick;
2032 m_aSnapshotIncomingDataSize[Conn] = 0;
2033 }
2034
2035 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));
2036 m_aSnapshotParts[Conn] |= (uint64_t)(1) << Part;
2037
2038 if(Part == NumParts - 1)
2039 {
2040 m_aSnapshotIncomingDataSize[Conn] = (NumParts - 1) * MAX_SNAPSHOT_PACKSIZE + PartSize;
2041 }
2042
2043 if((NumParts < CSnapshot::MAX_PARTS && m_aSnapshotParts[Conn] == (((uint64_t)(1) << NumParts) - 1)) ||
2044 (NumParts == CSnapshot::MAX_PARTS && m_aSnapshotParts[Conn] == std::numeric_limits<uint64_t>::max()))
2045 {
2046 unsigned char aTmpBuffer2[CSnapshot::MAX_SIZE];
2047 unsigned char aTmpBuffer3[CSnapshot::MAX_SIZE];
2048 CSnapshot *pTmpBuffer3 = (CSnapshot *)aTmpBuffer3; // Fix compiler warning for strict-aliasing
2049
2050 // reset snapshotting
2051 m_aSnapshotParts[Conn] = 0;
2052
2053 // find snapshot that we should use as delta
2054 const CSnapshot *pDeltaShot = CSnapshot::EmptySnapshot();
2055 if(DeltaTick >= 0)
2056 {
2057 int DeltashotSize = m_aSnapshotStorage[Conn].Get(Tick: DeltaTick, pTagtime: nullptr, ppData: &pDeltaShot, ppAltData: nullptr);
2058
2059 if(DeltashotSize < 0)
2060 {
2061 // couldn't find the delta snapshots that the server used
2062 // to compress this snapshot. force the server to resync
2063 if(g_Config.m_Debug)
2064 {
2065 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "client", pStr: "error, couldn't find the delta snapshot");
2066 }
2067
2068 // ack snapshot
2069 m_aAckGameTick[Conn] = -1;
2070 SendInput();
2071 return;
2072 }
2073 }
2074
2075 // decompress snapshot
2076 const void *pDeltaData = m_SnapshotDelta.EmptyDelta();
2077 int DeltaSize = sizeof(int) * 3;
2078
2079 if(m_aSnapshotIncomingDataSize[Conn])
2080 {
2081 int IntSize = CVariableInt::Decompress(pSrc: m_aaSnapshotIncomingData[Conn], SrcSize: m_aSnapshotIncomingDataSize[Conn], pDst: aTmpBuffer2, DstSize: sizeof(aTmpBuffer2));
2082
2083 if(IntSize < 0) // failure during decompression
2084 return;
2085
2086 pDeltaData = aTmpBuffer2;
2087 DeltaSize = IntSize;
2088 }
2089
2090 // unpack delta
2091 const int SnapSize = m_SnapshotDelta.UnpackDelta(pFrom: pDeltaShot, pTo: pTmpBuffer3, pSrcData: pDeltaData, DataSize: DeltaSize, Sixup: IsSixup());
2092 if(SnapSize < 0)
2093 {
2094 dbg_msg(sys: "client", fmt: "delta unpack failed. error=%d", SnapSize);
2095 return;
2096 }
2097 if(!pTmpBuffer3->IsValid(ActualSize: SnapSize))
2098 {
2099 dbg_msg(sys: "client", fmt: "snapshot invalid. SnapSize=%d, DeltaSize=%d", SnapSize, DeltaSize);
2100 return;
2101 }
2102
2103 if(Msg != NETMSG_SNAPEMPTY && pTmpBuffer3->Crc() != Crc)
2104 {
2105 log_error("client", "snapshot crc error #%d - tick=%d wantedcrc=%d gotcrc=%d compressed_size=%d delta_tick=%d",
2106 m_SnapCrcErrors, GameTick, Crc, pTmpBuffer3->Crc(), m_aSnapshotIncomingDataSize[Conn], DeltaTick);
2107
2108 m_SnapCrcErrors++;
2109 if(m_SnapCrcErrors > 10)
2110 {
2111 // to many errors, send reset
2112 m_aAckGameTick[Conn] = -1;
2113 SendInput();
2114 m_SnapCrcErrors = 0;
2115 }
2116 return;
2117 }
2118 else
2119 {
2120 if(m_SnapCrcErrors)
2121 m_SnapCrcErrors--;
2122 }
2123
2124 // purge old snapshots
2125 int PurgeTick = DeltaTick;
2126 if(m_aapSnapshots[Conn][SNAP_PREV] && m_aapSnapshots[Conn][SNAP_PREV]->m_Tick < PurgeTick)
2127 PurgeTick = m_aapSnapshots[Conn][SNAP_PREV]->m_Tick;
2128 if(m_aapSnapshots[Conn][SNAP_CURRENT] && m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick < PurgeTick)
2129 PurgeTick = m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick;
2130 m_aSnapshotStorage[Conn].PurgeUntil(Tick: PurgeTick);
2131
2132 // create a verified and unpacked snapshot
2133 int AltSnapSize = -1;
2134 unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
2135 CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
2136
2137 if(IsSixup())
2138 {
2139 unsigned char aTmpTransSnapBuffer[CSnapshot::MAX_SIZE];
2140 CSnapshot *pTmpTransSnapBuffer = (CSnapshot *)aTmpTransSnapBuffer;
2141 mem_copy(dest: pTmpTransSnapBuffer, source: pTmpBuffer3, size: CSnapshot::MAX_SIZE);
2142 AltSnapSize = GameClient()->TranslateSnap(pSnapDstSix: pAltSnapBuffer, pSnapSrcSeven: pTmpTransSnapBuffer, Conn, Dummy);
2143 }
2144 else
2145 {
2146 AltSnapSize = UnpackAndValidateSnapshot(pFrom: pTmpBuffer3, pTo: pAltSnapBuffer);
2147 }
2148
2149 if(AltSnapSize < 0)
2150 {
2151 dbg_msg(sys: "client", fmt: "unpack snapshot and validate failed. error=%d", AltSnapSize);
2152 return;
2153 }
2154
2155 // add new
2156 m_aSnapshotStorage[Conn].Add(Tick: GameTick, Tagtime: time_get(), DataSize: SnapSize, pData: pTmpBuffer3, AltDataSize: AltSnapSize, pAltData: pAltSnapBuffer);
2157
2158 if(!Dummy)
2159 {
2160 GameClient()->ProcessDemoSnapshot(pSnap: pTmpBuffer3);
2161
2162 unsigned char aSnapSeven[CSnapshot::MAX_SIZE];
2163 CSnapshot *pSnapSeven = (CSnapshot *)aSnapSeven;
2164 int DemoSnapSize = SnapSize;
2165 if(IsSixup())
2166 {
2167 DemoSnapSize = GameClient()->OnDemoRecSnap7(pFrom: pTmpBuffer3, pTo: pSnapSeven, Conn);
2168 if(DemoSnapSize < 0)
2169 {
2170 dbg_msg(sys: "sixup", fmt: "demo snapshot failed. error=%d", DemoSnapSize);
2171 }
2172 }
2173
2174 if(DemoSnapSize >= 0)
2175 {
2176 // add snapshot to demo
2177 for(auto &DemoRecorder : m_aDemoRecorder)
2178 {
2179 if(DemoRecorder.IsRecording())
2180 {
2181 // write snapshot
2182 DemoRecorder.RecordSnapshot(Tick: GameTick, pData: IsSixup() ? pSnapSeven : pTmpBuffer3, Size: DemoSnapSize);
2183 }
2184 }
2185 }
2186 }
2187
2188 // apply snapshot, cycle pointers
2189 m_aReceivedSnapshots[Conn]++;
2190
2191 // we got two snapshots until we see us self as connected
2192 if(m_aReceivedSnapshots[Conn] == 2)
2193 {
2194 // start at 200ms and work from there
2195 if(!Dummy)
2196 {
2197 m_PredictedTime.Init(Target: GameTick * time_freq() / GameTickSpeed());
2198 m_PredictedTime.SetAdjustSpeed(Direction: CSmoothTime::ADJUSTDIRECTION_UP, Value: 1000.0f);
2199 m_PredictedTime.UpdateMargin(Margin: PredictionMargin() * time_freq() / 1000);
2200 }
2201 m_aGameTime[Conn].Init(Target: (GameTick - 1) * time_freq() / GameTickSpeed());
2202 m_aapSnapshots[Conn][SNAP_PREV] = m_aSnapshotStorage[Conn].m_pFirst;
2203 m_aapSnapshots[Conn][SNAP_CURRENT] = m_aSnapshotStorage[Conn].m_pLast;
2204 m_aPrevGameTick[Conn] = m_aapSnapshots[Conn][SNAP_PREV]->m_Tick;
2205 m_aCurGameTick[Conn] = m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick;
2206 if(Conn == CONN_MAIN)
2207 {
2208 m_LocalStartTime = time_get();
2209#if defined(CONF_VIDEORECORDER)
2210 IVideo::SetLocalStartTime(m_LocalStartTime);
2211#endif
2212 }
2213 if(!Dummy)
2214 {
2215 GameClient()->OnNewSnapshot();
2216 }
2217 SetState(IClient::STATE_ONLINE);
2218 if(!Dummy)
2219 {
2220 DemoRecorder_HandleAutoStart();
2221 }
2222 }
2223
2224 // adjust game time
2225 if(m_aReceivedSnapshots[Conn] > 2)
2226 {
2227 int64_t Now = m_aGameTime[Conn].Get(Now: time_get());
2228 int64_t TickStart = GameTick * time_freq() / GameTickSpeed();
2229 int64_t TimeLeft = (TickStart - Now) * 1000 / time_freq();
2230 m_aGameTime[Conn].Update(pGraph: &m_aGametimeMarginGraphs[Conn], Target: (GameTick - 1) * time_freq() / GameTickSpeed(), TimeLeft, AdjustDirection: CSmoothTime::ADJUSTDIRECTION_DOWN);
2231 }
2232
2233 if(m_aReceivedSnapshots[Conn] > GameTickSpeed() && !m_aDidPostConnect[Conn])
2234 {
2235 OnPostConnect(Conn);
2236 m_aDidPostConnect[Conn] = true;
2237 }
2238
2239 // ack snapshot
2240 m_aAckGameTick[Conn] = GameTick;
2241 }
2242 }
2243 }
2244 else if(Conn == CONN_MAIN && Msg == NETMSG_RCONTYPE)
2245 {
2246 bool UsernameReq = Unpacker.GetInt() & 1;
2247 if(!Unpacker.Error())
2248 {
2249 GameClient()->OnRconType(UsernameReq);
2250 }
2251 }
2252 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_START)
2253 {
2254 const int ExpectedRconCommands = Unpacker.GetInt();
2255 if(Unpacker.Error() || ExpectedRconCommands < 0)
2256 return;
2257
2258 m_ExpectedRconCommands = ExpectedRconCommands;
2259 m_GotRconCommands = 0;
2260 }
2261 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_END)
2262 {
2263 m_ExpectedRconCommands = -1;
2264 }
2265 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_ADD)
2266 {
2267 while(true)
2268 {
2269 const char *pMapName = Unpacker.GetString(SanitizeType: CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
2270 if(Unpacker.Error())
2271 {
2272 return;
2273 }
2274 if(pMapName[0] != '\0')
2275 {
2276 m_vMaplistEntries.emplace_back(args&: pMapName);
2277 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
2278 }
2279 }
2280 }
2281 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_GROUP_START)
2282 {
2283 const int ExpectedMaplistEntries = Unpacker.GetInt();
2284 if(Unpacker.Error() || ExpectedMaplistEntries < 0)
2285 return;
2286
2287 m_vMaplistEntries.clear();
2288 GameClient()->ForceUpdateConsoleRemoteCompletionSuggestions();
2289 m_ExpectedMaplistEntries = ExpectedMaplistEntries;
2290 }
2291 else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAPLIST_GROUP_END)
2292 {
2293 m_ExpectedMaplistEntries = -1;
2294 }
2295 }
2296 // the client handles only vital messages https://github.com/ddnet/ddnet/issues/11178
2297 else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 || Msg == NETMSGTYPE_SV_PREINPUT)
2298 {
2299 // game message
2300 if(!Dummy)
2301 {
2302 for(auto &DemoRecorder : m_aDemoRecorder)
2303 if(DemoRecorder.IsRecording())
2304 DemoRecorder.RecordMessage(pData: pPacket->m_pData, Size: pPacket->m_DataSize);
2305 }
2306
2307 GameClient()->OnMessage(MsgId: Msg, pUnpacker: &Unpacker, Conn, Dummy);
2308 }
2309}
2310
2311int CClient::UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo)
2312{
2313 CUnpacker Unpacker;
2314 CSnapshotBuilder Builder;
2315 Builder.Init();
2316 CNetObjHandler *pNetObjHandler = GameClient()->GetNetObjHandler();
2317
2318 int Num = pFrom->NumItems();
2319 for(int Index = 0; Index < Num; Index++)
2320 {
2321 const CSnapshotItem *pFromItem = pFrom->GetItem(Index);
2322 const int FromItemSize = pFrom->GetItemSize(Index);
2323 const int ItemType = pFrom->GetItemType(Index);
2324 const void *pData = pFromItem->Data();
2325 Unpacker.Reset(pData, Size: FromItemSize);
2326
2327 void *pRawObj = pNetObjHandler->SecureUnpackObj(Type: ItemType, pUnpacker: &Unpacker);
2328 if(!pRawObj)
2329 {
2330 if(g_Config.m_Debug && ItemType != UUID_UNKNOWN)
2331 {
2332 char aBuf[256];
2333 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "dropped weird object '%s' (%d), failed on '%s'", pNetObjHandler->GetObjName(Type: ItemType), ItemType, pNetObjHandler->FailedObjOn());
2334 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
2335 }
2336 continue;
2337 }
2338 const int ItemSize = pNetObjHandler->GetUnpackedObjSize(Type: ItemType);
2339
2340 void *pObj = Builder.NewItem(Type: pFromItem->Type(), Id: pFromItem->Id(), Size: ItemSize);
2341 if(!pObj)
2342 return -4;
2343
2344 mem_copy(dest: pObj, source: pRawObj, size: ItemSize);
2345 }
2346
2347 return Builder.Finish(pSnapdata: pTo);
2348}
2349
2350void CClient::ResetMapDownload(bool ResetActive)
2351{
2352 if(m_pMapdownloadTask)
2353 {
2354 m_pMapdownloadTask->Abort();
2355 m_pMapdownloadTask = nullptr;
2356 }
2357
2358 if(m_MapdownloadFileTemp)
2359 {
2360 io_close(io: m_MapdownloadFileTemp);
2361 m_MapdownloadFileTemp = nullptr;
2362 }
2363
2364 if(Storage()->FileExists(pFilename: m_aMapdownloadFilenameTemp, Type: IStorage::TYPE_SAVE))
2365 {
2366 Storage()->RemoveFile(pFilename: m_aMapdownloadFilenameTemp, Type: IStorage::TYPE_SAVE);
2367 }
2368
2369 if(ResetActive)
2370 {
2371 m_MapdownloadChunk = 0;
2372 m_MapdownloadSha256Present = false;
2373 m_MapdownloadSha256 = SHA256_ZEROED;
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 SHA256_DIGEST *pSha256 = m_MapdownloadSha256Present ? &m_MapdownloadSha256 : nullptr;
2388
2389 bool FileSuccess = true;
2390 FileSuccess &= Storage()->RemoveFile(pFilename: m_aMapdownloadFilename, Type: IStorage::TYPE_SAVE);
2391 FileSuccess &= Storage()->RenameFile(pOldFilename: m_aMapdownloadFilenameTemp, pNewFilename: m_aMapdownloadFilename, Type: IStorage::TYPE_SAVE);
2392 if(!FileSuccess)
2393 {
2394 char aError[128 + IO_MAX_PATH_LENGTH];
2395 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Could not save downloaded map. Try manually deleting this file: %s"), m_aMapdownloadFilename);
2396 DisconnectWithReason(pReason: aError);
2397 return;
2398 }
2399
2400 const char *pError = LoadMap(pName: m_aMapdownloadName, pFilename: m_aMapdownloadFilename, pWantedSha256: pSha256, WantedCrc: m_MapdownloadCrc);
2401 if(!pError)
2402 {
2403 ResetMapDownload(ResetActive: true);
2404 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client/network", pStr: "loading done");
2405 SendReady(Conn: CONN_MAIN);
2406 }
2407 else if(m_pMapdownloadTask) // fallback
2408 {
2409 ResetMapDownload(ResetActive: false);
2410 SendMapRequest();
2411 }
2412 else
2413 {
2414 DisconnectWithReason(pReason: pError);
2415 }
2416}
2417
2418void CClient::ResetDDNetInfoTask()
2419{
2420 if(m_pDDNetInfoTask)
2421 {
2422 m_pDDNetInfoTask->Abort();
2423 m_pDDNetInfoTask = nullptr;
2424 }
2425}
2426
2427typedef std::tuple<int, int, int> TVersion;
2428static const TVersion gs_InvalidVersion = std::make_tuple(args: -1, args: -1, args: -1);
2429
2430static TVersion ToVersion(char *pStr)
2431{
2432 int aVersion[3] = {0, 0, 0};
2433 const char *p = strtok(s: pStr, delim: ".");
2434
2435 for(int i = 0; i < 3 && p; ++i)
2436 {
2437 if(!str_isallnum(str: p))
2438 return gs_InvalidVersion;
2439
2440 aVersion[i] = str_toint(str: p);
2441 p = strtok(s: nullptr, delim: ".");
2442 }
2443
2444 if(p)
2445 return gs_InvalidVersion;
2446
2447 return std::make_tuple(args&: aVersion[0], args&: aVersion[1], args&: aVersion[2]);
2448}
2449
2450void CClient::LoadDDNetInfo()
2451{
2452 const json_value *pDDNetInfo = m_ServerBrowser.LoadDDNetInfo();
2453
2454 if(!pDDNetInfo)
2455 {
2456 m_InfoState = EInfoState::ERROR;
2457 return;
2458 }
2459
2460 const json_value &DDNetInfo = *pDDNetInfo;
2461 const json_value &CurrentVersion = DDNetInfo["version"];
2462 if(CurrentVersion.type == json_string)
2463 {
2464 char aNewVersionStr[64];
2465 str_copy(dst&: aNewVersionStr, src: CurrentVersion);
2466 char aCurVersionStr[64];
2467 str_copy(dst&: aCurVersionStr, GAME_RELEASE_VERSION);
2468 if(ToVersion(pStr: aNewVersionStr) > ToVersion(pStr: aCurVersionStr))
2469 {
2470 str_copy(dst&: m_aVersionStr, src: CurrentVersion);
2471 }
2472 else
2473 {
2474 m_aVersionStr[0] = '0';
2475 m_aVersionStr[1] = '\0';
2476 }
2477 }
2478
2479 const json_value &News = DDNetInfo["news"];
2480 if(News.type == json_string)
2481 {
2482 // Only mark news button if something new was added to the news
2483 if(m_aNews[0] && str_find(haystack: m_aNews, needle: News) == nullptr)
2484 g_Config.m_UiUnreadNews = true;
2485
2486 str_copy(dst&: m_aNews, src: News);
2487 }
2488
2489 const json_value &MapDownloadUrl = DDNetInfo["map-download-url"];
2490 if(MapDownloadUrl.type == json_string)
2491 {
2492 str_copy(dst&: m_aMapDownloadUrl, src: MapDownloadUrl);
2493 }
2494
2495 const json_value &Points = DDNetInfo["points"];
2496 if(Points.type == json_integer)
2497 {
2498 m_Points = Points.u.integer;
2499 }
2500
2501 const json_value &StunServersIpv6 = DDNetInfo["stun-servers-ipv6"];
2502 if(StunServersIpv6.type == json_array && StunServersIpv6[0].type == json_string)
2503 {
2504 NETADDR Addr;
2505 if(!net_addr_from_str(addr: &Addr, string: StunServersIpv6[0]))
2506 {
2507 m_aNetClient[CONN_MAIN].FeedStunServer(StunServer: Addr);
2508 }
2509 }
2510 const json_value &StunServersIpv4 = DDNetInfo["stun-servers-ipv4"];
2511 if(StunServersIpv4.type == json_array && StunServersIpv4[0].type == json_string)
2512 {
2513 NETADDR Addr;
2514 if(!net_addr_from_str(addr: &Addr, string: StunServersIpv4[0]))
2515 {
2516 m_aNetClient[CONN_MAIN].FeedStunServer(StunServer: Addr);
2517 }
2518 }
2519 const json_value &ConnectingIp = DDNetInfo["connecting-ip"];
2520 if(ConnectingIp.type == json_string)
2521 {
2522 NETADDR Addr;
2523 if(!net_addr_from_str(addr: &Addr, string: ConnectingIp))
2524 {
2525 m_HaveGlobalTcpAddr = true;
2526 m_GlobalTcpAddr = Addr;
2527 log_debug("info", "got global tcp ip address: %s", (const char *)ConnectingIp);
2528 }
2529 }
2530 const json_value &WarnPngliteIncompatibleImages = DDNetInfo["warn-pnglite-incompatible-images"];
2531 Graphics()->WarnPngliteIncompatibleImages(Warn: WarnPngliteIncompatibleImages.type == json_boolean && (bool)WarnPngliteIncompatibleImages);
2532 m_InfoState = EInfoState::SUCCESS;
2533}
2534
2535int CClient::ConnectNetTypes() const
2536{
2537 const NETADDR *pConnectAddrs;
2538 int NumConnectAddrs;
2539 m_aNetClient[CONN_MAIN].ConnectAddresses(ppAddrs: &pConnectAddrs, pNumAddrs: &NumConnectAddrs);
2540 int NetType = 0;
2541 for(int i = 0; i < NumConnectAddrs; i++)
2542 {
2543 NetType |= pConnectAddrs[i].type;
2544 }
2545 return NetType;
2546}
2547
2548void CClient::PumpNetwork()
2549{
2550 for(auto &NetClient : m_aNetClient)
2551 {
2552 NetClient.Update();
2553 }
2554
2555 if(State() != IClient::STATE_DEMOPLAYBACK)
2556 {
2557 // check for errors of main and dummy
2558 if(State() != IClient::STATE_OFFLINE && State() < IClient::STATE_QUITTING)
2559 {
2560 if(m_aNetClient[CONN_MAIN].State() == NETSTATE_OFFLINE)
2561 {
2562 // This will also disconnect the dummy, so the branch below is an `else if`
2563 Disconnect();
2564 char aBuf[256];
2565 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "offline error='%s'", m_aNetClient[CONN_MAIN].ErrorString());
2566 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf, PrintColor: gs_ClientNetworkErrPrintColor);
2567 }
2568 else if((DummyConnecting() || DummyConnected()) && m_aNetClient[CONN_DUMMY].State() == NETSTATE_OFFLINE)
2569 {
2570 const bool WasConnecting = DummyConnecting();
2571 DummyDisconnect(pReason: nullptr);
2572 char aBuf[256];
2573 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "offline dummy error='%s'", m_aNetClient[CONN_DUMMY].ErrorString());
2574 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf, PrintColor: gs_ClientNetworkErrPrintColor);
2575 if(WasConnecting)
2576 {
2577 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Could not connect dummy"), m_aNetClient[CONN_DUMMY].ErrorString());
2578 GameClient()->Echo(pString: aBuf);
2579 }
2580 }
2581 }
2582
2583 // check if main was connected
2584 if(State() == IClient::STATE_CONNECTING && m_aNetClient[CONN_MAIN].State() == NETSTATE_ONLINE)
2585 {
2586 // we switched to online
2587 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: "connected, sending info", PrintColor: gs_ClientNetworkPrintColor);
2588 SetState(IClient::STATE_LOADING);
2589 SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_INITIAL);
2590 SendInfo(Conn: CONN_MAIN);
2591 }
2592
2593 // progress on dummy connect when the connection is online
2594 if(m_DummySendConnInfo && m_aNetClient[CONN_DUMMY].State() == NETSTATE_ONLINE)
2595 {
2596 m_DummySendConnInfo = false;
2597 SendInfo(Conn: CONN_DUMMY);
2598 m_aNetClient[CONN_DUMMY].Update();
2599 SendReady(Conn: CONN_DUMMY);
2600 GameClient()->SendDummyInfo(Start: true);
2601 SendEnterGame(Conn: CONN_DUMMY);
2602 }
2603 }
2604
2605 // process packets
2606 CNetChunk Packet;
2607 SECURITY_TOKEN ResponseToken;
2608 for(int Conn = 0; Conn < NUM_CONNS; Conn++)
2609 {
2610 while(m_aNetClient[Conn].Recv(pChunk: &Packet, pResponseToken: &ResponseToken, Sixup: IsSixup()))
2611 {
2612 if(Packet.m_ClientId == -1)
2613 {
2614 if(ResponseToken != NET_SECURITY_TOKEN_UNKNOWN)
2615 PreprocessConnlessPacket7(pPacket: &Packet);
2616
2617 ProcessConnlessPacket(pPacket: &Packet);
2618 continue;
2619 }
2620 if(Conn == CONN_MAIN || Conn == CONN_DUMMY)
2621 {
2622 ProcessServerPacket(pPacket: &Packet, Conn, Dummy: g_Config.m_ClDummy ^ Conn);
2623 }
2624 }
2625 }
2626}
2627
2628void CClient::OnDemoPlayerSnapshot(void *pData, int Size)
2629{
2630 // update ticks, they could have changed
2631 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
2632 m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
2633 m_aPrevGameTick[0] = pInfo->m_PreviousTick;
2634
2635 // create a verified and unpacked snapshot
2636 unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
2637 CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
2638 int AltSnapSize;
2639
2640 if(IsSixup())
2641 {
2642 AltSnapSize = GameClient()->TranslateSnap(pSnapDstSix: pAltSnapBuffer, pSnapSrcSeven: (CSnapshot *)pData, Conn: CONN_MAIN, Dummy: false);
2643 if(AltSnapSize < 0)
2644 {
2645 dbg_msg(sys: "sixup", fmt: "failed to translate snapshot. error=%d", AltSnapSize);
2646 return;
2647 }
2648 }
2649 else
2650 {
2651 AltSnapSize = UnpackAndValidateSnapshot(pFrom: (CSnapshot *)pData, pTo: pAltSnapBuffer);
2652 if(AltSnapSize < 0)
2653 {
2654 dbg_msg(sys: "client", fmt: "unpack snapshot and validate failed. error=%d", AltSnapSize);
2655 return;
2656 }
2657 }
2658
2659 // handle snapshots after validation
2660 std::swap(a&: m_aapSnapshots[0][SNAP_PREV], b&: m_aapSnapshots[0][SNAP_CURRENT]);
2661 mem_copy(dest: m_aapSnapshots[0][SNAP_CURRENT]->m_pSnap, source: pData, size: Size);
2662 mem_copy(dest: m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, source: pAltSnapBuffer, size: AltSnapSize);
2663
2664 GameClient()->OnNewSnapshot();
2665}
2666
2667void CClient::OnDemoPlayerMessage(void *pData, int Size)
2668{
2669 CUnpacker Unpacker;
2670 Unpacker.Reset(pData, Size);
2671 CMsgPacker Packer(NETMSG_EX, true);
2672
2673 // unpack msgid and system flag
2674 int Msg;
2675 bool Sys;
2676 CUuid Uuid;
2677
2678 int Result = UnpackMessageId(pId: &Msg, pSys: &Sys, pUuid: &Uuid, pUnpacker: &Unpacker, pPacker: &Packer);
2679 if(Result == UNPACKMESSAGE_ERROR)
2680 {
2681 return;
2682 }
2683
2684 if(!Sys)
2685 GameClient()->OnMessage(MsgId: Msg, pUnpacker: &Unpacker, Conn: CONN_MAIN, Dummy: false);
2686}
2687
2688void CClient::UpdateDemoIntraTimers()
2689{
2690 // update timers
2691 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
2692 m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
2693 m_aPrevGameTick[0] = pInfo->m_PreviousTick;
2694 m_aGameIntraTick[0] = pInfo->m_IntraTick;
2695 m_aGameTickTime[0] = pInfo->m_TickTime;
2696 m_aGameIntraTickSincePrev[0] = pInfo->m_IntraTickSincePrev;
2697}
2698
2699void CClient::Update()
2700{
2701 PumpNetwork();
2702
2703 if(State() == IClient::STATE_DEMOPLAYBACK)
2704 {
2705 if(m_DemoPlayer.IsPlaying())
2706 {
2707#if defined(CONF_VIDEORECORDER)
2708 if(IVideo::Current())
2709 {
2710 IVideo::Current()->NextVideoFrame();
2711 IVideo::Current()->NextAudioFrameTimeline(Mix: [this](short *pFinalOut, unsigned Frames) {
2712 Sound()->Mix(pFinalOut, Frames);
2713 });
2714 }
2715#endif
2716
2717 m_DemoPlayer.Update();
2718
2719 // update timers
2720 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
2721 m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
2722 m_aPrevGameTick[0] = pInfo->m_PreviousTick;
2723 m_aGameIntraTick[0] = pInfo->m_IntraTick;
2724 m_aGameTickTime[0] = pInfo->m_TickTime;
2725 }
2726 else
2727 {
2728 // Disconnect when demo playback stopped, either due to playback error
2729 // or because the end of the demo was reached when rendering it.
2730 DisconnectWithReason(pReason: m_DemoPlayer.ErrorMessage());
2731 if(m_DemoPlayer.ErrorMessage()[0] != '\0')
2732 {
2733 SWarning Warning(Localize(pStr: "Error playing demo"), m_DemoPlayer.ErrorMessage());
2734 Warning.m_AutoHide = false;
2735 AddWarning(Warning);
2736 }
2737 }
2738 }
2739 else if(State() == IClient::STATE_ONLINE)
2740 {
2741 if(m_LastDummy != (bool)g_Config.m_ClDummy)
2742 {
2743 // Invalidate references to !m_ClDummy snapshots
2744 GameClient()->InvalidateSnapshot();
2745 GameClient()->OnDummySwap();
2746 }
2747
2748 if(m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT])
2749 {
2750 // switch dummy snapshot
2751 int64_t Now = m_aGameTime[!g_Config.m_ClDummy].Get(Now: time_get());
2752 while(true)
2753 {
2754 if(!m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext)
2755 break;
2756 int64_t TickStart = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
2757 if(TickStart >= Now)
2758 break;
2759
2760 m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT];
2761 m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext;
2762
2763 // set ticks
2764 m_aCurGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick;
2765 m_aPrevGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV]->m_Tick;
2766 }
2767 }
2768
2769 if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT])
2770 {
2771 // switch snapshot
2772 bool Repredict = false;
2773 int64_t Now = m_aGameTime[g_Config.m_ClDummy].Get(Now: time_get());
2774 int64_t PredNow = m_PredictedTime.Get(Now: time_get());
2775
2776 if(m_LastDummy != (bool)g_Config.m_ClDummy && m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV])
2777 {
2778 // Load snapshot for m_ClDummy
2779 GameClient()->OnNewSnapshot();
2780 Repredict = true;
2781 }
2782
2783 while(true)
2784 {
2785 if(!m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext)
2786 break;
2787 int64_t TickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
2788 if(TickStart >= Now)
2789 break;
2790
2791 m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT];
2792 m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext;
2793
2794 // set ticks
2795 m_aCurGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick;
2796 m_aPrevGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick;
2797
2798 GameClient()->OnNewSnapshot();
2799 Repredict = true;
2800 }
2801
2802 if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV])
2803 {
2804 int64_t CurTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
2805 int64_t PrevTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick * time_freq() / GameTickSpeed();
2806 int PrevPredTick = (int)(PredNow * GameTickSpeed() / time_freq());
2807 int NewPredTick = PrevPredTick + 1;
2808
2809 m_aGameIntraTick[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)(CurTickStart - PrevTickStart);
2810 m_aGameTickTime[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)time_freq();
2811 m_aGameIntraTickSincePrev[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)(time_freq() / GameTickSpeed());
2812
2813 int64_t CurPredTickStart = NewPredTick * time_freq() / GameTickSpeed();
2814 int64_t PrevPredTickStart = PrevPredTick * time_freq() / GameTickSpeed();
2815 m_aPredIntraTick[g_Config.m_ClDummy] = (PredNow - PrevPredTickStart) / (float)(CurPredTickStart - PrevPredTickStart);
2816
2817 if(absolute(a: NewPredTick - m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick) > MaxLatencyTicks())
2818 {
2819 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: "prediction time reset!");
2820 m_PredictedTime.Init(Target: CurTickStart + 2 * time_freq() / GameTickSpeed());
2821 }
2822
2823 if(NewPredTick > m_aPredTick[g_Config.m_ClDummy])
2824 {
2825 m_aPredTick[g_Config.m_ClDummy] = NewPredTick;
2826 Repredict = true;
2827
2828 // send input
2829 SendInput();
2830 }
2831 }
2832
2833 // only do sane predictions
2834 if(Repredict)
2835 {
2836 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())
2837 GameClient()->OnPredict();
2838 }
2839
2840 // fetch server info if we don't have it
2841 if(m_CurrentServerInfoRequestTime >= 0 &&
2842 time_get() > m_CurrentServerInfoRequestTime)
2843 {
2844 m_ServerBrowser.RequestCurrentServer(Addr: ServerAddress());
2845 m_CurrentServerInfoRequestTime = time_get() + time_freq() * 2;
2846 }
2847
2848 // periodically ping server
2849 if(m_CurrentServerNextPingTime >= 0 &&
2850 time_get() > m_CurrentServerNextPingTime)
2851 {
2852 int64_t NowPing = time_get();
2853 int64_t Freq = time_freq();
2854
2855 char aBuf[64];
2856 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "pinging current server%s", !m_ServerCapabilities.m_PingEx ? ", using fallback via server info" : "");
2857 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "client", pStr: aBuf);
2858
2859 m_CurrentServerPingUuid = RandomUuid();
2860 if(!m_ServerCapabilities.m_PingEx)
2861 {
2862 m_ServerBrowser.RequestCurrentServerWithRandomToken(Addr: ServerAddress(), pBasicToken: &m_CurrentServerPingBasicToken, pToken: &m_CurrentServerPingToken);
2863 }
2864 else
2865 {
2866 CMsgPacker Msg(NETMSG_PINGEX, true);
2867 Msg.AddRaw(pData: &m_CurrentServerPingUuid, Size: sizeof(m_CurrentServerPingUuid));
2868 SendMsg(Conn: CONN_MAIN, pMsg: &Msg, Flags: MSGFLAG_FLUSH);
2869 }
2870 m_CurrentServerCurrentPingTime = NowPing;
2871 m_CurrentServerNextPingTime = NowPing + 600 * Freq; // ping every 10 minutes
2872 }
2873 }
2874
2875 if(m_DummyDeactivateOnReconnect && g_Config.m_ClDummy == 1)
2876 {
2877 m_DummyDeactivateOnReconnect = false;
2878 g_Config.m_ClDummy = 0;
2879 }
2880 else if(!m_DummyConnected && m_DummyDeactivateOnReconnect)
2881 {
2882 m_DummyDeactivateOnReconnect = false;
2883 }
2884
2885 m_LastDummy = (bool)g_Config.m_ClDummy;
2886 }
2887
2888 // STRESS TEST: join the server again
2889#ifdef CONF_DEBUG
2890 if(g_Config.m_DbgStress)
2891 {
2892 static int64_t s_ActionTaken = 0;
2893 int64_t Now = time_get();
2894 if(State() == IClient::STATE_OFFLINE)
2895 {
2896 if(Now > s_ActionTaken + time_freq() * 2)
2897 {
2898 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "stress", pStr: "reconnecting!");
2899 Connect(pAddress: g_Config.m_DbgStressServer);
2900 s_ActionTaken = Now;
2901 }
2902 }
2903 else
2904 {
2905 if(Now > s_ActionTaken + time_freq() * (10 + g_Config.m_DbgStress))
2906 {
2907 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "stress", pStr: "disconnecting!");
2908 Disconnect();
2909 s_ActionTaken = Now;
2910 }
2911 }
2912 }
2913#endif
2914
2915 if(m_pMapdownloadTask)
2916 {
2917 if(m_pMapdownloadTask->State() == EHttpState::DONE)
2918 FinishMapDownload();
2919 else if(m_pMapdownloadTask->State() == EHttpState::ERROR || m_pMapdownloadTask->State() == EHttpState::ABORTED)
2920 {
2921 dbg_msg(sys: "webdl", fmt: "http failed, falling back to gameserver");
2922 ResetMapDownload(ResetActive: false);
2923 SendMapRequest();
2924 }
2925 }
2926
2927 if(m_pDDNetInfoTask)
2928 {
2929 if(m_pDDNetInfoTask->State() == EHttpState::DONE)
2930 {
2931 if(m_ServerBrowser.DDNetInfoSha256() == m_pDDNetInfoTask->ResultSha256())
2932 {
2933 log_debug("client/info", "DDNet info already up-to-date");
2934 m_InfoState = EInfoState::SUCCESS;
2935 }
2936 else
2937 {
2938 log_debug("client/info", "Loading new DDNet info");
2939 LoadDDNetInfo();
2940 }
2941
2942 ResetDDNetInfoTask();
2943 }
2944 else if(m_pDDNetInfoTask->State() == EHttpState::ERROR || m_pDDNetInfoTask->State() == EHttpState::ABORTED)
2945 {
2946 ResetDDNetInfoTask();
2947 m_InfoState = EInfoState::ERROR;
2948 }
2949 }
2950
2951 if(State() == IClient::STATE_ONLINE)
2952 {
2953 if(!m_EditJobs.empty())
2954 {
2955 std::shared_ptr<CDemoEdit> pJob = m_EditJobs.front();
2956 if(pJob->State() == IJob::STATE_DONE)
2957 {
2958 char aBuf[IO_MAX_PATH_LENGTH + 64];
2959 if(pJob->Success())
2960 {
2961 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Successfully saved the replay to '%s'!", pJob->Destination());
2962 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: aBuf);
2963
2964 GameClient()->Echo(pString: Localize(pStr: "Successfully saved the replay!"));
2965 }
2966 else
2967 {
2968 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Failed saving the replay to '%s'...", pJob->Destination());
2969 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: aBuf);
2970
2971 GameClient()->Echo(pString: Localize(pStr: "Failed saving the replay!"));
2972 }
2973 m_EditJobs.pop_front();
2974 }
2975 }
2976 }
2977
2978 // update the server browser
2979 m_ServerBrowser.Update();
2980
2981 // update editor/gameclient
2982 if(m_EditorActive)
2983 m_pEditor->OnUpdate();
2984 else
2985 GameClient()->OnUpdate();
2986
2987 Discord()->Update();
2988 Steam()->Update();
2989 if(Steam()->GetConnectAddress())
2990 {
2991 HandleConnectAddress(pAddr: Steam()->GetConnectAddress());
2992 Steam()->ClearConnectAddress();
2993 }
2994
2995 if(m_ReconnectTime > 0 && time_get() > m_ReconnectTime)
2996 {
2997 if(State() != STATE_ONLINE)
2998 Connect(pAddress: m_aConnectAddressStr);
2999 m_ReconnectTime = 0;
3000 }
3001
3002 m_PredictedTime.UpdateMargin(Margin: PredictionMargin() * time_freq() / 1000);
3003}
3004
3005void CClient::RegisterInterfaces()
3006{
3007 Kernel()->RegisterInterface(pInterface: static_cast<IDemoRecorder *>(&m_aDemoRecorder[RECORDER_MANUAL]), Destroy: false);
3008 Kernel()->RegisterInterface(pInterface: static_cast<IDemoPlayer *>(&m_DemoPlayer), Destroy: false);
3009 Kernel()->RegisterInterface(pInterface: static_cast<IGhostRecorder *>(&m_GhostRecorder), Destroy: false);
3010 Kernel()->RegisterInterface(pInterface: static_cast<IGhostLoader *>(&m_GhostLoader), Destroy: false);
3011 Kernel()->RegisterInterface(pInterface: static_cast<IServerBrowser *>(&m_ServerBrowser), Destroy: false);
3012#if defined(CONF_AUTOUPDATE)
3013 Kernel()->RegisterInterface(pInterface: static_cast<IUpdater *>(&m_Updater), Destroy: false);
3014#endif
3015 Kernel()->RegisterInterface(pInterface: static_cast<IFriends *>(&m_Friends), Destroy: false);
3016 Kernel()->ReregisterInterface(pInterface: static_cast<IFriends *>(&m_Foes));
3017 Kernel()->RegisterInterface(pInterface: static_cast<IHttp *>(&m_Http), Destroy: false);
3018}
3019
3020void CClient::InitInterfaces()
3021{
3022 // fetch interfaces
3023 m_pEngine = Kernel()->RequestInterface<IEngine>();
3024 m_pEditor = Kernel()->RequestInterface<IEditor>();
3025 m_pFavorites = Kernel()->RequestInterface<IFavorites>();
3026 m_pSound = Kernel()->RequestInterface<IEngineSound>();
3027 m_pGameClient = Kernel()->RequestInterface<IGameClient>();
3028 m_pInput = Kernel()->RequestInterface<IEngineInput>();
3029 m_pMap = Kernel()->RequestInterface<IEngineMap>();
3030 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
3031 m_pConfig = m_pConfigManager->Values();
3032#if defined(CONF_AUTOUPDATE)
3033 m_pUpdater = Kernel()->RequestInterface<IUpdater>();
3034#endif
3035 m_pDiscord = Kernel()->RequestInterface<IDiscord>();
3036 m_pSteam = Kernel()->RequestInterface<ISteam>();
3037 m_pNotifications = Kernel()->RequestInterface<INotifications>();
3038 m_pStorage = Kernel()->RequestInterface<IStorage>();
3039
3040 m_DemoEditor.Init(pSnapshotDelta: &m_SnapshotDelta, pConsole: m_pConsole, pStorage: m_pStorage);
3041
3042 m_ServerBrowser.SetBaseInfo(pClient: &m_aNetClient[CONN_CONTACT], pNetVersion: m_pGameClient->NetVersion());
3043
3044#if defined(CONF_AUTOUPDATE)
3045 m_Updater.Init(pHttp: &m_Http);
3046#endif
3047
3048 m_pConfigManager->RegisterCallback(pfnFunc: IFavorites::ConfigSaveCallback, pUserData: m_pFavorites);
3049 m_Friends.Init();
3050 m_Foes.Init(Foes: true);
3051
3052 m_GhostRecorder.Init();
3053 m_GhostLoader.Init();
3054}
3055
3056void CClient::Run()
3057{
3058 m_LocalStartTime = m_GlobalStartTime = time_get();
3059#if defined(CONF_VIDEORECORDER)
3060 IVideo::SetLocalStartTime(m_LocalStartTime);
3061#endif
3062 m_aSnapshotParts[0] = 0;
3063 m_aSnapshotParts[1] = 0;
3064
3065 if(m_GenerateTimeoutSeed)
3066 {
3067 GenerateTimeoutSeed();
3068 }
3069
3070 unsigned int Seed;
3071 secure_random_fill(bytes: &Seed, length: sizeof(Seed));
3072 srand(seed: Seed);
3073
3074 if(g_Config.m_Debug)
3075 {
3076 g_UuidManager.DebugDump();
3077 }
3078
3079 char aNetworkError[256];
3080 if(!InitNetworkClient(pError: aNetworkError, ErrorSize: sizeof(aNetworkError)))
3081 {
3082 log_error("client", "%s", aNetworkError);
3083 ShowMessageBox(MessageBox: {.m_pTitle = "Network Error", .m_pMessage = aNetworkError});
3084 return;
3085 }
3086
3087 if(!m_Http.Init(ShutdownDelay: std::chrono::seconds{1}))
3088 {
3089 const char *pErrorMessage = "Failed to initialize the HTTP client.";
3090 log_error("client", "%s", pErrorMessage);
3091 ShowMessageBox(MessageBox: {.m_pTitle = "HTTP Error", .m_pMessage = pErrorMessage});
3092 return;
3093 }
3094
3095 // init graphics
3096 m_pGraphics = CreateEngineGraphicsThreaded();
3097 Kernel()->RegisterInterface(pInterface: m_pGraphics); // IEngineGraphics
3098 Kernel()->RegisterInterface(pInterface: static_cast<IGraphics *>(m_pGraphics), Destroy: false);
3099 {
3100 CMemoryLogger MemoryLogger;
3101 MemoryLogger.SetParent(log_get_scope_logger());
3102 bool Success;
3103 {
3104 CLogScope LogScope(&MemoryLogger);
3105 Success = m_pGraphics->Init() == 0;
3106 }
3107 if(!Success)
3108 {
3109 log_error("client", "Failed to initialize the graphics (see details above)");
3110 std::string Message = std::string("Failed to initialize the graphics. See details below.\n\n") + MemoryLogger.ConcatenatedLines();
3111 ShowMessageBox(MessageBox: {.m_pTitle = "Graphics Error", .m_pMessage = Message.c_str()});
3112 return;
3113 }
3114 }
3115
3116 // make sure the first frame just clears everything to prevent undesired colors when waiting for io
3117 Graphics()->Clear(r: 0, g: 0, b: 0);
3118 Graphics()->Swap();
3119
3120 // init localization first, making sure all errors during init can be localized
3121 GameClient()->InitializeLanguage();
3122
3123 // init sound, allowed to fail
3124 const bool SoundInitFailed = Sound()->Init() != 0;
3125
3126#if defined(CONF_VIDEORECORDER)
3127 // init video recorder aka ffmpeg
3128 CVideo::Init();
3129#endif
3130
3131 // init text render
3132 m_pTextRender = Kernel()->RequestInterface<IEngineTextRender>();
3133 m_pTextRender->Init();
3134
3135 // init the input
3136 Input()->Init();
3137
3138 // init the editor
3139 m_pEditor->Init();
3140
3141 m_ServerBrowser.OnInit();
3142 // loads the existing ddnet info file if it exists
3143 LoadDDNetInfo();
3144
3145 LoadDebugFont();
3146
3147 if(Steam()->GetPlayerName())
3148 {
3149 str_copy(dst&: g_Config.m_SteamName, src: Steam()->GetPlayerName());
3150 }
3151
3152 Graphics()->AddWindowResizeListener(pFunc: [this] { OnWindowResize(); });
3153
3154 GameClient()->OnInit();
3155
3156 m_Fifo.Init(pConsole: m_pConsole, pFifoFile: g_Config.m_ClInputFifo, Flag: CFGFLAG_CLIENT);
3157
3158 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));
3159 if(GIT_SHORTREV_HASH)
3160 {
3161 char aBuf[64];
3162 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "git revision hash: %s", GIT_SHORTREV_HASH);
3163 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf, PrintColor: ColorRGBA(0.7f, 0.7f, 1.0f, 1.0f));
3164 }
3165
3166 //
3167 m_FpsGraph.Init(Min: 0.0f, Max: 120.0f);
3168
3169 // never start with the editor
3170 g_Config.m_ClEditor = 0;
3171
3172 // process pending commands
3173 m_pConsole->StoreCommands(Store: false);
3174
3175 InitChecksum();
3176 m_pConsole->InitChecksum(pData: ChecksumData());
3177
3178 // request the new ddnet info from server if already past the welcome dialog
3179 if(g_Config.m_ClShowWelcome)
3180 g_Config.m_ClShowWelcome = 0;
3181 else
3182 RequestDDNetInfo();
3183
3184 if(SoundInitFailed)
3185 {
3186 SWarning Warning(Localize(pStr: "Sound error"), Localize(pStr: "The audio device couldn't be initialised."));
3187 Warning.m_AutoHide = false;
3188 AddWarning(Warning);
3189 }
3190
3191 bool LastD = false;
3192 bool LastE = false;
3193 bool LastG = false;
3194
3195 auto LastTime = time_get_nanoseconds();
3196 int64_t LastRenderTime = time_get();
3197
3198 while(true)
3199 {
3200 set_new_tick();
3201
3202 // handle pending connects
3203 if(m_aCmdConnect[0])
3204 {
3205 str_copy(dst&: g_Config.m_UiServerAddress, src: m_aCmdConnect);
3206 Connect(pAddress: m_aCmdConnect);
3207 m_aCmdConnect[0] = 0;
3208 }
3209
3210 // handle pending demo play
3211 if(m_aCmdPlayDemo[0])
3212 {
3213 const char *pError = DemoPlayer_Play(pFilename: m_aCmdPlayDemo, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
3214 if(pError)
3215 log_error("demo_player", "playing passed demo file '%s' failed: %s", m_aCmdPlayDemo, pError);
3216 m_aCmdPlayDemo[0] = 0;
3217 }
3218
3219 // handle pending map edits
3220 if(m_aCmdEditMap[0])
3221 {
3222 int Result = m_pEditor->HandleMapDrop(pFilename: m_aCmdEditMap, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
3223 if(Result)
3224 g_Config.m_ClEditor = true;
3225 else
3226 log_error("editor", "editing passed map file '%s' failed", m_aCmdEditMap);
3227 m_aCmdEditMap[0] = 0;
3228 }
3229
3230 // update input
3231 if(Input()->Update())
3232 {
3233 if(State() == IClient::STATE_QUITTING)
3234 break;
3235 else
3236 SetState(IClient::STATE_QUITTING); // SDL_QUIT
3237 }
3238
3239 char aFile[IO_MAX_PATH_LENGTH];
3240 if(Input()->GetDropFile(aBuf: aFile, Len: sizeof(aFile)))
3241 {
3242 if(str_startswith(str: aFile, CONNECTLINK_NO_SLASH))
3243 HandleConnectLink(pLink: aFile);
3244 else if(str_endswith(str: aFile, suffix: ".demo"))
3245 HandleDemoPath(pPath: aFile);
3246 else if(str_endswith(str: aFile, suffix: ".map"))
3247 HandleMapPath(pPath: aFile);
3248 }
3249
3250#if defined(CONF_AUTOUPDATE)
3251 Updater()->Update();
3252#endif
3253
3254 // update sound
3255 Sound()->Update();
3256
3257 if(CtrlShiftKey(Key: KEY_D, Last&: LastD))
3258 g_Config.m_Debug ^= 1;
3259
3260 if(CtrlShiftKey(Key: KEY_G, Last&: LastG))
3261 g_Config.m_DbgGraphs ^= 1;
3262
3263 if(CtrlShiftKey(Key: KEY_E, Last&: LastE))
3264 {
3265 if(g_Config.m_ClEditor)
3266 m_pEditor->OnClose();
3267 g_Config.m_ClEditor = g_Config.m_ClEditor ^ 1;
3268 }
3269
3270 // render
3271 {
3272 if(g_Config.m_ClEditor)
3273 {
3274 if(!m_EditorActive)
3275 {
3276 Input()->MouseModeRelative();
3277 GameClient()->OnActivateEditor();
3278 m_pEditor->OnActivate();
3279 m_EditorActive = true;
3280 }
3281 }
3282 else if(m_EditorActive)
3283 {
3284 m_EditorActive = false;
3285 }
3286
3287 Update();
3288 int64_t Now = time_get();
3289
3290 bool IsRenderActive = (g_Config.m_GfxBackgroundRender || m_pGraphics->WindowOpen());
3291
3292 bool AsyncRenderOld = g_Config.m_GfxAsyncRenderOld;
3293
3294 int GfxRefreshRate = g_Config.m_GfxRefreshRate;
3295
3296#if defined(CONF_VIDEORECORDER)
3297 // keep rendering synced
3298 if(IVideo::Current())
3299 {
3300 AsyncRenderOld = false;
3301 GfxRefreshRate = 0;
3302 }
3303#endif
3304
3305 if(IsRenderActive &&
3306 (!AsyncRenderOld || m_pGraphics->IsIdle()) &&
3307 (!GfxRefreshRate || (time_freq() / (int64_t)g_Config.m_GfxRefreshRate) <= Now - LastRenderTime))
3308 {
3309 // update frametime
3310 m_RenderFrameTime = (Now - m_LastRenderTime) / (float)time_freq();
3311 m_FpsGraph.Add(Value: 1.0f / m_RenderFrameTime);
3312
3313 if(m_BenchmarkFile)
3314 {
3315 char aBuf[64];
3316 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Frametime %d us\n", (int)(m_RenderFrameTime * 1000000));
3317 io_write(io: m_BenchmarkFile, buffer: aBuf, size: str_length(str: aBuf));
3318 if(time_get() > m_BenchmarkStopTime)
3319 {
3320 io_close(io: m_BenchmarkFile);
3321 m_BenchmarkFile = nullptr;
3322 Quit();
3323 }
3324 }
3325
3326 m_FrameTimeAverage = m_FrameTimeAverage * 0.9f + m_RenderFrameTime * 0.1f;
3327
3328 // keep the overflow time - it's used to make sure the gfx refreshrate is reached
3329 int64_t AdditionalTime = g_Config.m_GfxRefreshRate ? ((Now - LastRenderTime) - (time_freq() / (int64_t)g_Config.m_GfxRefreshRate)) : 0;
3330 // if the value is over the frametime of a 60 fps frame, reset the additional time (drop the frames, that are lost already)
3331 if(AdditionalTime > (time_freq() / 60))
3332 AdditionalTime = (time_freq() / 60);
3333 LastRenderTime = Now - AdditionalTime;
3334 m_LastRenderTime = Now;
3335
3336 Render();
3337 m_pGraphics->Swap();
3338 }
3339 else if(!IsRenderActive)
3340 {
3341 // 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
3342 LastRenderTime = g_Config.m_GfxRefreshRate ? (Now - (time_freq() / (int64_t)g_Config.m_GfxRefreshRate)) : Now;
3343 }
3344 }
3345
3346 AutoScreenshot_Cleanup();
3347 AutoStatScreenshot_Cleanup();
3348 AutoCSV_Cleanup();
3349
3350 m_Fifo.Update();
3351
3352 if(State() == IClient::STATE_QUITTING || State() == IClient::STATE_RESTARTING)
3353 break;
3354
3355 // beNice
3356 auto Now = time_get_nanoseconds();
3357 decltype(Now) SleepTimeInNanoSeconds{0};
3358 bool Slept = false;
3359 if(g_Config.m_ClRefreshRateInactive && !m_pGraphics->WindowActive())
3360 {
3361 SleepTimeInNanoSeconds = (std::chrono::nanoseconds(1s) / (int64_t)g_Config.m_ClRefreshRateInactive) - (Now - LastTime);
3362 std::this_thread::sleep_for(rtime: SleepTimeInNanoSeconds);
3363 Slept = true;
3364 }
3365 else if(g_Config.m_ClRefreshRate)
3366 {
3367 SleepTimeInNanoSeconds = (std::chrono::nanoseconds(1s) / (int64_t)g_Config.m_ClRefreshRate) - (Now - LastTime);
3368 auto SleepTimeInNanoSecondsInner = SleepTimeInNanoSeconds;
3369 auto NowInner = Now;
3370 while(std::chrono::duration_cast<std::chrono::microseconds>(d: SleepTimeInNanoSecondsInner) > 0us)
3371 {
3372 net_socket_read_wait(sock: m_aNetClient[CONN_MAIN].m_Socket, nanoseconds: SleepTimeInNanoSecondsInner);
3373 auto NowInnerCalc = time_get_nanoseconds();
3374 SleepTimeInNanoSecondsInner -= (NowInnerCalc - NowInner);
3375 NowInner = NowInnerCalc;
3376 }
3377 Slept = true;
3378 }
3379 if(Slept)
3380 {
3381 // if the diff gets too small it shouldn't get even smaller (drop the updates, that could not be handled)
3382 if(SleepTimeInNanoSeconds < -16666666ns)
3383 SleepTimeInNanoSeconds = -16666666ns;
3384 // don't go higher than the frametime of a 60 fps frame
3385 else if(SleepTimeInNanoSeconds > 16666666ns)
3386 SleepTimeInNanoSeconds = 16666666ns;
3387 // the time diff between the time that was used actually used and the time the thread should sleep/wait
3388 // will be calculated in the sleep time of the next update tick by faking the time it should have slept/wait.
3389 // so two cases (and the case it slept exactly the time it should):
3390 // - the thread slept/waited too long, then it adjust the time to sleep/wait less in the next update tick
3391 // - the thread slept/waited too less, then it adjust the time to sleep/wait more in the next update tick
3392 LastTime = Now + SleepTimeInNanoSeconds;
3393 }
3394 else
3395 LastTime = Now;
3396
3397 // update local and global time
3398 m_LocalTime = (time_get() - m_LocalStartTime) / (float)time_freq();
3399 m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq();
3400 }
3401
3402 GameClient()->RenderShutdownMessage();
3403 Disconnect();
3404
3405 if(!m_pConfigManager->Save())
3406 {
3407 char aError[128];
3408 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Saving settings to '%s' failed"), CONFIG_FILE);
3409 m_vQuittingWarnings.emplace_back(args: Localize(pStr: "Error saving settings"), args&: aError);
3410 }
3411
3412 m_Fifo.Shutdown();
3413 m_Http.Shutdown();
3414 Engine()->ShutdownJobs();
3415
3416 GameClient()->RenderShutdownMessage();
3417 GameClient()->OnShutdown();
3418 delete m_pEditor;
3419
3420 // close sockets
3421 for(unsigned int i = 0; i < std::size(m_aNetClient); i++)
3422 m_aNetClient[i].Close();
3423
3424 // shutdown text render while graphics are still available
3425 m_pTextRender->Shutdown();
3426}
3427
3428bool CClient::InitNetworkClient(char *pError, size_t ErrorSize)
3429{
3430 NETADDR BindAddr;
3431 if(g_Config.m_Bindaddr[0] == '\0')
3432 {
3433 mem_zero(block: &BindAddr, size: sizeof(BindAddr));
3434 }
3435 else if(net_host_lookup(hostname: g_Config.m_Bindaddr, addr: &BindAddr, types: NETTYPE_ALL) != 0)
3436 {
3437 str_format(buffer: pError, buffer_size: ErrorSize, format: "The configured bindaddr '%s' cannot be resolved.", g_Config.m_Bindaddr);
3438 return false;
3439 }
3440 BindAddr.type = NETTYPE_ALL;
3441 for(size_t i = 0; i < std::size(m_aNetClient); i++)
3442 {
3443 if(!InitNetworkClientImpl(BindAddr, Conn: i, pError, ErrorSize))
3444 {
3445 return false;
3446 }
3447 }
3448 return true;
3449}
3450
3451bool CClient::InitNetworkClientImpl(NETADDR BindAddr, int Conn, char *pError, size_t ErrorSize)
3452{
3453 int *pPort;
3454 const char *pName;
3455 switch(Conn)
3456 {
3457 case CONN_MAIN:
3458 pPort = &g_Config.m_ClPort;
3459 pName = "main";
3460 break;
3461 case CONN_DUMMY:
3462 pPort = &g_Config.m_ClDummyPort;
3463 pName = "dummy";
3464 break;
3465 case CONN_CONTACT:
3466 pPort = &g_Config.m_ClContactPort;
3467 pName = "contact";
3468 break;
3469 default:
3470 dbg_assert_failed("unreachable");
3471 }
3472 if(m_aNetClient[Conn].State() != NETSTATE_OFFLINE)
3473 {
3474 str_format(buffer: pError, buffer_size: ErrorSize, format: "Could not open network client %s while already connected.", pName);
3475 return false;
3476 }
3477 if(*pPort < 1024) // Reject users setting ports that we don't want to use
3478 *pPort = 0;
3479 BindAddr.port = *pPort;
3480
3481 unsigned RemainingAttempts = 25;
3482 while(!m_aNetClient[Conn].Open(BindAddr))
3483 {
3484 --RemainingAttempts;
3485 if(RemainingAttempts == 0)
3486 {
3487 if(g_Config.m_Bindaddr[0])
3488 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);
3489 else
3490 str_format(buffer: pError, buffer_size: ErrorSize, format: "Could not open network client %s.", pName);
3491 return false;
3492 }
3493 if(BindAddr.port != 0)
3494 BindAddr.port = 0;
3495 }
3496 return true;
3497}
3498
3499bool CClient::CtrlShiftKey(int Key, bool &Last)
3500{
3501 if(Input()->ModifierIsPressed() && Input()->ShiftIsPressed() && !Last && Input()->KeyIsPressed(Key))
3502 {
3503 Last = true;
3504 return true;
3505 }
3506 else if(Last && !Input()->KeyIsPressed(Key))
3507 Last = false;
3508
3509 return false;
3510}
3511
3512void CClient::Con_Connect(IConsole::IResult *pResult, void *pUserData)
3513{
3514 CClient *pSelf = (CClient *)pUserData;
3515 pSelf->HandleConnectLink(pLink: pResult->GetString(Index: 0));
3516}
3517
3518void CClient::Con_Disconnect(IConsole::IResult *pResult, void *pUserData)
3519{
3520 CClient *pSelf = (CClient *)pUserData;
3521 pSelf->Disconnect();
3522}
3523
3524void CClient::Con_DummyConnect(IConsole::IResult *pResult, void *pUserData)
3525{
3526 CClient *pSelf = (CClient *)pUserData;
3527 pSelf->DummyConnect();
3528}
3529
3530void CClient::Con_DummyDisconnect(IConsole::IResult *pResult, void *pUserData)
3531{
3532 CClient *pSelf = (CClient *)pUserData;
3533 pSelf->DummyDisconnect(pReason: nullptr);
3534}
3535
3536void CClient::Con_DummyResetInput(IConsole::IResult *pResult, void *pUserData)
3537{
3538 CClient *pSelf = (CClient *)pUserData;
3539 pSelf->GameClient()->DummyResetInput();
3540}
3541
3542void CClient::Con_Quit(IConsole::IResult *pResult, void *pUserData)
3543{
3544 CClient *pSelf = (CClient *)pUserData;
3545 pSelf->Quit();
3546}
3547
3548void CClient::Con_Restart(IConsole::IResult *pResult, void *pUserData)
3549{
3550 CClient *pSelf = (CClient *)pUserData;
3551 pSelf->Restart();
3552}
3553
3554void CClient::Con_Minimize(IConsole::IResult *pResult, void *pUserData)
3555{
3556 CClient *pSelf = (CClient *)pUserData;
3557 pSelf->Graphics()->Minimize();
3558}
3559
3560void CClient::Con_Ping(IConsole::IResult *pResult, void *pUserData)
3561{
3562 CClient *pSelf = (CClient *)pUserData;
3563
3564 CMsgPacker Msg(NETMSG_PING, true);
3565 pSelf->SendMsg(Conn: CONN_MAIN, pMsg: &Msg, Flags: MSGFLAG_FLUSH);
3566 pSelf->m_PingStartTime = time_get();
3567}
3568
3569void CClient::ConNetReset(IConsole::IResult *pResult, void *pUserData)
3570{
3571 CClient *pSelf = (CClient *)pUserData;
3572 pSelf->ResetSocket();
3573}
3574
3575void CClient::AutoScreenshot_Start()
3576{
3577 if(g_Config.m_ClAutoScreenshot)
3578 {
3579 Graphics()->TakeScreenshot(pFilename: "auto/autoscreen");
3580 m_AutoScreenshotRecycle = true;
3581 }
3582}
3583
3584void CClient::AutoStatScreenshot_Start()
3585{
3586 if(g_Config.m_ClAutoStatboardScreenshot)
3587 {
3588 Graphics()->TakeScreenshot(pFilename: "auto/stats/autoscreen");
3589 m_AutoStatScreenshotRecycle = true;
3590 }
3591}
3592
3593void CClient::AutoScreenshot_Cleanup()
3594{
3595 if(m_AutoScreenshotRecycle)
3596 {
3597 if(g_Config.m_ClAutoScreenshotMax)
3598 {
3599 // clean up auto taken screens
3600 CFileCollection AutoScreens;
3601 AutoScreens.Init(pStorage: Storage(), pPath: "screenshots/auto", pFileDesc: "autoscreen", pFileExt: ".png", MaxEntries: g_Config.m_ClAutoScreenshotMax);
3602 }
3603 m_AutoScreenshotRecycle = false;
3604 }
3605}
3606
3607void CClient::AutoStatScreenshot_Cleanup()
3608{
3609 if(m_AutoStatScreenshotRecycle)
3610 {
3611 if(g_Config.m_ClAutoStatboardScreenshotMax)
3612 {
3613 // clean up auto taken screens
3614 CFileCollection AutoScreens;
3615 AutoScreens.Init(pStorage: Storage(), pPath: "screenshots/auto/stats", pFileDesc: "autoscreen", pFileExt: ".png", MaxEntries: g_Config.m_ClAutoStatboardScreenshotMax);
3616 }
3617 m_AutoStatScreenshotRecycle = false;
3618 }
3619}
3620
3621void CClient::AutoCSV_Start()
3622{
3623 if(g_Config.m_ClAutoCSV)
3624 m_AutoCSVRecycle = true;
3625}
3626
3627void CClient::AutoCSV_Cleanup()
3628{
3629 if(m_AutoCSVRecycle)
3630 {
3631 if(g_Config.m_ClAutoCSVMax)
3632 {
3633 // clean up auto csvs
3634 CFileCollection AutoRecord;
3635 AutoRecord.Init(pStorage: Storage(), pPath: "record/csv", pFileDesc: "autorecord", pFileExt: ".csv", MaxEntries: g_Config.m_ClAutoCSVMax);
3636 }
3637 m_AutoCSVRecycle = false;
3638 }
3639}
3640
3641void CClient::Con_Screenshot(IConsole::IResult *pResult, void *pUserData)
3642{
3643 CClient *pSelf = (CClient *)pUserData;
3644 pSelf->Graphics()->TakeScreenshot(pFilename: nullptr);
3645}
3646
3647#if defined(CONF_VIDEORECORDER)
3648
3649void CClient::Con_StartVideo(IConsole::IResult *pResult, void *pUserData)
3650{
3651 CClient *pSelf = static_cast<CClient *>(pUserData);
3652
3653 if(pResult->NumArguments())
3654 {
3655 pSelf->StartVideo(pFilename: pResult->GetString(Index: 0), WithTimestamp: false);
3656 }
3657 else
3658 {
3659 pSelf->StartVideo(pFilename: "video", WithTimestamp: true);
3660 }
3661}
3662
3663void CClient::StartVideo(const char *pFilename, bool WithTimestamp)
3664{
3665 if(State() != IClient::STATE_DEMOPLAYBACK)
3666 {
3667 log_error("videorecorder", "Video can only be recorded in demo player.");
3668 return;
3669 }
3670
3671 if(IVideo::Current())
3672 {
3673 log_error("videorecorder", "Already recording.");
3674 return;
3675 }
3676
3677 char aFilename[IO_MAX_PATH_LENGTH];
3678 if(WithTimestamp)
3679 {
3680 char aTimestamp[20];
3681 str_timestamp(buffer: aTimestamp, buffer_size: sizeof(aTimestamp));
3682 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "videos/%s_%s.mp4", pFilename, aTimestamp);
3683 }
3684 else
3685 {
3686 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "videos/%s.mp4", pFilename);
3687 }
3688
3689 // wait for idle, so there is no data race
3690 Graphics()->WaitForIdle();
3691 // pause the sound device while creating the video instance
3692 Sound()->PauseAudioDevice();
3693 new CVideo(Graphics(), Sound(), Storage(), Graphics()->ScreenWidth(), Graphics()->ScreenHeight(), aFilename);
3694 Sound()->UnpauseAudioDevice();
3695 if(!IVideo::Current()->Start())
3696 {
3697 log_error("videorecorder", "Failed to start recording to '%s'", aFilename);
3698 m_DemoPlayer.Stop(pErrorMessage: "Failed to start video recording. See local console for details.");
3699 return;
3700 }
3701 if(m_DemoPlayer.Info()->m_Info.m_Paused)
3702 {
3703 IVideo::Current()->Pause(Pause: true);
3704 }
3705 log_info("videorecorder", "Recording to '%s'", aFilename);
3706}
3707
3708void CClient::Con_StopVideo(IConsole::IResult *pResult, void *pUserData)
3709{
3710 if(!IVideo::Current())
3711 {
3712 log_error("videorecorder", "Not recording.");
3713 return;
3714 }
3715
3716 IVideo::Current()->Stop();
3717 log_info("videorecorder", "Stopped recording.");
3718}
3719
3720#endif
3721
3722void CClient::Con_Rcon(IConsole::IResult *pResult, void *pUserData)
3723{
3724 CClient *pSelf = (CClient *)pUserData;
3725 pSelf->Rcon(pCmd: pResult->GetString(Index: 0));
3726}
3727
3728void CClient::Con_RconAuth(IConsole::IResult *pResult, void *pUserData)
3729{
3730 CClient *pSelf = (CClient *)pUserData;
3731 pSelf->RconAuth(pName: "", pPassword: pResult->GetString(Index: 0));
3732}
3733
3734void CClient::Con_RconLogin(IConsole::IResult *pResult, void *pUserData)
3735{
3736 CClient *pSelf = (CClient *)pUserData;
3737 pSelf->RconAuth(pName: pResult->GetString(Index: 0), pPassword: pResult->GetString(Index: 1));
3738}
3739
3740void CClient::Con_BeginFavoriteGroup(IConsole::IResult *pResult, void *pUserData)
3741{
3742 CClient *pSelf = (CClient *)pUserData;
3743 if(pSelf->m_FavoritesGroup)
3744 {
3745 log_error("client", "opening favorites group while there is already one, discarding old one");
3746 for(int i = 0; i < pSelf->m_FavoritesGroupNum; i++)
3747 {
3748 char aAddr[NETADDR_MAXSTRSIZE];
3749 net_addr_str(addr: &pSelf->m_aFavoritesGroupAddresses[i], string: aAddr, max_length: sizeof(aAddr), add_port: true);
3750 log_warn("client", "discarding %s", aAddr);
3751 }
3752 }
3753 pSelf->m_FavoritesGroup = true;
3754 pSelf->m_FavoritesGroupAllowPing = false;
3755 pSelf->m_FavoritesGroupNum = 0;
3756}
3757
3758void CClient::Con_EndFavoriteGroup(IConsole::IResult *pResult, void *pUserData)
3759{
3760 CClient *pSelf = (CClient *)pUserData;
3761 if(!pSelf->m_FavoritesGroup)
3762 {
3763 log_error("client", "closing favorites group while there is none, ignoring");
3764 return;
3765 }
3766 log_info("client", "adding group of %d favorites", pSelf->m_FavoritesGroupNum);
3767 pSelf->m_pFavorites->Add(pAddrs: pSelf->m_aFavoritesGroupAddresses, NumAddrs: pSelf->m_FavoritesGroupNum);
3768 if(pSelf->m_FavoritesGroupAllowPing)
3769 {
3770 pSelf->m_pFavorites->AllowPing(pAddrs: pSelf->m_aFavoritesGroupAddresses, NumAddrs: pSelf->m_FavoritesGroupNum, AllowPing: true);
3771 }
3772 pSelf->m_FavoritesGroup = false;
3773}
3774
3775void CClient::Con_AddFavorite(IConsole::IResult *pResult, void *pUserData)
3776{
3777 CClient *pSelf = (CClient *)pUserData;
3778 NETADDR Addr;
3779
3780 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)
3781 {
3782 char aBuf[128];
3783 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "invalid address '%s'", pResult->GetString(Index: 0));
3784 pSelf->m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "client", pStr: aBuf);
3785 return;
3786 }
3787 bool AllowPing = pResult->NumArguments() > 1 && str_find(haystack: pResult->GetString(Index: 1), needle: "allow_ping");
3788 char aAddr[NETADDR_MAXSTRSIZE];
3789 net_addr_str(addr: &Addr, string: aAddr, max_length: sizeof(aAddr), add_port: true);
3790 if(pSelf->m_FavoritesGroup)
3791 {
3792 if(pSelf->m_FavoritesGroupNum == (int)std::size(pSelf->m_aFavoritesGroupAddresses))
3793 {
3794 log_error("client", "discarding %s because groups can have at most a size of %d", aAddr, pSelf->m_FavoritesGroupNum);
3795 return;
3796 }
3797 log_info("client", "adding %s to favorites group", aAddr);
3798 pSelf->m_aFavoritesGroupAddresses[pSelf->m_FavoritesGroupNum] = Addr;
3799 pSelf->m_FavoritesGroupAllowPing = pSelf->m_FavoritesGroupAllowPing || AllowPing;
3800 pSelf->m_FavoritesGroupNum += 1;
3801 }
3802 else
3803 {
3804 log_info("client", "adding %s to favorites", aAddr);
3805 pSelf->m_pFavorites->Add(pAddrs: &Addr, NumAddrs: 1);
3806 if(AllowPing)
3807 {
3808 pSelf->m_pFavorites->AllowPing(pAddrs: &Addr, NumAddrs: 1, AllowPing: true);
3809 }
3810 }
3811}
3812
3813void CClient::Con_RemoveFavorite(IConsole::IResult *pResult, void *pUserData)
3814{
3815 CClient *pSelf = (CClient *)pUserData;
3816 NETADDR Addr;
3817 if(net_addr_from_str(addr: &Addr, string: pResult->GetString(Index: 0)) == 0)
3818 pSelf->m_pFavorites->Remove(pAddrs: &Addr, NumAddrs: 1);
3819}
3820
3821void CClient::DemoSliceBegin()
3822{
3823 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
3824 g_Config.m_ClDemoSliceBegin = pInfo->m_Info.m_CurrentTick;
3825}
3826
3827void CClient::DemoSliceEnd()
3828{
3829 const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
3830 g_Config.m_ClDemoSliceEnd = pInfo->m_Info.m_CurrentTick;
3831}
3832
3833void CClient::Con_DemoSliceBegin(IConsole::IResult *pResult, void *pUserData)
3834{
3835 CClient *pSelf = (CClient *)pUserData;
3836 pSelf->DemoSliceBegin();
3837}
3838
3839void CClient::Con_DemoSliceEnd(IConsole::IResult *pResult, void *pUserData)
3840{
3841 CClient *pSelf = (CClient *)pUserData;
3842 pSelf->DemoSliceEnd();
3843}
3844
3845void CClient::Con_SaveReplay(IConsole::IResult *pResult, void *pUserData)
3846{
3847 CClient *pSelf = (CClient *)pUserData;
3848 if(pResult->NumArguments())
3849 {
3850 int Length = pResult->GetInteger(Index: 0);
3851 if(Length <= 0)
3852 pSelf->m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: length must be greater than 0 second.");
3853 else
3854 {
3855 if(pResult->NumArguments() >= 2)
3856 pSelf->SaveReplay(Length, pFilename: pResult->GetString(Index: 1));
3857 else
3858 pSelf->SaveReplay(Length);
3859 }
3860 }
3861 else
3862 pSelf->SaveReplay(Length: g_Config.m_ClReplayLength);
3863}
3864
3865void CClient::SaveReplay(const int Length, const char *pFilename)
3866{
3867 if(!g_Config.m_ClReplays)
3868 {
3869 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "Feature is disabled. Please enable it via configuration.");
3870 GameClient()->Echo(pString: Localize(pStr: "Replay feature is disabled!"));
3871 return;
3872 }
3873
3874 if(!DemoRecorder(Recorder: RECORDER_REPLAYS)->IsRecording())
3875 {
3876 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: demorecorder isn't recording. Try to rejoin to fix that.");
3877 }
3878 else if(DemoRecorder(Recorder: RECORDER_REPLAYS)->Length() < 1)
3879 {
3880 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: demorecorder isn't recording for at least 1 second.");
3881 }
3882 else
3883 {
3884 char aFilename[IO_MAX_PATH_LENGTH];
3885 if(pFilename[0] == '\0')
3886 {
3887 char aTimestamp[20];
3888 str_timestamp(buffer: aTimestamp, buffer_size: sizeof(aTimestamp));
3889 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/replays/%s_%s_(replay).demo", m_aCurrentMap, aTimestamp);
3890 }
3891 else
3892 {
3893 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/replays/%s.demo", pFilename);
3894 IOHANDLE Handle = m_pStorage->OpenFile(pFilename: aFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_SAVE);
3895 if(!Handle)
3896 {
3897 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "ERROR: invalid filename. Try a different one!");
3898 return;
3899 }
3900 io_close(io: Handle);
3901 m_pStorage->RemoveFile(pFilename: aFilename, Type: IStorage::TYPE_SAVE);
3902 }
3903
3904 // Stop the recorder to correctly slice the demo after
3905 DemoRecorder(Recorder: RECORDER_REPLAYS)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
3906
3907 // Slice the demo to get only the last cl_replay_length seconds
3908 const char *pSrc = m_aDemoRecorder[RECORDER_REPLAYS].CurrentFilename();
3909 const int EndTick = GameTick(Conn: g_Config.m_ClDummy);
3910 const int StartTick = EndTick - Length * GameTickSpeed();
3911
3912 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "replay", pStr: "Saving replay...");
3913
3914 // Create a job to do this slicing in background because it can be a bit long depending on the file size
3915 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);
3916 Engine()->AddJob(pJob: pDemoEditTask);
3917 m_EditJobs.push_back(x: pDemoEditTask);
3918
3919 // And we restart the recorder
3920 DemoRecorder_UpdateReplayRecorder();
3921 }
3922}
3923
3924void CClient::DemoSlice(const char *pDstPath, CLIENTFUNC_FILTER pfnFilter, void *pUser)
3925{
3926 if(m_DemoPlayer.IsPlaying())
3927 {
3928 m_DemoEditor.Slice(pDemo: m_DemoPlayer.Filename(), pDst: pDstPath, StartTick: g_Config.m_ClDemoSliceBegin, EndTick: g_Config.m_ClDemoSliceEnd, pfnFilter, pUser);
3929 }
3930}
3931
3932const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType)
3933{
3934 // Don't disconnect unless the file exists (only for play command)
3935 if(!Storage()->FileExists(pFilename, Type: StorageType))
3936 return Localize(pStr: "No demo with this filename exists");
3937
3938 Disconnect();
3939 m_aNetClient[CONN_MAIN].ResetErrorString();
3940
3941 SetState(IClient::STATE_LOADING);
3942 SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_DEMO);
3943 if((bool)m_LoadingCallback)
3944 m_LoadingCallback(IClient::LOADING_CALLBACK_DETAIL_DEMO);
3945
3946 // try to start playback
3947 m_DemoPlayer.SetListener(this);
3948 if(m_DemoPlayer.Load(pStorage: Storage(), pConsole: m_pConsole, pFilename, StorageType))
3949 {
3950 DisconnectWithReason(pReason: m_DemoPlayer.ErrorMessage());
3951 return m_DemoPlayer.ErrorMessage();
3952 }
3953
3954 m_Sixup = m_DemoPlayer.IsSixup();
3955
3956 // load map
3957 const CMapInfo *pMapInfo = m_DemoPlayer.GetMapInfo();
3958 int Crc = pMapInfo->m_Crc;
3959 SHA256_DIGEST Sha = pMapInfo->m_Sha256;
3960 const char *pError = LoadMapSearch(pMapName: pMapInfo->m_aName, pWantedSha256: Sha != SHA256_ZEROED ? &Sha : nullptr, WantedCrc: Crc);
3961 if(pError)
3962 {
3963 if(!m_DemoPlayer.ExtractMap(pStorage: Storage()))
3964 {
3965 DisconnectWithReason(pReason: pError);
3966 return pError;
3967 }
3968
3969 Sha = m_DemoPlayer.GetMapInfo()->m_Sha256;
3970 pError = LoadMapSearch(pMapName: pMapInfo->m_aName, pWantedSha256: &Sha, WantedCrc: Crc);
3971 if(pError)
3972 {
3973 DisconnectWithReason(pReason: pError);
3974 return pError;
3975 }
3976 }
3977
3978 // setup current server info
3979 mem_zero(block: &m_CurrentServerInfo, size: sizeof(m_CurrentServerInfo));
3980 str_copy(dst&: m_CurrentServerInfo.m_aMap, src: pMapInfo->m_aName);
3981 m_CurrentServerInfo.m_MapCrc = pMapInfo->m_Crc;
3982 m_CurrentServerInfo.m_MapSize = pMapInfo->m_Size;
3983
3984 // enter demo playback state
3985 SetState(IClient::STATE_DEMOPLAYBACK);
3986
3987 GameClient()->OnConnected();
3988
3989 // setup buffers
3990 mem_zero(block: m_aaaDemorecSnapshotData, size: sizeof(m_aaaDemorecSnapshotData));
3991
3992 for(int SnapshotType = 0; SnapshotType < NUM_SNAPSHOT_TYPES; SnapshotType++)
3993 {
3994 m_aapSnapshots[0][SnapshotType] = &m_aDemorecSnapshotHolders[SnapshotType];
3995 m_aapSnapshots[0][SnapshotType]->m_pSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][0];
3996 m_aapSnapshots[0][SnapshotType]->m_pAltSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][1];
3997 m_aapSnapshots[0][SnapshotType]->m_SnapSize = 0;
3998 m_aapSnapshots[0][SnapshotType]->m_AltSnapSize = 0;
3999 m_aapSnapshots[0][SnapshotType]->m_Tick = -1;
4000 }
4001
4002 m_DemoPlayer.Play();
4003 GameClient()->OnEnterGame();
4004
4005 return nullptr;
4006}
4007
4008#if defined(CONF_VIDEORECORDER)
4009const char *CClient::DemoPlayer_Render(const char *pFilename, int StorageType, const char *pVideoName, int SpeedIndex, bool StartPaused)
4010{
4011 const char *pError = DemoPlayer_Play(pFilename, StorageType);
4012 if(pError)
4013 return pError;
4014
4015 StartVideo(pFilename: pVideoName, WithTimestamp: false);
4016 m_DemoPlayer.SetSpeedIndex(SpeedIndex);
4017 if(StartPaused)
4018 {
4019 m_DemoPlayer.Pause();
4020 }
4021 return nullptr;
4022}
4023#endif
4024
4025void CClient::Con_Play(IConsole::IResult *pResult, void *pUserData)
4026{
4027 CClient *pSelf = (CClient *)pUserData;
4028 pSelf->HandleDemoPath(pPath: pResult->GetString(Index: 0));
4029}
4030
4031void CClient::Con_DemoPlay(IConsole::IResult *pResult, void *pUserData)
4032{
4033 CClient *pSelf = (CClient *)pUserData;
4034 if(pSelf->m_DemoPlayer.IsPlaying())
4035 {
4036 if(pSelf->m_DemoPlayer.BaseInfo()->m_Paused)
4037 {
4038 pSelf->m_DemoPlayer.Unpause();
4039 }
4040 else
4041 {
4042 pSelf->m_DemoPlayer.Pause();
4043 }
4044 }
4045}
4046
4047void CClient::Con_DemoSpeed(IConsole::IResult *pResult, void *pUserData)
4048{
4049 CClient *pSelf = (CClient *)pUserData;
4050 pSelf->m_DemoPlayer.SetSpeed(pResult->GetFloat(Index: 0));
4051}
4052
4053void CClient::DemoRecorder_Start(const char *pFilename, bool WithTimestamp, int Recorder, bool Verbose)
4054{
4055 if(State() != IClient::STATE_ONLINE)
4056 {
4057 if(Verbose)
4058 {
4059 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demorec/record", pStr: "client is not online");
4060 }
4061 }
4062 else
4063 {
4064 char aFilename[IO_MAX_PATH_LENGTH];
4065 if(WithTimestamp)
4066 {
4067 char aTimestamp[20];
4068 str_timestamp(buffer: aTimestamp, buffer_size: sizeof(aTimestamp));
4069 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/%s_%s.demo", pFilename, aTimestamp);
4070 }
4071 else
4072 {
4073 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "demos/%s.demo", pFilename);
4074 }
4075
4076 m_aDemoRecorder[Recorder].Start(
4077 pStorage: Storage(),
4078 pConsole: m_pConsole,
4079 pFilename: aFilename,
4080 pNetversion: IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(),
4081 pMap: m_aCurrentMap,
4082 Sha256: m_pMap->Sha256(),
4083 MapCrc: m_pMap->Crc(),
4084 pType: "client",
4085 MapSize: m_pMap->MapSize(),
4086 pMapData: nullptr,
4087 MapFile: m_pMap->File(),
4088 pfnFilter: nullptr,
4089 pUser: nullptr);
4090 }
4091}
4092
4093void CClient::DemoRecorder_HandleAutoStart()
4094{
4095 if(g_Config.m_ClAutoDemoRecord)
4096 {
4097 DemoRecorder(Recorder: RECORDER_AUTO)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
4098
4099 char aFilename[IO_MAX_PATH_LENGTH];
4100 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "auto/%s", m_aCurrentMap);
4101 DemoRecorder_Start(pFilename: aFilename, WithTimestamp: true, Recorder: RECORDER_AUTO);
4102
4103 if(g_Config.m_ClAutoDemoMax)
4104 {
4105 // clean up auto recorded demos
4106 CFileCollection AutoDemos;
4107 AutoDemos.Init(pStorage: Storage(), pPath: "demos/auto", pFileDesc: "" /* empty for wild card */, pFileExt: ".demo", MaxEntries: g_Config.m_ClAutoDemoMax);
4108 }
4109 }
4110
4111 DemoRecorder_UpdateReplayRecorder();
4112}
4113
4114void CClient::DemoRecorder_UpdateReplayRecorder()
4115{
4116 if(!g_Config.m_ClReplays && DemoRecorder(Recorder: RECORDER_REPLAYS)->IsRecording())
4117 {
4118 DemoRecorder(Recorder: RECORDER_REPLAYS)->Stop(Mode: IDemoRecorder::EStopMode::REMOVE_FILE);
4119 }
4120
4121 if(g_Config.m_ClReplays && !DemoRecorder(Recorder: RECORDER_REPLAYS)->IsRecording())
4122 {
4123 char aFilename[IO_MAX_PATH_LENGTH];
4124 str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "replays/replay_tmp_%s", m_aCurrentMap);
4125 DemoRecorder_Start(pFilename: aFilename, WithTimestamp: true, Recorder: RECORDER_REPLAYS);
4126 }
4127}
4128
4129void CClient::DemoRecorder_AddDemoMarker(int Recorder)
4130{
4131 m_aDemoRecorder[Recorder].AddDemoMarker();
4132}
4133
4134class IDemoRecorder *CClient::DemoRecorder(int Recorder)
4135{
4136 return &m_aDemoRecorder[Recorder];
4137}
4138
4139void CClient::Con_Record(IConsole::IResult *pResult, void *pUserData)
4140{
4141 CClient *pSelf = (CClient *)pUserData;
4142
4143 if(pSelf->m_aDemoRecorder[RECORDER_MANUAL].IsRecording())
4144 {
4145 pSelf->m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demo_recorder", pStr: "Demo recorder already recording");
4146 return;
4147 }
4148
4149 if(pResult->NumArguments())
4150 pSelf->DemoRecorder_Start(pFilename: pResult->GetString(Index: 0), WithTimestamp: false, Recorder: RECORDER_MANUAL, Verbose: true);
4151 else
4152 pSelf->DemoRecorder_Start(pFilename: pSelf->m_aCurrentMap, WithTimestamp: true, Recorder: RECORDER_MANUAL, Verbose: true);
4153}
4154
4155void CClient::Con_StopRecord(IConsole::IResult *pResult, void *pUserData)
4156{
4157 CClient *pSelf = (CClient *)pUserData;
4158 pSelf->DemoRecorder(Recorder: RECORDER_MANUAL)->Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
4159}
4160
4161void CClient::Con_AddDemoMarker(IConsole::IResult *pResult, void *pUserData)
4162{
4163 CClient *pSelf = (CClient *)pUserData;
4164 for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++)
4165 pSelf->DemoRecorder_AddDemoMarker(Recorder);
4166}
4167
4168void CClient::Con_BenchmarkQuit(IConsole::IResult *pResult, void *pUserData)
4169{
4170 CClient *pSelf = (CClient *)pUserData;
4171 int Seconds = pResult->GetInteger(Index: 0);
4172 const char *pFilename = pResult->GetString(Index: 1);
4173 pSelf->BenchmarkQuit(Seconds, pFilename);
4174}
4175
4176void CClient::BenchmarkQuit(int Seconds, const char *pFilename)
4177{
4178 m_BenchmarkFile = Storage()->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: IStorage::TYPE_ABSOLUTE);
4179 m_BenchmarkStopTime = time_get() + time_freq() * Seconds;
4180}
4181
4182void CClient::UpdateAndSwap()
4183{
4184 Input()->Update();
4185 Graphics()->Swap();
4186 Graphics()->Clear(r: 0, g: 0, b: 0);
4187 m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq();
4188}
4189
4190void CClient::ServerBrowserUpdate()
4191{
4192 m_ServerBrowser.RequestResort();
4193}
4194
4195void CClient::ConchainServerBrowserUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4196{
4197 pfnCallback(pResult, pCallbackUserData);
4198 if(pResult->NumArguments())
4199 ((CClient *)pUserData)->ServerBrowserUpdate();
4200}
4201
4202void CClient::InitChecksum()
4203{
4204 CChecksumData *pData = &m_Checksum.m_Data;
4205 pData->m_SizeofData = sizeof(*pData);
4206 str_copy(dst&: pData->m_aVersionStr, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")");
4207 pData->m_Start = time_get();
4208 os_version_str(version: pData->m_aOsVersion, length: sizeof(pData->m_aOsVersion));
4209 secure_random_fill(bytes: &pData->m_Random, length: sizeof(pData->m_Random));
4210 pData->m_Version = GameClient()->DDNetVersion();
4211 pData->m_SizeofClient = sizeof(*this);
4212 pData->m_SizeofConfig = sizeof(pData->m_Config);
4213 pData->InitFiles();
4214}
4215
4216#ifndef DDNET_CHECKSUM_SALT
4217// salt@checksum.ddnet.tw: db877f2b-2ddb-3ba6-9f67-a6d169ec671d
4218#define DDNET_CHECKSUM_SALT \
4219 { \
4220 { \
4221 0xdb, 0x87, 0x7f, 0x2b, 0x2d, 0xdb, 0x3b, 0xa6, \
4222 0x9f, 0x67, 0xa6, 0xd1, 0x69, 0xec, 0x67, 0x1d, \
4223 } \
4224 }
4225#endif
4226
4227int CClient::HandleChecksum(int Conn, CUuid Uuid, CUnpacker *pUnpacker)
4228{
4229 int Start = pUnpacker->GetInt();
4230 int Length = pUnpacker->GetInt();
4231 if(pUnpacker->Error())
4232 {
4233 return 1;
4234 }
4235 if(Start < 0 || Length < 0 || Start > std::numeric_limits<int>::max() - Length)
4236 {
4237 return 2;
4238 }
4239 int End = Start + Length;
4240 int ChecksumBytesEnd = minimum(a: End, b: (int)sizeof(m_Checksum.m_aBytes));
4241 int FileStart = maximum(a: Start, b: (int)sizeof(m_Checksum.m_aBytes));
4242 unsigned char aStartBytes[sizeof(int32_t)];
4243 unsigned char aEndBytes[sizeof(int32_t)];
4244 uint_to_bytes_be(bytes: aStartBytes, value: Start);
4245 uint_to_bytes_be(bytes: aEndBytes, value: End);
4246
4247 if(Start <= (int)sizeof(m_Checksum.m_aBytes))
4248 {
4249 mem_zero(block: &m_Checksum.m_Data.m_Config, size: sizeof(m_Checksum.m_Data.m_Config));
4250#define CHECKSUM_RECORD(Flags) (((Flags) & CFGFLAG_CLIENT) == 0 || ((Flags) & CFGFLAG_INSENSITIVE) != 0)
4251#define MACRO_CONFIG_INT(Name, ScriptName, Def, Min, Max, Flags, Desc) \
4252 if(CHECKSUM_RECORD(Flags)) \
4253 { \
4254 m_Checksum.m_Data.m_Config.m_##Name = g_Config.m_##Name; \
4255 }
4256#define MACRO_CONFIG_COL(Name, ScriptName, Def, Flags, Desc) \
4257 if(CHECKSUM_RECORD(Flags)) \
4258 { \
4259 m_Checksum.m_Data.m_Config.m_##Name = g_Config.m_##Name; \
4260 }
4261#define MACRO_CONFIG_STR(Name, ScriptName, Len, Def, Flags, Desc) \
4262 if(CHECKSUM_RECORD(Flags)) \
4263 { \
4264 str_copy(m_Checksum.m_Data.m_Config.m_##Name, g_Config.m_##Name, sizeof(m_Checksum.m_Data.m_Config.m_##Name)); \
4265 }
4266#include <engine/shared/config_variables.h>
4267#undef CHECKSUM_RECORD
4268#undef MACRO_CONFIG_INT
4269#undef MACRO_CONFIG_COL
4270#undef MACRO_CONFIG_STR
4271 }
4272 if(End > (int)sizeof(m_Checksum.m_aBytes))
4273 {
4274 if(m_OwnExecutableSize == 0)
4275 {
4276 m_OwnExecutable = io_current_exe();
4277 // io_length returns -1 on error.
4278 m_OwnExecutableSize = m_OwnExecutable ? io_length(io: m_OwnExecutable) : -1;
4279 }
4280 // Own executable not available.
4281 if(m_OwnExecutableSize < 0)
4282 {
4283 return 3;
4284 }
4285 if(End - (int)sizeof(m_Checksum.m_aBytes) > m_OwnExecutableSize)
4286 {
4287 return 4;
4288 }
4289 }
4290
4291 SHA256_CTX Sha256Ctxt;
4292 sha256_init(ctxt: &Sha256Ctxt);
4293 CUuid Salt = DDNET_CHECKSUM_SALT;
4294 sha256_update(ctxt: &Sha256Ctxt, data: &Salt, data_len: sizeof(Salt));
4295 sha256_update(ctxt: &Sha256Ctxt, data: &Uuid, data_len: sizeof(Uuid));
4296 sha256_update(ctxt: &Sha256Ctxt, data: aStartBytes, data_len: sizeof(aStartBytes));
4297 sha256_update(ctxt: &Sha256Ctxt, data: aEndBytes, data_len: sizeof(aEndBytes));
4298 if(Start < (int)sizeof(m_Checksum.m_aBytes))
4299 {
4300 sha256_update(ctxt: &Sha256Ctxt, data: m_Checksum.m_aBytes + Start, data_len: ChecksumBytesEnd - Start);
4301 }
4302 if(End > (int)sizeof(m_Checksum.m_aBytes))
4303 {
4304 unsigned char aBuf[2048];
4305 if(io_seek(io: m_OwnExecutable, offset: FileStart - sizeof(m_Checksum.m_aBytes), origin: IOSEEK_START))
4306 {
4307 return 5;
4308 }
4309 for(int i = FileStart; i < End; i += sizeof(aBuf))
4310 {
4311 int Read = io_read(io: m_OwnExecutable, buffer: aBuf, size: minimum(a: (int)sizeof(aBuf), b: End - i));
4312 sha256_update(ctxt: &Sha256Ctxt, data: aBuf, data_len: Read);
4313 }
4314 }
4315 SHA256_DIGEST Sha256 = sha256_finish(ctxt: &Sha256Ctxt);
4316
4317 CMsgPacker Msg(NETMSG_CHECKSUM_RESPONSE, true);
4318 Msg.AddRaw(pData: &Uuid, Size: sizeof(Uuid));
4319 Msg.AddRaw(pData: &Sha256, Size: sizeof(Sha256));
4320 SendMsg(Conn, pMsg: &Msg, Flags: MSGFLAG_VITAL);
4321
4322 return 0;
4323}
4324
4325void CClient::ConchainWindowScreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4326{
4327 CClient *pSelf = (CClient *)pUserData;
4328 if(pSelf->Graphics() && pResult->NumArguments())
4329 {
4330 if(g_Config.m_GfxScreen != pResult->GetInteger(Index: 0))
4331 pSelf->Graphics()->SwitchWindowScreen(Index: pResult->GetInteger(Index: 0), MoveToCenter: true);
4332 }
4333 else
4334 pfnCallback(pResult, pCallbackUserData);
4335}
4336
4337void CClient::ConchainFullscreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4338{
4339 CClient *pSelf = (CClient *)pUserData;
4340 if(pSelf->Graphics() && pResult->NumArguments())
4341 {
4342 if(g_Config.m_GfxFullscreen != pResult->GetInteger(Index: 0))
4343 pSelf->Graphics()->SetWindowParams(FullscreenMode: pResult->GetInteger(Index: 0), IsBorderless: g_Config.m_GfxBorderless);
4344 }
4345 else
4346 pfnCallback(pResult, pCallbackUserData);
4347}
4348
4349void CClient::ConchainWindowBordered(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4350{
4351 CClient *pSelf = (CClient *)pUserData;
4352 if(pSelf->Graphics() && pResult->NumArguments())
4353 {
4354 if(!g_Config.m_GfxFullscreen && (g_Config.m_GfxBorderless != pResult->GetInteger(Index: 0)))
4355 pSelf->Graphics()->SetWindowParams(FullscreenMode: g_Config.m_GfxFullscreen, IsBorderless: !g_Config.m_GfxBorderless);
4356 }
4357 else
4358 pfnCallback(pResult, pCallbackUserData);
4359}
4360
4361void CClient::Notify(const char *pTitle, const char *pMessage)
4362{
4363 if(m_pGraphics->WindowActive() || !g_Config.m_ClShowNotifications)
4364 return;
4365
4366 Notifications()->Notify(pTitle, pMessage);
4367 Graphics()->NotifyWindow();
4368}
4369
4370void CClient::OnWindowResize()
4371{
4372 TextRender()->OnPreWindowResize();
4373 GameClient()->OnWindowResize();
4374 m_pEditor->OnWindowResize();
4375 TextRender()->OnWindowResize();
4376}
4377
4378void CClient::ConchainWindowVSync(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4379{
4380 CClient *pSelf = (CClient *)pUserData;
4381 if(pSelf->Graphics() && pResult->NumArguments())
4382 {
4383 if(g_Config.m_GfxVsync != pResult->GetInteger(Index: 0))
4384 pSelf->Graphics()->SetVSync(pResult->GetInteger(Index: 0));
4385 }
4386 else
4387 pfnCallback(pResult, pCallbackUserData);
4388}
4389
4390void CClient::ConchainWindowResize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4391{
4392 CClient *pSelf = (CClient *)pUserData;
4393 pfnCallback(pResult, pCallbackUserData);
4394 if(pSelf->Graphics() && pResult->NumArguments())
4395 {
4396 pSelf->Graphics()->ResizeToScreen();
4397 }
4398}
4399
4400void CClient::ConchainTimeoutSeed(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4401{
4402 CClient *pSelf = (CClient *)pUserData;
4403 pfnCallback(pResult, pCallbackUserData);
4404 if(pResult->NumArguments())
4405 pSelf->m_GenerateTimeoutSeed = false;
4406}
4407
4408void CClient::ConchainPassword(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->m_LocalStartTime) //won't set m_SendPassword before game has started
4413 pSelf->m_SendPassword = true;
4414}
4415
4416void CClient::ConchainReplays(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4417{
4418 CClient *pSelf = (CClient *)pUserData;
4419 pfnCallback(pResult, pCallbackUserData);
4420 if(pResult->NumArguments())
4421 {
4422 pSelf->DemoRecorder_UpdateReplayRecorder();
4423 }
4424}
4425
4426void CClient::ConchainInputFifo(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4427{
4428 CClient *pSelf = (CClient *)pUserData;
4429 pfnCallback(pResult, pCallbackUserData);
4430 if(pSelf->m_Fifo.IsInit())
4431 {
4432 pSelf->m_Fifo.Shutdown();
4433 pSelf->m_Fifo.Init(pConsole: pSelf->m_pConsole, pFifoFile: pSelf->Config()->m_ClInputFifo, Flag: CFGFLAG_CLIENT);
4434 }
4435}
4436
4437void CClient::ConchainNetReset(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 pSelf->ResetSocket();
4443}
4444
4445void CClient::ConchainLoglevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4446{
4447 CClient *pSelf = (CClient *)pUserData;
4448 pfnCallback(pResult, pCallbackUserData);
4449 if(pResult->NumArguments())
4450 {
4451 pSelf->m_pFileLogger->SetFilter(CLogFilter{.m_MaxLevel: IConsole::ToLogLevelFilter(ConsoleLevel: g_Config.m_Loglevel)});
4452 }
4453}
4454
4455void CClient::ConchainStdoutOutputLevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
4456{
4457 CClient *pSelf = (CClient *)pUserData;
4458 pfnCallback(pResult, pCallbackUserData);
4459 if(pResult->NumArguments() && pSelf->m_pStdoutLogger)
4460 {
4461 pSelf->m_pStdoutLogger->SetFilter(CLogFilter{.m_MaxLevel: IConsole::ToLogLevelFilter(ConsoleLevel: g_Config.m_StdoutOutputLevel)});
4462 }
4463}
4464
4465void CClient::RegisterCommands()
4466{
4467 m_pConsole = Kernel()->RequestInterface<IConsole>();
4468
4469 m_pConsole->Register(pName: "dummy_connect", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DummyConnect, pUser: this, pHelp: "Connect dummy");
4470 m_pConsole->Register(pName: "dummy_disconnect", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DummyDisconnect, pUser: this, pHelp: "Disconnect dummy");
4471 m_pConsole->Register(pName: "dummy_reset", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DummyResetInput, pUser: this, pHelp: "Reset dummy");
4472
4473 m_pConsole->Register(pName: "quit", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Quit, pUser: this, pHelp: "Quit the client");
4474 m_pConsole->Register(pName: "exit", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Quit, pUser: this, pHelp: "Quit the client");
4475 m_pConsole->Register(pName: "restart", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Restart, pUser: this, pHelp: "Restart the client");
4476 m_pConsole->Register(pName: "minimize", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Minimize, pUser: this, pHelp: "Minimize the client");
4477 m_pConsole->Register(pName: "connect", pParams: "r[host|ip]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Connect, pUser: this, pHelp: "Connect to the specified host/ip");
4478 m_pConsole->Register(pName: "disconnect", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Disconnect, pUser: this, pHelp: "Disconnect from the server");
4479 m_pConsole->Register(pName: "ping", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Ping, pUser: this, pHelp: "Ping the current server");
4480 m_pConsole->Register(pName: "screenshot", pParams: "", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Screenshot, pUser: this, pHelp: "Take a screenshot");
4481 m_pConsole->Register(pName: "net_reset", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: ConNetReset, pUser: this, pHelp: "Rebinds the client's listening address and port");
4482
4483#if defined(CONF_VIDEORECORDER)
4484 m_pConsole->Register(pName: "start_video", pParams: "?r[file]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_StartVideo, pUser: this, pHelp: "Start recording a video");
4485 m_pConsole->Register(pName: "stop_video", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_StopVideo, pUser: this, pHelp: "Stop recording a video");
4486#endif
4487
4488 m_pConsole->Register(pName: "rcon", pParams: "r[rcon-command]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Rcon, pUser: this, pHelp: "Send specified command to rcon");
4489 m_pConsole->Register(pName: "rcon_auth", pParams: "r[password]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RconAuth, pUser: this, pHelp: "Authenticate to rcon");
4490 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");
4491 m_pConsole->Register(pName: "play", pParams: "r[file]", Flags: CFGFLAG_CLIENT | CFGFLAG_STORE, pfnFunc: Con_Play, pUser: this, pHelp: "Play back a demo");
4492 m_pConsole->Register(pName: "record", pParams: "?r[file]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_Record, pUser: this, pHelp: "Start recording a demo");
4493 m_pConsole->Register(pName: "stoprecord", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_StopRecord, pUser: this, pHelp: "Stop recording a demo");
4494 m_pConsole->Register(pName: "add_demomarker", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_AddDemoMarker, pUser: this, pHelp: "Add demo timeline marker");
4495 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`");
4496 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`");
4497 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");
4498 m_pConsole->Register(pName: "remove_favorite", pParams: "r[host|ip]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_RemoveFavorite, pUser: this, pHelp: "Remove a server from favorites");
4499 m_pConsole->Register(pName: "demo_slice_start", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoSliceBegin, pUser: this, pHelp: "Mark the beginning of a demo cut");
4500 m_pConsole->Register(pName: "demo_slice_end", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoSliceEnd, pUser: this, pHelp: "Mark the end of a demo cut");
4501 m_pConsole->Register(pName: "demo_play", pParams: "", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoPlay, pUser: this, pHelp: "Play/pause the current demo");
4502 m_pConsole->Register(pName: "demo_speed", pParams: "f[speed]", Flags: CFGFLAG_CLIENT, pfnFunc: Con_DemoSpeed, pUser: this, pHelp: "Set current demo speed");
4503
4504 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");
4505 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");
4506
4507 RustVersionRegister(console&: *m_pConsole);
4508
4509 m_pConsole->Chain(pName: "cl_timeout_seed", pfnChainFunc: ConchainTimeoutSeed, pUser: this);
4510 m_pConsole->Chain(pName: "cl_replays", pfnChainFunc: ConchainReplays, pUser: this);
4511 m_pConsole->Chain(pName: "cl_input_fifo", pfnChainFunc: ConchainInputFifo, pUser: this);
4512 m_pConsole->Chain(pName: "cl_port", pfnChainFunc: ConchainNetReset, pUser: this);
4513 m_pConsole->Chain(pName: "cl_dummy_port", pfnChainFunc: ConchainNetReset, pUser: this);
4514 m_pConsole->Chain(pName: "cl_contact_port", pfnChainFunc: ConchainNetReset, pUser: this);
4515 m_pConsole->Chain(pName: "bindaddr", pfnChainFunc: ConchainNetReset, pUser: this);
4516
4517 m_pConsole->Chain(pName: "password", pfnChainFunc: ConchainPassword, pUser: this);
4518
4519 // used for server browser update
4520 m_pConsole->Chain(pName: "br_filter_string", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4521 m_pConsole->Chain(pName: "br_exclude_string", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4522 m_pConsole->Chain(pName: "br_filter_full", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4523 m_pConsole->Chain(pName: "br_filter_empty", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4524 m_pConsole->Chain(pName: "br_filter_spectators", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4525 m_pConsole->Chain(pName: "br_filter_friends", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4526 m_pConsole->Chain(pName: "br_filter_country", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4527 m_pConsole->Chain(pName: "br_filter_country_index", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4528 m_pConsole->Chain(pName: "br_filter_pw", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4529 m_pConsole->Chain(pName: "br_filter_gametype", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4530 m_pConsole->Chain(pName: "br_filter_gametype_strict", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4531 m_pConsole->Chain(pName: "br_filter_connecting_players", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4532 m_pConsole->Chain(pName: "br_filter_serveraddress", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4533 m_pConsole->Chain(pName: "br_filter_unfinished_map", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4534 m_pConsole->Chain(pName: "br_filter_login", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4535 m_pConsole->Chain(pName: "add_favorite", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4536 m_pConsole->Chain(pName: "remove_favorite", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4537 m_pConsole->Chain(pName: "end_favorite_group", pfnChainFunc: ConchainServerBrowserUpdate, pUser: this);
4538
4539 m_pConsole->Chain(pName: "gfx_screen", pfnChainFunc: ConchainWindowScreen, pUser: this);
4540 m_pConsole->Chain(pName: "gfx_screen_width", pfnChainFunc: ConchainWindowResize, pUser: this);
4541 m_pConsole->Chain(pName: "gfx_screen_height", pfnChainFunc: ConchainWindowResize, pUser: this);
4542 m_pConsole->Chain(pName: "gfx_screen_refresh_rate", pfnChainFunc: ConchainWindowResize, pUser: this);
4543 m_pConsole->Chain(pName: "gfx_fullscreen", pfnChainFunc: ConchainFullscreen, pUser: this);
4544 m_pConsole->Chain(pName: "gfx_borderless", pfnChainFunc: ConchainWindowBordered, pUser: this);
4545 m_pConsole->Chain(pName: "gfx_vsync", pfnChainFunc: ConchainWindowVSync, pUser: this);
4546
4547 m_pConsole->Chain(pName: "loglevel", pfnChainFunc: ConchainLoglevel, pUser: this);
4548 m_pConsole->Chain(pName: "stdout_output_level", pfnChainFunc: ConchainStdoutOutputLevel, pUser: this);
4549}
4550
4551static CClient *CreateClient()
4552{
4553 return new CClient;
4554}
4555
4556void CClient::HandleConnectAddress(const NETADDR *pAddr)
4557{
4558 net_addr_str(addr: pAddr, string: m_aCmdConnect, max_length: sizeof(m_aCmdConnect), add_port: true);
4559}
4560
4561void CClient::HandleConnectLink(const char *pLink)
4562{
4563 // Chrome works fine with ddnet:// but not with ddnet:
4564 // Check ddnet:// before ddnet: because we don't want the // as part of connect command
4565 const char *pConnectLink = nullptr;
4566 if((pConnectLink = str_startswith(str: pLink, CONNECTLINK_DOUBLE_SLASH)))
4567 str_copy(dst&: m_aCmdConnect, src: pConnectLink);
4568 else if((pConnectLink = str_startswith(str: pLink, CONNECTLINK_NO_SLASH)))
4569 str_copy(dst&: m_aCmdConnect, src: pConnectLink);
4570 else
4571 str_copy(dst&: m_aCmdConnect, src: pLink);
4572 // Edge appends / to the URL
4573 const int Length = str_length(str: m_aCmdConnect);
4574 if(m_aCmdConnect[Length - 1] == '/')
4575 m_aCmdConnect[Length - 1] = '\0';
4576}
4577
4578void CClient::HandleDemoPath(const char *pPath)
4579{
4580 str_copy(dst&: m_aCmdPlayDemo, src: pPath);
4581}
4582
4583void CClient::HandleMapPath(const char *pPath)
4584{
4585 str_copy(dst&: m_aCmdEditMap, src: pPath);
4586}
4587
4588static bool UnknownArgumentCallback(const char *pCommand, void *pUser)
4589{
4590 CClient *pClient = static_cast<CClient *>(pUser);
4591 if(str_startswith(str: pCommand, CONNECTLINK_NO_SLASH))
4592 {
4593 pClient->HandleConnectLink(pLink: pCommand);
4594 return true;
4595 }
4596 else if(str_endswith(str: pCommand, suffix: ".demo"))
4597 {
4598 pClient->HandleDemoPath(pPath: pCommand);
4599 return true;
4600 }
4601 else if(str_endswith(str: pCommand, suffix: ".map"))
4602 {
4603 pClient->HandleMapPath(pPath: pCommand);
4604 return true;
4605 }
4606 return false;
4607}
4608
4609static bool SaveUnknownCommandCallback(const char *pCommand, void *pUser)
4610{
4611 CClient *pClient = static_cast<CClient *>(pUser);
4612 pClient->ConfigManager()->StoreUnknownCommand(pCommand);
4613 return true;
4614}
4615
4616/*
4617 Server Time
4618 Client Mirror Time
4619 Client Predicted Time
4620
4621 Snapshot Latency
4622 Downstream latency
4623
4624 Prediction Latency
4625 Upstream latency
4626*/
4627
4628#if defined(CONF_PLATFORM_MACOS)
4629extern "C" int TWMain(int argc, const char **argv)
4630#elif defined(CONF_PLATFORM_ANDROID)
4631static int gs_AndroidStarted = false;
4632extern "C" [[gnu::visibility("default")]] int SDL_main(int argc, char *argv[]);
4633int SDL_main(int argc, char *argv2[])
4634#else
4635int main(int argc, const char **argv)
4636#endif
4637{
4638 const int64_t MainStart = time_get();
4639
4640#if defined(CONF_PLATFORM_ANDROID)
4641 const char **argv = const_cast<const char **>(argv2);
4642 // Android might not unload the library from memory, causing globals like gs_AndroidStarted
4643 // not to be initialized correctly when starting the app again.
4644 if(gs_AndroidStarted)
4645 {
4646 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."});
4647 std::exit(0);
4648 }
4649 gs_AndroidStarted = true;
4650#elif defined(CONF_FAMILY_WINDOWS)
4651 CWindowsComLifecycle WindowsComLifecycle(true);
4652#endif
4653 CCmdlineFix CmdlineFix(&argc, &argv);
4654
4655 std::vector<std::shared_ptr<ILogger>> vpLoggers;
4656 std::shared_ptr<ILogger> pStdoutLogger = nullptr;
4657#if defined(CONF_PLATFORM_ANDROID)
4658 pStdoutLogger = std::shared_ptr<ILogger>(log_logger_android());
4659#else
4660 bool Silent = false;
4661 for(int i = 1; i < argc; i++)
4662 {
4663 if(str_comp(a: "-s", b: argv[i]) == 0 || str_comp(a: "--silent", b: argv[i]) == 0)
4664 {
4665 Silent = true;
4666 }
4667 }
4668 if(!Silent)
4669 {
4670 pStdoutLogger = std::shared_ptr<ILogger>(log_logger_stdout());
4671 }
4672#endif
4673 if(pStdoutLogger)
4674 {
4675 vpLoggers.push_back(x: pStdoutLogger);
4676 }
4677 std::shared_ptr<CFutureLogger> pFutureFileLogger = std::make_shared<CFutureLogger>();
4678 vpLoggers.push_back(x: pFutureFileLogger);
4679 std::shared_ptr<CFutureLogger> pFutureConsoleLogger = std::make_shared<CFutureLogger>();
4680 vpLoggers.push_back(x: pFutureConsoleLogger);
4681 std::shared_ptr<CFutureLogger> pFutureAssertionLogger = std::make_shared<CFutureLogger>();
4682 vpLoggers.push_back(x: pFutureAssertionLogger);
4683 log_set_global_logger(logger: log_logger_collection(vpLoggers: std::move(vpLoggers)).release());
4684
4685#if defined(CONF_PLATFORM_ANDROID)
4686 // Initialize Android after logger is available
4687 const char *pAndroidInitError = InitAndroid();
4688 if(pAndroidInitError != nullptr)
4689 {
4690 log_error("android", "%s", pAndroidInitError);
4691 ShowMessageBoxWithoutGraphics({.m_pTitle = "Android Error", .m_pMessage = pAndroidInitError});
4692 std::exit(0);
4693 }
4694#endif
4695
4696 std::stack<std::function<void()>> CleanerFunctions;
4697 std::function<void()> PerformCleanup = [&CleanerFunctions]() mutable {
4698 while(!CleanerFunctions.empty())
4699 {
4700 CleanerFunctions.top()();
4701 CleanerFunctions.pop();
4702 }
4703 };
4704 std::function<void()> PerformFinalCleanup = []() {
4705#if defined(CONF_PLATFORM_ANDROID)
4706 // Forcefully terminate the entire process, to ensure that static variables
4707 // will be initialized correctly when the app is started again after quitting.
4708 // Returning from the main function is not enough, as this only results in the
4709 // native thread terminating, but the Java thread will continue. Java does not
4710 // support unloading libraries once they have been loaded, so all static
4711 // variables will not have their expected initial values anymore when the app
4712 // is started again after quitting. The variable gs_AndroidStarted above is
4713 // used to check that static variables have been initialized properly.
4714 // TODO: This is not the correct way to close an activity on Android, as it
4715 // ignores the activity lifecycle entirely, which may cause issues if
4716 // we ever used any global resources like the camera.
4717 std::exit(0);
4718#elif defined(CONF_PLATFORM_EMSCRIPTEN)
4719 // Hide canvas after client quit as it will be entirely black without visible
4720 // cursor, also blocking view of the console.
4721 EM_ASM({
4722 document.querySelector('#canvas').style.display = 'none';
4723 });
4724#endif
4725 };
4726 std::function<void()> PerformAllCleanup = [PerformCleanup, PerformFinalCleanup]() mutable {
4727 PerformCleanup();
4728 PerformFinalCleanup();
4729 };
4730
4731 // Register SDL for cleanup before creating the kernel and client,
4732 // so SDL is shutdown after kernel and client. Otherwise the client
4733 // may crash when shutting down after SDL is already shutdown.
4734 CleanerFunctions.emplace(args: []() { SDL_Quit(); });
4735
4736 CClient *pClient = CreateClient();
4737 pClient->SetLoggers(pFileLogger: pFutureFileLogger, pStdoutLogger: std::move(pStdoutLogger));
4738
4739 IKernel *pKernel = IKernel::Create();
4740 pKernel->RegisterInterface(pInterface: pClient, Destroy: false);
4741 pClient->RegisterInterfaces();
4742 CleanerFunctions.emplace(args: [pKernel, pClient]() {
4743 // Ensure that the assert handler doesn't use the client/graphics after they've been destroyed
4744 dbg_assert_set_handler(handler: nullptr);
4745 pKernel->Shutdown();
4746 delete pKernel;
4747 delete pClient;
4748 });
4749
4750 const std::thread::id MainThreadId = std::this_thread::get_id();
4751 dbg_assert_set_handler(handler: [MainThreadId, pClient](const char *pMsg) {
4752 if(MainThreadId != std::this_thread::get_id())
4753 return;
4754 char aOsVersionString[128];
4755 if(!os_version_str(version: aOsVersionString, length: sizeof(aOsVersionString)))
4756 {
4757 str_copy(dst&: aOsVersionString, src: "unknown");
4758 }
4759 char aGpuInfo[512];
4760 pClient->GetGpuInfoString(aGpuInfo);
4761 char aMessage[2048];
4762 str_format(buffer: aMessage, buffer_size: sizeof(aMessage),
4763 format: "An assertion error occurred. Please write down or take a screenshot of the following information and report this error.\n"
4764 "Please also share the assert log"
4765#if defined(CONF_CRASHDUMP)
4766 " and crash log"
4767#endif
4768 " which you should find in the 'dumps' folder in your config directory.\n\n"
4769 "%s\n\n"
4770 "Platform: %s (%s)\n"
4771 "Configuration: base"
4772#if defined(CONF_AUTOUPDATE)
4773 " + autoupdate"
4774#endif
4775#if defined(CONF_CRASHDUMP)
4776 " + crashdump"
4777#endif
4778#if defined(CONF_DEBUG)
4779 " + debug"
4780#endif
4781#if defined(CONF_DISCORD)
4782 " + discord"
4783#endif
4784#if defined(CONF_VIDEORECORDER)
4785 " + videorecorder"
4786#endif
4787#if defined(CONF_WEBSOCKETS)
4788 " + websockets"
4789#endif
4790 "\n"
4791 "Game version: %s %s %s\n"
4792 "OS version: %s\n\n"
4793 "%s", // GPU info
4794 pMsg,
4795 CONF_PLATFORM_STRING, CONF_ARCH_ENDIAN_STRING,
4796 GAME_NAME, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH != nullptr ? GIT_SHORTREV_HASH : "",
4797 aOsVersionString,
4798 aGpuInfo);
4799 // Also log all of this information to the assertion log file
4800 log_error("assertion", "%s", aMessage);
4801 std::vector<IGraphics::CMessageBoxButton> vButtons;
4802 // Storage may not have been initialized yet and viewing files is not supported on Android yet
4803#if !defined(CONF_PLATFORM_ANDROID)
4804 if(pClient->Storage() != nullptr)
4805 {
4806 vButtons.push_back(x: {.m_pLabel = "Show dumps"});
4807 }
4808#endif
4809 vButtons.push_back(x: {.m_pLabel = "OK", .m_Confirm = true, .m_Cancel = true});
4810 const std::optional<int> MessageResult = pClient->ShowMessageBox(MessageBox: {.m_pTitle = "Assertion Error", .m_pMessage = aMessage, .m_vButtons = vButtons});
4811#if !defined(CONF_PLATFORM_ANDROID)
4812 if(pClient->Storage() != nullptr && MessageResult && *MessageResult == 0)
4813 {
4814 char aDumpsPath[IO_MAX_PATH_LENGTH];
4815 pClient->Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "dumps", pBuffer: aDumpsPath, BufferSize: sizeof(aDumpsPath));
4816 pClient->ViewFile(pFilename: aDumpsPath);
4817 }
4818#else
4819 (void)MessageResult;
4820#endif
4821 // Client will crash due to assertion, don't call PerformAllCleanup in this inconsistent state
4822 });
4823
4824 // create the components
4825 IEngine *pEngine = CreateEngine(GAME_NAME, pFutureLogger: pFutureConsoleLogger);
4826 pKernel->RegisterInterface(pInterface: pEngine, Destroy: false);
4827 CleanerFunctions.emplace(args: [pEngine]() {
4828 // Engine has to be destroyed before the graphics so that skin download thread can finish
4829 delete pEngine;
4830 });
4831
4832 IStorage *pStorage;
4833 {
4834 CMemoryLogger MemoryLogger;
4835 MemoryLogger.SetParent(log_get_scope_logger());
4836 {
4837 CLogScope LogScope(&MemoryLogger);
4838 pStorage = CreateStorage(InitializationType: IStorage::EInitializationType::CLIENT, NumArgs: argc, ppArguments: argv);
4839 }
4840 if(!pStorage)
4841 {
4842 log_error("client", "Failed to initialize the storage location (see details above)");
4843 std::string Message = std::string("Failed to initialize the storage location. See details below.\n\n") + MemoryLogger.ConcatenatedLines();
4844 pClient->ShowMessageBox(MessageBox: {.m_pTitle = "Storage Error", .m_pMessage = Message.c_str()});
4845 PerformAllCleanup();
4846 return -1;
4847 }
4848 }
4849 pKernel->RegisterInterface(pInterface: pStorage);
4850
4851 pFutureAssertionLogger->Set(CreateAssertionLogger(pStorage, GAME_NAME));
4852
4853 {
4854 char aBufPath[IO_MAX_PATH_LENGTH];
4855 char aBufName[IO_MAX_PATH_LENGTH];
4856 char aDate[64];
4857 str_timestamp(buffer: aDate, buffer_size: sizeof(aDate));
4858 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 : "");
4859 pStorage->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: aBufName, pBuffer: aBufPath, BufferSize: sizeof(aBufPath));
4860 crashdump_init_if_available(log_file_path: aBufPath);
4861 }
4862
4863 IConsole *pConsole = CreateConsole(FlagMask: CFGFLAG_CLIENT).release();
4864 pKernel->RegisterInterface(pInterface: pConsole);
4865
4866 IConfigManager *pConfigManager = CreateConfigManager();
4867 pKernel->RegisterInterface(pInterface: pConfigManager);
4868
4869 IEngineSound *pEngineSound = CreateEngineSound();
4870 pKernel->RegisterInterface(pInterface: pEngineSound); // IEngineSound
4871 pKernel->RegisterInterface(pInterface: static_cast<ISound *>(pEngineSound), Destroy: false);
4872
4873 IEngineInput *pEngineInput = CreateEngineInput();
4874 pKernel->RegisterInterface(pInterface: pEngineInput); // IEngineInput
4875 pKernel->RegisterInterface(pInterface: static_cast<IInput *>(pEngineInput), Destroy: false);
4876
4877 IEngineTextRender *pEngineTextRender = CreateEngineTextRender();
4878 pKernel->RegisterInterface(pInterface: pEngineTextRender); // IEngineTextRender
4879 pKernel->RegisterInterface(pInterface: static_cast<ITextRender *>(pEngineTextRender), Destroy: false);
4880
4881 IEngineMap *pEngineMap = CreateEngineMap();
4882 pKernel->RegisterInterface(pInterface: pEngineMap); // IEngineMap
4883 pKernel->RegisterInterface(pInterface: static_cast<IMap *>(pEngineMap), Destroy: false);
4884
4885 IDiscord *pDiscord = CreateDiscord();
4886 pKernel->RegisterInterface(pInterface: pDiscord);
4887
4888 ISteam *pSteam = CreateSteam();
4889 pKernel->RegisterInterface(pInterface: pSteam);
4890
4891 INotifications *pNotifications = CreateNotifications();
4892 pKernel->RegisterInterface(pInterface: pNotifications);
4893
4894 pKernel->RegisterInterface(pInterface: CreateEditor(), Destroy: false);
4895 pKernel->RegisterInterface(pInterface: CreateFavorites().release());
4896 pKernel->RegisterInterface(pInterface: CreateGameClient());
4897
4898 pEngine->Init();
4899 pConsole->Init();
4900 pConfigManager->Init();
4901 pNotifications->Init(GAME_NAME " Client");
4902
4903 // register all console commands
4904 pClient->RegisterCommands();
4905
4906 pKernel->RequestInterface<IGameClient>()->OnConsoleInit();
4907
4908 // init client's interfaces
4909 pClient->InitInterfaces();
4910
4911 // execute config file
4912 if(pStorage->FileExists(CONFIG_FILE, Type: IStorage::TYPE_ALL))
4913 {
4914 pConsole->SetUnknownCommandCallback(pfnCallback: SaveUnknownCommandCallback, pUser: pClient);
4915 if(!pConsole->ExecuteFile(CONFIG_FILE))
4916 {
4917 const char *pError = "Failed to load config from '" CONFIG_FILE "'.";
4918 log_error("client", "%s", pError);
4919 pClient->ShowMessageBox(MessageBox: {.m_pTitle = "Config File Error", .m_pMessage = pError});
4920 PerformAllCleanup();
4921 return -1;
4922 }
4923 pConsole->SetUnknownCommandCallback(pfnCallback: IConsole::EmptyUnknownCommandCallback, pUser: nullptr);
4924 }
4925
4926 // execute autoexec file
4927 if(pStorage->FileExists(AUTOEXEC_CLIENT_FILE, Type: IStorage::TYPE_ALL))
4928 {
4929 pConsole->ExecuteFile(AUTOEXEC_CLIENT_FILE);
4930 }
4931 else // fallback
4932 {
4933 pConsole->ExecuteFile(AUTOEXEC_FILE);
4934 }
4935
4936 if(g_Config.m_ClConfigVersion < 1)
4937 {
4938 if(g_Config.m_ClAntiPing == 0)
4939 {
4940 g_Config.m_ClAntiPingPlayers = 1;
4941 g_Config.m_ClAntiPingGrenade = 1;
4942 g_Config.m_ClAntiPingWeapons = 1;
4943 }
4944 }
4945 g_Config.m_ClConfigVersion = 1;
4946
4947 // parse the command line arguments
4948 pConsole->SetUnknownCommandCallback(pfnCallback: UnknownArgumentCallback, pUser: pClient);
4949 pConsole->ParseArguments(NumArgs: argc - 1, ppArguments: &argv[1]);
4950 pConsole->SetUnknownCommandCallback(pfnCallback: IConsole::EmptyUnknownCommandCallback, pUser: nullptr);
4951
4952 if(pSteam->GetConnectAddress())
4953 {
4954 pClient->HandleConnectAddress(pAddr: pSteam->GetConnectAddress());
4955 pSteam->ClearConnectAddress();
4956 }
4957
4958 if(g_Config.m_Logfile[0])
4959 {
4960 const int Mode = g_Config.m_Logappend ? IOFLAG_APPEND : IOFLAG_WRITE;
4961 IOHANDLE Logfile = pStorage->OpenFile(pFilename: g_Config.m_Logfile, Flags: Mode, Type: IStorage::TYPE_SAVE_OR_ABSOLUTE);
4962 if(Logfile)
4963 {
4964 pFutureFileLogger->Set(log_logger_file(file: Logfile));
4965 }
4966 else
4967 {
4968 log_error("client", "failed to open '%s' for logging", g_Config.m_Logfile);
4969 pFutureFileLogger->Set(log_logger_noop());
4970 }
4971 }
4972 else
4973 {
4974 pFutureFileLogger->Set(log_logger_noop());
4975 }
4976
4977 // Register protocol and file extensions
4978#if defined(CONF_FAMILY_WINDOWS)
4979 pClient->ShellRegister();
4980#endif
4981
4982 // Do not automatically translate touch events to mouse events and vice versa.
4983 SDL_SetHint(name: "SDL_TOUCH_MOUSE_EVENTS", value: "0");
4984 SDL_SetHint(name: "SDL_MOUSE_TOUCH_EVENTS", value: "0");
4985
4986 // Support longer IME composition strings (enables SDL_TEXTEDITING_EXT).
4987#if SDL_VERSION_ATLEAST(2, 0, 22)
4988 SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, value: "1");
4989#endif
4990
4991#if defined(CONF_PLATFORM_MACOS)
4992 // Hints will not be set if there is an existing override hint or environment variable that takes precedence.
4993 // So this respects cli environment overrides.
4994 SDL_SetHint("SDL_MAC_OPENGL_ASYNC_DISPATCH", "1");
4995#endif
4996
4997#if defined(CONF_FAMILY_WINDOWS)
4998 SDL_SetHint("SDL_IME_SHOW_UI", g_Config.m_InpImeNativeUi ? "1" : "0");
4999#else
5000 SDL_SetHint(name: "SDL_IME_SHOW_UI", value: "1");
5001#endif
5002
5003#if defined(CONF_PLATFORM_ANDROID)
5004 // Trap the Android back button so it can be handled in our code reliably
5005 // instead of letting the system handle it.
5006 SDL_SetHint("SDL_ANDROID_TRAP_BACK_BUTTON", "1");
5007 // Force landscape screen orientation.
5008 SDL_SetHint("SDL_IOS_ORIENTATIONS", "LandscapeLeft LandscapeRight");
5009#endif
5010
5011 // init SDL
5012 if(SDL_Init(flags: 0) < 0)
5013 {
5014 char aError[256];
5015 str_format(buffer: aError, buffer_size: sizeof(aError), format: "Unable to initialize SDL base: %s", SDL_GetError());
5016 log_error("client", "%s", aError);
5017 pClient->ShowMessageBox(MessageBox: {.m_pTitle = "SDL Error", .m_pMessage = aError});
5018 PerformAllCleanup();
5019 return -1;
5020 }
5021
5022 // run the client
5023 log_trace("client", "initialization finished after %.2fms, starting...", (time_get() - MainStart) * 1000.0f / (float)time_freq());
5024 pClient->Run();
5025
5026 const bool Restarting = pClient->State() == CClient::STATE_RESTARTING;
5027#if !defined(CONF_PLATFORM_ANDROID)
5028 char aRestartBinaryPath[IO_MAX_PATH_LENGTH];
5029 if(Restarting)
5030 {
5031 pStorage->GetBinaryPath(PLAT_CLIENT_EXEC, pBuffer: aRestartBinaryPath, BufferSize: sizeof(aRestartBinaryPath));
5032 }
5033#endif
5034
5035 std::vector<SWarning> vQuittingWarnings = pClient->QuittingWarnings();
5036
5037 PerformCleanup();
5038
5039 for(const SWarning &Warning : vQuittingWarnings)
5040 {
5041 ShowMessageBoxWithoutGraphics(MessageBox: {.m_pTitle = Warning.m_aWarningTitle, .m_pMessage = Warning.m_aWarningMsg});
5042 }
5043
5044 if(Restarting)
5045 {
5046#if defined(CONF_PLATFORM_ANDROID)
5047 RestartAndroidApp();
5048#else
5049 shell_execute(file: aRestartBinaryPath, window_state: EShellExecuteWindowState::FOREGROUND);
5050#endif
5051 }
5052
5053 PerformFinalCleanup();
5054
5055 return 0;
5056}
5057
5058// DDRace
5059
5060const char *CClient::GetCurrentMap() const
5061{
5062 return m_aCurrentMap;
5063}
5064
5065const char *CClient::GetCurrentMapPath() const
5066{
5067 return m_aCurrentMapPath;
5068}
5069
5070SHA256_DIGEST CClient::GetCurrentMapSha256() const
5071{
5072 return m_pMap->Sha256();
5073}
5074
5075unsigned CClient::GetCurrentMapCrc() const
5076{
5077 return m_pMap->Crc();
5078}
5079
5080void CClient::RaceRecord_Start(const char *pFilename)
5081{
5082 if(State() != IClient::STATE_ONLINE)
5083 m_pConsole->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "demorec/record", pStr: "client is not online");
5084 else
5085 m_aDemoRecorder[RECORDER_RACE].Start(
5086 pStorage: Storage(),
5087 pConsole: m_pConsole,
5088 pFilename,
5089 pNetversion: IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(),
5090 pMap: m_aCurrentMap,
5091 Sha256: m_pMap->Sha256(),
5092 MapCrc: m_pMap->Crc(),
5093 pType: "client",
5094 MapSize: m_pMap->MapSize(),
5095 pMapData: nullptr,
5096 MapFile: m_pMap->File(),
5097 pfnFilter: nullptr,
5098 pUser: nullptr);
5099}
5100
5101void CClient::RaceRecord_Stop()
5102{
5103 if(m_aDemoRecorder[RECORDER_RACE].IsRecording())
5104 {
5105 m_aDemoRecorder[RECORDER_RACE].Stop(Mode: IDemoRecorder::EStopMode::KEEP_FILE);
5106 }
5107}
5108
5109bool CClient::RaceRecord_IsRecording()
5110{
5111 return m_aDemoRecorder[RECORDER_RACE].IsRecording();
5112}
5113
5114void CClient::RequestDDNetInfo()
5115{
5116 if(m_pDDNetInfoTask && !m_pDDNetInfoTask->Done())
5117 return;
5118
5119 char aUrl[256];
5120 str_copy(dst&: aUrl, src: DDNET_INFO_URL);
5121
5122 if(g_Config.m_BrIndicateFinished)
5123 {
5124 char aEscaped[128];
5125 EscapeUrl(pBuf: aEscaped, Size: sizeof(aEscaped), pStr: PlayerName());
5126 str_append(dst&: aUrl, src: "?name=");
5127 str_append(dst&: aUrl, src: aEscaped);
5128 }
5129
5130 m_pDDNetInfoTask = HttpGetFile(pUrl: aUrl, pStorage: Storage(), pOutputFile: DDNET_INFO_FILE, StorageType: IStorage::TYPE_SAVE);
5131 m_pDDNetInfoTask->Timeout(Timeout: CTimeout{.m_ConnectTimeoutMs: 10000, .m_TimeoutMs: 0, .m_LowSpeedLimit: 500, .m_LowSpeedTime: 10});
5132 m_pDDNetInfoTask->SkipByFileTime(SkipByFileTime: false); // Always re-download.
5133 // Use ipv4 so we can know the ingame ip addresses of players before they join game servers
5134 m_pDDNetInfoTask->IpResolve(IpResolve: IPRESOLVE::V4);
5135 Http()->Run(pRequest: m_pDDNetInfoTask);
5136 m_InfoState = EInfoState::LOADING;
5137}
5138
5139int CClient::GetPredictionTime()
5140{
5141 int64_t Now = time_get();
5142 return (int)((m_PredictedTime.Get(Now) - m_aGameTime[g_Config.m_ClDummy].Get(Now)) * 1000 / (float)time_freq());
5143}
5144
5145int CClient::GetPredictionTick()
5146{
5147 int PredictionTick = GetPredictionTime() * GameTickSpeed() / 1000.0f;
5148
5149 int PredictionMin = g_Config.m_ClAntiPingLimit * GameTickSpeed() / 1000.0f;
5150
5151 if(g_Config.m_ClAntiPingLimit == 0)
5152 {
5153 float PredictionPercentage = 1 - g_Config.m_ClAntiPingPercent / 100.0f;
5154 PredictionMin = std::floor(x: PredictionTick * PredictionPercentage);
5155 }
5156
5157 if(PredictionMin > PredictionTick - 1)
5158 {
5159 PredictionMin = PredictionTick - 1;
5160 }
5161
5162 if(PredictionMin <= 0)
5163 return PredGameTick(Conn: g_Config.m_ClDummy);
5164
5165 PredictionTick = PredGameTick(Conn: g_Config.m_ClDummy) - PredictionMin;
5166
5167 if(PredictionTick < GameTick(Conn: g_Config.m_ClDummy) + 1)
5168 {
5169 PredictionTick = GameTick(Conn: g_Config.m_ClDummy) + 1;
5170 }
5171 return PredictionTick;
5172}
5173
5174void CClient::GetSmoothTick(int *pSmoothTick, float *pSmoothIntraTick, float MixAmount)
5175{
5176 int64_t GameTime = m_aGameTime[g_Config.m_ClDummy].Get(Now: time_get());
5177 int64_t PredTime = m_PredictedTime.Get(Now: time_get());
5178 int64_t SmoothTime = std::clamp(val: GameTime + (int64_t)(MixAmount * (PredTime - GameTime)), lo: GameTime, hi: PredTime);
5179
5180 *pSmoothTick = (int)(SmoothTime * GameTickSpeed() / time_freq()) + 1;
5181 *pSmoothIntraTick = (SmoothTime - (*pSmoothTick - 1) * time_freq() / GameTickSpeed()) / (float)(time_freq() / GameTickSpeed());
5182}
5183
5184void CClient::AddWarning(const SWarning &Warning)
5185{
5186 const std::unique_lock<std::mutex> Lock(m_WarningsMutex);
5187 m_vWarnings.emplace_back(args: Warning);
5188}
5189
5190std::optional<SWarning> CClient::CurrentWarning()
5191{
5192 const std::unique_lock<std::mutex> Lock(m_WarningsMutex);
5193 if(m_vWarnings.empty())
5194 {
5195 return std::nullopt;
5196 }
5197 else
5198 {
5199 std::optional<SWarning> Result = std::make_optional(t&: m_vWarnings[0]);
5200 m_vWarnings.erase(position: m_vWarnings.begin());
5201 return Result;
5202 }
5203}
5204
5205int CClient::MaxLatencyTicks() const
5206{
5207 return GameTickSpeed() + (PredictionMargin() * GameTickSpeed()) / 1000;
5208}
5209
5210int CClient::PredictionMargin() const
5211{
5212 return m_ServerCapabilities.m_SyncWeaponInput ? g_Config.m_ClPredictionMargin : 10;
5213}
5214
5215int CClient::UdpConnectivity(int NetType)
5216{
5217 static const int NETTYPES[2] = {NETTYPE_IPV6, NETTYPE_IPV4};
5218 int Connectivity = CONNECTIVITY_UNKNOWN;
5219 for(int PossibleNetType : NETTYPES)
5220 {
5221 if((NetType & PossibleNetType) == 0)
5222 {
5223 continue;
5224 }
5225 NETADDR GlobalUdpAddr;
5226 int NewConnectivity;
5227 switch(m_aNetClient[CONN_MAIN].GetConnectivity(NetType: PossibleNetType, pGlobalAddr: &GlobalUdpAddr))
5228 {
5229 case CONNECTIVITY::UNKNOWN:
5230 NewConnectivity = CONNECTIVITY_UNKNOWN;
5231 break;
5232 case CONNECTIVITY::CHECKING:
5233 NewConnectivity = CONNECTIVITY_CHECKING;
5234 break;
5235 case CONNECTIVITY::UNREACHABLE:
5236 NewConnectivity = CONNECTIVITY_UNREACHABLE;
5237 break;
5238 case CONNECTIVITY::REACHABLE:
5239 NewConnectivity = CONNECTIVITY_REACHABLE;
5240 break;
5241 case CONNECTIVITY::ADDRESS_KNOWN:
5242 GlobalUdpAddr.port = 0;
5243 if(m_HaveGlobalTcpAddr && NetType == (int)m_GlobalTcpAddr.type && net_addr_comp(a: &m_GlobalTcpAddr, b: &GlobalUdpAddr) != 0)
5244 {
5245 NewConnectivity = CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES;
5246 break;
5247 }
5248 NewConnectivity = CONNECTIVITY_REACHABLE;
5249 break;
5250 default:
5251 dbg_assert(0, "invalid connectivity value");
5252 return CONNECTIVITY_UNKNOWN;
5253 }
5254 Connectivity = std::max(a: Connectivity, b: NewConnectivity);
5255 }
5256 return Connectivity;
5257}
5258
5259static bool ViewLinkImpl(const char *pLink)
5260{
5261#if defined(CONF_PLATFORM_ANDROID)
5262 if(SDL_OpenURL(pLink) == 0)
5263 {
5264 return true;
5265 }
5266 log_error("client", "Failed to open link '%s' (%s)", pLink, SDL_GetError());
5267 return false;
5268#else
5269 if(open_link(link: pLink))
5270 {
5271 return true;
5272 }
5273 log_error("client", "Failed to open link '%s'", pLink);
5274 return false;
5275#endif
5276}
5277
5278bool CClient::ViewLink(const char *pLink)
5279{
5280 if(!str_startswith(str: pLink, prefix: "https://"))
5281 {
5282 log_error("client", "Failed to open link '%s': only https-links are allowed", pLink);
5283 return false;
5284 }
5285 return ViewLinkImpl(pLink);
5286}
5287
5288bool CClient::ViewFile(const char *pFilename)
5289{
5290#if defined(CONF_PLATFORM_MACOS)
5291 return ViewLinkImpl(pFilename);
5292#else
5293 // Create a file link so the path can contain forward and
5294 // backward slashes. But the file link must be absolute.
5295 char aWorkingDir[IO_MAX_PATH_LENGTH];
5296 if(fs_is_relative_path(path: pFilename))
5297 {
5298 if(!fs_getcwd(buffer: aWorkingDir, buffer_size: sizeof(aWorkingDir)))
5299 {
5300 log_error("client", "Failed to open file '%s' (failed to get working directory)", pFilename);
5301 return false;
5302 }
5303 str_append(dst&: aWorkingDir, src: "/");
5304 }
5305 else
5306 aWorkingDir[0] = '\0';
5307
5308 char aFileLink[IO_MAX_PATH_LENGTH];
5309 str_format(buffer: aFileLink, buffer_size: sizeof(aFileLink), format: "file://%s%s", aWorkingDir, pFilename);
5310 return ViewLinkImpl(pLink: aFileLink);
5311#endif
5312}
5313
5314#if defined(CONF_FAMILY_WINDOWS)
5315void CClient::ShellRegister()
5316{
5317 char aFullPath[IO_MAX_PATH_LENGTH];
5318 Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
5319 if(!aFullPath[0])
5320 {
5321 log_error("client", "Failed to register protocol and file extensions: could not determine absolute path");
5322 return;
5323 }
5324
5325 bool Updated = false;
5326 if(!windows_shell_register_protocol("ddnet", aFullPath, &Updated))
5327 log_error("client", "Failed to register ddnet protocol");
5328 if(!windows_shell_register_extension(".map", "Map File", GAME_NAME, aFullPath, &Updated))
5329 log_error("client", "Failed to register .map file extension");
5330 if(!windows_shell_register_extension(".demo", "Demo File", GAME_NAME, aFullPath, &Updated))
5331 log_error("client", "Failed to register .demo file extension");
5332 if(!windows_shell_register_application(GAME_NAME, aFullPath, &Updated))
5333 log_error("client", "Failed to register application");
5334 if(Updated)
5335 windows_shell_update();
5336}
5337
5338void CClient::ShellUnregister()
5339{
5340 char aFullPath[IO_MAX_PATH_LENGTH];
5341 Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
5342 if(!aFullPath[0])
5343 {
5344 log_error("client", "Failed to unregister protocol and file extensions: could not determine absolute path");
5345 return;
5346 }
5347
5348 bool Updated = false;
5349 if(!windows_shell_unregister_class("ddnet", &Updated))
5350 log_error("client", "Failed to unregister ddnet protocol");
5351 if(!windows_shell_unregister_class(GAME_NAME ".map", &Updated))
5352 log_error("client", "Failed to unregister .map file extension");
5353 if(!windows_shell_unregister_class(GAME_NAME ".demo", &Updated))
5354 log_error("client", "Failed to unregister .demo file extension");
5355 if(!windows_shell_unregister_application(aFullPath, &Updated))
5356 log_error("client", "Failed to unregister application");
5357 if(Updated)
5358 windows_shell_update();
5359}
5360#endif
5361
5362std::optional<int> CClient::ShowMessageBox(const IGraphics::CMessageBox &MessageBox)
5363{
5364 std::optional<int> Result = m_pGraphics == nullptr ? std::nullopt : m_pGraphics->ShowMessageBox(MessageBox);
5365 if(!Result)
5366 {
5367 Result = ShowMessageBoxWithoutGraphics(MessageBox);
5368 }
5369 return Result;
5370}
5371
5372void CClient::GetGpuInfoString(char (&aGpuInfo)[512])
5373{
5374 if(m_pGraphics == nullptr || !m_pGraphics->IsBackendInitialized())
5375 {
5376 str_format(buffer: aGpuInfo, buffer_size: std::size(aGpuInfo),
5377 format: "Graphics backend: %s %d.%d.%d\n"
5378 "Graphics %s not yet initialized.",
5379 g_Config.m_GfxBackend, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch,
5380 m_pGraphics == nullptr ? "were" : "backend was");
5381 }
5382 else
5383 {
5384 // 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.
5385 str_format(aGpuInfo, std::size(aGpuInfo),
5386 "Graphics backend: %s %d.%d.%d\n"
5387 "GPU: %s - %s - %s\n"
5388 "Texture: %" PRIu64 " MiB, "
5389 "Buffer: %" PRIu64 " MiB, "
5390 "Streamed: %" PRIu64 " MiB, "
5391 "Staging: %" PRIu64 " MiB",
5392 g_Config.m_GfxBackend, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch,
5393 m_pGraphics->GetVendorString(), m_pGraphics->GetRendererString(), m_pGraphics->GetVersionString(),
5394 m_pGraphics->TextureMemoryUsage() / 1024 / 1024,
5395 m_pGraphics->BufferMemoryUsage() / 1024 / 1024,
5396 m_pGraphics->StreamedMemoryUsage() / 1024 / 1024,
5397 m_pGraphics->StagingMemoryUsage() / 1024 / 1024);
5398 }
5399}
5400
5401void CClient::SetLoggers(std::shared_ptr<ILogger> &&pFileLogger, std::shared_ptr<ILogger> &&pStdoutLogger)
5402{
5403 m_pFileLogger = pFileLogger;
5404 m_pStdoutLogger = pStdoutLogger;
5405}
5406