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