LiveAPI: initial WebSocket implementation

Working WebSocket implementation (foundation for game LiveAPI).
This commit is contained in:
Kawe Mazidjatari 2024-03-25 01:26:33 +01:00
parent aff556732e
commit 2d084db3a3
4 changed files with 347 additions and 0 deletions

View File

@ -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();

View File

@ -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()

View File

@ -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<string> 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;
}

View File

@ -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<ConnContext_s> servers;
};
LiveAPI* LiveAPISystem();
#endif // RTECH_LIVEAPI_H