Skip to content

Search

Back

C++ Client SDK

Download | View the source

A C++17 client for custom engines, Steamworks games and native tools, and the core an Unreal plugin will wrap. Every REST operation is a method, generated from the same OpenAPI document as the other SDKs and named as the Godot SDK names it, so a game ported between engines calls the same thing. It covers sign-in, the realtime connection and server hooks, live key-value rows, presence, protobuf events and WebRTC, and it builds without exceptions and without RTTI.

Add it

Every change to Gamend puts the SDK on the latest release as gamend-cpp-sdk.tar.gz. Fetch it from CMake:

include(FetchContent)
FetchContent_Declare(gamend
URL https://github.com/appsinacup/gamend/releases/download/latest/gamend-cpp-sdk.tar.gz)
FetchContent_MakeAvailable(gamend)
target_link_libraries(my_game PRIVATE gamend::gamend)

latest moves with each change. To stay on one version, unpack the tarball into your tree and add_subdirectory(cpp_sdk), or build it once and cmake --install it, then find_package(gamend 1 CONFIG REQUIRED). The install carries the libraries it fetched, so those are found too. GAMEND_VERSION in gamend/version.hpp names the release.

It brings nlohmann/json, and libcurl and IXWebSocket for the transports it ships, each from your build when it already has them. IXWebSocket has no Schannel backend, so on Windows wss:// goes through OpenSSL when the build has one and through an mbedTLS built alongside the SDK when it does not. WebRTC is opt-in (GAMEND_WITH_WEBRTC=ON, through libdatachannel). An engine with its own HTTP, WebSocket or WebRTC stack turns those off and implements the matching interface instead.

Connect and sign in

#include <gamend/gamend.hpp>
gamend::Config config;
config.base_url = "https://game.example.com";
config.http = gamend::make_curl_transport();
config.websocket = gamend::make_ix_websocket_transport();
gamend::Client client(std::move(config));
client.auth().login_device(device_id(), [&](const gamend::AuthResult& r) {
if (!r.ok) return show_error(r.error);
client.realtime().connect();
});
// Once per frame, on the game thread:
client.poll();

Callbacks run inside poll(), on the thread that calls it, never on a network thread. That is where your game can touch its own state safely.

Besides a device id, Auth signs in with an email and password (login_email), a Steam session ticket (login_steam), and any configured provider (sign_in("google")), which opens the provider's page through config.open_url and waits for the player to finish. Signing in never links: link("google") and link_steam(ticket) add a provider to the signed-in account.

register_email makes an account and is not a sign-in: its callback gets the new account (models::Registration), and the session does not change. The password signs in with login_email once the player opens the emailed link; until then that fails with email_not_confirmed.

Calls and replies

Each method takes the path's parameters, then the request body, then the query, then the callback. Replies read as typed models:

client.api().lobbies_list_lobbies({{"title", "duel"}, {"page_size", 10}},
[](const gamend::Response& r) {
if (!r.ok()) return log(r.error); // "not_found", ...
if (auto page = r.page<gamend::models::Lobby>()) {
for (const auto& lobby : page->data) show(lobby.title, lobby.max_users);
log(page->meta.total_count);
}
});

gamend/models.hpp has a struct for every schema the API answers with. The untyped data(), meta(), code(), message() and errors() read the four response shapes too. A required field left out is not sent: the callback gets an error naming it.

Sessions

The access token lasts 15 minutes and the refresh token 30 days, unless the server sets otherwise. The SDK reads the access token's lifetime from expires_in and refreshes it before it lapses, and once more if a call answers 401. To stay signed in across runs, keep what on_session_changed hands you and give it back to restore:

client.auth().on_session_changed([](const std::optional<gamend::Session>& s) {
if (s) save_secret("gamend", gamend::dump(s->to_json()));
});
if (auto kept = gamend::Session::from_json(gamend::parse(load_secret("gamend")))) {
client.auth().restore(*kept);
}

Realtime

connect() joins the player's user:<id> channel. Join others by topic and listen to everything that arrives:

client.realtime().join_lobby(lobby_id);
client.realtime().on_event([](const gamend::Event& e) {
if (e.kind == gamend::events::LOBBY_MEMBER_JOINED) greet(e.payload);
});
client.realtime().call_hook("arena", "start", gamend::json::array(),
[](const gamend::HookResult& r) { if (r.ok) begin(r.data); });

Event::kind names the event as gamend::events does (see realtime). A dropped connection reconnects with backoff, with a fresh token if the old one would not do, and rejoins every topic it had; on_state reports each step. Set config.realtime_format = gamend::RealtimeFormat::Protobuf for binary frames: they are decoded before they reach you, with timestamps as unix milliseconds. Metadata and KV data your plugin sends in its own schema reach a decoder you register (register_metadata_decoder, register_kv_decoder).

Live rows and presence

client.kv().subscribe({"progress", user_id});
client.kv().on_change([](const gamend::KvKey& key, const gamend::KvRow& row) {
if (key.key == "progress" && row.exists) show(row.data);
});

A subscription survives reconnects, and kv().row(key) is the latest value from any source. presence() keeps merged player profiles, lobbies and who is online, filled from the realtime events.

WebRTC

With config.webrtc = gamend::make_libdatachannel_transport(), webrtc().connect() opens a DataChannel to the server, signaled over the user channel, and webrtc().call_hook calls hooks over it at lower latency than the socket (see WebRTC).

Tests

The SDK ships fake transports and a fake clock (gamend/testing/fake_transport.hpp), so your own tests can answer requests, play the server's frames and move time by hand. gamend_conformance <url> runs the scenario every Gamend SDK passes against a running server: sign-in, refresh, the socket, a lobby, hooks, KV, a dropped connection, and WebRTC when built with it. CI runs it on every change, in JSON and protobuf.