ddnet/src/engine/client/client.cpp

4827 lines
142 KiB
C++
Raw Normal View History

2010-11-20 10:37:14 +00:00
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
2010-05-29 07:25:38 +00:00
2023-02-23 09:48:11 +00:00
#include <base/hash.h>
#include <base/hash_ctxt.h>
#include <base/logger.h>
#include <base/math.h>
2010-05-29 07:25:38 +00:00
#include <base/system.h>
2022-06-16 17:50:46 +00:00
#include <engine/external/json-parser/json.h>
2011-02-27 14:03:57 +00:00
#include <engine/config.h>
#include <engine/console.h>
2022-06-16 17:50:46 +00:00
#include <engine/discord.h>
2011-02-27 14:03:57 +00:00
#include <engine/editor.h>
#include <engine/engine.h>
#include <engine/favorites.h>
2011-02-27 14:03:57 +00:00
#include <engine/graphics.h>
#include <engine/input.h>
#include <engine/keys.h>
#include <engine/map.h>
#include <engine/serverbrowser.h>
#include <engine/sound.h>
2020-08-20 10:17:44 +00:00
#include <engine/steam.h>
2011-02-27 14:03:57 +00:00
#include <engine/storage.h>
#include <engine/textrender.h>
#include <engine/shared/assertion_logger.h>
2010-05-29 07:25:38 +00:00
#include <engine/shared/compression.h>
#include <engine/shared/config.h>
2011-02-27 14:03:57 +00:00
#include <engine/shared/demo.h>
#include <engine/shared/fifo.h>
#include <engine/shared/filecollection.h>
#include <engine/shared/http.h>
Add HTTP masterserver registering and HTTP masterserver Registering ----------- The idea is that game servers push their server info to the masterservers every 15 seconds or when the server info changes, but not more than once per second. The game servers do not support the old registering protocol anymore, the backward compatibility is handled by the masterserver. The register call is a HTTP POST to a URL like `https://master1.ddnet.tw/ddnet/15/register` and looks like this: ```json POST /ddnet/15/register HTTP/1.1 Address: tw-0.6+udp://connecting-address.invalid:8303 Secret: 81fa3955-6f83-4290-818d-31c0906b1118 Challenge-Secret: 81fa3955-6f83-4290-818d-31c0906b1118:tw0.6/ipv6 Info-Serial: 0 { "max_clients": 64, "max_players": 64, "passworded": false, "game_type": "TestDDraceNetwork", "name": "My DDNet server", "map": { "name": "dm1", "sha256": "0b0c481d77519c32fbe85624ef16ec0fa9991aec7367ad538bd280f28d8c26cf", "size": 5805 }, "version": "0.6.4, 16.0.3", "clients": [] } ``` The `Address` header declares that the server wants to register itself as a `tw-0.6+udp` server, i.e. a server speaking a Teeworlds-0.6-compatible protocol. The free-form `Secret` header is used as a server identity, the server list will be deduplicated via this secret. The free-form `Challenge-Secret` is sent back via UDP for a port forward check. This might have security implications as the masterserver can be asked to send a UDP packet containing some user-controlled bytes. This is somewhat mitigated by the fact that it can only go to an attacker-controlled IP address. The `Info-Serial` header is an integer field that should increase each time the server info (in the body) changes. The masterserver uses that field to ensure that it doesn't use old server infos. The body is a free-form JSON object set by the game server. It should contain certain keys in the correct form to be accepted by clients. The body is optional if the masterserver already confirmed the reception of the info with the given `Info-Serial`. Not shown in this payload is the `Connless-Token` header that is used for Teeworlds 0.7 style communication. Also not shown is the `Challenge-Token` that should be included once the server receives the challenge token via UDP. The masterserver responds with a `200 OK` with a body like this: ``` {"status":"success"} ``` The `status` field can be `success` if the server was successfully registered on the masterserver, `need_challenge` if the masterserver wants the correct `Challenge-Token` header before the register process is successful, `need_info` if the server sent an empty body but the masterserver doesn't actually know the server info. It can also be `error` if the request was malformed, only in this case an HTTP status code except `200 OK` is sent. Synchronization --------------- The masterserver keeps state and outputs JSON files every second. ```json { "servers": [ { "addresses": [ "tw-0.6+udp://127.0.0.1:8303", "tw-0.6+udp://[::1]:8303" ], "info_serial": 0, "info": { "max_clients": 64, "max_players": 64, "passworded": false, "game_type": "TestDDraceNetwork", "name": "My DDNet server", "map": { "name": "dm1", "sha256": "0b0c481d77519c32fbe85624ef16ec0fa9991aec7367ad538bd280f28d8c26cf", "size": 5805 }, "version": "0.6.4, 16.0.3", "clients": [] } } ] } ``` `servers.json` (or configured by `--out`) is a server list that is compatible with DDNet 15.5+ clients. It is a JSON object containing a single key `servers` with a list of game servers. Each game server is represented by a JSON object with an `addresses` key containing a list of all known addresses of the server and an `info` key containing the free-form server info sent by the game server. The free-form `info` JSON object re-encoded by the master server and thus canonicalized and stripped of any whitespace characters outside strings. ```json { "kind": "mastersrv", "now": 1816002, "secrets": { "tw-0.6+udp://127.0.0.1:8303": { "ping_time": 1811999, "secret": "42d8f991-f2fa-46e5-a9ae-ebcc93846feb" }, "tw-0.6+udp://[::1]:8303": { "ping_time": 1811999, "secret": "42d8f991-f2fa-46e5-a9ae-ebcc93846feb" } }, "servers": { "42d8f991-f2fa-46e5-a9ae-ebcc93846feb": { "info_serial": 0, "info": { "max_clients": 64, "max_players": 64, "passworded": false, "game_type": "TestDDraceNetwork", "name": "My DDNet server", "map": { "name": "dm1", "sha256": "0b0c481d77519c32fbe85624ef16ec0fa9991aec7367ad538bd280f28d8c26cf", "size": 5805 }, "version": "0.6.4, 16.0.3", "clients": [] } } } } ``` `--write-dump` outputs a JSON file compatible with `--read-dump-dir`, this can be used to synchronize servers across different masterservers. `--read-dump-dir` is also used to ingest servers from the backward compatibility layer that pings each server for their server info using the old protocol. The `kind` field describe that this is `mastersrv` output and not from a `backcompat`. This is used for prioritizing `mastersrv` information over `backcompat` information. The `now` field contains an integer describing the current time in milliseconds relative an unspecified epoch that is fixed for each JSON file. This is done instead of using the current time as the epoch for better compression of non-changing data. `secrets` is a map from each server address and to a JSON object containing the last ping time (`ping_time`) in milliseconds relative to the same epoch as before, and the server secret (`secret`) that is used to unify server infos from different addresses of the same logical server. `servers` is a map from the aforementioned `secret`s to the corresponding `info_serial` and `info`. ```json [ "tw-0.6+udp://127.0.0.1:8303", "tw-0.6+udp://[::1]:8303" ] ``` `--write-addresses` outputs a JSON file containing all addresses corresponding to servers that are registered to HTTP masterservers. It does not contain the servers that are obtained via backward compatibility measures. This file can be used by an old-style masterserver to also list new-style servers without the game servers having to register there. An implementation of this can be found at https://github.com/heinrich5991/teeworlds/tree/mastersrv_6_backcompat for Teeworlds 0.5/0.6 masterservers and at https://github.com/heinrich5991/teeworlds/tree/mastersrv_7_backcompat for Teeworlds 0.7 masterservers. All these JSON files can be sent over the network in an efficient way using https://github.com/heinrich5991/twmaster-collect. It establishes a zstd-compressed TCP connection authenticated by a string token that is sent in plain-text. It watches the specified file and transmits it every time it changes. Due to the zstd-compression, the data sent over the network is similar to the size of a diff. Implementation -------------- The masterserver implementation was done in Rust. The current gameserver register implementation doesn't support more than one masterserver for registering.
2022-05-19 20:03:17 +00:00
#include <engine/shared/masterserver.h>
2010-05-29 07:25:38 +00:00
#include <engine/shared/network.h>
#include <engine/shared/packer.h>
#include <engine/shared/protocol.h>
#include <engine/shared/protocol_ex.h>
#include <engine/shared/rust_version.h>
2011-02-27 14:03:57 +00:00
#include <engine/shared/snapshot.h>
#include <engine/shared/uuid_manager.h>
2010-05-29 07:25:38 +00:00
#include <game/generated/protocol.h>
2022-05-29 16:33:38 +00:00
#include <game/localization.h>
#include <game/version.h>
2020-09-10 18:14:47 +00:00
#include "client.h"
#include "demoedit.h"
2011-03-23 12:06:35 +00:00
#include "friends.h"
#include "notifications.h"
#include "serverbrowser.h"
2010-05-29 07:25:38 +00:00
2016-08-27 19:10:27 +00:00
#if defined(CONF_VIDEORECORDER)
#include "video.h"
2016-08-27 19:10:27 +00:00
#endif
2016-08-27 15:51:23 +00:00
#include "SDL.h"
#ifdef main
#undef main
#endif
2010-05-29 07:25:38 +00:00
2022-05-18 16:00:05 +00:00
#include <chrono>
#include <limits>
#include <new>
#include <stack>
2022-05-18 16:00:05 +00:00
#include <thread>
#include <tuple>
2022-05-18 16:00:05 +00:00
using namespace std::chrono_literals;
static const ColorRGBA gs_ClientNetworkPrintColor{0.7f, 1, 0.7f, 1.0f};
static const ColorRGBA gs_ClientNetworkErrPrintColor{1.0f, 0.25f, 0.25f, 1.0f};
CClient::CClient() :
Add time scale to debug graphs for constant scrolling speed Store X value (time) for all graph entries in addition to the Y value (FPS, prediction margin etc.). The `CGraph::Add` function adds values to the graph at the current time. The `CGraph::InsertAt` function allows specifying arbitrary X values, as long as the values are inserted in increasing order. The entries are kept in a ringbuffer and old entries are recycled when it's full. The size of the ringbuffer is configurable for each graph, as the FPS graph needs significantly more buffer because values are added more often. The scrolling speed of the graphs is fixed by specifying the maximum size of the window of values which should be displayed. For this purpose, a parameter is added to the `CGraph::Scale` function to specify the size of the window which should be rendered in the `CGraph::Render` function. For the FPS graph only the last second is rendered, so small spikes are still noticeable. For prediction and gametime margin graphs the last five seconds are rendered, which should result in a similar scrolling speed as before this change. The debug tuning graph is a special case, where the X values set manually and fixed to 0-127, same as before, instead of being based on the current time. The graph rendering is made much more efficient by precalculating when the vertex colors need to be updated, to avoid all unnecessary calls to `SetColorVertex`. Additionally, line items are bundled together in an array to avoid calling `LinesDraw` for every individual line item.
2024-01-02 22:46:52 +00:00
m_DemoPlayer(&m_SnapshotDelta, true, [&]() { UpdateDemoIntraTimers(); }),
m_InputtimeMarginGraph(128),
m_GametimeMarginGraph(128),
m_FpsGraph(4096)
2010-05-29 07:25:38 +00:00
{
m_StateStartTime = time_get();
for(auto &DemoRecorder : m_aDemoRecorder)
2020-10-26 14:14:07 +00:00
DemoRecorder = CDemoRecorder(&m_SnapshotDelta);
m_LastRenderTime = time_get();
mem_zero(m_aInputs, sizeof(m_aInputs));
mem_zero(m_aapSnapshots, sizeof(m_aapSnapshots));
for(auto &SnapshotStorage : m_aSnapshotStorage)
SnapshotStorage.Init();
mem_zero(m_aDemorecSnapshotHolders, sizeof(m_aDemorecSnapshotHolders));
mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
mem_zero(&m_Checksum, sizeof(m_Checksum));
for(auto &GameTime : m_aGameTime)
GameTime.Init(0);
m_PredictedTime.Init(0);
2010-05-29 07:25:38 +00:00
}
// ----- send functions -----
2020-04-13 13:34:53 +00:00
static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer)
{
Packer.Reset();
if(pMsg->m_MsgId < OFFSET_UUID)
2020-04-13 13:34:53 +00:00
{
Packer.AddInt((pMsg->m_MsgId << 1) | (pMsg->m_System ? 1 : 0));
2020-04-13 13:34:53 +00:00
}
else
{
Packer.AddInt(pMsg->m_System ? 1 : 0); // NETMSG_EX, NETMSGTYPE_EX
g_UuidManager.PackUuid(pMsg->m_MsgId, &Packer);
2020-04-13 13:34:53 +00:00
}
Packer.AddRaw(pMsg->Data(), pMsg->Size());
return false;
}
int CClient::SendMsg(int Conn, CMsgPacker *pMsg, int Flags)
2010-05-29 07:25:38 +00:00
{
CNetChunk Packet;
if(State() == IClient::STATE_OFFLINE)
return 0;
2020-04-13 13:34:53 +00:00
// repack message (inefficient)
CPacker Pack;
if(RepackMsg(pMsg, Pack))
return 0;
2010-05-29 07:25:38 +00:00
mem_zero(&Packet, sizeof(CNetChunk));
Packet.m_ClientId = 0;
2020-04-13 13:34:53 +00:00
Packet.m_pData = Pack.Data();
Packet.m_DataSize = Pack.Size();
2010-05-29 07:25:38 +00:00
if(Flags & MSGFLAG_VITAL)
2010-05-29 07:25:38 +00:00
Packet.m_Flags |= NETSENDFLAG_VITAL;
if(Flags & MSGFLAG_FLUSH)
2010-05-29 07:25:38 +00:00
Packet.m_Flags |= NETSENDFLAG_FLUSH;
if((Flags & MSGFLAG_RECORD) && Conn == g_Config.m_ClDummy)
2010-05-29 07:25:38 +00:00
{
for(auto &i : m_aDemoRecorder)
if(i.IsRecording())
i.RecordMessage(Packet.m_pData, Packet.m_DataSize);
2010-05-29 07:25:38 +00:00
}
if(!(Flags & MSGFLAG_NOSEND))
{
m_aNetClient[Conn].Send(&Packet);
}
2010-05-29 07:25:38 +00:00
return 0;
}
int CClient::SendMsgActive(CMsgPacker *pMsg, int Flags)
{
return SendMsg(g_Config.m_ClDummy, pMsg, Flags);
}
void CClient::SendInfo(int Conn)
2010-05-29 07:25:38 +00:00
{
CMsgPacker MsgVer(NETMSG_CLIENTVER, true);
MsgVer.AddRaw(&m_ConnectionId, sizeof(m_ConnectionId));
MsgVer.AddInt(GameClient()->DDNetVersion());
MsgVer.AddString(GameClient()->DDNetVersionStr());
SendMsg(Conn, &MsgVer, MSGFLAG_VITAL);
CMsgPacker Msg(NETMSG_INFO, true);
Msg.AddString(GameClient()->NetVersion());
Msg.AddString(m_aPassword);
SendMsg(Conn, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH);
2010-05-29 07:25:38 +00:00
}
void CClient::SendEnterGame(int Conn)
2010-05-29 07:25:38 +00:00
{
CMsgPacker Msg(NETMSG_ENTERGAME, true);
SendMsg(Conn, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH);
2010-05-29 07:25:38 +00:00
}
void CClient::SendReady(int Conn)
2010-05-29 07:25:38 +00:00
{
CMsgPacker Msg(NETMSG_READY, true);
SendMsg(Conn, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH);
2010-05-29 07:25:38 +00:00
}
void CClient::SendMapRequest()
{
if(m_MapdownloadFileTemp)
{
io_close(m_MapdownloadFileTemp);
Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE);
}
m_MapdownloadFileTemp = Storage()->OpenFile(m_aMapdownloadFilenameTemp, IOFLAG_WRITE, IStorage::TYPE_SAVE);
CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true);
Msg.AddInt(m_MapdownloadChunk);
SendMsg(CONN_MAIN, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH);
}
2010-05-29 07:25:38 +00:00
void CClient::RconAuth(const char *pName, const char *pPassword)
{
2014-04-27 11:44:04 +00:00
if(RconAuthed())
return;
if(pName != m_aRconUsername)
str_copy(m_aRconUsername, pName);
if(pPassword != m_aRconPassword)
2022-07-09 16:14:56 +00:00
str_copy(m_aRconPassword, pPassword);
CMsgPacker Msg(NETMSG_RCON_AUTH, true);
Msg.AddString(pName);
Msg.AddString(pPassword);
Msg.AddInt(1);
SendMsgActive(&Msg, MSGFLAG_VITAL);
2010-05-29 07:25:38 +00:00
}
void CClient::Rcon(const char *pCmd)
{
CMsgPacker Msg(NETMSG_RCON_CMD, true);
Msg.AddString(pCmd);
SendMsgActive(&Msg, MSGFLAG_VITAL);
2010-05-29 07:25:38 +00:00
}
float CClient::GotRconCommandsPercentage() const
{
if(m_ExpectedRconCommands < 1)
return -1.0f;
if(m_GotRconCommands > m_ExpectedRconCommands)
return -1.0f;
return (float)m_GotRconCommands / (float)m_ExpectedRconCommands;
}
bool CClient::ConnectionProblems() const
2010-05-29 07:25:38 +00:00
{
return m_aNetClient[g_Config.m_ClDummy].GotProblems(MaxLatencyTicks() * time_freq() / GameTickSpeed()) != 0;
2010-05-29 07:25:38 +00:00
}
void CClient::DirectInput(int *pInput, int Size)
{
CMsgPacker Msg(NETMSG_INPUT, true);
Msg.AddInt(m_aAckGameTick[g_Config.m_ClDummy]);
Msg.AddInt(m_aPredTick[g_Config.m_ClDummy]);
2010-05-29 07:25:38 +00:00
Msg.AddInt(Size);
for(int i = 0; i < Size / 4; i++)
2010-05-29 07:25:38 +00:00
Msg.AddInt(pInput[i]);
SendMsgActive(&Msg, 0);
2010-05-29 07:25:38 +00:00
}
void CClient::SendInput()
{
2021-06-23 05:05:49 +00:00
int64_t Now = time_get();
2010-05-29 07:25:38 +00:00
if(m_aPredTick[g_Config.m_ClDummy] <= 0)
2010-05-29 07:25:38 +00:00
return;
bool Force = false;
// fetch input
2020-10-18 14:51:26 +00:00
for(int Dummy = 0; Dummy < NUM_DUMMIES; Dummy++)
{
if(!m_DummyConnected && Dummy != 0)
2014-07-07 23:41:45 +00:00
{
break;
2014-07-07 23:41:45 +00:00
}
int i = g_Config.m_ClDummy ^ Dummy;
int Size = GameClient()->OnSnapInput(m_aInputs[i][m_aCurrentInput[i]].m_aData, Dummy, Force);
2014-04-28 18:43:08 +00:00
if(Size)
2014-04-28 20:12:50 +00:00
{
// pack input
CMsgPacker Msg(NETMSG_INPUT, true);
Msg.AddInt(m_aAckGameTick[i]);
Msg.AddInt(m_aPredTick[g_Config.m_ClDummy]);
Msg.AddInt(Size);
2014-04-28 20:12:50 +00:00
m_aInputs[i][m_aCurrentInput[i]].m_Tick = m_aPredTick[g_Config.m_ClDummy];
m_aInputs[i][m_aCurrentInput[i]].m_PredictedTime = m_PredictedTime.Get(Now);
m_aInputs[i][m_aCurrentInput[i]].m_PredictionMargin = PredictionMargin() * time_freq() / 1000;
m_aInputs[i][m_aCurrentInput[i]].m_Time = Now;
// pack it
for(int k = 0; k < Size / 4; k++)
Msg.AddInt(m_aInputs[i][m_aCurrentInput[i]].m_aData[k]);
m_aCurrentInput[i]++;
m_aCurrentInput[i] %= 200;
SendMsg(i, &Msg, MSGFLAG_FLUSH);
// ugly workaround for dummy. we need to send input with dummy to prevent
// prediction time resets. but if we do it too often, then it's
// impossible to use grenade with frozen dummy that gets hammered...
if(g_Config.m_ClDummyCopyMoves || m_aCurrentInput[i] % 2)
Force = true;
2014-04-28 20:12:50 +00:00
}
}
2010-05-29 07:25:38 +00:00
}
const char *CClient::LatestVersion() const
2010-05-29 07:25:38 +00:00
{
return m_aVersionStr;
}
2018-02-04 15:00:47 +00:00
// TODO: OPT: do this a lot smarter!
int *CClient::GetInput(int Tick, int IsDummy) const
2010-05-29 07:25:38 +00:00
{
int Best = -1;
const int d = IsDummy ^ g_Config.m_ClDummy;
2010-05-29 07:25:38 +00:00
for(int i = 0; i < 200; i++)
{
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))
2010-05-29 07:25:38 +00:00
Best = i;
}
if(Best != -1)
2022-06-06 02:17:55 +00:00
return (int *)m_aInputs[d][Best].m_aData;
2010-05-29 07:25:38 +00:00
return 0;
}
// ------ state handling -----
void CClient::SetState(EClientState State)
2010-05-29 07:25:38 +00:00
{
2020-10-02 13:44:27 +00:00
if(m_State == IClient::STATE_QUITTING || m_State == IClient::STATE_RESTARTING)
return;
if(m_State == State)
return;
2010-05-29 07:25:38 +00:00
if(g_Config.m_Debug)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "state change. last=%d current=%d", m_State, State);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf);
}
const EClientState OldState = m_State;
m_State = State;
m_StateStartTime = time_get();
GameClient()->OnStateChange(m_State, OldState);
if(State == IClient::STATE_OFFLINE && m_ReconnectTime == 0)
{
if(g_Config.m_ClReconnectFull > 0 && (str_find_nocase(ErrorString(), "full") || str_find_nocase(ErrorString(), "reserved")))
m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectFull;
else if(g_Config.m_ClReconnectTimeout > 0 && (str_find_nocase(ErrorString(), "Timeout") || str_find_nocase(ErrorString(), "Too weak connection")))
m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectTimeout;
}
if(State == IClient::STATE_ONLINE)
{
const bool AnnounceAddr = m_ServerBrowser.IsRegistered(ServerAddress());
Discord()->SetGameInfo(ServerAddress(), m_aCurrentMap, AnnounceAddr);
Steam()->SetGameInfo(ServerAddress(), m_aCurrentMap, AnnounceAddr);
}
else if(OldState == IClient::STATE_ONLINE)
{
Discord()->ClearGameInfo();
Steam()->ClearGameInfo();
}
2010-05-29 07:25:38 +00:00
}
// called when the map is loaded and we should init for a new round
void CClient::OnEnterGame(bool Dummy)
2010-05-29 07:25:38 +00:00
{
// reset input
for(int i = 0; i < 200; i++)
{
m_aInputs[Dummy][i].m_Tick = -1;
}
m_aCurrentInput[Dummy] = 0;
2010-05-29 07:25:38 +00:00
// reset snapshots
m_aapSnapshots[Dummy][SNAP_CURRENT] = nullptr;
m_aapSnapshots[Dummy][SNAP_PREV] = nullptr;
m_aSnapshotStorage[Dummy].PurgeAll();
m_aReceivedSnapshots[Dummy] = 0;
m_aSnapshotParts[Dummy] = 0;
m_aSnapshotIncomingDataSize[Dummy] = 0;
m_SnapCrcErrors = 0;
// Also make gameclient aware that snapshots have been purged
GameClient()->InvalidateSnapshot();
// reset times
m_aAckGameTick[Dummy] = -1;
m_aCurrentRecvTick[Dummy] = 0;
m_aPrevGameTick[Dummy] = 0;
m_aCurGameTick[Dummy] = 0;
m_aGameIntraTick[Dummy] = 0.0f;
m_aGameTickTime[Dummy] = 0.0f;
m_aGameIntraTickSincePrev[Dummy] = 0.0f;
m_aPredTick[Dummy] = 0;
m_aPredIntraTick[Dummy] = 0.0f;
m_aGameTime[Dummy].Init(0);
m_PredictedTime.Init(0);
if(!Dummy)
{
m_LastDummyConnectTime = 0;
}
GameClient()->OnEnterGame();
2010-05-29 07:25:38 +00:00
}
void CClient::EnterGame(int Conn)
2010-05-29 07:25:38 +00:00
{
if(State() == IClient::STATE_DEMOPLAYBACK)
return;
m_aCodeRunAfterJoin[Conn] = false;
2010-05-29 07:25:38 +00:00
// now we will wait for two snapshots
// to finish the connection
SendEnterGame(Conn);
OnEnterGame(Conn);
ServerInfoRequest(); // fresh one for timeout protection
m_CurrentServerNextPingTime = time_get() + time_freq() / 2;
}
void GenerateTimeoutCode(char *pBuffer, unsigned Size, char *pSeed, const NETADDR *pAddrs, int NumAddrs, bool Dummy)
{
MD5_CTX Md5;
md5_init(&Md5);
const char *pDummy = Dummy ? "dummy" : "normal";
md5_update(&Md5, (unsigned char *)pDummy, str_length(pDummy) + 1);
md5_update(&Md5, (unsigned char *)pSeed, str_length(pSeed) + 1);
for(int i = 0; i < NumAddrs; i++)
{
md5_update(&Md5, (unsigned char *)&pAddrs[i], sizeof(pAddrs[i]));
}
MD5_DIGEST Digest = md5_finish(&Md5);
unsigned short aRandom[8];
mem_copy(aRandom, Digest.data, sizeof(aRandom));
generate_password(pBuffer, Size, aRandom, 8);
}
void CClient::GenerateTimeoutSeed()
{
secure_random_password(g_Config.m_ClTimeoutSeed, sizeof(g_Config.m_ClTimeoutSeed), 16);
}
void CClient::GenerateTimeoutCodes(const NETADDR *pAddrs, int NumAddrs)
{
if(g_Config.m_ClTimeoutSeed[0])
{
for(int i = 0; i < 2; i++)
{
GenerateTimeoutCode(m_aTimeoutCodes[i], sizeof(m_aTimeoutCodes[i]), g_Config.m_ClTimeoutSeed, pAddrs, NumAddrs, i);
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "timeout code '%s' (%s)", m_aTimeoutCodes[i], i == 0 ? "normal" : "dummy");
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
}
}
else
{
2022-07-09 16:14:56 +00:00
str_copy(m_aTimeoutCodes[0], g_Config.m_ClTimeoutCode);
str_copy(m_aTimeoutCodes[1], g_Config.m_ClDummyTimeoutCode);
}
2010-05-29 07:25:38 +00:00
}
void CClient::Connect(const char *pAddress, const char *pPassword)
2010-05-29 07:25:38 +00:00
{
// Disconnect will not change the state if we are already quitting/restarting
if(m_State == IClient::STATE_QUITTING || m_State == IClient::STATE_RESTARTING)
return;
2010-05-29 07:25:38 +00:00
Disconnect();
dbg_assert(m_State == IClient::STATE_OFFLINE, "Disconnect must ensure that client is offline");
2010-05-29 07:25:38 +00:00
m_ConnectionId = RandomUuid();
if(pAddress != m_aConnectAddressStr)
2022-07-09 16:14:56 +00:00
str_copy(m_aConnectAddressStr, pAddress);
2010-05-29 07:25:38 +00:00
char aMsg[512];
str_format(aMsg, sizeof(aMsg), "connecting to '%s'", m_aConnectAddressStr);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aMsg, gs_ClientNetworkPrintColor);
2010-05-29 07:25:38 +00:00
ServerInfoRequest();
int NumConnectAddrs = 0;
NETADDR aConnectAddrs[MAX_SERVER_ADDRESSES];
mem_zero(aConnectAddrs, sizeof(aConnectAddrs));
const char *pNextAddr = pAddress;
char aBuffer[128];
while((pNextAddr = str_next_token(pNextAddr, ",", aBuffer, sizeof(aBuffer))))
{
NETADDR NextAddr;
char aHost[128];
int url = net_addr_from_url(&NextAddr, aBuffer, aHost, sizeof(aHost));
if(url > 0)
str_copy(aHost, aBuffer);
if(net_host_lookup(aHost, &NextAddr, m_aNetClient[CONN_MAIN].NetType()) != 0)
{
log_error("client", "could not find address of %s", aHost);
continue;
}
if(NumConnectAddrs == (int)std::size(aConnectAddrs))
{
log_warn("client", "too many connect addresses, ignoring %s", aHost);
continue;
}
if(NextAddr.port == 0)
{
NextAddr.port = 8303;
}
char aNextAddr[NETADDR_MAXSTRSIZE];
net_addr_str(&NextAddr, aNextAddr, sizeof(aNextAddr), true);
log_debug("client", "resolved connect address '%s' to %s", aBuffer, aNextAddr);
aConnectAddrs[NumConnectAddrs] = NextAddr;
NumConnectAddrs += 1;
}
2010-05-29 07:25:38 +00:00
if(NumConnectAddrs == 0)
{
log_error("client", "could not find any connect address, defaulting to localhost for whatever reason...");
net_host_lookup("localhost", &aConnectAddrs[0], m_aNetClient[CONN_MAIN].NetType());
NumConnectAddrs = 1;
}
2010-05-29 07:25:38 +00:00
if(m_SendPassword)
{
2022-07-09 16:14:56 +00:00
str_copy(m_aPassword, g_Config.m_Password);
m_SendPassword = false;
}
else if(!pPassword)
m_aPassword[0] = 0;
else
2022-07-09 16:14:56 +00:00
str_copy(m_aPassword, pPassword);
m_CanReceiveServerCapabilities = true;
m_aNetClient[CONN_MAIN].Connect(aConnectAddrs, NumConnectAddrs);
m_aNetClient[CONN_MAIN].RefreshStun();
2010-05-29 07:25:38 +00:00
SetState(IClient::STATE_CONNECTING);
m_InputtimeMarginGraph.Init(-150.0f, 150.0f);
m_GametimeMarginGraph.Init(-150.0f, 150.0f);
GenerateTimeoutCodes(aConnectAddrs, NumConnectAddrs);
2010-05-29 07:25:38 +00:00
}
void CClient::DisconnectWithReason(const char *pReason)
{
if(pReason != nullptr && pReason[0] == '\0')
pReason = nullptr;
DummyDisconnect(pReason);
char aBuf[512];
str_format(aBuf, sizeof(aBuf), "disconnecting. reason='%s'", pReason ? pReason : "unknown");
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkPrintColor);
2010-05-29 07:25:38 +00:00
// stop demo playback and recorder
// make sure to remove replay tmp demo
2010-05-29 07:25:38 +00:00
m_DemoPlayer.Stop();
for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++)
{
DemoRecorder(Recorder)->Stop(Recorder == RECORDER_REPLAYS ? IDemoRecorder::EStopMode::REMOVE_FILE : IDemoRecorder::EStopMode::KEEP_FILE);
}
2010-05-29 07:25:38 +00:00
m_aRconAuthed[0] = 0;
mem_zero(m_aRconUsername, sizeof(m_aRconUsername));
mem_zero(m_aRconPassword, sizeof(m_aRconPassword));
m_ServerSentCapabilities = false;
m_UseTempRconCommands = 0;
m_ExpectedRconCommands = -1;
m_GotRconCommands = 0;
m_pConsole->DeregisterTempAll();
m_aNetClient[CONN_MAIN].Disconnect(pReason);
2010-05-29 07:25:38 +00:00
SetState(IClient::STATE_OFFLINE);
m_pMap->Unload();
m_CurrentServerPingInfoType = -1;
m_CurrentServerPingBasicToken = -1;
m_CurrentServerPingToken = -1;
mem_zero(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid));
m_CurrentServerCurrentPingTime = -1;
m_CurrentServerNextPingTime = -1;
2010-05-29 07:25:38 +00:00
// disable all downloads
m_MapdownloadChunk = 0;
if(m_pMapdownloadTask)
m_pMapdownloadTask->Abort();
if(m_MapdownloadFileTemp)
{
io_close(m_MapdownloadFileTemp);
Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE);
}
m_MapdownloadFileTemp = 0;
m_MapdownloadSha256Present = false;
m_MapdownloadSha256 = SHA256_ZEROED;
2010-05-29 07:25:38 +00:00
m_MapdownloadCrc = 0;
m_MapdownloadTotalsize = -1;
m_MapdownloadAmount = 0;
m_MapDetailsPresent = false;
2010-05-29 07:25:38 +00:00
// clear the current server info
mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
// clear snapshots
m_aapSnapshots[0][SNAP_CURRENT] = 0;
m_aapSnapshots[0][SNAP_PREV] = 0;
m_aReceivedSnapshots[0] = 0;
m_LastDummy = false;
2010-05-29 07:25:38 +00:00
}
void CClient::Disconnect()
{
if(m_State != IClient::STATE_OFFLINE)
2019-05-20 21:55:40 +00:00
{
DisconnectWithReason(nullptr);
2019-05-20 21:55:40 +00:00
}
2010-05-29 07:25:38 +00:00
}
bool CClient::DummyConnected() const
{
return m_DummyConnected;
}
bool CClient::DummyConnecting() const
{
return !m_DummyConnected && m_LastDummyConnectTime > 0 && m_LastDummyConnectTime + GameTickSpeed() * 5 > GameTick(g_Config.m_ClDummy);
}
2014-04-28 13:19:57 +00:00
void CClient::DummyConnect()
{
if(m_LastDummyConnectTime > 0 && m_LastDummyConnectTime + GameTickSpeed() * 5 > GameTick(g_Config.m_ClDummy))
return;
if(m_aNetClient[CONN_MAIN].State() != NETSTATE_ONLINE)
return;
if(m_DummyConnected || !DummyAllowed())
return;
m_LastDummyConnectTime = GameTick(g_Config.m_ClDummy);
m_aRconAuthed[1] = 0;
m_DummySendConnInfo = true;
g_Config.m_ClDummyCopyMoves = 0;
g_Config.m_ClDummyHammer = 0;
// connect to the server
m_aNetClient[CONN_DUMMY].Connect(m_aNetClient[CONN_MAIN].ServerAddress(), 1);
}
void CClient::DummyDisconnect(const char *pReason)
{
if(!m_DummyConnected)
return;
m_aNetClient[CONN_DUMMY].Disconnect(pReason);
g_Config.m_ClDummy = 0;
if(!m_aRconAuthed[0] && m_aRconAuthed[1])
{
RconAuth(m_aRconUsername, m_aRconPassword);
}
m_aRconAuthed[1] = 0;
m_aapSnapshots[1][SNAP_CURRENT] = 0;
m_aapSnapshots[1][SNAP_PREV] = 0;
m_aReceivedSnapshots[1] = 0;
m_DummyConnected = false;
2014-04-28 14:47:44 +00:00
GameClient()->OnDummyDisconnect();
}
bool CClient::DummyAllowed() const
{
return m_ServerCapabilities.m_AllowDummy;
}
int CClient::GetCurrentRaceTime()
{
if(GameClient()->GetLastRaceTick() < 0)
return 0;
return (GameTick(g_Config.m_ClDummy) - GameClient()->GetLastRaceTick()) / GameTickSpeed();
}
void CClient::GetServerInfo(CServerInfo *pServerInfo) const
2010-05-29 07:25:38 +00:00
{
mem_copy(pServerInfo, &m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
}
void CClient::ServerInfoRequest()
{
mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
m_CurrentServerInfoRequestTime = 0;
}
void CClient::LoadDebugFont()
2010-05-29 07:25:38 +00:00
{
m_DebugFont = Graphics()->LoadTexture("debug_font.png", IStorage::TYPE_ALL);
2010-05-29 07:25:38 +00:00
}
// ---
void *CClient::SnapGetItem(int SnapId, int Index, CSnapItem *pItem) const
2010-05-29 07:25:38 +00:00
{
dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
const CSnapshot *pSnapshot = m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap;
const CSnapshotItem *pSnapshotItem = pSnapshot->GetItem(Index);
pItem->m_DataSize = pSnapshot->GetItemSize(Index);
pItem->m_Type = pSnapshot->GetItemType(Index);
pItem->m_Id = pSnapshotItem->Id();
return (void *)pSnapshotItem->Data();
2010-05-29 07:25:38 +00:00
}
int CClient::SnapItemSize(int SnapId, int Index) const
{
dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
return m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap->GetItemSize(Index);
}
const void *CClient::SnapFindItem(int SnapId, int Type, int Id) const
2010-05-29 07:25:38 +00:00
{
if(!m_aapSnapshots[g_Config.m_ClDummy][SnapId])
2022-10-09 15:57:43 +00:00
return nullptr;
2010-05-29 07:25:38 +00:00
return m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap->FindItem(Type, Id);
2010-05-29 07:25:38 +00:00
}
int CClient::SnapNumItems(int SnapId) const
2010-05-29 07:25:38 +00:00
{
dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId");
if(!m_aapSnapshots[g_Config.m_ClDummy][SnapId])
2010-05-29 07:25:38 +00:00
return 0;
return m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap->NumItems();
2010-05-29 07:25:38 +00:00
}
void CClient::SnapSetStaticsize(int ItemType, int Size)
{
m_SnapshotDelta.SetStaticsize(ItemType, Size);
}
void CClient::DebugRender()
{
if(!g_Config.m_Debug)
return;
static NETSTATS s_Prev, s_Current;
static int64_t s_LastSnapTime = 0;
static float s_FrameTimeAvg = 0;
char aBuffer[512];
2010-05-29 07:25:38 +00:00
Graphics()->TextureSet(m_DebugFont);
Graphics()->MapScreen(0, 0, Graphics()->ScreenWidth(), Graphics()->ScreenHeight());
Graphics()->QuadsBegin();
2010-05-29 07:25:38 +00:00
if(time_get() - s_LastSnapTime > time_freq())
2010-05-29 07:25:38 +00:00
{
s_LastSnapTime = time_get();
s_Prev = s_Current;
net_stats(&s_Current);
2010-05-29 07:25:38 +00:00
}
/*
eth = 14
ip = 20
udp = 8
total = 42
*/
s_FrameTimeAvg = s_FrameTimeAvg * 0.9f + m_RenderFrameTime * 0.1f;
2022-03-20 17:03:25 +00:00
str_format(aBuffer, sizeof(aBuffer), "ticks: %8d %8d gfx mem(tex/buff/stream/staging): (%" PRIu64 "k/%" PRIu64 "k/%" PRIu64 "k/%" PRIu64 "k) fps: %3d",
m_aCurGameTick[g_Config.m_ClDummy], m_aPredTick[g_Config.m_ClDummy],
2022-03-20 17:03:25 +00:00
(Graphics()->TextureMemoryUsage() / 1024),
(Graphics()->BufferMemoryUsage() / 1024),
(Graphics()->StreamedMemoryUsage() / 1024),
(Graphics()->StagingMemoryUsage() / 1024),
(int)(1.0f / s_FrameTimeAvg + 0.5f));
Graphics()->QuadsText(2, 2, 16, aBuffer);
2010-05-29 07:25:38 +00:00
{
uint64_t SendPackets = (s_Current.sent_packets - s_Prev.sent_packets);
uint64_t SendBytes = (s_Current.sent_bytes - s_Prev.sent_bytes);
uint64_t SendTotal = SendBytes + SendPackets * 42;
uint64_t RecvPackets = (s_Current.recv_packets - s_Prev.recv_packets);
uint64_t RecvBytes = (s_Current.recv_bytes - s_Prev.recv_bytes);
uint64_t RecvTotal = RecvBytes + RecvPackets * 42;
2010-05-29 07:25:38 +00:00
if(!SendPackets)
SendPackets++;
if(!RecvPackets)
RecvPackets++;
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,
SendPackets, SendBytes, SendPackets * 42, SendTotal, (SendTotal * 8) / 1024, SendBytes / SendPackets,
RecvPackets, RecvBytes, RecvPackets * 42, RecvTotal, (RecvTotal * 8) / 1024, RecvBytes / RecvPackets);
Graphics()->QuadsText(2, 14, 16, aBuffer);
2010-05-29 07:25:38 +00:00
}
// render rates
{
int y = 0;
str_format(aBuffer, sizeof(aBuffer), "%5s %20s: %8s %8s %8s", "ID", "Name", "Rate", "Updates", "R/U");
Graphics()->QuadsText(2, 100 + y * 12, 16, aBuffer);
y++;
for(int i = 0; i < NUM_NETOBJTYPES; i++)
2010-05-29 07:25:38 +00:00
{
if(m_SnapshotDelta.GetDataRate(i))
{
str_format(aBuffer, sizeof(aBuffer), "%5d %20s: %8d %8d %8d", i, GameClient()->GetItemName(i), m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i),
(m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8);
Graphics()->QuadsText(2, 100 + y * 12, 16, aBuffer);
2010-05-29 07:25:38 +00:00
y++;
}
}
for(int i = CSnapshot::MAX_TYPE; i > (CSnapshot::MAX_TYPE - 64); i--)
{
if(m_SnapshotDelta.GetDataRate(i) && m_aapSnapshots[g_Config.m_ClDummy][IClient::SNAP_CURRENT])
{
int Type = m_aapSnapshots[g_Config.m_ClDummy][IClient::SNAP_CURRENT]->m_pAltSnap->GetExternalItemType(i);
if(Type == UUID_INVALID)
{
str_format(aBuffer, sizeof(aBuffer), "%5d %20s: %8d %8d %8d", i, "Unknown UUID", m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i),
(m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8);
Graphics()->QuadsText(2, 100 + y * 12, 16, aBuffer);
y++;
}
else if(Type != i)
{
str_format(aBuffer, sizeof(aBuffer), "%5d %20s: %8d %8d %8d", Type, GameClient()->GetItemName(Type), m_SnapshotDelta.GetDataRate(i) / 8, m_SnapshotDelta.GetDataUpdates(i),
(m_SnapshotDelta.GetDataRate(i) / m_SnapshotDelta.GetDataUpdates(i)) / 8);
Graphics()->QuadsText(2, 100 + y * 12, 16, aBuffer);
y++;
}
}
}
2010-05-29 07:25:38 +00:00
}
2016-05-05 16:07:00 +00:00
str_format(aBuffer, sizeof(aBuffer), "pred: %d ms", GetPredictionTime());
Graphics()->QuadsText(2, 70, 16, aBuffer);
Graphics()->QuadsEnd();
2010-05-29 07:25:38 +00:00
// render graphs
if(g_Config.m_DbgGraphs)
{
float w = Graphics()->ScreenWidth() / 4.0f;
float h = Graphics()->ScreenHeight() / 6.0f;
float sp = Graphics()->ScreenWidth() / 100.0f;
float x = Graphics()->ScreenWidth() - w - sp;
2010-05-29 07:25:38 +00:00
Add time scale to debug graphs for constant scrolling speed Store X value (time) for all graph entries in addition to the Y value (FPS, prediction margin etc.). The `CGraph::Add` function adds values to the graph at the current time. The `CGraph::InsertAt` function allows specifying arbitrary X values, as long as the values are inserted in increasing order. The entries are kept in a ringbuffer and old entries are recycled when it's full. The size of the ringbuffer is configurable for each graph, as the FPS graph needs significantly more buffer because values are added more often. The scrolling speed of the graphs is fixed by specifying the maximum size of the window of values which should be displayed. For this purpose, a parameter is added to the `CGraph::Scale` function to specify the size of the window which should be rendered in the `CGraph::Render` function. For the FPS graph only the last second is rendered, so small spikes are still noticeable. For prediction and gametime margin graphs the last five seconds are rendered, which should result in a similar scrolling speed as before this change. The debug tuning graph is a special case, where the X values set manually and fixed to 0-127, same as before, instead of being based on the current time. The graph rendering is made much more efficient by precalculating when the vertex colors need to be updated, to avoid all unnecessary calls to `SetColorVertex`. Additionally, line items are bundled together in an array to avoid calling `LinesDraw` for every individual line item.
2024-01-02 22:46:52 +00:00
m_FpsGraph.Scale(time_freq());
m_FpsGraph.Render(Graphics(), TextRender(), x, sp * 5, w, h, "FPS");
Add time scale to debug graphs for constant scrolling speed Store X value (time) for all graph entries in addition to the Y value (FPS, prediction margin etc.). The `CGraph::Add` function adds values to the graph at the current time. The `CGraph::InsertAt` function allows specifying arbitrary X values, as long as the values are inserted in increasing order. The entries are kept in a ringbuffer and old entries are recycled when it's full. The size of the ringbuffer is configurable for each graph, as the FPS graph needs significantly more buffer because values are added more often. The scrolling speed of the graphs is fixed by specifying the maximum size of the window of values which should be displayed. For this purpose, a parameter is added to the `CGraph::Scale` function to specify the size of the window which should be rendered in the `CGraph::Render` function. For the FPS graph only the last second is rendered, so small spikes are still noticeable. For prediction and gametime margin graphs the last five seconds are rendered, which should result in a similar scrolling speed as before this change. The debug tuning graph is a special case, where the X values set manually and fixed to 0-127, same as before, instead of being based on the current time. The graph rendering is made much more efficient by precalculating when the vertex colors need to be updated, to avoid all unnecessary calls to `SetColorVertex`. Additionally, line items are bundled together in an array to avoid calling `LinesDraw` for every individual line item.
2024-01-02 22:46:52 +00:00
m_InputtimeMarginGraph.Scale(5 * time_freq());
m_InputtimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 6 + h, w, h, "Prediction Margin");
Add time scale to debug graphs for constant scrolling speed Store X value (time) for all graph entries in addition to the Y value (FPS, prediction margin etc.). The `CGraph::Add` function adds values to the graph at the current time. The `CGraph::InsertAt` function allows specifying arbitrary X values, as long as the values are inserted in increasing order. The entries are kept in a ringbuffer and old entries are recycled when it's full. The size of the ringbuffer is configurable for each graph, as the FPS graph needs significantly more buffer because values are added more often. The scrolling speed of the graphs is fixed by specifying the maximum size of the window of values which should be displayed. For this purpose, a parameter is added to the `CGraph::Scale` function to specify the size of the window which should be rendered in the `CGraph::Render` function. For the FPS graph only the last second is rendered, so small spikes are still noticeable. For prediction and gametime margin graphs the last five seconds are rendered, which should result in a similar scrolling speed as before this change. The debug tuning graph is a special case, where the X values set manually and fixed to 0-127, same as before, instead of being based on the current time. The graph rendering is made much more efficient by precalculating when the vertex colors need to be updated, to avoid all unnecessary calls to `SetColorVertex`. Additionally, line items are bundled together in an array to avoid calling `LinesDraw` for every individual line item.
2024-01-02 22:46:52 +00:00
m_GametimeMarginGraph.Scale(5 * time_freq());
m_GametimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 7 + h * 2, w, h, "Gametime Margin");
2010-05-29 07:25:38 +00:00
}
}
2014-12-31 14:35:49 +00:00
void CClient::Restart()
{
2020-01-25 16:48:32 +00:00
SetState(IClient::STATE_RESTARTING);
2014-12-31 14:35:49 +00:00
}
2010-05-29 07:25:38 +00:00
void CClient::Quit()
{
2020-10-02 13:44:27 +00:00
SetState(IClient::STATE_QUITTING);
2010-05-29 07:25:38 +00:00
}
const char *CClient::PlayerName() const
{
if(g_Config.m_PlayerName[0])
{
return g_Config.m_PlayerName;
}
if(g_Config.m_SteamName[0])
{
return g_Config.m_SteamName;
}
return "nameless tee";
}
const char *CClient::DummyName() const
{
if(g_Config.m_ClDummyName[0])
{
return g_Config.m_ClDummyName;
}
const char *pBase = 0;
if(g_Config.m_PlayerName[0])
{
pBase = g_Config.m_PlayerName;
}
else if(g_Config.m_SteamName[0])
{
pBase = g_Config.m_SteamName;
}
if(pBase)
{
static char aDummyNameBuf[16];
str_format(aDummyNameBuf, sizeof(aDummyNameBuf), "[D] %s", pBase);
return aDummyNameBuf;
}
return "brainless tee";
}
const char *CClient::ErrorString() const
2010-05-29 07:25:38 +00:00
{
return m_aNetClient[CONN_MAIN].ErrorString();
2010-05-29 07:25:38 +00:00
}
void CClient::Render()
{
if(g_Config.m_ClOverlayEntities)
{
ColorRGBA bg = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClBackgroundEntitiesColor));
Graphics()->Clear(bg.r, bg.g, bg.b);
}
else
{
ColorRGBA bg = color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClBackgroundColor));
Graphics()->Clear(bg.r, bg.g, bg.b);
}
2010-05-29 07:25:38 +00:00
GameClient()->OnRender();
DebugRender();
2015-06-21 16:00:09 +00:00
if(State() == IClient::STATE_ONLINE && g_Config.m_ClAntiPingLimit)
{
2021-06-23 05:05:49 +00:00
int64_t Now = time_get();
g_Config.m_ClAntiPing = (m_PredictedTime.Get(Now) - m_aGameTime[g_Config.m_ClDummy].Get(Now)) * 1000 / (float)time_freq() > g_Config.m_ClAntiPingLimit;
2015-06-21 16:00:09 +00:00
}
2010-05-29 07:25:38 +00:00
}
const char *CClient::LoadMap(const char *pName, const char *pFilename, SHA256_DIGEST *pWantedSha256, unsigned WantedCrc)
2010-05-29 07:25:38 +00:00
{
2017-03-21 10:24:44 +00:00
static char s_aErrorMsg[128];
2010-05-29 07:25:38 +00:00
SetState(IClient::STATE_LOADING);
SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_MAP);
if((bool)m_LoadingCallback)
m_LoadingCallback(IClient::LOADING_CALLBACK_DETAIL_MAP);
2010-05-29 07:25:38 +00:00
if(!m_pMap->Load(pFilename))
{
2017-03-21 10:24:44 +00:00
str_format(s_aErrorMsg, sizeof(s_aErrorMsg), "map '%s' not found", pFilename);
return s_aErrorMsg;
2010-05-29 07:25:38 +00:00
}
if(pWantedSha256 && m_pMap->Sha256() != *pWantedSha256)
{
char aWanted[SHA256_MAXSTRSIZE];
char aGot[SHA256_MAXSTRSIZE];
sha256_str(*pWantedSha256, aWanted, sizeof(aWanted));
sha256_str(m_pMap->Sha256(), aGot, sizeof(aWanted));
str_format(s_aErrorMsg, sizeof(s_aErrorMsg), "map differs from the server. %s != %s", aGot, aWanted);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", s_aErrorMsg);
m_pMap->Unload();
return s_aErrorMsg;
}
// Only check CRC if we don't have the secure SHA256.
if(!pWantedSha256 && m_pMap->Crc() != WantedCrc)
2010-05-29 07:25:38 +00:00
{
2017-03-21 10:24:44 +00:00
str_format(s_aErrorMsg, sizeof(s_aErrorMsg), "map differs from the server. %08x != %08x", m_pMap->Crc(), WantedCrc);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", s_aErrorMsg);
m_pMap->Unload();
2017-03-21 10:24:44 +00:00
return s_aErrorMsg;
2010-05-29 07:25:38 +00:00
}
// stop demo recording if we loaded a new map
for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++)
{
DemoRecorder(Recorder)->Stop(Recorder == RECORDER_REPLAYS ? IDemoRecorder::EStopMode::REMOVE_FILE : IDemoRecorder::EStopMode::KEEP_FILE);
}
2010-05-29 07:25:38 +00:00
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "loaded map '%s'", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
2010-05-29 07:25:38 +00:00
2022-07-09 16:14:56 +00:00
str_copy(m_aCurrentMap, pName);
str_copy(m_aCurrentMapPath, pFilename);
2010-05-29 07:25:38 +00:00
return 0;
2010-05-29 07:25:38 +00:00
}
static void FormatMapDownloadFilename(const char *pName, const SHA256_DIGEST *pSha256, int Crc, bool Temp, char *pBuffer, int BufferSize)
2010-05-29 07:25:38 +00:00
{
char aSuffix[32];
if(Temp)
{
IStorage::FormatTmpPath(aSuffix, sizeof(aSuffix), "");
}
else
{
2022-07-09 16:14:56 +00:00
str_copy(aSuffix, ".map");
}
if(pSha256)
{
char aSha256[SHA256_MAXSTRSIZE];
sha256_str(*pSha256, aSha256, sizeof(aSha256));
str_format(pBuffer, BufferSize, "downloadedmaps/%s_%s%s", pName, aSha256, aSuffix);
}
else
{
str_format(pBuffer, BufferSize, "downloadedmaps/%s_%08x%s", pName, Crc, aSuffix);
}
}
const char *CClient::LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedSha256, int WantedCrc)
{
char aBuf[512];
char aWanted[SHA256_MAXSTRSIZE + 16];
aWanted[0] = 0;
if(pWantedSha256)
{
char aWantedSha256[SHA256_MAXSTRSIZE];
sha256_str(*pWantedSha256, aWantedSha256, sizeof(aWantedSha256));
str_format(aWanted, sizeof(aWanted), "sha256=%s ", aWantedSha256);
}
str_format(aBuf, sizeof(aBuf), "loading map, map=%s wanted %scrc=%08x", pMapName, aWanted, WantedCrc);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
2010-05-29 07:25:38 +00:00
// try the normal maps folder
str_format(aBuf, sizeof(aBuf), "maps/%s.map", pMapName);
const char *pError = LoadMap(pMapName, aBuf, pWantedSha256, WantedCrc);
2010-05-29 07:25:38 +00:00
if(!pError)
return nullptr;
2010-05-29 07:25:38 +00:00
// try the downloaded maps
FormatMapDownloadFilename(pMapName, pWantedSha256, WantedCrc, false, aBuf, sizeof(aBuf));
pError = LoadMap(pMapName, aBuf, pWantedSha256, WantedCrc);
if(!pError)
return nullptr;
// backward compatibility with old names
if(pWantedSha256)
{
FormatMapDownloadFilename(pMapName, 0, WantedCrc, false, aBuf, sizeof(aBuf));
pError = LoadMap(pMapName, aBuf, pWantedSha256, WantedCrc);
if(!pError)
return nullptr;
}
// search for the map within subfolders
char aFilename[IO_MAX_PATH_LENGTH];
str_format(aFilename, sizeof(aFilename), "%s.map", pMapName);
if(Storage()->FindFile(aFilename, "maps", IStorage::TYPE_ALL, aBuf, sizeof(aBuf)))
{
pError = LoadMap(pMapName, aBuf, pWantedSha256, WantedCrc);
if(!pError)
return nullptr;
}
static char s_aErrorMsg[256];
str_format(s_aErrorMsg, sizeof(s_aErrorMsg), "Could not find map '%s'", pMapName);
return s_aErrorMsg;
2010-05-29 07:25:38 +00:00
}
void CClient::ProcessConnlessPacket(CNetChunk *pPacket)
2010-05-29 07:25:38 +00:00
{
// server info
if(pPacket->m_DataSize >= (int)sizeof(SERVERBROWSE_INFO))
2010-05-29 07:25:38 +00:00
{
int Type = -1;
if(mem_comp(pPacket->m_pData, SERVERBROWSE_INFO, sizeof(SERVERBROWSE_INFO)) == 0)
Type = SERVERINFO_VANILLA;
else if(mem_comp(pPacket->m_pData, SERVERBROWSE_INFO_EXTENDED, sizeof(SERVERBROWSE_INFO_EXTENDED)) == 0)
Type = SERVERINFO_EXTENDED;
else if(mem_comp(pPacket->m_pData, SERVERBROWSE_INFO_EXTENDED_MORE, sizeof(SERVERBROWSE_INFO_EXTENDED_MORE)) == 0)
Type = SERVERINFO_EXTENDED_MORE;
if(Type != -1)
{
void *pData = (unsigned char *)pPacket->m_pData + sizeof(SERVERBROWSE_INFO);
int DataSize = pPacket->m_DataSize - sizeof(SERVERBROWSE_INFO);
ProcessServerInfo(Type, &pPacket->m_Address, pData, DataSize);
}
}
}
static int SavedServerInfoType(int Type)
{
if(Type == SERVERINFO_EXTENDED_MORE)
return SERVERINFO_EXTENDED;
return Type;
}
void CClient::ProcessServerInfo(int RawType, NETADDR *pFrom, const void *pData, int DataSize)
{
CServerBrowser::CServerEntry *pEntry = m_ServerBrowser.Find(*pFrom);
CServerInfo Info = {0};
int SavedType = SavedServerInfoType(RawType);
if(SavedType == SERVERINFO_EXTENDED && pEntry && pEntry->m_GotInfo && SavedType == pEntry->m_Info.m_Type)
{
Info = pEntry->m_Info;
}
Info.m_Type = SavedType;
net_addr_str(pFrom, Info.m_aAddress, sizeof(Info.m_aAddress), true);
CUnpacker Up;
Up.Reset(pData, DataSize);
#define GET_STRING(array) str_copy(array, Up.GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES), sizeof(array))
#define GET_INT(integer) (integer) = str_toint(Up.GetString())
2010-05-29 07:25:38 +00:00
int Token;
int PacketNo = 0; // Only used if SavedType == SERVERINFO_EXTENDED
2010-05-29 07:25:38 +00:00
GET_INT(Token);
if(RawType != SERVERINFO_EXTENDED_MORE)
{
GET_STRING(Info.m_aVersion);
GET_STRING(Info.m_aName);
GET_STRING(Info.m_aMap);
if(SavedType == SERVERINFO_EXTENDED)
{
GET_INT(Info.m_MapCrc);
GET_INT(Info.m_MapSize);
}
2010-05-29 07:25:38 +00:00
GET_STRING(Info.m_aGameType);
GET_INT(Info.m_Flags);
GET_INT(Info.m_NumPlayers);
GET_INT(Info.m_MaxPlayers);
GET_INT(Info.m_NumClients);
GET_INT(Info.m_MaxClients);
2010-05-29 07:25:38 +00:00
// don't add invalid info to the server browser list
if(Info.m_NumClients < 0 || Info.m_MaxClients < 0 ||
Info.m_NumPlayers < 0 || Info.m_MaxPlayers < 0 ||
Info.m_NumPlayers > Info.m_NumClients || Info.m_MaxPlayers > Info.m_MaxClients)
{
return;
}
m_ServerBrowser.UpdateServerCommunity(&Info);
m_ServerBrowser.UpdateServerRank(&Info);
switch(SavedType)
{
case SERVERINFO_VANILLA:
if(Info.m_MaxPlayers > VANILLA_MAX_CLIENTS ||
Info.m_MaxClients > VANILLA_MAX_CLIENTS)
{
return;
}
break;
case SERVERINFO_64_LEGACY:
if(Info.m_MaxPlayers > MAX_CLIENTS ||
Info.m_MaxClients > MAX_CLIENTS)
2014-01-14 20:40:55 +00:00
{
return;
2014-01-14 20:40:55 +00:00
}
break;
case SERVERINFO_EXTENDED:
if(Info.m_NumPlayers > Info.m_NumClients)
return;
break;
default:
dbg_assert(false, "unknown serverinfo type");
}
2014-01-08 05:15:56 +00:00
if(SavedType == SERVERINFO_EXTENDED)
PacketNo = 0;
}
else
{
GET_INT(PacketNo);
// 0 needs to be excluded because that's reserved for the main packet.
if(PacketNo <= 0 || PacketNo >= 64)
2014-01-08 05:15:56 +00:00
return;
}
2014-01-08 05:15:56 +00:00
bool DuplicatedPacket = false;
if(SavedType == SERVERINFO_EXTENDED)
{
Up.GetString(); // extra info, reserved
2014-01-08 05:15:56 +00:00
2021-06-23 05:05:49 +00:00
uint64_t Flag = (uint64_t)1 << PacketNo;
DuplicatedPacket = Info.m_ReceivedPackets & Flag;
Info.m_ReceivedPackets |= Flag;
}
2014-01-08 05:15:56 +00:00
bool IgnoreError = false;
for(int i = 0; i < MAX_CLIENTS && Info.m_NumReceivedClients < MAX_CLIENTS && !Up.Error(); i++)
{
CServerInfo::CClient *pClient = &Info.m_aClients[Info.m_NumReceivedClients];
GET_STRING(pClient->m_aName);
if(Up.Error())
2014-01-08 05:15:56 +00:00
{
// Packet end, no problem unless it happens during one
// player info, so ignore the error.
IgnoreError = true;
break;
}
GET_STRING(pClient->m_aClan);
GET_INT(pClient->m_Country);
GET_INT(pClient->m_Score);
GET_INT(pClient->m_Player);
if(SavedType == SERVERINFO_EXTENDED)
{
Up.GetString(); // extra info, reserved
2014-01-08 05:15:56 +00:00
}
if(!Up.Error())
{
if(SavedType == SERVERINFO_64_LEGACY)
{
2021-06-23 05:05:49 +00:00
uint64_t Flag = (uint64_t)1 << i;
if(!(Info.m_ReceivedPackets & Flag))
{
Info.m_ReceivedPackets |= Flag;
Info.m_NumReceivedClients++;
}
}
else
{
Info.m_NumReceivedClients++;
}
}
}
2014-01-08 05:15:56 +00:00
str_clean_whitespaces(Info.m_aName);
if(!Up.Error() || IgnoreError)
{
if(!DuplicatedPacket && (!pEntry || !pEntry->m_GotInfo || SavedType >= pEntry->m_Info.m_Type))
{
m_ServerBrowser.OnServerInfoUpdate(*pFrom, Token, &Info);
}
// Player info is irrelevant for the client (while connected),
// it gets its info from elsewhere.
//
// SERVERINFO_EXTENDED_MORE doesn't carry any server
// information, so just skip it.
if(net_addr_comp(&ServerAddress(), pFrom) == 0 && RawType != SERVERINFO_EXTENDED_MORE)
{
// Only accept server info that has a type that is
// newer or equal to something the server already sent
// us.
if(SavedType >= m_CurrentServerInfo.m_Type)
2014-01-08 05:15:56 +00:00
{
mem_copy(&m_CurrentServerInfo, &Info, sizeof(m_CurrentServerInfo));
m_CurrentServerInfo.m_NumAddresses = 1;
m_CurrentServerInfo.m_aAddresses[0] = ServerAddress();
2014-01-08 05:15:56 +00:00
m_CurrentServerInfoRequestTime = -1;
}
bool ValidPong = false;
if(!m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && SavedType >= m_CurrentServerPingInfoType)
{
if(RawType == SERVERINFO_VANILLA)
{
ValidPong = Token == m_CurrentServerPingBasicToken;
}
else if(RawType == SERVERINFO_EXTENDED)
{
ValidPong = Token == m_CurrentServerPingToken;
}
}
if(ValidPong)
{
int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
m_ServerBrowser.SetCurrentServerPing(ServerAddress(), LatencyMs);
m_CurrentServerPingInfoType = SavedType;
m_CurrentServerCurrentPingTime = -1;
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "got pong from current server, latency=%dms", LatencyMs);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
}
2014-01-08 05:15:56 +00:00
}
}
#undef GET_STRING
#undef GET_INT
}
2010-05-29 07:25:38 +00:00
static CServerCapabilities GetServerCapabilities(int Version, int Flags)
{
CServerCapabilities Result;
bool DDNet = false;
if(Version >= 1)
{
DDNet = Flags & SERVERCAPFLAG_DDNET;
}
Result.m_ChatTimeoutCode = DDNet;
Result.m_AnyPlayerFlag = DDNet;
Result.m_PingEx = false;
Result.m_AllowDummy = true;
Result.m_SyncWeaponInput = false;
if(Version >= 1)
{
Result.m_ChatTimeoutCode = Flags & SERVERCAPFLAG_CHATTIMEOUTCODE;
}
if(Version >= 2)
{
Result.m_AnyPlayerFlag = Flags & SERVERCAPFLAG_ANYPLAYERFLAG;
}
if(Version >= 3)
{
Result.m_PingEx = Flags & SERVERCAPFLAG_PINGEX;
}
if(Version >= 4)
{
Result.m_AllowDummy = Flags & SERVERCAPFLAG_ALLOWDUMMY;
}
if(Version >= 5)
{
Result.m_SyncWeaponInput = Flags & SERVERCAPFLAG_SYNCWEAPONINPUT;
}
return Result;
}
void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy)
{
CUnpacker Unpacker;
Unpacker.Reset(pPacket->m_pData, pPacket->m_DataSize);
CMsgPacker Packer(NETMSG_EX, true);
2010-05-29 07:25:38 +00:00
// unpack msgid and system flag
int Msg;
bool Sys;
CUuid Uuid;
2010-05-29 07:25:38 +00:00
int Result = UnpackMessageId(&Msg, &Sys, &Uuid, &Unpacker, &Packer);
if(Result == UNPACKMESSAGE_ERROR)
{
return;
}
else if(Result == UNPACKMESSAGE_ANSWER)
{
SendMsg(Conn, &Packer, MSGFLAG_VITAL);
}
2010-05-29 07:25:38 +00:00
if(Sys)
{
// system message
if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_DETAILS)
{
const char *pMap = Unpacker.GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
SHA256_DIGEST *pMapSha256 = (SHA256_DIGEST *)Unpacker.GetRaw(sizeof(*pMapSha256));
int MapCrc = Unpacker.GetInt();
int MapSize = Unpacker.GetInt();
if(Unpacker.Error())
{
return;
}
const char *pMapUrl = Unpacker.GetString(CUnpacker::SANITIZE_CC);
if(Unpacker.Error())
{
pMapUrl = "";
}
m_MapDetailsPresent = true;
(void)MapSize;
2022-07-09 16:14:56 +00:00
str_copy(m_aMapDetailsName, pMap);
m_MapDetailsSha256 = *pMapSha256;
m_MapDetailsCrc = MapCrc;
str_copy(m_aMapDetailsUrl, pMapUrl);
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CAPABILITIES)
{
2019-06-13 22:24:50 +00:00
if(!m_CanReceiveServerCapabilities)
{
return;
}
int Version = Unpacker.GetInt();
int Flags = Unpacker.GetInt();
if(Unpacker.Error() || Version <= 0)
{
return;
}
m_ServerCapabilities = GetServerCapabilities(Version, Flags);
2019-06-13 22:24:50 +00:00
m_CanReceiveServerCapabilities = false;
m_ServerSentCapabilities = true;
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_CHANGE)
{
2019-06-13 22:24:50 +00:00
if(m_CanReceiveServerCapabilities)
{
m_ServerCapabilities = GetServerCapabilities(0, 0);
2019-06-13 22:24:50 +00:00
m_CanReceiveServerCapabilities = false;
}
bool MapDetailsWerePresent = m_MapDetailsPresent;
m_MapDetailsPresent = false;
const char *pMap = Unpacker.GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES);
int MapCrc = Unpacker.GetInt();
int MapSize = Unpacker.GetInt();
if(Unpacker.Error())
{
return;
}
if(MapSize < 0 || MapSize > 1024 * 1024 * 1024) // 1 GiB
{
DisconnectWithReason("invalid map size");
return;
}
for(int i = 0; pMap[i]; i++) // protect the player from nasty map names
2010-05-29 07:25:38 +00:00
{
if(pMap[i] == '/' || pMap[i] == '\\')
{
DisconnectWithReason("strange character in map name");
return;
}
}
2010-05-29 07:25:38 +00:00
if(m_DummyConnected)
{
DummyDisconnect(0);
}
2010-05-29 07:25:38 +00:00
SHA256_DIGEST *pMapSha256 = nullptr;
const char *pMapUrl = nullptr;
if(MapDetailsWerePresent && str_comp(m_aMapDetailsName, pMap) == 0 && m_MapDetailsCrc == MapCrc)
{
pMapSha256 = &m_MapDetailsSha256;
pMapUrl = m_aMapDetailsUrl[0] ? m_aMapDetailsUrl : nullptr;
}
2010-05-29 07:25:38 +00:00
if(LoadMapSearch(pMap, pMapSha256, MapCrc) == nullptr)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done");
SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_SENDING_READY);
SendReady(CONN_MAIN);
}
else
{
if(m_MapdownloadFileTemp)
2010-05-29 07:25:38 +00:00
{
io_close(m_MapdownloadFileTemp);
Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE);
}
2018-06-30 07:47:45 +00:00
// start map download
FormatMapDownloadFilename(pMap, pMapSha256, MapCrc, false, m_aMapdownloadFilename, sizeof(m_aMapdownloadFilename));
FormatMapDownloadFilename(pMap, pMapSha256, MapCrc, true, m_aMapdownloadFilenameTemp, sizeof(m_aMapdownloadFilenameTemp));
2010-05-29 07:25:38 +00:00
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "starting to download map to '%s'", m_aMapdownloadFilenameTemp);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", aBuf);
2010-05-29 07:25:38 +00:00
m_MapdownloadChunk = 0;
str_copy(m_aMapdownloadName, pMap);
m_MapdownloadSha256Present = (bool)pMapSha256;
m_MapdownloadSha256 = pMapSha256 ? *pMapSha256 : SHA256_ZEROED;
m_MapdownloadCrc = MapCrc;
m_MapdownloadTotalsize = MapSize;
m_MapdownloadAmount = 0;
2010-05-29 07:25:38 +00:00
ResetMapDownload();
2015-01-28 12:17:39 +00:00
if(pMapSha256)
{
char aUrl[256];
char aEscaped[256];
EscapeUrl(aEscaped, sizeof(aEscaped), m_aMapdownloadFilename + 15); // cut off downloadedmaps/
bool UseConfigUrl = str_comp(g_Config.m_ClMapDownloadUrl, "https://maps.ddnet.org") != 0 || m_aMapDownloadUrl[0] == '\0';
str_format(aUrl, sizeof(aUrl), "%s/%s", UseConfigUrl ? g_Config.m_ClMapDownloadUrl : m_aMapDownloadUrl, aEscaped);
m_pMapdownloadTask = HttpGetFile(pMapUrl ? pMapUrl : aUrl, Storage(), m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE);
m_pMapdownloadTask->Timeout(CTimeout{g_Config.m_ClMapDownloadConnectTimeoutMs, 0, g_Config.m_ClMapDownloadLowSpeedLimit, g_Config.m_ClMapDownloadLowSpeedTime});
m_pMapdownloadTask->MaxResponseSize(MapSize);
m_pMapdownloadTask->ExpectSha256(*pMapSha256);
2023-12-18 19:01:26 +00:00
Http()->Run(m_pMapdownloadTask);
}
else
{
SendMapRequest();
2010-05-29 07:25:38 +00:00
}
}
}
else if(Conn == CONN_MAIN && Msg == NETMSG_MAP_DATA)
{
if(!m_MapdownloadFileTemp)
{
return;
}
int Last = Unpacker.GetInt();
int MapCRC = Unpacker.GetInt();
int Chunk = Unpacker.GetInt();
int Size = Unpacker.GetInt();
const unsigned char *pData = Unpacker.GetRaw(Size);
if(Unpacker.Error() || Size <= 0 || MapCRC != m_MapdownloadCrc || Chunk != m_MapdownloadChunk)
{
return;
}
io_write(m_MapdownloadFileTemp, pData, Size);
m_MapdownloadAmount += Size;
if(Last)
2015-01-19 22:51:03 +00:00
{
if(m_MapdownloadFileTemp)
{
io_close(m_MapdownloadFileTemp);
m_MapdownloadFileTemp = 0;
}
FinishMapDownload();
2015-01-19 22:51:03 +00:00
}
else
2010-05-29 07:25:38 +00:00
{
// request new chunk
m_MapdownloadChunk++;
CMsgPacker MsgP(NETMSG_REQUEST_MAP_DATA, true);
MsgP.AddInt(m_MapdownloadChunk);
SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH);
2010-05-29 07:25:38 +00:00
if(g_Config.m_Debug)
2010-05-29 07:25:38 +00:00
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "requested chunk %d", m_MapdownloadChunk);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client/network", aBuf);
2010-05-29 07:25:38 +00:00
}
}
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CON_READY)
{
GameClient()->OnConnected();
}
else if(Conn == CONN_DUMMY && Msg == NETMSG_CON_READY)
{
m_DummyConnected = true;
g_Config.m_ClDummy = 1;
Rcon("crashmeplx");
if(m_aRconAuthed[0])
RconAuth(m_aRconUsername, m_aRconPassword);
}
else if(Msg == NETMSG_PING)
{
CMsgPacker MsgP(NETMSG_PING_REPLY, true);
int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0;
SendMsg(Conn, &MsgP, MSGFLAG_FLUSH | Vital);
}
else if(Msg == NETMSG_PINGEX)
{
CUuid *pId = (CUuid *)Unpacker.GetRaw(sizeof(*pId));
if(Unpacker.Error())
{
return;
}
CMsgPacker MsgP(NETMSG_PONGEX, true);
MsgP.AddRaw(pId, sizeof(*pId));
int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0;
SendMsg(Conn, &MsgP, MSGFLAG_FLUSH | Vital);
}
else if(Conn == CONN_MAIN && Msg == NETMSG_PONGEX)
{
CUuid *pId = (CUuid *)Unpacker.GetRaw(sizeof(*pId));
if(Unpacker.Error())
{
return;
}
if(m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && *pId == m_CurrentServerPingUuid)
{
int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq();
m_ServerBrowser.SetCurrentServerPing(ServerAddress(), LatencyMs);
m_CurrentServerCurrentPingTime = -1;
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "got pong from current server, latency=%dms", LatencyMs);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
}
}
else if(Msg == NETMSG_CHECKSUM_REQUEST)
{
CUuid *pUuid = (CUuid *)Unpacker.GetRaw(sizeof(*pUuid));
if(Unpacker.Error())
{
return;
}
int ResultCheck = HandleChecksum(Conn, *pUuid, &Unpacker);
if(ResultCheck)
{
CMsgPacker MsgP(NETMSG_CHECKSUM_ERROR, true);
MsgP.AddRaw(pUuid, sizeof(*pUuid));
MsgP.AddInt(ResultCheck);
SendMsg(Conn, &MsgP, MSGFLAG_VITAL);
}
}
else if(Msg == NETMSG_REDIRECT)
{
int RedirectPort = Unpacker.GetInt();
if(Unpacker.Error())
{
return;
}
char aAddr[NETADDR_MAXSTRSIZE];
NETADDR ServerAddr = ServerAddress();
ServerAddr.port = RedirectPort;
net_addr_str(&ServerAddr, aAddr, sizeof(aAddr), true);
Connect(aAddr);
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_ADD)
{
const char *pName = Unpacker.GetString(CUnpacker::SANITIZE_CC);
const char *pHelp = Unpacker.GetString(CUnpacker::SANITIZE_CC);
const char *pParams = Unpacker.GetString(CUnpacker::SANITIZE_CC);
if(!Unpacker.Error())
{
m_pConsole->RegisterTemp(pName, pParams, CFGFLAG_SERVER, pHelp);
}
m_GotRconCommands++;
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_REM)
{
const char *pName = Unpacker.GetString(CUnpacker::SANITIZE_CC);
if(!Unpacker.Error())
{
m_pConsole->DeregisterTemp(pName);
}
}
else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_AUTH_STATUS)
{
int ResultInt = Unpacker.GetInt();
if(!Unpacker.Error())
{
m_aRconAuthed[Conn] = ResultInt;
}
if(Conn == CONN_MAIN)
{
int Old = m_UseTempRconCommands;
m_UseTempRconCommands = Unpacker.GetInt();
if(Unpacker.Error())
{
m_UseTempRconCommands = 0;
}
if(Old != 0 && m_UseTempRconCommands == 0)
{
m_pConsole->DeregisterTempAll();
m_ExpectedRconCommands = -1;
}
}
}
else if(!Dummy && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_LINE)
{
const char *pLine = Unpacker.GetString();
if(!Unpacker.Error())
{
GameClient()->OnRconLine(pLine);
}
}
else if(Conn == CONN_MAIN && Msg == NETMSG_PING_REPLY)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "latency %.2f", (time_get() - m_PingStartTime) * 1000 / (float)time_freq());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client/network", aBuf);
}
else if(Msg == NETMSG_INPUTTIMING)
{
int InputPredTick = Unpacker.GetInt();
int TimeLeft = Unpacker.GetInt();
if(Unpacker.Error())
{
return;
}
2021-06-23 05:05:49 +00:00
int64_t Now = time_get();
// adjust our prediction time
2021-06-23 05:05:49 +00:00
int64_t Target = 0;
for(int k = 0; k < 200; k++)
2010-05-29 07:25:38 +00:00
{
if(m_aInputs[Conn][k].m_Tick == InputPredTick)
2010-05-29 07:25:38 +00:00
{
Target = m_aInputs[Conn][k].m_PredictedTime + (Now - m_aInputs[Conn][k].m_Time);
Target = Target - (int64_t)((TimeLeft / 1000.0f) * time_freq());
break;
2010-05-29 07:25:38 +00:00
}
}
if(Target)
m_PredictedTime.Update(&m_InputtimeMarginGraph, Target, TimeLeft, CSmoothTime::ADJUSTDIRECTION_UP);
}
else if(Msg == NETMSG_SNAP || Msg == NETMSG_SNAPSINGLE || Msg == NETMSG_SNAPEMPTY)
{
// we are not allowed to process snapshot yet
if(State() < IClient::STATE_LOADING)
{
return;
}
int GameTick = Unpacker.GetInt();
int DeltaTick = GameTick - Unpacker.GetInt();
int NumParts = 1;
int Part = 0;
if(Msg == NETMSG_SNAP)
2010-05-29 07:25:38 +00:00
{
NumParts = Unpacker.GetInt();
Part = Unpacker.GetInt();
2010-05-29 07:25:38 +00:00
}
unsigned int Crc = 0;
int PartSize = 0;
if(Msg != NETMSG_SNAPEMPTY)
{
Crc = Unpacker.GetInt();
PartSize = Unpacker.GetInt();
}
2010-05-29 07:25:38 +00:00
const char *pData = (const char *)Unpacker.GetRaw(PartSize);
2020-01-25 06:37:28 +00:00
if(Unpacker.Error() || NumParts < 1 || NumParts > CSnapshot::MAX_PARTS || Part < 0 || Part >= NumParts || PartSize < 0 || PartSize > MAX_SNAPSHOT_PACKSIZE)
{
return;
}
2010-05-29 07:25:38 +00:00
// Check m_aAckGameTick to see if we already got a snapshot for that tick
if(GameTick >= m_aCurrentRecvTick[Conn] && GameTick > m_aAckGameTick[Conn])
{
if(GameTick != m_aCurrentRecvTick[Conn])
2010-05-29 07:25:38 +00:00
{
m_aSnapshotParts[Conn] = 0;
m_aCurrentRecvTick[Conn] = GameTick;
m_aSnapshotIncomingDataSize[Conn] = 0;
2010-05-29 07:25:38 +00:00
}
mem_copy((char *)m_aaSnapshotIncomingData[Conn] + Part * MAX_SNAPSHOT_PACKSIZE, pData, clamp(PartSize, 0, (int)sizeof(m_aaSnapshotIncomingData[Conn]) - Part * MAX_SNAPSHOT_PACKSIZE));
m_aSnapshotParts[Conn] |= (uint64_t)(1) << Part;
2010-05-29 07:25:38 +00:00
if(Part == NumParts - 1)
{
m_aSnapshotIncomingDataSize[Conn] = (NumParts - 1) * MAX_SNAPSHOT_PACKSIZE + PartSize;
}
if((NumParts < CSnapshot::MAX_PARTS && m_aSnapshotParts[Conn] == (((uint64_t)(1) << NumParts) - 1)) ||
(NumParts == CSnapshot::MAX_PARTS && m_aSnapshotParts[Conn] == std::numeric_limits<uint64_t>::max()))
2010-05-29 07:25:38 +00:00
{
unsigned char aTmpBuffer2[CSnapshot::MAX_SIZE];
unsigned char aTmpBuffer3[CSnapshot::MAX_SIZE];
CSnapshot *pTmpBuffer3 = (CSnapshot *)aTmpBuffer3; // Fix compiler warning for strict-aliasing
// reset snapshoting
m_aSnapshotParts[Conn] = 0;
// find snapshot that we should use as delta
const CSnapshot *pDeltaShot = CSnapshot::EmptySnapshot();
if(DeltaTick >= 0)
2010-05-29 07:25:38 +00:00
{
2022-10-09 15:57:43 +00:00
int DeltashotSize = m_aSnapshotStorage[Conn].Get(DeltaTick, nullptr, &pDeltaShot, nullptr);
2010-05-29 07:25:38 +00:00
if(DeltashotSize < 0)
2010-05-29 07:25:38 +00:00
{
// couldn't find the delta snapshots that the server used
// to compress this snapshot. force the server to resync
2010-05-29 07:25:38 +00:00
if(g_Config.m_Debug)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", "error, couldn't find the delta snapshot");
2010-05-29 07:25:38 +00:00
}
// ack snapshot
m_aAckGameTick[Conn] = -1;
SendInput();
2010-05-29 07:25:38 +00:00
return;
}
}
2010-05-29 07:25:38 +00:00
// decompress snapshot
const void *pDeltaData = m_SnapshotDelta.EmptyDelta();
int DeltaSize = sizeof(int) * 3;
2010-05-29 07:25:38 +00:00
if(m_aSnapshotIncomingDataSize[Conn])
{
int IntSize = CVariableInt::Decompress(m_aaSnapshotIncomingData[Conn], m_aSnapshotIncomingDataSize[Conn], aTmpBuffer2, sizeof(aTmpBuffer2));
2010-05-29 07:25:38 +00:00
if(IntSize < 0) // failure during decompression
return;
2010-05-29 07:25:38 +00:00
pDeltaData = aTmpBuffer2;
DeltaSize = IntSize;
}
2010-05-29 07:25:38 +00:00
// unpack delta
const int SnapSize = m_SnapshotDelta.UnpackDelta(pDeltaShot, pTmpBuffer3, pDeltaData, DeltaSize);
if(SnapSize < 0)
{
Validate snapshot size and member variables and demo snapshots Add `CSnapshot::IsValid` to check if a snapshot unpacked from a snapshot delta or demo is valid: - ensure number of items and data size are not negative - ensure that the actual size of the snapshot matches the size derived from its member variables - ensure item offsets are within the valid range - ensure item sizes are not negative Add `CSnapshot::TotalSize` and `CSnapshot::OffsetSize` utility functions. Minor improvements to related error messages. Fixes buffer overflow: ``` ==47744==ERROR: AddressSanitizer: global-buffer-overflow on address 0x558618e3767f at pc 0x558614b9bdfb bp 0x7ffe58a32cd0 sp 0x7ffe58a32cc0 READ of size 4 at 0x558618e3767f thread T0 0x558614b9bdfa in CSnapshotItem::Type() const src/engine/shared/snapshot.h:16 0x558615c3c911 in CSnapshot::GetItemType(int) const src/engine/shared/snapshot.cpp:29 0x558614aebaba in CClient::UnpackAndValidateSnapshot(CSnapshot*, CSnapshot*) src/engine/client/client.cpp:2264 0x558614af87cb in CClient::OnDemoPlayerSnapshot(void*, int) src/engine/client/client.cpp:2598 0x558615b9db1a in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:659 0x558615babd3f in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x558614afb08b in CClient::Update() src/engine/client/client.cpp:2686 0x558614b1d9eb in CClient::Run() src/engine/client/client.cpp:3296 0x558614b8e64f in main src/engine/client/client.cpp:4761 ``` And fixes a buffer overflow that manifests itself as an internal ASan error: ``` ================================================================= ==4755==AddressSanitizer CHECK failed: ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 "((0 && "Address is not in memory and not in shadow?")) != (0)" (0x0, 0x0) 0x7f0bf5f368be in AsanCheckFailed ../../../../src/libsanitizer/asan/asan_rtl.cc:72 0x7f0bf5f54eee in __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_termination.cc:77 0x7f0bf5e4cb6f in GetShadowKind ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:95 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:92 0x7f0bf5e4e386 in __asan::AddressDescription::AddressDescription(unsigned long, unsigned long, bool) ../../../../src/libsanitizer/asan/asan_descriptions.cc:440 0x7f0bf5e50e94 in __asan::ErrorGeneric::ErrorGeneric(unsigned int, unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long) ../../../../src/libsanitizer/asan/asan_errors.cc:380 0x7f0bf5f35f4d in __asan::ReportGenericError(unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long, unsigned int, bool) ../../../../src/libsanitizer/asan/asan_report.cc:460 0x7f0bf5e86f5e in __interceptor_memset ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:762 0x558234873f1d in mem_zero src/base/system.cpp:213 0x55823481fc27 in CSnapshotBuilder::NewItem(int, int, int) src/engine/shared/snapshot.cpp:675 0x55823481be65 in CSnapshotDelta::UnpackDelta(CSnapshot*, CSnapshot*, void const*, int) src/engine/shared/snapshot.cpp:380 0x558234776641 in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:631 0x5582347861a9 in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x5582336d4c7d in CClient::Update() src/engine/client/client.cpp:2695 0x5582336f75dd in CClient::Run() src/engine/client/client.cpp:3305 0x558233768241 in main src/engine/client/client.cpp:4770 ```
2022-07-22 22:01:46 +00:00
dbg_msg("client", "delta unpack failed. error=%d", SnapSize);
return;
}
if(!pTmpBuffer3->IsValid(SnapSize))
{
dbg_msg("client", "snapshot invalid. SnapSize=%d, DeltaSize=%d", SnapSize, DeltaSize);
return;
}
2010-05-29 07:25:38 +00:00
if(Msg != NETMSG_SNAPEMPTY && pTmpBuffer3->Crc() != Crc)
{
if(g_Config.m_Debug)
2010-05-29 07:25:38 +00:00
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "snapshot crc error #%d - tick=%d wantedcrc=%d gotcrc=%d compressed_size=%d delta_tick=%d",
m_SnapCrcErrors, GameTick, Crc, pTmpBuffer3->Crc(), m_aSnapshotIncomingDataSize[Conn], DeltaTick);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf);
2010-05-29 07:25:38 +00:00
}
m_SnapCrcErrors++;
if(m_SnapCrcErrors > 10)
2010-05-29 07:25:38 +00:00
{
// to many errors, send reset
m_aAckGameTick[Conn] = -1;
SendInput();
m_SnapCrcErrors = 0;
2010-05-29 07:25:38 +00:00
}
return;
}
else
{
if(m_SnapCrcErrors)
m_SnapCrcErrors--;
}
2010-05-29 07:25:38 +00:00
// purge old snapshots
int PurgeTick = DeltaTick;
if(m_aapSnapshots[Conn][SNAP_PREV] && m_aapSnapshots[Conn][SNAP_PREV]->m_Tick < PurgeTick)
PurgeTick = m_aapSnapshots[Conn][SNAP_PREV]->m_Tick;
if(m_aapSnapshots[Conn][SNAP_CURRENT] && m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick < PurgeTick)
PurgeTick = m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick;
m_aSnapshotStorage[Conn].PurgeUntil(PurgeTick);
// create a verified and unpacked snapshot
unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
const int AltSnapSize = UnpackAndValidateSnapshot(pTmpBuffer3, pAltSnapBuffer);
if(AltSnapSize < 0)
{
Validate snapshot size and member variables and demo snapshots Add `CSnapshot::IsValid` to check if a snapshot unpacked from a snapshot delta or demo is valid: - ensure number of items and data size are not negative - ensure that the actual size of the snapshot matches the size derived from its member variables - ensure item offsets are within the valid range - ensure item sizes are not negative Add `CSnapshot::TotalSize` and `CSnapshot::OffsetSize` utility functions. Minor improvements to related error messages. Fixes buffer overflow: ``` ==47744==ERROR: AddressSanitizer: global-buffer-overflow on address 0x558618e3767f at pc 0x558614b9bdfb bp 0x7ffe58a32cd0 sp 0x7ffe58a32cc0 READ of size 4 at 0x558618e3767f thread T0 0x558614b9bdfa in CSnapshotItem::Type() const src/engine/shared/snapshot.h:16 0x558615c3c911 in CSnapshot::GetItemType(int) const src/engine/shared/snapshot.cpp:29 0x558614aebaba in CClient::UnpackAndValidateSnapshot(CSnapshot*, CSnapshot*) src/engine/client/client.cpp:2264 0x558614af87cb in CClient::OnDemoPlayerSnapshot(void*, int) src/engine/client/client.cpp:2598 0x558615b9db1a in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:659 0x558615babd3f in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x558614afb08b in CClient::Update() src/engine/client/client.cpp:2686 0x558614b1d9eb in CClient::Run() src/engine/client/client.cpp:3296 0x558614b8e64f in main src/engine/client/client.cpp:4761 ``` And fixes a buffer overflow that manifests itself as an internal ASan error: ``` ================================================================= ==4755==AddressSanitizer CHECK failed: ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 "((0 && "Address is not in memory and not in shadow?")) != (0)" (0x0, 0x0) 0x7f0bf5f368be in AsanCheckFailed ../../../../src/libsanitizer/asan/asan_rtl.cc:72 0x7f0bf5f54eee in __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_termination.cc:77 0x7f0bf5e4cb6f in GetShadowKind ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:95 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:92 0x7f0bf5e4e386 in __asan::AddressDescription::AddressDescription(unsigned long, unsigned long, bool) ../../../../src/libsanitizer/asan/asan_descriptions.cc:440 0x7f0bf5e50e94 in __asan::ErrorGeneric::ErrorGeneric(unsigned int, unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long) ../../../../src/libsanitizer/asan/asan_errors.cc:380 0x7f0bf5f35f4d in __asan::ReportGenericError(unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long, unsigned int, bool) ../../../../src/libsanitizer/asan/asan_report.cc:460 0x7f0bf5e86f5e in __interceptor_memset ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:762 0x558234873f1d in mem_zero src/base/system.cpp:213 0x55823481fc27 in CSnapshotBuilder::NewItem(int, int, int) src/engine/shared/snapshot.cpp:675 0x55823481be65 in CSnapshotDelta::UnpackDelta(CSnapshot*, CSnapshot*, void const*, int) src/engine/shared/snapshot.cpp:380 0x558234776641 in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:631 0x5582347861a9 in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x5582336d4c7d in CClient::Update() src/engine/client/client.cpp:2695 0x5582336f75dd in CClient::Run() src/engine/client/client.cpp:3305 0x558233768241 in main src/engine/client/client.cpp:4770 ```
2022-07-22 22:01:46 +00:00
dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize);
return;
}
// add new
m_aSnapshotStorage[Conn].Add(GameTick, time_get(), SnapSize, pTmpBuffer3, AltSnapSize, pAltSnapBuffer);
if(!Dummy)
{
// for antiping: if the projectile netobjects from the server contains extra data, this is removed and the original content restored before recording demo
SnapshotRemoveExtraProjectileInfo(pTmpBuffer3);
// add snapshot to demo
for(auto &DemoRecorder : m_aDemoRecorder)
{
if(DemoRecorder.IsRecording())
{
// write snapshot
DemoRecorder.RecordSnapshot(GameTick, pTmpBuffer3, SnapSize);
}
}
}
2010-05-29 07:25:38 +00:00
// apply snapshot, cycle pointers
m_aReceivedSnapshots[Conn]++;
// we got two snapshots until we see us self as connected
if(m_aReceivedSnapshots[Conn] == 2)
{
// start at 200ms and work from there
if(!Dummy)
{
m_PredictedTime.Init(GameTick * time_freq() / GameTickSpeed());
m_PredictedTime.SetAdjustSpeed(CSmoothTime::ADJUSTDIRECTION_UP, 1000.0f);
m_PredictedTime.UpdateMargin(PredictionMargin() * time_freq() / 1000);
}
m_aGameTime[Conn].Init((GameTick - 1) * time_freq() / GameTickSpeed());
m_aapSnapshots[Conn][SNAP_PREV] = m_aSnapshotStorage[Conn].m_pFirst;
m_aapSnapshots[Conn][SNAP_CURRENT] = m_aSnapshotStorage[Conn].m_pLast;
m_aPrevGameTick[Conn] = m_aapSnapshots[Conn][SNAP_PREV]->m_Tick;
m_aCurGameTick[Conn] = m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick;
if(!Dummy)
{
m_LocalStartTime = time_get();
#if defined(CONF_VIDEORECORDER)
IVideo::SetLocalStartTime(m_LocalStartTime);
#endif
GameClient()->OnNewSnapshot();
}
SetState(IClient::STATE_ONLINE);
if(!Dummy)
{
DemoRecorder_HandleAutoStart();
}
2010-05-29 07:25:38 +00:00
}
// adjust game time
if(m_aReceivedSnapshots[Conn] > 2)
{
int64_t Now = m_aGameTime[Conn].Get(time_get());
int64_t TickStart = GameTick * time_freq() / GameTickSpeed();
2021-06-23 05:05:49 +00:00
int64_t TimeLeft = (TickStart - Now) * 1000 / time_freq();
m_aGameTime[Conn].Update(&m_GametimeMarginGraph, (GameTick - 1) * time_freq() / GameTickSpeed(), TimeLeft, CSmoothTime::ADJUSTDIRECTION_DOWN);
2010-05-29 07:25:38 +00:00
}
if(m_aReceivedSnapshots[Conn] > GameTickSpeed() && !m_aCodeRunAfterJoin[Conn])
{
if(m_ServerCapabilities.m_ChatTimeoutCode)
{
CNetMsg_Cl_Say MsgP;
MsgP.m_Team = 0;
char aBuf[128];
char aBufMsg[256];
if(!g_Config.m_ClRunOnJoin[0] && !g_Config.m_ClDummyDefaultEyes && !g_Config.m_ClPlayerDefaultEyes)
str_format(aBufMsg, sizeof(aBufMsg), "/timeout %s", m_aTimeoutCodes[Conn]);
else
str_format(aBufMsg, sizeof(aBufMsg), "/mc;timeout %s", m_aTimeoutCodes[Conn]);
if(g_Config.m_ClRunOnJoin[0])
{
str_format(aBuf, sizeof(aBuf), ";%s", g_Config.m_ClRunOnJoin);
str_append(aBufMsg, aBuf);
}
if(g_Config.m_ClDummyDefaultEyes || g_Config.m_ClPlayerDefaultEyes)
{
int Emote = ((g_Config.m_ClDummy) ? !Dummy : Dummy) ? g_Config.m_ClDummyDefaultEyes : g_Config.m_ClPlayerDefaultEyes;
char aBufEmote[128];
aBufEmote[0] = '\0';
switch(Emote)
{
case EMOTE_NORMAL:
break;
case EMOTE_PAIN:
str_format(aBufEmote, sizeof(aBufEmote), "emote pain %d", g_Config.m_ClEyeDuration);
break;
case EMOTE_HAPPY:
str_format(aBufEmote, sizeof(aBufEmote), "emote happy %d", g_Config.m_ClEyeDuration);
break;
case EMOTE_SURPRISE:
str_format(aBufEmote, sizeof(aBufEmote), "emote surprise %d", g_Config.m_ClEyeDuration);
break;
case EMOTE_ANGRY:
str_format(aBufEmote, sizeof(aBufEmote), "emote angry %d", g_Config.m_ClEyeDuration);
break;
case EMOTE_BLINK:
str_format(aBufEmote, sizeof(aBufEmote), "emote blink %d", g_Config.m_ClEyeDuration);
break;
}
if(aBufEmote[0])
{
str_format(aBuf, sizeof(aBuf), ";%s", aBufEmote);
str_append(aBufMsg, aBuf);
}
}
MsgP.m_pMessage = aBufMsg;
CMsgPacker PackerTimeout(&MsgP);
MsgP.Pack(&PackerTimeout);
SendMsg(Conn, &PackerTimeout, MSGFLAG_VITAL);
}
m_aCodeRunAfterJoin[Conn] = true;
}
// ack snapshot
m_aAckGameTick[Conn] = GameTick;
2010-05-29 07:25:38 +00:00
}
}
}
else if(Conn == CONN_MAIN && Msg == NETMSG_RCONTYPE)
{
bool UsernameReq = Unpacker.GetInt() & 1;
if(!Unpacker.Error())
{
GameClient()->OnRconType(UsernameReq);
}
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_START)
{
int ExpectedRconCommands = Unpacker.GetInt();
if(Unpacker.Error())
return;
m_ExpectedRconCommands = ExpectedRconCommands;
m_GotRconCommands = 0;
}
else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_END)
{
m_ExpectedRconCommands = -1;
}
}
else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0)
{
// game message
if(!Dummy)
{
for(auto &DemoRecorder : m_aDemoRecorder)
if(DemoRecorder.IsRecording())
DemoRecorder.RecordMessage(pPacket->m_pData, pPacket->m_DataSize);
2014-05-03 00:30:05 +00:00
}
GameClient()->OnMessage(Msg, &Unpacker, Conn, Dummy);
2014-05-03 00:30:05 +00:00
}
}
int CClient::UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo)
{
CUnpacker Unpacker;
CSnapshotBuilder Builder;
Builder.Init();
CNetObjHandler *pNetObjHandler = GameClient()->GetNetObjHandler();
int Num = pFrom->NumItems();
for(int Index = 0; Index < Num; Index++)
{
const CSnapshotItem *pFromItem = pFrom->GetItem(Index);
const int FromItemSize = pFrom->GetItemSize(Index);
const int ItemType = pFrom->GetItemType(Index);
const void *pData = pFromItem->Data();
Unpacker.Reset(pData, FromItemSize);
void *pRawObj = pNetObjHandler->SecureUnpackObj(ItemType, &Unpacker);
if(!pRawObj)
{
if(g_Config.m_Debug && ItemType != UUID_UNKNOWN)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "dropped weird object '%s' (%d), failed on '%s'", pNetObjHandler->GetObjName(ItemType), ItemType, pNetObjHandler->FailedObjOn());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
}
continue;
}
const int ItemSize = pNetObjHandler->GetUnpackedObjSize(ItemType);
void *pObj = Builder.NewItem(pFromItem->Type(), pFromItem->Id(), ItemSize);
if(!pObj)
return -4;
mem_copy(pObj, pRawObj, ItemSize);
}
return Builder.Finish(pTo);
}
void CClient::ResetMapDownload()
{
2017-03-04 14:43:49 +00:00
if(m_pMapdownloadTask)
{
2017-04-11 19:47:27 +00:00
m_pMapdownloadTask->Abort();
2017-04-11 23:20:39 +00:00
m_pMapdownloadTask = NULL;
}
m_MapdownloadFileTemp = 0;
m_MapdownloadAmount = 0;
}
void CClient::FinishMapDownload()
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "download complete, loading map");
2018-06-19 12:28:53 +00:00
int Prev = m_MapdownloadTotalsize;
m_MapdownloadTotalsize = -1;
SHA256_DIGEST *pSha256 = m_MapdownloadSha256Present ? &m_MapdownloadSha256 : 0;
bool FileSuccess = true;
if(Storage()->FileExists(m_aMapdownloadFilename, IStorage::TYPE_SAVE))
FileSuccess &= Storage()->RemoveFile(m_aMapdownloadFilename, IStorage::TYPE_SAVE);
FileSuccess &= Storage()->RenameFile(m_aMapdownloadFilenameTemp, m_aMapdownloadFilename, IStorage::TYPE_SAVE);
if(!FileSuccess)
{
ResetMapDownload();
char aError[128 + IO_MAX_PATH_LENGTH];
str_format(aError, sizeof(aError), Localize("Could not save downloaded map. Try manually deleting this file: %s"), m_aMapdownloadFilename);
DisconnectWithReason(aError);
return;
}
// load map
const char *pError = LoadMap(m_aMapdownloadName, m_aMapdownloadFilename, pSha256, m_MapdownloadCrc);
if(!pError)
{
2015-01-19 22:51:03 +00:00
ResetMapDownload();
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done");
SendReady(CONN_MAIN);
}
2017-04-11 19:47:27 +00:00
else if(m_pMapdownloadTask) // fallback
{
ResetMapDownload();
2018-06-19 12:28:53 +00:00
m_MapdownloadTotalsize = Prev;
SendMapRequest();
}
2017-03-04 14:43:49 +00:00
else
{
if(m_MapdownloadFileTemp)
{
io_close(m_MapdownloadFileTemp);
m_MapdownloadFileTemp = 0;
Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE);
}
2015-01-19 22:51:03 +00:00
ResetMapDownload();
DisconnectWithReason(pError);
2015-01-19 22:51:03 +00:00
}
}
void CClient::ResetDDNetInfoTask()
{
if(m_pDDNetInfoTask)
{
m_pDDNetInfoTask->Abort();
m_pDDNetInfoTask = NULL;
}
}
void CClient::FinishDDNetInfo()
{
if(m_ServerBrowser.DDNetInfoSha256() == m_pDDNetInfoTask->ResultSha256())
{
log_debug("client/info", "DDNet info already up-to-date");
return;
}
char aTempFilename[IO_MAX_PATH_LENGTH];
IStorage::FormatTmpPath(aTempFilename, sizeof(aTempFilename), DDNET_INFO_FILE);
IOHANDLE File = Storage()->OpenFile(aTempFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!File)
{
log_error("client/info", "Failed to open temporary DDNet info '%s' for writing", aTempFilename);
return;
}
unsigned char *pResult;
size_t ResultLength;
m_pDDNetInfoTask->Result(&pResult, &ResultLength);
bool Error = io_write(File, pResult, ResultLength) != ResultLength;
Error |= io_close(File) != 0;
if(Error)
{
log_error("client/info", "Error writing temporary DDNet info to file '%s'", aTempFilename);
return;
}
if(Storage()->FileExists(DDNET_INFO_FILE, IStorage::TYPE_SAVE) && !Storage()->RemoveFile(DDNET_INFO_FILE, IStorage::TYPE_SAVE))
{
log_error("client/info", "Failed to remove old DDNet info '%s'", DDNET_INFO_FILE);
Storage()->RemoveFile(aTempFilename, IStorage::TYPE_SAVE);
return;
}
if(!Storage()->RenameFile(aTempFilename, DDNET_INFO_FILE, IStorage::TYPE_SAVE))
{
log_error("client/info", "Failed to rename temporary DDNet info '%s' to '%s'", aTempFilename, DDNET_INFO_FILE);
Storage()->RemoveFile(aTempFilename, IStorage::TYPE_SAVE);
return;
}
log_debug("client/info", "Loading new DDNet info");
LoadDDNetInfo();
}
2022-07-02 12:43:41 +00:00
typedef std::tuple<int, int, int> TVersion;
static const TVersion gs_InvalidVersion = std::make_tuple(-1, -1, -1);
2022-07-02 12:43:41 +00:00
TVersion ToVersion(char *pStr)
{
int aVersion[3] = {0, 0, 0};
const char *p = strtok(pStr, ".");
for(int i = 0; i < 3 && p; ++i)
{
if(!str_isallnum(p))
return gs_InvalidVersion;
aVersion[i] = str_toint(p);
p = strtok(NULL, ".");
}
if(p)
return gs_InvalidVersion;
return std::make_tuple(aVersion[0], aVersion[1], aVersion[2]);
}
void CClient::LoadDDNetInfo()
{
const json_value *pDDNetInfo = m_ServerBrowser.LoadDDNetInfo();
if(!pDDNetInfo)
return;
const json_value &DDNetInfo = *pDDNetInfo;
const json_value &CurrentVersion = DDNetInfo["version"];
if(CurrentVersion.type == json_string)
{
char aNewVersionStr[64];
2022-07-09 16:14:56 +00:00
str_copy(aNewVersionStr, CurrentVersion);
char aCurVersionStr[64];
2022-07-09 16:14:56 +00:00
str_copy(aCurVersionStr, GAME_RELEASE_VERSION);
if(ToVersion(aNewVersionStr) > ToVersion(aCurVersionStr))
{
2022-07-09 16:14:56 +00:00
str_copy(m_aVersionStr, CurrentVersion);
}
else
{
m_aVersionStr[0] = '0';
m_aVersionStr[1] = '\0';
}
}
const json_value &News = DDNetInfo["news"];
if(News.type == json_string)
{
// Only mark news button if something new was added to the news
if(m_aNews[0] && str_find(m_aNews, News) == nullptr)
g_Config.m_UiUnreadNews = true;
2022-07-09 16:14:56 +00:00
str_copy(m_aNews, News);
}
const json_value &MapDownloadUrl = DDNetInfo["map-download-url"];
if(MapDownloadUrl.type == json_string)
{
2022-07-09 16:14:56 +00:00
str_copy(m_aMapDownloadUrl, MapDownloadUrl);
}
const json_value &Points = DDNetInfo["points"];
if(Points.type == json_integer)
{
m_Points = Points.u.integer;
}
const json_value &StunServersIpv6 = DDNetInfo["stun-servers-ipv6"];
if(StunServersIpv6.type == json_array && StunServersIpv6[0].type == json_string)
{
NETADDR Addr;
if(!net_addr_from_str(&Addr, StunServersIpv6[0]))
{
m_aNetClient[CONN_MAIN].FeedStunServer(Addr);
}
}
const json_value &StunServersIpv4 = DDNetInfo["stun-servers-ipv4"];
if(StunServersIpv4.type == json_array && StunServersIpv4[0].type == json_string)
{
NETADDR Addr;
if(!net_addr_from_str(&Addr, StunServersIpv4[0]))
{
m_aNetClient[CONN_MAIN].FeedStunServer(Addr);
}
}
const json_value &ConnectingIp = DDNetInfo["connecting-ip"];
if(ConnectingIp.type == json_string)
{
NETADDR Addr;
if(!net_addr_from_str(&Addr, ConnectingIp))
{
m_HaveGlobalTcpAddr = true;
m_GlobalTcpAddr = Addr;
log_debug("info", "got global tcp ip address: %s", (const char *)ConnectingIp);
}
}
const json_value &WarnPngliteIncompatibleImages = DDNetInfo["warn-pnglite-incompatible-images"];
Graphics()->WarnPngliteIncompatibleImages(WarnPngliteIncompatibleImages.type == json_boolean && (bool)WarnPngliteIncompatibleImages);
}
int CClient::ConnectNetTypes() const
{
const NETADDR *pConnectAddrs;
int NumConnectAddrs;
m_aNetClient[CONN_MAIN].ConnectAddresses(&pConnectAddrs, &NumConnectAddrs);
int NetType = 0;
for(int i = 0; i < NumConnectAddrs; i++)
{
NetType |= pConnectAddrs[i].type;
}
return NetType;
}
2010-05-29 07:25:38 +00:00
void CClient::PumpNetwork()
{
for(auto &NetClient : m_aNetClient)
{
2020-10-26 14:14:07 +00:00
NetClient.Update();
}
2010-05-29 07:25:38 +00:00
if(State() != IClient::STATE_DEMOPLAYBACK)
{
// check for errors
if(State() != IClient::STATE_OFFLINE && State() < IClient::STATE_QUITTING && m_aNetClient[CONN_MAIN].State() == NETSTATE_OFFLINE)
2010-05-29 07:25:38 +00:00
{
Disconnect();
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "offline error='%s'", m_aNetClient[CONN_MAIN].ErrorString());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkErrPrintColor);
2010-05-29 07:25:38 +00:00
}
2020-10-02 13:44:27 +00:00
if(State() != IClient::STATE_OFFLINE && State() < IClient::STATE_QUITTING && m_DummyConnected &&
m_aNetClient[CONN_DUMMY].State() == NETSTATE_OFFLINE)
{
DummyDisconnect(0);
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "offline dummy error='%s'", m_aNetClient[CONN_DUMMY].ErrorString());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkErrPrintColor);
}
2010-05-29 07:25:38 +00:00
//
if(State() == IClient::STATE_CONNECTING && m_aNetClient[CONN_MAIN].State() == NETSTATE_ONLINE)
2010-05-29 07:25:38 +00:00
{
// we switched to online
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", "connected, sending info", gs_ClientNetworkPrintColor);
2010-05-29 07:25:38 +00:00
SetState(IClient::STATE_LOADING);
SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_INITIAL);
SendInfo(CONN_MAIN);
2010-05-29 07:25:38 +00:00
}
}
// process packets
CNetChunk Packet;
for(int Conn = 0; Conn < NUM_CONNS; Conn++)
{
while(m_aNetClient[Conn].Recv(&Packet))
{
if(Packet.m_ClientId == -1)
{
ProcessConnlessPacket(&Packet);
continue;
}
if(Conn == CONN_MAIN || Conn == CONN_DUMMY)
{
ProcessServerPacket(&Packet, Conn, g_Config.m_ClDummy ^ Conn);
}
}
}
2010-05-29 07:25:38 +00:00
}
void CClient::OnDemoPlayerSnapshot(void *pData, int Size)
{
// update ticks, they could have changed
const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
m_aPrevGameTick[0] = pInfo->m_PreviousTick;
2010-05-29 07:25:38 +00:00
// create a verified and unpacked snapshot
unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
const int AltSnapSize = UnpackAndValidateSnapshot((CSnapshot *)pData, pAltSnapBuffer);
if(AltSnapSize < 0)
{
Validate snapshot size and member variables and demo snapshots Add `CSnapshot::IsValid` to check if a snapshot unpacked from a snapshot delta or demo is valid: - ensure number of items and data size are not negative - ensure that the actual size of the snapshot matches the size derived from its member variables - ensure item offsets are within the valid range - ensure item sizes are not negative Add `CSnapshot::TotalSize` and `CSnapshot::OffsetSize` utility functions. Minor improvements to related error messages. Fixes buffer overflow: ``` ==47744==ERROR: AddressSanitizer: global-buffer-overflow on address 0x558618e3767f at pc 0x558614b9bdfb bp 0x7ffe58a32cd0 sp 0x7ffe58a32cc0 READ of size 4 at 0x558618e3767f thread T0 0x558614b9bdfa in CSnapshotItem::Type() const src/engine/shared/snapshot.h:16 0x558615c3c911 in CSnapshot::GetItemType(int) const src/engine/shared/snapshot.cpp:29 0x558614aebaba in CClient::UnpackAndValidateSnapshot(CSnapshot*, CSnapshot*) src/engine/client/client.cpp:2264 0x558614af87cb in CClient::OnDemoPlayerSnapshot(void*, int) src/engine/client/client.cpp:2598 0x558615b9db1a in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:659 0x558615babd3f in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x558614afb08b in CClient::Update() src/engine/client/client.cpp:2686 0x558614b1d9eb in CClient::Run() src/engine/client/client.cpp:3296 0x558614b8e64f in main src/engine/client/client.cpp:4761 ``` And fixes a buffer overflow that manifests itself as an internal ASan error: ``` ================================================================= ==4755==AddressSanitizer CHECK failed: ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 "((0 && "Address is not in memory and not in shadow?")) != (0)" (0x0, 0x0) 0x7f0bf5f368be in AsanCheckFailed ../../../../src/libsanitizer/asan/asan_rtl.cc:72 0x7f0bf5f54eee in __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_termination.cc:77 0x7f0bf5e4cb6f in GetShadowKind ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:95 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:92 0x7f0bf5e4e386 in __asan::AddressDescription::AddressDescription(unsigned long, unsigned long, bool) ../../../../src/libsanitizer/asan/asan_descriptions.cc:440 0x7f0bf5e50e94 in __asan::ErrorGeneric::ErrorGeneric(unsigned int, unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long) ../../../../src/libsanitizer/asan/asan_errors.cc:380 0x7f0bf5f35f4d in __asan::ReportGenericError(unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long, unsigned int, bool) ../../../../src/libsanitizer/asan/asan_report.cc:460 0x7f0bf5e86f5e in __interceptor_memset ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:762 0x558234873f1d in mem_zero src/base/system.cpp:213 0x55823481fc27 in CSnapshotBuilder::NewItem(int, int, int) src/engine/shared/snapshot.cpp:675 0x55823481be65 in CSnapshotDelta::UnpackDelta(CSnapshot*, CSnapshot*, void const*, int) src/engine/shared/snapshot.cpp:380 0x558234776641 in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:631 0x5582347861a9 in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x5582336d4c7d in CClient::Update() src/engine/client/client.cpp:2695 0x5582336f75dd in CClient::Run() src/engine/client/client.cpp:3305 0x558233768241 in main src/engine/client/client.cpp:4770 ```
2022-07-22 22:01:46 +00:00
dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize);
return;
}
Validate snapshot size and member variables and demo snapshots Add `CSnapshot::IsValid` to check if a snapshot unpacked from a snapshot delta or demo is valid: - ensure number of items and data size are not negative - ensure that the actual size of the snapshot matches the size derived from its member variables - ensure item offsets are within the valid range - ensure item sizes are not negative Add `CSnapshot::TotalSize` and `CSnapshot::OffsetSize` utility functions. Minor improvements to related error messages. Fixes buffer overflow: ``` ==47744==ERROR: AddressSanitizer: global-buffer-overflow on address 0x558618e3767f at pc 0x558614b9bdfb bp 0x7ffe58a32cd0 sp 0x7ffe58a32cc0 READ of size 4 at 0x558618e3767f thread T0 0x558614b9bdfa in CSnapshotItem::Type() const src/engine/shared/snapshot.h:16 0x558615c3c911 in CSnapshot::GetItemType(int) const src/engine/shared/snapshot.cpp:29 0x558614aebaba in CClient::UnpackAndValidateSnapshot(CSnapshot*, CSnapshot*) src/engine/client/client.cpp:2264 0x558614af87cb in CClient::OnDemoPlayerSnapshot(void*, int) src/engine/client/client.cpp:2598 0x558615b9db1a in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:659 0x558615babd3f in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x558614afb08b in CClient::Update() src/engine/client/client.cpp:2686 0x558614b1d9eb in CClient::Run() src/engine/client/client.cpp:3296 0x558614b8e64f in main src/engine/client/client.cpp:4761 ``` And fixes a buffer overflow that manifests itself as an internal ASan error: ``` ================================================================= ==4755==AddressSanitizer CHECK failed: ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 "((0 && "Address is not in memory and not in shadow?")) != (0)" (0x0, 0x0) 0x7f0bf5f368be in AsanCheckFailed ../../../../src/libsanitizer/asan/asan_rtl.cc:72 0x7f0bf5f54eee in __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_termination.cc:77 0x7f0bf5e4cb6f in GetShadowKind ../../../../src/libsanitizer/asan/asan_descriptions.cc:79 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:95 0x7f0bf5e4cb6f in __asan::GetShadowAddressInformation(unsigned long, __asan::ShadowAddressDescription*) ../../../../src/libsanitizer/asan/asan_descriptions.cc:92 0x7f0bf5e4e386 in __asan::AddressDescription::AddressDescription(unsigned long, unsigned long, bool) ../../../../src/libsanitizer/asan/asan_descriptions.cc:440 0x7f0bf5e50e94 in __asan::ErrorGeneric::ErrorGeneric(unsigned int, unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long) ../../../../src/libsanitizer/asan/asan_errors.cc:380 0x7f0bf5f35f4d in __asan::ReportGenericError(unsigned long, unsigned long, unsigned long, unsigned long, bool, unsigned long, unsigned int, bool) ../../../../src/libsanitizer/asan/asan_report.cc:460 0x7f0bf5e86f5e in __interceptor_memset ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:762 0x558234873f1d in mem_zero src/base/system.cpp:213 0x55823481fc27 in CSnapshotBuilder::NewItem(int, int, int) src/engine/shared/snapshot.cpp:675 0x55823481be65 in CSnapshotDelta::UnpackDelta(CSnapshot*, CSnapshot*, void const*, int) src/engine/shared/snapshot.cpp:380 0x558234776641 in CDemoPlayer::DoTick() src/engine/shared/demo.cpp:631 0x5582347861a9 in CDemoPlayer::Update(bool) src/engine/shared/demo.cpp:1007 0x5582336d4c7d in CClient::Update() src/engine/client/client.cpp:2695 0x5582336f75dd in CClient::Run() src/engine/client/client.cpp:3305 0x558233768241 in main src/engine/client/client.cpp:4770 ```
2022-07-22 22:01:46 +00:00
// handle snapshots after validation
std::swap(m_aapSnapshots[0][SNAP_PREV], m_aapSnapshots[0][SNAP_CURRENT]);
mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pSnap, pData, Size);
mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, pAltSnapBuffer, AltSnapSize);
2010-05-29 07:25:38 +00:00
GameClient()->OnNewSnapshot();
}
void CClient::OnDemoPlayerMessage(void *pData, int Size)
{
CUnpacker Unpacker;
Unpacker.Reset(pData, Size);
CMsgPacker Packer(NETMSG_EX, true);
2010-05-29 07:25:38 +00:00
// unpack msgid and system flag
int Msg;
bool Sys;
CUuid Uuid;
2010-05-29 07:25:38 +00:00
int Result = UnpackMessageId(&Msg, &Sys, &Uuid, &Unpacker, &Packer);
if(Result == UNPACKMESSAGE_ERROR)
{
2010-05-29 07:25:38 +00:00
return;
}
2010-05-29 07:25:38 +00:00
if(!Sys)
GameClient()->OnMessage(Msg, &Unpacker, CONN_MAIN, false);
2010-05-29 07:25:38 +00:00
}
2021-07-19 18:14:12 +00:00
void CClient::UpdateDemoIntraTimers()
{
// update timers
const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
m_aPrevGameTick[0] = pInfo->m_PreviousTick;
m_aGameIntraTick[0] = pInfo->m_IntraTick;
m_aGameTickTime[0] = pInfo->m_TickTime;
m_aGameIntraTickSincePrev[0] = pInfo->m_IntraTickSincePrev;
2021-07-19 18:14:12 +00:00
};
2010-05-29 07:25:38 +00:00
void CClient::Update()
{
PumpNetwork();
2010-05-29 07:25:38 +00:00
if(State() == IClient::STATE_DEMOPLAYBACK)
{
if(m_DemoPlayer.IsPlaying())
2020-09-06 13:43:02 +00:00
{
#if defined(CONF_VIDEORECORDER)
if(IVideo::Current())
{
IVideo::Current()->NextVideoFrame();
IVideo::Current()->NextAudioFrameTimeline([this](short *pFinalOut, unsigned Frames) {
Sound()->Mix(pFinalOut, Frames);
});
}
#endif
m_DemoPlayer.Update();
2010-05-29 07:25:38 +00:00
// update timers
const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick;
m_aPrevGameTick[0] = pInfo->m_PreviousTick;
m_aGameIntraTick[0] = pInfo->m_IntraTick;
m_aGameTickTime[0] = pInfo->m_TickTime;
2010-05-29 07:25:38 +00:00
}
else
{
// Disconnect when demo playback stopped, either due to playback error
// or because the end of the demo was reached when rendering it.
DisconnectWithReason(m_DemoPlayer.ErrorMessage());
if(m_DemoPlayer.ErrorMessage()[0] != '\0')
{
SWarning Warning(Localize("Error playing demo"), m_DemoPlayer.ErrorMessage());
Warning.m_AutoHide = false;
m_vWarnings.emplace_back(Warning);
}
2010-05-29 07:25:38 +00:00
}
}
else if(State() == IClient::STATE_ONLINE)
2010-05-29 07:25:38 +00:00
{
if(m_LastDummy != (bool)g_Config.m_ClDummy)
{
// Invalidate references to !m_ClDummy snapshots
GameClient()->InvalidateSnapshot();
GameClient()->OnDummySwap();
}
if(m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT])
{
// switch dummy snapshot
int64_t Now = m_aGameTime[!g_Config.m_ClDummy].Get(time_get());
2022-02-14 23:12:52 +00:00
while(true)
{
if(!m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext)
break;
int64_t TickStart = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
if(TickStart >= Now)
break;
m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT];
m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext;
// set ticks
m_aCurGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick;
m_aPrevGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV]->m_Tick;
}
}
if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT])
2010-05-29 07:25:38 +00:00
{
// switch snapshot
2022-04-08 17:40:28 +00:00
bool Repredict = false;
int64_t Now = m_aGameTime[g_Config.m_ClDummy].Get(time_get());
2021-06-23 05:05:49 +00:00
int64_t PredNow = m_PredictedTime.Get(time_get());
2010-05-29 07:25:38 +00:00
if(m_LastDummy != (bool)g_Config.m_ClDummy && m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV])
2010-05-29 07:25:38 +00:00
{
// Load snapshot for m_ClDummy
GameClient()->OnNewSnapshot();
2022-04-08 17:40:28 +00:00
Repredict = true;
}
2010-05-29 07:25:38 +00:00
2022-02-14 23:12:52 +00:00
while(true)
{
if(!m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext)
break;
int64_t TickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
if(TickStart >= Now)
break;
2010-05-29 07:25:38 +00:00
m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT];
m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext;
// set ticks
m_aCurGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick;
m_aPrevGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick;
GameClient()->OnNewSnapshot();
Repredict = true;
2010-05-29 07:25:38 +00:00
}
if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV])
{
int64_t CurTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed();
int64_t PrevTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick * time_freq() / GameTickSpeed();
int PrevPredTick = (int)(PredNow * GameTickSpeed() / time_freq());
int NewPredTick = PrevPredTick + 1;
2010-05-29 07:25:38 +00:00
m_aGameIntraTick[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)(CurTickStart - PrevTickStart);
m_aGameTickTime[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)time_freq();
m_aGameIntraTickSincePrev[g_Config.m_ClDummy] = (Now - PrevTickStart) / (float)(time_freq() / GameTickSpeed());
2010-05-29 07:25:38 +00:00
int64_t CurPredTickStart = NewPredTick * time_freq() / GameTickSpeed();
int64_t PrevPredTickStart = PrevPredTick * time_freq() / GameTickSpeed();
m_aPredIntraTick[g_Config.m_ClDummy] = (PredNow - PrevPredTickStart) / (float)(CurPredTickStart - PrevPredTickStart);
2010-05-29 07:25:38 +00:00
if(absolute(NewPredTick - m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick) > MaxLatencyTicks())
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", "prediction time reset!");
m_PredictedTime.Init(CurTickStart + 2 * time_freq() / GameTickSpeed());
}
if(NewPredTick > m_aPredTick[g_Config.m_ClDummy])
{
m_aPredTick[g_Config.m_ClDummy] = NewPredTick;
2022-04-08 17:40:28 +00:00
Repredict = true;
// send input
SendInput();
}
2010-05-29 07:25:38 +00:00
}
// only do sane predictions
if(Repredict)
2010-05-29 07:25:38 +00:00
{
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())
GameClient()->OnPredict();
2010-05-29 07:25:38 +00:00
}
// fetch server info if we don't have it
if(m_CurrentServerInfoRequestTime >= 0 &&
time_get() > m_CurrentServerInfoRequestTime)
{
m_ServerBrowser.RequestCurrentServer(ServerAddress());
m_CurrentServerInfoRequestTime = time_get() + time_freq() * 2;
}
// periodically ping server
if(m_CurrentServerNextPingTime >= 0 &&
time_get() > m_CurrentServerNextPingTime)
{
int64_t NowPing = time_get();
2021-06-23 05:05:49 +00:00
int64_t Freq = time_freq();
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "pinging current server%s", !m_ServerCapabilities.m_PingEx ? ", using fallback via server info" : "");
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf);
m_CurrentServerPingUuid = RandomUuid();
if(!m_ServerCapabilities.m_PingEx)
{
m_ServerBrowser.RequestCurrentServerWithRandomToken(ServerAddress(), &m_CurrentServerPingBasicToken, &m_CurrentServerPingToken);
}
else
{
CMsgPacker Msg(NETMSG_PINGEX, true);
Msg.AddRaw(&m_CurrentServerPingUuid, sizeof(m_CurrentServerPingUuid));
SendMsg(CONN_MAIN, &Msg, MSGFLAG_FLUSH);
}
m_CurrentServerCurrentPingTime = NowPing;
m_CurrentServerNextPingTime = NowPing + 600 * Freq; // ping every 10 minutes
}
2010-05-29 07:25:38 +00:00
}
m_LastDummy = (bool)g_Config.m_ClDummy;
2010-05-29 07:25:38 +00:00
}
// STRESS TEST: join the server again
#ifdef CONF_DEBUG
2010-05-29 07:25:38 +00:00
if(g_Config.m_DbgStress)
{
static int64_t s_ActionTaken = 0;
2021-06-23 05:05:49 +00:00
int64_t Now = time_get();
2010-05-29 07:25:38 +00:00
if(State() == IClient::STATE_OFFLINE)
{
if(Now > s_ActionTaken + time_freq() * 2)
2010-05-29 07:25:38 +00:00
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "stress", "reconnecting!");
2010-05-29 07:25:38 +00:00
Connect(g_Config.m_DbgStressServer);
s_ActionTaken = Now;
2010-05-29 07:25:38 +00:00
}
}
else
{
if(Now > s_ActionTaken + time_freq() * (10 + g_Config.m_DbgStress))
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "stress", "disconnecting!");
Disconnect();
s_ActionTaken = Now;
}
}
2010-05-29 07:25:38 +00:00
}
#endif
2010-05-29 07:25:38 +00:00
if(m_pMapdownloadTask)
{
if(m_pMapdownloadTask->State() == EHttpState::DONE)
FinishMapDownload();
else if(m_pMapdownloadTask->State() == EHttpState::ERROR || m_pMapdownloadTask->State() == EHttpState::ABORTED)
{
dbg_msg("webdl", "http failed, falling back to gameserver");
ResetMapDownload();
SendMapRequest();
}
}
if(m_pDDNetInfoTask)
{
if(m_pDDNetInfoTask->State() == EHttpState::DONE)
{
FinishDDNetInfo();
ResetDDNetInfoTask();
}
else if(m_pDDNetInfoTask->State() == EHttpState::ERROR || m_pDDNetInfoTask->State() == EHttpState::ABORTED)
{
ResetDDNetInfoTask();
}
}
2010-05-29 07:25:38 +00:00
if(State() == IClient::STATE_ONLINE)
{
if(!m_EditJobs.empty())
{
std::shared_ptr<CDemoEdit> pJob = m_EditJobs.front();
Allow background jobs to be aborted, refactoring Add `IJob::Abortable(bool)` function which jobs can call to specify whether they can be aborted. Jobs are not abortable per default. Abortable jobs may have their state set to `IJob::STATE_ABORTED` at any point if the job was aborted. The job state should be checked periodically in the `IJob::Run` function and the job should terminate at the earliest, safe opportunity when aborted. Scheduled jobs which are not abortable are guaranteed to fully complete before the job pool is shut down. However, if the job pool is already shutting down, no additional jobs will be enqueue anymore and abortable jobs will immediately be aborted. In particular, the sound loading, community icon loading, master chooser and host lookup jobs are specified as being abortable. Conversely, the jobs saving replay demos, editor maps and screenshots are expected to finish before the client is shut down. When the client is quitting/restarting, it will now disconnect from the current server first, before saving the config, to ensure that any actions that happen on disconnect (demo recorders being stopped etc.) happen first. The shutdown message is rendered before disconnecting and waiting for background jobs to finish. The HTTP client is now initialized later during server launch, after the network initialization. Error handling is added and the server stops if the HTTP client could not be initialized, same as the client. The `RunBlocking` functions are removed, as they are not used anymore after curl-multi was added. The function `IJob::Status` is renamed to `State` and `IJob::STATE_PENDING` is renamed to `STATE_QUEUED` for consistency with naming of the HTTP client. The member variables of the engine interface are encapsulated and the `jobs.h` include is removed from `engine.h`, which removes transitive includes of `system.h`. Documentation for all job and job pool API is added.
2024-02-16 19:44:33 +00:00
if(pJob->State() == IJob::STATE_DONE)
{
char aBuf[IO_MAX_PATH_LENGTH + 64];
2024-02-26 17:44:11 +00:00
if(pJob->Success())
{
str_format(aBuf, sizeof(aBuf), "Successfully saved the replay to '%s'!", pJob->Destination());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", aBuf);
2024-02-26 17:44:11 +00:00
GameClient()->Echo(Localize("Successfully saved the replay!"));
}
else
{
str_format(aBuf, sizeof(aBuf), "Failed saving the replay to '%s'...", pJob->Destination());
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", aBuf);
2024-02-26 17:44:11 +00:00
GameClient()->Echo(Localize("Failed saving the replay!"));
}
m_EditJobs.pop_front();
}
}
}
2010-05-29 07:25:38 +00:00
// update the server browser
m_ServerBrowser.Update();
// update editor/gameclient
if(m_EditorActive)
m_pEditor->OnUpdate();
else
GameClient()->OnUpdate();
Discord()->Update();
Steam()->Update();
if(Steam()->GetConnectAddress())
{
HandleConnectAddress(Steam()->GetConnectAddress());
Steam()->ClearConnectAddress();
}
if(m_ReconnectTime > 0 && time_get() > m_ReconnectTime)
{
2021-04-15 10:44:24 +00:00
if(State() != STATE_ONLINE)
Connect(m_aConnectAddressStr);
m_ReconnectTime = 0;
}
2022-01-09 12:06:41 +00:00
m_PredictedTime.UpdateMargin(PredictionMargin() * time_freq() / 1000);
2010-05-29 07:25:38 +00:00
}
void CClient::RegisterInterfaces()
{
Kernel()->RegisterInterface(static_cast<IDemoRecorder *>(&m_aDemoRecorder[RECORDER_MANUAL]), false);
Kernel()->RegisterInterface(static_cast<IDemoPlayer *>(&m_DemoPlayer), false);
Kernel()->RegisterInterface(static_cast<IGhostRecorder *>(&m_GhostRecorder), false);
Kernel()->RegisterInterface(static_cast<IGhostLoader *>(&m_GhostLoader), false);
Kernel()->RegisterInterface(static_cast<IServerBrowser *>(&m_ServerBrowser), false);
#if defined(CONF_AUTOUPDATE)
Kernel()->RegisterInterface(static_cast<IUpdater *>(&m_Updater), false);
2015-02-27 21:08:34 +00:00
#endif
Kernel()->RegisterInterface(static_cast<IFriends *>(&m_Friends), false);
Kernel()->ReregisterInterface(static_cast<IFriends *>(&m_Foes));
2023-12-18 19:01:26 +00:00
Kernel()->RegisterInterface(static_cast<IHttp *>(&m_Http), false);
2010-05-29 07:25:38 +00:00
}
void CClient::InitInterfaces()
{
// fetch interfaces
2011-02-27 14:03:57 +00:00
m_pEngine = Kernel()->RequestInterface<IEngine>();
2010-05-29 07:25:38 +00:00
m_pEditor = Kernel()->RequestInterface<IEditor>();
m_pFavorites = Kernel()->RequestInterface<IFavorites>();
2010-05-29 07:25:38 +00:00
m_pSound = Kernel()->RequestInterface<IEngineSound>();
m_pGameClient = Kernel()->RequestInterface<IGameClient>();
m_pInput = Kernel()->RequestInterface<IEngineInput>();
m_pMap = Kernel()->RequestInterface<IEngineMap>();
m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
m_pConfig = m_pConfigManager->Values();
#if defined(CONF_AUTOUPDATE)
m_pUpdater = Kernel()->RequestInterface<IUpdater>();
2015-02-27 21:08:34 +00:00
#endif
m_pDiscord = Kernel()->RequestInterface<IDiscord>();
2020-08-20 10:17:44 +00:00
m_pSteam = Kernel()->RequestInterface<ISteam>();
m_pNotifications = Kernel()->RequestInterface<INotifications>();
2010-05-29 07:25:38 +00:00
m_pStorage = Kernel()->RequestInterface<IStorage>();
2014-08-12 14:21:06 +00:00
m_DemoEditor.Init(m_pGameClient->NetVersion(), &m_SnapshotDelta, m_pConsole, m_pStorage);
2015-07-09 00:08:14 +00:00
2023-12-18 19:01:26 +00:00
m_ServerBrowser.SetBaseInfo(&m_aNetClient[CONN_CONTACT], m_pGameClient->NetVersion());
2015-02-27 21:08:34 +00:00
#if defined(CONF_AUTOUPDATE)
2023-12-18 19:01:26 +00:00
m_Updater.Init(&m_Http);
2015-02-27 21:08:34 +00:00
#endif
m_pConfigManager->RegisterCallback(IFavorites::ConfigSaveCallback, m_pFavorites);
2011-03-23 12:06:35 +00:00
m_Friends.Init();
2015-07-22 20:16:49 +00:00
m_Foes.Init(true);
m_GhostRecorder.Init();
m_GhostLoader.Init();
2010-05-29 07:25:38 +00:00
}
void CClient::Run()
{
m_LocalStartTime = m_GlobalStartTime = time_get();
#if defined(CONF_VIDEORECORDER)
IVideo::SetLocalStartTime(m_LocalStartTime);
#endif
m_aSnapshotParts[0] = 0;
m_aSnapshotParts[1] = 0;
2010-05-29 07:25:38 +00:00
if(m_GenerateTimeoutSeed)
{
GenerateTimeoutSeed();
}
unsigned int Seed;
secure_random_fill(&Seed, sizeof(Seed));
srand(Seed);
2014-08-13 10:58:53 +00:00
if(g_Config.m_Debug)
{
g_UuidManager.DebugDump();
}
2017-09-01 06:01:26 +00:00
// init graphics
m_pGraphics = CreateEngineGraphicsThreaded();
Kernel()->RegisterInterface(m_pGraphics); // IEngineGraphics
Kernel()->RegisterInterface(static_cast<IGraphics *>(m_pGraphics), false);
if(m_pGraphics->Init() != 0)
2017-09-01 06:01:26 +00:00
{
log_error("client", "couldn't init graphics");
ShowMessageBox("Graphics Error", "The graphics could not be initialized.");
return;
2017-09-01 06:01:26 +00:00
}
2020-10-11 09:09:24 +00:00
// make sure the first frame just clears everything to prevent undesired colors when waiting for io
Graphics()->Clear(0, 0, 0);
2020-10-11 09:09:24 +00:00
Graphics()->Swap();
// init sound, allowed to fail
const bool SoundInitFailed = Sound()->Init() != 0;
2010-05-29 07:25:38 +00:00
2016-08-27 19:10:27 +00:00
#if defined(CONF_VIDEORECORDER)
// init video recorder aka ffmpeg
CVideo::Init();
#endif
2022-03-25 08:26:37 +00:00
#ifndef CONF_WEBASM
char aNetworkError[256];
if(!InitNetworkClient(aNetworkError, sizeof(aNetworkError)))
{
log_error("client", "%s", aNetworkError);
ShowMessageBox("Network Error", aNetworkError);
2023-04-01 08:57:06 +00:00
return;
}
2022-03-25 08:26:37 +00:00
#endif
if(!m_Http.Init(std::chrono::seconds{1}))
{
const char *pErrorMessage = "Failed to initialize the HTTP client.";
log_error("client", "%s", pErrorMessage);
ShowMessageBox("HTTP Error", pErrorMessage);
return;
}
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
// init text render
m_pTextRender = Kernel()->RequestInterface<IEngineTextRender>();
m_pTextRender->Init();
2010-05-29 07:25:38 +00:00
// init the input
Input()->Init();
// init the editor
m_pEditor->Init();
2010-05-29 07:25:38 +00:00
m_ServerBrowser.OnInit();
// loads the existing ddnet info file if it exists
LoadDDNetInfo();
LoadDebugFont();
2010-05-29 07:25:38 +00:00
if(Steam()->GetPlayerName())
{
2022-07-09 16:14:56 +00:00
str_copy(g_Config.m_SteamName, Steam()->GetPlayerName());
}
Graphics()->AddWindowResizeListener([this] { OnWindowResize(); });
2010-05-29 07:25:38 +00:00
GameClient()->OnInit();
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", "version " GAME_RELEASE_VERSION " on " CONF_PLATFORM_STRING " " CONF_ARCH_STRING, ColorRGBA(0.7f, 0.7f, 1.0f, 1.0f));
if(GIT_SHORTREV_HASH)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "git revision hash: %s", GIT_SHORTREV_HASH);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, ColorRGBA(0.7f, 0.7f, 1.0f, 1.0f));
}
2010-05-29 07:25:38 +00:00
//
m_FpsGraph.Init(0.0f, 120.0f);
2010-05-29 07:25:38 +00:00
// never start with the editor
g_Config.m_ClEditor = 0;
// process pending commands
m_pConsole->StoreCommands(false);
2015-07-09 00:08:14 +00:00
2016-05-02 21:36:21 +00:00
m_Fifo.Init(m_pConsole, g_Config.m_ClInputFifo, CFGFLAG_CLIENT);
InitChecksum();
m_pConsole->InitChecksum(ChecksumData());
// request the new ddnet info from server if already past the welcome dialog
if(g_Config.m_ClShowWelcome)
g_Config.m_ClShowWelcome = 0;
else
RequestDDNetInfo();
if(SoundInitFailed)
{
SWarning Warning(Localize("Sound error"), Localize("The audio device couldn't be initialised."));
Warning.m_AutoHide = false;
m_vWarnings.emplace_back(Warning);
}
bool LastD = false;
bool LastE = false;
bool LastG = false;
auto LastTime = time_get_nanoseconds();
2021-06-23 05:05:49 +00:00
int64_t LastRenderTime = time_get();
2022-02-14 23:12:52 +00:00
while(true)
2010-05-29 07:25:38 +00:00
{
set_new_tick();
2010-05-29 07:25:38 +00:00
// handle pending connects
if(m_aCmdConnect[0])
{
2022-07-09 16:14:56 +00:00
str_copy(g_Config.m_UiServerAddress, m_aCmdConnect);
2010-05-29 07:25:38 +00:00
Connect(m_aCmdConnect);
m_aCmdConnect[0] = 0;
}
// handle pending demo play
if(m_aCmdPlayDemo[0])
{
const char *pError = DemoPlayer_Play(m_aCmdPlayDemo, IStorage::TYPE_ALL_OR_ABSOLUTE);
2019-04-10 06:56:20 +00:00
if(pError)
log_error("demo_player", "playing passed demo file '%s' failed: %s", m_aCmdPlayDemo, pError);
m_aCmdPlayDemo[0] = 0;
}
// handle pending map edits
if(m_aCmdEditMap[0])
{
2023-08-26 20:13:13 +00:00
int Result = m_pEditor->HandleMapDrop(m_aCmdEditMap, IStorage::TYPE_ALL_OR_ABSOLUTE);
if(Result)
g_Config.m_ClEditor = true;
else
log_error("editor", "editing passed map file '%s' failed", m_aCmdEditMap);
m_aCmdEditMap[0] = 0;
}
// progress on dummy connect when the connection is online
if(m_DummySendConnInfo && m_aNetClient[CONN_DUMMY].State() == NETSTATE_ONLINE)
{
m_DummySendConnInfo = false;
SendInfo(CONN_DUMMY);
m_aNetClient[CONN_DUMMY].Update();
SendReady(CONN_DUMMY);
GameClient()->SendDummyInfo(true);
SendEnterGame(CONN_DUMMY);
}
2010-05-29 07:25:38 +00:00
// update input
if(Input()->Update())
{
if(State() == IClient::STATE_QUITTING)
break;
else
SetState(IClient::STATE_QUITTING); // SDL_QUIT
}
char aFile[IO_MAX_PATH_LENGTH];
if(Input()->GetDropFile(aFile, sizeof(aFile)))
{
if(str_startswith(aFile, CONNECTLINK_NO_SLASH))
HandleConnectLink(aFile);
else if(str_endswith(aFile, ".demo"))
HandleDemoPath(aFile);
else if(str_endswith(aFile, ".map"))
HandleMapPath(aFile);
}
#if defined(CONF_AUTOUPDATE)
Updater()->Update();
2015-02-27 21:08:34 +00:00
#endif
2014-12-31 14:29:34 +00:00
2010-05-29 07:25:38 +00:00
// update sound
Sound()->Update();
2015-08-24 20:46:28 +00:00
if(CtrlShiftKey(KEY_D, LastD))
2010-05-29 07:25:38 +00:00
g_Config.m_Debug ^= 1;
2015-08-24 20:46:28 +00:00
if(CtrlShiftKey(KEY_G, LastG))
2010-05-29 07:25:38 +00:00
g_Config.m_DbgGraphs ^= 1;
2015-08-24 20:46:28 +00:00
if(CtrlShiftKey(KEY_E, LastE))
2023-10-18 11:27:31 +00:00
{
if(g_Config.m_ClEditor)
m_pEditor->OnClose();
g_Config.m_ClEditor = g_Config.m_ClEditor ^ 1;
2023-10-18 11:27:31 +00:00
}
2010-05-29 07:25:38 +00:00
// render
{
if(g_Config.m_ClEditor)
{
if(!m_EditorActive)
{
Input()->MouseModeRelative();
GameClient()->OnActivateEditor();
m_pEditor->OnActivate();
m_EditorActive = true;
}
}
else if(m_EditorActive)
{
m_EditorActive = false;
}
2010-05-29 07:25:38 +00:00
Update();
2021-06-23 05:05:49 +00:00
int64_t Now = time_get();
2015-07-09 00:08:14 +00:00
bool IsRenderActive = (g_Config.m_GfxBackgroundRender || m_pGraphics->WindowOpen());
bool AsyncRenderOld = g_Config.m_GfxAsyncRenderOld;
2021-12-20 16:26:48 +00:00
2022-03-02 08:32:51 +00:00
int GfxRefreshRate = g_Config.m_GfxRefreshRate;
#if defined(CONF_VIDEORECORDER)
// keep rendering synced
if(IVideo::Current())
{
AsyncRenderOld = false;
GfxRefreshRate = 0;
}
#endif
if(IsRenderActive &&
2021-12-20 16:26:48 +00:00
(!AsyncRenderOld || m_pGraphics->IsIdle()) &&
2022-03-02 08:32:51 +00:00
(!GfxRefreshRate || (time_freq() / (int64_t)g_Config.m_GfxRefreshRate) <= Now - LastRenderTime))
2010-05-29 07:25:38 +00:00
{
// update frametime
m_RenderFrameTime = (Now - m_LastRenderTime) / (float)time_freq();
m_FpsGraph.Add(1.0f / m_RenderFrameTime);
2020-11-19 15:15:07 +00:00
if(m_BenchmarkFile)
{
char aBuf[64];
str_format(aBuf, sizeof(aBuf), "Frametime %d us\n", (int)(m_RenderFrameTime * 1000000));
2021-06-05 14:46:01 +00:00
io_write(m_BenchmarkFile, aBuf, str_length(aBuf));
2020-11-19 15:15:07 +00:00
if(time_get() > m_BenchmarkStopTime)
{
io_close(m_BenchmarkFile);
m_BenchmarkFile = 0;
Quit();
}
}
m_FrameTimeAvg = m_FrameTimeAvg * 0.9f + m_RenderFrameTime * 0.1f;
// keep the overflow time - it's used to make sure the gfx refreshrate is reached
2021-06-23 05:05:49 +00:00
int64_t AdditionalTime = g_Config.m_GfxRefreshRate ? ((Now - LastRenderTime) - (time_freq() / (int64_t)g_Config.m_GfxRefreshRate)) : 0;
// if the value is over the frametime of a 60 fps frame, reset the additional time (drop the frames, that are lost already)
if(AdditionalTime > (time_freq() / 60))
AdditionalTime = (time_freq() / 60);
LastRenderTime = Now - AdditionalTime;
m_LastRenderTime = Now;
if(!m_EditorActive)
Render();
else
2010-05-29 07:25:38 +00:00
{
m_pEditor->OnRender();
DebugRender();
2010-05-29 07:25:38 +00:00
}
m_pGraphics->Swap();
2010-05-29 07:25:38 +00:00
}
else if(!IsRenderActive)
{
// 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
2021-06-23 05:05:49 +00:00
LastRenderTime = g_Config.m_GfxRefreshRate ? (Now - (time_freq() / (int64_t)g_Config.m_GfxRefreshRate)) : Now;
}
2010-05-29 07:25:38 +00:00
}
AutoScreenshot_Cleanup();
AutoStatScreenshot_Cleanup();
AutoCSV_Cleanup();
2016-05-02 21:36:21 +00:00
m_Fifo.Update();
if(State() == IClient::STATE_QUITTING || State() == IClient::STATE_RESTARTING)
break;
2010-05-29 07:25:38 +00:00
// beNice
auto Now = time_get_nanoseconds();
2022-05-18 16:00:05 +00:00
decltype(Now) SleepTimeInNanoSeconds{0};
bool Slept = false;
if(g_Config.m_ClRefreshRateInactive && !m_pGraphics->WindowActive())
{
2022-05-18 16:00:05 +00:00
SleepTimeInNanoSeconds = (std::chrono::nanoseconds(1s) / (int64_t)g_Config.m_ClRefreshRateInactive) - (Now - LastTime);
std::this_thread::sleep_for(SleepTimeInNanoSeconds);
Slept = true;
}
else if(g_Config.m_ClRefreshRate)
{
2022-05-18 16:00:05 +00:00
SleepTimeInNanoSeconds = (std::chrono::nanoseconds(1s) / (int64_t)g_Config.m_ClRefreshRate) - (Now - LastTime);
auto SleepTimeInNanoSecondsInner = SleepTimeInNanoSeconds;
auto NowInner = Now;
while((SleepTimeInNanoSecondsInner / std::chrono::nanoseconds(1us).count()) > 0ns)
{
net_socket_read_wait(m_aNetClient[CONN_MAIN].m_Socket, SleepTimeInNanoSecondsInner);
auto NowInnerCalc = time_get_nanoseconds();
SleepTimeInNanoSecondsInner -= (NowInnerCalc - NowInner);
NowInner = NowInnerCalc;
}
Slept = true;
}
if(Slept)
{
// if the diff gets too small it shouldn't get even smaller (drop the updates, that could not be handled)
2022-05-18 16:00:05 +00:00
if(SleepTimeInNanoSeconds < -16666666ns)
SleepTimeInNanoSeconds = -16666666ns;
// don't go higher than the frametime of a 60 fps frame
2022-05-18 16:00:05 +00:00
else if(SleepTimeInNanoSeconds > 16666666ns)
SleepTimeInNanoSeconds = 16666666ns;
// the time diff between the time that was used actually used and the time the thread should sleep/wait
// will be calculated in the sleep time of the next update tick by faking the time it should have slept/wait.
// so two cases (and the case it slept exactly the time it should):
// - the thread slept/waited too long, then it adjust the time to sleep/wait less in the next update tick
// - the thread slept/waited too less, then it adjust the time to sleep/wait more in the next update tick
2022-05-18 16:00:05 +00:00
LastTime = Now + SleepTimeInNanoSeconds;
}
else
LastTime = Now;
2010-05-29 07:25:38 +00:00
// update local and global time
m_LocalTime = (time_get() - m_LocalStartTime) / (float)time_freq();
m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq();
2010-05-29 07:25:38 +00:00
}
Allow background jobs to be aborted, refactoring Add `IJob::Abortable(bool)` function which jobs can call to specify whether they can be aborted. Jobs are not abortable per default. Abortable jobs may have their state set to `IJob::STATE_ABORTED` at any point if the job was aborted. The job state should be checked periodically in the `IJob::Run` function and the job should terminate at the earliest, safe opportunity when aborted. Scheduled jobs which are not abortable are guaranteed to fully complete before the job pool is shut down. However, if the job pool is already shutting down, no additional jobs will be enqueue anymore and abortable jobs will immediately be aborted. In particular, the sound loading, community icon loading, master chooser and host lookup jobs are specified as being abortable. Conversely, the jobs saving replay demos, editor maps and screenshots are expected to finish before the client is shut down. When the client is quitting/restarting, it will now disconnect from the current server first, before saving the config, to ensure that any actions that happen on disconnect (demo recorders being stopped etc.) happen first. The shutdown message is rendered before disconnecting and waiting for background jobs to finish. The HTTP client is now initialized later during server launch, after the network initialization. Error handling is added and the server stops if the HTTP client could not be initialized, same as the client. The `RunBlocking` functions are removed, as they are not used anymore after curl-multi was added. The function `IJob::Status` is renamed to `State` and `IJob::STATE_PENDING` is renamed to `STATE_QUEUED` for consistency with naming of the HTTP client. The member variables of the engine interface are encapsulated and the `jobs.h` include is removed from `engine.h`, which removes transitive includes of `system.h`. Documentation for all job and job pool API is added.
2024-02-16 19:44:33 +00:00
GameClient()->RenderShutdownMessage();
Disconnect();
if(!m_pConfigManager->Save())
{
char aError[128];
str_format(aError, sizeof(aError), Localize("Saving settings to '%s' failed"), CONFIG_FILE);
m_vQuittingWarnings.emplace_back(Localize("Error saving settings"), aError);
}
2016-05-02 21:36:21 +00:00
m_Fifo.Shutdown();
m_Http.Shutdown();
Allow background jobs to be aborted, refactoring Add `IJob::Abortable(bool)` function which jobs can call to specify whether they can be aborted. Jobs are not abortable per default. Abortable jobs may have their state set to `IJob::STATE_ABORTED` at any point if the job was aborted. The job state should be checked periodically in the `IJob::Run` function and the job should terminate at the earliest, safe opportunity when aborted. Scheduled jobs which are not abortable are guaranteed to fully complete before the job pool is shut down. However, if the job pool is already shutting down, no additional jobs will be enqueue anymore and abortable jobs will immediately be aborted. In particular, the sound loading, community icon loading, master chooser and host lookup jobs are specified as being abortable. Conversely, the jobs saving replay demos, editor maps and screenshots are expected to finish before the client is shut down. When the client is quitting/restarting, it will now disconnect from the current server first, before saving the config, to ensure that any actions that happen on disconnect (demo recorders being stopped etc.) happen first. The shutdown message is rendered before disconnecting and waiting for background jobs to finish. The HTTP client is now initialized later during server launch, after the network initialization. Error handling is added and the server stops if the HTTP client could not be initialized, same as the client. The `RunBlocking` functions are removed, as they are not used anymore after curl-multi was added. The function `IJob::Status` is renamed to `State` and `IJob::STATE_PENDING` is renamed to `STATE_QUEUED` for consistency with naming of the HTTP client. The member variables of the engine interface are encapsulated and the `jobs.h` include is removed from `engine.h`, which removes transitive includes of `system.h`. Documentation for all job and job pool API is added.
2024-02-16 19:44:33 +00:00
Engine()->ShutdownJobs();
GameClient()->RenderShutdownMessage();
2010-05-29 07:25:38 +00:00
GameClient()->OnShutdown();
Allow background jobs to be aborted, refactoring Add `IJob::Abortable(bool)` function which jobs can call to specify whether they can be aborted. Jobs are not abortable per default. Abortable jobs may have their state set to `IJob::STATE_ABORTED` at any point if the job was aborted. The job state should be checked periodically in the `IJob::Run` function and the job should terminate at the earliest, safe opportunity when aborted. Scheduled jobs which are not abortable are guaranteed to fully complete before the job pool is shut down. However, if the job pool is already shutting down, no additional jobs will be enqueue anymore and abortable jobs will immediately be aborted. In particular, the sound loading, community icon loading, master chooser and host lookup jobs are specified as being abortable. Conversely, the jobs saving replay demos, editor maps and screenshots are expected to finish before the client is shut down. When the client is quitting/restarting, it will now disconnect from the current server first, before saving the config, to ensure that any actions that happen on disconnect (demo recorders being stopped etc.) happen first. The shutdown message is rendered before disconnecting and waiting for background jobs to finish. The HTTP client is now initialized later during server launch, after the network initialization. Error handling is added and the server stops if the HTTP client could not be initialized, same as the client. The `RunBlocking` functions are removed, as they are not used anymore after curl-multi was added. The function `IJob::Status` is renamed to `State` and `IJob::STATE_PENDING` is renamed to `STATE_QUEUED` for consistency with naming of the HTTP client. The member variables of the engine interface are encapsulated and the `jobs.h` include is removed from `engine.h`, which removes transitive includes of `system.h`. Documentation for all job and job pool API is added.
2024-02-16 19:44:33 +00:00
delete m_pEditor;
2010-05-29 07:25:38 +00:00
Allow background jobs to be aborted, refactoring Add `IJob::Abortable(bool)` function which jobs can call to specify whether they can be aborted. Jobs are not abortable per default. Abortable jobs may have their state set to `IJob::STATE_ABORTED` at any point if the job was aborted. The job state should be checked periodically in the `IJob::Run` function and the job should terminate at the earliest, safe opportunity when aborted. Scheduled jobs which are not abortable are guaranteed to fully complete before the job pool is shut down. However, if the job pool is already shutting down, no additional jobs will be enqueue anymore and abortable jobs will immediately be aborted. In particular, the sound loading, community icon loading, master chooser and host lookup jobs are specified as being abortable. Conversely, the jobs saving replay demos, editor maps and screenshots are expected to finish before the client is shut down. When the client is quitting/restarting, it will now disconnect from the current server first, before saving the config, to ensure that any actions that happen on disconnect (demo recorders being stopped etc.) happen first. The shutdown message is rendered before disconnecting and waiting for background jobs to finish. The HTTP client is now initialized later during server launch, after the network initialization. Error handling is added and the server stops if the HTTP client could not be initialized, same as the client. The `RunBlocking` functions are removed, as they are not used anymore after curl-multi was added. The function `IJob::Status` is renamed to `State` and `IJob::STATE_PENDING` is renamed to `STATE_QUEUED` for consistency with naming of the HTTP client. The member variables of the engine interface are encapsulated and the `jobs.h` include is removed from `engine.h`, which removes transitive includes of `system.h`. Documentation for all job and job pool API is added.
2024-02-16 19:44:33 +00:00
// close sockets
for(unsigned int i = 0; i < std::size(m_aNetClient); i++)
m_aNetClient[i].Close();
2022-04-14 09:50:10 +00:00
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
// shutdown text render while graphics are still available
m_pTextRender->Shutdown();
2010-05-29 07:25:38 +00:00
}
bool CClient::InitNetworkClient(char *pError, size_t ErrorSize)
2023-04-01 08:57:06 +00:00
{
NETADDR BindAddr;
if(g_Config.m_Bindaddr[0] == '\0')
{
mem_zero(&BindAddr, sizeof(BindAddr));
}
else if(net_host_lookup(g_Config.m_Bindaddr, &BindAddr, NETTYPE_ALL) != 0)
{
str_format(pError, ErrorSize, "The configured bindaddr '%s' cannot be resolved.", g_Config.m_Bindaddr);
2023-04-01 08:57:06 +00:00
return false;
}
BindAddr.type = NETTYPE_ALL;
for(unsigned int i = 0; i < std::size(m_aNetClient); i++)
{
int &PortRef = i == CONN_MAIN ? g_Config.m_ClPort : i == CONN_DUMMY ? g_Config.m_ClDummyPort : g_Config.m_ClContactPort;
if(PortRef < 1024) // Reject users setting ports that we don't want to use
{
PortRef = 0;
}
BindAddr.port = PortRef;
unsigned RemainingAttempts = 25;
while(BindAddr.port == 0 || !m_aNetClient[i].Open(BindAddr))
{
if(BindAddr.port != 0)
{
--RemainingAttempts;
if(RemainingAttempts == 0)
{
if(g_Config.m_Bindaddr[0])
str_format(pError, ErrorSize, "Could not open the network client, try changing or unsetting the bindaddr '%s'.", g_Config.m_Bindaddr);
2023-04-01 08:57:06 +00:00
else
str_format(pError, ErrorSize, "Could not open the network client.");
2023-04-01 08:57:06 +00:00
return false;
}
}
BindAddr.port = (secure_rand() % 64511) + 1024;
}
}
return true;
}
bool CClient::CtrlShiftKey(int Key, bool &Last)
{
if(Input()->ModifierIsPressed() && Input()->ShiftIsPressed() && !Last && Input()->KeyIsPressed(Key))
{
Last = true;
return true;
}
2018-03-12 14:43:31 +00:00
else if(Last && !Input()->KeyIsPressed(Key))
Last = false;
return false;
}
2010-05-29 07:25:38 +00:00
void CClient::Con_Connect(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->HandleConnectLink(pResult->GetString(0));
2010-05-29 07:25:38 +00:00
}
void CClient::Con_Disconnect(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->Disconnect();
}
void CClient::Con_DummyConnect(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
2014-04-28 13:19:57 +00:00
pSelf->DummyConnect();
}
void CClient::Con_DummyDisconnect(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->DummyDisconnect(0);
}
2021-03-17 15:09:39 +00:00
void CClient::Con_DummyResetInput(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->GameClient()->DummyResetInput();
}
void CClient::Con_Quit(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->Quit();
}
2023-05-09 21:39:31 +00:00
void CClient::Con_Restart(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->Restart();
}
void CClient::Con_Minimize(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->Graphics()->Minimize();
}
void CClient::Con_Ping(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
CMsgPacker Msg(NETMSG_PING, true);
pSelf->SendMsg(CONN_MAIN, &Msg, MSGFLAG_FLUSH);
2010-05-29 07:25:38 +00:00
pSelf->m_PingStartTime = time_get();
}
void CClient::AutoScreenshot_Start()
{
if(g_Config.m_ClAutoScreenshot)
{
Graphics()->TakeScreenshot("auto/autoscreen");
m_AutoScreenshotRecycle = true;
}
}
2015-05-19 22:51:02 +00:00
void CClient::AutoStatScreenshot_Start()
{
if(g_Config.m_ClAutoStatboardScreenshot)
2015-05-19 22:51:02 +00:00
{
Graphics()->TakeScreenshot("auto/stats/autoscreen");
m_AutoStatScreenshotRecycle = true;
}
}
void CClient::AutoScreenshot_Cleanup()
{
if(m_AutoScreenshotRecycle)
{
if(g_Config.m_ClAutoScreenshotMax)
{
// clean up auto taken screens
CFileCollection AutoScreens;
AutoScreens.Init(Storage(), "screenshots/auto", "autoscreen", ".png", g_Config.m_ClAutoScreenshotMax);
}
m_AutoScreenshotRecycle = false;
}
}
2015-05-19 22:51:02 +00:00
void CClient::AutoStatScreenshot_Cleanup()
{
if(m_AutoStatScreenshotRecycle)
{
if(g_Config.m_ClAutoStatboardScreenshotMax)
2015-05-19 22:51:02 +00:00
{
// clean up auto taken screens
CFileCollection AutoScreens;
AutoScreens.Init(Storage(), "screenshots/auto/stats", "autoscreen", ".png", g_Config.m_ClAutoStatboardScreenshotMax);
2015-05-19 22:51:02 +00:00
}
m_AutoStatScreenshotRecycle = false;
}
}
void CClient::AutoCSV_Start()
{
2018-03-12 14:43:31 +00:00
if(g_Config.m_ClAutoCSV)
m_AutoCSVRecycle = true;
}
void CClient::AutoCSV_Cleanup()
{
2018-03-12 14:43:31 +00:00
if(m_AutoCSVRecycle)
{
2018-03-12 14:43:31 +00:00
if(g_Config.m_ClAutoCSVMax)
{
// clean up auto csvs
CFileCollection AutoRecord;
AutoRecord.Init(Storage(), "record/csv", "autorecord", ".csv", g_Config.m_ClAutoCSVMax);
}
m_AutoCSVRecycle = false;
}
}
void CClient::Con_Screenshot(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->Graphics()->TakeScreenshot(0);
2010-05-29 07:25:38 +00:00
}
2016-08-27 19:10:27 +00:00
#if defined(CONF_VIDEORECORDER)
2016-08-27 15:51:23 +00:00
void CClient::Con_StartVideo(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = static_cast<CClient *>(pUserData);
2016-08-27 15:51:23 +00:00
if(pResult->NumArguments())
{
pSelf->StartVideo(pResult->GetString(0), false);
}
else
2016-08-27 15:51:23 +00:00
{
pSelf->StartVideo("video", true);
}
}
void CClient::StartVideo(const char *pFilename, bool WithTimestamp)
{
if(State() != IClient::STATE_DEMOPLAYBACK)
{
log_error("videorecorder", "Video can only be recorded in demo player.");
return;
}
if(IVideo::Current())
{
log_error("videorecorder", "Already recording.");
return;
}
char aFilename[IO_MAX_PATH_LENGTH];
if(WithTimestamp)
{
char aTimestamp[20];
str_timestamp(aTimestamp, sizeof(aTimestamp));
str_format(aFilename, sizeof(aFilename), "videos/%s_%s.mp4", pFilename, aTimestamp);
2016-08-27 15:51:23 +00:00
}
else
{
str_format(aFilename, sizeof(aFilename), "videos/%s.mp4", pFilename);
}
// wait for idle, so there is no data race
Graphics()->WaitForIdle();
// pause the sound device while creating the video instance
Sound()->PauseAudioDevice();
new CVideo(Graphics(), Sound(), Storage(), Graphics()->ScreenWidth(), Graphics()->ScreenHeight(), aFilename);
Sound()->UnpauseAudioDevice();
if(!IVideo::Current()->Start())
{
log_error("videorecorder", "Failed to start recording to '%s'", aFilename);
m_DemoPlayer.Stop("Failed to start video recording. See local console for details.");
return;
}
if(m_DemoPlayer.Info()->m_Info.m_Paused)
{
IVideo::Current()->Pause(true);
}
log_info("videorecorder", "Recording to '%s'", aFilename);
2016-08-27 15:51:23 +00:00
}
void CClient::Con_StopVideo(IConsole::IResult *pResult, void *pUserData)
{
if(!IVideo::Current())
{
log_error("videorecorder", "Not recording.");
return;
}
IVideo::Current()->Stop();
log_info("videorecorder", "Stopped recording.");
2016-08-27 15:51:23 +00:00
}
2016-08-27 19:10:27 +00:00
#endif
void CClient::Con_Rcon(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->Rcon(pResult->GetString(0));
}
void CClient::Con_RconAuth(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->RconAuth("", pResult->GetString(0));
}
2017-03-06 09:31:05 +00:00
void CClient::Con_RconLogin(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->RconAuth(pResult->GetString(0), pResult->GetString(1));
}
void CClient::Con_BeginFavoriteGroup(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->m_FavoritesGroup)
{
log_error("client", "opening favorites group while there is already one, discarding old one");
for(int i = 0; i < pSelf->m_FavoritesGroupNum; i++)
{
char aAddr[NETADDR_MAXSTRSIZE];
net_addr_str(&pSelf->m_aFavoritesGroupAddresses[i], aAddr, sizeof(aAddr), true);
log_warn("client", "discarding %s", aAddr);
}
}
pSelf->m_FavoritesGroup = true;
pSelf->m_FavoritesGroupAllowPing = false;
pSelf->m_FavoritesGroupNum = 0;
}
void CClient::Con_EndFavoriteGroup(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(!pSelf->m_FavoritesGroup)
{
log_error("client", "closing favorites group while there is none, ignoring");
return;
}
log_info("client", "adding group of %d favorites", pSelf->m_FavoritesGroupNum);
pSelf->m_pFavorites->Add(pSelf->m_aFavoritesGroupAddresses, pSelf->m_FavoritesGroupNum);
if(pSelf->m_FavoritesGroupAllowPing)
{
pSelf->m_pFavorites->AllowPing(pSelf->m_aFavoritesGroupAddresses, pSelf->m_FavoritesGroupNum, true);
}
pSelf->m_FavoritesGroup = false;
}
void CClient::Con_AddFavorite(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
NETADDR Addr;
if(net_addr_from_str(&Addr, pResult->GetString(0)) != 0)
{
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "invalid address '%s'", pResult->GetString(0));
pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf);
return;
}
bool AllowPing = pResult->NumArguments() > 1 && str_find(pResult->GetString(1), "allow_ping");
char aAddr[NETADDR_MAXSTRSIZE];
net_addr_str(&Addr, aAddr, sizeof(aAddr), true);
if(pSelf->m_FavoritesGroup)
{
if(pSelf->m_FavoritesGroupNum == (int)std::size(pSelf->m_aFavoritesGroupAddresses))
{
log_error("client", "discarding %s because groups can have at most a size of %d", aAddr, pSelf->m_FavoritesGroupNum);
return;
}
log_info("client", "adding %s to favorites group", aAddr);
pSelf->m_aFavoritesGroupAddresses[pSelf->m_FavoritesGroupNum] = Addr;
pSelf->m_FavoritesGroupAllowPing = pSelf->m_FavoritesGroupAllowPing || AllowPing;
pSelf->m_FavoritesGroupNum += 1;
}
else
{
log_info("client", "adding %s to favorites", aAddr);
pSelf->m_pFavorites->Add(&Addr, 1);
if(AllowPing)
{
pSelf->m_pFavorites->AllowPing(&Addr, 1, true);
}
}
2010-05-29 07:25:38 +00:00
}
void CClient::Con_RemoveFavorite(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
NETADDR Addr;
if(net_addr_from_str(&Addr, pResult->GetString(0)) == 0)
pSelf->m_pFavorites->Remove(&Addr, 1);
}
void CClient::DemoSliceBegin()
2014-08-12 14:21:06 +00:00
{
const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
g_Config.m_ClDemoSliceBegin = pInfo->m_Info.m_CurrentTick;
2014-08-12 14:21:06 +00:00
}
void CClient::DemoSliceEnd()
2014-08-12 14:21:06 +00:00
{
const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info();
2014-08-12 14:21:06 +00:00
g_Config.m_ClDemoSliceEnd = pInfo->m_Info.m_CurrentTick;
}
void CClient::Con_DemoSliceBegin(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
2015-07-09 00:08:14 +00:00
pSelf->DemoSliceBegin();
}
void CClient::Con_DemoSliceEnd(IConsole::IResult *pResult, void *pUserData)
2014-08-12 14:21:06 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->DemoSliceEnd();
}
2014-08-12 14:21:06 +00:00
2019-05-20 21:55:40 +00:00
void CClient::Con_SaveReplay(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pResult->NumArguments())
{
int Length = pResult->GetInteger(0);
if(Length <= 0)
pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "ERROR: length must be greater than 0 second.");
else
{
if(pResult->NumArguments() >= 2)
pSelf->SaveReplay(Length, pResult->GetString(1));
else
pSelf->SaveReplay(Length);
}
}
else
pSelf->SaveReplay(g_Config.m_ClReplayLength);
2019-05-20 21:55:40 +00:00
}
2022-03-14 04:56:44 +00:00
void CClient::SaveReplay(const int Length, const char *pFilename)
2019-05-20 21:55:40 +00:00
{
if(!g_Config.m_ClReplays)
2019-05-20 21:55:40 +00:00
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "Feature is disabled. Please enable it via configuration.");
GameClient()->Echo(Localize("Replay feature is disabled!"));
2019-06-05 17:49:00 +00:00
return;
}
2019-06-05 17:49:00 +00:00
if(!DemoRecorder(RECORDER_REPLAYS)->IsRecording())
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "ERROR: demorecorder isn't recording. Try to rejoin to fix that.");
}
2019-06-05 17:49:00 +00:00
else if(DemoRecorder(RECORDER_REPLAYS)->Length() < 1)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "ERROR: demorecorder isn't recording for at least 1 second.");
}
else
{
char aFilename[IO_MAX_PATH_LENGTH];
if(pFilename[0] == '\0')
{
char aTimestamp[20];
str_timestamp(aTimestamp, sizeof(aTimestamp));
str_format(aFilename, sizeof(aFilename), "demos/replays/%s_%s_(replay).demo", m_aCurrentMap, aTimestamp);
}
else
{
2022-03-14 04:56:44 +00:00
str_format(aFilename, sizeof(aFilename), "demos/replays/%s.demo", pFilename);
IOHANDLE Handle = m_pStorage->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!Handle)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "ERROR: invalid filename. Try a different one!");
return;
}
io_close(Handle);
m_pStorage->RemoveFile(aFilename, IStorage::TYPE_SAVE);
}
2019-05-20 21:55:40 +00:00
// Stop the recorder to correctly slice the demo after
DemoRecorder(RECORDER_REPLAYS)->Stop(IDemoRecorder::EStopMode::KEEP_FILE);
2019-06-05 17:49:00 +00:00
// Slice the demo to get only the last cl_replay_length seconds
const char *pSrc = m_aDemoRecorder[RECORDER_REPLAYS].CurrentFilename();
const int EndTick = GameTick(g_Config.m_ClDummy);
2019-06-05 17:49:00 +00:00
const int StartTick = EndTick - Length * GameTickSpeed();
2019-06-05 17:49:00 +00:00
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "Saving replay...");
2019-06-05 17:49:00 +00:00
// Create a job to do this slicing in background because it can be a bit long depending on the file size
std::shared_ptr<CDemoEdit> pDemoEditTask = std::make_shared<CDemoEdit>(GameClient()->NetVersion(), &m_SnapshotDelta, m_pStorage, pSrc, aFilename, StartTick, EndTick);
Engine()->AddJob(pDemoEditTask);
m_EditJobs.push_back(pDemoEditTask);
2019-06-05 17:49:00 +00:00
// And we restart the recorder
DemoRecorder_UpdateReplayRecorder();
2019-05-20 21:55:40 +00:00
}
}
void CClient::DemoSlice(const char *pDstPath, CLIENTFUNC_FILTER pfnFilter, void *pUser)
{
2018-03-12 14:43:31 +00:00
if(m_DemoPlayer.IsPlaying())
2014-08-12 14:21:06 +00:00
{
m_DemoEditor.Slice(m_DemoPlayer.Filename(), pDstPath, g_Config.m_ClDemoSliceBegin, g_Config.m_ClDemoSliceEnd, pfnFilter, pUser);
2014-08-12 14:21:06 +00:00
}
}
2010-10-06 21:07:35 +00:00
const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType)
2010-05-29 07:25:38 +00:00
{
// Don't disconnect unless the file exists (only for play command)
if(!Storage()->FileExists(pFilename, StorageType))
return "No demo with this filename exists";
2010-05-29 07:25:38 +00:00
Disconnect();
m_aNetClient[CONN_MAIN].ResetErrorString();
2010-05-29 07:25:38 +00:00
SetState(IClient::STATE_LOADING);
SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_DEMO);
if((bool)m_LoadingCallback)
m_LoadingCallback(IClient::LOADING_CALLBACK_DETAIL_DEMO);
2010-05-29 07:25:38 +00:00
// try to start playback
m_DemoPlayer.SetListener(this);
2010-10-06 21:07:35 +00:00
if(m_DemoPlayer.Load(Storage(), m_pConsole, pFilename, StorageType))
{
DisconnectWithReason(m_DemoPlayer.ErrorMessage());
return m_DemoPlayer.ErrorMessage();
}
2010-05-29 07:25:38 +00:00
// load map
const CMapInfo *pMapInfo = m_DemoPlayer.GetMapInfo();
int Crc = pMapInfo->m_Crc;
SHA256_DIGEST Sha = pMapInfo->m_Sha256;
const char *pError = LoadMapSearch(pMapInfo->m_aName, Sha != SHA256_ZEROED ? &Sha : nullptr, Crc);
2010-05-29 07:25:38 +00:00
if(pError)
{
if(!m_DemoPlayer.ExtractMap(Storage()))
{
DisconnectWithReason(pError);
return pError;
}
2019-10-14 00:27:08 +00:00
Sha = m_DemoPlayer.GetMapInfo()->m_Sha256;
pError = LoadMapSearch(pMapInfo->m_aName, &Sha, Crc);
if(pError)
{
DisconnectWithReason(pError);
return pError;
}
2010-05-29 07:25:38 +00:00
}
// setup current server info
mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
str_copy(m_CurrentServerInfo.m_aMap, pMapInfo->m_aName);
m_CurrentServerInfo.m_MapCrc = pMapInfo->m_Crc;
m_CurrentServerInfo.m_MapSize = pMapInfo->m_Size;
2010-05-29 07:25:38 +00:00
GameClient()->OnConnected();
// setup buffers
Fix aliasing warnings in `CClient::DemoPlayer_Play` Fix warnings with `-fstrict-aliasing` and `-Wstrict-aliasing=2` by using char array instead of array of char pointers: ``` src/engine/client/client.cpp: In member function 'virtual const char* CClient::DemoPlayer_Play(const char*, int)': src/engine/client/client.cpp:3858:123: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] 3858 | m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pSnap = (CSnapshot *)m_aaapDemorecSnapshotData[SNAP_CURRENT][0]; | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ src/engine/client/client.cpp:3859:126: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] 3859 | m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pAltSnap = (CSnapshot *)m_aaapDemorecSnapshotData[SNAP_CURRENT][1]; | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ src/engine/client/client.cpp:3864:117: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] 3864 | m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_pSnap = (CSnapshot *)m_aaapDemorecSnapshotData[SNAP_PREV][0]; | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ src/engine/client/client.cpp:3865:120: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] 3865 | m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_pAltSnap = (CSnapshot *)m_aaapDemorecSnapshotData[SNAP_PREV][1]; | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ``` Snapshot data during demo playback was being stored in an array of `char *` instead of an array of `char`, which caused above aliasing warnings and used 8 times more memory for the snapshot storage than being necessary.
2022-11-12 19:12:24 +00:00
mem_zero(m_aaaDemorecSnapshotData, sizeof(m_aaaDemorecSnapshotData));
2010-05-29 07:25:38 +00:00
for(int SnapshotType = 0; SnapshotType < NUM_SNAPSHOT_TYPES; SnapshotType++)
{
m_aapSnapshots[0][SnapshotType] = &m_aDemorecSnapshotHolders[SnapshotType];
m_aapSnapshots[0][SnapshotType]->m_pSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][0];
m_aapSnapshots[0][SnapshotType]->m_pAltSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][1];
m_aapSnapshots[0][SnapshotType]->m_SnapSize = 0;
m_aapSnapshots[0][SnapshotType]->m_AltSnapSize = 0;
m_aapSnapshots[0][SnapshotType]->m_Tick = -1;
}
2010-05-29 07:25:38 +00:00
// enter demo playback state
SetState(IClient::STATE_DEMOPLAYBACK);
m_DemoPlayer.Play();
GameClient()->OnEnterGame();
return 0;
}
2019-09-27 07:22:50 +00:00
#if defined(CONF_VIDEORECORDER)
const char *CClient::DemoPlayer_Render(const char *pFilename, int StorageType, const char *pVideoName, int SpeedIndex, bool StartPaused)
{
const char *pError = DemoPlayer_Play(pFilename, StorageType);
2020-01-04 09:44:12 +00:00
if(pError)
return pError;
StartVideo(pVideoName, false);
m_DemoPlayer.SetSpeedIndex(SpeedIndex);
if(StartPaused)
{
m_DemoPlayer.Pause();
}
return nullptr;
}
2019-09-27 07:22:50 +00:00
#endif
void CClient::Con_Play(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->HandleDemoPath(pResult->GetString(0));
2010-05-29 07:25:38 +00:00
}
void CClient::Con_DemoPlay(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
2017-03-04 14:43:49 +00:00
if(pSelf->m_DemoPlayer.IsPlaying())
{
if(pSelf->m_DemoPlayer.BaseInfo()->m_Paused)
{
2015-08-12 10:17:19 +00:00
pSelf->m_DemoPlayer.Unpause();
}
2017-03-04 14:43:49 +00:00
else
{
2015-08-12 10:17:19 +00:00
pSelf->m_DemoPlayer.Pause();
}
}
}
2016-04-27 15:21:40 +00:00
void CClient::Con_DemoSpeed(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
pSelf->m_DemoPlayer.SetSpeed(pResult->GetFloat(0));
}
void CClient::DemoRecorder_Start(const char *pFilename, bool WithTimestamp, int Recorder, bool Verbose)
2010-05-29 07:25:38 +00:00
{
2010-08-09 12:14:15 +00:00
if(State() != IClient::STATE_ONLINE)
{
if(Verbose)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demorec/record", "client is not online");
}
}
2010-05-29 07:25:38 +00:00
else
{
char aFilename[IO_MAX_PATH_LENGTH];
if(WithTimestamp)
{
char aTimestamp[20];
str_timestamp(aTimestamp, sizeof(aTimestamp));
str_format(aFilename, sizeof(aFilename), "demos/%s_%s.demo", pFilename, aTimestamp);
}
else
{
str_format(aFilename, sizeof(aFilename), "demos/%s.demo", pFilename);
}
2020-01-03 09:12:37 +00:00
m_aDemoRecorder[Recorder].Start(Storage(), m_pConsole, aFilename, GameClient()->NetVersion(), m_aCurrentMap, m_pMap->Sha256(), m_pMap->Crc(), "client", m_pMap->MapSize(), 0, m_pMap->File());
2010-05-29 07:25:38 +00:00
}
}
void CClient::DemoRecorder_HandleAutoStart()
{
if(g_Config.m_ClAutoDemoRecord)
{
DemoRecorder(RECORDER_AUTO)->Stop(IDemoRecorder::EStopMode::KEEP_FILE);
char aFilename[IO_MAX_PATH_LENGTH];
str_format(aFilename, sizeof(aFilename), "auto/%s", m_aCurrentMap);
DemoRecorder_Start(aFilename, true, RECORDER_AUTO);
if(g_Config.m_ClAutoDemoMax)
{
// clean up auto recorded demos
CFileCollection AutoDemos;
AutoDemos.Init(Storage(), "demos/auto", "" /* empty for wild card */, ".demo", g_Config.m_ClAutoDemoMax);
}
}
DemoRecorder_UpdateReplayRecorder();
}
void CClient::DemoRecorder_UpdateReplayRecorder()
{
if(!g_Config.m_ClReplays && DemoRecorder(RECORDER_REPLAYS)->IsRecording())
2019-05-20 21:55:40 +00:00
{
DemoRecorder(RECORDER_REPLAYS)->Stop(IDemoRecorder::EStopMode::REMOVE_FILE);
2019-05-20 21:55:40 +00:00
}
if(g_Config.m_ClReplays && !DemoRecorder(RECORDER_REPLAYS)->IsRecording())
{
char aFilename[IO_MAX_PATH_LENGTH];
str_format(aFilename, sizeof(aFilename), "replays/replay_tmp_%s", m_aCurrentMap);
DemoRecorder_Start(aFilename, true, RECORDER_REPLAYS);
}
}
void CClient::DemoRecorder_AddDemoMarker(int Recorder)
{
m_aDemoRecorder[Recorder].AddDemoMarker();
}
class IDemoRecorder *CClient::DemoRecorder(int Recorder)
{
return &m_aDemoRecorder[Recorder];
}
void CClient::Con_Record(IConsole::IResult *pResult, void *pUserData)
2010-08-09 12:14:15 +00:00
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->m_aDemoRecorder[RECORDER_MANUAL].IsRecording())
{
pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Demo recorder already recording");
return;
}
if(pResult->NumArguments())
pSelf->DemoRecorder_Start(pResult->GetString(0), false, RECORDER_MANUAL, true);
else
pSelf->DemoRecorder_Start(pSelf->m_aCurrentMap, true, RECORDER_MANUAL, true);
2010-08-09 12:14:15 +00:00
}
void CClient::Con_StopRecord(IConsole::IResult *pResult, void *pUserData)
2010-05-29 07:25:38 +00:00
{
CClient *pSelf = (CClient *)pUserData;
pSelf->DemoRecorder(RECORDER_MANUAL)->Stop(IDemoRecorder::EStopMode::KEEP_FILE);
2010-05-29 07:25:38 +00:00
}
void CClient::Con_AddDemoMarker(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++)
pSelf->DemoRecorder_AddDemoMarker(Recorder);
}
2020-11-19 15:15:07 +00:00
void CClient::Con_BenchmarkQuit(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
int Seconds = pResult->GetInteger(0);
const char *pFilename = pResult->GetString(1);
pSelf->BenchmarkQuit(Seconds, pFilename);
}
void CClient::BenchmarkQuit(int Seconds, const char *pFilename)
{
char aBuf[IO_MAX_PATH_LENGTH];
2020-11-19 15:15:07 +00:00
m_BenchmarkFile = Storage()->OpenFile(pFilename, IOFLAG_WRITE, IStorage::TYPE_ABSOLUTE, aBuf, sizeof(aBuf));
m_BenchmarkStopTime = time_get() + time_freq() * Seconds;
}
void CClient::UpdateAndSwap()
{
Input()->Update();
Graphics()->Swap();
Graphics()->Clear(0, 0, 0);
}
void CClient::ServerBrowserUpdate()
{
m_ServerBrowser.RequestResort();
}
void CClient::ConchainServerBrowserUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
((CClient *)pUserData)->ServerBrowserUpdate();
}
void CClient::InitChecksum()
{
CChecksumData *pData = &m_Checksum.m_Data;
pData->m_SizeofData = sizeof(*pData);
2022-07-09 16:14:56 +00:00
str_copy(pData->m_aVersionStr, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")");
pData->m_Start = time_get();
os_version_str(pData->m_aOsVersion, sizeof(pData->m_aOsVersion));
secure_random_fill(&pData->m_Random, sizeof(pData->m_Random));
pData->m_Version = GameClient()->DDNetVersion();
pData->m_SizeofClient = sizeof(*this);
pData->m_SizeofConfig = sizeof(pData->m_Config);
2023-01-12 15:18:03 +00:00
pData->InitFiles();
}
#ifndef DDNET_CHECKSUM_SALT
// salt@checksum.ddnet.tw: db877f2b-2ddb-3ba6-9f67-a6d169ec671d
#define DDNET_CHECKSUM_SALT \
{ \
{ \
0xdb, 0x87, 0x7f, 0x2b, 0x2d, 0xdb, 0x3b, 0xa6, \
0x9f, 0x67, 0xa6, 0xd1, 0x69, 0xec, 0x67, 0x1d, \
} \
}
#endif
int CClient::HandleChecksum(int Conn, CUuid Uuid, CUnpacker *pUnpacker)
{
int Start = pUnpacker->GetInt();
int Length = pUnpacker->GetInt();
if(pUnpacker->Error())
{
return 1;
}
if(Start < 0 || Length < 0 || Start > std::numeric_limits<int>::max() - Length)
{
return 2;
}
int End = Start + Length;
int ChecksumBytesEnd = minimum(End, (int)sizeof(m_Checksum.m_aBytes));
int FileStart = maximum(Start, (int)sizeof(m_Checksum.m_aBytes));
2023-02-04 00:22:49 +00:00
unsigned char aStartBytes[sizeof(int32_t)];
unsigned char aEndBytes[sizeof(int32_t)];
uint_to_bytes_be(aStartBytes, Start);
uint_to_bytes_be(aEndBytes, End);
if(Start <= (int)sizeof(m_Checksum.m_aBytes))
{
mem_zero(&m_Checksum.m_Data.m_Config, sizeof(m_Checksum.m_Data.m_Config));
#define CHECKSUM_RECORD(Flags) (((Flags)&CFGFLAG_CLIENT) == 0 || ((Flags)&CFGFLAG_INSENSITIVE) != 0)
#define MACRO_CONFIG_INT(Name, ScriptName, Def, Min, Max, Flags, Desc) \
if(CHECKSUM_RECORD(Flags)) \
{ \
m_Checksum.m_Data.m_Config.m_##Name = g_Config.m_##Name; \
}
#define MACRO_CONFIG_COL(Name, ScriptName, Def, Flags, Desc) \
if(CHECKSUM_RECORD(Flags)) \
{ \
m_Checksum.m_Data.m_Config.m_##Name = g_Config.m_##Name; \
}
#define MACRO_CONFIG_STR(Name, ScriptName, Len, Def, Flags, Desc) \
if(CHECKSUM_RECORD(Flags)) \
{ \
str_copy(m_Checksum.m_Data.m_Config.m_##Name, g_Config.m_##Name, sizeof(m_Checksum.m_Data.m_Config.m_##Name)); \
}
#include <engine/shared/config_variables.h>
#undef CHECKSUM_RECORD
#undef MACRO_CONFIG_INT
#undef MACRO_CONFIG_COL
#undef MACRO_CONFIG_STR
}
if(End > (int)sizeof(m_Checksum.m_aBytes))
{
if(m_OwnExecutableSize == 0)
{
m_OwnExecutable = io_current_exe();
// io_length returns -1 on error.
m_OwnExecutableSize = m_OwnExecutable ? io_length(m_OwnExecutable) : -1;
}
// Own executable not available.
if(m_OwnExecutableSize < 0)
{
return 3;
}
if(End - (int)sizeof(m_Checksum.m_aBytes) > m_OwnExecutableSize)
{
return 4;
}
}
SHA256_CTX Sha256Ctxt;
sha256_init(&Sha256Ctxt);
CUuid Salt = DDNET_CHECKSUM_SALT;
sha256_update(&Sha256Ctxt, &Salt, sizeof(Salt));
sha256_update(&Sha256Ctxt, &Uuid, sizeof(Uuid));
sha256_update(&Sha256Ctxt, aStartBytes, sizeof(aStartBytes));
sha256_update(&Sha256Ctxt, aEndBytes, sizeof(aEndBytes));
if(Start < (int)sizeof(m_Checksum.m_aBytes))
{
sha256_update(&Sha256Ctxt, m_Checksum.m_aBytes + Start, ChecksumBytesEnd - Start);
}
if(End > (int)sizeof(m_Checksum.m_aBytes))
{
unsigned char aBuf[2048];
if(io_seek(m_OwnExecutable, FileStart - sizeof(m_Checksum.m_aBytes), IOSEEK_START))
{
return 5;
}
for(int i = FileStart; i < End; i += sizeof(aBuf))
{
int Read = io_read(m_OwnExecutable, aBuf, minimum((int)sizeof(aBuf), End - i));
sha256_update(&Sha256Ctxt, aBuf, Read);
}
}
SHA256_DIGEST Sha256 = sha256_finish(&Sha256Ctxt);
CMsgPacker Msg(NETMSG_CHECKSUM_RESPONSE, true);
Msg.AddRaw(&Uuid, sizeof(Uuid));
Msg.AddRaw(&Sha256, sizeof(Sha256));
SendMsg(Conn, &Msg, MSGFLAG_VITAL);
return 0;
}
void CClient::SwitchWindowScreen(int Index)
{
2024-02-06 23:01:06 +00:00
//Tested on windows 11 64 bit (gtx 1660 super, intel UHD 630 opengl 1.2.0, 3.3.0 and vulkan 1.1.0)
int IsFullscreen = g_Config.m_GfxFullscreen;
int IsBorderless = g_Config.m_GfxBorderless;
if(!Graphics()->SetWindowScreen(Index))
{
return;
}
2024-02-08 12:01:57 +00:00
SetWindowParams(3, false); // prevent DDNet to get stretch on monitors
2024-02-06 23:01:06 +00:00
2024-02-09 19:31:09 +00:00
CVideoMode CurMode;
Graphics()->GetCurrentVideoMode(CurMode, Index);
2024-02-06 23:01:06 +00:00
2024-02-09 19:31:09 +00:00
const int Depth = CurMode.m_Red + CurMode.m_Green + CurMode.m_Blue > 16 ? 24 : 16;
2024-02-06 23:01:06 +00:00
g_Config.m_GfxColorDepth = Depth;
2024-02-09 19:31:09 +00:00
g_Config.m_GfxScreenWidth = CurMode.m_WindowWidth;
g_Config.m_GfxScreenHeight = CurMode.m_WindowHeight;
g_Config.m_GfxScreenRefreshRate = CurMode.m_RefreshRate;
2024-02-06 23:01:06 +00:00
Graphics()->ResizeToScreen();
2024-02-06 23:01:06 +00:00
SetWindowParams(IsFullscreen, IsBorderless);
}
void CClient::ConchainWindowScreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(g_Config.m_GfxScreen != pResult->GetInteger(0))
pSelf->SwitchWindowScreen(pResult->GetInteger(0));
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::SetWindowParams(int FullscreenMode, bool IsBorderless)
{
g_Config.m_GfxFullscreen = clamp(FullscreenMode, 0, 3);
g_Config.m_GfxBorderless = (int)IsBorderless;
Graphics()->SetWindowParams(FullscreenMode, IsBorderless);
}
void CClient::ConchainFullscreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(g_Config.m_GfxFullscreen != pResult->GetInteger(0))
pSelf->SetWindowParams(pResult->GetInteger(0), g_Config.m_GfxBorderless);
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::ConchainWindowBordered(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(!g_Config.m_GfxFullscreen && (g_Config.m_GfxBorderless != pResult->GetInteger(0)))
pSelf->SetWindowParams(g_Config.m_GfxFullscreen, !g_Config.m_GfxBorderless);
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::ToggleWindowVSync()
{
if(Graphics()->SetVSync(g_Config.m_GfxVsync ^ 1))
g_Config.m_GfxVsync ^= 1;
}
void CClient::Notify(const char *pTitle, const char *pMessage)
{
2020-04-14 21:34:04 +00:00
if(m_pGraphics->WindowActive() || !g_Config.m_ClShowNotifications)
return;
Notifications()->Notify(pTitle, pMessage);
Graphics()->NotifyWindow();
}
void CClient::OnWindowResize()
{
TextRender()->OnPreWindowResize();
GameClient()->OnWindowResize();
m_pEditor->OnWindowResize();
TextRender()->OnWindowResize();
}
void CClient::ConchainWindowVSync(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
if(pSelf->Graphics() && pResult->NumArguments())
{
if(g_Config.m_GfxVsync != pResult->GetInteger(0))
pSelf->ToggleWindowVSync();
}
else
pfnCallback(pResult, pCallbackUserData);
}
void CClient::ConchainWindowResize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
pfnCallback(pResult, pCallbackUserData);
if(pSelf->Graphics() && pResult->NumArguments())
{
pSelf->Graphics()->ResizeToScreen();
}
}
void CClient::ConchainTimeoutSeed(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
pSelf->m_GenerateTimeoutSeed = false;
}
2018-06-20 06:43:55 +00:00
void CClient::ConchainPassword(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments() && pSelf->m_LocalStartTime) //won't set m_SendPassword before game has started
pSelf->m_SendPassword = true;
}
void CClient::ConchainReplays(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
{
pSelf->DemoRecorder_UpdateReplayRecorder();
}
}
void CClient::ConchainLoglevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments())
{
pSelf->m_pFileLogger->SetFilter(CLogFilter{IConsole::ToLogLevelFilter(g_Config.m_Loglevel)});
}
}
void CClient::ConchainStdoutOutputLevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
CClient *pSelf = (CClient *)pUserData;
pfnCallback(pResult, pCallbackUserData);
if(pResult->NumArguments() && pSelf->m_pStdoutLogger)
{
pSelf->m_pStdoutLogger->SetFilter(CLogFilter{IConsole::ToLogLevelFilter(g_Config.m_StdoutOutputLevel)});
}
}
2010-05-29 07:25:38 +00:00
void CClient::RegisterCommands()
{
m_pConsole = Kernel()->RequestInterface<IConsole>();
2021-03-17 15:09:39 +00:00
m_pConsole->Register("dummy_connect", "", CFGFLAG_CLIENT, Con_DummyConnect, this, "Connect dummy");
m_pConsole->Register("dummy_disconnect", "", CFGFLAG_CLIENT, Con_DummyDisconnect, this, "Disconnect dummy");
m_pConsole->Register("dummy_reset", "", CFGFLAG_CLIENT, Con_DummyResetInput, this, "Reset dummy");
2023-05-09 21:39:31 +00:00
m_pConsole->Register("quit", "", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Quit, this, "Quit the client");
m_pConsole->Register("exit", "", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Quit, this, "Quit the client");
m_pConsole->Register("restart", "", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Restart, this, "Restart the client");
m_pConsole->Register("minimize", "", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Minimize, this, "Minimize the client");
2020-11-19 15:15:07 +00:00
m_pConsole->Register("connect", "r[host|ip]", CFGFLAG_CLIENT, Con_Connect, this, "Connect to the specified host/ip");
m_pConsole->Register("disconnect", "", CFGFLAG_CLIENT, Con_Disconnect, this, "Disconnect from the server");
m_pConsole->Register("ping", "", CFGFLAG_CLIENT, Con_Ping, this, "Ping the current server");
m_pConsole->Register("screenshot", "", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Screenshot, this, "Take a screenshot");
2016-08-27 19:10:27 +00:00
#if defined(CONF_VIDEORECORDER)
m_pConsole->Register("start_video", "?r[file]", CFGFLAG_CLIENT, Con_StartVideo, this, "Start recording a video");
2016-08-27 15:51:23 +00:00
m_pConsole->Register("stop_video", "", CFGFLAG_CLIENT, Con_StopVideo, this, "Stop recording a video");
2016-08-27 19:10:27 +00:00
#endif
m_pConsole->Register("rcon", "r[rcon-command]", CFGFLAG_CLIENT, Con_Rcon, this, "Send specified command to rcon");
2020-11-19 15:15:07 +00:00
m_pConsole->Register("rcon_auth", "r[password]", CFGFLAG_CLIENT, Con_RconAuth, this, "Authenticate to rcon");
m_pConsole->Register("rcon_login", "s[username] r[password]", CFGFLAG_CLIENT, Con_RconLogin, this, "Authenticate to rcon with a username");
m_pConsole->Register("play", "r[file]", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Play, this, "Play back a demo");
m_pConsole->Register("record", "?r[file]", CFGFLAG_CLIENT, Con_Record, this, "Start recording a demo");
m_pConsole->Register("stoprecord", "", CFGFLAG_CLIENT, Con_StopRecord, this, "Stop recording a demo");
m_pConsole->Register("add_demomarker", "", CFGFLAG_CLIENT, Con_AddDemoMarker, this, "Add demo timeline marker");
m_pConsole->Register("begin_favorite_group", "", CFGFLAG_CLIENT, Con_BeginFavoriteGroup, this, "Use this before `add_favorite` to group favorites. End with `end_favorite_group`");
m_pConsole->Register("end_favorite_group", "", CFGFLAG_CLIENT, Con_EndFavoriteGroup, this, "Use this after `add_favorite` to group favorites. Start with `begin_favorite_group`");
m_pConsole->Register("add_favorite", "s[host|ip] ?s['allow_ping']", CFGFLAG_CLIENT, Con_AddFavorite, this, "Add a server as a favorite");
2020-11-19 15:15:07 +00:00
m_pConsole->Register("remove_favorite", "r[host|ip]", CFGFLAG_CLIENT, Con_RemoveFavorite, this, "Remove a server from favorites");
m_pConsole->Register("demo_slice_start", "", CFGFLAG_CLIENT, Con_DemoSliceBegin, this, "Mark the beginning of a demo cut");
m_pConsole->Register("demo_slice_end", "", CFGFLAG_CLIENT, Con_DemoSliceEnd, this, "Mark the end of a demo cut");
m_pConsole->Register("demo_play", "", CFGFLAG_CLIENT, Con_DemoPlay, this, "Play/pause the current demo");
m_pConsole->Register("demo_speed", "i[speed]", CFGFLAG_CLIENT, Con_DemoSpeed, this, "Set current demo speed");
m_pConsole->Register("save_replay", "?i[length] ?r[filename]", CFGFLAG_CLIENT, Con_SaveReplay, this, "Save a replay of the last defined amount of seconds");
2020-11-19 15:15:07 +00:00
m_pConsole->Register("benchmark_quit", "i[seconds] r[file]", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_BenchmarkQuit, this, "Benchmark frame times for number of seconds to file, then quit");
2019-05-20 21:55:40 +00:00
RustVersionRegister(*m_pConsole);
m_pConsole->Chain("cl_timeout_seed", ConchainTimeoutSeed, this);
m_pConsole->Chain("cl_replays", ConchainReplays, this);
2018-06-20 06:43:55 +00:00
m_pConsole->Chain("password", ConchainPassword, this);
2018-06-20 06:43:55 +00:00
// used for server browser update
m_pConsole->Chain("br_filter_string", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("br_filter_gametype", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("br_filter_serveraddress", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("add_favorite", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("remove_favorite", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("end_favorite_group", ConchainServerBrowserUpdate, this);
m_pConsole->Chain("gfx_screen", ConchainWindowScreen, this);
m_pConsole->Chain("gfx_screen_width", ConchainWindowResize, this);
m_pConsole->Chain("gfx_screen_height", ConchainWindowResize, this);
m_pConsole->Chain("gfx_screen_refresh_rate", ConchainWindowResize, this);
m_pConsole->Chain("gfx_fullscreen", ConchainFullscreen, this);
m_pConsole->Chain("gfx_borderless", ConchainWindowBordered, this);
m_pConsole->Chain("gfx_vsync", ConchainWindowVSync, this);
m_pConsole->Chain("loglevel", ConchainLoglevel, this);
m_pConsole->Chain("stdout_output_level", ConchainStdoutOutputLevel, this);
2010-05-29 07:25:38 +00:00
}
static CClient *CreateClient()
{
return new CClient;
}
2010-05-29 07:25:38 +00:00
void CClient::HandleConnectAddress(const NETADDR *pAddr)
{
net_addr_str(pAddr, m_aCmdConnect, sizeof(m_aCmdConnect), true);
}
void CClient::HandleConnectLink(const char *pLink)
{
2022-11-29 17:18:12 +00:00
// Chrome works fine with ddnet:// but not with ddnet:
2022-11-29 17:14:38 +00:00
// Check ddnet:// before ddnet: because we don't want the // as part of connect command
if(str_startswith(pLink, CONNECTLINK_DOUBLE_SLASH))
str_copy(m_aCmdConnect, pLink + sizeof(CONNECTLINK_DOUBLE_SLASH) - 1);
else if(str_startswith(pLink, CONNECTLINK_NO_SLASH))
str_copy(m_aCmdConnect, pLink + sizeof(CONNECTLINK_NO_SLASH) - 1);
else
str_copy(m_aCmdConnect, pLink);
2022-11-29 17:18:12 +00:00
// Edge appends / to the URL
const int len = str_length(m_aCmdConnect);
if(m_aCmdConnect[len - 1] == '/')
m_aCmdConnect[len - 1] = '\0';
}
2019-04-10 06:56:20 +00:00
void CClient::HandleDemoPath(const char *pPath)
{
2022-07-09 16:14:56 +00:00
str_copy(m_aCmdPlayDemo, pPath);
}
void CClient::HandleMapPath(const char *pPath)
{
2022-07-09 16:14:56 +00:00
str_copy(m_aCmdEditMap, pPath);
}
static bool UnknownArgumentCallback(const char *pCommand, void *pUser)
{
CClient *pClient = static_cast<CClient *>(pUser);
2022-11-29 17:14:38 +00:00
if(str_startswith(pCommand, CONNECTLINK_NO_SLASH))
{
pClient->HandleConnectLink(pCommand);
return true;
}
else if(str_endswith(pCommand, ".demo"))
{
pClient->HandleDemoPath(pCommand);
return true;
}
else if(str_endswith(pCommand, ".map"))
{
pClient->HandleMapPath(pCommand);
return true;
}
return false;
}
static bool SaveUnknownCommandCallback(const char *pCommand, void *pUser)
{
CClient *pClient = static_cast<CClient *>(pUser);
pClient->ConfigManager()->StoreUnknownCommand(pCommand);
return true;
}
static Uint32 GetSdlMessageBoxFlags(IClient::EMessageBoxType Type)
{
switch(Type)
{
case IClient::MESSAGE_BOX_TYPE_ERROR:
return SDL_MESSAGEBOX_ERROR;
case IClient::MESSAGE_BOX_TYPE_WARNING:
return SDL_MESSAGEBOX_WARNING;
case IClient::MESSAGE_BOX_TYPE_INFO:
return SDL_MESSAGEBOX_INFORMATION;
}
dbg_assert(false, "Type invalid");
return 0;
}
static void ShowMessageBox(const char *pTitle, const char *pMessage, IClient::EMessageBoxType Type = IClient::MESSAGE_BOX_TYPE_ERROR)
{
SDL_ShowSimpleMessageBox(GetSdlMessageBoxFlags(Type), pTitle, pMessage, nullptr);
}
2010-05-29 07:25:38 +00:00
/*
Server Time
Client Mirror Time
Client Predicted Time
Snapshot Latency
Downstream latency
Prediction Latency
Upstream latency
*/
#if defined(CONF_PLATFORM_MACOS)
extern "C" int TWMain(int argc, const char **argv)
2021-08-24 10:18:20 +00:00
#elif defined(CONF_PLATFORM_ANDROID)
2022-01-20 09:49:31 +00:00
extern "C" __attribute__((visibility("default"))) int SDL_main(int argc, char *argv[]);
2021-08-24 10:18:20 +00:00
extern "C" void InitAndroid();
2022-01-20 09:49:31 +00:00
int SDL_main(int argc, char *argv2[])
#else
int main(int argc, const char **argv)
#endif
2020-03-27 23:18:24 +00:00
{
const int64_t MainStart = time_get();
2022-01-20 09:49:31 +00:00
#if defined(CONF_PLATFORM_ANDROID)
const char **argv = const_cast<const char **>(argv2);
#elif defined(CONF_FAMILY_WINDOWS)
CWindowsComLifecycle WindowsComLifecycle(true);
2022-01-20 09:49:31 +00:00
#endif
CCmdlineFix CmdlineFix(&argc, &argv);
2021-08-24 10:18:20 +00:00
#if defined(CONF_PLATFORM_ANDROID)
InitAndroid();
#endif
2022-02-18 09:46:05 +00:00
#if defined(CONF_EXCEPTION_HANDLING)
init_exception_handler();
#endif
2022-06-15 17:34:41 +00:00
std::vector<std::shared_ptr<ILogger>> vpLoggers;
std::shared_ptr<ILogger> pStdoutLogger = nullptr;
#if defined(CONF_PLATFORM_ANDROID)
pStdoutLogger = std::shared_ptr<ILogger>(log_logger_android());
#else
bool Silent = false;
for(int i = 1; i < argc; i++)
{
if(str_comp("-s", argv[i]) == 0 || str_comp("--silent", argv[i]) == 0)
{
Silent = true;
}
}
if(!Silent)
{
pStdoutLogger = std::shared_ptr<ILogger>(log_logger_stdout());
}
#endif
if(pStdoutLogger)
{
vpLoggers.push_back(pStdoutLogger);
}
std::shared_ptr<CFutureLogger> pFutureFileLogger = std::make_shared<CFutureLogger>();
2022-06-15 17:34:41 +00:00
vpLoggers.push_back(pFutureFileLogger);
std::shared_ptr<CFutureLogger> pFutureConsoleLogger = std::make_shared<CFutureLogger>();
2022-06-15 17:34:41 +00:00
vpLoggers.push_back(pFutureConsoleLogger);
std::shared_ptr<CFutureLogger> pFutureAssertionLogger = std::make_shared<CFutureLogger>();
2022-06-15 17:34:41 +00:00
vpLoggers.push_back(pFutureAssertionLogger);
log_set_global_logger(log_logger_collection(std::move(vpLoggers)).release());
std::stack<std::function<void()>> CleanerFunctions;
std::function<void()> PerformCleanup = [&CleanerFunctions]() mutable {
while(!CleanerFunctions.empty())
{
CleanerFunctions.top()();
CleanerFunctions.pop();
}
};
std::function<void()> PerformFinalCleanup = []() {
#ifdef CONF_PLATFORM_ANDROID
// properly close this native thread, so globals are destructed
std::exit(0);
#endif
};
std::function<void()> PerformAllCleanup = [PerformCleanup, PerformFinalCleanup]() mutable {
PerformCleanup();
PerformFinalCleanup();
};
const bool RandInitFailed = secure_random_init() != 0;
if(!RandInitFailed)
2023-10-05 20:13:03 +00:00
CleanerFunctions.emplace([]() { secure_random_uninit(); });
// Register SDL for cleanup before creating the kernel and client,
// so SDL is shutdown after kernel and client. Otherwise the client
// may crash when shutting down after SDL is already shutdown.
2023-10-05 20:13:03 +00:00
CleanerFunctions.emplace([]() { SDL_Quit(); });
2011-07-30 11:50:22 +00:00
CClient *pClient = CreateClient();
pClient->SetLoggers(pFutureFileLogger, std::move(pStdoutLogger));
2010-05-29 07:25:38 +00:00
IKernel *pKernel = IKernel::Create();
pKernel->RegisterInterface(pClient, false);
2011-07-30 11:50:22 +00:00
pClient->RegisterInterfaces();
2023-10-05 20:13:03 +00:00
CleanerFunctions.emplace([pKernel, pClient]() {
// Ensure that the assert handler doesn't use the client/graphics after they've been destroyed
dbg_assert_set_handler(nullptr);
pKernel->Shutdown();
delete pKernel;
delete pClient;
});
2010-05-29 07:25:38 +00:00
const std::thread::id MainThreadId = std::this_thread::get_id();
dbg_assert_set_handler([MainThreadId, pClient](const char *pMsg) {
if(MainThreadId != std::this_thread::get_id())
return;
char aVersionStr[128];
if(!os_version_str(aVersionStr, sizeof(aVersionStr)))
str_copy(aVersionStr, "unknown");
char aGpuInfo[256];
pClient->GetGpuInfoString(aGpuInfo);
2023-05-27 07:51:22 +00:00
char aMessage[768];
str_format(aMessage, sizeof(aMessage),
2023-09-05 19:24:33 +00:00
"An assertion error occurred. Please write down or take a screenshot of the following information and report this error.\n"
2023-05-27 07:51:22 +00:00
"Please also share the assert log which you should find in the 'dumps' folder in your config directory.\n\n"
"%s\n\n"
"Platform: %s\n"
"Game version: %s %s\n"
"OS version: %s\n\n"
"%s", // GPU info
pMsg, CONF_PLATFORM_STRING, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH != nullptr ? GIT_SHORTREV_HASH : "", aVersionStr,
aGpuInfo);
pClient->ShowMessageBox("Assertion Error", aMessage);
// Client will crash due to assertion, don't call PerformAllCleanup in this inconsistent state
});
2010-05-29 07:25:38 +00:00
// create the components
IEngine *pEngine = CreateEngine(GAME_NAME, pFutureConsoleLogger, 2 * std::thread::hardware_concurrency() + 2);
pKernel->RegisterInterface(pEngine, false);
CleanerFunctions.emplace([pEngine]() {
// Engine has to be destroyed before the graphics so that skin download thread can finish
delete pEngine;
});
IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_CLIENT, argc, (const char **)argv);
pKernel->RegisterInterface(pStorage);
2010-05-29 07:25:38 +00:00
pFutureAssertionLogger->Set(CreateAssertionLogger(pStorage, GAME_NAME));
2022-02-18 09:46:05 +00:00
#if defined(CONF_EXCEPTION_HANDLING)
2022-03-21 15:07:46 +00:00
char aBufPath[IO_MAX_PATH_LENGTH];
2022-02-18 09:46:05 +00:00
char aBufName[IO_MAX_PATH_LENGTH];
char aDate[64];
str_timestamp(aDate, sizeof(aDate));
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 : "");
2022-03-21 15:07:46 +00:00
pStorage->GetCompletePath(IStorage::TYPE_SAVE, aBufName, aBufPath, sizeof(aBufPath));
set_exception_handler_log_file(aBufPath);
2022-02-18 09:46:05 +00:00
#endif
2018-01-07 11:03:33 +00:00
if(RandInitFailed)
2018-01-04 14:33:21 +00:00
{
const char *pError = "Failed to initialize the secure RNG.";
log_error("secure", "%s", pError);
pClient->ShowMessageBox("Secure RNG Error", pError);
PerformAllCleanup();
2018-01-04 14:33:21 +00:00
return -1;
}
IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT).release();
pKernel->RegisterInterface(pConsole);
IConfigManager *pConfigManager = CreateConfigManager();
pKernel->RegisterInterface(pConfigManager);
IEngineSound *pEngineSound = CreateEngineSound();
pKernel->RegisterInterface(pEngineSound); // IEngineSound
pKernel->RegisterInterface(static_cast<ISound *>(pEngineSound), false);
2010-05-29 07:25:38 +00:00
IEngineInput *pEngineInput = CreateEngineInput();
pKernel->RegisterInterface(pEngineInput); // IEngineInput
pKernel->RegisterInterface(static_cast<IInput *>(pEngineInput), false);
2010-05-29 07:25:38 +00:00
IEngineTextRender *pEngineTextRender = CreateEngineTextRender();
pKernel->RegisterInterface(pEngineTextRender); // IEngineTextRender
pKernel->RegisterInterface(static_cast<ITextRender *>(pEngineTextRender), false);
2010-05-29 07:25:38 +00:00
IEngineMap *pEngineMap = CreateEngineMap();
pKernel->RegisterInterface(pEngineMap); // IEngineMap
pKernel->RegisterInterface(static_cast<IMap *>(pEngineMap), false);
2010-05-29 07:25:38 +00:00
IDiscord *pDiscord = CreateDiscord();
pKernel->RegisterInterface(pDiscord);
2010-05-29 07:25:38 +00:00
ISteam *pSteam = CreateSteam();
pKernel->RegisterInterface(pSteam);
2010-05-29 07:25:38 +00:00
INotifications *pNotifications = CreateNotifications();
pKernel->RegisterInterface(pNotifications);
pKernel->RegisterInterface(CreateEditor(), false);
pKernel->RegisterInterface(CreateFavorites().release());
pKernel->RegisterInterface(CreateGameClient());
2010-05-29 07:25:38 +00:00
pEngine->Init();
pConsole->Init();
Refactor config manager, move config variable handling Move all code for handling of config variables from console to config manager. The console no longer depends on the config manager, instead the config manager now depends on the console. Add `struct`s to manage config variables of different types (int, color and string). The config manager now keeps a list of all config variables, so usage of the preprocessor can be avoided except for code to initially create all config variables. Additionally, a separate list of just the game config variables (config variables with `CFGFLAG_GAME`) is kept to optimize the `ResetGameSettings` function, as this function is called whenever connecting to a server and should be fast. Previously, this function was even less efficient because it preformed a linear search for every individual game config variable to find the respective command data. Move console commands that opperate only on config variables (`reset`, `toggle` and `+toggle`) to config manager. Ensure that these commands only opperate on the desired config variables (client or server, respectively) by checking `IConsole::FlagMask`. Add `IConfigManager::SetReadOnly` function to set/unset config variables as read-only instead of manually overriding the command handlers for the `sv_rescue` and `sv_test_cmds` config variables. This also fixes that read-only config variables could still be changed by using the `reset` command. A console message is now printed when trying to change a read-only config variable. Removing the special handling for these two config variables is additionally useful so the console does not need to keep a pointer to config values and manager. Use a `CHeap` for the config variables, their help texts and the previous values of string config variables to avoid many separate allocations as well usage of static variables. Also use the heap to store the unknown commands instead of using `std::string`s. Properly trigger command chain when resetting config variables with the `reset` command and when resetting game settings on map loading. Closes #7461. Format default value for color variables as RGB/RGBA hex with dollar sign prefix. Closes #5523. Add log message when using `reset` with a variable that does not exist. Use `log_error` instead of `dbg_msg` when saving config file fails. Support unlimited number of config save callbacks instead of at most 16. The code also becomes more readable by using an `std::vector` instead of a fixed-size array and a separate num variable. Consistently name `MACRO_CONFIG_*` parameters when declaring the macros. Add `IConsole::CMDLINE_LENGTH` constant to represent the maximum length of the console input and thereby reduce usage of magic numbers for buffer sizes.
2023-11-22 22:07:08 +00:00
pConfigManager->Init();
pNotifications->Init(GAME_NAME " Client");
2010-05-29 07:25:38 +00:00
// register all console commands
2011-07-30 11:50:22 +00:00
pClient->RegisterCommands();
2010-05-29 07:25:38 +00:00
pKernel->RequestInterface<IGameClient>()->OnConsoleInit();
// init client's interfaces
2011-07-30 11:50:22 +00:00
pClient->InitInterfaces();
2010-05-29 07:25:38 +00:00
// execute config file
if(pStorage->FileExists(CONFIG_FILE, IStorage::TYPE_ALL))
{
pConsole->SetUnknownCommandCallback(SaveUnknownCommandCallback, pClient);
if(!pConsole->ExecuteFile(CONFIG_FILE))
{
const char *pError = "Failed to load config from '" CONFIG_FILE "'.";
log_error("client", "%s", pError);
pClient->ShowMessageBox("Config File Error", pError);
PerformAllCleanup();
return -1;
}
pConsole->SetUnknownCommandCallback(IConsole::EmptyUnknownCommandCallback, nullptr);
}
2010-05-29 07:25:38 +00:00
// execute autoexec file
if(pStorage->FileExists(AUTOEXEC_CLIENT_FILE, IStorage::TYPE_ALL))
{
pConsole->ExecuteFile(AUTOEXEC_CLIENT_FILE);
}
else // fallback
{
pConsole->ExecuteFile(AUTOEXEC_FILE);
}
2010-05-29 07:25:38 +00:00
if(g_Config.m_ClConfigVersion < 1)
{
if(g_Config.m_ClAntiPing == 0)
{
g_Config.m_ClAntiPingPlayers = 1;
g_Config.m_ClAntiPingGrenade = 1;
g_Config.m_ClAntiPingWeapons = 1;
}
}
g_Config.m_ClConfigVersion = 1;
2010-05-29 07:25:38 +00:00
// parse the command line arguments
pConsole->SetUnknownCommandCallback(UnknownArgumentCallback, pClient);
pConsole->ParseArguments(argc - 1, (const char **)&argv[1]);
pConsole->SetUnknownCommandCallback(IConsole::EmptyUnknownCommandCallback, nullptr);
2010-05-29 07:25:38 +00:00
if(pSteam->GetConnectAddress())
{
pClient->HandleConnectAddress(pSteam->GetConnectAddress());
pSteam->ClearConnectAddress();
}
if(g_Config.m_Logfile[0])
{
const int Mode = g_Config.m_Logappend ? IOFLAG_APPEND : IOFLAG_WRITE;
IOHANDLE Logfile = pStorage->OpenFile(g_Config.m_Logfile, Mode, IStorage::TYPE_SAVE_OR_ABSOLUTE);
if(Logfile)
{
pFutureFileLogger->Set(log_logger_file(Logfile));
}
else
{
log_error("client", "failed to open '%s' for logging", g_Config.m_Logfile);
pFutureFileLogger->Set(log_logger_noop());
}
}
else
{
pFutureFileLogger->Set(log_logger_noop());
}
2010-08-06 18:38:13 +00:00
// Register protocol and file extensions
#if defined(CONF_FAMILY_WINDOWS)
pClient->ShellRegister();
#endif
#if defined(CONF_PLATFORM_MACOS)
// Hints will not be set if there is an existing override hint or environment variable that takes precedence.
// So this respects cli environment overrides.
SDL_SetHint("SDL_MAC_OPENGL_ASYNC_DISPATCH", "1");
#endif
#if defined(CONF_FAMILY_WINDOWS)
SDL_SetHint("SDL_IME_SHOW_UI", g_Config.m_InpImeNativeUi ? "1" : "0");
#else
SDL_SetHint("SDL_IME_SHOW_UI", "1");
#endif
#if defined(CONF_PLATFORM_ANDROID)
// Trap the Android back button so it can be handled in our code reliably
// instead of letting the system handle it.
SDL_SetHint("SDL_ANDROID_TRAP_BACK_BUTTON", "1");
// Force landscape screen orientation.
SDL_SetHint("SDL_IOS_ORIENTATIONS", "LandscapeLeft LandscapeRight");
#endif
// init SDL
if(SDL_Init(0) < 0)
{
char aError[256];
str_format(aError, sizeof(aError), "Unable to initialize SDL base: %s", SDL_GetError());
log_error("client", "%s", aError);
pClient->ShowMessageBox("SDL Error", aError);
PerformAllCleanup();
return -1;
}
2010-05-29 07:25:38 +00:00
// run the client
log_trace("client", "initialization finished after %.2fms, starting...", (time_get() - MainStart) * 1000.0f / (float)time_freq());
2011-07-30 11:50:22 +00:00
pClient->Run();
2010-05-29 07:25:38 +00:00
const bool Restarting = pClient->State() == CClient::STATE_RESTARTING;
char aRestartBinaryPath[IO_MAX_PATH_LENGTH];
if(Restarting)
{
pStorage->GetBinaryPath(PLAT_CLIENT_EXEC, aRestartBinaryPath, sizeof(aRestartBinaryPath));
}
2020-01-25 16:48:32 +00:00
std::vector<SWarning> vQuittingWarnings = pClient->QuittingWarnings();
PerformCleanup();
for(const SWarning &Warning : vQuittingWarnings)
{
::ShowMessageBox(Warning.m_aWarningTitle, Warning.m_aWarningMsg);
}
2020-01-25 16:48:32 +00:00
if(Restarting)
{
shell_execute(aRestartBinaryPath, EShellExecuteWindowState::FOREGROUND);
2020-01-25 16:48:32 +00:00
}
PerformFinalCleanup();
2021-08-24 10:18:20 +00:00
2010-05-29 07:25:38 +00:00
return 0;
}
// DDRace
const char *CClient::GetCurrentMap() const
{
return m_aCurrentMap;
}
const char *CClient::GetCurrentMapPath() const
{
2017-09-09 21:10:42 +00:00
return m_aCurrentMapPath;
}
SHA256_DIGEST CClient::GetCurrentMapSha256() const
{
return m_pMap->Sha256();
}
unsigned CClient::GetCurrentMapCrc() const
2016-08-23 01:08:36 +00:00
{
return m_pMap->Crc();
2016-08-23 01:08:36 +00:00
}
void CClient::RaceRecord_Start(const char *pFilename)
{
2017-09-09 21:10:42 +00:00
if(State() != IClient::STATE_ONLINE)
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demorec/record", "client is not online");
else
m_aDemoRecorder[RECORDER_RACE].Start(Storage(), m_pConsole, pFilename, GameClient()->NetVersion(), m_aCurrentMap, m_pMap->Sha256(), m_pMap->Crc(), "client", m_pMap->MapSize(), 0, m_pMap->File());
}
2017-09-09 21:10:42 +00:00
void CClient::RaceRecord_Stop()
{
if(m_aDemoRecorder[RECORDER_RACE].IsRecording())
{
m_aDemoRecorder[RECORDER_RACE].Stop(IDemoRecorder::EStopMode::KEEP_FILE);
}
}
2017-09-09 21:10:42 +00:00
bool CClient::RaceRecord_IsRecording()
{
return m_aDemoRecorder[RECORDER_RACE].IsRecording();
}
2014-09-19 22:36:22 +00:00
void CClient::RequestDDNetInfo()
{
2023-12-18 21:42:25 +00:00
if(m_pDDNetInfoTask && !m_pDDNetInfoTask->Done())
return;
char aUrl[256];
str_copy(aUrl, DDNET_INFO_URL);
if(g_Config.m_BrIndicateFinished)
{
char aEscaped[128];
EscapeUrl(aEscaped, sizeof(aEscaped), PlayerName());
str_append(aUrl, "?name=");
str_append(aUrl, aEscaped);
}
2022-03-07 14:01:37 +00:00
// Use ipv4 so we can know the ingame ip addresses of players before they join game servers
m_pDDNetInfoTask = HttpGet(aUrl);
m_pDDNetInfoTask->Timeout(CTimeout{10000, 0, 500, 10});
m_pDDNetInfoTask->IpResolve(IPRESOLVE::V4);
2023-12-18 19:01:26 +00:00
Http()->Run(m_pDDNetInfoTask);
}
2016-05-05 16:07:00 +00:00
int CClient::GetPredictionTime()
{
2021-06-23 05:05:49 +00:00
int64_t Now = time_get();
return (int)((m_PredictedTime.Get(Now) - m_aGameTime[g_Config.m_ClDummy].Get(Now)) * 1000 / (float)time_freq());
2016-05-05 16:07:00 +00:00
}
void CClient::GetSmoothTick(int *pSmoothTick, float *pSmoothIntraTick, float MixAmount)
{
int64_t GameTime = m_aGameTime[g_Config.m_ClDummy].Get(time_get());
2021-06-23 05:05:49 +00:00
int64_t PredTime = m_PredictedTime.Get(time_get());
int64_t SmoothTime = clamp(GameTime + (int64_t)(MixAmount * (PredTime - GameTime)), GameTime, PredTime);
*pSmoothTick = (int)(SmoothTime * GameTickSpeed() / time_freq()) + 1;
*pSmoothIntraTick = (SmoothTime - (*pSmoothTick - 1) * time_freq() / GameTickSpeed()) / (float)(time_freq() / GameTickSpeed());
}
void CClient::AddWarning(const SWarning &Warning)
{
m_vWarnings.emplace_back(Warning);
}
SWarning *CClient::GetCurWarning()
{
if(m_vWarnings.empty())
{
return NULL;
}
else if(m_vWarnings[0].m_WasShown)
{
m_vWarnings.erase(m_vWarnings.begin());
return NULL;
}
else
{
2022-07-10 19:22:50 +00:00
return m_vWarnings.data();
}
}
2022-01-09 12:06:41 +00:00
int CClient::MaxLatencyTicks() const
{
return GameTickSpeed() + (PredictionMargin() * GameTickSpeed()) / 1000;
}
int CClient::PredictionMargin() const
{
return m_ServerCapabilities.m_SyncWeaponInput ? g_Config.m_ClPredictionMargin : 10;
2022-01-09 12:06:41 +00:00
}
int CClient::UdpConnectivity(int NetType)
{
static const int NETTYPES[2] = {NETTYPE_IPV6, NETTYPE_IPV4};
int Connectivity = CONNECTIVITY_UNKNOWN;
for(int PossibleNetType : NETTYPES)
{
if((NetType & PossibleNetType) == 0)
{
continue;
}
NETADDR GlobalUdpAddr;
int NewConnectivity;
switch(m_aNetClient[CONN_MAIN].GetConnectivity(PossibleNetType, &GlobalUdpAddr))
{
case CONNECTIVITY::UNKNOWN:
NewConnectivity = CONNECTIVITY_UNKNOWN;
break;
case CONNECTIVITY::CHECKING:
NewConnectivity = CONNECTIVITY_CHECKING;
break;
case CONNECTIVITY::UNREACHABLE:
NewConnectivity = CONNECTIVITY_UNREACHABLE;
break;
case CONNECTIVITY::REACHABLE:
NewConnectivity = CONNECTIVITY_REACHABLE;
break;
case CONNECTIVITY::ADDRESS_KNOWN:
GlobalUdpAddr.port = 0;
if(m_HaveGlobalTcpAddr && NetType == (int)m_GlobalTcpAddr.type && net_addr_comp(&m_GlobalTcpAddr, &GlobalUdpAddr) != 0)
{
NewConnectivity = CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES;
break;
}
NewConnectivity = CONNECTIVITY_REACHABLE;
break;
default:
dbg_assert(0, "invalid connectivity value");
return CONNECTIVITY_UNKNOWN;
}
Connectivity = std::max(Connectivity, NewConnectivity);
}
return Connectivity;
}
#if defined(CONF_FAMILY_WINDOWS)
void CClient::ShellRegister()
{
char aFullPath[IO_MAX_PATH_LENGTH];
Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
if(!aFullPath[0])
{
log_error("client", "Failed to register protocol and file extensions: could not determine absolute path");
return;
}
bool Updated = false;
if(!shell_register_protocol("ddnet", aFullPath, &Updated))
log_error("client", "Failed to register ddnet protocol");
if(!shell_register_extension(".map", "Map File", GAME_NAME, aFullPath, &Updated))
log_error("client", "Failed to register .map file extension");
if(!shell_register_extension(".demo", "Demo File", GAME_NAME, aFullPath, &Updated))
log_error("client", "Failed to register .demo file extension");
if(!shell_register_application(GAME_NAME, aFullPath, &Updated))
log_error("client", "Failed to register application");
if(Updated)
shell_update();
}
void CClient::ShellUnregister()
{
char aFullPath[IO_MAX_PATH_LENGTH];
Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
if(!aFullPath[0])
{
log_error("client", "Failed to unregister protocol and file extensions: could not determine absolute path");
return;
}
bool Updated = false;
if(!shell_unregister_class("ddnet", &Updated))
log_error("client", "Failed to unregister ddnet protocol");
if(!shell_unregister_class(GAME_NAME ".map", &Updated))
log_error("client", "Failed to unregister .map file extension");
if(!shell_unregister_class(GAME_NAME ".demo", &Updated))
log_error("client", "Failed to unregister .demo file extension");
if(!shell_unregister_application(aFullPath, &Updated))
log_error("client", "Failed to unregister application");
if(Updated)
shell_update();
}
#endif
void CClient::ShowMessageBox(const char *pTitle, const char *pMessage, EMessageBoxType Type)
{
if(m_pGraphics == nullptr || !m_pGraphics->ShowMessageBox(GetSdlMessageBoxFlags(Type), pTitle, pMessage))
::ShowMessageBox(pTitle, pMessage, Type);
}
2023-05-27 07:51:22 +00:00
void CClient::GetGpuInfoString(char (&aGpuInfo)[256])
2023-05-27 07:51:22 +00:00
{
if(m_pGraphics != nullptr && m_pGraphics->IsBackendInitialized())
{
str_format(aGpuInfo, std::size(aGpuInfo), "GPU: %s - %s - %s", m_pGraphics->GetVendorString(), m_pGraphics->GetRendererString(), m_pGraphics->GetVersionString());
2023-05-27 07:51:22 +00:00
}
else
{
str_copy(aGpuInfo, "Graphics backend was not yet initialized.");
2023-05-27 07:51:22 +00:00
}
}
void CClient::SetLoggers(std::shared_ptr<ILogger> &&pFileLogger, std::shared_ptr<ILogger> &&pStdoutLogger)
{
m_pFileLogger = pFileLogger;
m_pStdoutLogger = pStdoutLogger;
}