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