Skip to content

Latest commit

 

History

History
268 lines (233 loc) · 19.6 KB

avatars.md

File metadata and controls

268 lines (233 loc) · 19.6 KB

Avatars

CNA implements the XNA 4.0 avatar API (AvatarDescription, AvatarAnimation, AvatarRenderer) with real behaviour on original CNA avatars. It is not an Xbox LIVE avatar system: it neither reads nor produces Xbox avatar data or assets, and every avatar asset is original CNA work generated by tools/avatar_builder/. On Windows, XNA's avatar classes were stubs; CNA implements the behaviour XNA's documentation describes for the Xbox 360 (never traced on a console), with avatars that resemble that era in original art. What remains limited (baked shading, catalog edge cases, likeness) is in gamer-services-known-limitations.md.

Who owns what. The CNA service owns each account's AvatarDescription and its revision; the CNA runtime owns the standard, immutable avatar catalogs (meshes, textures, animations), compiled into every release. AvatarDescription.BeginGetFromGamer retrieves only the description, and AvatarRenderer draws it with the locally installed catalog of exactly the version it names. Normal rendering never downloads a per-user model or asset. The service's catalog packs are only an optional, versioned update path for a catalog a client does not have (below).

Descriptions

AvatarDescription keeps the XNA contract: an opaque buffer of exactly 1021 bytes, IsValid true when the first byte is non-zero, Height in meters from the feet to the top of the head, and BodyType. Inside the buffer CNA stores its own versioned encoding. There are two formats, and no newer one: format 1 and format 2 share bytes 0–42; format 2 adds individuality format 1 cannot express:

Bytes Field
0 format: 1, or 2 when facial hair or a face shape is set — non-zero, so XNA's IsValid rule holds
1–3 C N A
4 body type (0 female, 1 male)
5–6 height in millimeters, little endian (1450–2050)
7 build, 0 (slim) to 255 (heavy); 128 is the authored body
8–9 asset catalog version the item ids refer to
10–30 RGB colors: skin, hair, eyes, top, bottom, shoes, accessory
31–42 catalog item ids, 16-bit: hair, top, bottom, shoes, glasses, hat (0 = none, accessories only)
43–44 format 2: facial-hair feature item id (0 = none)
45–60 format 2: face shape, one byte each, 128 = as authored: head width, head height, jaw width, chin size, cheek fullness, eye size, eye spacing, eye height, eye tilt, brow height, nose size, nose width, nose height, mouth width, mouth height, ear size
up to 1016 reserved, zero (from byte 43 in format 1, from 61 in format 2)
1017–1020 CRC-32 (IEEE) of bytes 0–1016, little endian

CreateRandom() and CreateRandom(bodyType) produce valid descriptions from the newest catalog compiled in, with a random face and, for male bodies, sometimes facial hair; passing a description's Description bytes to the constructor reproduces the same avatar. A description that uses no format 2 field is written as format 1, which every CNA build reads. A buffer that is not a CNA description (another format, a damaged checksum, non-zero reserved bytes) keeps XNA's IsValid answer, which only looks at the first byte, but reports height 0 and the female body type, and a renderer given it is Unavailable. CNA builds older than format 2 read format 2 descriptions that way; the service hands them a format 1 copy instead (below).

Descriptions from the service

AvatarDescription.BeginGetFromGamer/EndGetFromGamer read a gamer's avatar from the configured CNA service (see docs/gamer-services-server.md): a signed-in gamer, a gamer returned by Gamer.GetFromGamertag, or a remote NetworkGamer met in an online session all carry a service identity. The operation completes during GamerServicesDispatcher.Update (never synchronously) and EndGetFromGamer returns the stored description. A gamer without an avatar, without a service identity (a remote System Link gamer), or an unreachable service yields an invalid, all-zero description, so games take their usual "no avatar" path. A signed-in local offline profile (no service configured, see docs/gamer-services-server.md) has the avatar stored with the profile, created at random when the profile is, and the operation completes synchronously with it. The service stores one avatar per account and accepts only CNA descriptions (formats 1 and 2) whose items exist in a catalog it has imported; players change theirs with the CNA avatar editor (below) and administration can assign one (cna-gamer-services-admin <db> avatar <user> random).

Games that share avatars in a session the XNA way (sending Description bytes to other machines and constructing an AvatarDescription from them) need nothing from the service at all.

As in XNA, each signed-in player index keeps the description EndGetFromGamer returned for it until that gamer's avatar changes; other gamers get a fresh description on every read. GamerServicesDispatcher.Update reads a kept avatar again every 10 seconds (the service's copy, or the local profile's store, which the CNA avatar editor writes) and, when it differs, empties the slot and raises that description's Changed on the dispatcher thread with the SignedInGamer as sender. The C++ description is a value, so its Changed is shared by every copy of it (subscribing or unsubscribing through any copy is subscribing to the one event). An unchanged avatar, a new catalog, or an unreachable service raises nothing; signing out empties the slot without an event.

The avatar editor

The CNA avatar editor is a system screen of the Guide: Edit avatar on the Guide's Home opens it over any game for a signed-in account or local profile, and cna_avatar_editor is the same screen as a program of its own, as the console's editor was. No game contains customization code and no XNA API is added for it. Opening it over a running game rather than leaving the game is CNA policy.

The avatar stands large on a lit stage, idle-animated (a wave on opening, a small gesture now and then, a cheer on saving). Beside it are twelve categories -- Body, Skin, Face, Eyes, Nose & mouth, Hair, Facial hair, Tops, Bottoms, Shoes, Glasses, Headwear -- and each category's choices:

  • cards rendered by the standard AvatarRenderer wearing that item (styles, body type, and the face, eye and nose-and-mouth shapes, which move several face features together: Balanced resets them);
  • swatches for skin, hair, eye, clothing and accessory colors;
  • sliders for height (1.45-2.05 m), build and each face feature, labelled in words ("A little wide", "Very broad") rather than numbers.

What the focus rests on is tried on at once; Accept keeps it, and leaving the row without it goes back. The camera eases to what the category is about -- the whole avatar, head and shoulders, the top, the bottoms, the shoes -- and the player turns it (right stick, Z/C, dragging) and zooms (triggers, +/-, the wheel). X shuffles the category shown, Y randomizes the whole avatar coherently (natural hair and eye colors mostly, clothes in colors that go together, a face within its usual range, accessories now and then) and keeps the body type. Catalog 1 offers no face shapes or facial hair, and says so.

It edits against the newest catalog the avatar's owner accepts -- the service's newest imported catalog for an account (the one it validates a save against; a catalog this computer lacks is installed as a pack first), the newest compiled-in one for a local profile -- and carries an older catalog's avatar into it item by item. Saving is transactional: B asks to save, discard or keep editing when anything changed; the service's avatars.set (one change per account every two seconds) or the local profile store either holds the new avatar, or the editor explains why not and keeps the edits. When the stored avatar changes elsewhere while the editor is open, an untouched editor shows the new one and an edited one says that saving replaces it. Running games see a saved avatar through AvatarDescription.Changed.

cna_avatar_editor is built with the examples of a 3D renderer (CNA_BUILD_EXAMPLES). --capture DIR walks the categories, tries on and keeps choices, randomizes and saves, writing a PNG of each step; with CNA_GAMER_SERVICES_AUTO_SIGN_IN naming a profile and CNA_GAMER_SERVICES_PROFILES_DIR a scratch store it runs unattended on a private display.

Rendering

AvatarRenderer needs nothing beyond the XNA API. A game that creates a GraphicsDeviceManager, adds a GamerServicesComponent (or calls GamerServicesDispatcher.Initialize), and constructs a renderer from a description gets:

  • States. A description without an avatar, or one CNA cannot read, is Unavailable and draws nothing. A valid one is Loading while one shared background thread assembles the body, wardrobe and face from the asset catalog, then Ready. Identical descriptions share the assembled model.
  • Bind pose and skeleton. ParentBones is XNA's 71-entry table. Once ready, BindPose holds 71 local transforms relative to the parent, scaled to the description's height (bind rotations are identity, so they are pure translations).
  • Draw. Every bone must be decomposable (InvalidOperationException otherwise). Each bone's rotation and scale are applied relative to its parent. The root also takes the supplied translation, and every other bone keeps the avatar's own bind offset, so one standard animation fits every height and build. Lighting is exactly LightDirection/LightColor plus AmbientLightColor (defaults 0.65 key and 0.35 ambient from the front). The face features are decals chosen by the AvatarExpression (left and right eyes and eyebrows independently). The model always renders solid, with clockwise front faces as in XNA, and the blend, depth, rasterizer and first sampler states are restored afterwards. There is no baked ambient occlusion or self-shadowing: a visual-quality limit, not a missing API (shadows a game draws itself, as in the XNA AvatarShadows sample, work).
  • Loading effect. While loading, a renderer constructed with useLoadingEffect (the one-argument constructor's default) draws a softly pulsing translucent silhouette the height of the avatar. If loading completes during a Draw call, that call remains on the loading path and the next update observes Ready before the next draw consumes animation transforms.
  • Device. The graphics device comes from the IGraphicsDeviceService of the service provider given to the dispatcher (XNA's rule); drawing before gamer services are initialized throws InvalidOperationException.

Animations

Each of the 31 AvatarAnimationPreset values is an original CNA clip on the 71-bone skeleton, 2.5 to 10 seconds long, with a keyed facial expression (idles blink). BoneTransforms holds 71 local animation deltas at CurrentPosition: non-root translations are zero, because the renderer's BindPose supplies the joint offsets. Thus BoneTransforms[i] * BindPose[i] * parentWorld computes the same joint as drawing that pose, including attachments. Update follows the reference exactly: negative time plays backward, looping wraps by the remainder of Length (a position equal to Length is kept), a non-looping animation clamps, and an overflowing step clamps to the nearer end. An undefined preset value is a zero-length bind pose.

Assets

All geometry, textures and motion are original CNA work generated by tools/avatar_builder/generate_avatar_catalog.py (pure Python, deterministic, no downloaded or third-party content). Every catalog version lives in its own directory, modules/gamer-services/assets/avatars/v<N>/, and all of them are compiled into the library by cmake/EmbedBinaryFiles.cmake as v<N>/<file>. A released catalog never changes: catalog v1 is frozen data (the generator that wrote it is in Git history), catalog v2 likewise (generator at commit d1730d4f3), and catalog v3, the current one, was released on 2026-09-30 and is frozen from then on: new art goes into a new catalog version.

A description is drawn from exactly the catalog version it names — the compiled-in manifest of that version, else an installed pack of it (below) — never from another. Files are found by size and SHA-256, so two versions may use one file name for different contents and identical contents are shared. If the named catalog cannot be obtained at all, the newest compiled-in body is drawn with every item replaced by its slot default (its ids are not read against another version). Preset animations are system data, not part of a description, and come from the newest compiled-in catalog.

Catalog v3 (the newest) has:

  • a body per body type in toy-like console proportions: a large round head (about a fifth larger than v2's, a softer jaw and fuller cheeks) on a short neck, lowered shoulders, shorter legs, sturdier limbs and larger hands (a rounded palm as wide as its finger row, four separate fingers and a thumb, closing on the forearm without a seam); a rounded nose, ears and the face-feature decal patches;
  • 39 wardrobe items fitted to both bodies, with exactly catalog v2's ids and slots: 10 hair styles (as broader clumps than v2's, so each reads as one shaped mass), 9 tops, 6 bottoms, 5 shoes, 4 glasses and 5 hats, with per-body CreateRandom weights;
  • featureItems: 4 facial-hair styles a format 2 description can name;
  • faceControls per body type: what each face-shape byte does, as scale/move/rotate deformers with an ellipsoidal falloff on Head-weighted vertices — the face-only ones move the head, face decals and facial hair, the whole-head ones (width, height) also hair, hats and glasses;
  • for every hair style, hatAssets: the style with what a hat hides removed and the rest flattened under one shared hat envelope, used whenever the hat item coversHair;
  • face_features.png, an atlas of every eye (almond openings under a bold lid line with a small outer wing; a separate iris layer tinted by the eye color), eyebrow and mouth state (fuller upper and lower lips, a gentle resting smile) at 128 pixels per tile; lips are translucent so they suit any skin tone; face controls reach about half again as far as v2's;
  • animations.glb with the 31 presets as glTF cubic-spline rotation curves (the standing idle sways from foot to foot with the head following);
  • catalog.json, listing every file with its SHA-256.

Every GLB uses the same canonical 71-joint rig in XNA's exact topology: meters, Y up, facing +Z, feet on y = 0, the avatar's left on +X. This is the asset space, not the standard API's space: XNA avatars face -Z and their left is -X. AvatarAnimation.BoneTransforms and AvatarRenderer.BindPose expose the half-turn about Y of the catalog transforms. The renderer converts caller bones back to asset space for skinning and applies the half-turn before the caller's World, so attachments computed from the public bones and bind pose agree with the drawn body. Released GLBs, manifests, assembled-model digests and descriptions stay unchanged. The Guide editor and GPU art-review utility orient their own camera view accordingly. Joints XNA leaves unnamed are helpers named SlotNN, never skinned or animated. Materials are a base color factor, an optional embedded base-color PNG (a near-white multiplier carrying shading and fabric detail) and a description color tint. The reader refuses anything outside that contract: external URIs, other rigs, non-identity bind rotations, out-of-range indices or weights, and oversized data. Catalogs v2 and v3 keep to what the format 1 reader accepts (normals and texture coordinates are normalized 16-bit accessors), so CNA builds from before catalog packs, which fetched a missing catalog's items one by one, can draw them; they ignore facial hair, face controls and hat variants. GamerServices_AvatarCatalogUpToDate regenerates the newest catalog and compares it byte for byte (on a C library other than glibc, every byte except catalog v3's six FemaleShocked arm rotations, which a non-convergent head solve froze from glibc's libm and are checked structurally there; plans/plan_apple_m4.md AM4-166); AvatarCatalogTest.CatalogV1IsFrozen, V1DescriptionsAssembleExactlyAsReleased, CatalogV2IsFrozen and CatalogV3IsFrozen pin the released ones, and EmbeddedAssetsResolveByContentNotByName and AnUnavailableCatalogNeverReadsItsIdsAgainstAnotherVersion keep each version's files and ids its own. A v2 avatar keeps drawing from v2; the avatar editor carries it into v3 item by item when it is edited. tools/avatar_builder/avatar_review.py renders review sheets through the real renderer (cna_avatar_review) and a CPU preview.

Catalogs a release does not have: catalog packs

The avatar model is the console's: the service owns each account's description (1,021 bytes and a revision); the avatar catalogs are client content. A description on a catalog of this release renders offline, and BeginGetFromGamer + AvatarRenderer transfer nothing but the description. A pack is a whole immutable catalog version, installed once per client, never a per-user download.

When a description names a catalog this build does not compile in, the renderer's loader installs that exact catalog as one pack, if the configuration allows it (avatarCatalogUpdates, default on; CNA_AVATAR_CATALOG_UPDATES=0 turns it off; maxAvatarCatalogBytes, default 64 MiB):

  1. avatars.catalogPack describes it: version, pack format, contract level (reader), description formats, the manifest's SHA-256 and size, and the total size. A pack this client cannot read or is not allowed to keep is refused before anything is downloaded.
  2. The manifest and each file the client lacks are downloaded by hash from the service's file route; files this build compiles in (or another installed pack holds) are copied, not downloaded. Every file is verified into a staging directory.
  3. The whole catalog is validated as the renderer will use it: every model against the 71-bone contract and bounds, every texture decoded, every item fitted to its body, the face atlas against its layout, all 31 animation presets.
  4. One rename activates it. An interrupted install leaves only staging, which a retry reuses after verifying each file again; an invalid pack is discarded; nothing half-installed is ever read.

Installed packs are kept, not cached: CNA_GAMER_SERVICES_CATALOGS_DIR, else $XDG_DATA_HOME/cna/avatar-catalogs (%LOCALAPPDATA%\cna\avatar-catalogs on Windows), one self-contained v<N>/ per version. Catalog versions are CNA-wide identities: the pack installed first is that version, and a different catalog offered under the same number is refused. A version that failed to install is not asked for again for a minute.

The service negotiates what each client receives (avatars.get states the formats it reads, the catalogs it has, whether and how large it installs packs). A client that can draw the stored avatar, or will install its catalog, receives it; otherwise the service sends a projection onto the newest catalog the client has, marked as such, and leaves the stored avatar unchanged. Games see an ordinary description either way, and AvatarDescription.Changed follows the stored avatar's revision, so installing a catalog or receiving a projection is never a change.

The fallback order for a description is: (1) its catalog is installed; (2) its pack is installed now; (3) the service's projection (when it came from the service); (4) the deterministic default avatar -- the newest installed body with the description's body type, height, build and colours, every item its slot default (its ids are never read against another catalog); (5) Unavailable only when not even a body can be drawn. Descriptions exchanged directly between games (bytes over a NetworkSession) take (1), (2) and (4).