# Authentication Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/authentication # Authentication A player signs in once, at startup: from the Game Instance in C++, from the Game Mode in Blueprint. Every method, email and password, a one-time link by email, a social provider, converges on one pipeline: the sign-in returns an identity session token, the SDK mints a short-lived app-scoped token from it, adopts the app's endpoints, fires a success event, and requests the realtime connection with the app token. Your code picks the method and reacts to the event; the tokens are the SDK's business. ## When you touch this Once per project for the sign-in itself, which the [Quickstart](../quickstart.md) already did with a password. Again when you add a sign-in screen (which methods to offer, restoring a saved session, a forgotten password, free access), when a player signs out, when you build an account settings screen, and when you let a player link a second way to sign in. ## Two tokens | Token | Plane | Who holds it | Lifetime | |---|---|---|---| | Identity **session** token | Management: the account, Studio, linked identities | The SDK persists it so a later launch can restore the session | Until sign-out | | **App-scoped** token | Gameplay: the Game API and the realtime connection, confined to one app | The SDK keeps it in memory only | About 30 minutes; refreshed for you before expiry, and again on a realtime `TOKEN_EXPIRED` | The session token is rejected for gameplay and the app token cannot manage the account, so neither can stand in for the other. You never handle either: the SDK stores, mints, refreshes, and installs them. The mint itself is described on [Portals and app-scoped tokens](/management-api/portals-and-app-tokens#minting-an-app-token); the sign-in calls underneath are on [Sign in](/management-api/authentication). :::danger[Never log, print, persist, or display a token.] `FCrowdyAuthResult::GameToken`, despite its name, is the identity session token, and the SDK already holds it. The SDK's own `On Login` and `On Register` events on the Crowdy SDK Subsystem carry a human-readable message and never a credential, because anything on a Blueprint-assignable event can end up on screen. Keep yours the same way: read `UserID`, ignore `GameToken`. ::: ## Choosing a method | Method | Needs | Call | |---|---|---| | Email and password | Nothing but the two strings. No inbox, no browser. Permanent and first-class, not a legacy path. | `Login`, or `Register` for a new account | | Magic link | An inbox the player can reach while the game runs | `BeginMagicLinkSignIn` | | Social provider | The system browser, and a provider the server has enabled | `BeginSocialSignIn` | | A saved session | A previous sign-in on this machine | `RestoreSession` | Password is the right default for a build machine, a bot, or an agent, and for any flow that must complete without leaving the game. ## Two surfaces, one pipeline `UCrowdySDKSubsystem` (Game Instance subsystem, category **CrowdySDK, Authentication**) is the simple surface the Quickstart uses: `Login(Email, Password)`, `Register(Email, Password)`, `CompleteLoginLink(Token)`, `BeginMagicLinkSignIn(Email)`, `BeginSocialSignIn(Provider)`, `RefreshAppToken()`, `Logout()`, and `RequestUDPAccess()`. Each forwards to the rich surface below with no per-call delegate, and the outcome arrives on the subsystem's multicast events: `OnLogin` and `OnRegister` (`FOnLogin`, `FOnRegister`: `bSuccess`, `Message`) and `OnLogout` (`FOnLogout`: `bSuccess`). `UCrowdyAuthentication` (Game Instance subsystem, **Crowdy Authentication**, category **Crowdy SDK, Authentication**) is the rich surface: every call takes per-call delegates, `FOnAuthSuccess` with an `FCrowdyAuthResult` and `FOnAuthError` with a message, and every success also broadcasts an event carrying the result. | Call | What it does | |---|---| | `Login(Email, Password, OnSuccess, OnError)` | Password sign-in. Persists the session so a later launch can restore it. | | `Register(Email, Password, OnSuccess, OnError)` | Creates the account and signs in. An address that already has an account is refused with `EMAIL_ALREADY_REGISTERED`, not silently converted to a login. | | `BeginMagicLinkSignIn(Email, OnSuccess, OnError)` | Opens a loopback listener on 127.0.0.1, emails the link, and completes itself when the player clicks it. `OnError` on failure or timeout. Nothing else to call. | | `RequestLoginLink(Email, RedirectUri, OnLinkSent, OnError)`, `CompleteLoginLink(Token, OnSuccess, OnError)` | The two steps behind the one call above, for a flow you drive yourself. `FOnLoginLinkSent` carries one `bSent`, always true so an address cannot be probed. | | `GetAvailableLoginProviders(OnResult, OnError)` | The enabled providers (`FOnLoginProvidersReceived`, a string array). Public: no session needed. Build the sign-in buttons from it. | | `BeginSocialSignIn(Provider, OnSuccess, OnError)` | Opens the provider's consent page in the system browser, never an embedded view, and completes when the redirect lands on the loopback listener. | | `RestoreSession(OnSuccess, OnError)` | Restores a saved session and re-mints. Returns false at once, and calls `OnError`, when nothing is saved. | | `ClearSavedSession()`, `HasSavedSession()` | Delete the persisted credential and cancel the refresh timer; whether one exists on disk. | | `IsSignedIn()` | Signed in right now: a sign-in or restore completed and its app token is in memory. | | `RefreshAppToken()` | Rotate the app token by hand. It also happens automatically before expiry. | | `GetMyIdentities(OnResult, OnError)`, `BeginLinkIdentity(Provider, OnResult, OnError)`, `UnlinkIdentity(IdentityId, OnResult, OnError)` | Linked sign-in methods; below. | | `RecordPlayerConsents(bAcceptLegal, bAttestAgeOfMajority, OnRecorded, OnError)`, `DeclineLegalAcceptance()` | Store the player's agreement and let a held sign-in finish, or fail it. Only for a game that draws its own prompt; [Let the player agree to the terms](#let-the-player-agree-to-the-terms). | | `IsLegalAcceptancePending()`, `GetLegalDocuments()` | Whether a sign-in is waiting for the player's agreement; the documents to link, as `FCrowdyLegalDocument` rows (`Title`, `Url`). | | `RequestPasswordReset`, `ResendConfirmationEmail`, `ChangePassword`, `AddPassword`, `DeleteAccount`, `SignOutEverywhere` | The player's account settings; [Account settings](#account-settings). | | `GetMyAccess(OnResult, OnError)`, `ClaimFreeAccess(OnSuccess, OnError)` | The player's own access to this app, and claiming the app's free access; [When the player has no access yet](#when-the-player-has-no-access-yet). | Events on `UCrowdyAuthentication`, all Blueprint-assignable: `OnLogin` (`FOnAuthLoginEvent`) and `OnRegister` (`FOnAuthRegisterEvent`) with an `FCrowdyAuthResult`; `OnLoginFailed` (`FOnAuthLoginFailed`) and `OnRegisterFailed` (`FOnAuthRegisterFailed`) with a message; `OnSessionRestored` (`FOnAuthSessionRestored`) and `OnSessionRestoreFailed` (`FOnAuthSessionRestoreFailed`); `OnAppTokenRefreshed` (`FOnAppTokenRefreshed`), with no parameters, after a proactive or reactive rotation; and `OnLegalAcceptanceRequired` (`FOnLegalAcceptanceRequired`) and `OnLegalAcceptanceEnded` (`FOnLegalAcceptanceEnded`), with no parameters, when a sign-in starts and stops waiting for the player to agree to the terms. `FCrowdyAuthResult` has two fields: `UserID`, the player's `int64` id, and `GameToken`, the session token you never read. ## Signing in The game signs the player in at startup, binds the outcome event before it calls, and enables play in the handler. In C++ the Game Instance does it from `Init`; in Blueprint the Game Mode does it from **Event BeginPlay**. This is the Quickstart's step, on the same `ULanternGameInstance` and Game Mode. The C++ is the `ULanternGameInstance` block on [Player Sign-in](../runtime/player-sign-in.md): bind `OnLogin` and `OnUDPConnectionSuccess` on the Crowdy SDK Subsystem in `Init`, then call `Login` with the account read from `Config/DefaultGame.ini`. The rich surface in the table above is reached the same way, through `GetSubsystem()`, with the per-call delegates passed as arguments. In the Game Mode Blueprint, **Event BeginPlay** gets the **Crowdy Authentication** subsystem and branches on its **Is Signed In**; only when that is false does it run the latent **Login** node with `Email` and `Password`. Both its `On Success` and `On Error` pins carry a `Result` and a `Message`: `Result` is filled on success, `Message` says why on error. The other sign-in nodes, and **Record Player Consents** (`Message` only), work the same way. [Blueprint snippet: auth-login, see /bp/unreal-sdk/auth-login.txt] The two literals on the node are placeholders for a test account; a shipped Blueprint reads them from a widget, as [Player Sign-in](../runtime/player-sign-in.md) does. :::caution[In Blueprint, start from the Game Mode's Event BeginPlay, not the Game Instance's Event Init.] A Blueprint Game Instance's **Event Init** runs before the SDK's subsystems exist. There the subsystem getters return nothing, **Login** and **Restore Session** fail at once, and **Bind Event** binds nothing. The Game Mode's **Event BeginPlay** runs after the Game Instance has finished starting. It runs again in every map, so a graph that signs in checks **Is Signed In** first and skips the call when the player already is. Is Signed In stays false until a sign-in finishes, so open another map only after **On Login**, or the next map signs in a second time. A graph has one Event BeginPlay: if your Game Mode already has one, run the new chain from it through a **Sequence** node, the binds on the first pin and the sign-in on a later one. A C++ Game Instance can use `Init` as the C++ examples do, because the subsystems exist once `Super::Init()` has returned. If your clients join an Unreal listen or dedicated server, a client has no Game Mode: use the local Player Controller's BeginPlay there instead. ::: :::caution[Bind your delegates before you start the call.] A response can arrive quickly enough to be missed by a handler bound after the call returns. The per-call delegates are bound as arguments, so they are always in place; for the multicast events, bind before the first sign-in: in `Init` after `Super::Init()` in C++, from the Game Mode's Event BeginPlay in Blueprint. ::: In Blueprint, five latent sign-in nodes wrap the rich surface (a sixth, **Record Player Consents** (`UCrowdyAuth_RecordPlayerConsents`), is for a game that draws its own terms screen; its pins are `FRecordPlayerConsentsDelegateOnSuccess` and `FRecordPlayerConsentsDelegateOnError`), each with `On Success` (a `Result`) and `On Error` (a `Message`) pins and no delegate wiring: **Login** (`UCrowdyAuth_Login`), **Register** (`UCrowdyAuth_Register`), **Magic Link Sign In** (`UCrowdyAuth_BeginMagicLinkSignIn`), **Social Sign In** (`UCrowdyAuth_BeginSocialSignIn`), and **Restore Session** (`UCrowdyAuth_RestoreSession`). Their pin delegate types are `FLoginDelegateOnSuccess`, `FLoginDelegateOnError`, `FRegisterDelegateOnSuccess`, `FRegisterDelegateOnFailure`, `FBeginMagicLinkSignInDelegateOnSuccess`, `FBeginMagicLinkSignInDelegateOnError`, `FBeginSocialSignInDelegateOnSuccess`, `FBeginSocialSignInDelegateOnError`, `FRestoreSessionDelegateOnSuccess`, and `FRestoreSessionDelegateOnError`, should you bind one from C++. ### Register A Create Account action on your sign-in screen calls `RegisterNewAccount`; success is the account created and signed in. It binds the two handlers every sign-in path on this page shares, `HandleSignedIn` and `HandleSignInFailed`, with `BindDynamic` before the call. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" // A Create Account button calls this; the outcome arrives on the two handlers every sign-in path shares. UFUNCTION(BlueprintCallable, Category = "Lantern") void RegisterNewAccount(const FString& NewEmail, const FString& NewPassword); // LanternGameInstance.cpp void ULanternGameInstance::RegisterNewAccount(const FString& NewEmail, const FString& NewPassword) { UCrowdyAuthentication* Auth = GetSubsystem(); FOnAuthSuccess OnSuccess; OnSuccess.BindDynamic(this, &ULanternGameInstance::HandleSignedIn); FOnAuthError OnError; OnError.BindDynamic(this, &ULanternGameInstance::HandleSignInFailed); Auth->Register(NewEmail, NewPassword, OnSuccess, OnError); } ``` A custom event `OnCreateAccountPressed` runs **Register** with `Email` and `Password`. [Blueprint snippet: auth-register, see /bp/unreal-sdk/auth-register.txt] ### Magic link One call, `RequestMagicLink`. The SDK opens the loopback listener, sends the email, and completes the sign-in when the player follows the link. There is no second step for you to call, and `OnError` fires if the player never returns. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" UFUNCTION(BlueprintCallable, Category = "Lantern") void RequestMagicLink(const FString& MagicEmail); // LanternGameInstance.cpp void ULanternGameInstance::RequestMagicLink(const FString& MagicEmail) { UCrowdyAuthentication* Auth = GetSubsystem(); FOnAuthSuccess OnSuccess; OnSuccess.BindDynamic(this, &ULanternGameInstance::HandleSignedIn); FOnAuthError OnError; OnError.BindDynamic(this, &ULanternGameInstance::HandleSignInFailed); // Emails the link and listens on 127.0.0.1 for the click; nothing else to call, OnError also covers the timeout. Auth->BeginMagicLinkSignIn(MagicEmail, OnSuccess, OnError); } ``` A custom event `OnRequestMagicLinkPressed` with an `Email` parameter runs **Magic Link Sign In**. [Blueprint snippet: auth-magic, see /bp/unreal-sdk/auth-magic.txt] ### Social `SignInWithGoogle` opens the provider's consent page in the system browser. The provider string comes from `GetAvailableLoginProviders`; the example hard-codes `google` only to stay short. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" UFUNCTION(BlueprintCallable, Category = "Lantern") void SignInWithGoogle(); // LanternGameInstance.cpp void ULanternGameInstance::SignInWithGoogle() { UCrowdyAuthentication* Auth = GetSubsystem(); FOnAuthSuccess OnSuccess; OnSuccess.BindDynamic(this, &ULanternGameInstance::HandleSignedIn); FOnAuthError OnError; OnError.BindDynamic(this, &ULanternGameInstance::HandleSignInFailed); // Opens the provider's consent page in the system browser; build a real sign-in screen from GetAvailableLoginProviders. Auth->BeginSocialSignIn(TEXT("google"), OnSuccess, OnError); } ``` A custom event `OnSignInWithGooglePressed` runs **Social Sign In** with `Provider` = `google`. [Blueprint snippet: auth-social, see /bp/unreal-sdk/auth-social.txt] :::warning[Build the provider list from Get Available Login Providers, never from a literal.] A server can disable or rename a provider; a hard-coded button then opens a consent page that fails. Do not hard-code a provider you saw once in the list; build the buttons from the call every launch. ::: ## Let the player agree to the terms The server hands out no gameplay token until the signed-in player has agreed to the current terms and has an age. A new version of a document asks again. What the server requires and how it is stored is on [Terms, age and country](/management-api/portals-and-app-tokens#terms-age-and-country); this section is what the SDK does about it and what you can change. :::caution[Since 2026-11-04] Crowded Kingdoms admits players from 13, and a player's age comes from their date of birth, not from a confirmation box: the documents are the [Player Terms](https://crowdedkingdoms.com/player-terms.html) and the [Player Privacy Policy](https://crowdedkingdoms.com/player-privacy.html), and the age confirmation the prompt below asks for records nothing. An account with no age is refused `AGE_REQUIRED` until it gives its birth date and country with `recordPlayerAge`, which this plugin does not ask for yet; an account that confirmed its age before that date keeps it. See [Age, country and who may play](/management-api/age-and-territory). ::: ### What the player sees by default You write nothing. When a sign-in, sign-up, restore, or token refresh is refused for this reason, the SDK holds it instead of failing it, and opens a prompt over the game viewport in the Crowded Kingdoms colors: the five documents as links, one checkbox to agree to them, one to confirm the age, and **Accept** and **Decline**. **Accept** is enabled once both boxes are ticked. - **Accept** records the player's agreement, closes the prompt, and finishes the sign-in they started: your `OnLogin`, `OnRegister`, `OnSessionRestored`, or `OnAppTokenRefreshed` handler runs exactly as it would have without the prompt. - **Decline** closes the prompt and fails that sign-in with `Sign-in needs the terms and the age confirmation.` through its usual failure path (`OnError` and the failed event), so your sign-in screen shows it like any other refusal. - If the agreement cannot be recorded (the network dropped, say), the prompt shows the error and stays up, and the sign-in stays held. The prompt also appears in the middle of a session when a document gets a new version: the next token refresh is refused, held, and the prompt opens over the game. Declining it leaves the current app token to run out. Only one sign-in waits at a time. While one waits, the SDK does not rotate the app token, and a newer sign-in that is refused takes the place of the older one, which fails first. To see the prompt without an account that still needs it, run `crowdy.auth.consent.preview` in the console of a development build. It opens the same prompt over the game; **Accept** and **Decline** only close it, and nothing is recorded. The command is not in Shipping builds. ### Draw your own prompt A game with its own sign-in screens can turn the built-in prompt off and show the same choice in its own style. Clear **Show Consent Prompt** (`bShowConsentPrompt`, on by default) under **Project Settings, Plugins, Crowdy SDK, Authentication**, then handle the hold yourself: 1. Bind `OnLegalAcceptanceRequired` on the Crowdy Authentication subsystem before the first sign-in. It fires once per hold, with no parameters. 2. Show the documents from `GetLegalDocuments()`, an array of `FCrowdyLegalDocument` (`Title`, `Url`), as links, in that order, with two checkboxes: one to agree to the documents, one for the age. 3. When the player ticks both and presses accept, call `RecordPlayerConsents(true, true, ...)`. If they decline, call `DeclineLegalAcceptance()`. 4. Close your UI on `OnLegalAcceptanceEnded`. It fires whenever nothing is held any more, however the hold ended, so a recorded consent, a decline, and a sign-out all end up in one place. :::danger[Record consent only from the player's own accept.] `RecordPlayerConsents` stores the player's agreement on their account. Never call it for them, from a timer, or with a box that was pre-ticked: the call does nothing unless both arguments are true, and both must come from boxes the player ticked. ::: `ULanternGameInstance` shows its own terms screen. `ShowTermsScreen` and `HideTermsScreen` are your widget code; `OnTermsAccepted` is called by the accept button with the two boxes' states. The success path is empty on purpose: when the consent is stored the held sign-in resumes and reaches the handlers you already bound. ```cpp // LanternGameInstance.h UFUNCTION() void ShowTermsScreen(); UFUNCTION() void HideTermsScreen(); UFUNCTION() void HandleConsentError(FString Message); void OnTermsAccepted(bool bAgreedToDocuments, bool bConfirmedAge); void OnTermsDeclined(); // LanternGameInstance.cpp void ULanternGameInstance::Init() { Super::Init(); UCrowdyAuthentication* Auth = GetSubsystem(); Auth->OnLegalAcceptanceRequired.AddDynamic(this, &ULanternGameInstance::ShowTermsScreen); Auth->OnLegalAcceptanceEnded.AddDynamic(this, &ULanternGameInstance::HideTermsScreen); } void ULanternGameInstance::OnTermsAccepted(bool bAgreedToDocuments, bool bConfirmedAge) { if (!bAgreedToDocuments || !bConfirmedAge) { return; } FOnAuthError OnError; OnError.BindDynamic(this, &ULanternGameInstance::HandleConsentError); GetSubsystem()->RecordPlayerConsents(true, true, FOnLegalConsentsRecorded(), OnError); } void ULanternGameInstance::OnTermsDeclined() { GetSubsystem()->DeclineLegalAcceptance(); } ``` `ShowTermsScreen` builds its list from `UCrowdyAuthentication::GetLegalDocuments()`, which is static and needs no subsystem. `HandleConsentError` shows the message and leaves the screen up so the player can try again: when recording fails, the sign-in stays held. In the Game Mode Blueprint, **Event BeginPlay** gets the **Crowdy Authentication** subsystem and binds **Bind Event to On Legal Acceptance Required** to a custom event that creates your terms widget and adds it to the viewport, and **Bind Event to On Legal Acceptance Ended** to one that removes it. The widget fills its document list from **Get Legal Documents**, a pure node returning an array of **Crowdy Legal Document** (`Title`, `Url`). The widget's accept button runs a branch on its two check boxes, and only when both are true calls the latent **Record Player Consents** node with **Accept Legal** and **Attest Age Of Majority** set true. Its **On Error** pin carries a `Message` to show; **On Success** needs nothing, because the held sign-in resumes and reaches the nodes you already wired. The decline button calls **Decline Legal Acceptance** on the Crowdy Authentication subsystem. **Is Legal Acceptance Pending** reads whether a sign-in is waiting right now, for a widget that opens late. ### Sign-in for a server or a build with no window The prompt needs a viewport. A dedicated server, a headless client, a commandlet, or any process with no game viewport, no Slate, or no way to render cannot show it and cannot record consent for a player. A sign-in there that needs consent, with nothing else bound to `OnLegalAcceptanceRequired`, now fails at once with `Sign-in needs the terms and the age confirmation.` through its usual failure path and a Warning in the log, instead of waiting for a prompt that can never appear. Sign such an account in once from a game or Crowdy Studio with a window; after it has agreed, a headless process signs in as before. A headless process that answers consent itself, by binding `OnLegalAcceptanceRequired` and recording consent as in [Draw your own prompt](#draw-your-own-prompt), keeps working. For a headless smoke test, development builds have console commands that sign a test account in from a JSON file, report the sign-in state and sign out: `crowdy.auth.signin.file`, `crowdy.auth.status` and `crowdy.auth.signout`. See [Sign in from the console](../guides/testing-locally.md#sign-in-from-the-console). They are not in Test or Shipping builds. Crowdy Studio shows the same prompt in a window of its own when the account you sign in with has not agreed yet; see [Signing In](../studio/sign-in.md#agreeing-to-the-terms). ## After sign-in The SDK requests the realtime connection itself, on every path: password, link, social, and restore all end in the same success handler inside the SDK, which calls `RequestUDPAccess()`, and a token refresh reinstalls the new token on the live connection. Do not call `RequestUDPAccess()` from your own success handler; by the time it runs, the SDK already did. Wait for **On UDP Connection Success** on the Crowdy SDK Subsystem for the moment the connection is up; [Connection and reconnect](../runtime/connection-and-reconnect.md) covers what follows, including `TOKEN_EXPIRED` and the automatic re-mint. `RequestUDPAccess()` remains callable for a manual recovery and nothing else. The events are also where a game enables play. `WatchAuthEvents` binds `OnLogin` and `OnSessionRestored` to the same `HandleSignedIn`, which lights every lantern in the world, and the `Result` it receives is read for nothing: the token in it belongs to the SDK. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" void WatchAuthEvents(); UFUNCTION() void HandleSignedIn(FCrowdyAuthResult Result); UFUNCTION() void HandleSignInFailed(FString Message); // LanternGameInstance.cpp #include "EngineUtils.h" #include "Lantern.h" void ULanternGameInstance::WatchAuthEvents() { UCrowdyAuthentication* Auth = GetSubsystem(); Auth->OnLogin.AddDynamic(this, &ULanternGameInstance::HandleSignedIn); Auth->OnSessionRestored.AddDynamic(this, &ULanternGameInstance::HandleSignedIn); } void ULanternGameInstance::HandleSignedIn(FCrowdyAuthResult Result) { // Result also carries the session token; the SDK holds it, so never print or store it. The village lights up. for (TActorIterator It(GetWorld()); It; ++It) { It->Light->SetVisibility(true); } } void ULanternGameInstance::HandleSignInFailed(FString Message) { UE_LOG(LogTemp, Warning, TEXT("Sign-in failed: %s"), *Message); } ``` The Game Mode Blueprint needs a Boolean variable `bCanPlay`. **Event BeginPlay** gets the **Crowdy Authentication** subsystem and **Bind Event to On Login**; the bound custom event `OnSignedIn` receives the `Result` and sets `bCanPlay` true. Each map has its own Game Mode, so in a map the player travels to after signing in, set `bCanPlay` from **Is Signed In** at BeginPlay instead of waiting for an event that already fired. [Blueprint snippet: auth-events, see /bp/unreal-sdk/auth-events.txt] ## What you may read about the signed-in player `UCrowdyGameSession` (Game Instance subsystem, category **Crowdy SDK, Game Session**) is the client's own connection and sign-in state, written by the SDK's login and handshake. A game legitimately reads four things from it: | Accessor | Returns | |---|---| | `GetAppID()` | The app id, `int64`. | | `GetUserID()` | The signed-in user's id, `int64`. | | `GetUUID()`, `GetID()` | This client's actor identity as a string and as an `FGuid`. | Its events `OnOwnerUUIDUpdated` (`FOnOwnerUUIDUpdated`) and `OnHostIDUpdated` (`FOnHostIDUpdated`) announce a new client identity and a change of the elected view-plane host; the host one belongs to [Host election](../runtime/host-authority.md). The other two, for a paused app and for ended access, are the next two sections. Everything else on the class, the setters, the token and URL getters, the clear call, and the getter that returns the whole connection-state struct, is written by the SDK and is not for a game to read, display, or log: that struct carries both tokens as fields, so breaking it into a widget is exactly the leak the box above forbids. ### When the app is paused The same class tells your game when the app has been switched off or paused, so you can show a message instead of letting players retry. It is read-only, like the rest, and safe to show: | Blueprint (C++) | Returns | |---|---| | **Is App Paused** (`IsAppPaused()`), pure | True while the app is paused. | | **Get App Paused Reason** (`GetAppPausedReason()`), pure | Why it is paused, such as "the app is paused" when the realtime server refused the player's updates. | | **On App Paused Changed** (`OnAppPausedChanged`, a `FOnCrowdyAppPausedChanged`) | An event with `Paused` and `Reason`, every time the state or its reason changes. | It turns on when signing in or refreshing the app token reports the app paused (its owner switched it off, or it ran out of funds), or when the realtime server refuses the player's updates because the app is paused. It turns off when a later token answer says the app is active or carries no runtime gate at all (which counts as not paused, so a pause an earlier answer raised clears; a pause the realtime server raised lasts until its own expiry), when the realtime server relays to the player again (or the player's updates go through for 15 seconds), and on sign-out. Bind the event, show a "the world is paused" message while it is on, and take it down when it turns off. [Server Objects](../exec/troubleshooting.md#waiting-is-not-failing) behave the same way: a call comes back **Paused**, and a read waits. ### When the player's access ends A player's access to the game can end while they play: the platform suspends it, revokes it, lets it expire or signs the player out, or the account is blocked. The Game Session tells your game, so you can say why and send the player back to sign in. It is read-only and safe to show: | Blueprint (C++) | Returns | |---|---| | **On Access Ended** (`OnAccessEnded`, a `FOnCrowdyAccessEnded`) | An event with `Reason` and `BlockedUntil`. | | **Has Access Ended** (`HasAccessEnded()`), pure | True from the moment access ends until the player signs in again. | | **Get Access Ended Reason** (`GetAccessEndedReason()`), pure | Why, in the platform's words. Empty while access has not ended. | | **Get Access Blocked Until** (`GetAccessBlockedUntil()`), pure | When a timed block or a suspension ends, as an ISO-8601 time such as `2026-11-01T09:00:00Z`. Empty otherwise, or while it lasts until someone lifts it. | [Server Objects](../exec/troubleshooting.md#when-access-ends) stop at once. Every one fails with `Your access ended: ` followed by the reason, a call in flight or made later finishes as the outcome **Access Ended**, which is not retryable, and nothing reconnects until the player signs in again. What your game does: - Bind **On Access Ended** before the first sign-in: in the Game Instance's `Init` in C++, from the Game Mode's Event BeginPlay in Blueprint. - Show the reason, and **Blocked Until** when it is not empty, then return the player to the sign-in screen. - When the end time of a timed block or a suspension is learned after access already ended, the event fires once more with **Blocked Until** filled in. Update the text you showed; do not add a second message. - A successful sign-in clears the ended state. A **Crowdy Server Object** component, and a typed node set to **Find By Asset**, then get their Server Object again by themselves. Code that calls `Acquire` itself acquires again. The game binds the event next to the sign-in events, in the Game Instance's `Init` in C++ and from the Game Mode's Event BeginPlay in Blueprint, and shows the two strings. A shipped game puts them on its sign-in screen. ```cpp // LanternGameInstance.h public: // Init calls this to hear when the player's access to the game ends. void WatchAccess(); UFUNCTION() void HandleAccessEnded(const FString& Reason, const FString& BlockedUntil); // LanternGameInstance.cpp #include "Subsystem/CrowdyGameSession.h" void ULanternGameInstance::WatchAccess() { GetSubsystem()->OnAccessEnded.AddDynamic(this, &ULanternGameInstance::HandleAccessEnded); } void ULanternGameInstance::HandleAccessEnded(const FString& Reason, const FString& BlockedUntil) { // A game shows these on its sign-in screen. BlockedUntil is empty unless a timed block has a known end. UE_LOG(LogTemp, Warning, TEXT("Access ended: %s. Blocked until: %s"), *Reason, *BlockedUntil); } ``` In the Game Mode Blueprint, **Event BeginPlay** gets the **Crowdy Game Session** subsystem and runs **Bind Event to On Access Ended**; the bound custom event `OnAccessEnded` receives `Reason` and `BlockedUntil`, and prints each. [Blueprint snippet: access-ended-bind, see /bp/unreal-sdk/access-ended-bind.txt] ### When the app refuses the player's access The app itself can refuse a player: it [revoked their access, suspended it until a time](/management-api/game-setup#revoking-suspending-and-banning), or never granted it (or the grant expired). The SDK learns it when it signs the player in or renews the app token, which the server answers with `ACCESS_REVOKED`, `ACCESS_SUSPENDED` or `ACCESS_NOT_GRANTED`. A cut also revokes the player's current token, so the realtime connection is refused when it reconnects; the SDK then asks for a new token at once, which is how a game that holds no Server Object hears of the refusal within seconds, and how a player whose access is restored reconnects without signing in again. The sign-in fails through its usual failure path with the server's message, **On Access Ended** fires with that message, and **Blocked Until** carries the end of a suspension. The SDK then stops renewing the token, since the server would refuse it again. A suspension lifts by itself at that time, and a revoke or a missing grant when the app restores or grants the access; the next sign-in after that goes through and clears the ended state. ### When the account is blocked A blocked account cannot sign in. **Login**, **Register**, **Magic Link Sign In**, **Social Sign In** and **Restore Session** all fail through their usual failure path (`OnError`, and the failed event) with the server's own message, so your sign-in screen shows why like any other refusal. The block also ends access: **On Access Ended** fires, with **Blocked Until** set when the block has an end. Until the player signs in again, the SDK does not rotate the app token in the background, since the server would refuse it. Once the block is lifted or has run out, the next sign-in goes through and clears the ended state. ### When the player has no access yet A sign-in refused with `ACCESS_NOT_GRANTED` keeps the player's session, so your sign-in screen can say what to do next rather than only showing the refusal. Two calls on `UCrowdyAuthentication` work in that state, because they use the session rather than a gameplay token: | Blueprint (C++) | What it does | |---|---| | **Get My Access** (`GetMyAccess(OnResult, OnError)`) | Reads the player's own access to this app as an `FCrowdyAppAccess`: `State` (**None**, **Active**, **Suspended**, **Expired** or **Revoked**), `TierID`, `ExpiresAt` and `SuspendedUntil` (zero when not set), and `bFreeGrant`, true when the access came from the app's free tier. | | **Claim Free Access** (`ClaimFreeAccess(OnSuccess, OnError)`) | Claims the app's free access for the player, then finishes the sign-in that was refused. **On Success** carries the same `Result` as **Login**, and `On Login` fires. | `GetMyAccess` answers on `FOnAppAccessRead`, and `State` is an `ECrowdyAccessState`. The nodes are `UCrowdyAccount_GetMyAccess`, whose pins are `FAccessAsyncResult` (`Access`, `Error`, `Message`), and `UCrowdyAccount_ClaimFreeAccess`, whose pins are the sign-in nodes' `FAuthAsyncResult` (`Result`, `Message`). **Claim Free Access** never lifts a revoke: a player whose access was revoked stays refused, and **On Error** says so. It also fails when the app has no free tier. A player who already has access gets a success without a new token. Buying a paid tier happens on the web portal, not in the game. A Get Free Access button on the sign-in screen, shown when the refusal says the player has no access, is all it takes. `GetFreeAccess` reuses the two handlers every sign-in path on this page binds. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" public: // A Get Free Access button, shown when sign-in was refused because the player has no access yet. void GetFreeAccess(); // LanternGameInstance.cpp void ULanternGameInstance::GetFreeAccess() { FOnAuthSuccess SignedIn; SignedIn.BindDynamic(this, &ULanternGameInstance::HandleSignedIn); FOnAuthError Failed; Failed.BindDynamic(this, &ULanternGameInstance::HandleSignInFailed); // Success means the refused sign-in finished: the player is in, and On Login has fired. GetSubsystem()->ClaimFreeAccess(SignedIn, Failed); } ``` A custom event `OnGetFreeAccessPressed` runs **Claim Free Access**; its **On Error** prints the `Message`. **On Success** needs nothing here, because the **On Login** handlers you already bound run. [Blueprint snippet: acct-claim, see /bp/unreal-sdk/acct-claim.txt] ## Session persistence and sign-out `Login`, `Register`, the link, and the social flow persist the session token; `RestoreSession` on the next launch re-mints from it and fires `OnSessionRestored`, and the SDK connects as after any sign-in. Gate your login screen on `IsSignedIn()`. ### Restore a saved session A returning player should type nothing. The game calls `RestoreSession(OnSuccess, OnError)` first, before it shows a sign-in screen or calls `Login`. The call returns `bool`: true means a saved session was found and the restore is under way, and the outcome arrives on the delegates; false means nothing is saved on this machine, `OnError` has already run with `No saved session found`, and `OnSessionRestoreFailed` has already fired, so the error path is the sign-in screen. On success the SDK re-mints the app token from the saved session token, calls `OnSuccess`, and fires `OnSessionRestored`; then, exactly as after a `Login`, it requests the realtime connection with the fresh app token and raises its own `OnLogin` on the Crowdy SDK Subsystem, so the handlers you bound for a first sign-in, `OnLogin` and `OnUDPConnectionSuccess`, run unchanged. A restore that fails after finding a saved session (the token was revoked, the network is down) reaches `OnError` the same way. The Crowdy SDK Subsystem's `OnLogin` also fires, with `bSuccess` false and the message, for every failed restore: the `No saved session found` of a first launch and a restore the player's decline of the terms ended included. The saved session lives in the game's `Saved` folder (for a Shipping package, under the Windows user's local app data). Each PIE window inside the editor process keeps its own; every other client using that folder, a PIE client the editor starts as a separate process included, shares one, unless you start it with `-crowdysession=` (letters, digits, `-` and `_`, up to 32 characters), which gives it a session of its own. With no switch the game uses the default session. See [Two packaged or standalone clients](../guides/testing-locally.md#two-packaged-or-standalone-clients); before v2.21.1 there is no switch. `RestoreOrSignIn` is what `Init` calls in place of the bare `Login` from the Quickstart. `OnSuccess` is the same `HandleSignedIn` every path on this page binds; `HandleRestoreFailed` falls through to `Login` with the configured account, and a game with a sign-in screen shows it there instead. The return value is ignored on purpose: both outcomes arrive on the delegates. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" // Init calls this in place of Login: a machine that signed in before comes back without typing anything. void RestoreOrSignIn(); UFUNCTION() void HandleRestoreFailed(FString Message); // LanternGameInstance.cpp void ULanternGameInstance::RestoreOrSignIn() { UCrowdyAuthentication* Auth = GetSubsystem(); FOnAuthSuccess OnSuccess; OnSuccess.BindDynamic(this, &ULanternGameInstance::HandleSignedIn); FOnAuthError OnError; OnError.BindDynamic(this, &ULanternGameInstance::HandleRestoreFailed); // False means nothing is saved on this machine and OnError has already run; a saved token the server refuses reaches OnError later. Auth->RestoreSession(OnSuccess, OnError); } void ULanternGameInstance::HandleRestoreFailed(FString Message) { GetSubsystem()->Login(Email, Password); } ``` In the Game Mode Blueprint, **Event BeginPlay** gets the **Crowdy Authentication** subsystem and branches on **Is Signed In**; only when that is false does it run the latent **Restore Session** node, which takes nothing but the world context. Its **On Error** pin is the only place that asks for credentials: the graph runs **Login** with the configured account there, as the Quickstart does, and a game with a sign-in screen creates the widget from [Player Sign-in](../runtime/player-sign-in.md) and adds it to the viewport on that pin instead; a machine with nothing saved reaches it at once. **On Success** carries the `Result`, read for nothing. [Blueprint snippet: auth-restore, see /bp/unreal-sdk/auth-restore.txt] :::warning[IsSignedIn is not HasSavedSession.] `HasSavedSession` only says a credential exists on disk. It stays true across a restart, before a restore has run, and after a restore that failed. `IsSignedIn` is true only while a sign-in or restore has completed and its app token is held in memory right now. ::: :::warning[Sign out with Logout on the Crowdy SDK Subsystem, not ClearSavedSession alone.] `Logout()` first tells the server to end the session, which also ends every gameplay token made from it, without waiting for the answer, so sign-out works offline too. It then clears the saved session, clears the connection state, scrubs both bearers from the shared API client, and closes the realtime connection, and broadcasts `OnLogout`. A sign-in, token mint or refresh still in flight when you log out is cancelled: the caller's own `OnError` gets `Signed out before sign-in finished.`, nothing else fires (not `OnLoginFailed`), and the player is not signed back in when the answer lands. A magic-link or social sign-in still waiting for the browser is stopped too. `ClearSavedSession()` only deletes the on-disk credential; called alone during a live session it leaves the in-memory tokens and the open connection signed in as the player who just left. ::: ### Cancel a sign-in A sign-in screen's Cancel or Back button, and a game that travels or quits while a sign-in is still running, call `CancelSignIn()` on `UCrowdyAuthentication` (or on the Crowdy SDK Subsystem). It stops whatever sign-in has not finished: a restore, a password sign-in or registration, an email link or social sign-in waiting for the browser, or one held for the terms. Unlike `Logout`, a cancelled restore keeps the saved session, so it restores again on the next launch. `IsSignInPending()` is true from the moment a sign-in starts until it succeeds, fails or is cancelled; show the Cancel button on it. Once the player is signed in, both are false and `CancelSignIn` does nothing: signing out is `Logout`. What the caller sees: - Only the pending call's own `OnError` answers, with `Sign-in cancelled.`: no `OnLoginFailed`, `OnSessionRestoreFailed` or `OnRegisterFailed` fires, as after `Logout`. A browser wait and a sign-in held for the terms answer at once, and the terms prompt closes; a call already on its way to the server answers when its answer lands, and the player is not signed in by it. A game that started the sign-in from the Crowdy SDK Subsystem, which passes no `OnError`, reads `IsSignInPending` instead. - A new sign-in (password, registration, email link or social) saves its session as soon as the server accepts it, before the app token is minted, and that replaces any session saved before. Cancelling it after that point ends the new session on the server and deletes it from disk, so the next launch restores nobody rather than an account the player backed out of. `CancelPendingSignIn` is what the Cancel button calls. The answer arrives on the `HandleSignInFailed` the sign-in itself bound. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" // A Cancel button on the sign-in screen calls this. UFUNCTION(BlueprintCallable, Category = "Lantern") void CancelPendingSignIn(); // LanternGameInstance.cpp void ULanternGameInstance::CancelPendingSignIn() { UCrowdyAuthentication* Auth = GetSubsystem(); // Only the pending call's OnError answers, "Sign-in cancelled."; a cancelled restore stays saved for the next launch. if (Auth->IsSignInPending()) { Auth->CancelSignIn(); } } ``` The Cancel button's event gets the **Crowdy Authentication** subsystem, branches on **Is Sign in Pending**, and calls **Cancel Sign In** on the true pin. [Blueprint snippet: auth-cancel, see /bp/unreal-sdk/auth-cancel.txt] ## Account settings Six calls on `UCrowdyAuthentication`, each a latent node under **Crowdy SDK, Account**, cover what a settings screen and a sign-in screen need. Every one answers on `FOnAccountDone` (no parameters) or `FOnAccountError`, which carries an `FCrowdyAccountError` (`Code`, `Message`) and the `Message` again; on the nodes, **On Success** and **On Error** share the `Error` and `Message` pins, filled only on error. | Blueprint (C++) | Needs | What it does | |---|---|---| | **Request Password Reset** (`RequestPasswordReset(Email, ...)`) | Nothing: works signed out | Emails a link to set a new password. It succeeds whether or not the address has an account, so an address cannot be probed. | | **Resend Confirmation Email** (`ResendConfirmationEmail(Email, ...)`) | Nothing | Sends the address confirmation again, answering the same way. | | **Change Password** (`ChangePassword(CurrentPassword, NewPassword, ...)`) | A sign-in | Changes the password and signs the player out on their other devices; this one stays signed in. | | **Add Password** (`AddPassword(NewPassword, ...)`) | A sign-in, and an account with no password | Adds a password to an account made by magic link or social sign-in, which then signs in with it too. A security email goes to the account's address. | | **Sign Out Everywhere** (`SignOutEverywhere(...)`) | A sign-in | Ends every session of the account on every device and in every game, then signs out here as `Logout` does. | | **Delete Account** (`DeleteAccount(...)`) | A sign-in | Deletes the player's account: their personal data is removed and every session ends. The SDK then signs out here. It cannot be undone, so ask the player to confirm in your own dialog first. | The reset link and the confirmation link finish in the browser; there is nothing more to call from the game. Should you bind a node from C++: they are `UCrowdyAccount_RequestPasswordReset`, `UCrowdyAccount_ResendConfirmationEmail`, `UCrowdyAccount_ChangePassword`, `UCrowdyAccount_AddPassword`, `UCrowdyAccount_SignOutEverywhere` and `UCrowdyAccount_DeleteAccount`, whose pins are `FAccountAsyncResult` (`Error`, `Message`). `FCrowdyAccountError::Code` (`ECrowdyAccountErrorCode`) names the refusals worth handling on their own: | Code | Means | What to show | |---|---|---| | **Wrong Password** | The current password was wrong | Ask again | | **No Password** | Change Password on an account that has none | Offer **Add Password** | | **Password Already Set** | Add Password on an account that has one | Offer **Change Password** | | **Not Signed In** | The call needs a sign-in and there is none, or the session ended | The sign-in screen | | **Rate Limited** | Too many tries in a short time | Ask the player to wait | | **Network Error**, **Server Error**, **Unknown** | Anything else | The `Message` | The same error type, with **Name Taken** and **Not Found**, comes back from the player names calls on [Player Names](./player-names.md). ### Forgot password A Forgot Password link on the sign-in screen sends the reset email to the address the player typed. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" public: // A Forgot Password link on the sign-in screen calls this with the address the player typed. void SendPasswordReset(const FString& ResetEmail); UFUNCTION() void HandleResetSent(); UFUNCTION() void HandleAccountFailed(FCrowdyAccountError Error, FString Message); // LanternGameInstance.cpp void ULanternGameInstance::SendPasswordReset(const FString& ResetEmail) { FOnAccountDone Sent; Sent.BindDynamic(this, &ULanternGameInstance::HandleResetSent); FOnAccountError Failed; Failed.BindDynamic(this, &ULanternGameInstance::HandleAccountFailed); GetSubsystem()->RequestPasswordReset(ResetEmail, Sent, Failed); } void ULanternGameInstance::HandleResetSent() { // The same answer whether or not the address has an account, so say exactly that. UE_LOG(LogTemp, Log, TEXT("If that address has an account, a reset link is on its way.")); } void ULanternGameInstance::HandleAccountFailed(FCrowdyAccountError Error, FString Message) { UE_LOG(LogTemp, Warning, TEXT("Could not send the email: %s"), *Message); } ``` A custom event `OnForgotPasswordPressed` with an `Email` parameter runs **Request Password Reset**; **On Success** prints a message that holds whether or not the address has an account. [Blueprint snippet: acct-forgot, see /bp/unreal-sdk/acct-forgot.txt] ### Change or add a password A settings screen calls **Change Password** with the current and the new password. An account made by magic link or social sign-in has no password to change, and is refused with **No Password**; show **Add Password** for it instead. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" public: // The settings screen's Change Password button. void ChangeMyPassword(const FString& CurrentPassword, const FString& NewPassword); UFUNCTION() void HandlePasswordChanged(); UFUNCTION() void HandlePasswordRefused(FCrowdyAccountError Error, FString Message); // LanternGameInstance.cpp void ULanternGameInstance::ChangeMyPassword(const FString& CurrentPassword, const FString& NewPassword) { FOnAccountDone Changed; Changed.BindDynamic(this, &ULanternGameInstance::HandlePasswordChanged); FOnAccountError Refused; Refused.BindDynamic(this, &ULanternGameInstance::HandlePasswordRefused); GetSubsystem()->ChangePassword(CurrentPassword, NewPassword, Changed, Refused); } void ULanternGameInstance::HandlePasswordChanged() { UE_LOG(LogTemp, Log, TEXT("Password changed. Your other devices are signed out.")); } void ULanternGameInstance::HandlePasswordRefused(FCrowdyAccountError Error, FString Message) { // An account made by magic link or social sign-in has no password to change: offer Add Password instead. if (Error.Code == ECrowdyAccountErrorCode::NoPassword) { UE_LOG(LogTemp, Log, TEXT("This account has no password yet. Add one instead.")); return; } UE_LOG(LogTemp, Warning, TEXT("The password was not changed: %s"), *Message); } ``` A custom event `OnChangePasswordPressed` runs **Change Password**. **On Error** breaks the `Error`, compares its `Code` with **No Password**, and only on a match prints the hint to add one. [Blueprint snippet: acct-password, see /bp/unreal-sdk/acct-password.txt] ### Delete the account Stores that list a game with accounts expect the player to be able to delete their account from inside it. **Delete Account** runs only from the confirm button of your own "are you sure" dialog; the SDK draws none. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyAuthentication.h" public: // Called only after the player confirmed, in your own dialog, that the account should go. void DeleteMyAccount(); UFUNCTION() void HandleAccountDeleted(); // LanternGameInstance.cpp void ULanternGameInstance::DeleteMyAccount() { FOnAccountDone Deleted; Deleted.BindDynamic(this, &ULanternGameInstance::HandleAccountDeleted); FOnAccountError Failed; Failed.BindDynamic(this, &ULanternGameInstance::HandleAccountFailed); GetSubsystem()->DeleteAccount(Deleted, Failed); } void ULanternGameInstance::HandleAccountDeleted() { // The SDK has already signed out; the next screen is the sign-in screen. UE_LOG(LogTemp, Log, TEXT("The account is deleted.")); } ``` A custom event `OnDeleteAccountConfirmed` runs **Delete Account**; **On Success** prints that the account is gone and **On Error** prints the `Message`. The SDK has signed out by the time **On Success** runs. [Blueprint snippet: acct-delete, see /bp/unreal-sdk/acct-delete.txt] ## Linked identities A signed-in player can attach more ways to sign in. `GetMyIdentities` lists them (`FOnIdentitiesReceived`, an array of `FCrowdyUserIdentity`: `IdentityId`, `UserId`, `Provider`, `Subject`, `Email`, `bEmailVerified`, `CreatedAt`, `LastLoginAt`). `BeginLinkIdentity(Provider, ...)` runs the same browser and loopback flow as the social sign-in but attaches the result to the current session instead of starting one (`FOnIdentityLinked`). `UnlinkIdentity(IdentityId, ...)` removes one (`FOnIdentityUnlinked`, `bRemoved`). All three need an active session. If the player signs out while `BeginLinkIdentity` is waiting for the server's first answer, the answer is dropped when it arrives and the call reports `Signed out before sign-in finished.`; no identity is linked. :::caution[The server refuses to unlink a player's last remaining sign-in method.] Expect the refusal in a UI that lists identities with a remove button; do not treat it as an error to retry. ::: ## Gotchas - Nothing connects until someone signs in. Every entity and event depends on it. - A sign-in can wait for the player. While `IsLegalAcceptancePending()` is true none of that sign-in's handlers has fired yet, so do not retry it or time it out; [Let the player agree to the terms](#let-the-player-agree-to-the-terms). - `Register` on an existing address is a refusal, `EMAIL_ALREADY_REGISTERED`; offer Login instead. - The link and social flows need the player to reach a browser or inbox while the game runs; on a machine that cannot, use a password. - `OnAppTokenRefreshed` fires on every rotation. You rarely need it; the connection is re-armed for you. - **Delete Account** and **Sign Out Everywhere** end this session too: the SDK signs out, and the player signs in again to play. - Studio's own sign-in is the same account and the same methods; [Signing In](../studio/sign-in.md). ## Related - [Quickstart](../quickstart.md): the sign-in step this page extends. - [Connection and reconnect](../runtime/connection-and-reconnect.md): the realtime connection the sign-in requests. - [Sessions and presence](../concepts/sessions-and-presence.md): the other meanings of the word session. - [Teams](../services/teams.md) and [Avatars](../services/avatars.md): services that need a signed-in player. - [Player Names](./player-names.md): nametags, rosters, and letting the player choose a name. - [Sign in on the Management API](/management-api/authentication): the calls at the GraphQL level. # Player Names Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/player-names # Player Names Every account has a public name: a **gamertag**, and a **disambiguation** that makes the pair unique, so two players can share a gamertag as long as their disambiguations differ. A game reads names by User ID, the `int64` it already has for every player, and the signed-in player can choose their own. A player who has never chosen a name reads an empty gamertag. ## When you touch this When you draw a nametag over a player, list the members of a team or a match, or add a Choose Your Name step to the first launch. ## The surface `UCrowdyPlayers` (Game Instance subsystem, **Crowdy Players**, category **Crowdy SDK, Players**) has three calls, each also a latent node. Names arrive as `FCrowdyPlayerProfile`: `UserID`, `Gamertag`, `Disambiguation`. A failure carries an `FCrowdyAccountError` (`Code`, `Message`) and the `Message`, the same error the [account settings](./authentication.md#account-settings) calls use. | Blueprint (C++) | What it does | |---|---| | **Get Player Name** (`GetPlayerName(UserID, OnResult, OnError)`) | One player's name. A User ID with no player fails with **Not Found**. | | **Get Player Names** (`GetPlayerNames(UserIDs, OnResult, OnError)`) | Up to 100 players' names in one call (`UCrowdyPlayers::MaxProfilesPerCall`); more fails at once without a request, and none answers an empty list. Unknown User IDs are left out of the answer, so match the rows by `UserID`, not by position. | | **Set Player Name** (`SetPlayerName(Gamertag, Disambiguation, OnResult, OnError)`) | Renames the signed-in player and answers the new name. A pair another player has fails with **Name Taken**. A gamertag is at most 64 characters, a disambiguation at most 128. | The C++ calls answer on `FOnPlayerProfileRead` (one profile), `FOnPlayerProfilesRead` (an array) and `FOnPlayersError`. The nodes are `UCrowdyPlayers_GetPlayerName`, `UCrowdyPlayers_GetPlayerNames` and `UCrowdyPlayers_SetPlayerName`, whose pins are `FPlayerProfileAsyncResult` (`Profile`, `Error`, `Message`) and, for the batch, `FPlayerProfilesAsyncResult` (`Profiles`, `Error`, `Message`). All three need a signed-in player. Any player may read any other player's name, and nothing else about them. ## Show a nametag When a player appears, read their name by User ID and put the gamertag on their nametag. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyPlayers.h" public: // A nametag asks for its player's name when the player appears. void ShowNametag(int64 UserId); UFUNCTION() void HandleNametag(FCrowdyPlayerProfile Profile); UFUNCTION() void HandlePlayersFailed(FCrowdyAccountError Error, FString Message); // LanternGameInstance.cpp void ULanternGameInstance::ShowNametag(int64 UserId) { FOnPlayerProfileRead Read; Read.BindDynamic(this, &ULanternGameInstance::HandleNametag); FOnPlayersError Failed; Failed.BindDynamic(this, &ULanternGameInstance::HandlePlayersFailed); GetSubsystem()->GetPlayerName(UserId, Read, Failed); } void ULanternGameInstance::HandleNametag(FCrowdyPlayerProfile Profile) { // A player who has not chosen a name has an empty Gamertag. const FString Shown = Profile.Gamertag.IsEmpty() ? TEXT("A villager") : Profile.Gamertag; UE_LOG(LogTemp, Log, TEXT("Player %lld is %s"), Profile.UserID, *Shown); } void ULanternGameInstance::HandlePlayersFailed(FCrowdyAccountError Error, FString Message) { UE_LOG(LogTemp, Warning, TEXT("Could not read the name: %s"), *Message); } ``` A custom event `OnPlayerAppeared` with a `UserID` parameter runs **Get Player Name**; **On Success** breaks the `Profile` and prints its `Gamertag`. [Blueprint snippet: players-nametag, see /bp/unreal-sdk/players-nametag.txt] For a roster, collect the User IDs and make one **Get Player Names** call rather than one call per player. ## Let the player choose a name A Choose Your Name screen on the first launch calls **Set Player Name** with the two fields. When the pair is taken, ask for another disambiguation. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyPlayers.h" public: // The first-launch Choose Your Name screen. void ChooseName(const FString& Gamertag, const FString& Disambiguation); UFUNCTION() void HandleNameChosen(FCrowdyPlayerProfile Profile); UFUNCTION() void HandleNameRefused(FCrowdyAccountError Error, FString Message); // LanternGameInstance.cpp void ULanternGameInstance::ChooseName(const FString& Gamertag, const FString& Disambiguation) { FOnPlayerProfileRead Chosen; Chosen.BindDynamic(this, &ULanternGameInstance::HandleNameChosen); FOnPlayersError Refused; Refused.BindDynamic(this, &ULanternGameInstance::HandleNameRefused); GetSubsystem()->SetPlayerName(Gamertag, Disambiguation, Chosen, Refused); } void ULanternGameInstance::HandleNameChosen(FCrowdyPlayerProfile Profile) { UE_LOG(LogTemp, Log, TEXT("You are %s (%s)"), *Profile.Gamertag, *Profile.Disambiguation); } void ULanternGameInstance::HandleNameRefused(FCrowdyAccountError Error, FString Message) { if (Error.Code == ECrowdyAccountErrorCode::NameTaken) { UE_LOG(LogTemp, Warning, TEXT("Someone already has that name. Try another disambiguation.")); return; } UE_LOG(LogTemp, Warning, TEXT("The name was not changed: %s"), *Message); } ``` A custom event `OnChooseNamePressed` runs **Set Player Name**. **On Error** breaks the `Error`, compares its `Code` with **Name Taken**, and only on a match prints the hint. [Blueprint snippet: players-rename, see /bp/unreal-sdk/players-rename.txt] ## Gotchas - Names are not live: read again when you need a current one, for example when a player joins a team or a match. - The pair is the unique handle. Show both where players must tell two names apart, such as a friends list. - A player's own name is read with their own User ID, from `UCrowdyGameSession::GetUserID()`. - Crowdy Studio's [Account page](../studio/sign-in.md#your-account) sets your own name as the developer. ## Related - [Authentication](./authentication.md): signing in, and the account settings that share this page's error type. - [Teams](./teams.md): the member lists a roster names. # Teams Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/teams # Teams A team is a persistent, named group of players, scoped to your app, with membership, roles, and permissions the app controls. A game creates, joins, and manages teams live from `UCrowdyTeams`; Crowdy Studio's Teams pane edits the same rows ahead of time. ## When you touch this Any time players need a group that outlives one session, a guild, a faction, a village, with roles you can grant and revoke. In the example world a village is a team: joining one is how a player shows they belong to a place. ## Runtime and Studio, one dataset Creating, updating, and deleting teams is not a Studio-only workflow. The same create, update, delete, role, and policy operations run from game code through `UCrowdyTeams`, subject to your app's team creation policy, and Crowdy Studio's Teams pane reads and writes the identical server rows. There is one store, not two. Membership, roles, and permissions are server-authoritative truth, pulled through `UCrowdyTeams` rather than trusted from the client, the same split every server-owned system in the SDK follows: see [The Two Planes](../concepts/two-planes.md) for the general story. There is no container here, just the Team API `UCrowdyTeams` wraps one call at a time. Joining a team does not put a player in a match; see [Sessions and presence](../concepts/sessions-and-presence.md). The server owns team, role, and permission storage and every authorization check; a disallowed call comes back through the error delegate rather than being refused on the client. The exact GraphQL shape behind each call is on [Teams](/game-api/teams) and in the [operations reference](/game-api/reference/graphql/operations/mutations/create-team); a team can also be granted permissions over a grid region, covered on [Grids and permissions](/game-api/grids-and-permissions#group-grants-per-team-or-group). Every call below is asynchronous and answers through exactly one of the two delegates it is given, even when the request never reaches the server. ## Reading the cache first `GetCachedMyTeams` and its neighbors read a local snapshot of the player's own memberships. The SDK keeps it current for you: - Signing in (log in, register, magic link, social, or a restored session) clears it and fills it again with a `GetMyTeams`. - Signing out clears it: `HasCachedTeams()` is false, and `OnMyTeamsCacheChanged` fires with an empty list if the cache held anything. - A successful `CreateTeam`, `JoinTeam`, `LeaveTeam`, `DeleteTeam`, `UpdateTeam`, `RemoveTeamMember`, `SetTeamMemberRoles`, `UpdateTeamRole` or `DeleteTeamRole` refreshes it. `RequestToJoin` does not, since the membership is still pending. - An answer to a `GetMyTeams` sent before a sign-out never fills the cache, and an older answer never replaces a newer one. Read it for the value to show now; treat a query's result and `OnMyTeamsCacheChanged` as the update that follows. In C++, `ClearMyTeamsCache()` empties it by hand and `RefreshMyTeams()` asks for the player's teams again, with the answer arriving through `OnMyTeamsCacheChanged`; neither is exposed to Blueprint. | Call | Returns | |---|---| | `HasCachedTeams()` | Whether the cache has been populated yet. | | `GetCachedMyTeams()` | The player's memberships (`TArray`) as of the last refresh. | | `IsPlayerInTeam(TeamId)` | Whether the player belongs to that team, from the cache. | | `GetMyTeamById(TeamId, OutMembership)` | The cached membership for one team, if any. | | `IsInAnyTeam()` | Whether the cache holds at least one membership. | | `GetPrimaryMembership(OutMembership)` | The first cached membership. | | `HasPermissionInTeam(TeamId, Permission)` | Whether the cached membership grants an `ECrowdyTeamPermission`. | `OnMyTeamsCacheChanged` (`FOnMyTeamsCacheChanged`, `Memberships`) is a multicast, Blueprint-assignable delegate that fires whenever the cache changes: after a `GetMyTeams` answers, after a mutation above refreshes it, and when a sign-out empties it. Bind it once and re-read the cache; do not poll. ## Queries Each query takes an `FOnTeamError` alongside its success delegate; bind it to a `UFUNCTION` with the signature `(FCrowdyTeamError Error, FString Message)`, where `Message` repeats `Error.Message`. | Call | Success delegate | Notes | |---|---|---| | `GetMyTeams(OnSuccess, OnError)` | `FOnMyTeamsSuccess` | Repopulates the cache and fires `OnMyTeamsCacheChanged`. | | `GetTeam(TeamId, OnSuccess, OnError)` | `FOnTeamSuccess` | One team by id. | | `GetTeams(OnSuccess, OnError)` | `FOnTeamsSuccess` | Every team in the app, not just the player's. | | `GetTeamMembers(TeamId, OnSuccess, OnError)` | `FOnTeamMembersSuccess` | | | `GetTeamRoles(TeamId, OnSuccess, OnError)` | `FOnTeamRolesSuccess` | | | `GetTeamPolicy(OnSuccess, OnError)` | `FOnTeamPolicySuccess` | App-wide, not per-team. | | `GetPendingJoinRequests(TeamId, OnSuccess, OnError)` | `FOnTeamMembersSuccess` | See the warning below. | :::note[GetPendingJoinRequests re-fetches the full member list and filters on the client.] It runs the same query as `GetTeamMembers` and then keeps only the members whose status is pending. That is fine at team scale, a handful to a few dozen members, but do not call it in a hot loop expecting a cheaper, dedicated request. ::: ## Creating and managing a team Every mutation reports success through its own delegate and failure through the shared `FOnTeamError`. **Team lifecycle:** `CreateTeam(Name, Description, MembershipPolicy, OnSuccess, OnError)` returns `FOnTeamSuccess` with the new `FCrowdyTeam`; `UpdateTeam(TeamId, Name, Description, OnSuccess, OnError)` also returns `FOnTeamSuccess`; `DeleteTeam(TeamId, OnSuccess, OnError)` returns `FOnTeamVoidSuccess`. **Membership:** `JoinTeam(TeamId, OnSuccess, OnError)` honors the team's `ECrowdyTeamMembershipPolicy`, seating the player immediately for an `Open` team; `RequestToJoinTeam(TeamId, OnSuccess, OnError)` is for a `Request` policy team and returns a pending member. Both return `FOnTeamMemberSuccess`. `LeaveTeam(TeamId, OnSuccess, OnError)` returns `FOnTeamVoidSuccess`. `AddTeamMember(TeamId, UserId, OnSuccess, OnError)` is the admin add that bypasses the membership policy; `RemoveTeamMember(TeamId, UserId, OnSuccess, OnError)` takes them off. `AddTeamMember` answers with `FOnTeamMemberSuccess`, `RemoveTeamMember` with `FOnTeamVoidSuccess`. **Roles and permissions:** `CreateTeamRole(TeamId, RoleName, Permissions, Rank, OnSuccess, OnError)` and `UpdateTeamRole(TeamRoleId, RoleName, Permissions, OnSuccess, OnError)` return `FOnTeamRoleSuccess`; `DeleteTeamRole(TeamRoleId, OnSuccess, OnError)` returns `FOnTeamVoidSuccess`. `SetTeamMemberRoles(TeamId, UserId, RoleIds, OnSuccess, OnError)` returns `FOnTeamMemberSuccess`. `Permissions` is an `FCrowdyTeamPermissions` your code builds before the call. :::warning[SetTeamMemberRoles replaces the member's roles, it does not add to them.] Pass every role the member should end up holding, including ones they already have, or the call drops the roles you left out. ::: **App-wide policy:** `SetTeamPolicy(CreationPolicy, DefaultMembershipPolicy, OnSuccess, OnError)` returns `FOnTeamPolicySuccess` and is an app-admin operation. ## Ban a member A manager removes a member for good with `BanTeamMember(TeamId, UserId, Reason, OnSuccess, OnError)`. It takes the user off the team and refuses every later join, request to join and add of that user until a manager lifts the ban with `UnbanTeamMember(TeamId, UserId, OnSuccess, OnError)`. The caller needs the manage-members permission, and the team's owner and the caller themselves cannot be banned. A user who is not on the team yet can be banned too, and leaving or being removed does not lift a ban. `Reason` is optional and at most 500 characters. A longer one fails at once with a `PolicyViolation` error, and nothing is sent. Ban answers the member record (`FOnTeamMemberSuccess`), whose `Status` is `banned`; unban answers `bWasBanned` (`FOnTeamUnbanSuccess`), which is false when the user had no ban. A banned user's `JoinTeam`, `RequestToJoinTeam`, or a manager's `AddTeamMember` for them, fails with the error code `Banned` in `ECrowdyTeamErrorCode`. It comes from the server's own error code, not from the message text, so it is safe to branch on. A ban lasts until a manager lifts it, so do not retry: tell the player instead. The village's elder bans a member from the village team. `VillageTeamId` is the id the game instance kept when it founded the village. A Ban button on the member list calls `BanFromVillage` with the member picked. `HandleVillageBanned` receives the member record, and `HandleVillageBanFailed` the error. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyTeams.h" public: void BanFromVillage(int64 UserId, const FString& Reason); UFUNCTION() void HandleVillageBanned(FCrowdyTeamMember Member); UFUNCTION() void HandleVillageBanFailed(FCrowdyTeamError Error, FString Message); // LanternGameInstance.cpp void ULanternGameInstance::BanFromVillage(int64 UserId, const FString& Reason) { UCrowdyTeams* Teams = GetSubsystem(); FOnTeamMemberSuccess Banned; Banned.BindDynamic(this, &ULanternGameInstance::HandleVillageBanned); FOnTeamError Failed; Failed.BindDynamic(this, &ULanternGameInstance::HandleVillageBanFailed); Teams->BanTeamMember(VillageTeamId, UserId, Reason, Banned, Failed); } void ULanternGameInstance::HandleVillageBanned(FCrowdyTeamMember Member) { UE_LOG(LogTemp, Log, TEXT("User %lld is banned from the village"), Member.UserId); } void ULanternGameInstance::HandleVillageBanFailed(FCrowdyTeamError Error, FString Message) { UE_LOG(LogTemp, Warning, TEXT("The ban failed: %s"), *Message); } ``` **Ban Team Member** is a latent node with `Team Id`, `User Id` and `Reason` inputs, and **Unban Team Member** one with `Team Id` and `User Id`. They are wired the way **Ban Channel Member** is in [Ban a member](../runtime/channels.md#ban-a-member): **On Success** carries the member record (`Was Banned` for an unban), and **On Error** carries `Error` and `Message`. Break `Error` with **Break Crowdy Team Error** and compare its `Code` with **Banned** in a **Branch** to handle a ban on **Join Team**, as [Handle a ban on join](../runtime/channels.md#handle-a-ban-on-join) does for a channel. ## Data types | Struct | Holds | |---|---| | `FCrowdyTeam` | `TeamId`, `AppId`, `Name`, `Description`, `OwnerUserId`, `MembershipPolicy`, `Status`, `CreatedAt`. | | `FCrowdyTeamMember` | `TeamMemberId`, `TeamId`, `UserId`, `Status` (active, pending, or banned in the answer to a ban), `Roles` sorted by rank, `CreatedAt`. | | `FCrowdyTeamMembership` | `Team`, `Roles`, the effective `Permissions` (a server-computed union across roles), `JoinedAt`, and `HasPermission(ECrowdyTeamPermission)`. | | `FCrowdyTeamPermissions` | Flags `bManageTeam`, `bManageMembers`, `bManageRoles`, `bInviteMembers`, all `BlueprintReadWrite`, and `Has()`. Build one to pass into `CreateTeamRole` or `UpdateTeamRole`. | | `FCrowdyTeamPolicy` | `AppId`, `CreationPolicy`, `DefaultMembershipPolicy`, `MaxMembers` (0 is uncapped), `MaxTeamsPerUser` (0 is uncapped). | | `FCrowdyTeamRole` | `TeamRoleId`, `TeamId`, `RoleName`, `Rank`, `bIsSystem` (every team has an undeletable system leader role), `Permissions`, `CreatedAt`. | | `FCrowdyTeamError` | `Code` (`ECrowdyTeamErrorCode`) and `Message`. | | Enum | Values | |---|---| | `ECrowdyTeamCreationPolicy` | `Admin`, `Member`, `Anyone` | | `ECrowdyTeamMembershipPolicy` | `Open`, `Request`, `Invite`, `Admin` | | `ECrowdyTeamPermission` | `ManageTeam`, `ManageMembers`, `ManageRoles`, `InviteMembers` | | `ECrowdyTeamErrorCode` | `Unknown`, `NotFound`, `Forbidden`, `PolicyViolation`, `AlreadyMember`, `NotMember`, `NetworkError`, `ServerError`, `Banned` | :::note[Code is a coarse bucket, not a precise diagnosis, except Banned.] `FCrowdyTeamError` is built by pattern-matching the server's message text, so every `Code` but one is a guess. `Banned` is the exception: it comes from the server's own error code. Read `Message` for the detail and branch on `Code` only for the broad cases. ::: ## Blueprint nodes The subsystem's per-call delegates need a Create Event node per call. For a graph that just drops in a node, use the async-action latent nodes instead: one static factory and `Activate()` per node, with multicast `OnSuccess`/`OnError`, wrapping the same server calls. | Node | Class | Returns | |---|---|---| | Get My Teams | `UCrowdyTeams_GetMyTeams` | `FCrowdyMyTeamsResult` | | Get Team | `UCrowdyTeams_GetTeam` | `FCrowdyTeam` | | Get All Teams | `UCrowdyTeams_GetTeams` | `FCrowdyTeamsResult` | | Get Team Members | `UCrowdyTeams_GetTeamMembers` | `FCrowdyTeamMembersResult` | | Get Team Roles | `UCrowdyTeams_GetTeamRoles` | `FCrowdyTeamRolesResult` | | Get Team Policy | `UCrowdyTeams_GetTeamPolicy` | `FCrowdyTeamPolicy` | | Get Pending Join Requests | `UCrowdyTeams_GetPendingJoinRequests` | `FCrowdyTeamMembersResult` | | Create Team | `UCrowdyTeams_CreateTeam` | `FCrowdyTeam` | | Update Team | `UCrowdyTeams_UpdateTeam` | `FCrowdyTeam` | | Delete Team | `UCrowdyTeams_DeleteTeam` | void | | Join Team | `UCrowdyTeams_JoinTeam` | `FCrowdyTeamMember` | | Request to Join Team | `UCrowdyTeams_RequestToJoinTeam` | `FCrowdyTeamMember` | | Leave Team | `UCrowdyTeams_LeaveTeam` | void | | Add Team Member | `UCrowdyTeams_AddTeamMember` | `FCrowdyTeamMember` | | Remove Team Member | `UCrowdyTeams_RemoveTeamMember` | void | | Ban Team Member | `UCrowdyTeams_BanTeamMember` | `FCrowdyTeamMember` | | Unban Team Member | `UCrowdyTeams_UnbanTeamMember` | `Was Banned` (bool) | | Set Team Member Roles | `UCrowdyTeams_SetTeamMemberRoles` | `FCrowdyTeamMember` | | Create Team Role | `UCrowdyTeams_CreateTeamRole` | `FCrowdyTeamRole` | | Update Team Role | `UCrowdyTeams_UpdateTeamRole` | `FCrowdyTeamRole` | | Delete Team Role | `UCrowdyTeams_DeleteTeamRole` | void | | Set Team Policy | `UCrowdyTeams_SetTeamPolicy` | `FCrowdyTeamPolicy` | `FCrowdyTeamsResult`, `FCrowdyMyTeamsResult`, `FCrowdyTeamMembersResult`, and `FCrowdyTeamRolesResult` each wrap one multi-item result (`Teams`, `Memberships`, `Members`, `Roles`); a single-item call like `GetTeam` or `CreateTeam` returns its struct directly. **On Success** and **On Error** of a node share one set of data pins: the result, then `Error` and `Message`. On **On Success**, `Error` and `Message` are empty. On **On Error** the result is empty, `Error` is an `FCrowdyTeamError` (break it to read its `Code`) and `Message` is the text to show; a node that returns nothing has only `Error` and `Message`. The pins are a multicast delegate named after the result: `FTeamAsyncResult` (Get Team, Create Team, Update Team), `FTeamsAsyncResult` (Get All Teams), `FMyTeamsAsyncResult` (Get My Teams), `FMemberAsyncResult` (Join Team, Request to Join Team, Add Team Member, Ban Team Member, Set Team Member Roles), `FMembersAsyncResult` (Get Team Members, Get Pending Join Requests), `FRoleAsyncResult` (Create Team Role, Update Team Role), `FRolesAsyncResult` (Get Team Roles), `FPolicyAsyncResult` (Get Team Policy, Set Team Policy), `FTeamUnbanAsyncResult` (Unban Team Member), and `FTeamsAsyncOnError` (Delete Team, Leave Team, Remove Team Member, Delete Team Role). These are separate from the per-call `FOnTeam*Success` delegates above; the async-action nodes never use those. :::caution[Bind a per-call delegate to a UFUNCTION on a UObject that outlives the request.] The response arrives asynchronously; if the object is gone by then, the callback has nowhere to land. ::: :::note[The array delegates pass `const TArray&`.] `FOnTeamsSuccess`, `FOnTeamMembersSuccess`, `FOnTeamRolesSuccess`, `FOnMyTeamsSuccess`, and the cache event `FOnMyTeamsCacheChanged` pass their array as `const TArray&`, which is what the C++ handlers on this page take. A handler must match or it fails to bind, and a project coming from 2.14.0 or earlier, where these passed by value, changes its handlers once; see [What's Changed](../guides/whats-changed.md#2026-09-22-sdk-v2150). In Blueprint prefer the async-action nodes above, whose result pins are structs; a Blueprint pin has no by-value distinction. ::: ## Example: founding and joining a village A village is a team. The player asks to join "the Village," and a successful join lights their `Torch`, the light component the example cast already carries, to show membership at a glance. A "Found the Village" button calls `FoundVillage()`, which calls `CreateTeam("The Village", "Everyone who shares one fire.", Open, HandleVillageFounded, FOnTeamError())`; `HandleVillageFounded(FCrowdyTeam)` stores the new id in a private `VillageTeamId`, read back through the `BlueprintPure` `GetVillageTeamId()`: ```cpp // LanternGameInstance.h #include "Subsystem/CrowdyTeams.h" public: // A Found the Village button calls this; the id the handler keeps is the one every player joins. UFUNCTION(BlueprintCallable, Category = "Lantern") void FoundVillage(); UFUNCTION(BlueprintPure, Category = "Lantern") int64 GetVillageTeamId() const { return VillageTeamId; } private: int64 VillageTeamId = 0; UFUNCTION() void HandleVillageFounded(FCrowdyTeam Team); // LanternGameInstance.cpp void ULanternGameInstance::FoundVillage() { UCrowdyTeams* Teams = GetSubsystem(); FOnTeamSuccess Founded; Founded.BindDynamic(this, &ULanternGameInstance::HandleVillageFounded); Teams->CreateTeam(TEXT("The Village"), TEXT("Everyone who shares one fire."), ECrowdyTeamMembershipPolicy::Open, Founded, FOnTeamError()); } void ULanternGameInstance::HandleVillageFounded(FCrowdyTeam Team) { VillageTeamId = Team.TeamId; } ``` Overlapping the village post calls `JoinVillage()`, which calls `JoinTeam(GetVillageTeamId(), ...)`; on success `HandleJoinedVillage(FCrowdyTeamMember)` sets `Torch->SetVisibility(true)`: ```cpp // LanternPlayer.h #include "Subsystem/CrowdyTeams.h" virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; void JoinVillage(); UFUNCTION() void HandleJoinedVillage(FCrowdyTeamMember Member); // LanternPlayer.cpp #include "LanternGameInstance.h" #include "LanternPost.h" void ALanternPlayer::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); if (Cast(OtherActor)) { JoinVillage(); } } void ALanternPlayer::JoinVillage() { const ULanternGameInstance* Game = GetGameInstance(); UCrowdyTeams* Teams = Game ? Game->GetSubsystem() : nullptr; if (!Teams || Game->GetVillageTeamId() == 0) { return; } // Open membership seats the player at once; a Request team would answer with a pending member instead. FOnTeamMemberSuccess Joined; Joined.BindDynamic(this, &ALanternPlayer::HandleJoinedVillage); Teams->JoinTeam(Game->GetVillageTeamId(), Joined, FOnTeamError()); } void ALanternPlayer::HandleJoinedVillage(FCrowdyTeamMember Member) { Torch->SetVisibility(true); } ``` `ShowVillageMembership()`, called from `BeginPlay`, guards on `HasCachedTeams()` and sets `Torch->SetVisibility(Teams->IsPlayerInTeam(GetVillageTeamId()))` from the cache alone, before any round trip completes: ```cpp // LanternPlayer.h void ShowVillageMembership(); // LanternPlayer.cpp #include "LanternGameInstance.h" #include "Subsystem/CrowdyTeams.h" void ALanternPlayer::ShowVillageMembership() { const ULanternGameInstance* Game = GetGameInstance(); UCrowdyTeams* Teams = Game ? Game->GetSubsystem() : nullptr; // The cache holds this player's own memberships only, and nothing until the first Get My Teams has answered. if (!Teams || !Teams->HasCachedTeams()) { return; } Torch->SetVisibility(Teams->IsPlayerInTeam(Game->GetVillageTeamId())); } ``` `WatchVillage()`, bound at `BeginPlay`, calls `OnMyTeamsCacheChanged.AddDynamic` and `GetMyTeams()`; `HandleVillageCacheChanged(const TArray&)` re-runs `ShowVillageMembership()` whenever the cache changes, from anywhere: ```cpp // LanternPlayer.h #include "Subsystem/CrowdyTeams.h" void WatchVillage(); UFUNCTION() void HandleVillageCacheChanged(const TArray& Memberships); // LanternPlayer.cpp void ALanternPlayer::WatchVillage() { UCrowdyTeams* Teams = GetGameInstance()->GetSubsystem(); Teams->OnMyTeamsCacheChanged.AddDynamic(this, &ALanternPlayer::HandleVillageCacheChanged); // Only Get My Teams fills the cache; a join or a leave does not, so pull again after one. Teams->GetMyTeams(FOnMyTeamsSuccess(), FOnTeamError()); } void ALanternPlayer::HandleVillageCacheChanged(const TArray& Memberships) { ShowVillageMembership(); } ``` The Blueprint needs an `int64` variable `VillageTeamId` and the `Torch` light component the cast already has. Creating the village: the **Create Team** async node takes `Name` and `Description` literals and a `MembershipPolicy` of `Open`; its `On Success` pin fires when the server has the team, breaks the `Team` output, and sets `TeamId` into `VillageTeamId`. [Blueprint snippet: team-create, see /bp/unreal-sdk/team-create.txt] Joining it: overlapping the village post fires **Event ActorBeginOverlap**, which reads `VillageTeamId` and calls **Join Team**; its `On Success` pin gets the `Torch` and sets its visibility. [Blueprint snippet: team-join, see /bp/unreal-sdk/team-join.txt] Reading the cache on spawn: **Event BeginPlay** gets the `Torch`, gets the **Crowdy Teams** subsystem and `VillageTeamId`, and feeds them into **Is Player in Team** (pure), whose return value drives the torch's visibility directly, no network call. The cache is empty until the sign-in refresh or a **Get My Teams** has answered, so on its own this graph hides the torch on spawn; the sync step below is what shows it. A **Has Cached Teams** (pure) check into **Branch** guards the read, so the visibility set only runs once the cache has answered. [Blueprint snippet: team-query, see /bp/unreal-sdk/team-query.txt] Staying in sync: add a custom event with one input, `Memberships` (array of `Crowdy Team Membership`), connect it with **Bind Event to On My Teams Cache Changed** at Begin Play, then call **Get My Teams**; the event's body re-runs the membership check above. No figure is shown for this step; the C++ tab shows the same one. ## Gotchas - The cache holds only the player's own memberships. `GetTeams` lists every team in the app; the cache never does. - A successful join, leave, create, delete or role change refreshes the cache, but a pending `RequestToJoin` does not, since the player is not a member yet. Between a change and its refresh, `IsPlayerInTeam` still answers from before it. - `GetPendingJoinRequests` filters the full member list on the client; it is not a dedicated server call. - `SetTeamMemberRoles` replaces the member's role set. Pass the full set every time. - `FCrowdyTeamError::Code` is pattern-matched from the server's message text, except `Banned`, which is the server's own code; treat the rest as a coarse bucket and `Message` as the detail. - A per-call delegate needs a `UFUNCTION` on a `UObject` that outlives the request, including when the request never reaches the server. - The array delegates take `const TArray&` handlers; a mismatch fails to bind. The async-action nodes in Blueprint have no such split. ## Related - [Authentication](./authentication.md): a signed-in player is what every team call needs. - [Avatars](./avatars.md) - [Voice chat](./voice-chat.md) - [Host election](./host-election.md) - [Channels](../runtime/channels.md): a sibling group-like system that shares the same permission table. - [Teams and Channels in Crowdy Studio](../studio/teams-and-channels.md): the same data, authored ahead of time. - [Play together](./play-together.md): parties and guilds, a team and its chat channel as the Game Kit's other clients make them. # Play together with parties, chat, emotes and spectating Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/play-together # Play together with parties, chat, emotes and spectating :::note These are the platform's [Game Kit](/game-kit/intro) conventions for playing together. None of them is a starter: nothing is deployed, and they work in any app with [teams](./teams.md) and [channels](../runtime/channels.md). ::: Players group up, talk, wave at each other and watch each other's matches. The SDK does this the way the platform's other clients do, so a player on Unreal, CrowdyCPP or CrowdyJS finds the same party, reads the same chat and sees the same emote: - **A party** is a [team](./teams.md) named `party:` that only its members can add players to, and a chat [channel](../runtime/channels.md) of the same name. **A guild** is the same with `guild:`, a team players ask to join, with roles. **A chat room** is one channel, found or made by its name. - **An emote** is a short name, such as `wave`, that a player sends to the players near them, optionally aimed at one actor. - **A spectator** is a player with no avatar in the world who still receives what happens near the place they watch. Parties, guilds and chat are in the **Crowdy SDK|Game Kit|Social** category, emotes in **Crowdy SDK|Game Kit|Emotes** and the spectator in **Crowdy SDK|Game Kit|Spectate**. ## Make a party and talk in it **Create Party** makes the team `party:`, whose policy is invite only, and its chat channel, and makes the player its leader. It answers **Completed** with the party's **Group**, an `FCrowdyKitGroup`: its full **Name**, **Team Id**, **Channel Id** and **Owner User Id**. It answers **Failed** when either cannot be made, for example when the name is taken. **Find Party** finds a party by its name, and **Invite To Party** adds a player, by their user id, to the team and its channel; only a member with the manage members permission can invite, as for any team. **Join Party** joins a party whose team is open, **Leave Party** leaves its channel and then its team, and **Get Party Members** lists its team's members. **Send Chat** sends text of at most 1024 bytes of UTF-8 to a group's channel. Its result is an exec pin each: **Sent**, **Empty Text**, **Too Long**, **No Channel** (the group has none) and **Not Connected** (the realtime connection is not up). **Sent** means the message left this client: the server delivers it to the channel's members and drops it from a player who is not one, or from any player whose access tier does not grant the `use_text_chat` permission; [On Send Refused](../runtime/connection-and-reconnect.md#when-the-server-refuses-a-send) is where that refusal shows. **On Chat Message** on the **Crowdy Kit Social** subsystem fires for each message from a party, guild or chat room this player has, with the **Group**, the sender's **Sender Uuid** and the **Text**. A player's own messages do not come back to them. Anyone can make a team named `party:Bob` before Bob does. Find Party only pairs a team with a channel of the same name when one player owns both, and **Owner User Id** says whose they are, so a game that looks up a friend's party can check it is the friend's. The sender of a chat message is what the sender's client says; treat it as a name to show, not as proof. In Oakford, the lamplighters go out together at dusk. **In Blueprint**, on the lamplighters' board: 1. Add **Create Party** with **Name** `lamplighters`. From **Completed**, set `Room` to its **Group**, then add **Invite To Party** with the **Group** and a friend's **User Id**. 2. On the chat box's commit, add **Send Chat (Crowdy Kit Social)** with the **Group** and the text; on **Too Long**, ask for a shorter line. 3. At **Event BeginPlay**, from the **Crowdy Kit Social** subsystem getter, bind **On Chat Message** and add **Text** to the chat box. `FriendUserId` and `Room` are the board's variables: the friend's user id, and the party's **Group**, which the chat sends to. [Blueprint snippet: social-party-invite, see /bp/unreal-sdk/social-party-invite.txt] [Blueprint snippet: social-chat, see /bp/unreal-sdk/social-chat.txt] **In C++**: ```cpp #include "Subsystem/CrowdyKitSocial.h" void ALamplighterBoard::StartParty(int64 FriendUserId) { UCrowdyKitSocial* Social = GetGameInstance()->GetSubsystem(); Social->OnChatMessage.AddDynamic(this, &ALamplighterBoard::ShowChat); Social->CreateParty(TEXT("lamplighters"), FCrowdyKitGroupDone::CreateWeakLambda(this, [this, Social, FriendUserId](const FCrowdyKitGroup& Party) { LamplighterParty = Party; Social->InviteToParty(Party, FriendUserId, FCrowdyKitGroupDone(), FCrowdyKitFailed()); }), FCrowdyKitFailed::CreateWeakLambda(this, [](const FCrowdyTeamError& Error) { UE_LOG(LogTemp, Log, TEXT("No party: %s"), *Error.Message); })); } void ALamplighterBoard::Say(const FString& Text) { const ECrowdyKitChatResult Result = GetGameInstance()->GetSubsystem()->SendChat(LamplighterParty, Text); UE_CLOG(Result != ECrowdyKitChatResult::Sent, LogTemp, Log, TEXT("Not sent: %s"), *UCrowdyKitSocial::DescribeChatResult(Result)); } ``` `ShowChat` is a `UFUNCTION` taking `FCrowdyKitGroup Group, FString SenderUuid, FString Text`. Every call answers through exactly one of its two delegates, including when the request never reaches the server; a group with no team is **Not Found**, and no connection to the game's API is **Network Error**. ## Found a guild and let players in **Create Guild** makes the team `guild:`, players asking to join by default (its **Membership Policy**), and its chat channel. **Find Guild** finds it. A player calls **Join Guild**, which asks to join the team; a member with the manage members permission sees the request in **Get Guild Roster**, which lists pending requests too, and calls **Accept Into Guild** with the player's user id, which lets them into the team and adds them to the guild's channel. **Promote In Guild** replaces a member's roles with the role ids it is given; make the roles with [Create Team Role](./teams.md#creating-and-managing-a-team) on the guild's **Team Id**. A step that was done before is not an error the second time: inviting a player already in the party, or leaving a channel the player has left, goes on to the next step, so a call that failed half way can be made again. **Claim Guild Territory** grants the guild's members, or only the holders of one role, permission keys on a [grid](../studio/grids.md) of the app: `access` and `update_voxel_data` when its **Permission Keys** are empty. It needs grid admin rights on the app, so it fails **Forbidden** for a player who has none; call it from a tool or a server's account, not from a player's game. ## Open a chat room **Find Or Create Chat Room** finds the channel with that exact name, or makes it, and joins it unless the player is already in. Its **Group** has a **Channel Id** and no team. Chat in it with **Send Chat** and **On Chat Message** as in a party. The first player to make a room owns it, as for any channel. In Oakford, everyone talks on the square. **In Blueprint**, add **Find Or Create Chat Room** with **Name** `oakford-square` at **Event BeginPlay**, and keep its **Group** for **Send Chat**. **In C++**, `Social->FindOrCreateChatRoom(TEXT("oakford-square"), OnDone, OnFail)`. ## Send an emote **Send Emote** sends an emote from the local player's pawn to the players within two chunks of it, aimed at **Target**, an actor, or at nobody. It answers false, with **Reason** saying why, when nothing went out: the emote is empty, the emote and its target do not fit in 1024 bytes, the player is not signed in or not connected, there is no local pawn, or the target is not a replicated actor this client has heard from. True means the connection took it, not that anyone saw it. **On Emote** on the world's **Crowdy Kit Emotes** subsystem (**Get Kit Emotes**) fires for each emote that reaches this client, with the **Sender Actor** and **Target Actor** (None when this client has no such actor), their **Sender Uuid** and **Target Uuid** (empty when it is aimed at nobody) and the **Emote**. Nothing about an emote is kept or checked: the sender and the text are what the sender's client says. Map the text through your own list of emotes before playing an animation for it, and use a [Server Object](../exec/overview.md) for anything a player must not fake. Inbound emotes are limited twice. Each sender has a small budget of their own, `crowdy.kit.emotes.persender.maxpersecond` (default 4) refilling up to `crowdy.kit.emotes.persender.burst` (default 4), so one player flooding emotes runs out of their own and no longer silences everyone else. It is checked ahead of the shared one, and an emote the shared budget then drops costs its sender nothing. Every emote also has to fit one budget shared across all senders, since a sender id is only what the sender claims: `crowdy.kit.emotes.maxpersecond` (default 64) refills it, up to `crowdy.kit.emotes.burst` (default 16). An emote over either budget is dropped before it is read, with one Warning in the log. A client tracks at most 256 senders; when that is full, the sender that has been silent longest is forgotten. Because the sender id is what the sender claims, a peer that forges a player's id can use up that player's per-sender budget. That only costs that player their own emotes, which are cosmetic. See [Console variables](../reference/console-cvars.md). In Oakford, a villager waves at the one in front of them. **In Blueprint**: 1. On the wave key, add **Send Emote** with **Emote** `wave` and **Target** the actor the player looks at. 2. At **Event BeginPlay**, from **Get Kit Emotes**, bind **On Emote**; when **Emote** is `wave`, play the wave montage on **Sender Actor**. The graph's **Target** is left empty, so it waves at nobody in particular; wire the actor the player looks at into it. Where the game plays the montage, the graph prints who waved: [Blueprint snippet: social-emote, see /bp/unreal-sdk/social-emote.txt] **In C++**: ```cpp #include "Subsystem/CrowdyKitEmotes.h" void AVillager::Wave(AActor* LookedAt) { FString Reason; UE_CLOG(!UCrowdyKitEmotes::SendEmoteInWorld(this, TEXT("wave"), LookedAt, Reason), LogTemp, Log, TEXT("No wave: %s"), *Reason); } void AVillager::BeginPlay() { Super::BeginPlay(); if (UCrowdyKitEmotes* Emotes = UCrowdyKitEmotes::GetKitEmotes(this)) { Emotes->OnEmote.AddDynamic(this, &AVillager::HandleEmote); } } ``` `HandleEmote` is a `UFUNCTION` taking `AActor* SenderActor, const FString& SenderUuid, const FString& Emote, AActor* TargetActor, const FString& TargetUuid`. ## Watch a match without an avatar A player's game receives what happens near its own avatar: other players moving, a starter's [actors](../exec/use-game-kit-starters.md#show-a-starters-actors), emotes. A player who is only watching has no avatar, so add a **Crowdy Spectator** component to the actor that stands for them, a camera or a spectator pawn. While it is spectating it tells the realtime tier where that actor is: at once, every **Interval** (2 seconds, at least 0.25) and at once when the actor moves into another chunk, reaching **Distance** chunks around it (8, at most 8). With **Auto Start** on, it starts when play begins; **Start Spectating**, **Stop Spectating** and **Is Spectating** do it by hand. Other players' games draw nothing for a spectator. ![The Crowdy Spectator component's Spectate rows: Interval 2.0 s, Distance 8 and Auto Start ticked](/img/unreal-sdk/social-spectator-component.png) The spectator speaks for the player only while the player has no avatar of their own in the world: while their pawn is live, it waits. A player who leaves their pawn to watch keeps their pawn on other screens until it times out, 12 seconds later by default. Watching the match itself is reading its Server Object: **Get Server Object** on the match's session, such as a `CSO_Kit_Session` or a `CSO_Kit_Race` with its Instance Id, without **Join**, and the **On Changed** node of each variable to follow, such as **On Racers Changed (CSO_Kit_Race)**. In Oakford, villagers watch the cart race from the mill. **In Blueprint**, give the mill's camera actor a **Crowdy Spectator** component, and on the camera's **Event BeginPlay** add **Get Server Object** for `CSO_Kit_Race` with `mill-loop-1` and wire it as the **Target** of **On Racers Changed (CSO_Kit_Race)**, which updates the race board. **In C++**: ```cpp #include "Replication/Components/CrowdySpectatorComponent.h" AMillCamera::AMillCamera() { Spectator = CreateDefaultSubobject(TEXT("Spectator")); Spectator->Interval = 1.0f; } ``` ## The C++ names | In Blueprint | In C++ | |---|---| | The **Crowdy Kit Social** subsystem | `UCrowdyKitSocial`, a game instance subsystem. Its calls take an `FCrowdyKitGroupDone` (or, for the two that list members, an `FCrowdyKitMembersDone` with the group and its `TArray`) and an `FCrowdyKitFailed` with an `FCrowdyTeamError`: `CreateParty`, `FindParty`, `InviteToParty`, `JoinParty`, `LeaveParty`, `GetPartyMembers`, `CreateGuild`, `FindGuild`, `JoinGuild`, `AcceptIntoGuild`, `GetGuildRoster`, `PromoteInGuild`, `FindOrCreateChatRoom` and `ClaimGuildTerritory`, whose default keys are `DefaultTerritoryPermissionKeys()`. | | The party, guild, chat room and territory nodes | One async action each, `UCrowdyKitSocial_CreateParty`, `UCrowdyKitSocial_FindParty`, `UCrowdyKitSocial_InviteToParty`, `UCrowdyKitSocial_JoinParty`, `UCrowdyKitSocial_LeaveParty`, `UCrowdyKitSocial_GetPartyMembers`, `UCrowdyKitSocial_CreateGuild`, `UCrowdyKitSocial_FindGuild`, `UCrowdyKitSocial_JoinGuild`, `UCrowdyKitSocial_AcceptIntoGuild`, `UCrowdyKitSocial_GetGuildRoster`, `UCrowdyKitSocial_PromoteInGuild`, `UCrowdyKitSocial_FindOrCreateChatRoom` and `UCrowdyKitSocial_ClaimGuildTerritory`, on `UCrowdyKitSocialGroupAction` (pins `FCrowdyKitGroupAsyncResult`) or `UCrowdyKitSocialMembersAction` (pins `FCrowdyKitMembersAsyncResult`). | | **On Chat Message** | `UCrowdyKitSocial::OnChatMessage`, an `FOnCrowdyKitChatMessage` | | **Send Emote**, **On Emote** | `UCrowdyKitEmotes::SendEmote` on the world's subsystem, or the static `SendEmoteInWorld`; `OnEmote`, an `FCrowdyKitOnEmote` | | **Crowdy Spectator** | `UCrowdySpectatorComponent` | A game that changes channels outside these calls, such as with **Join Channel**, can have the signed-in player's channel list read again with `UCrowdyChannels::RefreshMyChannels`. ## Related - [Teams](./teams.md) and [Channels](../runtime/channels.md): the groups and the messaging these are made of - [Use the Game Kit starters](../exec/use-game-kit-starters.md): sessions, races and the other matches to watch - [Game Kit](/game-kit/intro) on the platform: its client conventions # Avatars Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/avatars # Avatars An avatar is a player-owned profile: an identity a player picks from and carries across sessions, holding small state other players can read and only the owner can write. It is not a live world actor and it is not server-checked game state; it is a stored profile the rest of the SDK reads to seed one, distinct from [authoritative gameplay truth](../concepts/two-planes.md). ## When you touch this When you build a profile or loadout screen, when a player changes a cosmetic or setting that should survive between sessions, or when you seed an entity's spawn state from a stored value the player already owns. ## The record `UCrowdyAvatars` (Game Instance subsystem, category **Crowdy SDK > Avatars**) owns every avatar call. An avatar record, `FCrowdyAvatar`, holds: | Field | Holds | |---|---| | `AvatarId` | The avatar's `int64` id. | | `UserId` | The owning player's `int64` id. | | `Name` | Display name. | | `PublicState` | Base64 blob, readable by every player. | | `PrivateState` | Base64 blob, readable by the owner only; stripped server-side on a non-owner read. | | `CreatedAt` | Creation timestamp. | Per-app data is a separate record, `FCrowdyAppAvatarState`, keyed by `AppId` and `AvatarId`: `RawState` (base64, empty string means not set), `CreatedAt`, `UpdatedAt`. This is the slot for cosmetics, settings, or any small struct your game defines; the avatar record's own fields do not hold arbitrary game data. :::warning[Only the owner may write an avatar, and the SDK does not check locally.] Every write, `UpdateAvatar`, `UpdateAvatarState`, `UpdatePublicAvatarState`, `UpdatePrivateAvatarState`, `UpdateAvatarAppState`, `DeleteAvatar`, is owner-only, enforced by the Game API against the caller's app-scoped token. `UCrowdyAvatars` performs no local ownership check: a write for an avatar the signed-in player does not own is sent over the wire and refused server-side, surfaced through `OnError` and usually classified `Forbidden`. ::: ## Reading avatars: cache versus network `GetCachedMyAvatars`, `HasCachedAvatars`, and `GetMyAvatarById` are synchronous, cache-only, and never populate themselves. Only `GetMyAvatars` (a network refresh) repopulates the cache and fires `OnMyAvatarsCacheChanged` (`FOnMyAvatarsCacheChanged`, one param, the refreshed array) so anything watching the cache updates without polling. A create, rename, delete or state write does not touch the cache, so call `GetMyAvatars` again after a write you want the cache to reflect. `GetAvatar` reads any avatar by id, including one you do not own; `GetUserAvatars` lists another user's avatars. Neither is limited to the signed-in player. ## Reading and writing per-app state `GetAvatarAppState` and `GetAvatarAppStates` (batch) read the calling app's slot for one or many avatars. `UpdateAvatarAppState` replaces it. :::warning[AppId always comes from project settings; there is no per-call override.] `GetAvatarAppState`, `GetAvatarAppStates`, and `UpdateAvatarAppState` resolve `AppId` from project settings, not a parameter. This subsystem cannot target a different app's slot. ::: `UpdatePublicAvatarState` and `UpdatePrivateAvatarState` each leave the other field untouched on the server; `UpdateAvatarState` replaces both public and private state atomically. :::caution[The combined write still round-trips an unchanged field.] Calling `UpdateAvatarState` with a field's old value does not clear it, it just resends it. Prefer the separate `UpdatePublicAvatarState` / `UpdatePrivateAvatarState` calls when only one field changes. ::: ## Serializing struct state State fields are base64 strings on the wire. `SerializeToAvatarState` and `DeserializeFromAvatarState` convert any `USTRUCT` to and from that string; `GetAvatarAppStateAs` and `SetAvatarAppStateAs` are latent wrappers that fetch-and-deserialize or serialize-and-write in one call. :::warning[The State pin is a wildcard: one struct, string or scalar, never an array.] `SerializeToAvatarState`, `DeserializeFromAvatarState`, `GetAvatarAppStateAs`, and `SetAvatarAppStateAs` are `CustomThunk` with a `CustomStructureParam`. The `int32&` in the C++ signature is a wildcard pin in Blueprint: connect a struct pin (or a single string, bool, integer or float) and the pin takes that type. An array, set or map pin is refused. ::: The lantern serializes its torch color as a small cosmetic struct, writes it to the app-state slot, and re-applies it on spawn. The example assumes the player already has an avatar (created with `CreateAvatar`, or **Create Avatar** in Blueprint); with none, `AvatarId` stays 0 and every call below returns early. `SetTorchColor` writes the picked `FLinearColor` directly with `FMemoryWriter`, byte-identical to what `SerializeToAvatarState` would produce for a linear color; C++ cannot call that helper itself, since its struct pin is a `CustomThunk` wildcard. `UpdateAvatarAppState` sends the encoded bytes, and `Torch` only recolours once the write's `OnSuccess` handler, `ApplyTorchColor`, confirms it. ```cpp // LanternPlayer.h UFUNCTION(BlueprintCallable, Category = "Lantern") void SetTorchColor(FLinearColor NewColor); // LanternPlayer.cpp #include "Misc/Base64.h" #include "Serialization/MemoryWriter.h" #include "Subsystem/CrowdyAvatars.h" void ALanternPlayer::SetTorchColor(FLinearColor NewColor) { UCrowdyAvatars* Avatars = GetGameInstance()->GetSubsystem(); if (!Avatars || AvatarId == 0) { return; } // App state is an opaque base64 blob; these 16 bytes match what Serialize Struct to Avatar State writes for a Linear Color. TArray Bytes; FMemoryWriter Writer(Bytes); Writer << NewColor; FOnAppStateSuccess Saved; Saved.BindDynamic(this, &ALanternPlayer::ApplyTorchColor); Avatars->UpdateAvatarAppState(AvatarId, FBase64::Encode(Bytes), Saved, FOnAvatarError()); } ``` `LoadTorchColor` calls `GetAvatarAppState`; the handler `ApplyTorchColor`, shared with the write above, base64-decodes, guards on 16 bytes, and reads the color with `FMemoryReader` straight into `Torch`. The latent `GetAvatarAppStateAs` is not used here for the same wildcard-thunk reason as above. ```cpp // LanternPlayer.h #include "Subsystem/CrowdyAvatars.h" void LoadTorchColor(); UFUNCTION() void ApplyTorchColor(FCrowdyAppAvatarState AppState); // LanternPlayer.cpp #include "Misc/Base64.h" #include "Serialization/MemoryReader.h" void ALanternPlayer::LoadTorchColor() { UCrowdyAvatars* Avatars = GetGameInstance()->GetSubsystem(); if (!Avatars || AvatarId == 0) { return; } FOnAppStateSuccess Loaded; Loaded.BindDynamic(this, &ALanternPlayer::ApplyTorchColor); Avatars->GetAvatarAppState(AvatarId, Loaded, FOnAvatarError()); } void ALanternPlayer::ApplyTorchColor(FCrowdyAppAvatarState AppState) { // An empty Raw State is an avatar that never saved a color; anything but a Linear Color's 16 bytes is ignored too. TArray Bytes; if (!FBase64::Decode(AppState.RawState, Bytes) || Bytes.Num() != static_cast(sizeof(FLinearColor))) { return; } FLinearColor Color; FMemoryReader Reader(Bytes); Reader << Color; Torch->SetLightColor(Color); } ``` `WatchAvatars`, bound at `BeginPlay`, picks up `AvatarId` from the first entry of a cache refresh and calls `LoadTorchColor`, so a cache change elsewhere still lands the stored color. The handler takes `const TArray&`. ```cpp // LanternPlayer.h #include "Subsystem/CrowdyAvatars.h" // The keeper this player plays as: their first avatar, learned when the cache answers. UPROPERTY(BlueprintReadOnly, Category = "Lantern") int64 AvatarId = 0; void WatchAvatars(); UFUNCTION() void HandleAvatarsChanged(const TArray& Avatars); // LanternPlayer.cpp void ALanternPlayer::WatchAvatars() { UCrowdyAvatars* Avatars = GetGameInstance()->GetSubsystem(); Avatars->OnMyAvatarsCacheChanged.AddDynamic(this, &ALanternPlayer::HandleAvatarsChanged); Avatars->GetMyAvatars(FOnAvatarsSuccess(), FOnAvatarError()); } void ALanternPlayer::HandleAvatarsChanged(const TArray& Avatars) { if (Avatars.Num() == 0) { return; } AvatarId = Avatars[0].AvatarId; LoadTorchColor(); } ``` Variables the Blueprint needs: `Torch` (a `UPointLightComponent` reference) and `AvatarId` (`int64`). Set `AvatarId` from a **Get My Avatars** result (the first entry's `AvatarId`) or from the cache-changed event described below; the two graphs read it and do nothing while it is 0. Saving the color: `OnTorchColorPicked` (custom event, `NewColor`) runs **Serialize Struct to Avatar State** on `NewColor`, reads `AvatarId`, and passes the result to the **Update Avatar App State** async node; its `OnSuccess` pin calls **Set Light Color** on `Torch` with `NewColor`. [Blueprint snippet: avatar-set, see /bp/unreal-sdk/avatar-set.txt] Loading it on spawn: `Event BeginPlay` gets the **Crowdy Avatars** subsystem, reads `AvatarId`, and calls the latent **Get Avatar App State As**, whose wildcard output feeds `NewLightColor`; a **Branch** on its `bSuccess` output calls **Set Light Color** on `Torch`. [Blueprint snippet: avatar-get, see /bp/unreal-sdk/avatar-get.txt] Following cache changes: no figure is shown for this step. Add a custom event with one input, `Avatars` (array of `Crowdy Avatar`), bind it with **Bind Event to On My Avatars Cache Changed** at Begin Play, set `AvatarId` from the first entry in its body, then run the loading path above; the C++ tab's `WatchAvatars` is the same step. ## Async action nodes Every network call above also exists as a Blueprint async action node with the same name and, as its underlying class, the subsystem name plus the call (**Get My Avatars** `UCrowdyAvatars_GetMyAvatars`, **Get Avatar** `UCrowdyAvatars_GetAvatar`, **Get User Avatars** `UCrowdyAvatars_GetUserAvatars`, **Get Avatar App State** `UCrowdyAvatars_GetAvatarAppState`, **Get Avatar App States** `UCrowdyAvatars_GetAvatarAppStates`, **Create Avatar** `UCrowdyAvatars_CreateAvatar`, **Update Avatar** `UCrowdyAvatars_UpdateAvatar`, **Delete Avatar** `UCrowdyAvatars_DeleteAvatar`, **Update Public Avatar State** `UCrowdyAvatars_UpdatePublicAvatarState`, **Update Private Avatar State** `UCrowdyAvatars_UpdatePrivateAvatarState`, **Update Avatar App State** `UCrowdyAvatars_UpdateAvatarAppState`), each with its own `On Success` and `On Error` exec pins instead of a bound delegate. **On Success** and **On Error** share one set of data pins: the result (`Avatar`, `Result` or `AppState`; **Delete Avatar** has none), then `Error` and `Message`. On **On Success**, `Error` and `Message` are empty. On **On Error** the result is empty, `Error` is an `FCrowdyAvatarError` (break it to read its `Code`), and `Message` says what went wrong, so a graph can show or branch on the failure. The pin types are the multicast `FAvatarAsyncResult` (Get Avatar, Create Avatar, Update Avatar, Update Public Avatar State, Update Private Avatar State), `FAvatarsAsyncResult` (Get My Avatars, Get User Avatars), `FAppStateAsyncResult` (Get Avatar App State, Update Avatar App State), `FAppStatesAsyncResult` (Get Avatar App States) and `FAvatarsAsyncOnError` (Delete Avatar); the subsystem's per-call `FOnAvatarSuccess`, `FOnAvatarsSuccess`, `FOnAppStateSuccess`, `FOnAppStatesSuccess`, `FOnAvatarVoidSuccess` and `FOnAvatarError` are a separate family the async nodes never expose. `TArray` results arrive wrapped, `FCrowdyAvatarList` for a list of avatars and `FCrowdyAppAvatarStateList` for a list of app states, so the result is one struct pin. `UpdateAvatarState` (the combined public-and-private write), `GetMyAvatarById`, `HasCachedAvatars`, `GetCachedMyAvatars`, `SerializeToAvatarState`, `DeserializeFromAvatarState`, `GetAvatarAppStateAs`, and `SetAvatarAppStateAs` have no async action node; a Blueprint reaches those only through the subsystem functions shown above. ## Creating and removing avatars `CreateAvatar(Name, OnSuccess, OnError)` makes a new avatar for the signed-in player; `UpdateAvatar(AvatarId, Name, ...)` renames one; `DeleteAvatar(AvatarId, OnVoidSuccess, OnError)` removes one. All three are owner-only, enforced the same way as the state writes above. ## Error codes `FCrowdyAvatarError` carries a `Code` (`ECrowdyAvatarErrorCode`: `Unknown`, `NotFound`, `Forbidden`, `NetworkError`, `ServerError`) and a `Message`. Bind `FOnAvatarError` to a `UFUNCTION` with the signature `(FCrowdyAvatarError Error, FString Message)`; `Message` repeats `Error.Message`. On an async node, **On Error** shows the same two values as its `Error` and `Message` pins. :::caution[The error code is a client-side guess, not a server code.] `ECrowdyAvatarErrorCode` is classified from the error message text by substring match, not a code the server sends. Branch on it for a coarse UI response; do not treat it as a stable server contract. ::: Cosmetic selections and progression flags belong here; currency balances and match results do not. Avatar state, public, private, or app, is profile data, not server-authoritative gameplay truth, which belongs in your [server code](../concepts/two-planes.md). ## Gotchas - `GetCachedMyAvatars`, `HasCachedAvatars`, and `GetMyAvatarById` never populate themselves; only `GetMyAvatars` does. A create, rename, delete or state write leaves the cache as it was. - Every write is owner-only and enforced server-side; a non-owner's write is sent, not blocked locally. - `AppId` for the per-app state calls always comes from project settings; there is no per-call override. - The serialization helpers take a wildcard pin: one struct, string or scalar. An array, set or map pin is refused. - `ECrowdyAvatarErrorCode` is a client-side text classification, not a server-issued code. ## Related - [Teams](./teams.md): a sibling service on the same subsystem pattern. - [The Two Planes](../concepts/two-planes.md): the contrast; avatar state is profile data, not server-checked truth. - [Entities and spawning](../runtime/entities-and-spawning.md): seeding an entity's spawn state from an avatar's public part. # Voice Chat Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/voice-chat # Voice Chat Voice chat is a small, opt-in add-on. It opens your microphone, sends what you say over the SDK's own UDP transport, and plays back what other players send. The two-plane model does not apply here. There is no server truth for voice, only a live stream the server relays between clients and never stores. ## When you touch this When your game wants live player-to-player audio next to its gameplay: push-to-talk, an always-on party channel, a "who is talking" indicator, or a quick check of a build's audio path with owner echo before anyone else connects. :::caution[Every player needs this SDK version for voice.] Voice uses the voice packet format shared with the CrowdyJS and CrowdyCPP SDKs: Opus audio in 20 ms frames, with reordering and loss concealment. Players on an earlier SDK version cannot hear players on this one, and the reverse. Update every client that should take part in voice. See [What's Changed](../guides/whats-changed.md#2026-10-30-sdk-v2220). ::: ## The nodes Everything lives on the **Crowdy SDK Subsystem** (`UCrowdySDKSubsystem`, a Game Instance subsystem, category **Crowdy SDK > Voice**). There is no wrapper to write: get the subsystem and call it. Your module needs only `CrowdySDK` in its Build.cs. | Blueprint | C++ | Does | |---|---|---| | **Start Voice Chat** | `StartVoiceChat()` | Opens your microphone and sends your voice while you speak. | | **Stop Voice Chat** | `StopVoiceChat()` | Closes your microphone. Other players see you stop talking. | | **Play Voice Chat** | `PlayVoiceChat()` | Lets you hear other players. | | **Mute Voice Chat** | `MuteVoiceChat()` | Stops you hearing other players. | | **Set Voice Chat Stream Timeout Threshold** | `SetVoiceChatStreamTimeoutThreshold(float InSeconds)` | How long a silent player's stream is kept before it is released. Default 2 seconds; the value is clamped to 0.5 seconds up to 1 hour. | | **Toggle Owner Echo** | `ToggleOwnerEcho(bool bEnable)` | Plays your own voice back to you, as other players hear it. | | **Set Voice Chat Distance** | `SetVoiceChatDistance(int32 Chunks)` | How far your voice reaches, in chunks around the chunk your pawn stands in: 0 to 8, default 8. | | **Get Voice Chat Distance** | `GetVoiceChatDistance()` | The distance last set. | | **Get Voice Mic State** | `GetVoiceMicState()` | What your microphone is doing right now. | | **On Voice Mic State Changed** | `OnVoiceMicStateChanged` | Fires when that state changes. | | **On Voice Speaker Talking** | `OnVoiceSpeakerTalking` | Fires when a player starts or stops talking. | Sending and hearing are independent. You can send without listening, listen without sending, or do both. Other players' voices are muted until you call **Play Voice Chat**, so a game that wants to hear anyone calls it once, for example when it starts. **Toggle Owner Echo** is a normal exec node: wire it into your graph like any other call. Earlier SDK versions never ran it from Blueprint. ## A push-to-talk toggle `StartTalking` and `StopTalking` bind to an Enhanced Input action's Started and Completed triggers in `SetupPlayerInputComponent`, using the same "fetch the game instance subsystem" pattern every page in this section uses. The microphone opens when the action starts and closes on release. ```cpp // LanternPlayer.h #include "InputAction.h" virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; // Hold to talk: capture runs between the action's Started and Completed triggers. UPROPERTY(EditDefaultsOnly, Category = "Lantern") TObjectPtr TalkAction; void StartTalking(); void StopTalking(); // LanternPlayer.cpp #include "EnhancedInputComponent.h" #include "Subsystem/CrowdySDKSubsystem.h" void ALanternPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); UEnhancedInputComponent* Input = Cast(PlayerInputComponent); if (Input && TalkAction) { Input->BindAction(TalkAction, ETriggerEvent::Started, this, &ALanternPlayer::StartTalking); Input->BindAction(TalkAction, ETriggerEvent::Completed, this, &ALanternPlayer::StopTalking); } } void ALanternPlayer::StartTalking() { GetGameInstance()->GetSubsystem()->StartVoiceChat(); Torch->SetIntensity(8000.f); } void ALanternPlayer::StopTalking() { GetGameInstance()->GetSubsystem()->StopVoiceChat(); Torch->SetIntensity(2000.f); } ``` Two custom events stand for the input action's Started and Completed pins. **OnTalkStarted** calls **Start Voice Chat**; **OnTalkCompleted** calls **Stop Voice Chat**. One **Crowdy SDK Subsystem** node feeds both calls. [Blueprint snippet: voice-toggle, see /bp/unreal-sdk/voice-toggle.txt] `StartTalking` and `StopTalking` also brighten and dim the `Torch` (`SetIntensity(8000.f)` / `SetIntensity(2000.f)`), so the example has a visible signal as well. ## Showing who is talking **On Voice Speaker Talking** gives you two values: **Speaker Uuid**, the talking player's actor id as text, and **Talking**, true when that player starts and false when they stop. "Talking" means their voice rose above a silence gate. It ends about 0.3 seconds after they stop. A microphone that is open but quiet sends nothing, so it does not count as talking. The event fires whether or not you called **Play Voice Chat**, so a talk indicator works even while you have others muted. `WatchVoice` binds `OnVoiceSpeakerTalking` with `AddDynamic` to a `UFUNCTION` that takes the speaker id and the talking flag, then calls `PlayVoiceChat()`. The handler turns the torch green while a player talks and white when they stop. ```cpp // LanternPlayer.h #include "Subsystem/CrowdySDKSubsystem.h" void WatchVoice(); UFUNCTION() void HandleSpeakerTalking(FString SpeakerUuid, bool bTalking); // LanternPlayer.cpp void ALanternPlayer::WatchVoice() { // Other players' voices stay muted until Play Voice Chat; On Voice Speaker Talking fires either way. UCrowdySDKSubsystem* Voice = GetGameInstance()->GetSubsystem(); Voice->OnVoiceSpeakerTalking.AddDynamic(this, &ALanternPlayer::HandleSpeakerTalking); Voice->PlayVoiceChat(); } void ALanternPlayer::HandleSpeakerTalking(FString SpeakerUuid, bool bTalking) { // Talking means that player's microphone is open; the torch turns green while anyone nearby has theirs open. Torch->SetLightColor(bTalking ? FLinearColor::Green : FLinearColor::White); } ``` Event BeginPlay binds a custom event, **OnSpeakerTalking** (with **SpeakerUuid** and **bTalking**), to **On Voice Speaker Talking** on the **Crowdy SDK Subsystem**, then calls **Play Voice Chat**. The custom event branches on **bTalking** and sets the **Torch** light color to green or white. [Blueprint snippet: voice-notify, see /bp/unreal-sdk/voice-notify.txt] The example keeps no list of speakers, so with several people talking the torch follows whichever event arrived last. For a real roster, keep a set keyed by **Speaker Uuid** and add or remove on each event. ## How far your voice reaches Your voice is addressed to the chunk your pawn stands in and reaches every player whose chunk is within the voice distance of it on each axis. The distance counts whole chunks: 0 is your own chunk only, and 8, the default, is the farthest the server relays. At the default chunk size of 1600 units, a distance of 1 is a 4800-unit square centred on your chunk. See [Recipients and routing](../runtime/recipients-and-routing.md) for how chunk rings work. Set it once, for example when the player arrives. It is kept across map changes like the other voice settings. `KeepVoiceNearby` sets the distance to one chunk when the player's pawn is assigned to this client. ```cpp // LanternPlayer.h void KeepVoiceNearby(); // LanternPlayer.cpp #include "Subsystem/CrowdySDKSubsystem.h" void ALanternPlayer::KeepVoiceNearby() { // Only players whose chunk is within one chunk of this pawn's hear it; kept across map changes. GetGameInstance()->GetSubsystem()->SetVoiceChatDistance(1); } ``` Event BeginPlay calls **Set Voice Chat Distance** with **Chunks** set to 1. [Blueprint snippet: voice-distance, see /bp/unreal-sdk/voice-distance.txt] Nearby text has a distance of its own on every line; see [Nearby Text Chat](./nearby-text-chat.md). ## Reading the microphone **Get Voice Mic State** returns one of five values, and **On Voice Mic State Changed** fires with the new one. | State | Means | |---|---| | **Off** | Voice chat is not using the microphone. | | **Starting** | The microphone is open and its first audio has not arrived yet. | | **Capturing** | The microphone is live. This does not mean you are speaking; silence is not sent. | | **No Device** | No microphone was found. Voice chat opens one as soon as it is available. | | **Error** | The microphone could not be used, or this platform cannot capture audio. Check that it is connected and that the OS lets this app use it, then call **Start Voice Chat** again. | A map change closes and reopens the microphone, so you see **Off** then **Starting** again. That is expected. A microphone that opens but delivers no audio for about three seconds goes to **Error**, with a log warning that points at the OS microphone permission. The usual cause is a denied permission. ## Confirming capture with owner echo Call **Start Voice Chat**, then **Toggle Owner Echo** with Enable set to true, and speak. You should hear yourself, a little late: your voice goes to the server and comes back like any other player's. While echo is on, your own **Speaker Uuid** also shows up in **On Voice Speaker Talking**. You need **Play Voice Chat** for this too, since echo is played like anyone else's voice. :::caution[Turn owner echo back off once capture is confirmed.] Leaving it on means the player hears their own microphone continuously, not just during the check. The setting carries across map changes, so it stays on until you turn it off. ::: ## What to expect - **Your pawn places your voice.** Each frame is addressed to the chunk your pawn stands in when it is sent, so voice follows the player as they move. Between pawns, during a respawn or as a map ends, it stays where the pawn last stood in that map. Before the map's first pawn, it is addressed to the chunk last given to **Set Player Current Chunk Coordinates** on the game session, which is the origin chunk unless your game sets it. Frames are dropped until the connection is up and your player has an id. - **Settings carry across maps.** Whether your microphone is on, play or mute, owner echo, the distance and the timeout all stay as you set them when the next map loads. You do not call anything again after a map change. - **A missing microphone is not a failure.** The state reads **No Device**, and voice chat opens the microphone as soon as one is available. On Windows a newly connected microphone is noticed right away; elsewhere voice chat looks again every couple of seconds. - **A changed default microphone restarts capture on Windows.** If the player unplugs a headset or picks another default device, voice chat reopens the microphone by itself. - **Up to 16 speakers are heard at once.** A seventeenth speaker is not played until one of the others is released. - **Received voice plays about 120 ms behind.** The delay absorbs uneven delivery, and it is on top of the network time. - **Lost packets are concealed.** Short gaps are filled in rather than heard as clicks. ## Platforms - **Windows:** full support, including the default-device watch above. - **Mac:** a packaged game needs the `NSMicrophoneUsageDescription` key in its Info.plist. The engine's own editor Info.plist has it; the engine's Mac template for game targets (`Build/Mac/Resources/Info.Template.plist` in the engine) does not include `NSMicrophoneUsageDescription`. To add it, open **Project Settings > Platforms > Xcode Projects**, and under **Plist Files** press **Restore Info.plist to default** once. That copies the engine template into your project's `Build/Mac/Resources/` folder and points **Mac: Info.plist Template** at it. Then add the `NSMicrophoneUsageDescription` key to that file with a short sentence for the player, such as "This game uses the microphone for voice chat." - **Linux:** the engine has no audio capture on Linux, so voice capture reports that it is not supported there: **Start Voice Chat** leaves the state at **Error** and logs a warning. The same happens in a build that runs with audio turned off. ## From C++ beyond the nodes The nodes above are the normal way in, and the Crowdy SDK subsystem keeps your settings across maps. C++ code that wants the per-map voice object can use these instead. `ECrowdyVoiceMicState` is the enum behind **Get Voice Mic State**: `Off`, `Starting`, `Capturing`, `NoDevice` (shown as **No Device**) and `Error`. Each means what the table under [Reading the microphone](#reading-the-microphone) says. On `UCrowdySDKSubsystem` the two Blueprint events have delegate types you can bind from C++ with `AddDynamic`: | Delegate type | Property | Parameters | |---|---|---| | `FOnVoiceMicStateChanged` | `OnVoiceMicStateChanged` | `ECrowdyVoiceMicState State` | | `FOnVoiceSpeakerTalking` | `OnVoiceSpeakerTalking` | `FString SpeakerUuid`, `bool bTalking` | `UVoiceChatSubsystem` is the voice chat of one map: a world subsystem, so `GetWorld()->GetSubsystem()` gives you the one for the current map, and it ends with the map. Its members: | Member | Does | |---|---| | `GetMicState()` | The current `ECrowdyVoiceMicState`. | | `IsCapturing()` | True while the state is `Capturing`. | | `IsPlaying()` | True while other players' voices are being played, after `PlayVoiceChat()` and before `MuteVoiceChat()`. | | `SetOwnerEcho(bool bEnable)` | Plays your own voice back to you, as **Toggle Owner Echo** does. | | `SetDistance(int32 Chunks)`, `GetDistance()` | How far your voice reaches in this map, as **Set Voice Chat Distance** sets it for every map. | | `FeedVoicePcm(TConstArrayView Samples)` | Sends 48 kHz mono samples as your voice, through the same silence gate, encoder and send path as the microphone. See [Sending audio that is not the microphone](#sending-audio-that-is-not-the-microphone). | | `EndVoiceFeed()` | Ends the talk burst `FeedVoicePcm` started, so listeners see you stop talking. | | `GetStats()` | Returns an `FCrowdyVoiceStats` of running counters for this map; see [Watching activity](#watching-activity). | | `OnMicStateChanged` | Native multicast delegate (`FOnCrowdyVoiceMicStateChanged`) with the new `ECrowdyVoiceMicState`. Fires on the game thread, and also when the map ends with the microphone open. | | `OnSpeakerTalking` | Native multicast delegate (`FOnCrowdyVoiceSpeakerTalking`) with the speaker's actor id as an `FString` and a `bool` for talking. Fires on the game thread. | The native delegates are bound with `AddUObject` or `AddLambda`, not `AddDynamic`, and a binding on `UVoiceChatSubsystem` is gone when the map is. The subsystem's start, stop, play, mute and distance calls are better made through the nodes, so your settings carry to the next map. ### Sending audio that is not the microphone `FeedVoicePcm` sends audio your code produces, such as a recorded line, a bot's voice or a test tone, exactly as the microphone's audio is sent. Give it 48 kHz mono 16-bit samples in real time: about 960 samples every 20 ms, for example one 20 ms frame per tick of a ticker. A larger push is cut into frames and sent at once, which a listener hears as a burst. Call `EndVoiceFeed` when the sound ends, so listeners see you stop talking. The feed and the microphone share one stream, so `FeedVoicePcm` answers false and sends nothing while the microphone is starting or capturing, and **Start Voice Chat** ends a fed burst before it opens the microphone. It also answers false in a map with no voice chat. Silence below the gate is skipped, as it is from the microphone. ## Watching activity The tool for troubleshooting is a console variable. `crowdy.voice.trace 1` turns on `LogCrowdyVoice` info logging for the microphone, playback and device changes. Warnings and errors are always on. Use it as a troubleshooting knob, not as a gameplay hook; for gameplay, bind **On Voice Speaker Talking** and **On Voice Mic State Changed**. From C++, `UVoiceChatSubsystem::GetStats()` (a world subsystem, so `GetWorld()->GetSubsystem()`) returns running counters for the current map: packets sent, silent frames the gate skipped, packets received, frames played, frames concealed for loss, frames dropped because a speaker's playback was full, malformed packets, and the number of speakers heard right now. A debug overlay or a soak test can read them; they are not a Blueprint node. ## How the transport works Your voice is cut into 20 ms frames, compressed with Opus, and sent as an ordinary audio message over the same UDP connection every other message uses, not a separate one. Frames go out only while you speak, and the last one of a burst tells listeners you stopped. On the receiving side the SDK puts each player's packets back in order, decodes them, conceals lost frames, and plays them through a 2D audio component per speaker, so voice is not positional and needs no sound asset. The `CrowdyVoice` module holds all of this and is a public dependency of `CrowdySDK`, so you do not list it yourself. Apart from the members listed under [From C++ beyond the nodes](#from-c-beyond-the-nodes), the voice classes inside it are internal, and the nodes above are the supported way in. ## Gotchas - Every player needs the same SDK version for voice. A mixed group splits into players who hear each other and players who hear nothing. - Others are muted until **Play Voice Chat**. A game that never calls it hears nobody, though **On Voice Speaker Talking** still fires. - **Capturing** means the microphone is live, not that the player is speaking. - **Toggle Owner Echo** true is a one-time check, not a setting to leave on, and it survives map changes. - **Set Voice Chat Stream Timeout Threshold** only affects how long a silent stream is kept; it does not mute or cut off active audio. A value outside 0.5 seconds to 1 hour is clamped and logged. - **Set Voice Chat Distance** counts chunks from the chunk your pawn stands in, not units from the pawn, and a value outside 0 to 8 is clamped and logged. - A player whose access tier lacks `use_voice_chat` has every voice packet refused. The refusal arrives late on the realtime subsystem's **On Send Refused**, with the message type 134; see [Connection and reconnect](../runtime/connection-and-reconnect.md). - A packaged Mac game needs `NSMicrophoneUsageDescription` in its Info.plist, and the engine's Mac template does not include it. - The old voice events `OnAudioNotify`, `OnAudioDataGenerated`, `OnAudioDataReceived` and `OnStreamTimeout` are gone. Use **On Voice Speaker Talking** for who is talking. ## Related - [Authentication](./authentication.md): the subsystem-fetch pattern this page reuses. - [Connection and reconnect](../runtime/connection-and-reconnect.md): the shared UDP transport voice rides on. - [Troubleshooting](../guides/troubleshooting.md): the trace CVar pattern used above. - [Blueprint Delegates](../reference/delegates.md): the two voice events in the delegate table. # Nearby Text Chat Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/nearby-text-chat # Nearby Text Chat Nearby text chat sends a line of text to every player near your pawn, and hands you every line players near you send. There is no room to join and no list of members: who hears a line is decided by where the players stand, like a voice in a crowd. Like voice, it is a live relay with no server truth; the server keeps no history. ## When you touch this When your game wants a local chat: players talk to whoever is standing around them, and a player who walks away stops hearing them. For chat with a fixed group wherever its members are, a party or a guild, use a channel instead ([Play Together](./play-together.md), [Channels](../runtime/channels.md)). ## The nodes Everything lives on the **Crowdy Proximity Chat** subsystem (`UCrowdyProximityChat`, a Game Instance subsystem, category **Crowdy SDK > Proximity Chat**). Your module needs `CrowdyServices` in its Build.cs. | Blueprint | C++ | Does | |---|---|---| | **Send Proximity Text** | `SendProximityText(const FString& Text, int32 DistanceChunks)` | Sends Text to every player within Distance Chunks of the chunk your pawn stands in. One exec pin per result. | | **On Proximity Text Received** | `OnProximityTextReceived` | Fires with each line another player sends you. | | **On Proximity Text Refused** | `OnProximityTextRefused` | Fires when the server refuses one of your lines. | | **Describe Proximity Chat Result** | `DescribeProximityChatResult(ECrowdyProximityChatResult Result)` | A sentence for a result, for a log or a status line. | **Send Proximity Text** checks the line before it sends anything, and leaves on the pin that says how it went: | Result | Means | |---|---| | **Sent** | The line left this client. It is not a delivery receipt: the platform sends none. | | **Empty Text** | There was nothing to send. | | **Too Long** | The line is over 1024 bytes of UTF-8. A character outside plain ASCII takes two to four bytes. | | **Not Connected** | The realtime connection is not up yet, or the player has no actor id yet. | | **No Position** | There is no local pawn to measure the line's reach from. | Nothing is sent unless the result is **Sent**. ## How far a line reaches A line is addressed to the chunk your pawn stands in, and reaches every player whose chunk is within **Distance Chunks** of it on each axis: 0 is your own chunk only, and 8 is the farthest the server relays. A value outside 0 to 8 is clamped. Each player is placed by their own last actor update. At the default chunk size of 1600 units, a distance of 4 reaches a square of nine chunks on a side, 14400 units across, centred on your chunk. [Recipients and routing](../runtime/recipients-and-routing.md) explains chunk rings. Every line names its own distance, so a game can have a short "say" and a longer "shout". ## Saying a line `SayNearby` sends a line to players within four chunks and brightens the torch only when it left. ```cpp // LanternPlayer.h #include "Subsystem/CrowdyProximityChat.h" UFUNCTION(BlueprintCallable, Category = "Lantern") void SayNearby(const FString& Line); // LanternPlayer.cpp void ALanternPlayer::SayNearby(const FString& Line) { // Players within four chunks of this pawn's chunk get the line. Sent means it left this client, not that anyone read it. UCrowdyProximityChat* Chat = GetGameInstance()->GetSubsystem(); if (Chat->SendProximityText(Line, 4) == ECrowdyProximityChatResult::Sent) { Torch->SetIntensity(8000.f); } } ``` A custom event, **SayNearby** with a **Line** string, calls **Send Proximity Text** with **Distance Chunks** set to 4. Only the **Sent** pin brightens the torch. [Blueprint snippet: nearby-text-send, see /bp/unreal-sdk/nearby-text-send.txt] ## Hearing lines **On Proximity Text Received** gives you one **Crowdy Proximity Text**: | Field | Is | |---|---| | **Sender Uuid** | The sender's actor id as text, the same value **On Voice Speaker Talking** gives for that player. | | **Sender Entity Id** | The sender's entity id. For a player on this SDK it is their pawn, so **Find Entity** returns the pawn while your client draws it. It is unset when the actor id is not one, for example from a CrowdyJS player. | | **Text** | The line exactly as it arrived. | Your own lines never come back to you; show a line as sent from your own **Sent** pin. `WatchNearbyText` binds the event with `AddDynamic`. The handler looks the sender's entity up and turns the torch yellow while this client draws the sender, white when it does not. ```cpp // LanternPlayer.h #include "Subsystem/CrowdyProximityChat.h" void WatchNearbyText(); UFUNCTION() void HandleNearbyText(const FCrowdyProximityText& Message); // LanternPlayer.cpp #include "Replication/Subsystems/CrowdyEntitySubsystem.h" void ALanternPlayer::WatchNearbyText() { UCrowdyProximityChat* Chat = GetGameInstance()->GetSubsystem(); Chat->OnProximityTextReceived.AddDynamic(this, &ALanternPlayer::HandleNearbyText); } void ALanternPlayer::HandleNearbyText(const FCrowdyProximityText& Message) { // The sender's entity is its pawn when this client draws it; the text is another player's, so treat it as untrusted. const UCrowdyEntitySubsystem* Entities = GetWorld()->GetSubsystem(); const AActor* Sender = Entities ? Entities->FindEntity(Message.SenderEntityId) : nullptr; Torch->SetLightColor(IsValid(Sender) ? FLinearColor::Yellow : FLinearColor::White); } ``` Event BeginPlay binds a custom event, **OnNearbyText** with a **Message**, to **On Proximity Text Received**. The event breaks the message, passes **Sender Entity Id** to **Find Entity**, and sets the torch yellow or white on whether the answer **Is Valid**. [Blueprint snippet: nearby-text-receive, see /bp/unreal-sdk/nearby-text-receive.txt] No name travels with a line. To show one, read it from the sender's pawn if your game puts a name there, or look the player up with [Player Names](./player-names.md) once you know who they are. ## Refusals A line leaves before the server judges it, so a refusal arrives later, on **On Proximity Text Refused**, with the same **Crowdy Send Refusal** that the realtime subsystem's **On Send Refused** carries ([Connection and reconnect](../runtime/connection-and-reconnect.md)). The usual cause is a player whose access tier lacks `use_text_chat`; every tier that grants access has it unless the app was set up otherwise. The server names a refused send only by a one-byte sequence, so on a busy connection a refusal can name the wrong send, and the same refusal is reported at most once a second. Show it; do not act on it. ## What to expect - **Lines are not reliable.** A line is one UDP packet with no retry, like a voice frame. Over a healthy connection that is rarely a problem; do not build anything that must arrive on it. - **It follows the pawn.** Each line is addressed to the chunk the pawn stands in when it is sent, and a listener who has just moved is placed by their next actor update. - **It reaches other SDKs.** A line is plain UTF-8 and nothing else, the same as CrowdyJS's and CrowdyCPP's nearby text, so players on those SDKs in the same app hear it and you hear theirs. - **The sender is a claim.** The actor id is the one the sender chose, and the server does not check it against the sender. Treat **Sender Uuid**, **Sender Entity Id** and **Text** as untrusted input: cut long lines and line breaks before you put them on screen, and keep anything authoritative out of chat. ## From C++ The nodes above are `UFUNCTION`s and `UPROPERTY`s on `UCrowdyProximityChat`, so C++ calls them by the names in the table. The types behind them: | Type | Is | |---|---| | `FCrowdyProximityText` | The line **On Proximity Text Received** carries: `SenderUuid` (`FString`), `SenderEntityId` (`FGuid`) and `Text` (`FString`). | | `ECrowdyProximityChatResult` | The result of `SendProximityText`: `Sent`, `EmptyText`, `TooLong`, `NotConnected`, `NoPosition`. | | `FOnCrowdyProximityTextReceived` | The delegate type of `OnProximityTextReceived`, with one `const FCrowdyProximityText&`. Bind it with `AddDynamic`. | | `FOnCrowdyProximityTextRefused` | The delegate type of `OnProximityTextRefused`, with one `const FCrowdySendRefusal&`. | `UCrowdyProximityChat::MaxTextBytes` is the 1024-byte limit, and the static `EntityIdFromSenderUuid(const FString&)` turns an actor id into the entity id it names, the same way the received line's `SenderEntityId` is made. The chunk a line is addressed to comes from the game session: `UCrowdyGameSession::GetLocalPlayerChunk(const UWorld*)` is the chunk the world's first local player's pawn stands in, or the chunk last set with **Set Player Current Chunk Coordinates** when there is no pawn, and `TryGetLocalPlayerChunk(const UWorld*, FInt64Vector&)` answers false when there is no pawn instead. Voice chat is addressed the same way. ## Gotchas - **Sent** is not delivery. A line nobody was near enough to hear is still **Sent**. - **No Position** means no local pawn. A spectator or a menu with no pawn cannot send nearby text. - The 1024-byte limit counts UTF-8 bytes, not characters. - Text received is whatever another client sent: any length that fits one packet, with line breaks. ## Related - [Voice Chat](./voice-chat.md): the voice twin, with a distance of its own. - [Play Together](./play-together.md): chat rooms, parties and guilds over channels. - [Recipients and routing](../runtime/recipients-and-routing.md): chunk rings and distances. - [Connection and reconnect](../runtime/connection-and-reconnect.md): **On Send Refused**. # Host Election Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/host-election # Host Election `UCrowdyHostSubsystem` tracks who the SDK elected host and can also ask the server directly, for the one caller who cannot afford to be wrong. The local convenience answer, and why election is a convention and not enforcement, lives on [Host Authority](../runtime/host-authority.md); this page is the subsystem and the server-validated check it is built around. ## When you touch this Reading `IsHost` or the elected id for ordinary view-plane logic is the common case, and [Host Authority](../runtime/host-authority.md) covers it. Reach for the check on this page only when a wrong answer costs something: granting an ownership transfer, starting a host-only sequence you cannot cheaply undo, anything where a stale or spoofed local read is a real risk. ## The host subsystem `UCrowdyHostSubsystem` is a world subsystem, re-created per world, that re-reads the elected host from the login session each time. | Member | One line | |---|---| | `GetHostID()` | The host's entity id as an `FGuid`. No display name override, so Blueprint lists the node as **Get Host ID**: an upper-case run stays together. | | `IsHostSet()` | Whether an election result has arrived yet. | | `GetHostUserID()` (**Get Host User ID**) | The host's raw user id, 0 until known. Backed by a `std::atomic` read with relaxed ordering, so it is safe to call from any thread as well as from Blueprint. | | `IsHost()` (**Is Host**) | Compares this client's own user id to the tracked host id. The same local, unenforced answer as `GetCrowdyHasAuthority` on [Host Authority](../runtime/host-authority.md); also safe from any thread. | | `OnHostElected` (`FOnCrowdyHostElected`, `HostID`, `PreviousHostID`) | Fires on the game thread when the elected host changes, the first result included (0 to the first host); a poll that repeats the same host does not fire it. | | `CheckEntityIsHost(Entity, Callback)` | The server-validated check, C++ entry point; see below. | ### After sign-out When the player signs out, the host is cleared before `OnLogout` fires, on whichever map the player is: `IsHostSet()` is false, `GetHostID()` is an invalid `FGuid`, and `IsHost()` is false. When a host had been elected on the current map, `OnHostElected` also fires once, on the next frame, with an invalid `HostID` and the previous host as `PreviousHostID`. The host subsystem belongs to the world, so it starts again after every level travel: the first election on each map fires `OnHostElected` with an invalid `PreviousHostID`, which does not mean a new session started. While nobody is signed in, a host poll answer is ignored, so a poll still in flight at sign-out cannot elect a host afterwards. If you bind `OnHostElected`, treat an invalid `HostID` or `PreviousHostID` as "no host". This is the behaviour from SDK 2.24; on the tagged v2.23.0 plugin, sign-out left `IsHostSet()` true and `GetHostID()` returned an id that belongs to no player (or the previous host, after a level travel), and the first election's `PreviousHostID` was that made-up id (see [What's Changed](../guides/whats-changed.md#2026-11-02-sdk-v2240)). :::note[Under normal operation the server agrees with the local view.] `IsHost()` and the server-validated `CheckEntityIsHost` answer the same question at different trust levels, and day to day they say the same thing. The server check exists for the moment you cannot assume the caller is running an unmodified client, not because the local view is usually wrong. ::: ## The server-validated check `CheckEntityIsHost` is server-validated in a specific sense: for the local player's own entity it asks the server's `amIGameHost` query directly; for any other actor it resolves that actor's server-side owner user id and compares it to the elected host user id. [Host discovery](/game-api/host-discovery#amigamehost) on the Game API covers the query itself; this page states only what the SDK does with the answer. That answer is still not enforcement by itself. `CheckEntityIsHost` tells the caller the current truth about who is host so the caller can gate something; the check is not the gate. A change worth cheating for is decided by your server code, on the [truth plane](../concepts/two-planes.md). The lantern world uses it exactly there: a claim on a lantern post is only granted after a definite server yes. `GetSubsystem()` gets the world subsystem; its callback runs on the game thread with `bSuccess` and `bIsHost`. Capturing `this` as a `TWeakObjectPtr` matters because the callback runs after a round trip to the server. ```cpp // LanternPost.h void VerifyThenGrant(const FGuid& RequesterID); // LanternPost.cpp #include "Replication/Ownership/CrowdyOwnershipTransfer.h" #include "Subsystem/CrowdyHostSubsystem.h" void ALanternPost::VerifyThenGrant(const FGuid& RequesterID) { UCrowdyHostSubsystem* Host = GetWorld()->GetSubsystem(); if (!Host) { return; } // Failed is not the same as "not host"; grant only on a definite yes from the server, and only if the post still exists. TWeakObjectPtr WeakThis(this); Host->CheckEntityIsHost(this, [WeakThis, RequesterID](bool bSuccess, bool bIsHost) { ALanternPost* Post = WeakThis.Get(); if (!Post || !bSuccess || !bIsHost) { return; } UCrowdyOwnershipTransfer::GrantOwnershipTransferToPlayer(Post, Post, RequesterID); }); } ``` This graph needs no extra variables; everything comes from the event's own parameters and Self. From the `OnClaimRequested` custom event (params `TargetEntity` Actor, `RequesterActor` Actor, `RequesterID` Guid), the latent **Is Crowdy Entity Host (Server)** node (`Entity` defaults to Self) runs the check; its **Is Host** pin continues to **Grant Ownership Transfer To Player**, with `Target Entity` fed by Self and `New Owner Player ID` fed by the event's `Requester ID`. [Blueprint snippet: host-check-server, see /bp/unreal-sdk/host-check-server.txt] The **Is Not Host** and **Failed** pins are left unwired in this graph; both are explained below. `UCrowdyIsEntityHostServer::IsCrowdyEntityHostServer` (display **Is Crowdy Entity Host (Server)**, category **Crowdy SDK > Session**) is the Blueprint front end for `CheckEntityIsHost`; from C++, call `CheckEntityIsHost` directly rather than this node. It exposes three pins of type `FCrowdyHostCheckPin`: **Is Host** (`OnIsHost`), **Is Not Host** (`OnIsNotHost`), and **Failed** (`OnFailed`). Exactly one of `OnIsHost` or `OnIsNotHost` fires on a definite answer; `OnFailed` fires when the answer could not be determined at all. :::warning[Failed and Is Not Host are different answers. Treat Failed as unknown, never as a confirmed no.] `bSuccess == false`, or the **Failed** pin, means the check could not be resolved: no host elected yet, a network or GraphQL error, or an actor the server has no row for because it never sent an update. `bIsHost` carries no meaning in that case. Retry or wait; do not take the branch you would take for "not the host". ::: :::caution[Every completion still runs, even after the awaiting Blueprint node is gone.] The subsystem is world-scoped, but a Blueprint graph waiting on `CheckEntityIsHost` is not tied to that lifetime the same way. Each call owns its own completion, so concurrent checks never answer each other, and a completion still fires after the world that started it has torn down; a dropped node's pins simply never receive it, rather than the callback hanging or crashing. Do not rely on a check outliving a level travel to reach your handler. ::: `UCrowdyHostSubsystem::Initialize` null-checks `GetGameInstance()`, and any world subsystem you write alongside it must too; [Replicated subsystems](../runtime/replicated-subsystems.md#inherit-a-base) has the reason. ## Gotchas - `IsHost()` and `GetHostUserID()` are the local, unenforced view. Reach for `CheckEntityIsHost` only when the caller cannot afford a spoofed or stale answer. - `GetHostID()` has no display name override; find it in Blueprint as **Get Host ID**. - `CheckEntityIsHost` needs the target actor to have sent at least one update. A freshly spawned, never-updated actor answers Failed, the same as an unresolved entity. - `IsHost` is documented safe from any thread; `GetHostUserID` is a single atomic read, so it is too. Nothing else on this subsystem carries that guarantee. - The check itself gates nothing. What you do with `bIsHost` still needs your server code to decide it if the outcome is worth cheating for. ## Related - [Host Authority](../runtime/host-authority.md): the local, unenforced view of who is host, and the same examples from the view-plane side. - [Ownership transfer](../runtime/ownership-transfer.md): the grant flow the server check feeds in the lantern world. - [The Two Planes](../concepts/two-planes.md): where a rule is actually enforced. - [Host discovery](/game-api/host-discovery#amigamehost) on the Game API: the `amIGameHost` and actor-owner queries underneath this subsystem. # UI Interfaces Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/services/ui-interfaces # UI Interfaces A small set of Blueprint interfaces a game can implement to let the SDK drive its HUD, player, and minigame classes, plus two things that are not UI at all but live in the same module: the Blueprint reception layer, which forwards incoming events and actor updates to delegates without a line of C++, and `UCrowdyUtilities`, an entity and session helper library. All of it lives in `CrowdyServices`. ## When you touch this When you want the SDK to open and close your widgets instead of doing it by hand, when a Blueprint-only project needs to receive game events or actor updates without binding a delegate in C++, or when gameplay code needs an entity's id, owner, or host state and you would rather call a library function than reach into the entity component yourself. Nothing else in the SDK requires any of this. CrowdyState and RPC work with none of `ICrowdyHUD`, `ICrowdyUIRootWidget`, `ICrowdyPlayer`, `ICrowdyMinigame`, or `ACrowdyHUDBase` implemented. ## HUD hooks `ICrowdyUIRootWidget` is the contract for a widget that owns the four UI layers and stacks widgets on them. `ICrowdyHUD` is the contract for a HUD actor that routes commands to that root widget. Each `I` interface is the Blueprint-facing half of a `UINTERFACE` pair; the generated class you never call directly is `UCrowdyUIRootWidget` for the first and `UCrowdyHUD` for the second. `ACrowdyHUDBase` (`Blueprintable`, `Abstract`, derives `AHUD`) implements `ICrowdyHUD` for you and adds two editable properties, `HUDWidgetClass` and `WidgetSetConfig`. `UW_CrowdyHUD` is the SDK's own `UUserWidget` implementation of `ICrowdyUIRootWidget`. `HUDWidgetClass` defaults to none: make a Widget Blueprint parented to `UW_CrowdyHUD` with four Overlay widgets named `GameLayer`, `GameMenuLayer`, `MenuLayer` and `ModalLayer`, and assign it as **HUD Widget Class** on your HUD Blueprint's class defaults; until then BeginPlay logs a warning and starts no widget system. | Interface | Function | Does | Parameters that matter | |---|---|---|---| | `ICrowdyUIRootWidget` | `PushToLayer` | Creates and pushes a widget onto one of the four layers | `Layer` (`ECrowdyUILayer`), `bClearLayerFirst`, `WidgetClass` | | `ICrowdyUIRootWidget` | `PopFromLayer` | Pops the top widget off a layer | `Layer`, `bRemoveFromParent` | | `ICrowdyUIRootWidget` | `GetLayer` | Returns the `UOverlay` backing a layer | `Layer` | | `ICrowdyHUD` | `ExecuteUICommand` (**Execute Crowdy UI Command**) | Routes an `FGameplayTag` command to the HUD | `Command`, `bHideOtherWidgetsInLayer`, `bChangeInputMode` | | `ICrowdyHUD` | `GetWidgetReference` | Returns an already-created widget of the given class, or null | `WidgetClass` | | `ICrowdyHUD` | `ToggleLayerVisibility` | Shows or hides a whole layer | `Layer`, `bVisible` | | `ACrowdyHUDBase` | `InitializeCrowdyWidgets` | `BlueprintNativeEvent` called once from BeginPlay; the default creates every widget in `WidgetSetConfig`. Override to add to that and call the parent | none | `UCrowdySDKSubsystem::OnCrowdyHUDReady` (**Crowdy HUD Ready**, `FOnCrowdyHUDReady`, no parameters) fires once `ACrowdyHUDBase` has created every widget in `WidgetSetConfig`; bind it on the Crowdy SDK Subsystem when something must wait for the HUD to exist. `ECrowdyUILayer` (`GameLayer`, `GameMenuLayer`, `MenuLayer`, `ModalLayer`) names the four layers. `ECrowdyInputMode` (`Game`, `GameAndUI`, `UI`) is the input mode a widget switches to. A widget set is a `UCrowdyWidgetSet` data asset, one `FWidgetSpec` per tag in its `WidgetSpecMap`, each spec carrying a `WidgetClass`, `Layer`, `bClearLayerFirst`, `bStartVisible`, and the entering and exiting input modes. `ACrowdyHUDBase::WidgetSetConfig` points at one; its private `ToggleWidget` and `GetWidget` do the actual show and lookup work behind `ExecuteUICommand` and `GetWidgetReference`. Set both `HUDWidgetClass` and `WidgetSetConfig` on the HUD Blueprint's class defaults; they are saved with it. This section has no example: the tables above are the reference. ## Player and minigame hooks `ICrowdyPlayer::GetPlayerName` and `ICrowdyPlayer::InteractMessage` (an interact prompt with a `Message` and a `Duration`) are the player-facing hooks, generated alongside `UCrowdyPlayer`. `ICrowdyMinigame::GetMinigameName` and `ICrowdyMinigame::JoinMinigame` are their minigame equivalents, generated alongside `UCrowdyMinigame`. :::warning[GetPlayerName and GetMinigameName are placeholders today.] Both are hooks the SDK never calls and never implements; whatever you return is yours, and nothing in the SDK reads it. Do not build a feature that expects the SDK to fill either one. ::: ## The Blueprint reception layer `UCrowdyBlueprintReceptionLayer` lets a Blueprint receive game events and actor updates without binding a delegate in C++. `CreateAndRegisterLayer(WorldContextObject, Class)` spawns one and registers it. Once registered, it broadcasts `OnEventNotificationReceived` (an `FGameEventNotificationBP`) for a matching game event and `OnActorUpdateReceived` (an `FActorUpdateNotificationBP`) for a matching actor update. Three `EditDefaultsOnly` arrays say what a layer subscribes to: `SupportedResponseTypes` (`ECrowdyMessageType`), `SupportedActorUpdateTypes` (`FName`), and `SupportedEvents` (the struct types to receive). :::warning[An empty SupportedEvents array is a catch-all when the event opcode is listed.] With `SupportedEvents` empty and `Client Event Notification` or `Server Event Notification` in `SupportedResponseTypes`, the layer receives every event no other layer claimed. With all three arrays empty it receives nothing. The same rule pairs `SupportedActorUpdateTypes` with `Actor Update Notification`: empty plus the opcode is every actor update, empty alone is none. ::: `DispatchToBlueprint` and the `RegisterLayer` internals are C++-only and not exposed to Blueprint; `RegisterLayer` is safe to call more than once and builds one subscription per declared type, or the unclaimed fallback when nothing is declared and the matching opcode is listed. ## The utilities library `UCrowdyUtilities` is a static Blueprint function library for entities, sessions, and events. It is not UI-specific, but it lives in the same module. | Function | Does | |---|---| | `SpawnCrowdyEntity`, `DestroyCrowdyEntity` | Spawns or destroys an entity actor | | `GetCrowdyEntityID`, `GetCrowdyEntity` | Converts between an entity actor and its id | | `GetLocalPlayerEntityID` | The local player's entity id | | `GetCrowdyEntityOwnerID`, `GetAllCrowdyEntitiesByOwner` | An entity's owner, or every entity a given owner has | | `CrowdyIsEntityRegistered` | Whether an id is a known entity | | `GetCrowdyEntityRole` | An entity's `ECrowdyRole` | | `DoesCrowdyEntityOwn` | Whether one actor owns another, by Crowdy identity | | `GetCrowdyEntityComponent` | An actor's entity component, falling back to `FindComponentByClass` when the actor does not implement `ICrowdyEntityComponentProvider` | | `IsCrowdyEntityLocallyControlled`, `IsCrowdyEntityPlayerControlled` (and its exec-flow twin `SwitchIsCrowdyEntityPlayerControlled`) | Local or player control checks | | `CrowdyIsRemoteProxy` (and its exec-flow twin `SwitchCrowdyIsRemoteProxy`) | Whether this is a remote proxy of the entity | | `K2_SendCrowdyEvent` (display name **Send Crowdy Event**; C++-only template `SendCrowdyEvent` for the same call without the Blueprint thunk) | Sends a game event struct with `Recipient`, `DecayRate`, and `ReplicationDistance` | | `CrowdyHasAuthority` (exec-flow, **Switch Crowdy Has Authority**) / `GetCrowdyHasAuthority` (pure, **Crowdy Has Authority**) | True when this client is the host | | `CrowdyIsConnectedToServer` | Whether the realtime connection is up | | `CrowdyGetHostID` | The current host's id | | `IsCrowdyEntityHost` | Whether this actor is the host player's own entity (its id equals the host id). For "owned by the host" compare `GetCrowdyEntityOwnerID` with `CrowdyGetHostID` | :::warning[DoesCrowdyEntityOwn compares Crowdy ownership, not AActor::GetOwner.] The name invites Unreal's actor-owner meaning, but this checks Crowdy entity ownership instead; it will not agree with `AActor::GetOwner()` for an entity whose Unreal owner was never set to match. ::: `IsCrowdyEntityHost` and `IsCrowdyEntityLocallyControlled` both fold in host election state. See [Host election](./host-election.md) and [Host authority](../runtime/host-authority.md) for what "host" means before relying on either. ## Gotchas - Every hook on this page is optional. Nothing else in the SDK requires an implementation of any of them. - `GetPlayerName` and `GetMinigameName` are placeholders the SDK never calls; nothing reads what you return. - `HUDWidgetClass` defaults to none. Without a `UW_CrowdyHUD` Widget Blueprint assigned, the HUD base starts no widget system and `OnCrowdyHUDReady` never fires. - The reception layer's `SupportedEvents` is a claim list, not a filter: empty plus the event opcode in `SupportedResponseTypes` widens the layer to every unclaimed event; empty alone receives nothing. - The reception layer forwards transport-level notifications to Blueprint. - Everything on this page logs under `LogCrowdyServices`, gated by two trace CVars: `crowdy.services.trace` (`CrowdyServicesTrace::Services`) for the utilities library and reception layer, and `crowdy.hud.trace` (`CrowdyServicesTrace::Hud`) for HUD-side calls. ## Related - [Host election](./host-election.md) and [Host authority](../runtime/host-authority.md): what `IsCrowdyEntityHost` and `IsCrowdyEntityLocallyControlled` fold in. - [Channels](../runtime/channels.md): the message plane the reception layer's subscriptions ride.