From 2a05e3466b9eacc3133f3e83e00bb08a73205576 Mon Sep 17 00:00:00 2001 From: Nomads_Reach <144523850+NomadsReach@users.noreply.github.com> Date: Sun, 16 Aug 2026 00:19:32 -0400 Subject: [PATCH] Add native GNS server bridge --- .github/workflows/gns-transport.yml | 46 ++ server/native_transport/CMakeLists.txt | 45 ++ .../native_transport/co_gns_server_bridge.cpp | 570 ++++++++++++++++++ .../native_transport/co_gns_server_bridge.h | 88 +++ .../test_co_gns_server_bridge.cpp | 215 +++++++ 5 files changed, 964 insertions(+) create mode 100644 .github/workflows/gns-transport.yml create mode 100644 server/native_transport/CMakeLists.txt create mode 100644 server/native_transport/co_gns_server_bridge.cpp create mode 100644 server/native_transport/co_gns_server_bridge.h create mode 100644 server/native_transport/test_co_gns_server_bridge.cpp diff --git a/.github/workflows/gns-transport.yml b/.github/workflows/gns-transport.yml new file mode 100644 index 0000000..233613c --- /dev/null +++ b/.github/workflows/gns-transport.yml @@ -0,0 +1,46 @@ +name: GNS Transport Bridge + +on: + push: + paths: + - "server/native_transport/**" + - ".github/workflows/gns-transport.yml" + pull_request: + paths: + - "server/native_transport/**" + - ".github/workflows/gns-transport.yml" + +jobs: + linux: + name: Linux native GNS bridge + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build libssl-dev libprotobuf-dev protobuf-compiler + + - name: Fetch pinned GameNetworkingSockets + run: | + GNS_SHA=f4525e39ee10f6b45181fd92d01fee75f7d71756 + rm -rf /tmp/co-gns-src + git init /tmp/co-gns-src + git -C /tmp/co-gns-src remote add origin https://github.com/ValveSoftware/GameNetworkingSockets.git + git -C /tmp/co-gns-src fetch --depth 1 origin "$GNS_SHA" + git -C /tmp/co-gns-src checkout --detach FETCH_HEAD + test "$(git -C /tmp/co-gns-src rev-parse HEAD)" = "$GNS_SHA" + + - name: Build and run native bridge test + run: | + cmake -S server/native_transport -B /tmp/co-server-gns-build -G Ninja \ + -DGNS_SOURCE_DIR=/tmp/co-gns-src \ + -DCMAKE_BUILD_TYPE=Release + cmake --build /tmp/co-server-gns-build --target co-gns-server-bridge-tests -j2 + ctest --test-dir /tmp/co-server-gns-build --output-on-failure + + - name: Confirm shared bridge artifact + run: | + test -f /tmp/co-server-gns-build/libcommonwealth_online_gns_bridge.so + ldd /tmp/co-server-gns-build/libcommonwealth_online_gns_bridge.so diff --git a/server/native_transport/CMakeLists.txt b/server/native_transport/CMakeLists.txt new file mode 100644 index 0000000..27b77cf --- /dev/null +++ b/server/native_transport/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.20) +project(CommonwealthOnlineGnsServerBridge LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +set(GNS_SOURCE_DIR "" CACHE PATH "Path to the pinned GameNetworkingSockets source tree") +if(NOT EXISTS "${GNS_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR "GNS_SOURCE_DIR must point to an initialized GameNetworkingSockets source tree") +endif() + +set(BUILD_STATIC_LIB OFF CACHE BOOL "" FORCE) +set(BUILD_SHARED_LIB ON CACHE BOOL "" FORCE) +set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(ENABLE_ICE OFF CACHE BOOL "" FORCE) +set(USE_STEAMWEBRTC OFF CACHE BOOL "" FORCE) +set(USE_CRYPTO OpenSSL CACHE STRING "" FORCE) +set(USE_CRYPTO25519 OpenSSL CACHE STRING "" FORCE) + +add_subdirectory("${GNS_SOURCE_DIR}" "${CMAKE_BINARY_DIR}/gns") + +add_library(commonwealth_online_gns_bridge SHARED co_gns_server_bridge.cpp) +target_include_directories(commonwealth_online_gns_bridge PUBLIC "${CMAKE_CURRENT_LIST_DIR}") +target_compile_definitions(commonwealth_online_gns_bridge PRIVATE + CO_GNS_BRIDGE_BUILD + STEAMNETWORKINGSOCKETS_STANDALONELIB +) +target_compile_options(commonwealth_online_gns_bridge PRIVATE -Wall -Wextra -Werror) +target_link_libraries(commonwealth_online_gns_bridge PRIVATE GameNetworkingSockets::shared) +set_target_properties(commonwealth_online_gns_bridge PROPERTIES OUTPUT_NAME "commonwealth_online_gns_bridge") + +add_executable(co-gns-server-bridge-tests test_co_gns_server_bridge.cpp) +target_compile_definitions(co-gns-server-bridge-tests PRIVATE STEAMNETWORKINGSOCKETS_STANDALONELIB) +target_compile_options(co-gns-server-bridge-tests PRIVATE -Wall -Wextra -Werror) +target_link_libraries(co-gns-server-bridge-tests PRIVATE + commonwealth_online_gns_bridge + GameNetworkingSockets::shared +) + +enable_testing() +add_test(NAME co-gns-server-bridge COMMAND co-gns-server-bridge-tests) diff --git a/server/native_transport/co_gns_server_bridge.cpp b/server/native_transport/co_gns_server_bridge.cpp new file mode 100644 index 0000000..d214d5a --- /dev/null +++ b/server/native_transport/co_gns_server_bridge.cpp @@ -0,0 +1,570 @@ +#include "co_gns_server_bridge.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr std::size_t kMaximumMessageBytes = 64uz * 1024uz; + constexpr std::size_t kReceiveBatchSize = 64; + + std::mutex g_runtimeMutex; + std::size_t g_runtimeReferenceCount = 0; + + class ServerBridge; + + std::mutex g_ownerMutex; + std::unordered_map g_listenOwners; + std::unordered_map g_connectionOwners; + + void WriteError(char* buffer, std::size_t bufferSize, const std::string& message) + { + if (buffer == nullptr || bufferSize == 0) { + return; + } + std::snprintf(buffer, bufferSize, "%s", message.c_str()); + } + + bool AcquireRuntime(std::string& error) + { + const std::scoped_lock lock(g_runtimeMutex); + if (g_runtimeReferenceCount > 0) { + ++g_runtimeReferenceCount; + return true; + } + + SteamNetworkingErrMsg errorMessage{}; + if (!GameNetworkingSockets_Init(nullptr, errorMessage)) { + error = errorMessage[0] ? std::string{ errorMessage } : "GameNetworkingSockets_Init failed."; + return false; + } + g_runtimeReferenceCount = 1; + return true; + } + + void ReleaseRuntime() + { + const std::scoped_lock lock(g_runtimeMutex); + if (g_runtimeReferenceCount == 0) { + return; + } + --g_runtimeReferenceCount; + if (g_runtimeReferenceCount == 0) { + GameNetworkingSockets_Kill(); + } + } + + struct QueuedEvent + { + co_gns_event metadata{}; + std::vector payload; + }; + + class ServerBridge + { + public: + ~ServerBridge() + { + Stop(); + } + + bool Start(const char* bindHost, std::uint16_t port, std::string& error) + { + if (!AcquireRuntime(error)) { + return false; + } + runtimeHeld_ = true; + networking_ = SteamNetworkingSockets(); + if (networking_ == nullptr) { + error = "SteamNetworkingSockets returned no server interface."; + Stop(); + return false; + } + + pollGroup_ = networking_->CreatePollGroup(); + if (pollGroup_ == k_HSteamNetPollGroup_Invalid) { + error = "GameNetworkingSockets failed to create a server poll group."; + Stop(); + return false; + } + + SteamNetworkingIPAddr address{}; + address.Clear(); + const std::string host = bindHost != nullptr ? std::string{ bindHost } : std::string{}; + if (host.empty() || host == "0.0.0.0") { + address.SetIPv4(0U, port); + } else { + if (!address.ParseString(host.c_str()) || !address.IsIPv4()) { + error = "GNS bind address must be a valid IPv4 address."; + Stop(); + return false; + } + address.m_port = port; + } + + SteamNetworkingConfigValue_t option{}; + option.SetPtr( + k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged, + reinterpret_cast(+ConnectionStatusChanged)); + listenSocket_ = networking_->CreateListenSocketIP(address, 1, std::addressof(option)); + if (listenSocket_ == k_HSteamListenSocket_Invalid) { + error = "GameNetworkingSockets failed to create a listen socket."; + Stop(); + return false; + } + + { + const std::scoped_lock ownerLock(g_ownerMutex); + g_listenOwners[listenSocket_] = this; + } + + SteamNetworkingIPAddr actualAddress{}; + if (!networking_->GetListenSocketAddress(listenSocket_, std::addressof(actualAddress))) { + error = "GameNetworkingSockets could not report the bound listen address."; + Stop(); + return false; + } + localPort_ = actualAddress.m_port; + return true; + } + + void Stop() + { + ISteamNetworkingSockets* networking = networking_; + HSteamListenSocket listenSocket = listenSocket_; + HSteamNetPollGroup pollGroup = pollGroup_; + std::vector connections; + { + const std::scoped_lock lock(mutex_); + connections.assign(connections_.begin(), connections_.end()); + connections_.clear(); + connectedConnections_.clear(); + events_.clear(); + listenSocket_ = k_HSteamListenSocket_Invalid; + pollGroup_ = k_HSteamNetPollGroup_Invalid; + networking_ = nullptr; + localPort_ = 0; + } + + { + const std::scoped_lock ownerLock(g_ownerMutex); + if (listenSocket != k_HSteamListenSocket_Invalid) { + g_listenOwners.erase(listenSocket); + } + for (const auto connection : connections) { + g_connectionOwners.erase(connection); + } + } + + if (networking != nullptr) { + for (const auto connection : connections) { + networking->CloseConnection(connection, 0, nullptr, false); + } + if (listenSocket != k_HSteamListenSocket_Invalid) { + networking->CloseListenSocket(listenSocket); + } + if (pollGroup != k_HSteamNetPollGroup_Invalid) { + networking->DestroyPollGroup(pollGroup); + } + } + + if (runtimeHeld_) { + runtimeHeld_ = false; + ReleaseRuntime(); + } + } + + std::uint16_t LocalPort() const + { + const std::scoped_lock lock(mutex_); + return localPort_; + } + + std::uint32_t ConnectionCount() const + { + const std::scoped_lock lock(mutex_); + return static_cast(connectedConnections_.size()); + } + + int Poll(co_gns_event& outEvent, void* payloadBuffer, std::uint32_t payloadCapacity) + { + ISteamNetworkingSockets* networking = nullptr; + { + const std::scoped_lock lock(mutex_); + networking = networking_; + } + if (networking == nullptr) { + return -1; + } + + networking->RunCallbacks(); + + bool queueIsEmpty = false; + { + const std::scoped_lock lock(mutex_); + queueIsEmpty = events_.empty(); + } + if (queueIsEmpty) { + PumpMessages(); + } + + const std::scoped_lock lock(mutex_); + if (events_.empty()) { + std::memset(std::addressof(outEvent), 0, sizeof(outEvent)); + return 0; + } + + const auto& next = events_.front(); + outEvent = next.metadata; + if (outEvent.type == CO_GNS_EVENT_MESSAGE && next.payload.size() > payloadCapacity) { + return -2; + } + if (outEvent.type == CO_GNS_EVENT_MESSAGE && !next.payload.empty()) { + if (payloadBuffer == nullptr) { + return -2; + } + std::memcpy(payloadBuffer, next.payload.data(), next.payload.size()); + } + events_.pop_front(); + return 1; + } + + int Send( + std::uint32_t connectionId, + const void* payload, + std::uint32_t payloadSize, + std::uint32_t delivery) + { + if (payload == nullptr || payloadSize == 0) { + return CO_GNS_SEND_ERROR; + } + if (payloadSize > kMaximumMessageBytes) { + return CO_GNS_SEND_TOO_LARGE; + } + if (delivery != CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED && delivery != CO_GNS_DELIVERY_RELIABLE_ORDERED) { + return CO_GNS_SEND_ERROR; + } + + const auto connection = static_cast(connectionId); + ISteamNetworkingSockets* networking = nullptr; + { + const std::scoped_lock lock(mutex_); + if (!connectedConnections_.contains(connection)) { + return CO_GNS_SEND_NOT_CONNECTED; + } + networking = networking_; + } + if (networking == nullptr) { + return CO_GNS_SEND_NOT_CONNECTED; + } + + const int flags = delivery == CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED ? + k_nSteamNetworkingSend_UnreliableNoDelay : + k_nSteamNetworkingSend_ReliableNoNagle; + const auto result = networking->SendMessageToConnection( + connection, + payload, + payloadSize, + flags, + nullptr); + switch (result) { + case k_EResultOK: + return CO_GNS_SEND_SENT; + case k_EResultIgnored: + return CO_GNS_SEND_DROPPED; + case k_EResultLimitExceeded: + return CO_GNS_SEND_BACKPRESSURE; + case k_EResultNoConnection: + case k_EResultInvalidState: + return CO_GNS_SEND_NOT_CONNECTED; + default: + return CO_GNS_SEND_ERROR; + } + } + + int Disconnect(std::uint32_t connectionId, std::int32_t reason, const char* debug) + { + const auto connection = static_cast(connectionId); + ISteamNetworkingSockets* networking = nullptr; + { + const std::scoped_lock lock(mutex_); + if (!connections_.contains(connection)) { + return 0; + } + connections_.erase(connection); + connectedConnections_.erase(connection); + networking = networking_; + } + { + const std::scoped_lock ownerLock(g_ownerMutex); + g_connectionOwners.erase(connection); + } + if (networking == nullptr) { + return 0; + } + return networking->CloseConnection(connection, reason, debug, false) ? 1 : 0; + } + + private: + static ServerBridge* FindOwner(const SteamNetConnectionStatusChangedCallback_t& info) + { + const std::scoped_lock ownerLock(g_ownerMutex); + const auto connectionOwner = g_connectionOwners.find(info.m_hConn); + if (connectionOwner != g_connectionOwners.end()) { + return connectionOwner->second; + } + const auto listenOwner = g_listenOwners.find(info.m_info.m_hListenSocket); + return listenOwner == g_listenOwners.end() ? nullptr : listenOwner->second; + } + + static void ConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* info) + { + if (info == nullptr) { + return; + } + if (auto* owner = FindOwner(*info); owner != nullptr) { + owner->OnConnectionStatusChanged(*info); + } + } + + static co_gns_event MakeEvent( + std::uint32_t type, + HSteamNetConnection connection, + std::int32_t reason, + std::uint32_t payloadSize, + const char* debug) + { + co_gns_event event{}; + event.type = type; + event.connection_id = static_cast(connection); + event.reason = reason; + event.payload_size = payloadSize; + if (debug != nullptr && debug[0] != '\0') { + std::snprintf(event.debug, sizeof(event.debug), "%s", debug); + } + return event; + } + + void OnConnectionStatusChanged(const SteamNetConnectionStatusChangedCallback_t& info) + { + if (info.m_info.m_eState == k_ESteamNetworkingConnectionState_Connecting && + info.m_info.m_hListenSocket == listenSocket_) { + if (networking_->AcceptConnection(info.m_hConn) != k_EResultOK || + !networking_->SetConnectionPollGroup(info.m_hConn, pollGroup_)) { + networking_->CloseConnection(info.m_hConn, 0, "Could not admit GNS connection", false); + return; + } + { + const std::scoped_lock lock(mutex_); + connections_.insert(info.m_hConn); + } + { + const std::scoped_lock ownerLock(g_ownerMutex); + g_connectionOwners[info.m_hConn] = this; + } + return; + } + + if (info.m_info.m_eState == k_ESteamNetworkingConnectionState_Connected) { + const std::scoped_lock lock(mutex_); + if (connections_.contains(info.m_hConn) && connectedConnections_.insert(info.m_hConn).second) { + events_.push_back(QueuedEvent{ + MakeEvent(CO_GNS_EVENT_CONNECTED, info.m_hConn, 0, 0, nullptr), + {} + }); + } + return; + } + + if (info.m_info.m_eState != k_ESteamNetworkingConnectionState_ClosedByPeer && + info.m_info.m_eState != k_ESteamNetworkingConnectionState_ProblemDetectedLocally) { + return; + } + + bool knownConnection = false; + { + const std::scoped_lock lock(mutex_); + knownConnection = connections_.erase(info.m_hConn) > 0; + connectedConnections_.erase(info.m_hConn); + if (knownConnection) { + events_.push_back(QueuedEvent{ + MakeEvent( + CO_GNS_EVENT_DISCONNECTED, + info.m_hConn, + info.m_info.m_eEndReason, + 0, + info.m_info.m_szEndDebug), + {} + }); + } + } + if (!knownConnection) { + return; + } + { + const std::scoped_lock ownerLock(g_ownerMutex); + g_connectionOwners.erase(info.m_hConn); + } + networking_->CloseConnection(info.m_hConn, 0, nullptr, false); + } + + void PumpMessages() + { + ISteamNetworkingSockets* networking = nullptr; + HSteamNetPollGroup pollGroup = k_HSteamNetPollGroup_Invalid; + { + const std::scoped_lock lock(mutex_); + networking = networking_; + pollGroup = pollGroup_; + } + if (networking == nullptr || pollGroup == k_HSteamNetPollGroup_Invalid) { + return; + } + + std::array messages{}; + const auto count = networking->ReceiveMessagesOnPollGroup( + pollGroup, + messages.data(), + static_cast(messages.size())); + if (count <= 0) { + return; + } + + for (int index = 0; index < count; ++index) { + auto* message = messages[static_cast(index)]; + if (message == nullptr) { + continue; + } + const auto size = message->m_cbSize > 0 ? static_cast(message->m_cbSize) : 0uz; + const auto connection = message->m_conn; + QueuedEvent queued{}; + if (size > kMaximumMessageBytes) { + queued.metadata = MakeEvent( + CO_GNS_EVENT_OVERSIZE_MESSAGE, + connection, + 0, + static_cast((std::min)(size, static_cast(UINT32_MAX))), + "Inbound GNS message exceeded 64 KiB"); + } else { + queued.metadata = MakeEvent( + CO_GNS_EVENT_MESSAGE, + connection, + 0, + static_cast(size), + nullptr); + if (size > 0 && message->m_pData != nullptr) { + const auto* begin = static_cast(message->m_pData); + queued.payload.assign(begin, begin + size); + } + } + { + const std::scoped_lock lock(mutex_); + if (connections_.contains(connection)) { + events_.push_back(std::move(queued)); + } + } + message->Release(); + } + } + + mutable std::mutex mutex_; + ISteamNetworkingSockets* networking_{ nullptr }; + HSteamListenSocket listenSocket_{ k_HSteamListenSocket_Invalid }; + HSteamNetPollGroup pollGroup_{ k_HSteamNetPollGroup_Invalid }; + std::unordered_set connections_; + std::unordered_set connectedConnections_; + std::deque events_; + std::uint16_t localPort_{ 0 }; + bool runtimeHeld_{ false }; + }; +} + +extern "C" +{ + int co_gns_server_create( + const char* bind_host, + uint16_t port, + co_gns_server_handle* out_handle, + char* error_buffer, + size_t error_buffer_size) + { + if (out_handle == nullptr) { + WriteError(error_buffer, error_buffer_size, "out_handle is required."); + return 0; + } + *out_handle = nullptr; + auto* server = new ServerBridge(); + std::string error; + if (!server->Start(bind_host, port, error)) { + delete server; + WriteError(error_buffer, error_buffer_size, error); + return 0; + } + *out_handle = server; + WriteError(error_buffer, error_buffer_size, ""); + return 1; + } + + void co_gns_server_destroy(co_gns_server_handle handle) + { + delete static_cast(handle); + } + + uint16_t co_gns_server_local_port(co_gns_server_handle handle) + { + const auto* server = static_cast(handle); + return server == nullptr ? 0 : server->LocalPort(); + } + + uint32_t co_gns_server_connection_count(co_gns_server_handle handle) + { + const auto* server = static_cast(handle); + return server == nullptr ? 0 : server->ConnectionCount(); + } + + int co_gns_server_poll( + co_gns_server_handle handle, + co_gns_event* out_event, + void* payload_buffer, + uint32_t payload_capacity) + { + auto* server = static_cast(handle); + if (server == nullptr || out_event == nullptr) { + return -1; + } + return server->Poll(*out_event, payload_buffer, payload_capacity); + } + + int co_gns_server_send( + co_gns_server_handle handle, + uint32_t connection_id, + const void* payload, + uint32_t payload_size, + uint32_t delivery) + { + auto* server = static_cast(handle); + return server == nullptr ? CO_GNS_SEND_ERROR : + server->Send(connection_id, payload, payload_size, delivery); + } + + int co_gns_server_disconnect( + co_gns_server_handle handle, + uint32_t connection_id, + int32_t reason, + const char* debug) + { + auto* server = static_cast(handle); + return server == nullptr ? 0 : server->Disconnect(connection_id, reason, debug); + } +} diff --git a/server/native_transport/co_gns_server_bridge.h b/server/native_transport/co_gns_server_bridge.h new file mode 100644 index 0000000..cfe3457 --- /dev/null +++ b/server/native_transport/co_gns_server_bridge.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#if defined(_WIN32) +#if defined(CO_GNS_BRIDGE_BUILD) +#define CO_GNS_API __declspec(dllexport) +#else +#define CO_GNS_API __declspec(dllimport) +#endif +#else +#define CO_GNS_API __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* co_gns_server_handle; + +enum co_gns_event_type +{ + CO_GNS_EVENT_NONE = 0, + CO_GNS_EVENT_CONNECTED = 1, + CO_GNS_EVENT_DISCONNECTED = 2, + CO_GNS_EVENT_MESSAGE = 3, + CO_GNS_EVENT_OVERSIZE_MESSAGE = 4 +}; + +enum co_gns_delivery +{ + CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED = 0, + CO_GNS_DELIVERY_RELIABLE_ORDERED = 1 +}; + +enum co_gns_send_result +{ + CO_GNS_SEND_ERROR = -1, + CO_GNS_SEND_SENT = 0, + CO_GNS_SEND_DROPPED = 1, + CO_GNS_SEND_BACKPRESSURE = 2, + CO_GNS_SEND_NOT_CONNECTED = 3, + CO_GNS_SEND_TOO_LARGE = 4 +}; + +typedef struct co_gns_event +{ + uint32_t type; + uint32_t connection_id; + int32_t reason; + uint32_t payload_size; + char debug[128]; +} co_gns_event; + +CO_GNS_API int co_gns_server_create( + const char* bind_host, + uint16_t port, + co_gns_server_handle* out_handle, + char* error_buffer, + size_t error_buffer_size); + +CO_GNS_API void co_gns_server_destroy(co_gns_server_handle handle); +CO_GNS_API uint16_t co_gns_server_local_port(co_gns_server_handle handle); +CO_GNS_API uint32_t co_gns_server_connection_count(co_gns_server_handle handle); + +CO_GNS_API int co_gns_server_poll( + co_gns_server_handle handle, + co_gns_event* out_event, + void* payload_buffer, + uint32_t payload_capacity); + +CO_GNS_API int co_gns_server_send( + co_gns_server_handle handle, + uint32_t connection_id, + const void* payload, + uint32_t payload_size, + uint32_t delivery); + +CO_GNS_API int co_gns_server_disconnect( + co_gns_server_handle handle, + uint32_t connection_id, + int32_t reason, + const char* debug); + +#ifdef __cplusplus +} +#endif diff --git a/server/native_transport/test_co_gns_server_bridge.cpp b/server/native_transport/test_co_gns_server_bridge.cpp new file mode 100644 index 0000000..959bc8d --- /dev/null +++ b/server/native_transport/test_co_gns_server_bridge.cpp @@ -0,0 +1,215 @@ +#include "co_gns_server_bridge.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using namespace std::chrono_literals; + + HSteamNetConnection g_clientConnection = k_HSteamNetConnection_Invalid; + bool g_clientConnected = false; + bool g_clientFailed = false; + + void ClientConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* info) + { + if (info == nullptr || info->m_hConn != g_clientConnection) { + return; + } + switch (info->m_info.m_eState) { + case k_ESteamNetworkingConnectionState_Connected: + g_clientConnected = true; + break; + case k_ESteamNetworkingConnectionState_ClosedByPeer: + case k_ESteamNetworkingConnectionState_ProblemDetectedLocally: + g_clientFailed = info->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally; + if (auto* networking = SteamNetworkingSockets(); networking != nullptr) { + networking->CloseConnection(info->m_hConn, 0, nullptr, false); + } + g_clientConnection = k_HSteamNetConnection_Invalid; + g_clientConnected = false; + break; + default: + break; + } + } + + struct PolledEvent + { + co_gns_event event{}; + std::string payload; + }; + + std::optional PollBridge(co_gns_server_handle server) + { + std::array payload{}; + co_gns_event event{}; + const auto result = co_gns_server_poll( + server, + &event, + payload.data(), + static_cast(payload.size())); + assert(result >= 0); + if (result == 0) { + return std::nullopt; + } + PolledEvent out{}; + out.event = event; + if (event.type == CO_GNS_EVENT_MESSAGE) { + out.payload.assign(payload.data(), event.payload_size); + } + return out; + } + + template + bool WaitUntil(co_gns_server_handle server, Predicate&& predicate, std::chrono::milliseconds timeout = 3s) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + PollBridge(server); + if (predicate()) { + return true; + } + std::this_thread::sleep_for(2ms); + } + return false; + } + + std::optional WaitForEvent( + co_gns_server_handle server, + std::uint32_t eventType, + std::chrono::milliseconds timeout = 3s) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + auto event = PollBridge(server); + if (event && event->event.type == eventType) { + return event; + } + std::this_thread::sleep_for(2ms); + } + return std::nullopt; + } + + std::optional WaitForClientMessage( + co_gns_server_handle server, + ISteamNetworkingSockets* networking, + std::chrono::milliseconds timeout = 3s) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + PollBridge(server); + ISteamNetworkingMessage* message = nullptr; + const auto count = networking->ReceiveMessagesOnConnection(g_clientConnection, &message, 1); + if (count > 0 && message != nullptr) { + std::string payload; + if (message->m_cbSize > 0 && message->m_pData != nullptr) { + payload.assign( + static_cast(message->m_pData), + static_cast(message->m_cbSize)); + } + message->Release(); + return payload; + } + assert(count >= 0); + std::this_thread::sleep_for(2ms); + } + return std::nullopt; + } +} + +int main() +{ + co_gns_server_handle server = nullptr; + std::array error{}; + assert(co_gns_server_create("127.0.0.1", 0, &server, error.data(), error.size()) == 1); + assert(server != nullptr); + const auto port = co_gns_server_local_port(server); + assert(port != 0); + + auto* networking = SteamNetworkingSockets(); + assert(networking != nullptr); + SteamNetworkingIPAddr serverAddress{}; + serverAddress.Clear(); + serverAddress.SetIPv4(0x7f000001U, port); + SteamNetworkingConfigValue_t option{}; + option.SetPtr( + k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged, + reinterpret_cast(+ClientConnectionStatusChanged)); + g_clientConnection = networking->ConnectByIPAddress(serverAddress, 1, &option); + assert(g_clientConnection != k_HSteamNetConnection_Invalid); + + const auto connectedEvent = WaitForEvent(server, CO_GNS_EVENT_CONNECTED); + assert(connectedEvent.has_value()); + const auto serverConnectionId = connectedEvent->event.connection_id; + assert(serverConnectionId != 0); + assert(WaitUntil(server, []() { return g_clientConnected && !g_clientFailed; })); + assert(co_gns_server_connection_count(server) == 1); + + const std::string reliable = R"({"type":"playerState","characterName":"Nomad"})"; + assert(networking->SendMessageToConnection( + g_clientConnection, + reliable.data(), + static_cast(reliable.size()), + k_nSteamNetworkingSend_ReliableNoNagle, + nullptr) == k_EResultOK); + const auto messageEvent = WaitForEvent(server, CO_GNS_EVENT_MESSAGE); + assert(messageEvent.has_value()); + assert(messageEvent->event.connection_id == serverConnectionId); + assert(messageEvent->payload == reliable); + assert(messageEvent->payload.find('\n') == std::string::npos); + + const std::string reliableResponse = R"({"type":"sessionReady","playerId":1})"; + assert(co_gns_server_send( + server, + serverConnectionId, + reliableResponse.data(), + static_cast(reliableResponse.size()), + CO_GNS_DELIVERY_RELIABLE_ORDERED) == CO_GNS_SEND_SENT); + const auto clientReliable = WaitForClientMessage(server, networking); + assert(clientReliable.has_value()); + assert(*clientReliable == reliableResponse); + + const std::string snapshotResponse = R"({"type":"transform","snapshotSequence":8,"x":1})"; + assert(co_gns_server_send( + server, + serverConnectionId, + snapshotResponse.data(), + static_cast(snapshotResponse.size()), + CO_GNS_DELIVERY_UNRELIABLE_SEQUENCED) == CO_GNS_SEND_SENT); + const auto clientSnapshot = WaitForClientMessage(server, networking); + assert(clientSnapshot.has_value()); + assert(*clientSnapshot == snapshotResponse); + + const std::string tooLarge((64 * 1024) + 1, 'x'); + assert(co_gns_server_send( + server, + serverConnectionId, + tooLarge.data(), + static_cast(tooLarge.size()), + CO_GNS_DELIVERY_RELIABLE_ORDERED) == CO_GNS_SEND_TOO_LARGE); + assert(networking->SendMessageToConnection( + g_clientConnection, + tooLarge.data(), + static_cast(tooLarge.size()), + k_nSteamNetworkingSend_ReliableNoNagle, + nullptr) == k_EResultOK); + const auto oversizeEvent = WaitForEvent(server, CO_GNS_EVENT_OVERSIZE_MESSAGE); + assert(oversizeEvent.has_value()); + assert(oversizeEvent->event.connection_id == serverConnectionId); + assert(oversizeEvent->event.payload_size == tooLarge.size()); + + assert(co_gns_server_disconnect(server, serverConnectionId, 1000, "test complete") == 1); + assert(WaitUntil(server, []() { return g_clientConnection == k_HSteamNetConnection_Invalid; })); + assert(!g_clientFailed); + + co_gns_server_destroy(server); + return 0; +}