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