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