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