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