Skip to main content

Quick start

This walkthrough takes a native client from sign-in to a moving actor in a shared world. It follows the platform's standard flow — the same one the Replication API guide documents — with CrowdyCPP handling the mechanics.

The steps:

  1. Sign in with an identity client (session token, same GraphQL origin).
  2. Mint an app-scoped token and build a per-game client.
  3. Connect the native replication client (server assignment + UDP).
  4. Drive a world session from your game loop.

1. Identity client and sign-in​

Sign-in is auth().login(email, password) or registerUser(...), a magic link, or social/OIDC, and yields an identity session token. (devLogin was removed in CrowdyCPP 0.26.0 / CrowdyJS 15.0.0, along with the tier-side bypass.) There is one API origin — do not set managementUrl (removed in CrowdyCPP 0.20.0 / CrowdyJS 14):

#include <crowdy/crowdy.hpp>

using namespace crowdy;

ClientConfig identityCfg;
identityCfg.httpUrl = "https://ck.example.com";
CrowdyClient identity(std::move(identityCfg));

// Dev bypass — development/test servers only; production apps use the
// magic-link or social flows on client.auth().
auto login = identity.auth().login("player@example.com", password);

Then the age and the terms (ck-api v2.35.0 and v2.60.0, CrowdyCPP 0.57.0 and 0.75.0). No gameplay token is issued until the player has an age and has agreed to the current required legal documents, the Player Terms and the Player Privacy Policy; the mint in step 2 answers AGE_REQUIRED or LEGAL_ACCEPTANCE_REQUIRED until then. Show your own checkboxes, linking each document (auth().playerLegalDocuments(), and the app's own auth().appRequiredDocuments(appId)), and record the player's answer with the session token:

if (!identity.auth().playerLegalAcceptance(appId)) {
// Only after the player ticked the boxes: this records their agreement.
domains::PlayerConsents consents;
consents.acceptLegal = true;
consents.appId = appId; // the app's own documents too
identity.auth().recordPlayerConsents(consents);
}

An account with no age (users().me() reports ageBracket "unknown") gives its birth date and country once, from a neutral age screen, with auth().recordPlayerAge(PlayerAgeInput{birthDate, country, parentalConfirmation}); auth().registerUser(RegisterUserInput) takes the same answers at sign-up. The gate's refusals read with graphql::territoryNotSupportedOf, ageRequiredOf, ageRequirementNotMetOf (it surfaces no age), legalAcceptanceRequiredOf (with missingDocuments), safetyLogRequiredOf, and from 0.76.0 safetyModeratorRequiredOf and parentalConsentUnavailableOf, which returns the refusal with its reason; admin().apps().get() reads hasSafetyModerator, and platform().territoryPolicies() rows carry regionCode. See Terms, age and country.

2. App token and per-game client​

Gameplay requires a short-lived app-scoped token per app. Mint one and build a second client pointed at the app's Game API endpoint (the mint result tells you where that is — never hard-code it):

auto minted = identity.portal().mintAppToken(appId);

ClientConfig gameCfg;
gameCfg.httpUrl = minted.gameApiUrl;
gameCfg.discoveryUrl = minted.discoveryUrl; // shared origin if the instance dies
CrowdyClient game(std::move(gameCfg));
game.setToken(minted.token);

This is the platform's two-client pattern: one identity client (session token) for account and studio work, one client per game (app token) for all world and UDP calls. See Portals & app-scoped tokens.

Or start the session from the token in one call (CrowdyCPP 0.75.0), whichever way the game got it: a mint, a portal exchange, an organization's sign-in of its own player handed to the device, or signInWithMinecraft. crowdy::startGameSession builds the per-game client on the token's endpoints, stores the token, and opens the replication connection of step 3, waiting until it is connected:

#include <crowdy/game_session.hpp>

crowdy::GameSessionOptions options;
crowdy::GameSession session = crowdy::startGameSession(minted, std::move(options));
if (!session.ok()) { /* session.status says why */ }
crowdy::CrowdyClient& game = *session.client; // the app token's calls
auto conn = session.connection; // build the WorldSession of step 4 on it

With the app token, game.safety() holds the player's own controls (voice and video settings, a parent's consent, contacts, blocks and fileReport, each with an Async twin) and game.admin().apps().recording(appId) what the safety log records, for the notice and the indicator; a moderator's identity client uses client.moderation(). See Voice, video, contacts and blocks and Safety reports and moderation.

3. Connect the native replication client​

connect asks the Game API for a replication-server assignment (which also installs your UDP session server-side), opens the socket, and waits for session-ready:

replication::Config rc;
rc.appId = std::strtoll(appId.c_str(), nullptr, 10);
rc.token.token = minted.token; // 64-octet token = HMAC key
rc.token.gameTokenId = minted.gameTokenId;
rc.token.expiresAtEpochMs =
core::parseIso8601Millis(minted.expiresAt.data(), minted.expiresAt.size());

auto conn = game.replication().connect(rc);
while (conn->state() != replication::ConnState::Connected) {
conn->poll();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}

By default the SDK owns one network thread and you drain notifications with poll() from your game thread. Token refresh and reconnect commands are handled automatically — see Replication client for the details and the thread-free manual-pump mode.

4. Join the world and tick​

WorldSession layers SDK-managed game state over the connection: your actor with a fixed-Hz send loop, a remote-actor registry, a chunk cache, and message inboxes. Call tick() once per frame:

session::WorldSessionConfig sc;
sc.appId = appId;
session::WorldSession world(conn, &game, sc);

world.actors().onJoin([](const session::RemoteActor& actor) {
std::printf("+ actor joined\n");
});
world.actors().onLeave([](const session::RemoteActor& actor) {
std::printf("- actor left\n");
});

// Join at chunk (0,0,0) with an initial state payload. UnrealPose is the
// 88-byte layout shared with the Unreal SDK; any byte layout works.
session::UnrealPose pose;
world.join({0, 0, 0},
Bytes(reinterpret_cast<const std::uint8_t*>(&pose), sizeof(pose)));

double t = 0;
while (running) {
t += 0.05;
pose.positionX = 100.0 * std::cos(t);
pose.positionY = 100.0 * std::sin(t);
world.self().setState(
Bytes(reinterpret_cast<const std::uint8_t*>(&pose), sizeof(pose)));
world.tick(); // polls the connection, drives the send loop, reaps stale actors
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}

world.dispose();

setState only marks the state dirty; the session's send loop dedupes unchanged states, sends keyframes periodically, and falls back to cheap heartbeats while you idle — see World session.

The complete program is examples/walker.cpp in the SDK repository; run two instances against the same app and each prints the other's actor. To call your app's server code from the same client, see connect from a game.