Gamer services internals
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, the dispatcher harness, the CMake gates and CNA's own Gamer Services documents at the snapshot; the 36 test files (637 test-macro definitions at c1c316b9) were located, none executed. CNA records its own Linux qualification (GamerServices 649 passed, Net 524 passed, the companion server 33 passed and 7 skipped, 2026-10-02) and its Apple campaign's run on one Apple M4; neither was repeated here. Windows clients, real wide-area networks, Guide sounds heard by a person and avatar pixels on Direct3D, Metal and FNA3D remain unverified.
modules/gamer-services/ combines XNA-compatible signed-in gamer, Guide, achievement, leaderboard, friend, profile and avatar types with two backends behind one internal interface: local offline profiles, the default, and an optional CNA account service that a deployment configures outside game code. It is built together with the network module under CNA_ENABLE_NET. Neither backend is Xbox LIVE: the service side speaks CNA's own protocol to the separate, MIT-licensed cna-gamer-services-server, which an operator runs and provisions; no public instance of it is evidenced. This page is for maintainers who change sign-in lifetime, the local store, the service backend, the Guide or the asynchronous operations, and who need to know which objects own which pointers and which thread runs which callback. The user-facing view is Tutorial 123 and the GamerServices row of XNA compatibility; CNA's own operator and limitation records are docs/gamer-services-server.md and docs/gamer-services-known-limitations.md.
Composition and runtime entry
CNA_ENABLE_NET is declared in CMakeLists.txt with the description “Build CNA networking (GamerServices + Net, requires ENet)” and defaults to ON. modules/CMakeLists.txt enters gamer-services and net only inside that option, so the module is optional but present in an ordinary configure. Several presets in CMakePresets.json turn it off: dev, unit (and the presets that inherit it), release-modules, ios and ios-simulator; tests, web, macos, multi-renderer and cnaext leave the default. The C API is the one consumer that cannot do without it: the root file refuses CNA_BUILD_C_API=ON with networking off, because the C adapter links CNA_GamerServices (CMake options reference).
modules/gamer-services/CMakeLists.txt globs src/*.cpp recursively (65 translation units at this snapshot, in src/Xna for the XNA types and src/Internal for the backend, the Guide screens, the stores and the avatar catalog code) into a STATIC library CNA_GamerServices with the alias CNA::GamerServices. It links cna_build_config, cna_runtime and cna_storage publicly, so Guide and SignedInGamer reach Game, graphics, input and audio through the runtime layer and reach StorageDevice for the local store, plus the Sharp Runtime components Core.Base, IO, Collections.Core, Globalization, Runtime, Threading, Security.Cryptography and TimeZone. Three further edges belong to the newer parts of the module: libcurl 7.85 or newer is linked privately and defines CNA_SERVICE_TLS_TRANSPORT (required on desktop, optional on Android, never linked under Emscripten); every avatar catalog version under assets/avatars/v* is embedded into the library with cmake/EmbedBinaryFiles.cmake; and the GLB reader uses the cgltf implementation compiled into cna_content. With Clang or GCC, src/Internal/Avatars/AvatarModel.cpp is compiled with -ffp-contract=off, so a released avatar description assembles the same vertices on arm64 as on x86-64. With CNA_BUILD_TESTS and Python 3 the file also registers GamerServices_AvatarCatalogUpToDate, which checks the committed catalogs against tools/avatar_builder. Neither CNA_GamerServices nor CNA_Net appears in the _cna_runtime_parts list that defines the CNA umbrella, so a game links CNA_GamerServices explicitly (the module layout table says “linked explicitly, built with CNA_ENABLE_NET”).
It is also not an automatic Game component. An application adds a GamerServicesComponent to Game::Components; the component is the whole runtime entry:
Game + GamerServicesComponent (application adds it; nothing else does)
Initialize -> GamerServicesDispatcher::setWindowHandleProperty(Game window handle)
-> InstallingTitleUpdate handler: Game::Exit (CNA raises no title update)
-> GamerServicesDispatcher::Initialize(Game services) [does not call base]
second call -> InvalidOperationException
backend() (created once from resolveConfiguration())
no service: auto-sign-in local profiles (profiles.json / CNA_GAMER_SERVICES_AUTO_SIGN_IN)
installGuideOverlay(services) (IGameOverlay + the IGraphicsDeviceService it draws with)
isInitialized_ = true (process static, never reset)
Update -> GamerServicesDispatcher::Update() [does not call base]
latch trial mode, publish changed presence (accounts only)
backend->pump(): completions, SignedIn / SignedOut / Failed / PolicyChanged
service updates, invitations, parties, social, input ownership, Guide button
SignedInGamer::AwardAchievement / LeaderboardEntry::setRatingProperty
local profile -> CNA::Internal::GamerServices::Save*EXT -> StorageDevice::GetStorageRootEXT()/GamerServices/<json>
account -> backend job on the service queue; completion published at the next Update
Guide::Show* / BeginShowKeyboardInput / BeginShowMessageBox
-> Guide screen or game dialog -> drawn by the overlay after Game::Draw -> completion callback + End* result
Both component overrides deliberately skip the base implementation, matching the FNA source the module was ported from. Unlike that source, the dispatcher's Update is the module's pump: every service result, sign-in and sign-out reaches the game there, on the thread that runs Game::Update, and nowhere else. A gamer configured to sign in automatically is queued by Initialize but appears, and raises SignedIn, at the first Update, as on XNA. GamerServicesDispatcher::UpdateAsync calls Update only when initialised and returns the initialised flag; the polling loops of the offline operations still call it, and the service operations pump through it while an End* waits. InstallingTitleUpdate is never raised.
Offline profiles or a configured service
The choice is made once per process, outside game code. CNA/GamerServices/Configuration.hpp declares the public CNA::GamerServices::Configuration struct (endpoint, gameId, caBundle, insecureLoopback, avatarCatalogUpdates, maxAvatarCatalogBytes, titleVersion; no credential field) and setConfigurationOverride, resolveConfiguration and validateConfiguration. ServiceConfiguration.cpp resolves, in that header's order of precedence: a complete override set before the dispatcher initialises (meant for test and deployment hosts), then the environment (CNA_GAMER_SERVICES_ENDPOINT, CNA_GAME_ID, CNA_GAMER_SERVICES_CA_BUNDLE, CNA_GAME_VERSION, CNA_GAMER_SERVICES_INSECURE_LOOPBACK, CNA_AVATAR_CATALOG_UPDATES), then a title manifest cna-title.json in the launch directory or the file CNA_GAMER_SERVICES_MANIFEST names, then the user settings file $XDG_CONFIG_HOME/cna/gamer-services.json (or ~/.config/…). An empty endpoint selects offline mode.
Validation runs without connecting. The endpoint path must be exactly /cna/v1, with no user, password, query or fragment, at most 2,048 characters; the scheme must be https, except http on the numeric loopback addresses 127.0.0.1 or [::1] with insecureLoopback set (otherwise SECURE_TRANSPORT_REQUIRED); and the game id is a short identifier the operator provisions. A configured endpoint on a target without the TLS transport is refused outright: BROWSER_SERVICE_TRANSPORT_UNAVAILABLE under Emscripten, SERVICE_TRANSPORT_UNAVAILABLE on a native build without libcurl, such as an Android build that found none. CNA's operator document adds the trust rules this page does not re-derive: TLS 1.2 or newer with chain and hostname verification, caBundle configuring trust without bypassing checks, and only a refresh credential kept on the client (a desktop keyring through libsecret where available, otherwise 0700/0600 files that are not encrypted at rest, DPAPI on Windows, nothing in a browser).
GamerServicesBackend.cpp creates the one process backend lazily in backend() from the resolved configuration; serviceEnabled() is simply “the endpoint is not empty”. Work for the service goes through a bounded queue (128 entries, beyond which a call throws GamerServicesNotAvailableException) that a background std::jthread drains on native builds and that pump() drains on the calling thread under Emscripten; either way, results become BackendEvent completions that only GamerServicesDispatcher::Update delivers. That is the module's threading rule: game-visible state changes on the dispatcher's thread, while network I/O happens on the worker. A test can replace the backend with setBackendForTesting or makeFakeBackend, which is how the service paths are exercised without a server.
Signed-in gamers: slots, ownership and sign-in
GamerServicesDispatcher.cpp keeps four player slots and publishes them into Gamer::getSignedInGamersProperty(). Without a service nobody is signed in until the player creates or picks a local profile in the Guide (Guide::ShowSignIn) or a profile is configured to sign in automatically; there are no synthetic identities. Each sign-in arrives from the backend as an event and is applied in Update:
| Identity | Where it comes from | IsSignedInToLive | Online sessions | Achievements and leaderboards |
|---|---|---|---|---|
| Local offline profile | profiles.json (LocalProfiles.cpp): names of 1–15 ASCII letters, digits and single spaces, starting with a letter; "autoSignIn": true or CNA_GAMER_SERVICES_AUTO_SIGN_IN (at most four) | false | no; Local and SystemLink only | local store under the storage root |
| Service account | Guide sign-in against the configured endpoint; operator-provisioned, no self-registration | true | as the account's policy allows | service catalog and boards |
| Guest of an account | Guide sign-in with a service; leaves when its host account signs out | true | no | none |
A local profile never reaches a service, gets no online-session or purchase privilege, and carries its own game defaults; an account's privileges, block list and game defaults are the policy the service states and are refreshed by PolicyChanged events. NetworkSession still skips guests when it builds its default local-gamer list. SignedInGamerCollection::operator[](PlayerIndex) returns the gamer whose PlayerIndex matches, or null, as XNA does, so a collection with a lower player missing answers correctly.
Who frees a gamer
GamerCollection.hpp states the rule once for the whole GamerServices and Net family: every GamerCollection<T> and every type built on it (SignedInGamerCollection, FriendCollection, the NetworkSession views) is a non-owning view of T*. Destruction and Clear never delete an element. The creator of a gamer owns it through a registry of its own, and the header names the ones that exist: the dispatcher (the SignedInGamers it signs in), NetworkSession::ownedGamers_, ENetBackend::SessionState::OwnedRemoteGamers and a FriendCollection snapshot (the FriendGamers it was built with). The collection type also has no virtual destructor and must never be deleted through a base pointer.
For the dispatcher this produces an order a maintainer must not disturb. A sign-out or a replacement in a slot first nulls the slot, marks the old gamer disposed, republishes the collection and then raises SignedOut; guests of a departing account leave the same way. The old object is not deleted: it moves to a process-lifetime registry of retired gamers (ownedGamers in the dispatcher's translation unit), because game code, events and other collections may still hold its pointer. Pointers to signed-out gamers therefore stay valid but disposed, and the registry grows with every sign-in for the life of the process (CNA's own Gamer Services audit records this retention). A second Initialize is refused with InvalidOperationException, so the old free-before-replace path, and the test counter GetFreedGamerCountForTesting it fed, now stay at zero.
Two hazards remain from the same reading. First, Gamer::setSignedInGamersProperty is still public, but Update republishes the four slots into whatever collection is installed by calling Clear and Add, so an application collection published that way is overwritten at the next sign-in change; it is a test seam, not an API for games. Second, a Gamer must never be copied or moved once its LeaderboardWriter may be used: Gamer's constructor initialises that writer with this, no copy or move constructor re-points it, and the header says such an object's address must stay fixed. A SignedInGamer held by value (the internal factory returns one, as tests and the dispatcher harness use it) must not be copied or moved afterwards. SignedIn has a replay hook: a handler added late is told about every gamer already signed in, so subscribing after the component initialised no longer loses sign-ins.
The social surface depends on the backend, not on the type existing. On a local profile, GetFriends and IsFriend throw GamerPrivilegeException (the header comment that still promises an empty collection is stale), Gamer::GetFromGamertag throws NotSupportedException, and the GetPartnerToken family throws NotSupportedException on every backend. On an account, GetFriends returns a snapshot that owns its FriendGamer objects; presence, joinability, invitations and voice state come from the service.
Asynchronous operations and who frees their result
Every Begin* in this module returns a raw System::IAsyncResult* that the caller owns, as the synchronous wrappers show by holding it in a std::unique_ptr or deleting it after End*. That still differs from StorageDevice, whose Begin* members return std::unique_ptr<IAsyncResult>. What differs between the two backends is when the operation completes:
| Begin call | Local profile | Service account |
|---|---|---|
SignedInGamer::BeginAwardAchievement | Before returning: the award is saved first, then the callback runs | Queued; completes at a later Dispatcher.Update; the toast “Achievement unlocked” is posted when the service names the award |
SignedInGamer::BeginGetAchievements | Before returning; a second Begin before EndGetAchievements throws InvalidOperationException | Queued ServiceAsyncResult |
Gamer::BeginGetProfile, LeaderboardReader::BeginRead, BeginPageDown, BeginPageUp | Before returning | Queued |
Guide::BeginShowKeyboardInput, BeginShowMessageBox | Later, when the player answers the Guide dialog (or a test simulates it); End* waits by running the game's modal Guide frames | |
NetworkSession::BeginCreate and friends | See the net page | |
A ServiceAsyncResult keeps its shared operation state apart from the caller's result object: releasing a pending result cancels callback delivery instead of leaving a dangling target, its wait handle starts unsignalled, and CompletedSynchronously is false. Its End validates the operation family and the owning object, then pumps the dispatcher until the completion is published or a 30-second deadline passes (TimeoutException), so calling End from inside a service callback still progresses work without publishing nested sign-in events. For the offline path every re-entrancy fix keeps the old shape: the action pointer is captured (and the pending pointer cleared) before the callback runs, so a callback that immediately calls the matching End* or begins a new request sees consistent state. Tests named for this pattern include BeginAwardAchievementCallbackCanReentrantlyCallEndAwardAchievement, BeginGetAchievementsCallbackCanReentrantlyCallEndGetAchievements and the Guide's CallbackCanReentrantlyBeginANewMessageBox; ServiceAsyncLifetimeTests.cpp covers the queued results.
Awarding and reading achievements
SignedInGamer::AwardAchievement refuses a guest (GamerPrivilegeException) and then branches. For an account it asks the service, whose title catalog the operator provisions; an award is idempotent and the service stamps the first completion. For a local profile it consults the optional title catalog GamerServices/Achievements.json, read through TitleContainer (at most 1,024 entries with key, name, description, how-to-earn text, a score of 0–1,000, a display flag and a title-relative PNG picture; a malformed catalog throws InvalidOperationException naming the file). With a catalog, an unknown key throws GamerServicesNotAvailableException; awarding a key already earned keeps its first date and does nothing else; a new award writes the key and DateTime::getNowProperty().getTicksProperty() to the local store and posts the Guide toast. EndGetAchievements rebuilds the collection from the catalog, in its order with each entry earned or not, or, without a catalog, from the stored records alone, where only key, earned flag and earned time are real. An account's earned time arrives in UTC from the service and is converted to local time, matching the offline record. The polling loop in GetAchievements remains, but the offline action is marked complete at construction, so it never spins; the out-of-process regression tests described below pin that.
Local JSON store and its failure model
LocalGamerServicesStore.cpp (declared in LocalGamerServicesStore.hpp) is a set of free functions in CNA::Internal::GamerServices serving local profiles. Its root is StorageDevice::GetStorageRootEXT() plus /GamerServices, with two sub-directories:
| Data | Path under the root | Content |
|---|---|---|
| Earned achievements | achievements/<gamertag>.json | {"achievements":[{"key":"...","earnedTicks":N}]} |
| Leaderboard entries | leaderboards/<key>_<gameMode>.json | {"entries":[{"gamertag":"...","rating":N,"columns":[{"key":"...","type":"...","value":V}]}]} |
File-name components go through SanitizeStoreFileNameComponent: letters and digits (as reported by std::isalnum), -, _ and . are kept and every other byte becomes _, an empty result becomes _. The mapping is many-to-one; local profile names (letters, digits and single spaces, unique ignoring case) narrow the exposure, but two names that differ only in a space and an underscore would still share one file, and the sanitiser is what keeps a hostile name from leaving the directory. The leaderboard file key is the sanitised LeaderboardIdentity key, an underscore and the game mode. PropertyDictionary columns persist as one of eight tagged types (int32, int64, double, single, string, dateTime, timeSpan, outcome); a Stream* value is skipped on write (offline boards keep no stream columns), and an unknown type tag on read is skipped rather than raising.
The storage root comes from the storage module, not from the platform layer: the environment-variable chain in StorageDevice::EnsureStorageRoot (see Storage internals and the user-level storage-root chain) followed by the app folder, which is the literal name game unless the game called SetAppNameEXT. The store recomputes its root on every call and caches nothing, so changing the app name after data was written silently starts a second, empty store; call SetAppNameEXT once, early. In a browser build the root is the IndexedDB-backed /cna-storage mount, so the store should survive a reload when that mount is available; the profile list itself (profiles.json) is resolved by LocalProfiles.cpp from its own environment chain and lies outside it (both are source readings, not runs).
Failure model
- Retrieval is forgiving. A missing or unparseable file yields an empty list for reads; a record without the expected member types is skipped. Tests cover a corrupt achievement file (
GetAchievementsHandlesMissingOrCorruptStoreFileGracefully). - Updates are not. Before a save,
ReadForUpdaterefuses to treat an existing but unreadable history as empty: it throws (“Cannot update unreadable GamerServices progress.”) instead of overwriting it, so a corrupt file is preserved rather than replaced by the next save. - Writes report failure.
WriteJsonFilecreates the directory, writes<name>.json.tmpwith stream exceptions enabled, flushes, and renames it over the target; any failure removes the temporary file and propagates, and the previous record is never truncated. The source comment is explicit that this is an atomic replacement on supported filesystems, not a power-loss durability guarantee. - Every save is a locked read-modify-write of the whole file.
LocalStoreLocktakes an exclusive lock on<file>.lock(flockon POSIX,LockFileExon Windows, refusing a symbolic link or a non-regular file; a process-local mutex elsewhere), so concurrent writers on one machine no longer lose each other's updates. - Numbers are integers. The store writes ratings, int64 columns and tick counts as JSON integers through
nlohmann::json, so 64-bit values round-trip exactly; for files written by older revisions, an integral floating-point number is still accepted, but a value that was already rounded cannot be recovered. - Test isolation is the tests' job.
ResetStoreForTestingEXTremoves the entireGamerServicesdirectory. The gamer tests wrap their cases in a guard (GamerServicesStoreGuard) that sets the app nameCnaTestsGamerServicesand resets the store before and after, which places the data under the running user's real storage root.
Leaderboards: writer, entry and reader
LeaderboardWriter::GetLeaderboard returns a pointer to a LeaderboardEntry stored by value in a std::map owned by that writer (so it is stable while the writer lives), and its persistence depends on the owner. For a local profile, first access seeds the entry from the store (rating 0 and no columns if nothing was written) and installs a hook so that setRatingProperty saves the rating and the current columns for the owning gamertag, with no session needed; column edits persist only at the next rating set. For an account, GetLeaderboard throws InvalidOperationException (“Leaderboards can only be written during network gameplay.”) outside a playing LocalWithLeaderboards, PlayerMatch or Ranked session; inside one, edits are collected as drafts and committed at the host's EndGame, or when the gamer leaves, by the network module (the net page).
LeaderboardReader.cpp reads service boards in pages from the service (global ranks, ascending or descending as provisioned, at most 100 rows a page) and local boards from the store on every BeginRead: it loads the leaderboard's entries, sorts them by rating descending with std::sort (ties have no defined order), and matches each stored gamertag against the currently signed-in Gamer* collection. A local record with no signed-in match is skipped and takes no rank, so ranks are contiguous over the visible entries only: an offline board shows only profiles signed in on this machine. The pivot overload centres the page on the pivot gamer (start 0 when the pivot is absent); the gamer-list overload restricts the board to the given gamers and marks it a friends board. The sort order, pivot centring and friends restriction of the local path are CNA-original: the referenced FNA stub throws everywhere. The reader's constructor keeps FNA's slice bound (i < pageSize), which is only correct for a first page, so every Begin* that supplies a real start calls ResliceEntriesEXT afterwards. Entries hold borrowed Gamer* pointers; because signed-out gamers are retired rather than deleted, those pointers stay valid but may name a disposed gamer.
The Guide is a CNA-drawn system UI
Guide.cpp is the XNA surface; the screens live under src/Internal/Guide/ and are drawn by GuideOverlay.cpp. Dispatcher::Initialize registers that overlay in the game's services as CNA::Internal::Runtime::IGameOverlay together with the game's IGraphicsDeviceService; Game draws it after the application's Draw and before presentation, and reports IsActive false while a modal Guide screen is visible. A game without a GraphicsDeviceManager therefore has no Guide picture, and a game without a GamerServicesComponent can still show its own dialogs through the RenderPending*EXT calls below. getIsVisibleProperty throws InvalidOperationException before the dispatcher is initialised and is otherwise true while any Guide screen or dialog is open; every Show* and BeginShow* throws GuideAlreadyVisibleException while it is. The look is CNA's own visual language, not the console's.
Keyboard input
BeginShowKeyboardInput validates the player and the 256-character limits on title, description and default text, then opens the Guide keyboard dialog. The pending request starts TextInputEXT, seeds its UTF-16 buffer from the default text and subscribes to TextInputEXT::TextInput, which fires from the engine's own event pump (Game::PollEvents), so typing works without any Guide call from the game; the Guide's on-screen keyboard feeds the same handler. Enter (code 13 or 10) confirms; Backspace edits and, for a surrogate pair, removes both halves; Home, End, Tab, Ctrl+V and Delete are ignored rather than inserted; everything else is appended. Completion unsubscribes from TextInput and calls StopTextInput at that moment, not when the game later calls End, so text capture stops immediately.
EndShowKeyboardInput may be called once per result. If the answer has not arrived it waits: PrepareForEnd runs the game's modal Guide frames (input and the Guide over a cleared screen, the game's own Update and Draw frozen) until the dialog completes, and when no running game can present the Guide it throws InvalidOperationException instead of hanging. A cancelled request returns an empty string, so a caller must ask WasKeyboardInputCanceledEXT to tell cancel from an empty confirm; password mode masks the display with one * per UTF-16 code unit while End returns the real text. RenderPendingKeyboardInputEXT(device, spriteBatch, font, whitePixel) survives for games that draw dialogs themselves: it draws the CNA system dialog into the caller's begun batch, and its font and texture arguments are no longer used. A test or headless run can call SimulateKeyboardInputCancelEXT or raise the text event with a carriage return.
Message box
BeginShowMessageBox requires a non-empty title and text shorter than 256 characters, one to three buttons and a valid focus index, and stores title, text, buttons, focus and icon (the overload without a player shows the box for player one). It completes when the player picks a button in the dialog, or when SimulateMessageBoxClickEXT(index) is called (range-checked). EndShowMessageBox returns a std::optional<int> button index, empty when the box was cancelled, and waits for the answer through the same modal frames as the keyboard. RenderPendingMessageBoxEXT draws the dialog into a caller's batch for games without the overlay.
Input ownership and lifetime
A real console shell owns the screen while the Guide is up, so the module withholds input from the game for as long as any Guide screen is visible: SyncTouchInputSuppression calls TouchPanel::setInputSuppressedEXT and syncSystemInputOwnership, which gives the game neutral keyboard, gamepad and mouse-button states while the Guide owns them; input still held when the Guide closes stays hidden until it is released. When something new appears on top of the Guide, its input reader primes itself, taking every key and button already down as held, so the press that opened a dialog cannot also answer it; “new” is judged by a creation serial carried by every Guide request and screen, not by text or frame time (AnIdenticalMessageBoxOpenedWhileEnterIsDownDoesNotAnswerItself). The click that answers a message box is withheld until the mouse button is released, so the release does not arrive as a tap on whatever was behind the box. The Home key or a gamepad's Guide button opens the system Guide in games that attached the overlay (CNA_GAMER_SERVICES_GUIDE_BUTTON=0 turns that off). After drawing, the overlay restores the title's blend, depth-stencil, rasterizer and sampler-0 states. Tests include AVisibleKeyboardPromptWithholdsTouchInputFromTheGame, AVisibleMessageBoxWithholdsTouchInputFromTheGame, TheClickThatAnswersAMessageBoxDoesNotAlsoReachTheGame, ARaisedOverlayDiscardsGesturesAlreadyQueued and the suites GuideInputTests.cpp and GuideRenderStateTests.cpp. Because input subscription, input ownership, the static pending pointers and the caller-owned result all outlive a single call, a change to any Guide path needs cases for reentrant callbacks, cancel, subscription removal and the graphics objects the overlay borrows. The Reset*ForTestingEXT helpers clear the pending pointers (the keyboard one also removes the subscription) without deleting the action or stopping text input; they are not a cleanup path for production code.
System pages behind the Show* calls
Every standard Show* opens a real Guide page: ShowSignIn (pane count 1, 2 or 4; an online-only sign-in without a service throws GamerServicesNotAvailableException), ShowFriends, ShowFriendRequest, ShowGamerCard, ShowComposeMessage, ShowMessages, ShowGameInvite(player, recipients) (which needs an online session), ShowParty, ShowPartySessions, ShowPlayers, ShowPlayerReview, ShowMarketplace (CNA's “Game content” page: licence, installed avatar catalogs and a test purchase while simulating trial mode; there is no store or payment) and the CNAEXT ShowAchievementsEXT. The social pages throw GamerServicesNotAvailableException without a service or a signed-in account, and ShowMarketplace throws GamerPrivilegeException without a purchase privilege. ShowGameInvite(sessionId) always throws NotSupportedException, as it is Windows Phone only in XNA. DelayNotifications defers the Guide's own notifications for at most 120 seconds. IsTrialMode is latched from SimulateTrialMode at each dispatcher Update, NotificationPosition places the toasts, and IsScreenSaverEnabled goes through the platform layer's display service (true when the platform supplies none). The Guide also hosts the full-screen avatar editor for accounts and local profiles; no XNA API was added for it.
A safe human modification route
CnaGamerServicesTests is the focused target for this module's 36 test files (634 test-macro definitions by a line-anchored TEST/TEST_F/TEST_P count; not executed for this page). It is an EXCLUDE_FROM_ALL executable produced by cmake/UnitTests.cmake, which groups every modules/<name>/tests tree into an object library and links the focused executable against CNA_GamerServices only; the same files also feed the aggregate CnaTests. With CNA_ENABLE_NET=OFF the script filters the GamerServices and Net test sources out of the aggregate and fails the configure if any survive, so a green NET=OFF run says nothing about this module. Note the preset consequence above: cmake --preset unit configures with networking off and therefore has neither target.
GamerServicesGamerTests.cpp(99 macros) coversGamer,SignedInGamer, the local store through achievements, andLeaderboardWriter,LeaderboardEntryandLeaderboardReader, including persistence across fresh objects, per-gamertag and per-identity isolation, sorting, pivot, restriction and paging.GamerServicesServiceTests.cpp(57 macros) covers the dispatcher's un-initialised behaviour and the Guide dialog paths described above, driven through simulation helpers and a canned platform mouse.LocalProfileTests.cpp,ServiceConfigurationTests.cpp,ServiceProtocolTests.cpp,ServiceBackendAuthorityTests.cpp,ServiceAsyncLifetimeTests.cpp,CredentialStoreTests.cpp,SystemGuideTests.cpp, theGuide*Tests.cppfamily and the avatar suites cover profiles, configuration, the service backend against a fake, credentials, the Guide pages and avatars; collection, data, enum, event-argument and exception suites live in the same directory.- Where
Initializeitself is tested: four cases inGamerServicesDispatcherHangRegressionTest(net tests) spawn a separate process runninggamerservices_dispatcher_harness.cppin a mode each, with an empty configuration override: session creation afterInitializedoes not hang,GetAchievementsdoes not hang, a secondInitializeis refused and frees nothing, andInitializewithout a service signs in nobody and raises noSignedIn. They belong toCnaNetTests, and the harness executable is registered incmake/Harnesses.cmakewhenever networking and tests are on. The test source itself is filtered out ofCnaTestson Windows, Emscripten, Android and iOS because it spawns processes with POSIX APIs, so on those hosts the four cases do not exist rather than being skipped.
Recipes. For a dispatcher lifetime change, extend the harness modes rather than adding an in-process test, then check the Guide, leaderboard and network consumers that hold borrowed pointers. For a persistence change, keep the test guard's isolated app name, exercise missing, corrupt and truncated files and a concurrent writer, and inspect both the achievement and the leaderboard formats. For a service change, use the fake backend for the client and the companion server's own CTests for the protocol; CNA's final Linux qualification ran those on one Linux host with the namespace-isolated gates skipped, and its Apple campaign ran them on one Apple M4 with a WebSocket-capable libcurl. For Guide, test callback re-entrancy, cancel, input-ownership hand-back, render-state restoration and the lifetime of the graphics objects the overlay borrows. A green local suite cannot establish behaviour against a public Internet deployment or on Windows clients.
Competitive trust is the client's. Achievements and leaderboard scores sent to a service are authenticated, validated client claims: a modified or colluding client can still submit plausible outcomes, and Ranked rows are written only on a strict majority of reporting machines, not through TrueSkill. CNA's own audit accepts this as a known high-severity limitation of the design rather than a defect; do not describe the service as anti-cheat.
Curated source tour and remaining uncertainty
gamer-services/CMakeLists.txt, the composition block ofmodules/CMakeLists.txtand the NET filter inUnitTests.cmake: establish the optional build, the libcurl edge and the test gate.Configuration.hpp,ServiceConfiguration.cppandGamerServicesBackend.cpp: how offline and service mode are chosen, and the queue every service call goes through.GamerServicesComponent.cpp,GamerServicesDispatcher.cpp,LocalProfiles.cppandGamer.cpp: trace initialisation, the slots, retired gamers and the published collection, then readGamerCollection.hppfor the ownership rule.SignedInGamer.cpp,LocalGamerServicesStore.cpp,LeaderboardWriter.cppandLeaderboardReader.cpp: connect each operation to its offline file or its service request.Guide.cpp, thenGuideOverlay.cpp, the screens insrc/Internal/GuideandTouchPanel.cppforsetInputSuppressedEXT: the dialogs, the system pages and input ownership.The dispatcher harnesswith its regression test: the only placeInitializeruns under test.
What this page does not establish
This trace covers the sign-in, configuration, local-store and Guide boundaries. It does not audit the avatar family or the service wire protocol. For orientation only: AvatarDescription holds CNA's own versioned 1,021-byte format (not Xbox avatar data), and AvatarRenderer in AvatarRenderer.cpp moves from Loading to Ready or Unavailable and draws the 71-bone rig from the original CNA catalogs embedded in the library (generated by tools/avatar_builder, under the repository's own licence), with public bone transforms in XNA's coordinate space; CNA records avatar renders on OpenGL 3.3, Vulkan, SOFTWARE, SDL_GPU and WebGPU, and none on Direct3D, Metal or FNA3D (SDL_RENDERER cannot draw the 3D avatar). Whether the service behaves correctly over the public Internet, how the Guide behaves on Windows (on macOS CNA's Apple campaign records the Guide and gamer-services UI tests passing on Metal), and whether its sounds and voice are heard through real devices were not verified here, and CNA's own records state them as unqualified. Local-disk behaviour on Windows or macOS is likewise outside this page. The user-level statement of what works is Tutorial 123; ownership across the whole tree is in the ownership map, callback and thread rules in the thread and callback map, and the module list in the module index. For a first regression test see I need to add a regression test; the Maintainer Handbook lists the other routes.
Evidence level: everything above was checked by reading the TARGET sources at c1c316b9; nothing was built or executed, and the test names and counts are source facts, not pass results.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Avatars: the standard XNA avatar API on CNA's own avatars — How CNA's XNA avatar classes draw original CNA avatars on XNA's 71-bone rig: descriptions, the renderer, the 31 presets, catalogs and the editor, plus the separate SkinnedModelEXT extension and the evidence behind both.
- GamerServices behaviour contract — What CNA's GamerServices does with local offline profiles and with a configured CNA service: sign-in, configuration and credentials, the offline store, achievements, leaderboards, the Guide and the dispatcher.
- Network sessions and the SystemLink protocol — What each NetworkSessionType does in CNA, online sessions through the CNA service relay, the SystemLink wire and discovery protocols, the trust model, delivery guarantees, host migration and the evidence behind them.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-016: The .cnj JSON parser (CNA::Internal::ParseJson) recurses without a nesting limit, so a small crafted document overflows the stack — JsonParser recurses once per nesting level with no ceiling; a document of 100,000 nested arrays (200,000 bytes) crashed a probe with SIGSEGV instead of throwing JsonParseException, and with the default 8 MiB stack at -O0
- CNA-BUG-050: Guide message-box and keyboard-input results expose an AsyncWaitHandle that is already signalled while the answer is pending — Guide's message-box and keyboard-input results construct their AsyncWaitHandle already signalled, so WaitOne returns while IsCompleted is still false; End* now waits by running the game's modal frames and refuses only wh
- CNA-BUG-154: A save container titled GamerServices shares its directory with the gamer-services store, so DeleteContainer("GamerServices") deletes local achievements and leaderboards — The gamer-services store lives at <root>/GamerServices, where a StorageContainer titled GamerServices is also created, so deleting that title removes every persisted achievement and leaderboard entry.
- CNA-BUG-168: LocalGamerServicesStore maps distinct gamertags to one achievements file when they differ only in characters outside A-Z, a-z, 0-9, '.', '_' and '-' — SanitizeStoreFileNameComponent replaces every other byte with an underscore, so gamertags such as "Ann B" and "Ann_B", or two same-length non-ASCII names, share one achievements file and see each other's achievements.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Tutorial 123: achievements and leaderboards with local persistence · XNA compatibility: GamerServices · Storage: where the root is
- Architecture
- Architecture: module layout · Architecture overview
- Internals
- Network session internals (the other half of CNA_ENABLE_NET) · Storage internals · Phone compatibility internals
- Maintainer workflow
- Ownership and lifetime master map · Thread and callback map · I need to add a regression test · Maintainer Handbook
- Tests and validation
- Test architecture and change recipes · What to test after changing X
- Reference
- Module index · Test target index · CMake option index