r5sdk/r5dev/rtech/liveapi/liveapi.cpp
Kawe Mazidjatari bea1deeb9d LiveAPI: initial WebSocket implementation
Working WebSocket implementation (foundation for game LiveAPI).
2024-04-05 18:30:22 +02:00

259 lines
7.2 KiB
C++

//===========================================================================//
//
// 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;
}