GamerServices behaviour contract

CNA snapshot c1c316b9  ·  Deep Dives › Input, audio, media & services  ·  source links pinned to c1c316b9

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit c1c316b9; unless a sentence says otherwise, nothing here was built or executed. Checked by reading modules/gamer-services, docs/gamer-services-server.md, docs/gamer-services-known-limitations.md, the dispatcher harness and its regression test, and CNA's GamerServices audit record at c1c316b9. The code fragments were not compiled for this page; the test totals quoted are CNA's own recorded results, not re-run. The macOS evidence comes from CNA's Apple campaign record (plans/plan_apple_m4.md), which CNA's own known-limitations document has not yet absorbed.

XNA tied Microsoft::Xna::Framework::GamerServices to Xbox LIVE. CNA implements the namespace against a backend of its own: with nothing configured, gamers are local offline profiles, as on an Xbox 360 without Xbox LIVE; with a CNA service configured, they are accounts with service-backed achievements, leaderboards, friends, presence, messages, parties and online sessions. The service is CNA's own (cna-gamer-services-server, a separate C++ server an operator runs); it is not Xbox LIVE compatible and does not claim to be. This page is the behavioural contract, family by family: what each call does in each mode, what is persisted and how, and what is deliberately left out. It is for porters deciding what to keep; Tutorial 123 teaches the task and the gamer services internals trace ownership for maintainers.

Offline profiles and service accounts

Which mode a game runs in is decided outside game code. Configuration.hpp declares the settings (endpoint, game id, CA bundle, title version, avatar catalog updates), and ServiceConfiguration.cpp resolves them in this precedence: a complete CNA::GamerServices::setConfigurationOverride (for deployment and test hosts, before the dispatcher initialises), then the environment (CNA_GAMER_SERVICES_ENDPOINT, CNA_GAME_ID, CNA_GAMER_SERVICES_CA_BUNDLE, CNA_GAME_VERSION), then a title manifest (cna-title.json in the launch directory, or the file named by CNA_GAMER_SERVICES_MANIFEST), then the user's $XDG_CONFIG_HOME/cna/gamer-services.json. An empty endpoint selects offline mode. The rules a configured endpoint must meet:

  • HTTPS only. The endpoint is a full https://host:port/cna/v1 URL with no user, password, query or fragment; TLS 1.2 or later with certificate and hostname verification, no redirects, no proxies. Plain HTTP is accepted only for a numeric loopback host (127.0.0.1 or [::1]) with CNA_GAMER_SERVICES_INSECURE_LOOPBACK=1, for development; anything else fails with SECURE_TRANSPORT_REQUIRED. A CA bundle adds trust and never disables the checks.
  • No credentials in configuration. No configuration source accepts an account field; the game id is an identifier an administrator provisions on the service.
  • A target without a transport refuses the endpoint. A browser build, or an Android build without libcurl, refuses a configured endpoint (BROWSER_SERVICE_TRANSPORT_UNAVAILABLE, SERVICE_TRANSPORT_UNAVAILABLE) rather than running half-connected.
FamilyOffline (local profile)With a CNA service (account)
Signed-in gamersLocal profiles chosen in the Guide's sign-in or signed in automatically; IsSignedInToLive falseAccounts signed in through the Guide; IsSignedInToLive true; guests of an account possible
AchievementsLocal store, with an optional title catalogThe title's catalog provisioned on the service
LeaderboardsLocal store, written on every rating assignmentService boards, written only during network gameplay
GuideSign-in, message box, keyboard, achievements, the system GuideAlso friends, messages, gamer cards, reviews, invitations, parties
Presence, privileges, profileStored values; no online-session or purchase privilegePresence published; privileges are the account's policy; profile from the service
Friends, gamertag lookupGamerPrivilegeException or NotSupportedExceptionService-backed
Partner tokensNotSupportedException in both modes
AvatarsStandard avatars in both modes; see Avatars

The whole namespace, with the network module, is built only when CNA_ENABLE_NET is on (its default), and a game links CNA_GamerServices explicitly. The native service client needs libcurl 7.85 or newer with TLS, and online sessions and the event channel need libcurl 7.86 or newer with WebSockets (ws and wss); a build whose libcurl lacks them, Apple's system libcurl among them, refuses the relay by name (RELAY_SECURE_WEBSOCKET_UNAVAILABLE). A browser build has local profiles only, and iOS builds are configured without networking (CNA_ENABLE_NET=OFF; the iOS SDK has no libcurl).

Who is signed in

GamerServicesDispatcher::Initialize (GamerServicesDispatcher.cpp) creates no gamers. Without a service, a gamer exists only after a player picks or creates a local profile in Guide::ShowSignIn(paneCount, false), or when a profile is signed in automatically: an entry marked "autoSignIn": true in the profile store, or up to four comma-separated names in CNA_GAMER_SERVICES_AUTO_SIGN_IN (created when missing; meant for unattended runs and CI). Automatic sign-ins appear, and raise SignedIn, at the first Update, as XNA reports profiles already signed in at startup. A second Initialize throws InvalidOperationException.

  • Local profiles. Names are 1 to 15 ASCII letters, digits and single spaces, start with a letter and are unique ignoring case (LocalProfiles.cpp). The store is profiles.json under CNA_GAMER_SERVICES_PROFILES_DIR, else $XDG_DATA_HOME/cna/gamer-services/ or ~/.local/share/cna/gamer-services/ (%LOCALAPPDATA%\CNA\gamer-services on Windows); a profile also carries its own random avatar and optional GameDefaults. Local profiles are never sent to a service.
  • Accounts. With a service, sign-in uses accounts only; accounts are created by the service's operator, and there is no self-registration or password reset over the wire. Only a rotating refresh credential is kept between runs (never the password or an access token): in the desktop keyring (freedesktop Secret Service) on Linux when one answers, otherwise in owner-only 0600 files that are protected plaintext, not encrypted at rest (also on macOS, which does not use the Keychain); on Windows in files sealed with DPAPI, exercised under Wine only; a browser keeps nothing. Update restores up to four identities, renews access before it expires and sends a heartbeat every 30 seconds.
  • Guests. Only with a service: ShowSignIn(panes, true) lets a player type Guest to play as a guest of the first signed-in account ("Alice (1)", IsGuest). A guest earns no achievements, cannot join PlayerMatch or Ranked sessions and signs out with its account.

Gamer::getSignedInGamersProperty() lists the signed-in gamers, and indexing the collection with a PlayerIndex returns that player's gamer or nullptr. SignedIn replays every already-signed-in gamer to a handler that subscribes late, as XNA's add accessor does, and SignedOut is raised when a gamer signs out. A signed-out gamer is marked disposed but kept alive for the rest of the process, so pointers games, events and collections still hold do not dangle.

The social surface follows the mode. For a local profile, IsFriend and GetFriends() throw GamerPrivilegeException (the profile is not signed in to an online account) and Gamer::GetFromGamertag with its Begin/End pair throws NotSupportedException, because there is no directory to search. For an account they return the service's current snapshot. GetPartnerToken and its pair always throw NotSupportedException: no partner identity federation exists.

⚠

A copied gamer has its own, empty leaderboard writer. Every Gamer owns a LeaderboardWriter; copying or moving a gamer gives the new object a fresh writer instead of sharing the original's. Entries obtained from the original's writer stay with the original. Work with the pointers the signed-in collection hands out rather than with copies.

Presence, privileges and profile

GamerPresence

XNA exposes SignedInGamer.Presence as a get-only property whose object is mutable. C++ needs a non-const accessor for the same idiom, so CNA adds the CNAEXT overload GamerPresence& getPresenceProperty() next to the XNA-shaped const one. GamerPresence (GamerPresence.cpp) holds a 60-entry table of XNA's display strings in GamerPresenceMode declaration order, including the parameterised templates such as "Level {0}" and "Versus: Score {0}", with an empty entry for None. Setting PresenceMode selects the mode's string and setting PresenceValue stores the value; either marks the presence changed. For an account, the next Dispatcher.Update substitutes the value into {0} and publishes the text to the service, retrying later when the service queue is full; friends read it. A local profile has no one to publish to, so its presence is stored only.

GamerPrivileges

A local profile has no online-session and no purchase privilege; its other privileges keep their permissive defaults (GamerPrivilegeSetting::Everyone and true). An account's privileges are the policy its service states (the operator's stand-in for parental controls): communication, profile viewing and user content as Everyone, FriendsOnly or Blocked, plus trade, purchase and premium content, refreshed on renewal and heartbeat, with the account's block list muting voice from those players. A guest never has the online-session privilege, and as in XNA a game in trial mode reports AllowOnlineSessions false whatever the profile allows. Online sessions and the marketplace page enforce these values.

GamerProfile

Gamer::GetProfile() returns a caller-owned profile. For an account it comes from the service: gamer score, total achievements, titles played, the GamerZone the member chose, the gamer picture the operator assigned (as a caller-owned stream), and a Reputation computed from real CNA player reviews by CNA's own rule (5 × prefer / (prefer + avoid), rounded to a quarter star; 0 when nobody has reviewed the member). Accounts have no motto or region setter, so they report an empty motto and the region US. For a local profile, GamerScore and TotalAchievements total the earned entries of the offline store (scores only when the title ships a catalog), TitlesPlayed is 0 or 1, GamerZone is Unknown, Reputation 0, the motto empty, Region the host's, and GetGamerPicture() returns nullptr. These values are snapshots; no rating is invented.

// gamer is a SignedInGamer* from Gamer::getSignedInGamersProperty()
gamer->AwardAchievement("ACH_FIRST_BOSS_DEFEATED");     // offline: written to the local store now

gamer->getPresenceProperty().setPresenceModeProperty(  // CNAEXT non-const overload
    GamerPresenceMode::SinglePlayer);                   // an account publishes it at the next Update

The fragments on this page show the call shapes; they were not compiled for this page.

The offline store

Offline achievements and leaderboards persist in LocalGamerServicesStore.cpp, below StorageDevice::GetStorageRootEXT(), in a GamerServices directory with achievements/ and leaderboards/ beneath it. That root is the storage module's own per-user directory, not the SDL preference path (see where the root is), so StorageDevice::SetAppNameEXT relocates saved achievements and leaderboards together with save files; call it once, early, before any award, because renaming the application later starts a second, empty store. In a browser build that root lies in the IndexedDB-backed /cna-storage mount the storage module restores before main(), so the store follows the persistence saves get there (read from the source; not run in a browser for this page).

  • Files. One achievements file per gamertag, {"achievements":[{"key":…,"earnedTicks":…}]}, and one leaderboard file per (key)_(gameMode) holding each entry's gamertag, rating and typed columns. File-name components keep letters, digits, ., _ and -; every other byte becomes _, and an empty name becomes _. The mapping is many-to-one: it keeps a hostile gamertag inside the directory, it does not make identities unique.
  • Writes. Each update takes an exclusive lock on a sibling .lock file (an operating-system file lock, so separate processes are serialised too), refuses to replace an existing file it cannot read, writes the whole document to an adjacent .tmp file and renames it over the target. A failure is reported to the caller and leaves the previous file in place. This is atomic replacement on ordinary filesystems, not a crash or power-loss durability guarantee: no file is synced to disk.
  • Reads. A missing or malformed file reads as empty rather than throwing, and a record without the expected members is skipped; but an unreadable existing file is never overwritten by the next update, so a damaged history is preserved for inspection instead of being silently replaced.

Numbers are 64-bit integers

Earned ticks, leaderboard ratings and the 64-bit, DateTime, TimeSpan and outcome column values are written as JSON integers and read back exactly; double and float columns stay floating-point. A file written by an earlier CNA build that stored these numbers as doubles is still read when the stored value is integral, but bits lost at that time cannot be recovered. Values outside their type's range are skipped on reading: an earned time outside DateTime's range, an int32 column outside 32 bits, an outcome outside 0–3. Stream columns are not kept by the offline store.

Achievements

A title can ship GamerServices/Achievements.json in its title directory: an array of up to 1,024 objects with key, name, description, howToEarn, score (0–1000), display and picture (a title-relative PNG path), read through TitleContainer. With that catalog, offline GetAchievements() lists every defined achievement, earned or not, with its text, score and picture; AwardAchievement refuses a key the catalog does not define with GamerServicesNotAvailableException; Achievement::GetPicture() opens the PNG through TitleContainer. A malformed catalog is refused with an InvalidOperationException naming the file. Without a catalog, offline achievements are the earned keys alone: Name and Description are empty, the score is 0 and GetPicture() throws GamerServicesNotAvailableException.

Offline, awarding an achievement that is already earned keeps its first earned time, also when two writers race; a new award raises the Guide's "Achievement unlocked" toast. A guest cannot earn achievements (GamerPrivilegeException). For an account, awards go to the service's provisioned catalog for the title (at most 128 per title), are idempotent, carry a server-stamped first completion, and complete through the dispatcher like every other service request. BeginGetAchievements on a local profile refuses a second request while one is outstanding.

Service-side achievements and scores are authenticated client claims: the service checks that the signed-in account submitted them for its own title, not that the game play behind them happened. CNA records this as an accepted limitation of high severity: a modified or colluding client can submit plausible fabricated achievements, scores and Ranked reports, and there is no anti-cheat.

Leaderboards

LeaderboardWriter, LeaderboardReader and LeaderboardEntry behave differently in the two modes (LeaderboardWriter.cpp). For a local profile the policy is CNA's own, because the FNA code CNA ports from has no functioning PC implementation to inherit sorting or paging from:

  • Entries sort by rating, descending; ties have no defined order.
  • Ranks (getRankingEXTProperty(), 1-based) are assigned over the visible entries only.
  • A persisted entry whose gamertag matches no gamer currently signed in on this machine is omitted and takes no rank.
  • Reading centred on a pivot gamer centres the page on that gamer; if the pivot is absent, the page starts at the top.
  • The Begin/End pairs complete before Begin returns.
  • There is no submit call, because XNA's LeaderboardWriter has none: assigning Rating on an entry obtained from the writer persists it immediately, with no session needed, together with the entry's current columns. Column edits alone are therefore persisted only at the next rating assignment.
  • An entry read through LeaderboardReader has no persistence hook. Changing its rating changes the object in memory only; the file is untouched.

For an account, GetLeaderboard throws InvalidOperationException (“Leaderboards can only be written during network gameplay.”) outside a playing LocalWithLeaderboards, PlayerMatch or Ranked session. Writes made while playing are collected and committed when the host calls EndGame (or when a gamer leaves with IsLeaving), and the session's WriteUnarbitratedLeaderboard, WriteArbitratedLeaderboard and WriteTrueSkill events are raised in that commit. Service boards are provisioned per title, key and game mode with their own sort direction, best-or-latest aggregation and typed columns; reads page remotely (at most 100 entries per page) with global ranks. A Ranked row is written only when a strict majority of the reporting machines agree, CNA computes no TrueSkill, and the …Recent keys have no time window.

LeaderboardWriter& writer = gamer->getLeaderboardWriterProperty();
LeaderboardIdentity board = LeaderboardIdentity::Create(LeaderboardKey::BestScoreLifeTime);

LeaderboardEntry* entry = writer.GetLeaderboard(board);   // owned by the writer
entry->setRatingProperty(newHighScore);                   // offline: the assignment is the commit

The entry pointer stays valid while the writer (and so the gamer) lives. Entries returned by a reader hold borrowed Gamer* pointers; because signed-out gamers are kept alive for the process lifetime, those pointers stay valid after a sign-out, and the gamer then reports itself disposed.

The Guide

CNA draws its own system UI over the game, in a visual language of its own rather than a copy of the console's (Guide.cpp). A GamerServicesComponent in a game with a graphics device service installs an overlay that draws the Guide after the game's Draw. Every standard Guide::Show* call opens a real screen, and the Home key or a controller's Guide button opens the system Guide for that player without any call from the game (CNA_GAMER_SERVICES_GUIDE_BUTTON=0 turns that off). The system Guide has the gamer's portrait, gamertag, score and status and pages for Home, Friends, Party, Messages, Achievements, Leaderboards, Recent players, Game content and Settings, and its avatar editor (see Avatars).

  • One screen at a time. Any Show* or Begin* call while a Guide screen is up throws GuideAlreadyVisibleException. Guide::IsVisible is true while a message box, keyboard input, sign-in pane or any other Guide screen (the system Guide included) is shown, and throws if gamer services are not initialised.
  • Input ownership. While the Guide is visible it owns the keyboard, the pads and the mouse buttons: the game reads them released, touch input is withheld from it, and input still held when the Guide closes stays hidden until it is released.
  • Keyboard input. Typed text arrives through TextInputEXT, kept as UTF-16; Enter completes, Escape cancels. Password mode masks each displayed UTF-16 code unit with *, while EndShowKeyboardInput returns the real text. Title, description and default text must be shorter than 256 characters. Because a std::string cannot be null the way XNA's cancelled result is, call WasKeyboardInputCanceledEXT to tell a cancel from a confirmed empty string.
  • Message box. One to three non-empty button labels; the mouse, the keyboard or a controller moves the focus and chooses, and Escape, B or Back cancel. EndShowMessageBox returns the chosen index as std::optional<int>, empty when cancelled.
  • End waits. Calling End before the player answers runs the game's modal Guide frames (input and the Guide over the game, whose own Update and Draw are frozen) until the answer arrives, as a console keeps its Guide running while the game waits. With no running game to present the Guide, End throws InvalidOperationException instead of hanging. The caller owns the returned IAsyncResult* and deletes it after End.
  • Render state. The Guide and its notification toasts restore the title's blend, depth-stencil, rasterizer and first sampler states after drawing, so a game's own state survives the overlay.
System::IAsyncResult* pending = Guide::BeginShowKeyboardInput(
    PlayerIndex::One, "Enter passphrase", "Protects this save file",
    "", nullptr, std::any{}, /*usePasswordMode=*/true);

// Later, for example in Update once getIsCompletedProperty() is true
// (or at once: End then runs the Guide until the player answers):
const bool canceled = Guide::WasKeyboardInputCanceledEXT(pending);
std::string passphrase = Guide::EndShowKeyboardInput(pending);   // "" when canceled
delete pending;                                                  // caller-owned

A game without a GamerServicesComponent can still show the message box and the keyboard: it draws them itself, once per frame between SpriteBatch::Begin and End, with the CNAEXT hooks RenderPendingMessageBoxEXT and RenderPendingKeyboardInputEXT (device, SpriteBatch, SpriteFont and a white-pixel texture). The Guide's sounds are synthesized by CNA (CNA_GAMER_SERVICES_SOUNDS=0 turns them off) and its motion can be reduced with CNA_GAMER_SERVICES_REDUCED_MOTION=1.

Guide screens that need a service

ShowSignIn(paneCount, false) works offline, with a pane count of 1, 2 or 4; ShowSignIn(paneCount, true) requires the service. ShowFriends, ShowFriendRequest, ShowGamerCard, ShowComposeMessage, ShowMessages, ShowGameInvite, ShowParty, ShowPartySessions, ShowPlayers, ShowPlayerReview and the CNAEXT ShowAchievementsEXT throw GamerServicesNotAvailableException without a configured service and a signed-in account. ShowGameInvite(string) always throws NotSupportedException, as XNA supports it only on Windows Phone. ShowMarketplace throws GamerPrivilegeException without an account allowed to purchase; with one it opens the Guide's Game content page, because CNA runs no store and takes no payments. DelayNotifications delays the Guide's invitation prompts by up to 120 seconds.

The dispatcher and the component

As on XNA 4.0 for Windows, adding a GamerServicesComponent to Game::Components is the normal way in: its Initialize passes the window handle to GamerServicesDispatcher and calls GamerServicesDispatcher::Initialize with the game's services, which installs the Guide overlay, and its Update forwards to the dispatcher; neither override calls the base, as in FNA. A game that wants the Guide and avatars drawn also keeps a GraphicsDeviceManager, because both find the device through IGraphicsDeviceService. A program without a Game can call GamerServicesDispatcher::Initialize directly (see Tutorial 97).

GamerServicesDispatcher::Update() is the owner-thread pump. Each call latches the trial-mode state, publishes changed presence, applies the backend's completions and sign-in, sign-out and policy events, runs the invitation, party and social watchers, hands input back from a Guide closed by code and polls the Guide button. Results of service requests are delivered only there, on the thread that runs the game, and an End call that must wait pumps the dispatcher itself, so the synchronous XNA wrappers such as GetAchievements or NetworkSession::Create neither hang nor need a second thread. UpdateAsync() runs Update and returns whether gamer services are initialised. InstallingTitleUpdate is never raised, because CNA installs no title updates; a service may still refuse a game older than the title's minimum version, at sign-in and with GameUpdateRequiredException when a session starts.

The dispatcher's initialised flag is a process-lifetime static with no reset hook, so tests that initialise it run out of process. Four cases in GamerServicesDispatcherHangRegressionTest.cpp spawn gamerservices_dispatcher_harness.cpp as a separate process under a ten-second watchdog, one mode each: creating a Local session after initialisation does not hang, GetAchievements does not hang, a second initialisation is refused and frees nothing, and initialisation without a service signs nobody in and raises no SignedIn. The harness uses posix_spawn, so on Windows, Emscripten, Android and iOS the cases are compiled out rather than skipped.

Evidence and what it cannot show

The public headers and implementations establish the API shapes, the configuration rules, the offline file layout and locking, the Guide's behaviour and the refusals. CNA's own audit record (gamer-services-audit.md, 2026-10-02) reports the GamerServices suite at 649 passed, 0 failed, 0 skipped, the Net suite at 524/0/0 and the server's suite at 33 passed with 7 cases that need Linux network namespaces skipped, all on Linux; that was the last Linux run in which the server's tests drove real CNA processes. The server's own handoff qualification (2026-10-07, Linux loopback, commit 4ab12fd) passed 29 of 46 tests with the 17 CNA-driven ones skipped as unavailable and none failed, and CNA's Apple campaign records the client tests and the server suite on one Apple M4 (37 of 46 server tests passed, 9 skipped, none failed, with a WebSocket-capable libcurl). Those are recorded results, not re-run for this page. gamer-services-known-limitations.md is the authoritative list of what is missing, partial or unverified. What none of this establishes: interoperation with Xbox LIVE (there is none), the online client paths, credential stores or Guide input on Windows (not run there; the Windows credential store was exercised under Wine only), a public Internet deployment of the service (none is qualified, and no public instance is evidenced), or crash and power-loss durability of the offline store.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.