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