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