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