diff --git a/src/engine/host_state.cpp b/src/engine/host_state.cpp index 579c1918..6908a929 100644 --- a/src/engine/host_state.cpp +++ b/src/engine/host_state.cpp @@ -35,6 +35,7 @@ #include "engine/cmodel_bsp.h" #ifndef CLIENT_DLL #include "engine/server/server.h" +#include "rtech/liveapi/liveapi.h" #endif // !CLIENT_DLL #include "rtech/stryder/stryder.h" #include "rtech/playlists/playlists.h" @@ -156,6 +157,10 @@ void CHostState::FrameUpdate(CHostState* pHostState, double flCurrentTime, float RCONClient()->RunFrame(); #endif // !DEDICATED +#ifndef CLIENT_DLL + LiveAPISystem()->RunFrame(); // TODO[ AMOS ]: move to server frame !!! +#endif // !CLIENT_DLL + // Disable "warning C4611: interaction between '_setjmp' and C++ object destruction is non-portable" #pragma warning(push) #pragma warning(disable : 4611) @@ -305,6 +310,10 @@ void CHostState::Setup(void) RCONClient()->Init(); #endif // !DEDICATED +#ifndef CLIENT_DLL + LiveAPISystem()->Init(); // TODO[ AMOS ]: move to server frame !!! +#endif // !CLIENT_DLL + if (net_useRandomKey.GetBool()) { NET_GenerateKey(); diff --git a/src/rtech/CMakeLists.txt b/src/rtech/CMakeLists.txt index 7dc2d1fd..45022df4 100644 --- a/src/rtech/CMakeLists.txt +++ b/src/rtech/CMakeLists.txt @@ -34,6 +34,11 @@ add_sources( SOURCE_GROUP "Pak" "pak/paktools.h" ) +add_sources( SOURCE_GROUP "LiveAPI" + "liveapi/liveapi.cpp" + "liveapi/liveapi.h" +) + add_sources( SOURCE_GROUP "Public" "${ENGINE_SOURCE_DIR}/public/rtech/iasync.h" "${ENGINE_SOURCE_DIR}/public/rtech/ipakfile.h" @@ -41,6 +46,11 @@ add_sources( SOURCE_GROUP "Public" end_sources() +target_include_directories( ${PROJECT_NAME} PRIVATE + "${THIRDPARTY_SOURCE_DIR}/dirtysdk/include/" + "${THIRDPARTY_SOURCE_DIR}/ea/" +) + add_module( "lib" "rson" "vpc" ${FOLDER_CONTEXT} TRUE TRUE ) start_sources() diff --git a/src/rtech/liveapi/liveapi.cpp b/src/rtech/liveapi/liveapi.cpp new file mode 100644 index 00000000..1d4eed05 --- /dev/null +++ b/src/rtech/liveapi/liveapi.cpp @@ -0,0 +1,258 @@ +//===========================================================================// +// +// Purpose: LiveAPI WebSocket implementation +// +//===========================================================================// +#include "liveapi.h" + +#include "DirtySDK/dirtysock.h" +#include "DirtySDK/dirtysock/netconn.h" +#include "DirtySDK/proto/protossl.h" +#include "DirtySDK/proto/protowebsocket.h" + +//----------------------------------------------------------------------------- +// console variables +//----------------------------------------------------------------------------- +static ConVar liveapi_enabled("liveapi_enabled", "1", FCVAR_RELEASE); +static ConVar liveapi_servers("liveapi_servers", "ws://127.0.0.1:7777" , FCVAR_RELEASE, "Comma separated list of addresses to connect to", "'ws://domain.suffix:port'"); + +static ConVar liveapi_timeout("liveapi_timeout", "300", FCVAR_RELEASE, "WebSocket connection timeout in seconds"); +static ConVar liveapi_keepalive("liveapi_keepalive", "30", FCVAR_RELEASE, "Interval of time to send Pong to any connected server"); +static ConVar liveapi_lax_ssl("liveapi_lax_ssl", "1", FCVAR_RELEASE, "Skip SSL certificate validation for all WSS connections (allows the use of self-signed certificates)"); + +static ConVar liveapi_retry_count("liveapi_retry_count", "5", FCVAR_RELEASE, "Amount of times to retry connecting before marking the connection as unavailable"); +static ConVar liveapi_retry_time("liveapi_retry_time", "30", FCVAR_RELEASE, "Amount of time between each retry"); + +//----------------------------------------------------------------------------- +// constructors/destructors +//----------------------------------------------------------------------------- +LiveAPI::LiveAPI() +{ + initialized = false; +} + +//----------------------------------------------------------------------------- +// Initialization of the LiveAPI system +//----------------------------------------------------------------------------- +void LiveAPI::Init() +{ + if (!liveapi_enabled.GetBool()) + return; + + NetConnStatus('open', 0, NULL, 0); + + const int32_t startupRet = NetConnStartup("-servicename=liveapi"); + + if (startupRet < 0) + { + Error(eDLL_T::RTECH, 0, "LiveAPI: initialization failed! [%x]\n", startupRet); + return; + } + + ProtoSSLStartup(); + const vector addresses = StringSplit(liveapi_servers.GetString(), ','); + + for (const string& addres : addresses) + { + const ConnContext_s conn(addres); + servers.push_back(conn); + } + + initialized = true; +} + +//----------------------------------------------------------------------------- +// Shutdown of the LiveAPI system +//----------------------------------------------------------------------------- +void LiveAPI::Shutdown() +{ + initialized = false; + + for (ConnContext_s& conn : servers) + { + conn.Destroy(); + } + + servers.clear(); +} + +//----------------------------------------------------------------------------- +// LiveAPI state machine +//----------------------------------------------------------------------------- +void LiveAPI::RunFrame() +{ + if (!IsEnabled()) + return; + + const double queryTime = Plat_FloatTime(); + + for (ConnContext_s& conn : servers) + { + if (conn.webSocket) + ProtoWebSocketUpdate(conn.webSocket); + + if (conn.state == CS_CREATE || conn.state == CS_RETRY) + { + conn.Connect(queryTime); + continue; + } + + if (conn.state == CS_CONNECTED || conn.state == CS_LISTENING) + { + conn.Status(queryTime); + continue; + } + + if (conn.state == CS_DESTROYED) + { + if (conn.retryCount > liveapi_retry_count.GetInt()) + { + // All retry attempts have been used; mark unavailable for deletion + conn.state = CS_UNAVAIL; + } + else + { + // Mark as retry, this will recreate the socket and reattempt + // the connection + conn.state = CS_RETRY; + } + } + } + + DeleteUnavailable(); +} + +//----------------------------------------------------------------------------- +// Delete all server connections marked unavailable +//----------------------------------------------------------------------------- +void LiveAPI::DeleteUnavailable() +{ + servers.erase(std::remove_if(servers.begin(), servers.end(), + [](const ConnContext_s& conn) + { + return conn.state == CS_UNAVAIL; + } + ), servers.end()); +} + +//----------------------------------------------------------------------------- +// Send an event to all sockets +//----------------------------------------------------------------------------- +void LiveAPI::SendEvent(const char* const dataBuf, const int32_t dataSize) +{ + for (ConnContext_s& conn : servers) + { + if (conn.state != CS_LISTENING) + continue; + + if (ProtoWebSocketSend(conn.webSocket, dataBuf, dataSize) < 0) + conn.Destroy(); // Reattempt the connection for this socket + } +} + +//----------------------------------------------------------------------------- +// Returns whether the system is enabled and able to run +//----------------------------------------------------------------------------- +bool LiveAPI::IsEnabled() const +{ + return initialized && liveapi_enabled.GetBool(); +} + +//----------------------------------------------------------------------------- +// Connect to a socket +//----------------------------------------------------------------------------- +bool LiveAPI::ConnContext_s::Connect(const double queryTime) +{ + const double retryTimeTotal = retryTime + liveapi_retry_time.GetFloat(); + const double currTime = Plat_FloatTime(); + + if (retryTimeTotal > currTime) + return false; // Still within retry period + + retryCount++; + webSocket = ProtoWebSocketCreate(LIVE_API_MAX_FRAME_BUFFER_SIZE); + + if (!webSocket) + { + state = CS_UNAVAIL; + return false; + } + + const int32_t timeOut = liveapi_timeout.GetInt(); + + if (timeOut > 0) + { + ProtoWebSocketControl(webSocket, 'time', timeOut, 0, NULL); + } + + const int32_t keepAlive = liveapi_keepalive.GetInt(); + + if (keepAlive > 0) + { + ProtoWebSocketControl(webSocket, 'keep', keepAlive, 0, NULL); + } + + ProtoWebSocketControl(webSocket, 'ncrt', liveapi_lax_ssl.GetInt(), 0, NULL); + ProtoWebSocketUpdate(webSocket); + + if (ProtoWebSocketConnect(webSocket, address.c_str()) != NULL) + { + // Failure + Destroy(); + return false; + } + + state = CS_CONNECTED; + retryTime = queryTime; + + return true; +} + +//----------------------------------------------------------------------------- +// Check the connection status and destroy if not connected (-1) +//----------------------------------------------------------------------------- +bool LiveAPI::ConnContext_s::Status(const double queryTime) +{ + const int32_t status = ProtoWebSocketStatus(webSocket, 'stat', NULL, 0); + + if (status == -1) + { + Destroy(); + retryTime = queryTime; + + return false; + } + else if (!status) + { + retryTime = queryTime; + return false; + } + + retryCount = 0; + state = CS_LISTENING; + + return true; +} + +//----------------------------------------------------------------------------- +// Destroy the connection +//----------------------------------------------------------------------------- +void LiveAPI::ConnContext_s::Destroy() +{ + ProtoWebSocketDisconnect(webSocket); + ProtoWebSocketUpdate(webSocket); + ProtoWebSocketDestroy(webSocket); + + webSocket = nullptr; + state = CS_DESTROYED; +} + +static LiveAPI s_liveApi; + +//----------------------------------------------------------------------------- +// Singleton accessor +//----------------------------------------------------------------------------- +LiveAPI* LiveAPISystem() +{ + return &s_liveApi; +} diff --git a/src/rtech/liveapi/liveapi.h b/src/rtech/liveapi/liveapi.h new file mode 100644 index 00000000..ef16d20b --- /dev/null +++ b/src/rtech/liveapi/liveapi.h @@ -0,0 +1,70 @@ +#ifndef RTECH_LIVEAPI_H +#define RTECH_LIVEAPI_H + +#define LIVE_API_MAX_FRAME_BUFFER_SIZE 0x8000 + +struct ProtoWebSocketRefT; +typedef void (*LiveAPISendCallback_t)(ProtoWebSocketRefT* webSocket); + +class LiveAPI +{ +public: + enum ConnState_e + { + CS_CREATE = 0, + + CS_CONNECTED, + CS_LISTENING, + + CS_DESTROYED, + + CS_RETRY, + CS_UNAVAIL + }; + + struct ConnContext_s + { + ConnContext_s(const string& addr) + { + webSocket = nullptr; + address = addr; + + state = CS_CREATE; + + retryCount = 0; + retryTime = 0; + } + + bool Connect(const double queryTime); + bool Process(const double queryTime); + + void Destroy(); + + ProtoWebSocketRefT* webSocket; + ConnState_e state; + + int retryCount; + double retryTime; + + string address; + }; + + LiveAPI(); + + void Init(); + void Shutdown(); + + void RunFrame(); + void DeleteUnavailable(); + + void SendEvent(const char* const dataBuf, const int32_t dataSize); + bool IsEnabled() const; + +private: + bool initialized; + vector servers; +}; + +LiveAPI* LiveAPISystem(); + +#endif // RTECH_LIVEAPI_H