# Crowdy Unreal SDK Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/intro # Crowdy Unreal SDK The Crowdy Unreal SDK is an Unreal Engine plugin that connects your game to the Crowded Kingdoms platform. It gives you a small surface you can drive from C++ or Blueprint: real-time networking for large numbers of players, voice chat, teams, channels, and avatars. This guide describes **SDK 2.24**. Open `Plugins/CrowdySDK/CrowdySDK.uplugin` and check `VersionName` before you rely on a feature described here. :::note[This guide is written for Unreal Engine 5.8 and a C++ project.] Gameplay can be driven entirely from Blueprint, but the plugin itself compiles as C++, so your project needs a C++ target. See [Installation](./installation.md). ::: ## The two planes Everything the SDK replicates lives on one of two planes, and the whole guide leans on the split. 1. **The view plane (CrowdyState).** Fast, client-owned state such as movement, animation, and transient effects. The client that owns an entity writes its state and everyone else sees it. Nothing here is checked by a server. 2. **The elected host is a convention.** One client is elected host to coordinate the view plane. It is a helper for shared logic, not a rule enforcer. 3. **The truth plane (your server code).** Server-owned gameplay state such as hit points, stats, and inventory, held by your app's code on [ck-exec](/exec/intro). Clients ask the server for it and ask the server to change it; the server decides. This is the only place a rule is enforced. The SDK reaches it from C++ through [Server Logic](./exec/overview.md). 4. **The planes touch in one way.** A client asks the server's code for a change and is told the result. No gameplay value ever flows from the view plane into the truth plane as trusted input. Read [The Two Planes](./concepts/two-planes.md) before you decide where a new piece of state belongs. :::caution[The host is a convention the SDK helps you follow, not a server that enforces rules.] ::: ## Entities Almost everything on the view plane is an **entity**: an actor with a `UCrowdyEntityComponent` that has a shared identity (`NetID`) on every client, one owner, and a remote proxy everywhere else. Events (RPC calls) are aimed at entities, and replicated properties belong to them. [Entities, Identity, and Ownership](./concepts/entities-identity-ownership.md) covers the details. ## How to read this guide Work through it in order the first time. 1. **Start**: [Installation](./installation.md) and the [Quickstart](./quickstart.md). 2. **Concepts**: [The Two Planes](./concepts/two-planes.md), [Entities, Identity, and Ownership](./concepts/entities-identity-ownership.md), [Sessions and Presence](./concepts/sessions-and-presence.md), [The Host Is a Convention](./concepts/host-is-a-convention.md). 3. **Crowdy Studio**: [sign in and connect your app](./studio/overview.md) from inside the editor. 4. **Runtime (view plane)**: [map profiles](./runtime/map-profile.md), entities, RPC events, Crowdy State. 5. **Server Logic (truth plane)**: [server-owned state on ck-exec and the code that changes it](./exec/overview.md), from Blueprint or C++. 6. **Services**: [voice chat](./services/voice-chat.md), [teams](./services/teams.md), [avatars](./services/avatars.md). 7. **Guides** and **Reference**: [best practices](./guides/best-practices.md), the [sample project](./guides/sample-project.md), [packaging](./guides/packaging.md), and lookup tables such as [console variables](./reference/console-cvars.md). :::tip[Do Crowdy Studio first] Nothing on the network works until your project knows which app it belongs to. Sign in and run [Config Sync](./studio/config-sync.md) before anything else. ::: ## What is next Add the plugin with [Installation](./installation.md), connect your app in [Crowdy Studio](./studio/overview.md), then follow the [Quickstart](./quickstart.md) to put your player, one entity, one event, and one replicated property on screen. # Installation Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/installation # Installation This page adds the CrowdySDK plugin to your project, enables it, adds the module dependencies your code needs, and points the project at your app. ## Prerequisites - Unreal Engine 5.8. - A C++ project (Visual Studio 2022 with the Desktop C++ workload). Gameplay can be driven entirely from Blueprint, but the plugin compiles as C++. :::caution[The plugin compiles C++ modules, so a Blueprint only project cannot use it.] Add a C++ class to a Blueprint project once (File, New C++ Class) and it becomes a C++ project. ::: ## Step 1: Get the release Download the `v2.24.0` release of the plugin from the public repository, `CrowdedKingdoms/CrowdySDK-Unreal` on GitHub. :::note[The release you install must match the version this guide describes (2.24).] ::: From 2.24.0 the plugin root also carries a `MIGRATION.md` that lists, per release, what changed and the exact steps to migrate; read it when you update an existing project. The packaged zip of 2.24.0 is the first to include the plugin's `Config/` folder (see [What's Changed](./guides/whats-changed.md#2026-11-02-sdk-v2240)). ## Step 2: Place the plugin at `Plugins/CrowdySDK` Unpack the release into your project's `Plugins` folder so the plugin descriptor sits at: ``` YourProject/Plugins/CrowdySDK/CrowdySDK.uplugin ``` Create the `Plugins` folder if it does not exist. The first time you open the project afterwards, the editor offers to build the plugin's modules; accept. :::warning[Keep it under Plugins. Do not reach it through an external plugin directory.] A plugin mounted from anywhere else (for example through `AdditionalPluginDirectories` in the `.uproject`) is an External plugin, and Unreal silently drops the config it ships in `Plugins/CrowdySDK/Config/*.ini`. That config is load-bearing: - `CoreRedirects` that repoint content saved against the SDK's older single-module layout to the current modules, so older assets still load. - `AssetRegistry` cook-tag exclusions that keep editor-only tags, left on assets saved by an older SDK, out of a packaged build's asset registry. - A packaging entry that cooks the plugin's shipped map profile (the per-map data asset that switches the SDK on; see [Map profiles](./runtime/map-profile.md)), without which a packaged build is inactive on any map that configured nothing. Once the project opens in the editor (after Step 5), check the Output Log for `Mounting Project plugin CrowdySDK`. If the line says External, move the plugin. ::: ## Step 3: Enable the plugin From the editor: open the project, go to **Edit, Plugins**, search for `CrowdySDK`, tick **Enabled**, and restart when asked. ![The Plugins window with CrowdySDK found and enabled](/img/unreal-sdk/plugins-window.png) Or add it to your `.uproject` by hand: ```json { "Name": "CrowdySDK", "Enabled": true } ``` Enabling `CrowdySDK` brings in all twelve modules in this table, including the two Mass modules, and the engine plugins they need (MassGameplay and MassCrowd). You never enable them one by one, and there is no separate Mass plugin to add. The engine marks MassGameplay and MassCrowd experimental, and they bring in the engine plugins they depend on (among them MassAI, ZoneGraph, SmartObjects, StateTree and PoseSearch), so every project with the SDK carries them and the engine's Mass processing runs each frame, even on maps that draw no crowd rows; the SDK's own Mass work runs only on maps whose profile selects the Mass backend. That holds since v2.19.0; on v2.18.0 and earlier CrowdyMass was a separate plugin, which a project upgrading removes from its `.uproject`. See [What's Changed](./guides/whats-changed.md#2026-10-12-sdk-v2190). | Module | Type | What it holds | |---|---|---| | `CrowdySDK` | Runtime | The top-level subsystem: login, connection, voice toggle. | | `CrowdyReplication` | Runtime | Entities, RPC events, Crowdy State, map profiles, project settings. | | `CrowdyServices` | Runtime | Authentication, teams, channels, avatars, host election. | | `CrowdyNet` | Runtime | The UDP transport, the API client, and the routing enums. | | `CrowdyVoice` | Runtime | Microphone capture, encode and decode, playback. | | `CKSharedTypes` | Runtime | Shared data types used across modules. | | `CrowdyCppBridge` | Runtime | The vendored native client the transport is built on. You never include it directly. | | `CrowdyMass` | Runtime | The Crowdy Mass Rendering Backend, which draws remote entities as crowd rows. See [Draw a crowd with Mass](./mass/draw-a-crowd-with-mass.md). | | `CrowdyNodes` | UncookedOnly | The compiler checks for Crowdy-replicated variables. | | `CrowdyMassNodes` | UncookedOnly | The editor-side Blueprint nodes for crowd rows. | | `CrowdySDKEditor` | Editor | Turns event and state markers into Blueprint authoring surfaces and bakes them for cooked builds. | | `CrowdyStudio` | Editor | Crowdy Studio, the management console: sign-in, apps, Config Sync, authoring pages. | The SDK has no Game Model API: the game API no longer serves game models, and the SDK dropped its side of them in v2.19.0. Keep the state your game must trust in your server code on [ck-exec](/exec/intro); see [Move from Game Models](./exec/move-from-game-models.md) and [the two planes](./concepts/two-planes.md). ## Step 4: Add the module dependencies In your game module's `Build.cs`, add the modules your code calls. Most projects need these four: ```csharp PrivateDependencyModuleNames.AddRange(new string[] { "CrowdySDK", "CrowdyReplication", "CrowdyServices", "CrowdyNet" }); ``` :::note[Add CrowdyVoice only if your code calls the voice capture module directly.] The voice toggle on the SDK subsystem does not need it. `CKSharedTypes` arrives through the public dependencies of the four modules above, so it rarely needs listing. ::: ## Step 5: Recompile Regenerate project files and build: - **Rider or Visual Studio**: right-click the `.uproject`, choose *Generate project files*, then build. - **Unreal Editor**: accept the prompt to recompile on the next launch. Or from a command line, which is what a build machine runs: ```text "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\Build.bat" YourProjectEditor Win64 Development -Project="C:\Path\To\YourProject.uproject" -WaitMutex ``` The first build compiles the whole plugin and takes a few minutes. The log ends with `Link [x64] UnrealEditor-CrowdyStudio.dll` and `Result: Succeeded`. Expect a few warnings from a third-party header inside the plugin (a `C4324` padding warning from the vendored client's `spsc.hpp`); they are harmless and repeat on every build. Once it compiles, the SDK subsystems are available in both C++ and Blueprint. ## Step 6: Connect your app Open Crowdy Studio inside the editor, sign in, pick your app, and run Config Sync. It writes the connection settings into the project for you. Until you run Config Sync for the first time, the project has no Backend of its own, so it dials the tier the plugin release you downloaded was built for: Production for a tagged GitHub release. See [Project settings](./reference/project-settings.md#the-default-backend). :::warning[Do not type your app id and API URLs into Project Settings by hand.] The network settings on `UCrowdySDKDeveloperSettings` (`AppID`, `OrgId`, `Environment`, `DiscoveryUrl`, `GameApiHttpUrl`, `GameApiWsUrl`, `UDPProtocol`, `UDPTimeoutSeconds`, `HostPollIntervalSeconds`) are read-only in Project Settings on purpose (they live under **Project Settings, Plugins, Crowdy SDK**). Crowdy Studio owns them; a value you type elsewhere is overwritten by the next sync. See [Config Sync](./studio/config-sync.md). ::: ## Related - [Crowdy Studio overview](./studio/overview.md): open the console and sign in. - [Quickstart](./quickstart.md): your first entity, event, replicated property, and server-owned value. - [Map profiles](./runtime/map-profile.md): every playable map needs one before anything replicates. # Quickstart Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/quickstart # Quickstart This page puts you on the wire, then builds one small thing, a lantern, with one of each SDK idea on it: an entity, an RPC event, and a Crowdy State property. Each step is a few lines of C++ or a few Blueprint nodes, and each ends with something you can see. A single Play in Editor client is enough for all of it. Two words you will meet on every step. An **entity** is an actor other players can see: an actor with a Crowdy Entity Component, which gives it a shared identity and one **owner**, the client that simulates it; every other client holds a **proxy** that plays back what the owner sends. A **map profile** is a small data asset (`UCrowdyMapProfile`) that switches the SDK on for one map; it is assigned per map under **Project Settings, Plugins, Crowdy SDK, Map Profiles**. One fact shapes the order of the steps. The server knows a client by the actor updates it sends: your player pawn, as an entity, is what makes you present, and a client with no fresh actor is at no position on the map and receives no spatial event. So the pawn comes first, the lantern after. ## Before you start 1. [Install the plugin](./installation.md) into a project whose player already moves, the Third Person template or your own game, and confirm it compiles. Step 1 adds a component to the character you have; a bare Character has no body and no input, so start from one that walks. 2. Open Crowdy Studio, [sign in](./studio/sign-in.md), pick your app on the Project page, and press **Sync to project** on its Configuration tab. See [Config Sync](./studio/config-sync.md). 3. Optional: give your test map a [map profile](./runtime/map-profile.md) of its own. Without one it runs on the SDK's shipped default, which is enough for this page. 4. Know that nothing on this page reaches another client until your own pawn is sending updates (step 1). An event sent before that goes to nobody. ![The Project page's Configuration tab, with the diff and the Sync to project button](/img/unreal-sdk/qs-config-sync.png) :::caution[If you skip the sync, the SDK has no app id or Game API URLs to connect with, and nothing replicates.] ::: :::note[A map with no profile of its own runs on the SDK's shipped default.] Config Sync points the project at an app; a map profile decides what the SDK does on a map. With none assigned, the shipped default applies (networking on, the actor pool drawing remote entities with the shipped transform policy), so every step below works without authoring one. Author a profile when you need per-map settings; see [Map profiles](./runtime/map-profile.md). ::: ## About the code on this page The C++ steps build three small things. Step 0 is the Game Instance, `LanternGameInstance.h` and `LanternGameInstance.cpp`. Step 1 is your player, `LanternPlayer.h` and `LanternPlayer.cpp`, with a Game Mode, `LanternGameMode.h` and `LanternGameMode.cpp`, that spawns it at the right moment. Steps 3 to 5 build one actor, `ALantern`, in `Lantern.h` and `Lantern.cpp`: step 3 gives you both files, each later step is what you add to them, and the finished actor is at the end of the page. The first comment line of each block names the file it goes in. The usual boilerplate is left out so the SDK parts stand out: add `#pragma once`, `#include "CoreMinimal.h"`, and the base class include (`GameFramework/Actor.h`, `GameFramework/Character.h`, `GameFramework/GameModeBase.h`, `Components/ActorComponent.h`, or `Engine/GameInstance.h`) at the top of each header yourself. Everything else, including the `.generated.h` line, is in the block. The Blueprint steps build the same things with one difference: the sign-in lives in the Game Mode Blueprint, not in a Game Instance, so there is a Game Mode Blueprint, a Character Blueprint, and one Actor Blueprint for the lantern. The lantern needs two components before you start: a **Sphere Collision** (the trigger a player walks into) and a **Point Light** named `Light`, the thing every lantern graph drives. Every Blueprint figure can be panned with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. ## 0. Sign the player in Nothing connects by itself. The SDK requests its realtime connection only after a player signs in, so the game signs the player in once, at startup (from its Game Instance in C++, from its Game Mode in Blueprint): call `Login` (or `Register` for a new account) on the Crowdy SDK subsystem and wait for two events: **On Login** (the account is signed in) and then **On UDP Connection Success** (the realtime connection is up). Every entity, event, and property below depends on that connection. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdySDKSubsystem.h" #include "LanternGameInstance.generated.h" UCLASS(Config = Game) class ULanternGameInstance : public UGameInstance { GENERATED_BODY() public: virtual void Init() override; // A development account read from Config/DefaultGame.ini. Never ship credentials in a game. UPROPERTY(Config) FString Email; UPROPERTY(Config) FString Password; private: UFUNCTION() void HandleLogin(bool bSuccess, FString Message); UFUNCTION() void HandleConnected(); }; // LanternGameInstance.cpp #include "LanternGameInstance.h" void ULanternGameInstance::Init() { Super::Init(); UCrowdySDKSubsystem* Sdk = GetSubsystem(); Sdk->OnLogin.AddDynamic(this, &ULanternGameInstance::HandleLogin); Sdk->OnUDPConnectionSuccess.AddDynamic(this, &ULanternGameInstance::HandleConnected); Sdk->Login(Email, Password); } void ULanternGameInstance::HandleLogin(bool bSuccess, FString Message) { UE_LOG(LogTemp, Log, TEXT("Login %s %s"), bSuccess ? TEXT("ok") : TEXT("failed:"), *Message); } void ULanternGameInstance::HandleConnected() { // From here on entities register on the wire and a host is elected. UE_LOG(LogTemp, Log, TEXT("Connected to the app")); } ``` Make it the project's Game Instance under **Project Settings, Maps & Modes, Game Instance Class**, then give it a test account in `Config/DefaultGame.ini` (`YourModule` is your game module's name): ```ini [/Script/YourModule.LanternGameInstance] Email=you@example.com Password=your-test-password ``` A package ships `DefaultGame.ini`, so remove these two lines before you package the game. Create a Blueprint with **Game Mode Base** as its parent class and make it the project's default under **Project Settings, Maps & Modes, Default Modes, Default GameMode**, or set it as the test map's Game Mode Override in World Settings. In its event graph, **Event BeginPlay** gets the **Crowdy Authentication** subsystem and branches on its **Is Signed In**, so a map the player travels to later does not sign in again; when it is false, the latent **Login** node runs, with **On Success** and **On Error** pins. Fill in a test account. [Blueprint snippet: qs-login, see /bp/unreal-sdk/qs-login.txt] Sign in from the Game Mode, not from a Game Instance Blueprint's **Event Init**: that event runs before the SDK's subsystems exist, so **Login** would fail at once. [Authentication](./services/authentication.md#signing-in) says why. **Success signal.** The log reports `Login ok`, then `Connected to the app` (the two handlers in the C++ block; in Blueprint, bind **On UDP Connection Success** on the Crowdy SDK Subsystem for the second moment). From then on `Get UDP Connection State` on the subsystem reads connected. See [Authentication](./services/authentication.md) for the sign-in link and provider flows, and for restoring a saved session so a returning player types nothing. :::caution[A test account belongs in a config file or a pin default, never in shipped code.] ::: ## 1. Make your player a Crowdy entity Your player pawn is the first entity, before any lantern, because it is what makes you exist to the server. A pawn with a `UCrowdyEntityComponent` in **Dynamic** mode sends an actor update every replication interval (ten a second on the shipped profile), and a heartbeat at least once a second while it stands still; the server registers your position from those updates, and from then on you are a recipient of everything sent near you. The component needs three settings changed from their defaults: **Mode** Dynamic (the continuous channel; the default, Static, is for things that do not move), **Identity Policy** Player Derived (the identity is derived from the signed-in account, so it is the same on every client and on every launch; only valid on the locally controlled pawn), and **Ownership** Local Client (this client simulates the pawn; a pawn the Game Mode spawns is owned by the client that spawned it whatever this reads, so Local Client is the honest value; the default, Host, is for a level-placed world entity, which sends nothing until the elected host is known). Leave **State Executor** empty: the SDK's default executor snapshots the pawn's location and rotation, which is all this page needs. The identity is derived once and nothing re-derives it later: at `BeginPlay` when the pawn already has a controller, otherwise inside its first possession, which for a pawn the Game Mode spawns during play is the `RestartPlayer` call that spawned it. A pawn spawned by the Game Mode on the first frame begins play before the sign-in has answered, and its first updates leave before the server is ready for them, so the Game Mode starts players as spectators and the connected handler spawns the pawn instead. **On UDP Connection Success** fires once the server has assigned you and its readiness wait has elapsed (the SDK times that, you never do), and every update the pawn sends after it counts. Where the account id becomes readable. The engine possesses a pawn spawned during play only after its `BeginPlay`, so the component waits and registers inside that first possession, after the pawn's own **Possessed** and **Controller Changed** events; the first controller of any kind completes it, and an AI-possessed pawn takes the random-id fallback with its warning. A respawn that leaves the old pawn standing hands the account id to the new one, and the old pawn's component stops replicating. Read the id from the component's **On Crowdy Ownership Assigned** (`OnCrowdyOwnershipAssigned`) or the entity subsystem's `OnEntityRegistered`, never from `BeginPlay` or **Possessed**; **Is Locally Owned** on the pawn reads false until that registration, so a pawn graph that gates on it at `BeginPlay` runs nothing. `ALanternPlayer` is a Character with a `Torch` point light and the entity component. `ALanternGameMode` names it as the default pawn, starts players as spectators, binds **On UDP Connection Success** in its own `BeginPlay`, and spawns the pawn with `RestartPlayer` when the event fires. The event fires again after a re-assignment, and `RestartPlayer` keeps a pawn the player already has, so a reconnect does not respawn you. ```cpp // LanternPlayer.h #include "Components/PointLightComponent.h" #include "Replication/Components/CrowdyEntityComponent.h" #include "LanternPlayer.generated.h" UCLASS() class ALanternPlayer : public ACharacter { GENERATED_BODY() public: ALanternPlayer(); UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Torch; UPROPERTY(VisibleAnywhere, Category = "Crowdy") TObjectPtr CrowdyEntity; }; // LanternPlayer.cpp #include "LanternPlayer.h" ALanternPlayer::ALanternPlayer() { Torch = CreateDefaultSubobject(TEXT("Torch")); Torch->SetupAttachment(RootComponent); // Dynamic sends the transform every interval, Player Derived names the entity after the signed-in user, Local Client makes this client its sender. CrowdyEntity = CreateDefaultSubobject(TEXT("CrowdyEntity")); CrowdyEntity->Mode = ECrowdyEntityMode::Dynamic; CrowdyEntity->IdentityPolicy = ECrowdyIdentityPolicy::PlayerDerived; CrowdyEntity->Ownership = ECrowdyOwnership::LocalClient; } ``` ```cpp // LanternGameMode.h #include "LanternGameMode.generated.h" // Spawns the Lantern Player only once the app connection is up, so its first update carries the signed-in identity. UCLASS() class ALanternGameMode : public AGameModeBase { GENERATED_BODY() public: ALanternGameMode(); virtual void BeginPlay() override; private: UFUNCTION() void HandleConnected(); }; // LanternGameMode.cpp #include "LanternGameMode.h" #include "LanternPlayer.h" #include "Subsystem/CrowdySDKSubsystem.h" ALanternGameMode::ALanternGameMode() { DefaultPawnClass = ALanternPlayer::StaticClass(); bStartPlayersAsSpectators = true; } void ALanternGameMode::BeginPlay() { Super::BeginPlay(); GetGameInstance()->GetSubsystem()->OnUDPConnectionSuccess.AddDynamic(this, &ALanternGameMode::HandleConnected); } void ALanternGameMode::HandleConnected() { // The pawn's updates from here on are what make this client present to the server; on a reconnect the player keeps the pawn it has. RestartPlayer(GetWorld()->GetFirstPlayerController()); } ``` `ALanternPlayer` has no mesh and no input of its own, so keep the body and the movement you already have: open the template's `BP_ThirdPersonCharacter` (or your own Character), set its **Parent Class** to `Lantern Player` under **Class Settings**, then create a Blueprint whose parent is `Lantern Game Mode` and set its **Default Pawn Class** to that character, overriding the C++ class the constructor names. Make that Game Mode Blueprint the project's default under **Project Settings, Maps & Modes, Default Modes, Default GameMode**, or set it as the test map's Game Mode Override in World Settings. The Game Instance from step 0 is unchanged. One more Blueprint, the Character, and the Game Mode from step 0. 1. **The Character.** Open the template's `BP_ThirdPersonCharacter`, or your own Character that already moves; a new Blueprint with **Character** as its parent has no body and no input, so do not start from one. In the Components panel click **Add** and choose **Crowdy Entity Component**. In its Details panel, under **Crowdy SDK, Entity Component**, set **Mode** to **Dynamic**, **Identity Policy** to **Player Derived**, and **Ownership** to **Local Client**. Two rows appear once Mode is Dynamic, **State Executor** and **Auto Register**: leave the executor at None and Auto Register ticked; a third, **Host Override**, appears once Ownership is Local Client: leave it at Allow. Add a **Point Light** named `Torch` while you are there; a later page drives it. 2. **The Game Mode.** Open the Game Mode Blueprint from step 0. Set **Default Pawn Class** to your Character and tick **Start Players as Spectators** under **Game Mode**. 3. **The spawn.** In the same Game Mode's event graph, from **Event BeginPlay**, get the **Crowdy SDK Subsystem** and **Bind Event to On UDP Connection Success** to a custom event `OnConnected` (the same **Bind Event** shape the [Authentication](./services/authentication.md#after-sign-in) page uses for On Login). A graph has one **Event BeginPlay** and step 0 already uses it, so run the event into a **Sequence** node: the bind on its first pin, the sign-in from step 0 on its second. `OnConnected` calls **Restart Player** with **Get Player Controller** (index 0) on its New Player pin. In a map the player travels to later, the connection is already up and On UDP Connection Success does not fire again, so on a third Sequence pin check **Get UDP Connection State** and, when it is **Connected**, call **Restart Player** the same way. ![The entity component's Details panel at its defaults: change Mode, Identity Policy, and Ownership](/img/unreal-sdk/entity-mode-identity.png) [Blueprint snippet: qs-player-spawn, see /bp/unreal-sdk/qs-player-spawn.txt] **Success signal.** Press Play. The map sits still under a fixed camera until `Connected to the app`, then your character spawns at a Player Start and you can move it. With `crowdy.entity.trace 1` on, the log shows the pawn's entity registering on the wire right after the connection comes up. :::caution[A pawn that begins play before the sign-in has answered derives its identity from user id 0.] Nothing warns. Two such clients conflate into one player, and the host cannot be found by its avatar. Keep the pawn's spawn behind **On UDP Connection Success**, as the block does: the pawn then registers under your account's id inside the possession that `RestartPlayer` performs. ::: ## 2. See yourself Walk forward. Within a second or two a second copy of your character appears where you were a moment ago and follows you around at a small delay (if you kept a bare Character with no mesh, the copy is the torch light alone). That is not a local mirror: your updates went to the server, the server fanned them back to every client in range, you included, and the actor pool on this client drew the result as a proxy of your own class, interpolated to about 100 milliseconds plus one round trip behind you. The shipped profile keeps that echo because **Enable Owner Tracking** on the map profile defaults to on; turning it off drops your own echo and nothing else. What it means: the server received your updates, so you exist to it. The same updates are what make you a recipient of the lantern's event in step 4. In a shipped game you hide one side of the pair yourself (`SetActorHiddenInGame` on the local pawn when `IsLocallyOwned()`, or leave the echo off); for this page, keep it: it is the proof. {/* Screenshot pending: qs-own-reflection (window shot, user-driven; steps in the shot manifest). */} The other way to see the same thing is two real clients. Open two Play in Editor windows signed into two different accounts, as [Testing locally](./guides/testing-locally.md#two-pie-client-setup) describes; each window shows the other's pawn as a proxy, moving as its owner moves. **Success signal.** A second character follows you. If none appears, check in this order: the log shows `Connected to the app`; the pawn spawned after it, not on the first frame; the map's profile (or the shipped default) has its actor tracker on; `crowdy.entity.trace 1` shows the registration. ## 3. Light a lantern An entity is an actor with a `UCrowdyEntityComponent`. Two of the component's defaults are what most level-placed things want: **Static** mode (event-only; the other mode, **Dynamic**, streams a state snapshot every replication interval, as your pawn does) and a **Stable** identity every client derives the same way. This example changes the third: it sets **Ownership** to Local Client, so the client that placed the lantern owns it the moment it registers. With the default, Host, an entity is owned by the elected host, and `IsLocallyOwned()` stays false until the host is known, which happens after the sign-in above completes. The lantern is a sphere trigger with a point light on it. On the client that owns it, the light turns blue; on every other client it keeps its default colour. That is the ownership question, asked once at `BeginPlay`. ```cpp // Lantern.h #include "Components/PointLightComponent.h" #include "Components/SphereComponent.h" #include "Replication/Components/CrowdyEntityComponent.h" #include "Lantern.generated.h" UCLASS() class ALantern : public AActor { GENERATED_BODY() public: ALantern(); virtual void BeginPlay() override; UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Trigger; UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Light; // Makes the actor an entity: every client in the session sees it, one of them owns it. UPROPERTY(VisibleAnywhere, Category = "Crowdy") TObjectPtr CrowdyEntity; }; // Lantern.cpp #include "Lantern.h" ALantern::ALantern() { Trigger = CreateDefaultSubobject(TEXT("Trigger")); Trigger->InitSphereRadius(100.f); SetRootComponent(Trigger); Light = CreateDefaultSubobject(TEXT("Light")); Light->SetupAttachment(Trigger); // The client that places the lantern owns it at once; a host-owned entity waits for the host election. CrowdyEntity = CreateDefaultSubobject(TEXT("CrowdyEntity")); CrowdyEntity->Ownership = ECrowdyOwnership::LocalClient; } void ALantern::BeginPlay() { Super::BeginPlay(); if (CrowdyEntity->IsLocallyOwned()) { Light->SetLightColor(FLinearColor(0.4f, 0.7f, 1.f)); } } ``` In the Actor Blueprint, click **Add** in the Components panel and choose **Crowdy Entity Component**, then set its **Ownership** to **Local Client** in the Details panel. In the event graph: [Blueprint snippet: qs-entity, see /bp/unreal-sdk/qs-entity.txt] **Success signal.** Place the lantern in the map and press Play. Its light is blue, because this client owns it. The component also broadcasts `OnCrowdyOwnershipAssigned` on every client on the tick after the entity registers, with `bIsLocallyOwned` true on the owner; bind to it when you need the answer from an event rather than a poll. Once the sign-in has connected, `crowdy.entity.trace 1` logs the entity's registration on the wire. ## 4. Send one RPC event An RPC event is a function you mark with `CrowdyEvent`. You call it like any function; the SDK runs the matching receiver on the other clients. The recipient decides who: `SpatialMulticast` (everyone in range, the default), `Multicast` (everyone on the session channel), `OwningClient`, or `Host`. A spatial event is addressed by position, and a client's position is the one its pawn's updates registered: this works now because step 1 put you on the map, and it reaches only clients whose pawns did the same. The lantern's event is `Flicker`: walking into the lantern blinks its light off and on for a moment, and every client in range sees the blink. Only the owning client reacts to the overlap; on a `SpatialMulticast` event the owner runs the body itself and sends it to everyone in range, so a proxy's copy of the overlap must not send a second one. In C++ the pattern has two halves. The **receiver** is a normal `UFUNCTION` you declare yourself, named `_Implementation` and tagged with the `CrowdyEvent` meta; it is what runs on every recipient. The **call site** is `CROWDY_EVENT()`, a macro placed inside the class body (no trailing semicolon) that generates `(...)`: calling it marshals the arguments and sends them. The receiver has to be a real `UFUNCTION` because the header tool does not expand macros when it scans for reflected functions. ```cpp // Lantern.h #include "Replication/RPC/CrowdyEvent.h" virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; // The receiver runs on every client in range, the caller included; Flicker() is the call that sends it. UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "SpatialMulticast")) void Flicker_Implementation(); CROWDY_EVENT(Flicker) // Lantern.cpp #include "TimerManager.h" void ALantern::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); if (!CrowdyEntity->IsLocallyOwned()) { return; } Flicker(); } void ALantern::Flicker_Implementation() { Light->ToggleVisibility(); FTimerHandle Handle; GetWorldTimerManager().SetTimer(Handle, FTimerDelegate::CreateWeakLambda(this, [this] { Light->ToggleVisibility(); }), 0.2f, false); } ``` A replicated event is a Custom Event with **Crowdy Replicates** ticked in its Details panel and a **Recipient** chosen there. The figure is the receiver. Call it by name from any graph on the actor, exactly like a local custom event: from **Event ActorBeginOverlap**, behind the same **Is Locally Owned** branch as step 3; the call node is the ordinary one the editor offers for any custom event. [Blueprint snippet: qs-event, see /bp/unreal-sdk/qs-event.txt] **Success signal.** Walk your pawn into the lantern: the light blinks. With one client there is nobody else to receive the event (a sender never receives its own event from the network); to see the send and the receive on the wire, loop it back to yourself: ```text crowdy.rpc.loopback 1 crowdy.rpc.trace 1 ``` Now the blink comes from the looped-back receive instead of the local run, and the trace logs the send and the receive (function, entity, addressing, parameter bytes). Turn both off before you measure anything. ## 5. Replicate one property A Crowdy State property is a `UPROPERTY` marked `CrowdyState`. On the lantern, which step 3 made Local Client owned, the owner assigns and stops: the SDK diffs the property every tick, ships the change to every proxy, then runs the `CrowdyOnRep` function there. That is the client-owned pattern, in either entity mode, Static or Dynamic. A host-owned entity is different: the lantern post on the [identity page](./concepts/entities-identity-ownership.md#example), or any placed actor left at **Ownership** = Host, never ships a plain assignment; mark the property `CrowdyManualDirty` and call `MarkStateDirty` on the entity component after each write (or give it `CrowdyHeartbeat`), as [the host push](./runtime/crowdy-state.md#on-an-entity-you-do-not-own-the-host-push) explains. The flicker in step 4 is a moment: a client that joins afterwards never sees it. The lantern's lit state is different, it has to be the same on every client, late joiners included, so it is a replicated property, `bLit`. Walking out of the lantern flips it, and `OnRep_Lit` applies it to the light wherever the value lands. The SDK runs the owner's notify too, on the tick that ships the change; calling it here as well makes the light react on the same frame. That is safe because `OnRep_Lit` only applies the current value. ```cpp // Lantern.h virtual void NotifyActorEndOverlap(AActor* OtherActor) override; // View state: a wrong value is a visual glitch, not a cheat, so it belongs on the Crowdy State plane. UPROPERTY(meta = (CrowdyState, CrowdyOnRep = "OnRep_Lit")) bool bLit = true; UFUNCTION() void OnRep_Lit(); // Lantern.cpp void ALantern::NotifyActorEndOverlap(AActor* OtherActor) { Super::NotifyActorEndOverlap(OtherActor); if (!CrowdyEntity->IsLocallyOwned()) { return; } // On a client-owned entity, assign and stop: the SDK ships the change to every proxy; the owner runs the notify itself. A host-owned entity needs CrowdyManualDirty and MarkStateDirty. bLit = !bLit; OnRep_Lit(); } void ALantern::OnRep_Lit() { Light->SetVisibility(bLit); } ``` There is no node for this; it is a setting on the variable. 1. Add a Boolean variable named `Lit` to the Actor Blueprint, default true. 2. Select it in **My Blueprint** and find the **Crowdy Replication** dropdown in the Details panel. 3. Set it to **Replicated**. A **RepNotify** field appears; the editor creates an `OnRep_Lit` function for you. In it, call **Set Visibility** on `Light` with `Lit`. 4. Compile. The variable's Get and Set nodes now show the replication badge in their corner. From **Event ActorEndOverlap**, behind the same **Is Locally Owned** check, set `Lit` to NOT `Lit` and call `OnRep_Lit`. That is the client-owned pattern, the lantern's. On a host-owned actor (Ownership left at Host) a plain Set ships nothing: tick **Update manually** in the same dropdown and call **Mark Crowdy State Dirty** with the variable on its Property Name pin after each Set; see [the host push](./runtime/crowdy-state.md#on-an-entity-you-do-not-own-the-host-push). See [Crowdy State](./runtime/crowdy-state.md) for the other toggles in that dropdown. :::warning[Crowdy State and Unreal replication are mutually exclusive on one variable.] A Blueprint variable that is both natively Replicated and Crowdy Replicated fails to compile with an error in the Message Log. In C++, keep `Replicated` off a `CrowdyState` property. Pick one. ::: **Success signal.** Walk through the lantern: the light goes out, and stays out; walk through again and it comes back. A single client owns the property but has no proxy to receive it, so to see the delivery, spawn a local mirror: ```text crowdy.state.loopback 1 crowdy.state.trace 1 ``` The trace logs each delta and its size, and `OnRep_Lit` runs on the mirror. Turn both off afterwards. ## 6. The finished lantern The two lantern files after steps 3 to 5, for comparison with your own: ```cpp // Lantern.h #include "Components/PointLightComponent.h" #include "Components/SphereComponent.h" #include "Replication/Components/CrowdyEntityComponent.h" #include "Replication/RPC/CrowdyEvent.h" #include "Lantern.generated.h" UCLASS() class ALantern : public AActor { GENERATED_BODY() public: ALantern(); virtual void BeginPlay() override; UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Trigger; UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Light; // Makes the actor an entity: every client in the session sees it, one of them owns it. UPROPERTY(VisibleAnywhere, Category = "Crowdy") TObjectPtr CrowdyEntity; virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; // The receiver runs on every client in range, the caller included; Flicker() is the call that sends it. UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "SpatialMulticast")) void Flicker_Implementation(); CROWDY_EVENT(Flicker) virtual void NotifyActorEndOverlap(AActor* OtherActor) override; // View state: a wrong value is a visual glitch, not a cheat, so it belongs on the Crowdy State plane. UPROPERTY(meta = (CrowdyState, CrowdyOnRep = "OnRep_Lit")) bool bLit = true; UFUNCTION() void OnRep_Lit(); }; // Lantern.cpp #include "Lantern.h" #include "TimerManager.h" ALantern::ALantern() { Trigger = CreateDefaultSubobject(TEXT("Trigger")); Trigger->InitSphereRadius(100.f); SetRootComponent(Trigger); Light = CreateDefaultSubobject(TEXT("Light")); Light->SetupAttachment(Trigger); // The client that places the lantern owns it at once; a host-owned entity waits for the host election. CrowdyEntity = CreateDefaultSubobject(TEXT("CrowdyEntity")); CrowdyEntity->Ownership = ECrowdyOwnership::LocalClient; } void ALantern::BeginPlay() { Super::BeginPlay(); if (CrowdyEntity->IsLocallyOwned()) { Light->SetLightColor(FLinearColor(0.4f, 0.7f, 1.f)); } } void ALantern::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); if (!CrowdyEntity->IsLocallyOwned()) { return; } Flicker(); } void ALantern::Flicker_Implementation() { Light->ToggleVisibility(); FTimerHandle Handle; GetWorldTimerManager().SetTimer(Handle, FTimerDelegate::CreateWeakLambda(this, [this] { Light->ToggleVisibility(); }), 0.2f, false); } void ALantern::NotifyActorEndOverlap(AActor* OtherActor) { Super::NotifyActorEndOverlap(OtherActor); if (!CrowdyEntity->IsLocallyOwned()) { return; } // On a client-owned entity, assign and stop: the SDK ships the change to every proxy; the owner runs the notify itself. A host-owned entity needs CrowdyManualDirty and MarkStateDirty. bLit = !bLit; OnRep_Lit(); } void ALantern::OnRep_Lit() { Light->SetVisibility(bLit); } ``` ## Gotchas - No login, no connection. Until `Get UDP Connection State` reads connected, every step looks dead. - No pawn, no presence. The server knows you by your pawn's updates; a client whose pawn is not sending is at no position and receives no spatial event, and its own spatial sends reach nobody. A `Multicast` event is the exception: it goes by session channel membership, which connecting joins. - Spawn the pawn after **On UDP Connection Success**. Before it, the identity derives from user id 0 and the first updates leave before the server is ready to take them; nothing is queued or replayed, the next interval's update is the recovery. A pawn spawned after it registers inside its first possession, so read its id from **On Crowdy Ownership Assigned**, not at `BeginPlay`. - The Game Mode binds the event in its own `BeginPlay`. A map opened after the connection is already up never sees it; check **Get UDP Connection State** at `BeginPlay` first and restart the player at once when it reads connected, or bind in the Game Instance and spawn from there. - A map whose `MapProfiles` row or `DefaultProfile` names an asset that did not load is inactive; the warning names the asset. Check that before any code. A map with no row at all runs on the shipped default. - The loopback variables are test aids. Leave them off in a normal session; they change what a single client sees. - An RPC receiver must be a real `UFUNCTION` you declare yourself; the `CROWDY_EVENT` macro only generates the call site. ## Next steps - [The Two Planes](./concepts/two-planes.md): decide where each new piece of state goes. - [Entities, Identity, and Ownership](./concepts/entities-identity-ownership.md): owner, proxy, host, and what Ownership chooses. - [Sessions and presence](./concepts/sessions-and-presence.md): what the server counts as a present player, and when it stops. - [Continuous state](./runtime/continuous-state.md): what your pawn sends every interval, and how to send more than the transform. - [Entities and spawning](./runtime/entities-and-spawning.md): spawn entities at runtime and receive them on remote clients. - [RPC events in C++](./runtime/rpc-events-cpp.md) and [in Blueprint](./runtime/rpc-events-blueprint.md). - [Crowdy State](./runtime/crowdy-state.md): the five metadata keys and what each does. # For AI Agents Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/for-ai-agents # For AI Agents This page is the routing layer for a coding agent working inside a studio's Unreal project. It does not re-explain the SDK; every row links the page that owns the fact, and the text of every page is available as plain-text files, one per section. Read this page, then pull only the pages a task needs. Machine-readable files sit beside the docs. This page is where they are described. Start at the index, pull one section file, and check the slim symbol index before the full manifest. | File | Size | Use it when | |---|---|---| | [`/helpers/unreal-sdk/llms.txt`](pathname:///helpers/unreal-sdk/llms.txt) | about 42 KB | You are starting a task. Every page with its URL and a one-line description, grouped into sections, each with the URL and size of its section file. | | `/helpers/unreal-sdk/llms-
.txt`: [`start`](pathname:///helpers/unreal-sdk/llms-start.txt), [`concepts`](pathname:///helpers/unreal-sdk/llms-concepts.txt), [`studio`](pathname:///helpers/unreal-sdk/llms-studio.txt), [`runtime`](pathname:///helpers/unreal-sdk/llms-runtime.txt), [`mass`](pathname:///helpers/unreal-sdk/llms-mass.txt), [`exec`](pathname:///helpers/unreal-sdk/llms-exec.txt), [`services`](pathname:///helpers/unreal-sdk/llms-services.txt), [`guides`](pathname:///helpers/unreal-sdk/llms-guides.txt), [`reference`](pathname:///helpers/unreal-sdk/llms-reference.txt) | 20 to 430 KB each | You know which part of the SDK the task touches. Every page of that section as plain text, with C++ snippets inlined and a pointer line for each Blueprint graph. A section over 200 KB is split by subfolder: `exec` and `mass` also have `llms-exec-from-blueprint.txt`, `llms-exec-from-cpp.txt`, `llms-exec-examples.txt` and the same three for `mass`; `llms-exec.txt` then holds only the section's top-level pages. The index lists every file with its size. | | [`/helpers/unreal-sdk/sdk-surface-index.jsonl`](pathname:///helpers/unreal-sdk/sdk-surface-index.jsonl) | about 340 KB | You need to know whether a symbol exists, which module and header it lives in, or where to look. One line per symbol: name, kind, module, header, and the first sentence of its help text where the manifest has one (console variables only today). Line one names the columns. | | [`/helpers/unreal-sdk/sdk-surface.json`](pathname:///helpers/unreal-sdk/sdk-surface.json) | about 3 MB | The index is not enough: you need a function's specifiers and return type, a struct's metadata, an enum's values or a delegate's signature. Too large to read whole; query it with `jq` or a script. See [SDK surface manifest](./reference/sdk-surface.md). | | [`/helpers/unreal-sdk/llms-full.txt`](pathname:///helpers/unreal-sdk/llms-full.txt) | about 1.6 MB | A tool wants every page in one file. About 400,000 tokens, so it does not fit a context window. Prefer the section files. | | [`/helpers/unreal-sdk/AGENTS.md`](pathname:///helpers/unreal-sdk/AGENTS.md) | under 8 KB | A drop-in for a project root, `CLAUDE.md`, or `.cursorrules`: the locked rules and one example per marker. | ## Where does this state go Ask one question: can a malicious client benefit from lying about this value? If yes, it belongs on the truth plane, in your app's server code on [ck-exec](/exec/intro), which the SDK reaches from C++ or Blueprint through [Server Logic](./exec/overview.md). If no, it is view state. [The Two Planes](./concepts/two-planes.md#deciding-where-a-field-goes) owns the rule and every row but the door's; that one applies the same test. | State | Plane | Mechanism | Owning page | |---|---|---|---| | Movement | View | Dynamic entity mode, continuous state channel | [Continuous state](./runtime/continuous-state.md) | | Animation blend state | View | `meta=(CrowdyState)` property, diffed on change | [Crowdy State](./runtime/crowdy-state.md) | | A one-shot trigger (muzzle flash, impact) | View | `meta=(CrowdyEvent)` RPC, not a state field | [RPC events](./runtime/rpc-events-cpp.md) | | Hit points, inventory, currency | Truth | Your server code | [The Two Planes](./concepts/two-planes.md#deciding-where-a-field-goes) | | Match score | Truth | A hub for the match in your server code | [The Two Planes](./concepts/two-planes.md#deciding-where-a-field-goes) | | A chat line | View | `CrowdyEvent` with the `Multicast` recipient, session channel | [Channels](./runtime/channels.md) | | A door's open flag | View | `CrowdyState` boolean: a lie is a visual glitch. A placed door is host-owned, so the flag is also `CrowdyManualDirty` and the host calls `MarkStateDirty` after each write. If the door gates progress (a lock), the lock state belongs in your server code | [The Two Planes](./concepts/two-planes.md#deciding-where-a-field-goes), [the host push](./runtime/crowdy-state.md#on-an-entity-you-do-not-own-the-host-push) | | A player's display name | View | `CrowdyState` property; route it through the avatars service if it must persist | [The Two Planes](./concepts/two-planes.md#deciding-where-a-field-goes), [Avatars](./services/avatars.md) | :::danger[Authoritative or cheat-sensitive state never belongs in Crowdy State.] Crowdy State is written by the owning client and believed by everyone else. Low latency is not a reason to move a value to the view plane; predict locally and let the server's confirmed value overwrite the prediction. [The Two Planes](./concepts/two-planes.md#gotchas). ::: ## The marker vocabulary Two of the three are `meta=(...)` keys; the entity is a component. One canonical example each, all from the same lantern the [Quickstart](./quickstart.md) builds; the snippet id names the file the docs render. | Marker | Goes on | Canonical example | Owning page | |---|---|---|---| | `CrowdyEntity` | A `UCrowdyEntityComponent` on an `AActor` (the component is the marker; set `Ownership`) | `qs-entity` | [Entities, identity, ownership](./concepts/entities-identity-ownership.md) | | `CrowdyState` (+ `CrowdyOnRep`) | A `UPROPERTY` on an entity actor. On a client-owned entity (`Ownership` = Local Client, the lantern) the owner assigns and calls the notify itself; on a host-owned entity (`Ownership` = Host, the default a placed actor keeps) a plain assignment never ships: add `CrowdyManualDirty` and call `MarkStateDirty` after each write, or add `CrowdyHeartbeat` | `qs-state` | [Crowdy State](./runtime/crowdy-state.md#mark-a-property), [the host push](./runtime/crowdy-state.md#on-an-entity-you-do-not-own-the-host-push) | | `CrowdyEvent` + `CrowdyRecipient` | A `UFUNCTION` named `X_Implementation`, plus `CROWDY_EVENT(X)`; recipient is one of `SpatialMulticast`, `Multicast`, `OwningClient`, `Host` | `qs-event` | [RPC events](./runtime/rpc-events-cpp.md#routing-keys) | Four rules the examples assume: the first thing a client needs is its own pawn as a `Dynamic`, `PlayerDerived`, `LocalClient` entity spawned after **On UDP Connection Success** (`qs-player`), because the server knows a client by its actor updates and a `SpatialMulticast` reaches only clients with a fresh actor; that pawn registers inside its first possession, since the engine possesses a runtime spawn after its `BeginPlay`, so read its id and `IsLocallyOwned()` from `OnCrowdyOwnershipAssigned`, not at `BeginPlay`; a `SpatialMulticast` body runs on the caller too, so guard the call with `IsLocallyOwned()` ([RPC events](./runtime/rpc-events-cpp.md#gotchas)); a `CrowdyOnRep` function takes no parameters, read the property for the new value ([Crowdy State](./runtime/crowdy-state.md)); and the lantern's assign-and-stop is the client-owned pattern, because the Quickstart sets its `Ownership` to Local Client: a host-owned entity (`Ownership` = Host, the default a placed actor keeps: Static, Stable, level-placed) never ships a plain `CrowdyState` write, so mark the property `CrowdyManualDirty` and call `MarkStateDirty` after each write, or give it `CrowdyHeartbeat` ([the host push](./runtime/crowdy-state.md#on-an-entity-you-do-not-own-the-host-push)). ## Three things that break silently in a cooked build Editor and Play in Editor hide all three. Only a packaged build shows them. | Break | Why it is silent | Fix | Owning page | |---|---|---|---| | Reading `HasMetaData` at runtime | Cooked and Shipping builds strip UObject metadata; the check answers `false` and marker-driven code goes dark with no error | Read `UCrowdyBakedRegistry`; the SDK already does this for RPC and Crowdy State metadata | [Packaging](./guides/packaging.md#1-rebuild-the-baked-registry) | | A marker added after the last cook | The baked registry is a snapshot taken at cook time; a new marker is invisible until it is rebaked | Cook again: the cook rebakes the registry at its start, so the break is a package cooked before the marker existed. **Tools > Rebuild Crowdy Registry** or Studio's **Rebuild (Deep Scan)** only refresh the in-editor view of the bake | [Packaging](./guides/packaging.md#1-rebuild-the-baked-registry), [Inspector and Registry](./studio/inspector-and-registry.md) | | A placement guid read at `BeginPlay` | The engine releases the per-placement guid before `BeginPlay` in a cooked build; the editor keeps it alive | Read placement data at `OnRegister`, never at `BeginPlay` | [Stable identity in a packaged build](./concepts/entities-identity-ownership.md#stable-identity-in-a-packaged-build) | A fourth hazard is loud, not silent: Shipping sets `WITH_DEV_AUTOMATION_TESTS` to 0, and a target that forces dev tests back on can then fail to compile a test file through a `!UE_BUILD_SHIPPING` region it reaches via a header. [What Shipping strips](./guides/packaging.md#5-what-shipping-strips). ## Settings never to hand-edit Config Sync in Crowdy Studio writes the Network category of `UCrowdySDKDeveloperSettings` into `Config/DefaultGame.ini` under `[/Script/CrowdyReplication.CrowdySDKDeveloperSettings]`. The fields are read-only in Project Settings on purpose; a hand-pasted value is overwritten by the next sync, and a drifted project fails to connect with no obvious error. | Never hand-type | Owned by you (fine to edit) | Owning page | |---|---|---| | `Environment`, `DiscoveryUrl`, `AppID`, `OrgId`, `GameApiHttpUrl`, `GameApiWsUrl`, `UDPProtocol`, `UDPTimeoutSeconds`, `HostPollIntervalSeconds` | `MapProfiles`, `DefaultProfile`, `BakedRegistry`, `PreloadedEntityClasses`, `IDOverrides`, `ClassIDOverrides` | [Config Sync](./studio/config-sync.md#what-config-sync-writes-and-what-you-own), [Project settings](./reference/project-settings.md) | Do not copy the plugin's `[CoreRedirects]` into the project's `DefaultEngine.ini` either; `Plugins/CrowdySDK/Config/DefaultCrowdySDK.ini` already carries them. [Packaging](./guides/packaging.md#6-coreredirects-ship-with-the-plugin). ## Drive the editor without a window An agent cannot press editor buttons. Two commandlets do what the Server Compute panel and Crowdy Studio's Project page do, from a shell: ``` UnrealEditor-Cmd.exe .uproject -run=CrowdyServerCompute -op= [switches] [-out=result.json] UnrealEditor-Cmd.exe .uproject -run=CrowdyStudio -op= [switches] [-out=result.json] ``` Exit code 0 is success, 1 is a refusal or a failure, and `-out=` writes the result as JSON (`ok`, `error`, and the op's own fields). An op that changes the project or the app needs `-yes`; without it the op says what it would do and changes nothing. A commandlet run with an unknown or no `-op` prints every op it has. Close the Unreal Editor first. It holds the project's Server Object types and settings in memory and saves its own copy over what an op wrote, so an op that writes the project refuses while an editor runs unless you pass `-editoropen`. | Step | Op | Owning section | |---|---|---| | Check the sign-in and the project's app | `-run=CrowdyServerCompute -op=whoami` | [From the command line](./exec/deploy-with-server-compute.md#from-the-command-line) | | List organizations, apps and datacenters | `-run=CrowdyStudio -op=apps` | [Sync from the command line](./studio/config-sync.md#sync-from-the-command-line) | | Create an app | `-run=CrowdyStudio -op=createapp -org= -name="My Game" -slug=my-game -datacenter= -yes` | [Sync from the command line](./studio/config-sync.md#sync-from-the-command-line) | | Point the project at an app | `-run=CrowdyStudio -op=sync -app= -yes` | [Sync from the command line](./studio/config-sync.md#sync-from-the-command-line) | | Read every entity class's CrowdyEvent routing and CrowdyState flags | `-run=CrowdyStudio -op=registry [-class=] [-norebuild] [-out=file]` | [Sync from the command line](./studio/config-sync.md#sync-from-the-command-line) | | Add Game Kit starters | `-op=addstarter -starter=` | [Add a starter](./exec/use-game-kit-starters.md#add-a-starter) | | Define a Server Object type | `-op=exporttype -spec=type.json` (a template), `-op=newtype -spec=@type.json`, `-op=edittype -type=T -spec=@type.json -yes` | [Define a type from a file](./exec/deploy-with-server-compute.md#define-a-type-from-a-file) | | Generate its server code | `-op=generate -type=T`, or `-op=generate -yes` for every type that is out of date | [From the command line](./exec/deploy-with-server-compute.md#from-the-command-line) | | Remove or rename a type | `-op=removetype -type=T -yes`, `-op=renametype -type=T -to=/Game/Path/NewName -yes` (without `-yes` they only say what they would do) | [From the command line](./exec/deploy-with-server-compute.md#from-the-command-line) | | Write its logic | Edit `Server//src/logic.rs` | [Write logic.rs](./exec/write-server-logic.md#write-logicrs) | | Deploy, call and read back | `-op=deploy -yes`, `-op=call -type=T -fn=F -key=K -inputs=@in.json -yes`, `-op=logs [-level=errors]`, `-op=instances` | [From the command line](./exec/deploy-with-server-compute.md#from-the-command-line) | | Move running Server Objects to the new deploy | `-op=restart -type=T -yes`, or `-op=restart -yes` for every type with an instance on older code | [From the command line](./exec/deploy-with-server-compute.md#from-the-command-line) | | Set up access features, teams, channels and grids | `-run=CrowdyStudio -op=features`, `-op=definefeature`, `-op=createteam`, `-op=createchannel`, `-op=creategrid`, `-op=grantgrid` and more (comma lists without spaces: `-keys=a,b`) | [Access, teams, channels and grids](./studio/config-sync.md#access-teams-channels-and-grids) | `-op=generate` reports, per type, the functions `logic.rs` is missing and the ones it defines that the type no longer has, so the next edit is known. `-op=batch -ops=` runs many ops in one editor start. Who owns which file: | File | Owner | |---|---| | `Server//src/logic.rs`, and any `src/logic/.rs` it declares with `mod ;` | You | | `Cargo.toml`, `src/lib.rs`, `src/types.rs` in the same folder | Generate rewrites them; never edit | | `Server//revisions.json` | Server Compute: every kept deploy's code in full, so a restore is a copy | | `GameKitWorld`, `GameKitActorStarters`, `PreloadedEntityClasses` and the map profiles in `Config/DefaultGame.ini` | You | | The Network settings in `Config/DefaultGame.ini` | Config Sync, or `-run=CrowdyStudio -op=sync` | Two things still need a person. Crowdy Studio's sign-in is once per machine and again when it expires; `-op=whoami` says whether it is there. The Blueprint-only editors (the Crowdy Replication variable panel, Crowdy Replicates events) have no text form; write the [C++ markers](#the-marker-vocabulary) instead. The `sbx.*` console commands exist only in the SDK's own Sandbox project, not in yours. ## Testing without a second client Both switches are off by default. Turn them back off before testing real two-client routing. [Testing locally](./guides/testing-locally.md). | Console variable | What it does | Owning section | |---|---|---| | `crowdy.rpc.loopback 1` | A sent `CrowdyEvent` is also delivered to your own receive path, once; a call made from inside the replayed body does not loop again | [Single-client RPC loopback](./guides/testing-locally.md#single-client-rpc-loopback) | | `crowdy.state.loopback 1` | A state delta for an owned entity is decoded onto a lazily spawned local mirror (`RemoteProxy` role, its own NetID), so `OnRep` fires with one client. The mirror's trace line always says `(targeted)`; that label is cosmetic | [Single-client Crowdy State loopback](./guides/testing-locally.md#single-client-crowdy-state-loopback) | For a headless smoke test in a development build, `crowdy.auth.signin.file ` signs in a test account from a JSON file (`{"email": "...", "password": "..."}`; never type a password on a console line), and `crowdy.auth.status` reports `SignedIn=true` once it has finished: poll it before player-side commands. [Sign in from the console](./guides/testing-locally.md#sign-in-from-the-console). ## Trace console variables Every area exposes `crowdy..trace`, off by default; warnings and errors print regardless. The four to reach for first, and the two to leave alone: | Console variable | Shows | Owning page | |---|---|---| | `crowdy.rpc.trace` | Every `CrowdyEvent` send and receive: function, entity, addressing, owned or delegated | [Testing locally](./guides/testing-locally.md#trace-console-variables) | | `crowdy.state.trace` | Owned-entity diffing, delta emission, datagram sizes | [Testing locally](./guides/testing-locally.md#trace-console-variables) | | `crowdy.entity.trace` | Registry add and remove, event routing and dispatch, actor tracking | [Testing locally](./guides/testing-locally.md#trace-console-variables) | | `crowdy.net.trace` | UDP transport: socket open and close, sends and receives, pool churn | [Testing locally](./guides/testing-locally.md#trace-console-variables) | | `crowdy.serialize.scopes`, `crowdy.state.scopes` | CPU trace scopes around every message decode, a real per-message cost; leave off unless reading that timing | [Console variables](./reference/console-cvars.md#trace-gates) | The full table, including behavior switches and diagnostic commands, is on [Console variables](./reference/console-cvars.md); which log category pairs with which trace is on [Log categories](./reference/log-categories.md). ## Locked rules in one place | Rule | Owning page | |---|---| | Every id is `int64`; the app id travels as a JSON string and the SDK encodes it, never you | [The Two Planes](./concepts/two-planes.md#what-the-split-means-for-your-code) | | The host is a convention, not enforcement; never gate cheat-sensitive state on it | [Host is a convention](./concepts/host-is-a-convention.md), [Host authority](./runtime/host-authority.md) | | Presence is the player's actor: a client with no fresh actor is at no position and receives no spatial event | [Sessions and presence](./concepts/sessions-and-presence.md#presence-is-the-players-actor) | | A world subsystem null-checks `GetGameInstance()` in `Initialize()`; it is null in the transient world at engine start | [Host authority](./runtime/host-authority.md#the-host-subsystem), [Replicated subsystems](./runtime/replicated-subsystems.md) | | An array parameter on a delegate handler is `const TArray&`; a by-value parameter does not match and `AddDynamic` will not compile | [Teams](./services/teams.md) | | A `Multicast` call has a 1024-byte payload budget and is dropped loudly, never truncated; large payloads go on `SpatialMulticast` | [RPC events](./runtime/rpc-events-cpp.md#containers-and-their-bounds) | ## Related - [Quickstart](./quickstart.md): the lantern every example above comes from. - [Best Practices](./guides/best-practices.md): the four rules stated in the abstract. - [Packaging](./guides/packaging.md): the checklist before a cooked build. # The Two Planes Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/concepts/two-planes # The Two Planes Every piece of networked state in the SDK lives on one of two planes. The view plane is fast and client-owned; the truth plane is server-owned and enforced. Keeping them apart is the one rule the rest of the SDK is built on. ## The split **Plane A, the view plane (Crowdy State).** Continuous, client-authoritative state that has to move fast: movement, animation, transient effects. The client that owns an entity writes its view state; the SDK ships it to every other client over UDP, through RPC events and property replication. Nothing on this plane is checked by a server. An elected host helps coordinate it, but as a convention, not as an authority. **Plane B, the truth plane (your server code).** Gameplay state the game must be able to trust: hit points, stats, inventory, currency, anything a client could benefit from lying about. The server owns it: your app's code on [ck-exec](/exec/intro) holds it, and a client asks that code for a change, which it decides and confirms. This is the only place a rule is enforced. :::caution[The SDK reaches ck-exec through Server Objects.] The SDK once had its own truth-plane API (Game Model containers and attributes, `CrowdyModel` markers, Crowdy Effects, invoke policies and sessions), built on the game API's legacy game-model engine, which ck-exec replaced. The game API no longer serves it and the SDK has removed it. Its replacement is [Server Objects](../exec/overview.md), from Blueprint or C++. See [Move from Game Models](../exec/move-from-game-models.md). ::: :::note[The elected host is a convention, not an enforcement point.] Any client can, in principle, lie about its own view state. The host is a coordinator for shared view-plane logic, not a referee. See [The Host Is a Convention](./host-is-a-convention.md). ::: ## Where the planes are allowed to touch A client can ask the server's code to change a truth value, and is told the result. No gameplay value ever flows from the view plane into the truth plane as trusted input: the server's code decides from what it holds, not from what a client says it sees. ## What the split means for your code Each of these follows from the split above. The page that owns the detail is linked. 1. **If a cheater would want to change it, it belongs on the truth plane.** Hit points, currency, inventory, scores, cooldowns, anything a win depends on. Crowdy State is for what players see: position, animation, a light that is on or off. The low latency of Crowdy State is not a reason to put a trusted value there. 2. **Never decide gameplay from another client's Crowdy State.** Every client writes its own view state and nothing checks it, so a remote client's health bar, hit flag or score in Crowdy State is a claim, not a fact. Read the trusted value from the server instead. See [Crowdy State](../runtime/crowdy-state.md). 3. **Change a trusted value by asking, not by writing.** The server's code decides and confirms; there is no client-side write to a trusted value. See [Game API best practices](/game-api/best-practices). 4. **Expect the confirmed value to arrive a moment later.** Predict locally if you must, and let the confirmed value overwrite the prediction. 5. **Do not build authority on "I am the host".** The host is an elected client that coordinates view-plane work; it can leave, change, or lie. Anything that must be true for everyone goes through the server's code. See [The Host Is a Convention](./host-is-a-convention.md). 6. **Every server id is 64-bit.** Store app, user and team ids as `int64` (Blueprint `Integer64`), never as a 32-bit integer or a parsed number; the app id travels as a string. 7. **A packaged build only knows the markers it was cooked with.** `CrowdyEvent` and `CrowdyState` markers are baked into a registry at cook time; add a marker, cook again, and never read Unreal metadata at runtime in shipped code. See [Packaging](../guides/packaging.md). ## Deciding where a field goes Ask one question: **can a malicious client benefit from lying about this value?** If yes, it belongs on the truth plane. If no, it is view state. :::danger[Authoritative or cheat-sensitive state never belongs in Crowdy State.] Crowdy State is written by the client that owns the entity and believed by everyone else. A hit-point value there is a hit-point value the owner can set to a million. ::: The table below is a guide, not a rule book. Each row names the mechanism that fits. | State | Plane | Mechanism | |---|---|---| | Movement | View | Dynamic entity mode: the continuous state channel sends a snapshot every replication interval. | | Animation blend state | View | A Crowdy State property (`meta=(CrowdyState)`), diffed and sent on change. | | A one-shot effect trigger (a muzzle flash, an impact) | View | An RPC event (`meta=(CrowdyEvent)`), because a trigger is not continuous state. | | Hit points | Truth | Your server code, which applies damage and heals. | | Inventory | Truth | Your server code. | | Currency | Truth | Server code that outlives any one match. | | Match score | Truth | A hub for the match, such as the `session` [starter pack](/exec/builds#starter-packs)'s. | | Chat | View | An RPC event with the `Multicast` recipient over the session channel. Not gameplay truth. | | A cosmetic display name | View | A Crowdy State property. Route it through the avatars service instead if it has to persist. | ## Gotchas - Low latency is not a reason to move a value to the view plane. Predict locally if you must, but let the server's confirmed value overwrite the prediction. - The Blueprint **Crowdy Replication** dropdown offers only **None** and **Replicated**. **Replicated** is for view state; keep trusted values in your server code. ## Related - [Entities, Identity, and Ownership](./entities-identity-ownership.md): what an entity is and who may write its view state. - [The Host Is a Convention](./host-is-a-convention.md): what host election does and does not decide. - [Crowdy State](../runtime/crowdy-state.md): the view-plane property system. - [ck-exec overview](/exec/intro): where the truth plane's code runs. # Entities, Identity, and Ownership Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/concepts/entities-identity-ownership # Entities, Identity, and Ownership An entity is an actor that other players can see and react to. It carries one `UCrowdyEntityComponent`, which gives it a shared identity, a role on each client, and a place in the entity registry. RPC events and Crowdy State are both built on top of that. ## What an entity is Add a `UCrowdyEntityComponent` to an actor and it becomes an entity. On every client the entity has: - a **NetID**, the name every client uses for this one thing; - a **role**: `Owner` on the client that simulates it, `RemoteProxy` everywhere else, or `HostOwned` for a world entity the elected host drives; - an entry in the entity registry, which is how an event aimed at "this rock" finds the right rock on each machine. ## The NetID The SDK hands you the NetID as an `FGuid` (`GetNetID()`), which is convenient to compare and store. On the wire it is 32 opaque bytes; the `FGuid` is a derived form. Treat it as an opaque handle: compare it, pass it along, never build one by hand. :::warning[Never construct a NetID yourself.] The wire identity is not an RFC 4122 UUID, and a hand-made `FGuid` does not name anything. Let the identity policy below, or a spawn call, mint it. ::: ### `FCrowdyActorId`, the wire type `FGuid` is the convenient face; `FCrowdyActorId` is what actually travels the wire, 32 raw octets rather than an RFC 4122 UUID. Build one only through its named constructors: `FromGuid` converts an `FGuid`, `FromOctets` and the fallible `TryFromOctets` build from an existing 32-byte view (the fallible one returns `bool` instead of asserting), `FromStringOrUnset` and `TryFromString` parse a hex string, and `FromAnsiLiteral` builds a compile-time one from a string literal for tests and defaults. Once you have one, `IsSet()` says whether it names anything, `AsOctets()` gives a read-only view of the 32 bytes, `AppendTo` and `WriteTo` copy them into a buffer you already own, `ToString()` renders the hex form back, and `Reset()` clears it to unset. ## Identity policies `IdentityPolicy` on the component decides how the NetID is chosen. - **Stable** (the default). Derived from the actor's placement in the level, so every client computes the same NetID for the same level-placed actor with no spawn message and no hand-typed seed. Use it for anything that already exists when the map loads: props, spawners, doors. - **PlayerDerived**. Derived from the signed-in user id. Valid only on the locally controlled player pawn. - **Random**. A fresh id each time, with no cross-client agreement. Use it for a purely local actor nobody else needs to address. An actor you spawn during play through the SDK's spawn call is told its identity by the spawner, so its policy does not matter; the spawn travels to every client carrying the NetID. :::note[PlayerDerived on anything but the local player's pawn falls back to Random.] The SDK logs one warning and the actor gets an id no other client shares. Keep PlayerDerived for the pawn. The identity is read once: at the component's `BeginPlay` when the pawn already has a controller, otherwise inside its first possession, after the pawn's own **Possessed** and **Controller Changed** events, since a pawn spawned during play (`RestartPlayer`, a respawn) is possessed only after `BeginPlay`. The first controller of any kind completes it; an AI controller gets the fallback. A respawn that leaves the earlier pawn alive moves the account id to the new pawn and clears the old one, which stops replicating. Read the id from the component's **On Crowdy Ownership Assigned** or the entity subsystem's `OnEntityRegistered`, never from `BeginPlay` or **Possessed**; **Is Locally Owned** on the pawn is false until the registration. ::: ![The entity component's Details panel: Mode, Identity Policy, and Ownership](/img/unreal-sdk/entity-mode-identity.png) ### Stable identity in a packaged build Stable identity prefers the engine's per-placement instance guid, which is saved in the level and is identical on every client. In a cooked build the engine releases that guid before `BeginPlay`, so the SDK snapshots it when the component registers and uses the snapshot. You do not have to do anything, but if you write your own identity code, read placement data at `OnRegister`, never at `BeginPlay`. :::danger[In a cooked build, read the placement guid at OnRegister, never at BeginPlay.] Editor and Play in Editor keep the guid alive and hide the bug; only a packaged build shows it. ::: ## Owner and proxy For each entity exactly one client is the authority, and only the authority writes the entity's view state. `Ownership` on the component chooses the authority for a level-placed actor: - **Host** (the default): whichever client is currently the elected host owns it (`Role = HostOwned`). Every client shares one authority, which is what a world or AI entity wants. - **LocalClient**: this client owns and simulates it (`Role = Owner`). An entity spawned at runtime is always owned by the client that spawned it, whatever the component says. `Ownership` only applies to level-placed actors that resolve their own identity. Ask `IsLocallyOwned()` before doing owner work. It is true when this client is the entity's owner, or when the entity is host-owned and this client is the host. A remote proxy never sends state; it receives and plays back. A player's own avatar has `NetID == OwnerID == the local player id`, and every entity that player spawns carries that id as its `OwnerID`. This is Crowdy ownership and has nothing to do with `AActor::GetOwner()`. ## Example A lantern post is a lantern that belongs to the level rather than to a player: the [Quickstart](../quickstart.md) lantern with the three settings a level-placed world entity wants. It is event-only, every client derives the same NetID for it, and the elected host is its one authority. All three settings are `EditAnywhere`, so a constructor (or the Details panel, or Class Defaults) is where they are set. `ALanternPost` inherits everything else from `ALantern`, so a post flickers and lights like any lantern, and its `IsLocallyOwned()` checks are true on the host alone: ```cpp // LanternPost.h #include "Lantern.h" #include "LanternPost.generated.h" UCLASS() class ALanternPost : public ALantern { GENERATED_BODY() public: ALanternPost(); }; // LanternPost.cpp #include "LanternPost.h" ALanternPost::ALanternPost() { // A level-placed post: event-only, the same NetID on every client, and one shared authority, the elected host. CrowdyEntity->Mode = ECrowdyEntityMode::Static; CrowdyEntity->IdentityPolicy = ECrowdyIdentityPolicy::Stable; CrowdyEntity->Ownership = ECrowdyOwnership::Host; } ``` Set Mode, Identity Policy, and Ownership on the component in the Details panel (see the screenshot above). At runtime, `Is Locally Owned` is the question to ask before owner work; on a host-owned post it is true on the elected host and false everywhere else. This graph, in an Actor Blueprint with a **Point Light** named `Light` whose **Visible** box is unticked, lights the post on exactly one client, the host: [Blueprint snippet: identity-policies, see /bp/unreal-sdk/identity-policies.txt] Pan the graph with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. `Get Mode`, `Get Ownership`, `Get Role`, and `Get Net ID` are the component's pure getters for the resolved identity. There is no getter for Identity Policy; it is a design-time setting only. ## Gotchas - `IsLocallyOwned()` is the question to ask. Do not compare ids by hand to decide who is in charge. - A proxy of a Static entity never exists on its owner; the owner has the real thing, and an event it sends never comes back to it. A Dynamic entity's owner may see its own echoed proxy as well; see [Rendering backends](../runtime/rendering-backends.md). - Stable identity depends on level placement. An actor you create in code with Stable set does not agree across clients; spawn it through the SDK instead. - Identity Policy has no Blueprint getter. If a graph needs it, it is probably asking the wrong question; ask `Get Role` instead. ## Related - [The Two Planes](./two-planes.md): which state an owner is allowed to write. - [The Host Is a Convention](./host-is-a-convention.md): what host-owned means. - [Entities and spawning](../runtime/entities-and-spawning.md): spawning at runtime and receiving spawns. # Sessions and Presence Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/concepts/sessions-and-presence # Sessions and Presence "Session" names several things in this SDK, and none of them is a match. A match, a lobby waiting to become one, a room or a table is kept by your app's server code, and every client reads that record rather than keeping its own. :::note[One word, three things.] The **login session** is your signed-in identity. The **session channel** is a transport every client of the app joins (`__crowdy_session_`). A **Crowdy Team** is a persistent group with membership and roles. None of them is a match, and being in one does not make a player part of one. ::: ## Presence is the player's actor There is no heartbeat call. While a client's UDP connection is up and it is replicating an actor in the app, that client is present. The server knows a client by the actor updates it sends: a client with no fresh actor is at no position on the map and receives no spatial event. A headless or API-only client, a test bot with no UDP connection, or a menu-only flow has no actor to be present with. Server code hears presence too: on [ck-exec](/exec/intro), the app's root hub is told who entered or left the app's world. See [presence](/exec/timers-and-presence#presence). The root the Unreal SDK deploys keeps the count, which a game reads with **Get Players Online**; see [How many players are online](../exec/from-blueprint/read-and-follow-variables.md#how-many-players-are-online). ## The session channel The session channel is a shared transport, named `__crowdy_session_`, that every client of the app joins as soon as it connects. Reliable RPC events ride it. You never have to create it; the runtime creates it on demand, and the Channels page in Crowdy Studio can pre-create it so it is visible right away. ## Matches, lobbies and rooms A match is state the game must be able to trust (its roster, its host, whose turn it is, the score), so it lives on the [truth plane](./two-planes.md), in your server code. On ck-exec that is a hub for the match: the `session` [starter pack](/exec/builds#starter-packs) keeps one game from lobby to result, with a host, ready checks, turns with a time limit, and a winner. :::caution[The SDK's Game Model sessions are gone.] The SDK's own session API (Create, Join, Leave and End on the Game Model subsystem, and the active session) was built on the game API's legacy game-model engine, which ck-exec replaced; the game API no longer serves it and the SDK has removed it. A match is a [Server Object](../exec/overview.md) (from Blueprint or C++); see [Move from Game Models](../exec/move-from-game-models.md). ::: ## Gotchas - A client that never spawns an actor is never present. Spawn the player's pawn once the connection is up. - The session channel is not a match and carries no roster. Keep who is playing in your server code. ## Related - [The Two Planes](./two-planes.md): why the server, not your client, keeps the record of a match. - [The Host Is a Convention](./host-is-a-convention.md): what the elected view-plane host does and does not decide. - [Channels](../runtime/channels.md): the session channel and named channels. # The Host Is a Convention Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/concepts/host-is-a-convention # The Host Is a Convention One client of your app is elected host. The host is the natural place to run shared, world-level logic on the view plane: driving world entities, spawning things that belong to nobody. It is a convention the SDK helps you follow, not a rule the server enforces. ## Election is a convention, not enforcement Host election happens server-side, per user, and the SDK tracks the result locally in `UCrowdyHostSubsystem` (`IsHost()`, `GetHostUserID()`, and the `OnHostElected` event). The identity itself lives with your login session, so it survives level travel. What election does not do is protect anything. Nothing on the view plane is server-checked, so any client can lie about its own view state, host or not. A host check answers "who is this app's coordinator right now", never "is this write allowed". :::danger[Never gate anything cheat-sensitive on a host check.] Neither the client-derived check nor the server-validated one below is an enforcement point. Real enforcement is your server code checking who is asking, on the truth plane. Use host checks to decide who runs shared view-plane logic, and nothing else. ::: ## Two checks, two costs **Client-derived, instant.** `Is Crowdy Entity Host` (`UCrowdyUtilities::IsCrowdyEntityHost`) compares the entity's id with the locally known host id. No round trip. Use it for cosmetic and convenience decisions: showing a button, choosing who plays the ambient logic. **Server-validated, one round trip.** `Is Crowdy Entity Host (Server)`, a latent Blueprint node (`UCrowdyIsEntityHostServer`) with `OnIsHost`, `OnIsNotHost`, and `OnFailed` pins, backed in C++ by `UCrowdyHostSubsystem::CheckEntityIsHost`. For the local player's own entity it asks the server directly; for any other actor it resolves that actor's owner on the server and compares with the elected host. Use it when the answer has to be current before you do something a non-host should not trigger, remembering that the trigger itself is still not enforced. :::note[OnFailed means "unknown", not "no".] `bSuccess == false` from `CheckEntityIsHost`, or the `OnFailed` pin, means the answer could not be determined: no host elected yet, a network error, an unresolved entity. It is not the same as "not the host". Do not treat it as a confident no. ::: ## Host-owned world entities A level-placed entity with `Ownership = Host` is `HostOwned`: whichever client is host at the moment owns it, so every client shares one authority for world and AI objects. The host's writes to such an entity are explicit pushes (`MarkStateDirty`, `MarkAllStateDirty`), never a background diff, so an implicit and an explicit write can never race. A client-owned entity (`Ownership = LocalClient`) has a `HostOverride` policy: `Allow` (the default) lets the host correct that entity's view state as a super-user, `OwnerOnly` refuses even a host correction. This is precedence by convention. It orders who wins on the view plane; it does not make either value trustworthy. ## Gotchas - Host changes. Bind `OnHostElected` rather than caching the answer at BeginPlay. - A host-owned entity is written by explicit pushes. Changing a property and waiting does nothing on it. - `HostOverride` is a view-plane tie-breaker. If the value matters, it belongs in your server code and the question does not arise. ## Related - [Host authority](../runtime/host-authority.md): the calls and nodes, with examples. - [Entities, Identity, and Ownership](./entities-identity-ownership.md): owner, proxy, and host-owned roles. - [Sessions and Presence](./sessions-and-presence.md): a match's host, a different thing your server code keeps. - [The Two Planes](./two-planes.md): where enforcement actually lives. # Crowdy Studio Overview Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/studio/overview # Crowdy Studio Overview Crowdy Studio is the management console for your app, and it runs inside the Unreal Editor as a dockable tab. You use it to point your project at an app, and to author the server-side pieces your runtime reads: teams, channels, and grids. Use it when you: - Start a project: sign in, pick the app, and write its ids and endpoints into the project with Config Sync. - Author server-side data ahead of play: teams, channels, grids. - Debug: watch a running Play in Editor session in the Inspector, or check the baked registry. - Manage your own account: your name, your password, and your sessions. :::tip[Start here before you write any runtime code.] Most setup mistakes come from a project pointed at the wrong app, or one that never ran Config Sync. Both are fixed on the Project page. ::: ## Open Crowdy Studio Two ways: - The Tools menu: **Tools, Crowdy SDK, Crowdy Studio**. - The **Crowded Kingdoms** wordmark button on the level editor toolbar, next to Play. The tab is called **Crowdy Studio**. Dock it anywhere, or float it on a second monitor, and it reopens from your saved layout. ![The Home page of Crowdy Studio: account, organization, active app, and project sync status cards](/img/unreal-sdk/studio-home.png) ## Editor-only, one-way Crowdy Studio is an editor module and never ships in a packaged build. It writes settings and server data; the runtime reads those on its own and never talks to Studio. :::info[Studio is editor-only and has no runtime API.] Studio writes three things into your project: the connection settings in `DefaultGame.ini` (Config Sync and the Connection tab), the baked registry asset under `Content/CrowdySDK` (the Registry page), and a per-user session file under `Saved/`. Your game reads the settings and the registry at startup. Everything else Studio authors lives on the server. ::: ## Native pages and the Web Console Studio has two surfaces. **Native pages** are Slate UI built into the editor. They cover the work you do while building: choosing an app, syncing config, checking the app's features and tier grants, authoring teams, channels and grids, and reading live state. **The Web Console** is an embedded browser for account and organization administration: members, billing, tokens, secrets. It signs you in with the session you already have in Studio. The sidebar's bottom button opens it on your organization's overview. ## The sidebar The sidebar on the left lists the pages. A toggle at the top folds it to icons (hover an icon for its label) and remembers your choice. | Page | Group | Needs sign-in | What it is for | |---|---|---|---| | **Setup Wizard** | | No | The guided first run: sign in, pick an app, sync. Studio lands here after you sign in. | | **Home** | | Yes | Status cards: account, organization, active app, project sync. | | **Account** | | Yes, with your account | Your own name, password and sessions; an organization token has no account of its own. See [Your account](./sign-in.md#your-account). | | **Project** | CONFIGURE | Yes | Pick the app, create one, and run [Config Sync](./config-sync.md). See [Projects and Apps](./projects-and-apps.md). | | **Access** | CONFIGURE | Yes | The selected app's feature keys, and which access tiers grant each one. See [The Access page](#the-access-page). | | **Teams** | AUTHORING | Yes | Author teams, members, roles, and the team policy. See [Teams and Channels](./teams-and-channels.md). | | **Channels** | AUTHORING | Yes | Author named channels and the session channel. | | **Grid** | AUTHORING | Yes | Author spatial permission regions. See [Grids](./grids.md). | | **Inspector** | DEBUG | No | A read-only view of a running Play in Editor session. See [Inspector and Registry](./inspector-and-registry.md). | | **Registry** | DEBUG | No | The baked metadata that ships in packaged builds, and the button that rebuilds it. | | **Server Compute** | COMPUTE | Yes | Deploy your Server Object types and run them: versions, switching off and on, restarts. See [Deploy it with Server Compute](../exec/deploy-with-server-compute.md). | | **Web Console** | | Yes | Pinned at the bottom: opens the browser console for admin surfaces. | ## The Access page **Access** sits in the Configure group, under **Project**, and works on the app selected on the Project page. A feature is a key that server logic checks a player's access tier for (`ctx.players().features(player)`, see [World and platform data](/exec/world-and-platform-data)); the tiers themselves are created in the web console. - The list on the left holds the app's features, with a search box. Each row shows the key, its description and how many tiers grant it. - Select a feature to see every access tier on the right, marked **GRANTED** where it has the feature, with a **Grant** or **Revoke** button on each. - **Define feature** opens a form for a new key and an optional description. A key is 1 to 64 letters, digits, underscores, hyphens and dots, the same rule a Server Object's **Required Feature** follows. Defining a key that exists updates its description. The page loads its lists the first time you open it for the selected app, not when Studio starts. Press **Refresh** to read them again. Features work with a sign-in with your account or with an organization token. A Server Object function uses them through [Required Feature](../exec/access-members-and-timers.md#required-feature). From a script, `-op=features`, `-op=definefeature`, `-op=grantfeature` and `-op=revokefeature` do the same work; see [Access, teams, channels and grids](./config-sync.md#access-teams-channels-and-grids). ## Signing in Sign in with your account: email and password, a sign-in link sent by email, or a social provider. That gives you full authoring. An organization token gives management-only access: it can browse apps and run Config Sync, but not author teams, channels or grids. See [Signing In](./sign-in.md). :::caution[If an authoring page looks read-only, check which credential you signed in with.] Token sign-in is the usual cause. ::: ## Where to go next - New project: [sign in](./sign-in.md), then [pick your app](./projects-and-apps.md) and run [Config Sync](./config-sync.md). - Building gameplay: [Entities and Spawning](../runtime/entities-and-spawning.md). - Shipping a build: [Packaging](../guides/packaging.md), and rebuild the [registry](./inspector-and-registry.md) first. # Signing In Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/studio/sign-in # Signing In Crowdy Studio opens on its Sign In page. You sign in once; Studio remembers the session across editor launches until you sign out. ## The sign-in page, top to bottom ![The Sign In page: backend selector, email and password, sign-in link, organization token; the provider buttons appear above the form when the backend offers them](/img/unreal-sdk/studio-sign-in.png) 1. **Backend.** A selector above the form. Leave it at its default unless Crowded Kingdoms told you otherwise. 2. **Continue with ...** buttons, one per social provider your backend offers: Google, GitHub, Discord, Apple, or Microsoft. The section is hidden when none is offered. A provider sign-in opens your system browser, never an embedded web view, and returns to Studio when it completes. 3. **Email** and **Password**, with **Log In**. 4. **Email me a sign-in link.** Uses the email you typed above and sends a one-time link. Click the link in your mail client and this Studio window signs you in, no password needed. 5. **Use an organization token instead.** A toggle at the bottom that reveals a token field and **Sign In with Token**. Options 2, 3, and 4 are all account sign-ins and end in the same place: a session that can author everything. ## What each credential grants **An account sign-in** (email and password, a sign-in link, or a provider) gives you full authoring: Config Sync, teams, channels, grids, and the Web Console. **An organization token** gives management-only access. It can browse apps, run Config Sync, and reach the Web Console, but it cannot author game-plane data. :::caution[An organization token cannot author teams, channels, or grids.] Those pages need an account sign-in. If you signed in with a token and an authoring page looks read-only, sign out and sign in with your account. The token is the right choice for a build machine that syncs config and must not hold authoring credentials. ::: After a successful sign-in Studio lands on the Setup Wizard, so the guided path is the first thing you see. ## Agreeing to the terms When Studio asks the server for its access to your app and the account has not yet agreed to the current terms and age confirmation, Studio opens a small window titled **Crowdy Studio: terms and age** over the editor. It is the same prompt a player sees in your game: the documents as links, two check boxes, **Accept** and **Decline**. **Accept** carries on where Studio left off. **Decline**, or closing the window, stops there and Studio reports `Studio needs the terms and the age confirmation accepted for this account.` You see the prompt again only when a document gets a new version. How the same prompt works in a game, and how to draw your own, is on [Authentication](../services/authentication.md#let-the-player-agree-to-the-terms). ## Where the session is kept Studio stores the signed-in session in a per-user file under your project's `Saved/CrowdyStudio/` folder, never under `Config/` and never in anything you would commit. The stored record remembers whether it was an account or a token sign-in, so a remembered account session comes back with its full authoring rights. :::info[On Windows the stored session is encrypted for your user account with the operating system's data protection (DPAPI). On other platforms it is written in the clear, and Studio warns about it.] Treat the `Saved` folder on a shared macOS or Linux machine as you would a credential file. ::: ## Your account The **Account** page, under **Home** in the sidebar, is for the account Studio is signed in with: yours, not a player's. It needs an account sign-in; an organization token has no account of its own, and the page says so. **Refresh** reads your name and email again, and **Web console** opens your account on the web, where sign-in methods and connected apps are managed. ![The Account page: the name card with email, gamertag and disambiguation, then the Password tab](/img/unreal-sdk/studio-account.png) - **The name card** shows your gamertag, its disambiguation and your user id (with a copy button), and your email. **Edit name** turns it into two fields; **Save** changes both, and a pair another account has is refused. - **Password**: pick **I have a password** to change it, with your current password and a new one; your other devices are signed out while Studio stays signed in. Pick **I don't have one yet** to add one to an account made by magic link or social sign-in; it works at once, and a security email goes to your address. - **Sessions**: **Sign out everywhere** ends every session of your account, on every device and in every game, Studio's included, and returns Studio to the Sign In page. What a player can change from inside your game is on [Authentication](../services/authentication.md#account-settings) and [Player Names](../services/player-names.md). ## Signing out Press **Sign Out** in the Studio header. It ends the session on the server, clears the stored session, clears the Web Console's browser session too, and returns you to the Sign In page. A token sign-in is only forgotten: the token stays valid, since others may use it. ## Gotchas - The provider buttons come from the backend. A provider you expect but do not see is not enabled for that backend. - A sign-in link arrives by email. Check the address you typed if nothing comes. - Token sign-in is silent about what it cannot do until you open an authoring page. The page subtitle tells you it needs an account sign-in. - The backend selector is remembered with the project. Changing it changes which servers Config Sync writes into the project. ## Related - [Projects and Apps](./projects-and-apps.md): pick or create the app after signing in. - [Config Sync](./config-sync.md): write the app's ids and endpoints into the project. - [Crowdy Studio overview](./overview.md): the sidebar and what needs a sign-in. # Projects and Apps Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/studio/projects-and-apps # Projects and Apps An **app** is one Crowded Kingdoms backend application: it has an id, an organization, a datacenter, a status, a visibility, and Game API endpoints. Your **project** is this Unreal project. The Project page points the project at exactly one app, then [Config Sync](./config-sync.md) writes that app's ids and endpoints into it. :::note[Since 2.14] The Project page is a single split view: the app list on the left, the selected app's details and its Configuration and Connection tabs on the right. ::: ## The Project page Open Crowdy Studio, sign in, and pick **Project** in the sidebar (the CONFIGURE group). ![The Project page: organization filter and app list on the left, the selected app's details and Configuration tab on the right](/img/unreal-sdk/studio-projects.png) The left rail: - An **organization** combo box, with **All organizations** at the top. This is a filter for the list; it does not by itself change which organization new resources are created in. - A **Search apps** box. - A status strip: **All**, **Live**, **Draft**, **Archived**. It opens on All. - The app list, each row showing the app's name, its URL identifier, and its status. The count under the list says how many apps match, and how many free app slots the organization has used when one organization is selected. :::tip[Picking an organization in the filter narrows the list.] It does not move where a new app is created until you also pick or create an app there. ::: Select an app and the right pane shows its details. ## The app details The header card shows the app's name, a status pill (Draft or Live), a visibility pill (Private, Unlisted, or Public), a copyable app id, a copyable URL identifier, the organization, and creation and update dates. Below it, a fact grid: **Datacenter**, **Runtime** (the app's current runtime status), **Deployment**, **Game API URL**, **Game API WS URL**, **Org ID**, **Reserved UDP** (bytes per second), and **Reserved GraphQL** (operations per second). The action row: - **Edit details**: rename the app or change its description inline. - **Status**: Draft or Live. - **Visibility**: Private, Unlisted, or Public. - **Web console**: opens the organization's Apps tab in the browser console. - **Archive**: reversible. :::caution[Create, Edit details, Status, Visibility, and Archive need the manage_apps permission on the organization.] Without it the controls are disabled. That is a permission on your account or token, not a Studio setting. ::: Under the details are two tabs, **Configuration** and **Connection**, which are the subject of [Config Sync](./config-sync.md). ## Create an app Press **Create app** in the page header. It needs an organization to create in: pick one in the filter, or have an app of that organization selected. The dialog opens inside the page in two steps. ![The Create app dialog, step 1: name, URL identifier preview, datacenter cards, advanced options](/img/unreal-sdk/studio-create-app.png) **Step 1, Name your app.** Type a name; the URL identifier (lowercase letters, digits, hyphens) is derived from it, and **Advanced options** lets you override it and add a description. Pick a datacenter card. The first available datacenter is preselected. **Step 2, Review.** The name, identifier, and organization, the datacenter with its permanence notice, and the organization's free app slots. Press **Create app**. The Game API connection is available immediately; there is no provisioning wait. :::note[The datacenter is chosen once, at creation, and cannot be changed later.] Everything the app stores lives in that one datacenter. Pick the one closest to your players. Status and visibility are not asked at creation; the server defaults them and the details pane changes them afterwards. ::: ## Gotchas - The organization filter and the selected app are separate things. Picking an app never changes the filter; picking an organization is remembered. - The strip opens on All. Pick Draft to see only unpublished apps. - The URL identifier must be lowercase letters, digits, and hyphens; the dialog refuses anything else. - Archive is reversible, but an archived app disappears from Live and Draft. Look under Archived. ## Related - [Config Sync](./config-sync.md): write the selected app into the project. - [Signing In](./sign-in.md): the account or token you need. - [Crowdy Studio overview](./overview.md). # Config Sync Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/studio/config-sync # Config Sync Config Sync writes the selected app's ids and endpoints into your project's settings, so connecting a game to an app is one button instead of copying ids and URLs by hand. It lives on the Project page's **Configuration** tab. :::caution[If you never run it, the SDK has no app id or Game API URLs to connect with, and nothing replicates.] ::: ## Run a sync 1. Open Crowdy Studio, [sign in](./sign-in.md), and pick **Project** in the sidebar. 2. Select the app the project should talk to. 3. Open the **Configuration** tab under the app's details. It shows every value the sync would write. A value that differs from what the project holds is drawn in gold with the current value on a "was" line beneath it, and a badge beside the tab strip reads **IN SYNC** or **N TO CHANGE**. 4. Read the diff, then press **Sync to project**. ![The Configuration tab with a pending change: gold values, the N TO CHANGE badge, and the Sync to project button](/img/unreal-sdk/studio-config-sync.png) The rows are **App ID**, **Org ID**, **Game API HTTP URL**, **Game API WS URL**, and a read-only **Shared origin**, which comes from the backend selector on the Sign In page rather than from the app. :::tip[A running Play in Editor session picks the change up without a restart.] Sync pushes the new values into any running PIE or standalone session; a fresh Play always reads them. The **Connection** tab works differently: its UDP knobs and its Server Object timeouts are written the moment you change them, with no Sync button. The UDP knobs reach a running session at once; the Server Object timeouts apply from the next Play. ::: ## Sync from the command line For a script or a coding agent, the same steps run without the editor window. [Sign in](./sign-in.md) to Crowdy Studio on the machine once; the commands use that sign-in, and no token goes on the command line: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyStudio -op=apps ``` | Operation | What it does | |---|---| | `-op=apps` | Reads, and changes nothing. The account's organizations and their apps, each with its id, name, slug, datacenter, status and whether it is archived; the datacenters an app can be placed in; who is signed in; and the project's current Network settings. | | `-op=createapp -org= -name="My Game" -slug=my-game -datacenter= [-description="..."] -yes` | Creates an app, as the Create app sheet on the Project page does, and prints the new app's id. The organization must be one of the account's, and the datacenter one of the codes `-op=apps` lists, written with the same capitals. The datacenter cannot be changed later. | | `-op=sync -app= -yes` | Writes the app's id, its organization and its game endpoints into `Config/DefaultGame.ini`, exactly what **Sync to project** writes. `Environment` and the shared origin stay as they are. An app the project is already synced to succeeds without `-yes` and changes nothing. A write that fails, for example because the file is read only, fails the operation. | | `-op=registry [-class=] [-norebuild] [-out=]` | Rebuilds the baked registry, then prints every entity class's CrowdyEvents (recipient, decay, distance) and CrowdyState properties (owner-only, manual dirty, heartbeat, OnRep) as JSON. `-class=` keeps only the classes whose path contains the text; `-norebuild` reads the saved registry as it is. It needs no sign-in. It needs `-yes` only when the project has no registry asset yet and the rebuild would create it. See [Inspector and Registry](./inspector-and-registry.md). | `createapp` and `sync` only say what they would do, and exit with code 1, until you pass `-yes`. Add `-out=` to get the result as JSON. The sign-in token is never printed. `sync -yes` refuses while an Unreal Editor is running, since the editor holds its own copy of the settings and would write it over the synced app. Close the editor first, or pass `-editoropen` to write anyway. A typical run on a new project: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyStudio -op=apps -out=Saved/apps.json UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyStudio -op=createapp -org=1234 -name="Oakford" -slug=oakford -datacenter= -yes UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyStudio -op=sync -app= -yes ``` The [Server Compute](../exec/deploy-with-server-compute.md#from-the-command-line) operations then use the app you synced; its `-op=whoami` checks the sign-in and the app. The command line never changes which app Crowdy Studio opens on in the editor. ### Access, teams, channels and grids The same commandlet does what the Access, Teams, Channels and Grid pages do, so a script can set up an app without the editor window. Every operation below follows the same rules: - It runs against `-app=`, or against the app the project is synced to when you leave `-app` out. The result says which app it used. - It needs `-yes` to change anything. Without it, it says what it would do, changes nothing and exits with code 1. - When nothing would change, it succeeds and the result says `changed:false`. - After a write it reads the result back from the server to check it, and fails if the server does not hold what was written. - A list of values is written with commas and no spaces: `-keys=a,b`. Quotes are not needed (the engine drops them from a value with no spaces). A list with a space after a comma, `-keys=a, b`, is refused rather than cut short. **Access.** See [The Access page](./overview.md#the-access-page). A feature key is 1 to 64 letters, digits, `_`, `-` and `.`. | Operation | What it does | |---|---| | `-op=features` | Reads, and changes nothing. The app's feature keys. | | `-op=definefeature -key= [-description="..."] -yes` | Defines a feature key, like **Define feature**. | | `-op=grantfeature -tier= -key= -yes` | Grants the feature to an access tier, like **Grant**. The key must be defined first. Needs `manage_apps`. | | `-op=revokefeature -tier= -key= -yes` | Takes it away again, like **Revoke**. The same rules. | **Teams and channels.** See [Teams and Channels](./teams-and-channels.md#from-the-command-line). These need a sign-in with your account, not an organization token. `-kind=` is `team` or `channel`, `-group=` the team's or channel's id, and `-user=` a user id. | Operation | What it does | |---|---| | `-op=teams`, `-op=channels` | Read the app's teams or channels. | | `-op=groupdetail -kind=team -group=` | Reads one team or channel, with its members and their roles. | | `-op=createteam -name="Oakford Guild" [-description="..."] [-policy=] -yes` | Creates a team. `-policy=` is `open`, `request`, `invite` or `admin`. | | `-op=createchannel -name=oakford-chat [-description="..."] [-policy=] [-memberscansend=] -yes` | Creates a channel. | | `-op=updategroup -kind= -group= [-name=] [-description=] [-policy=] -yes` | Changes a team or channel. | | `-op=deletegroup -kind= -group= -yes` | Deletes it, with its members and roles. This cannot be undone. | | `-op=addmember`, `-op=removemember` with `-kind= -group= -user= -yes` | Adds a user to a team or channel, or removes them. | | `-op=setroles -kind= -group= -user= -roles=1,2 -yes` | Sets a member's roles by role id. `-noroles` in place of `-roles=` clears them. | | `-op=createrole -kind= -group= -name= [-perms=manage_members,invite_members] [-rank=] -yes` | Creates a custom role. A team role can grant `manage_group`, `manage_members`, `manage_roles` and `invite_members`; a channel role can also grant `send_messages`, which decides who may post. `-op=updaterole` and `-op=deleterole` change and delete one. | | `-op=setteampolicy`, `-op=setchannelpolicy` | Write the app-wide policy, like **Set Policy**. They take `-creation=` (`admin`, `member` or `anyone`), `-membership=`, `-maxmembers=` and `-maxgroupsperuser=`, all optional, and `-yes`. 0 for a maximum means unlimited. | Creating a team or channel whose name already exists succeeds with `changed:false` when the existing one matches what you asked for, and fails naming the differences when it does not. **Grids.** See [Grids](./grids.md#from-the-command-line). Corners and regions are chunk coordinates written `x,y,z`, with no spaces. | Operation | What it does | |---|---| | `-op=grids -low=x,y,z -high=x,y,z [-user=]` | Reads the grids a region touches, like **Scan for grids**. | | `-op=creategrid -corner1=x,y,z -corner2=x,y,z -yes` | Creates a grid, like **Create grid**. A grid cannot be deleted from the command line. | | `-op=grantgrid -grid= -user= -keys=a,b -yes` | Grants permission keys on a grid to a user. Give `-group=` and an optional `-role=` in place of `-user=` to grant them to a team or channel; the group must be one of the app's teams or channels. | | `-op=revokegrid -grid= -user= -keys=a,b -yes` | Takes keys away again, with `-group=` in place of `-user=` for a group. `-all` in place of `-keys=` takes all of them. Checked by reading the keys back. | | `-op=gridlimits -grid= -keys=a,b -yes` | Sets the grid's permission whitelist. `-nolimit` in place of `-keys=` removes the limit. | | `-op=gridgrants -grid= -group=` | Reads a group's grants on a grid. | | `-op=gridpermissions -grid= [-user=]` | Reads the permissions a user has on a grid. | ## What Config Sync writes, and what you own All of it lands on one settings class, `UCrowdySDKDeveloperSettings`, which you can see under **Project Settings, Plugins, Crowdy SDK**. The network fields are shown there read-only. ![Project Settings, Plugins, Crowdy SDK after a sync: the network fields filled in and greyed out](/img/unreal-sdk/dev-settings-after-sync.png) **Written by Crowdy Studio** (category Network; read-only in Project Settings): | Property | Written from | |---|---| | `Environment` | The backend selector on the Sign In page. | | `DiscoveryUrl` | The shared origin, used when the backend is Custom. | | `AppID` | The selected app. | | `OrgId` | The selected app's organization. | | `GameApiHttpUrl` | The app's own routing. | | `GameApiWsUrl` | The same endpoint with a WebSocket scheme. | | `UDPProtocol` | The Connection tab. | | `UDPTimeoutSeconds` | The Connection tab (6 to 120 seconds). | | `HostPollIntervalSeconds` | The Connection tab (1 to 60 seconds). | | `ServerObjectCallTimeoutSeconds` | The Connection tab, under Server Objects, as **Call Timeout** (11 to 120 seconds, default 20). | | `ServerObjectOpenTimeoutSeconds` | The Connection tab, under Server Objects, as **Open Timeout** (1 to 60 seconds, default 10). | Under a **Server Objects** heading, the Connection tab holds two timeouts for [Server Object](../exec/overview.md) connections: - **Call Timeout** is how long a Server Object call waits for the platform's answer before it fails with a `Timeout`. It runs from 11 to 120 seconds, 20 by default, and stays above the longest **Time per Call** a type may have (10 seconds), so a call the server is allowed to finish is not given up on. A **Call** node's **Timeout** can shorten it for one call; see [Give up sooner](../exec/from-blueprint/call-functions.md#give-up-sooner). - **Open Timeout** is how long opening the Server Object connection waits before the attempt fails, from 1 to 60 seconds, 10 by default. Both apply from the next Play, and to the editor's developer connections. A dropped Server Object connection is always reopened, and has no setting. **Owned by you** (editable in Project Settings): | Property | What it does | |---|---| | `MapProfiles`, `DefaultProfile` | Per-map SDK configuration and the fallback when a map has no entry. See [Map profiles](../runtime/map-profile.md). | | `BakedRegistry` | The cooked snapshot of Crowdy metadata. See [Inspector and Registry](./inspector-and-registry.md). | | `PreloadedEntityClasses` | Entity classes to preload so a class nobody has spawned yet can still resolve off the wire. | | `IDOverrides`, `ClassIDOverrides` | Populate only when the log reports a type id hash collision. | :::warning[Never hand-type the network settings.] They are read-only in Project Settings on purpose, so that Crowdy Studio is the one source of truth. A value pasted into `DefaultGame.ini` by hand is overwritten by the next sync, and a project whose settings drift from its app fails to connect with no obvious error. ::: ### The runtime setters The same goes for the two runtime setters on `UCrowdySDKSubsystem`: `SetDiscoveryUrl` stores a shared origin for this session and is read only when `Environment` is Custom (it warns otherwise), and `SetGameApiUrl` stores one Game API HTTP endpoint for this session, an address that is normally resolved from the origin rather than typed. `ReloadEndpointsFromSettings` is deprecated and does nothing, so remove it from your graphs and code; it is kept only so existing call sites still compile: the API client re-reads both endpoints from the settings on every call, so a sync already reaches a running session. `RequestVersionInfo` asks the server for its version before or after sign-in and answers on `OnVersionInfo` (`FOnVersionInfo`, two `FGameVersion` parameters: the server's version and, in `ClientVersion`, the minimum client version it still accepts). Both arrive as zeroes when the query fails. ## Where the settings land The class is `Config=Game`, so the values are written to `Config/DefaultGame.ini` under: ```ini [/Script/CrowdyReplication.CrowdySDKDeveloperSettings] ``` Commit that file as usual; it holds ids and URLs, not secrets. ## When to sync again The **N TO CHANGE** badge tells you. It goes non-zero whenever the values the selected app would write differ from what the project holds: you selected a different app, changed the backend selector, or the app's endpoints moved. Press **Sync to project** again. Updating the plugin does not require a sync: the values live in `DefaultGame.ini` and the badge stays **IN SYNC**. Sync again only when the badge says so. :::warning[Config Sync does not decide what a map does; the map profile does.] Config Sync points the project at an app. A map with no [map profile](../runtime/map-profile.md) of its own runs on the SDK's shipped default; a `MapProfiles` row or `DefaultProfile` naming an asset that fails to load resolves to no profile, and on that map the entity subsystem, the auto replicator, and the actor manager do nothing, so replication looks dead even though the connection is fine. The warning in the log names the asset. ::: ## The Setup Wizard If you prefer a guided path, the **Setup Wizard** page walks through the same steps in order: sign in, pick an organization and app, run the sync. Studio lands on it after you sign in. Once you know the flow, the Project page does each step directly. ## Gotchas - The Configuration tab writes on **Sync to project**. The Connection tab writes on every edit. - The shared origin is not per app. Change it on the Sign In page's backend selector, then sync. - A PIE session that was already running picks up a sync; a packaged build needs the new `DefaultGame.ini` cooked in. - An organization token is enough to run Config Sync. It is not enough to author teams, channels, or grids, from the pages or from the command line. - A Studio sign-in with an organization token is cleared only when the server rejects the token. A network error no longer signs you out. ## Related - [Projects and Apps](./projects-and-apps.md): picking and creating the app. - [Map profiles](../runtime/map-profile.md): the second half of "make replication work". - [Project settings reference](../reference/project-settings.md): every property on the settings class. # Teams and Channels Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/studio/teams-and-channels # Teams and Channels Teams group players together: guilds, parties, factions. Channels are named groups your runtime sends reliable events and messages over. Both live on the game server, and the Teams and Channels pages in Crowdy Studio author them ahead of play. The same create, update, delete, role, and policy operations exist at runtime through `UCrowdyTeams` and `UCrowdyChannels`. Use Studio to set things up ahead of time or to do admin work in the editor; use the runtime APIs when your game creates teams or channels on the fly. The runtime side is on the [Teams](../services/teams.md) and [Channels](../runtime/channels.md) pages. :::note[Authoring teams and channels needs an account sign-in.] Sign in with your account (email and password, a sign-in link, or a provider). An organization token gives management-only access and these pages stay empty. The page subtitle says so when that is the case. ::: ## Open the pages Open Crowdy Studio (**Tools, Crowdy SDK, Crowdy Studio**), sign in, and select your app on the Project page. Then pick **Teams** or **Channels** in the AUTHORING group of the sidebar. Press **Refresh** if the list is empty. ## Teams ![The Teams page: the team list with Refresh, the Create team card, and the Team policy card](/img/unreal-sdk/studio-teams.png) The page has three cards plus the detail view, top to bottom: the list, the detail view for the selected row, Create team, Team policy. **Teams.** Every team in the app, one row each: the name, an id chip, the team's membership policy, and a status badge. Select a row to open its detail view. **Create team.** A name, an optional description, and a **Default membership** choice (Open, Request, Invite, or Admin) that decides how players enter this team at runtime. Press **Create Team** and the row appears in the list. **Team policy.** The app-wide rules for teams: - **Creation policy**: who may create a team. Admin, Member, or Anyone. - **Default membership**: the membership policy a new team starts with. Open, Request, Invite, or Admin. - **Max members per team** and **Max teams per user**. Blank means unlimited. Press **Set Policy** to write it. The current policy is echoed above the controls once loaded; before a Refresh it reads "Current policy not loaded". ## Channels ![The Channels page: the channel list, the Create channel card, and the Channel policy card](/img/unreal-sdk/studio-channels.png) The Channels page mirrors Teams: a **Channels** list, a **Create channel** card with a name, a description, and a **Members can send** checkbox (on by default), and a **Channel policy** card with the same four settings as the team policy. :::caution[A channel name authored here is the exact string gameplay code references.] It is the value of the `CrowdyChannel` meta on a Multicast `CrowdyEvent`, and the target of a raw channel publish. Keep the name stable once code uses it. ::: ### The session channel Every client of the app joins one reliable channel on connect, named `__crowdy_session_`. Multicast events with no channel named ride it. The runtime creates it on demand, so you never have to. The Channels page has a **Create session channel** button that creates it ahead of time with the settings it wants: creation by members, open membership, members can send. The policy card repeats those three settings as a hint. :::tip[Create the session channel once per app if you want to see it in the list right away.] If you do not, the runtime still creates it the first time a client connects. Either way it behaves like any other channel you author here. ::: ## The detail view Select a team or channel row and the detail view opens beneath the list. It is the same widget for both. **Edit team** (or **Edit channel**): name, description (blank keeps the current one), a membership choice with **Keep** as the default, and **Save Changes**. **Members.** Add a member by user id, remove one, and see who is in. A pending join request shows **Approve** and **Reject**; an active member shows **Remove**. Select a member to edit their roles. **Roles.** A role is a named, ranked permission set you assign to members. Create one with a name, a rank (higher is more senior), and a checklist of the permissions its kind of group has. A team's are **Manage Team**, **Manage Members**, **Manage Roles** and **Invite Members**; a channel's are **Send Messages**, **Manage Channel**, **Manage Members**, **Manage Roles** and **Invite Members**. A role badged `system` (the leader role, for example) is read-only; a `custom` role can be edited and deleted. :::note[Group roles are membership permissions, not gameplay permissions.] The keys above decide who may manage the team or channel, who may invite to it and who may send on a channel. The spatial permission keys a grid checks (see [Grids](./grids.md)) are a separate catalog. A team can be granted grid permissions, but the two lists never mix. ::: **Danger zone.** Delete the whole team or channel. Deleting a team removes all its members and roles and revokes any grid grants it conferred, and it cannot be undone. ## From the command line A script can do the same work without the editor window: create and delete teams and channels, add and remove members, set roles, and write the policies. It needs a sign-in with your account, and every change needs `-yes`. The operations are listed under [Access, teams, channels and grids](./config-sync.md#access-teams-channels-and-grids) on the Config Sync page. ## What a policy is A **policy** is four settings that apply to every team (or channel) in the app: who may create one, the membership policy a new one starts with, and two optional caps. A **membership policy** on an individual team or channel decides how a player gets in: Open (anyone joins), Request (a member approves), Invite (a member invites), or Admin. Runtime joins go through `UCrowdyTeams::JoinTeam`, and the server applies the policy you set here. ## Gotchas - Nothing loads until an app is selected on the Project page. The list stays empty otherwise. - Refresh is manual. After another client changes a team at runtime, press it to see the change. - Deleting a team also deletes its grid grants. Check the Grid page first if the team carried any. - A `system` role cannot be edited or deleted. Create a `custom` role for anything you want to change later. ## Related - [Teams](../services/teams.md): join, query, and read membership at runtime. - [Channels](../runtime/channels.md): send channel messages and receive them at game time. - [Grids](./grids.md): grant a team permissions inside a region. # Grids Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/studio/grids # Grids A grid is a region of your world, in chunk coordinates, that carries a permission whitelist and per-user or per-team grants. When a client acts inside the region, the server checks the grid and allows or refuses the action. You author grids on the **Grid** page in Crowdy Studio. ## Grids are server-side only - Every grid operation requires the `manage_apps` permission on the organization. - There is no player-scoped grid query. Game code cannot list or read grids at runtime, and there is no runtime grid API in the SDK. The one runtime call that touches grids is `Request Teleport Permission` on the Crowdy SDK subsystem, which asks the server whether the player may enter a chunk and answers on `On Teleport Permission` (`OnTeleportPermission`, an `FOnTeleportPermission` with one `bAllowed` parameter). - Enforcement is server-side. When a client tries to act inside a region, the server checks the whitelist and the grants, then allows or rejects. The client never receives the grid definition. :::warning[Do not try to drive grid operations from gameplay code.] The only surface is this Studio page. Sign in with your account; the page's subtitle tells you if the credential you used is not enough. ::: ## Open the Grid page Open Crowdy Studio, sign in, select your app on the Project page, then pick **Grid** in the AUTHORING group of the sidebar. ![The Grid page: Create grid, Create grid from selection, Scan for grids, Visualize in viewport, and Effective permissions cards](/img/unreal-sdk/studio-grids.png) The page is a column of cards. Top to bottom: ## Create grid Type the two corners of the region (**Corner 1** and **Corner 2**, each X, Y, Z in chunk coordinates) and press **Create**. **Reset** clears the boxes. Use this when you know the bounds you want, for example a region that lines up with a named area of your level. ## Create grid from selection Select one or more actors in the level, then press **Create grid from selection**. Studio derives the region from the chunks the actors occupy. The **Map by actor location (one chunk each)** checkbox chooses between mapping each actor to the single chunk it stands in (on, which matches the runtime) and covering every chunk the actor's collision bounds touch (off). With no session running, the editor uses the preview grid size from the Visualize card below, which may differ from a session's configured chunk size. ## Scan for grids Grids have no list-all query. To discover the ones that exist, type a user id and a region (**lowX** to **highZ**) and press **Scan**. The results list shows every grid that region touches; select one to populate the **Selected grid** card. Scanning also loads the app's runtime permission catalog, which the whitelist and grants below pick from. ## Visualize in viewport Tick **Show grids in viewport** to draw debug boxes in the level, in the editor and in Play in Editor: red for the grid you are about to create (from the corners above), cyan for the existing grids from the last scan. **Preview grid size** scales those boxes at edit time only. The real chunk size is set on the game session at runtime, and while Play in Editor runs the live session's size is used instead. ## Effective permissions (what-if) Pick a user id and an access tier to simulate (the dropdown reads **No tier (grants only)** by default), then press **Simulate**. The card lists every permission key the user would have on the selected grid and where each comes from: the tier, or a grid grant. The grid's whitelist is applied last: effective permissions are the tier's keys plus the user's grants, restricted to the whitelist. Admins bypass all of this on the server. ## Selected grid Once a scan result is selected: - **Permission whitelist**: a checklist of runtime permission keys. Nothing checked means no restriction. - **User grants**: grant, revoke, or read a user's keys on this grid. - **Group grants**: assign a team (optionally with a role) to a set of keys, with an expiry shown per grant. **List Grants** reads the current ones. :::note[Teams themselves are authored on the Teams page.] A group grant references a team by id. Create the team first; see [Teams and Channels](./teams-and-channels.md). Deleting a team revokes every grid grant it conferred. ::: ## From the command line A script can scan for grids, create one, grant and revoke keys for a user or a team, set a grid's whitelist, and read the grants and effective permissions, all without the editor window. Every change needs `-yes`. The operations are listed under [Access, teams, channels and grids](./config-sync.md#access-teams-channels-and-grids) on the Config Sync page. ## Gotchas - There is no list of all grids. Scan a region to find them, and scan again after you create one. - Chunk coordinates, not world units. The preview grid size only affects the debug boxes. - An empty whitelist means no limit, not no access. - The what-if simulation reads the grants and whitelist from the server; it is a preview of what the server would decide, not a change. ## Related - [Crowdy Studio overview](./overview.md) - [Teams and Channels](./teams-and-channels.md) # Inspector and Registry Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/studio/inspector-and-registry # Inspector and Registry The two pages in the sidebar's DEBUG group. Neither needs a sign-in. - The **Inspector** is a read-only view of the running Play in Editor session. - The **Registry** shows the baked metadata a packaged build reads, and rebuilds it. Open Crowdy Studio (**Tools, Crowdy SDK, Crowdy Studio**) and pick either page in the sidebar. ## Inspector The Inspector reads the live state of the current Play in Editor session and shows it as text. Nothing here changes the game. Press **Refresh** after the session connects or fetches data, or tick **Auto-refresh** and pick an interval (1, 2, or 5 seconds). :::note[Start a Play in Editor session first.] With none running the page reads "No running session". ::: What it lists: - **Session**: the app id, your user id, the game token id, your UUID, the host id, and whether a game token is held (never its value). - **Teams (N)**: the teams the local player is in, from the runtime cache. Until the game has fetched teams it says so. - **Channels (N)**: the channels the client has joined, whether the reliable RPC channels are ready, and the session channel's id. - **Avatars (N)**: the local player's avatars, from the cache. - **Entities**: the local player id, the host id, and the entities this client owns, by actor name. There is no every-entity list; the owned slice is the one that confirms spawns and ownership while you debug. ![The Inspector page with no session running](/img/unreal-sdk/studio-inspector.png) :::tip[If the Inspector is empty while you are playing, check the map profile.] A [map profile](../runtime/map-profile.md) row that names an asset that failed to load resolves to no profile; the entity subsystem then does nothing on that map and the session looks dead. The log warning names the asset. A map with no row runs on the shipped default. ::: To change teams, channels, or entity state, use the runtime APIs in your game: [Teams](../services/teams.md), [Channels](../runtime/channels.md), [Entities and Spawning](../runtime/entities-and-spawning.md). ## Registry Every marker you write (`CrowdyEvent` and its routing keys, `CrowdyState` and its keys) is Unreal metadata, and a cooked build strips metadata. The SDK therefore bakes what it needs into an asset, `UCrowdyBakedRegistry` at `/Game/CrowdySDK/CrowdyBakedRegistry`, and a packaged build reads that instead. The editor and Play in Editor read live metadata, so the page can look stale until you rebuild. ![The Registry page: Rebuild (Deep Scan), Refresh View, the summary pills, and one card per class](/img/unreal-sdk/registry-panel.png) :::warning[Shipped code never reads metadata. It reads the baked registry.] If your own game code reads a Crowdy marker at runtime, read it through the registry, never through `HasMetaData`; in a packaged build the metadata is gone and the call returns nothing. ::: ### The two buttons **Rebuild (Deep Scan)** regenerates the registry from every C++ class and Blueprint in the project, then refreshes the view. It is the authoritative bake, and packaging runs the same thing automatically at cook time. It runs asynchronously (assets stream in; the editor does not freeze) and shows "Rebuilding registry..." while it works. If the button is disabled, the CrowdySDK editor module is not loaded. **Refresh View** re-reads the already-baked asset and redraws the page without re-baking anything. Use it after something else changed the asset on disk, such as a cook. :::note[Rebuild is optional for a correct package.] The cook rebakes on its own, so a packaged build is right even if you forget. Press Rebuild to verify the bake before a long cook, or to bring the in-editor view up to date. ::: ### What the page shows Three summary pills: classes, functions, and replicated properties. A search box filters by class, function, or property name, and a **Cards** / **Table** toggle picks the layout. In Cards view, each class is an expandable card with an **RPC FUNCTIONS** section and a **REPLICATED PROPERTIES** section. Expand an RPC function for its function id, recipient, decay rate, distance, whether its parameters are plain data, and whether it is replicated. Expand a replicated property for its property id, layout order, owner-only flag, manual update flag, and rep-notify function. The class header shows a layout hash and, where the class has an entity component, its Ownership and Host Override, read live from the class defaults rather than baked. In Table view the same data is one flat table: type, name, routing or notify, flags, id. To read the baked registry from the console instead, type `crowdy.registry.dump` in a non-shipping build. It logs every entry the cook baked in, one line each: the structs and their layout hashes, the CrowdyState properties, and the CrowdyEvent functions, with their ids. See [Console variables and commands](../reference/console-cvars.md). ### When to press Rebuild - After you add or change a `CrowdyEvent` receiver, or its recipient, channel, or other routing key. - After you change a container or any Crowdy-tagged struct. - Before you cook, if you want to check the bake first. :::note[The baked asset is generated.] It is rewritten by every Rebuild and every cook. Ignore it in your version control rather than committing it. ::: For the full packaging flow, see the [Packaging guide](../guides/packaging.md). ## Gotchas - The Inspector's team, channel, and avatar lists come from the runtime caches. An empty list with a "not populated yet" note means the game has not fetched, not that there is nothing. - Refresh View does not rebake. If a marker you just added is missing, press Rebuild. - Ownership and Host Override on a class card are live values, not part of the bake. ## Related - [Packaging](../guides/packaging.md) - [Crowdy Studio overview](./overview.md) # Player Sign-in Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/player-sign-in # Player Sign-in Nothing replicates until a player is signed in. Once sign-in succeeds the SDK opens the realtime connection itself, so this is the one step every page in this section assumes has already happened. This page shows the most basic way to do it and nothing more. ## When you touch this Once per project, at startup, before any entity, event, or property. ## Sign in with a password The Game Instance signs in from `Init`, after `Super::Init()` has created its subsystems: bind **On Login** and **On UDP Connection Success** on the Crowdy SDK Subsystem, then call `Login` with an email and password read from `Config/DefaultGame.ini`. The first handler reports the sign-in; the second fires when the connection is up. ```cpp // LanternGameInstance.h #include "Subsystem/CrowdySDKSubsystem.h" #include "LanternGameInstance.generated.h" UCLASS(Config = Game) class ULanternGameInstance : public UGameInstance { GENERATED_BODY() public: virtual void Init() override; // A development account read from Config/DefaultGame.ini. Never ship credentials in a game. UPROPERTY(Config) FString Email; UPROPERTY(Config) FString Password; private: UFUNCTION() void HandleLogin(bool bSuccess, FString Message); UFUNCTION() void HandleConnected(); }; // LanternGameInstance.cpp #include "LanternGameInstance.h" void ULanternGameInstance::Init() { Super::Init(); UCrowdySDKSubsystem* Sdk = GetSubsystem(); Sdk->OnLogin.AddDynamic(this, &ULanternGameInstance::HandleLogin); Sdk->OnUDPConnectionSuccess.AddDynamic(this, &ULanternGameInstance::HandleConnected); Sdk->Login(Email, Password); } void ULanternGameInstance::HandleLogin(bool bSuccess, FString Message) { UE_LOG(LogTemp, Log, TEXT("Login %s %s"), bSuccess ? TEXT("ok") : TEXT("failed:"), *Message); } void ULanternGameInstance::HandleConnected() { // From here on entities register on the wire and a host is elected. UE_LOG(LogTemp, Log, TEXT("Connected to the app")); } ``` Make it the project's Game Instance under **Project Settings, Maps & Modes, Game Instance Class**, and give it a test account in `Config/DefaultGame.ini` (`YourModule` is your game module's name): ```ini [/Script/YourModule.LanternGameInstance] Email=you@example.com Password=your-test-password ``` :::caution[A test account belongs in a config file or a pin default, never in shipped code.] A package stages `Config/DefaultGame.ini`, so an account written there ships with the game. Remove the `Email` and `Password` lines before you package, and sign players in from your own sign-in screen instead. ::: Create a Widget Blueprint with two **Editable Text Box** widgets and a **Button**, and two String variables, `Email` and `Password`, each set from its box's **On Text Committed**. The button's **On Clicked** calls a custom event `SignIn`, whose graph reads the two variables into the latent **Login** node and removes the widget from its parent on `On Success`. Add the widget to the viewport from your Game Mode's **Event BeginPlay** or a level Blueprint at startup, not from a Game Instance's **Event Init**, which runs before the SDK's subsystems exist ([why](../services/authentication.md#signing-in)). [Blueprint snippet: signin-widget, see /bp/unreal-sdk/signin-widget.txt] :::warning[Never print, log, or store the token in the result.] The `Result` a success carries holds the player's `UserID` and a token the SDK already keeps for you. The SDK's own **On Login** event on the Crowdy SDK Subsystem carries a message and no credential, on purpose; keep anything you put on screen the same way. ::: A player who has not yet agreed to the current terms sees a prompt over the game before the sign-in completes, and it finishes on its own once they accept; you add nothing for it. [Authentication](../services/authentication.md#let-the-player-agree-to-the-terms) covers what it shows and how to draw your own. If this machine signed in before, restore instead of asking: call `RestoreSession` first and show the widget only on its error path, as [Authentication](../services/authentication.md#restore-a-saved-session) shows; the connection then comes up exactly as after a `Login`. **Success signal.** The log reads `Login ok`, then `Connected to the app`. From then on `Get UDP Connection State` on the subsystem reads connected, and the rest of the Runtime section applies. Start replicating your pawn only from that moment: **On UDP Connection Success** fires once the server has assigned this client and its readiness wait has elapsed, an update sent earlier is not held for it, and the [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity) spawns the pawn from the connected handler for that reason. ## Related - [Authentication](../services/authentication.md): register, magic link, social sign-in, restoring a saved session, sign-out, and the events. - [Quickstart](../quickstart.md): this step in the context of one complete lantern, and the pawn that makes the signed-in player present. - [Connection and reconnect](./connection-and-reconnect.md): what the SDK does once the sign-in succeeds. # Map Profiles Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/map-profile # Map Profiles A map profile is a `UCrowdyMapProfile` data asset: one asset configures one map end to end. A map you configure nothing for runs on the profile the plugin ships; a profile of your own is how you change a map's settings, or switch the SDK off there. A configured row whose asset fails to load resolves to no profile, and that map has no networking however correct the code is. ## When you need one When a map needs settings the shipped default does not have: a Backend Config of your own, a different send cadence, a tighter relevance distance. The [Quickstart](../quickstart.md) authors none and runs on the shipped default. A map that should run without the SDK gets a profile with **Enable Networking** unticked; that records the intent, and it is the only way to switch the SDK off on one map. ## Create the asset In the Content Browser, **Add**, **Miscellaneous**, **Data Asset**, pick **Crowdy Map Profile**. The Details panel shows every field of the asset: ![The shipped default map profile asset in the Details panel](/img/unreal-sdk/map-profile-asset.png) The screenshot is the SDK's own shipped default profile. **Backend Config** is empty on it, which is fine: the actor pool runs on a built-in config when none is set. | Field | Default | What it controls | |---|---|---| | `bEnableNetworking` | on | Gates the host subsystem, the event router, and the entity subsystem on this map. Off means the map runs with the SDK loaded but idle. | | `ActorManagement` | see [Rendering backends](./rendering-backends.md) | Which backend draws remote entities (`BackendClass`, `BackendConfig`) and the actor tracker settings. Pick **Crowdy Mass Rendering Backend** as the **Backend Class** to draw a crowd; see [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md). | | `bUseAutoReplicator` | on | The continuous state channel for Dynamic entities. See [Continuous state](./continuous-state.md). | | `ReplicationIntervalHz` | 10 (1 to 10) | Shown as **Replication Interval (Hertz)**. The send cadence shared by the continuous channel and the Crowdy State replicator. Greyed out in the Details panel when Use Auto Replicator is off, but still read by the Crowdy State replicator. | | `bSendActorStateOnlyOnChange` | on | Shown as **Send Actor State Only On Change**. An unchanged Dynamic entity sends a keyframe or a heartbeat instead of restating its state every interval. | | `ActorKeyframeIntervalSeconds` | 3.0 | Full re-send period for an unchanged Dynamic entity, so a late observer converges. 0 turns keyframes off. | | `ActorHeartbeatIntervalSeconds` | 1.0 | Heartbeat period for an unchanged Dynamic entity: the spatial header and no state, which is what keeps an idle entity from being reaped. 0 turns heartbeats off. | | `bUseStateReplicator` | on | Master switch for Crowdy State property replication. See [Crowdy State](./crowdy-state.md). | | `StateRelevanceDistance` | Four Chunks | Shown as **State Relevance Distance**. How far a Crowdy State delta travels, as a chunk count (`ECrowdyReplicationDistance`), tighter than the continuous channel. | | `StateKeyframeIntervalSeconds` | 2.0 | Period of the Crowdy State keyframe, the full re-send of every property marked `CrowdyHeartbeat`. 0 turns the keyframe off map-wide; on-change replication is unaffected. | :::note[An empty Backend Config means the built-in default, not a disabled backend.] `ActorManagement.BackendClass` defaults to `UCrowdyActorPoolBackend`. With `ActorManagement.BackendConfig` left empty, the backend creates a transient **Actor Pool Backend Config** and applies remote movement with `UCrowdyTransformRepPolicy`, the shipped policy that reads the default executor's `FCrowdyActorState` (location and rotation) and interpolates across a ring of recent samples, extrapolating briefly when an update is late. Set a config of your own when your executor sends a state struct of its own, when you want pool sizes per class, or when you want a different pool policy. The actor pool refuses a config of another backend's class with a warning naming the field. The Crowdy Mass Rendering Backend takes its own Backend Config, and ignores a config of another class without a word. See [Rendering Backends](./rendering-backends.md) and [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md). ::: ## Assign the profile Open **Project Settings, Plugins, Crowdy SDK** and find the **Map Profiles** category of `UCrowdySDKDeveloperSettings`: ![The Map Profiles category in Project Settings: Default Profile and the Map Profiles map](/img/unreal-sdk/map-profiles-settings.png) - `MapProfiles` maps a level asset to a profile. The lookup is by the map's short name, with the Play in Editor prefix stripped, so a PIE session resolves the same entry as a packaged build. - `DefaultProfile` covers every map without an entry. The SDK resolves the profile for a world through `UCrowdySDKDeveloperSettings::ResolveProfileForWorld`, in this order: 1. A `MapProfiles` entry whose map matches the current one. 2. `DefaultProfile`, if set. 3. The profile a plugin ships. The Crowdy SDK plugin registers its own `DA_CrowdySDKDefaultProfile` at module startup through `RegisterShippedDefaultProfile`, so a project that sets nothing at all runs on that asset rather than going inactive. The log says so once per world. 4. Nothing: the SDK stays inactive on the map and logs a warning, once per world, naming the map and the fix. :::caution[An entry that names an asset that fails to load is a broken setting, not an absent one. Resolution stops there.] A `MapProfiles` row or a `DefaultProfile` that points at an asset the build cannot load returns no profile and does not fall through to the next step. The warning names the asset path; check that it still exists and is packaged. ::: Every SDK subsystem resolves through the same function, and both warnings are reported once per world rather than once per subsystem, so you will see one line, not a dozen. ## Check it from code A lantern that never lights for other players is nearly always a map whose profile did not resolve. The example, `CheckMapProfile`, called from the lantern's `BeginPlay`, asks the same question the SDK asks, once, so the answer is in the log before anything else is suspected. ```cpp // Lantern.cpp #include "Data/CrowdyMapProfile.h" #include "Utils/CrowdySDKDeveloperSettings.h" void ALantern::CheckMapProfile() { // A lantern that never lights for other players usually has no active profile; rule that out first. const UCrowdyMapProfile* Profile = UCrowdySDKDeveloperSettings::ResolveProfileForWorld(GetWorld()); if (!Profile || !Profile->bEnableNetworking) { UE_LOG(LogTemp, Warning, TEXT("ALantern: no active map profile; this lantern will not replicate.")); } } ``` `ResolveProfileForWorld` is a plain C++ static. There is no Blueprint node for it. There is no Blueprint node for profile resolution: assigning a profile is Project Settings and Data Asset editing only, which the two screenshots above cover. What a Blueprint can read is the connection, `Get UDP Connection State` on the Crowdy SDK Subsystem; a connected state with dead entities is the profile symptom. **Success signal.** With a resolved profile the entity subsystem logs registrations once `crowdy.entity.trace 1` is on. Without one, the log carries a single `resolved no map profile` warning for the map and no entity ever registers. ## The shipped default, in one paragraph The plugin registers a shipped profile under the provider name `CrowdySDK`, and only one registration is in effect at a time: a second plugin offering a different profile is refused and logged, so which one a map runs on never depends on module load order. `UnregisterShippedDefaultProfile` withdraws an offer at module shutdown; `GetShippedDefaultProfilePath` and `GetShippedDefaultProfileProvider` report what is currently offered; `ResolveShippedDefaultProfile` returns the asset, or null with a reason when an offer exists but its asset failed to load. None of these is something a game calls; they exist so a plugin can carry a profile. ## Gotchas - A `MapProfiles` row or `DefaultProfile` naming an asset that did not load means no networking on that map. Check this first when a map looks dead; the warning names the asset. - The shipped default profile has no `BackendConfig`, which means the built-in transform policy draws remote entities. On 2.14.0 and earlier nothing was drawn from the continuous channel in that state; see [What's Changed](../guides/whats-changed.md#2026-09-22-sdk-v2150). - A non-networked map wants a profile with **Enable Networking** off, not no profile. The warning text asks for exactly that. - `StateKeyframeIntervalSeconds` is a safety net, not the mechanism. Changed properties ship on change whatever the interval; 0 only stops the periodic baseline for `CrowdyHeartbeat` properties. - `ReplicationIntervalHz` is one clock for two channels: continuous state and Crowdy State share it but send independently. ## Related - [Rendering backends](./rendering-backends.md): `BackendClass`, `BackendConfig`, and the policy classes. - [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md): choose the Crowdy Mass Rendering Backend and give it a Backend Config. - [Continuous state](./continuous-state.md): what `bUseAutoReplicator` and the keyframe and heartbeat intervals drive. - [Crowdy State](./crowdy-state.md): what `bUseStateReplicator`, `StateRelevanceDistance`, and `StateKeyframeIntervalSeconds` drive. - [Config Sync](../studio/config-sync.md): the network half of the same settings class, written by Crowdy Studio. # Entities and Spawning Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/entities-and-spawning # Entities and Spawning An entity is an actor with a `UCrowdyEntityComponent`. A level-placed one needs no spawn call: every client loads it and derives the same NetID. This page is about the other kind, an actor created during play, which one client spawns and every other client rebuilds from a spawn event. For identity policies, NetIDs, and the owner and proxy roles, read [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md) first. ## When to spawn through the SDK Whenever an actor created at runtime must exist on other clients: a dropped item, a thrown object, a summoned creature. The plain engine `SpawnActor` creates a local actor and tells nobody; the SDK's spawn call creates the same actor and announces it. ## Spawning `UCrowdyEntitySubsystem::SpawnEntity(EntityClass, SpawnTransform, InitialState)` spawns the class locally with a deferred spawn, injects the entity's identity into its component before `BeginPlay`, and broadcasts a spawn event so every client in range spawns the same class as a proxy. The client that called it is the owner; `Role` is `Owner` there and `RemoteProxy` everywhere else. `UCrowdyUtilities::SpawnCrowdyEntity(WorldContextObject, EntityActorClass, SpawnTransform, InitialState)` is the same operation as a static call with a world context pin, which is the form a Blueprint uses. `InitialState` is an `FInstancedStruct` of your own type. It travels with the spawn and lands in the component's `OnCrowdySpawned` event on every client; pass an empty struct when there is nothing to send. The lantern world's runtime spawn is the player arriving with a lantern. `ALanternPlayer` is the player character: a pawn carrying a `UCrowdyEntityComponent` in Dynamic mode with PlayerDerived identity and LocalClient ownership, declared in full on [Continuous state](./continuous-state.md). Once the character knows it is locally owned, `ALanternPlayer::DropLantern` spawns an `ALantern` a short way in front of it, and the new lantern appears there on every client in range. The moment is `OnCrowdyOwnershipAssigned`, not `BeginPlay`: a player pawn spawned during play (`RestartPlayer`) is possessed after its `BeginPlay`, and its Player Derived identity resolves inside that first possession, so `IsLocallyOwned()` is still false at `BeginPlay`. The `AddDynamic` in the block sits in `ALanternPlayer::BeginPlay`, after `Super::BeginPlay()`; the handler fires on the tick after the entity registers, on the first-frame pawn and the runtime-spawned one alike. ```cpp // LanternPlayer.h virtual void BeginPlay() override; UFUNCTION() void HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned); void DropLantern(); // LanternPlayer.cpp #include "Lantern.h" #include "Utils/CrowdyUtilities.h" void ALanternPlayer::BeginPlay() { Super::BeginPlay(); // A pawn spawned during play registers inside its first possession, after BeginPlay, so the owner answer arrives here. CrowdyEntity->OnCrowdyOwnershipAssigned.AddDynamic(this, &ALanternPlayer::HandleOwnershipAssigned); } void ALanternPlayer::HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned) { // Each player arrives carrying a lantern; only the owner spawns it, every client then sees it. if (bIsLocallyOwned) { DropLantern(); } } void ALanternPlayer::DropLantern() { const FTransform SpawnAt(GetActorRotation(), GetActorLocation() + GetActorForwardVector() * 150.f); UCrowdyUtilities::SpawnCrowdyEntity(this, ALantern::StaticClass(), SpawnAt, FInstancedStruct()); } ``` Any Actor Blueprint can spawn; nothing on the spawner needs a component. In the player pawn's Blueprint, at **Event BeginPlay**, **Get Crowdy Entity Component** feeds **Bind Event to On Crowdy Ownership Assigned**, whose `Event` pin is wired to a custom event, `OnOwnershipAssigned`, that receives the owner id, the role, and the `Is Locally Owned` flag; the pawn registers inside its first possession, after `BeginPlay`, so this is where the answer arrives. The event's `Is Locally Owned` pin feeds a **Branch** (the owner gate: only the owner spawns), and on the true side **Get Actor Transform** feeds the `Spawn Transform` pin of **Spawn Crowdy Entity**, which takes the entity class and an optional Initial State struct and returns the spawned actor. The class pin in the figure names the sample lantern; pick your own entity class there. A spawner that is not a Player Derived pawn can run the same Branch straight from **Event BeginPlay** with the pure **Is Crowdy Entity Locally Controlled** as the condition; only a Player Derived pawn learns its identity after `BeginPlay`. [Blueprint snippet: spawn-entity, see /bp/unreal-sdk/spawn-entity.txt] :::warning[A runtime-spawned entity never consults its Identity Policy or its Ownership.] `SpawnEntity` injects a fresh NetID, `Role = Owner`, and the spawning client's player id before `BeginPlay`, so the component's `IdentityPolicy` and `Ownership` settings are not read. Setting Stable on a class you spawn does not give it a stable id, and setting Host does not make it host-owned; a level-placed `ALanternPost` is how you get both, and [Ownership transfer](./ownership-transfer.md) is how a spawned entity changes hands afterwards. ::: :::danger[Do not spawn an entity with the engine's SpawnActor.] A plain `SpawnActor` never broadcasts the spawn event. The component still registers locally, so the actor looks like an entity on the spawning client and exists nowhere else. ::: :::caution[A Dynamic entity's spawn event is a one-shot spatial send, so a client that joins later never receives its `InitialState`.] It reaches the clients within eight chunks of the spawn point at that moment and is never replayed. A client that joins afterwards, or comes into range later, still draws a Dynamic entity from its position updates, but with no spawn event `OnCrowdySpawned` does not fire there. A Static entity is different: its owner repeats the spawn event every 4 seconds (below), so a late joiner gets the actor, its owner, and `OnCrowdySpawned` with the `InitialState`. Anything a late joiner must see on a Dynamic entity belongs in class defaults or in replicated state, and anything that must exist for everyone from the start is level-placed. ::: **Success signal.** On a second client the proxy appears and its `OnCrowdySpawned` fires with `bIsLocallyOwned` false. A single client can only confirm the send: `crowdy.entity.trace 1` logs the registration and the spawn event; `crowdy.rpc.loopback` replays RPC calls, not spawn events, so no proxy appears. ## Destroying Three calls converge on the same path: broadcast a destroy event, then destroy the actor after the component's `DestroyDelay` (0 by default, so immediately). | Call | Where | |---|---| | `UCrowdyEntitySubsystem::DestroyEntity(TargetEntity)` | The subsystem, given the actor. | | `UCrowdyUtilities::DestroyCrowdyEntity(WorldContextObject, TargetEntity)` | The static form with a world context pin; the Blueprint node. `TargetEntity` defaults to self. | | `UCrowdyEntityComponent::DestroyEntity()` | The component, on itself. See [Entity component](./entity-component.md). | On every client `OnCrowdyDestroyed` fires first, with `bIsLocallyOwned` true on the client that called the destroy and false everywhere else; nothing stops a non-owner from destroying an entity. The actor goes after the delay. A `DestroyDelay` above zero is for a dissolve or a fade the actor plays out before it disappears. The entity stops sending updates when the destroy is called, not when the actor goes, and other clients do not draw updates that arrive in the half second after the destroy event, so an update already on its way does not bring the entity back. ```cpp // LanternPlayer.h virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; void PickUpLantern(AActor* Target); // LanternPlayer.cpp #include "Lantern.h" #include "Utils/CrowdyUtilities.h" void ALanternPlayer::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); // Walking over one of your own lanterns that has gone out picks it up; every client sees it go. const ALantern* Lantern = Cast(OtherActor); if (Lantern && !Lantern->bLit && Lantern->CrowdyEntity->IsLocallyOwned()) { PickUpLantern(OtherActor); } } void ALanternPlayer::PickUpLantern(AActor* Target) { UCrowdyUtilities::DestroyCrowdyEntity(this, Target); } ``` The player picks up a lantern of their own that has gone out: in C++ the character's overlap checks the lantern is unlit and locally owned, then `PickUpLantern` destroys it. The graph is the call alone: from **Event ActorBeginOverlap**, **Destroy Crowdy Entity** takes `Other Actor` on its `Target Entity` pin; the "unlit and yours" check is yours to add in front of it. The removal is visible on every client without any further node. [Blueprint snippet: despawn, see /bp/unreal-sdk/despawn.txt] ## Receiving a spawn on a remote client A remote client that receives the spawn event spawns the named class, injects the identity as `RemoteProxy`, finishes spawning, and broadcasts `OnCrowdySpawned` with the `InitialState` from the event. The event carries the class path, so a class that is not loaded yet is streamed in and spawned when it arrives; a destroy event that lands during the load cancels the spawn. If the class ships without a `UCrowdyEntityComponent`, the SDK adds one so the lifecycle callbacks have a home. That actor is the one players see: the actor pool draws it rather than a pooled copy, so whatever your `OnCrowdySpawned` handler set on it stays. When the entity's first update arrives before its spawn event, the pool draws it at once and the spawn event then hands its `InitialState` to that actor instead of spawning a second one. Either way `OnCrowdySpawned` fires once per entity on each client. :::note[The spawn and destroy payloads are wire format, not something you build.] `FCrowdyEntitySpawnEvent`, `FCrowdyEntityDestroyEvent`, and the registry's `FCrowdyEntityRecord` are Blueprint-visible types because they cross a delegate, but you never construct one. `InitialState` is the only payload you author. ::: ### A Dynamic entity can appear with no spawn event at all A Dynamic-mode entity (see [Continuous state](./continuous-state.md)) streams position updates, and each update carries the class id and the state struct type. The `UCrowdyActorManager` resolves the class from that (`RegisterStateClass` is filled in for you by each entity component's `BeginPlay`) and asks the rendering backend to draw a proxy, so a moving entity shows up on a client that joined late without any spawn round trip. A Static entity spawned with `SpawnEntity` sends no updates, so its owner repeats the spawn event instead: every 4 seconds while a client owns it, that client sends the spawn event again, from where the entity is now, naming its owner and carrying its `InitialState`. A client that joins later builds the entity from a repeat exactly as if it had seen the spawn, and its `OnCrowdySpawned` fires there. A client that already has the entity ignores repeats, and `OnCrowdySpawned` never fires twice. A repeat costs about 280 bytes plus your `InitialState`: about 70 bytes per second of upload per entity on its owner, and the same in download for each client within eight chunks. A spawn event for an entity destroyed less than 10 seconds earlier is ignored, so a repeat already on its way cannot bring it back; an owner that never received the destroy keeps repeating, and the entity reappears after those 10 seconds. A Static entity placed in the level, or registered with `RegisterStaticEntity`, sends nothing. :::warning[Preloaded Entity Classes is empty by default, and an observer on this path needs it.] This path resolves the class by id against loaded classes; a spawn event carries the class path and loads it for you, this does not. The startup scan walks loaded classes, and a Blueprint entity class that nobody on this client has spawned yet is not loaded, so an observer that meets it first over the network cannot name it. The actor manager warns `cannot resolve to a loaded class ... Preload the entity class or give it a ClassIDOverride` and the entity stays undrawn. List every Blueprint entity class a client can be shown before it spawns one itself under **Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes** (`PreloadedEntityClasses` on `UCrowdySDKDeveloperSettings`). Classes already loaded for another reason need no entry, and a duplicate costs nothing. ::: ## The actor tracker Inbound actor updates arrive at `UCrowdyActorTracker`, a world subsystem the actor manager listens to. Its settings live on the `ActorManagement` struct of the [map profile](./map-profile.md) (`FCrowdyActorManagementConfigStruct`, the same struct a `UCrowdyActorManagementConfig` asset holds): ![The map profile with Actor Management expanded and an Actor Pool Backend Config assigned: the tracker settings and the backend selection](/img/unreal-sdk/actor-config.png) The shipped default profile leaves **Backend Config** at None and runs on the built-in one; the [map profile page](./map-profile.md#create-the-asset) shows that state and when to assign your own. | Field | Default | Effect | |---|---|---| | `bUseCrowdyActorTracker` | on | Off means no tracker, no actor manager, and no backend on this map. | | `bDispatchUpdatesOnGameThread` | off | Also broadcast each update batch to `OnUpdatesGameThread` for a Blueprint listener. | | `bEnableOwnerTracking` | on | Track the local player's own actor echo as well: the reflection the [Quickstart](../quickstart.md#2-see-yourself) uses as proof that the server is receiving you. | | `ActorTimeoutThreshold` | 12 s | Seconds of silence before an entity is dropped on a guess. The server announces a real departure about five seconds after the last update, so this is the fallback. | | `MaxTrackedActors` | 4096 | How many network-received actors may be tracked at once. A bound on forged ids, not a performance setting; set it above the largest crowd you intend to show. | | `MaxUpdatesPerBatch`, `MaxBatchWaitTime` | 100, 5 ms | How updates for not-yet-seen entities are batched off the game thread. | The tracker's events, all on the game thread: `OnRemoteEntityAppeared` (an id seen for the first time, with its `InitialState`), `OnRemoteEntityTimedOut` (this client heard nothing for `ActorTimeoutThreshold`), `OnRemoteEntityLeft` (the server said it is gone, with an `FCrowdyActorLeft` carrying the `ECrowdyActorLeftReason`: `Stale`, or `SessionReleased` when the server ended that actor's session), and `OnRemoteEntityLeftAnnounced` (every departure, including one for an actor this client never held, for code keeping its own per-sender state). Whichever of timed-out and left reaches an actor first is the only one that reports it. The delegate types are `FOnActorSpawnRequested`, `FOnActorTimeoutRequested`, `FOnActorLeftReported` (used by both departure events), and `FOnActorUpdateGameThreadBatch`. `ToggleOwnerTracking` and `ToggleBroadcastUpdatesToGameThread` change those two settings at runtime. `Configure` does not: it applies only before the tracker starts, which is before any Blueprint can call it, so on a running tracker it logs a Warning and changes nothing, and its Blueprint node is deprecated. Set the limits in the [map profile](./map-profile.md) or in the Crowdy SDK developer settings instead. `FCrowdyActorUpdate` is the batch element, carrying `PayloadTypeID`, the payload type id the sender's state blob led with, kept even when no struct is registered for it on this client. `crowdy.replication.tracker.maxgatheredupdates` caps the backlog of gathered updates for actors already on screen. You do not normally touch any of this: the actor manager and the [rendering backend](./rendering-backends.md) consume it for you. ## Looking entities up `UCrowdyEntitySubsystem` is the registry. `FindEntity(NetID)` returns the actor or null; `FindEntityID(Actor)` is the reverse lookup; `IsLocallyOwned(NetID)` is true when the record names the local player as the owner and the role says this client simulates it (a record that contradicts itself reads as not mine); `GetLocalPlayerID` and `GetHostID` read the two ids everything else is compared against; `SetLocalPlayerID` exists for a game that manages the player id itself, which the session normally seeds for you. `OnEntityRegistered` and `OnEntityUnregistered` (`FOnCrowdyEntityRegistered`, `FOnCrowdyEntityUnregistered`) fire with the NetID as records come and go. The Blueprint-friendly statics on `UCrowdyUtilities` wrap the same registry: `GetCrowdyEntityID`, `GetCrowdyEntity`, `GetCrowdyEntityOwnerID`, `GetCrowdyEntityRole` (an `ECrowdyRole`), `GetAllCrowdyEntitiesByOwner`, `CrowdyIsEntityRegistered`, and `GetLocalPlayerEntityID`. ## Gotchas - `IsLocallyOwned` means two different things: on the subsystem it takes a NetID and asks the record; on the component it takes nothing and asks whether this client is the entity's authority, host-owned included. See [Entity component](./entity-component.md). - A spawned entity is owned by its spawner, full stop. To hand it over, use [Ownership transfer](./ownership-transfer.md). - `InitialState` reaches `OnCrowdySpawned` once on each client that receives the spawn event, on the actor that client draws. A late joiner receives it for a Static entity (from a repeat) but not for a Dynamic one. - Two unrelated participants can derive the same NetID (a NetID has 32 bits of real entropy). The incumbent keeps it and the newcomer is refused with a log line; a system that binds state to an id should register through `TryRegisterEntity` and give up its claim when the `ECrowdyEntityRegistration` it returns is `RefusedIdHeldByLiveParticipant` (or `RefusedInvalidNetID` for a record with no usable id) rather than `Registered`. - `UHelperFunctions` has the id helpers a custom identity scheme would reach for: `GetDeterministicID(Seed)` turns a seed into the same `FGuid` on every client, with those same 32 bits of entropy, and `GetNewID` and `GetNewUUID` mint random ones. Prefer the component's identity policies; these exist for code that already has a seed. ## Related - [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md): NetIDs, policies, owner and proxy. - [Entity component](./entity-component.md): every callable and event on the component. - [Ownership transfer](./ownership-transfer.md): moving an entity to another player. - [Rendering backends](./rendering-backends.md): how a proxy is drawn, and the pooled-proxy gotchas. - [Continuous state](./continuous-state.md): Dynamic mode and the executor. # Entity Component Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/entity-component # Entity Component `UCrowdyEntityComponent` is the one component the SDK tracks an actor by. It registers the actor in the entity registry, hands out its identity, optionally feeds the continuous state channel, and raises the lifecycle events. This page is the reference; the concepts are on [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md). ## Getting the component In C++ the actor usually holds the pointer it created in its constructor. From anywhere else, `UCrowdyUtilities::GetCrowdyEntityComponent(Actor)` (Blueprint: **Get Crowdy Entity Component**, `Actor` defaults to self) resolves it GAS-style: if the actor implements `ICrowdyEntityComponentProvider` its answer is used, otherwise `FindComponentByClass`. Implement the interface when the component lives on a child actor or is chosen at runtime; it is deliberately not a node you call directly. ![The entity component's Details panel](/img/unreal-sdk/entity-component-details.png) ## Settings All `EditAnywhere`, so a constructor, a Blueprint's Class Defaults, or a placed instance can set them. | Setting | Default | One line | |---|---|---| | `Mode` (`ECrowdyEntityMode`) | Static | Static is event-only. Dynamic also streams the `StateExecutor` snapshot every replication interval. Unrelated to ownership and to Crowdy State, which work in either mode. | | `IdentityPolicy` (`ECrowdyIdentityPolicy`) | Stable | Stable, PlayerDerived, or Random. Read only when the actor resolves its own identity; a runtime spawn ignores it. | | `Ownership` (`ECrowdyOwnership`) | Host | Host: the elected host owns it (`Role = HostOwned`). LocalClient: this client owns it (`Role = Owner`). Level-placed actors only. | | `HostOverride` (`ECrowdyHostOverride`) | Allow | For a LocalClient entity: may the host correct its Crowdy State as a super-user (Allow), or never (OwnerOnly). Hidden when Ownership is Host. | | `StateHeartbeat` (`ECrowdyStateHeartbeat`) | Inherit | Inherit follows the map profile's keyframe; Off never emits a keyframe for this entity. On-change replication is unaffected. | | `bAutoApproveOwnershipRequests` | off | The current authority grants any transfer request at once instead of raising `OnOwnershipRequested`. | | `StateExecutor` (`UActorUpdateExecutor`) | none | Dynamic only. Builds the snapshot; unset means the SDK's transform-only default. | | `bAutoRegister` | on | Dynamic only. Join the continuous channel at `BeginPlay`; off means you call `StartReplication` yourself. | | `DestroyDelay` (float) | 0 | Seconds between `OnCrowdyDestroyed` and the actor being destroyed. | ## Callables Category **Crowdy SDK, Entity Component** in the Blueprint palette. | Function | One line | |---|---| | `GetNetID()` | The entity's shared id, as an `FGuid`. | | `GetOwnerID()` | The owning player's id; invalid for a host-owned entity. | | `GetRole()` | `ECrowdyRole`: `Owner`, `RemoteProxy`, `HostOwned`, or `None` before registration. | | `IsLocallyOwned()` | True when this client is the authority: it owns the entity, or the entity is host-owned and this client is the host. The question to ask before owner work. | | `GetMode()`, `GetOwnership()`, `GetHostOverridePolicy()`, `GetStateHeartbeat()` | The settings above, read back. There is no getter for `IdentityPolicy`. | | `StartReplication()`, `StopReplication()` | Join or leave the continuous channel. Meaningful for a Dynamic entity this client owns; `bAutoRegister` calls the first one for you. | | `SendEvent(Payload, Scope)` | Sends a struct payload addressed at this entity. `ECrowdyEventScope::Everyone` runs the entity's handlers on every client, `OwnerOnly` only on the owner. The lower-level sibling of a `CrowdyEvent`; see [Recipients and routing](./recipients-and-routing.md). | | `MarkStateDirty(PropertyName)`, `MarkAllStateDirty()` | Schedule one, or every, `CrowdyManualDirty` property to ship on the next replication tick. See [Crowdy State: marking state from outside the actor](./crowdy-state.md#marking-state-from-outside-the-actor). | | `DestroyEntity()` | Broadcast the destroy event and destroy the owner after `DestroyDelay`. The same path as the subsystem's `DestroyEntity(Actor)` and `UCrowdyUtilities::DestroyCrowdyEntity`. | `IsLocallyOwned()` here takes no argument and answers for the authority; `UCrowdyEntitySubsystem::IsLocallyOwned(NetID)` answers a narrower question about the record's owner id and does not count the host of a host-owned entity. Do not treat them as interchangeable. ## Events | Event | Signature | When | |---|---|---| | `OnCrowdySpawned` (`FOnCrowdyEntitySpawned`) | `InitialState`, `bIsLocallyOwned` | Runtime-spawned entities only: fired by `SpawnEntity` on the spawner and by the spawn event on every proxy, with the spawn payload. A level-placed entity never receives it; bind `OnCrowdyOwnershipAssigned` there instead. | | `OnCrowdyDestroyed` (`FOnCrowdyEntityDestroyed`) | `bIsLocallyOwned` | On every client when the entity is torn down, before the delay. | | `OnCrowdyOwnershipAssigned` (`FOnCrowdyOwnershipAssigned`) | `NewOwnerID`, `NewRole`, `bIsLocallyOwned` | Once the entity knows its owner, on the tick after registration (so a bind in `BeginPlay` still gets it), and again on every reassignment. `NewOwnerID` is invalid for a host-owned entity. | The example binds all three in `ALantern::BeginPlay`, after `Super::BeginPlay()`; the three `AddDynamic` lines in the block go there. A lantern the player drops at runtime is the kind of entity that receives all three: the spawn path announces `OnCrowdySpawned` right after `BeginPlay`, on the owner and on every proxy, so a bind made there is in place in time. A level-placed post never gets the spawn event; on it only the destroy and ownership handlers ever run. The light shows on spawn, hides on destroy, and brightens or dims as the answer to "do I own this" arrives. ```cpp // Lantern.h UFUNCTION() void HandleSpawned(const FInstancedStruct& InitialState, bool bIsLocallyOwned); UFUNCTION() void HandleDestroyed(bool bIsLocallyOwned); UFUNCTION() void HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned); // Lantern.cpp CrowdyEntity->OnCrowdySpawned.AddDynamic(this, &ALantern::HandleSpawned); CrowdyEntity->OnCrowdyDestroyed.AddDynamic(this, &ALantern::HandleDestroyed); CrowdyEntity->OnCrowdyOwnershipAssigned.AddDynamic(this, &ALantern::HandleOwnershipAssigned); void ALantern::HandleSpawned(const FInstancedStruct& InitialState, bool bIsLocallyOwned) { // Spawned entities only: the spawn path announces it right after BeginPlay, on the owner and on every proxy. Light->SetVisibility(true); } void ALantern::HandleDestroyed(bool bIsLocallyOwned) { Light->SetVisibility(false); } void ALantern::HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned) { Light->SetIntensity(bIsLocallyOwned ? 8000.f : 2000.f); } ``` In the spawned lantern's Blueprint, at **Event BeginPlay**, **Get Crowdy Entity Component** feeds **Bind Event to On Crowdy Spawned**, whose `Event` pin is wired to a custom event, `OnLanternSpawned`, that receives the spawn payload and the ownership flag; its body calls **Set Visibility** on `Light`. The other two events bind the same way from the same getter. [Blueprint snippet: ec-on-spawned, see /bp/unreal-sdk/ec-on-spawned.txt] ## Asking a different question than IsLocallyOwned The [Quickstart](../quickstart.md) and the identity page both branch on `IsLocallyOwned()`. The accessor family answers finer questions: `GetRole()` distinguishes the owner from the host of a host-owned entity from a proxy, which the boolean cannot. The example grades the post's light by role. Four more `UCrowdyUtilities` nodes ask role questions without fetching the component: `CrowdyIsRemoteProxy(Entity)` (**Crowdy Is Remote Proxy**) is true when the entity's role is `RemoteProxy`, and `IsCrowdyEntityPlayerControlled(TargetActor)` (**Is Crowdy Entity Player Controlled**) is true for an actor with a Dynamic entity component or a registered remote-proxy record; `SwitchCrowdyIsRemoteProxy` and `SwitchIsCrowdyEntityPlayerControlled` are the same two answers as exec pins. ```cpp // LanternPost.h void ShowRole(); // LanternPost.cpp void ALanternPost::ShowRole() { // The same client can own, host, or only mirror this post; each reads a different brightness. switch (CrowdyEntity->GetRole()) { case ECrowdyRole::Owner: Light->SetIntensity(8000.f); break; case ECrowdyRole::HostOwned: Light->SetIntensity(4000.f); break; default: Light->SetIntensity(1000.f); break; } } ``` The generated graphs branch on booleans, so the three-way role grading of the C++ half is shown as its two-way half: **Get Crowdy Entity Component**, **Is Locally Owned**, a **Branch**, and **Set Intensity** to 8000 on `Light` for the owner. For the full grading in Blueprint, replace the Branch with a **Switch on ECrowdyRole** fed by **Get Role**. [Blueprint snippet: ec-is-owned, see /bp/unreal-sdk/ec-is-owned.txt] ## The two RPC receivers on the component `RequestOwnership_Implementation(RequesterID)` and `GrantOwnership_Implementation(NewOwnerID, PreviousOwnerID)` are `CrowdyEvent` functions with `CrowdyRecipient = "Multicast"` that carry [ownership transfer](./ownership-transfer.md) over the session channel. Call the static `UCrowdyOwnershipTransfer` nodes instead of these; `GrantOwnershipTo(NewOwnerID)` is the C++ entry the static nodes funnel through. :::caution[These two events are Multicast with no CrowdyChannel on purpose. Do not re-declare them with a channel.] They ride the app's session channel, which the SDK always joins, so a request or a grant reaches every client. Re-declared on a channel of their own they would reach that channel's members instead of the entity's audience. Ownership on this plane is a convention in any case; the enforced kind lives in your server code, on the [truth plane](../concepts/two-planes.md). ::: ## Advanced: the static identity helpers C++ only, pure, and the same functions the SDK's own identity resolution calls. Useful when writing custom identity code; not needed otherwise. | Static | What it answers | |---|---| | `DeriveAuthority(Ownership, LocalPlayerID, OutRole, OutOwnerID)` | The role and owner id an `Ownership` value produces. | | `ResolveEffectiveOwnership(AuthoredOwnership, bIsLevelPlaced)` | Why a runtime spawn is always owned by its spawner. | | `ResolveActorInstanceGuid(Actor)` | The engine's per-placement guid, the Stable identity seed. | | `ShouldUsePlacementGuid(bIsLevelPlaced, bHasInstanceGuid)`, `SelectPlacementGuid(AtRegister, Live)` | When that guid may be used, and why the snapshot taken at `OnRegister` wins over a live read in a cooked build. | | `ComputeStableNetID(bOutUsedPathFallback)` | The Stable NetID for this actor, and whether it fell back to hashing the path. | | `DoesOwnershipMatch(OwnerRole, OwnerNetID, TargetRole, TargetOwnerID, HostID)` | The ownership test behind `UCrowdyUtilities::DoesCrowdyEntityOwn`; fails closed when a host-owned side has no host id. | | `ClassifyPlayerDerivedIdentity(Facts)`, `DescribePlayerDerivedIdentity(Verdict)` | Which condition made PlayerDerived fall back to Random, and the sentence the warning uses. | | `ShouldReportHostOwnedAutoRegister(Mode, bAutoRegister, Role)`, `DescribeHostElectionState(bHostKnown, bLocalClientIsHost)` | The two authoring diagnostics: a Dynamic host-owned entity ends `BeginPlay` sending nothing, and what this client currently knows about the host. | | `GetClassID()` | The class hash carried on spawn events. | `InitIdentity`, `AssignPooledIdentity`, `ClearIdentity`, `MarkPooledDormant`, and `IsPooledDormant` are the injection points the entity subsystem and the actor pool use; game code has no reason to call them. ## Gotchas - `OnCrowdyOwnershipAssigned` is announced on the tick after registration, deliberately, so a bind in the actor's own `BeginPlay` is not too late. - A Dynamic entity with `Ownership = Host` and `bAutoRegister` on leaves `BeginPlay` sending nothing: auto-register only starts the channel for an entity this client owns outright. The log says so once per component. - `DestroyEntity()` exists in three places with three signatures and one behaviour. Use whichever you already hold a reference for. - The enum getters return the setting, not a live derivation. `GetRole()` is the live answer. ## Related - [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md): what the settings mean. - [Entities and spawning](./entities-and-spawning.md): the subsystem's spawn, destroy, and lookup calls. - [Ownership transfer](./ownership-transfer.md): the static nodes that replace calling the two receivers. - [Crowdy State](./crowdy-state.md): the properties `MarkStateDirty` serves. - [Continuous state](./continuous-state.md): `Mode`, `StateExecutor`, and `bAutoRegister`. # RPC Events in C++ Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/rpc-events-cpp # RPC Events in C++ A CrowdyEvent is a `UFUNCTION` you call like a normal method. The SDK marshals the arguments by reflection, routes them over the transport, and runs the matching `_Implementation` on the receivers the recipient names. No payload struct, no base class, no manual send call. ## When to use one For a moment, not a value: a flicker, a swing, a chat line, a one-shot trigger. Anything a late joiner must still see is a [Crowdy State](./crowdy-state.md) property; anything a client could gain by lying about belongs on the [truth plane](../concepts/two-planes.md), in your server code. ## The two halves **The receiver** is a reflected `UFUNCTION` named `_Implementation`, declared by you, tagged `meta=(CrowdyEvent)` plus the routing keys. It is what runs on every recipient, the caller included when the recipient says so. **The call site** is `CROWDY_EVENT()`, a macro placed in the class body with no trailing semicolon. It generates a template `(Args...)` that forwards to `FCrowdyRPC::SendChecked`, which checks the argument count and each argument's convertibility against the receiver's declared parameters at compile time, marshals them, and routes the call. Both halves work on any actor or component that carries a `UCrowdyEntityComponent`, and on a [replicated subsystem](./replicated-subsystems.md). :::warning[The receiver must be a literal UFUNCTION you write by hand, never one emitted from inside another macro.] UnrealHeaderTool does not expand macros when it scans for reflected declarations. A `UFUNCTION` produced by a macro is invisible to reflection, so `CROWDY_EVENT` generates only the non-reflected thunk and the routing lives on the receiver's metadata. ::: The lantern's second event is `Sparkle`: the owner announces a one-time ignition sparkle at `BeginPlay`, everyone in range tints the light and reads a set of spark offsets. The four blocks below are facets of that one member. `FLanternSparkleStyle`, the struct the receiver takes, is declared under [Parameters](#parameters) below. ```cpp // Lantern.h // The receiver runs on every client in range; Sparkle() is the call that sends it. UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "SpatialMulticast")) void Sparkle_Implementation(const FLanternSparkleStyle& Style, const TArray& SparkOffsets); CROWDY_EVENT(Sparkle) ``` The call, inside `ALantern::BeginPlay` after `Super::BeginPlay()`, behind the owner check that every `SpatialMulticast` send needs: ```cpp // Lantern.cpp // The owner announces its ignition sparkle to everyone nearby. if (CrowdyEntity->IsLocallyOwned()) { FLanternSparkleStyle Style; Style.Color = FLinearColor::Yellow; Style.Intensity = 8000.f; TArray Offsets = { FVector(0.f, 0.f, 40.f), FVector(0.f, 0.f, 80.f) }; Sparkle(Style, Offsets); } ``` ## Routing keys The meta keys are read from the receiver at scan time and baked into a registry, so a packaged build never reads metadata at runtime. | Meta key | Value | Default when omitted | |---|---|---| | `CrowdyEvent` | none | Required. Marks the function as a receiver. | | `CrowdyRecipient` | An `ECrowdyEventRecipient` enumerator name: `SpatialMulticast`, `Multicast`, `OwningClient`, `Host` | `SpatialMulticast` | | `CrowdyDecay` | An `ECrowdyDecayRate` name | `No_Decay`. Spatial only. | | `CrowdyDistance` | An `ECrowdyReplicationDistance` name | `Eight_Chunks`. Spatial only. | | `CrowdyChannel` | A channel name | Empty, the app's session channel. Multicast only. | Who each recipient reaches, and over which transport, is on [Recipients and routing](./recipients-and-routing.md). One more key, `CrowdyAction`, declares that the parameters describe a one-shot action; it changes nothing for an actor that runs the body itself and matters only to a backend that holds the entity as data. ## Parameters `FCrowdyRPC::IsSupportedParamType` decides what may ride the wire. A parameter may be: - a primitive (`bool`, any integer width, `float`, `double`, a byte), an enum, an `FName`, an `FString`, or an `FText`; - a `USTRUCT`; - an object or class reference (`UObject*`, `TSubclassOf`, and their soft forms), resolved by identity on the receiver; - a `TArray` of any of the above, object references included; - a `TSet` or `TMap` whose element, key, and value are not object references. Not supported: a return value, a non-const output reference, a delegate, an interface, a `TSet` or `TMap` of object references, and a container buried inside a struct parameter at any depth a real struct reaches (`FCrowdyRPC::StructTransitivelyContainsContainer` is the check; it stops at eight levels). A `const TArray&` is an input and is fine. :::warning[An unsupported signature is not a compile error. The event is dropped at scan time and every call to it goes nowhere.] The startup scan runs `FCrowdyRPC::DescribeSignatureProblem` on each receiver and logs an error of the shape `[CrowdyAutoRegistry] CrowdyEvent 'ALantern::Sparkle_Implementation' parameter 'X' has unsupported type 'Y' ... not registered; calls to it will be dropped.` Grep the log for `not registered` when an event compiles but never arrives. ::: A struct parameter travels as a value, so a small cosmetic struct is the natural way to pass several fields at once: ```cpp // Lantern.h // Cosmetic view data for the ignition sparkle; server-owned state never goes here. USTRUCT() struct FLanternSparkleStyle { GENERATED_BODY() UPROPERTY() FLinearColor Color = FLinearColor::White; UPROPERTY() float Intensity = 1.f; }; ``` ### Containers and their bounds A container parameter is decoded with a bound of 65536 elements; a count past that is treated as forged and the whole call is dropped on receipt. The struct-buried form is refused at scan time because that decode cannot be bounded. ```cpp // Lantern.cpp void ALantern::Sparkle_Implementation(const FLanternSparkleStyle& Style, const TArray& SparkOffsets) { Light->SetLightColor(Style.Color); // One spark per offset in a real project; here each offset brightens the ignition flash. Light->SetIntensity(Style.Intensity * SparkOffsets.Num()); } ``` :::warning[A Multicast call has a 1024-byte budget. A call that would exceed it is dropped loudly, never truncated.] `CrowdyChannelPayloadMaxBytes` caps the encoded payload of a reliable (`Multicast`) call. A receiver whose fixed-width parameters alone can never fit is refused at registration with an error naming the byte count; a call whose actual encoded size exceeds the cap is dropped at send with `[CrowdyRPC] Reliable '' dropped ... over the 1024-byte channel limit`. A payload that might be large, such as a list of offsets, belongs on `SpatialMulticast`, which fragments across datagrams. ::: ### Object references An object parameter is encoded by stable identity, tagged as one of three kinds: null, a tracked entity (addressed by its NetID), or an asset or class (addressed by its object path). On the receiver an entity resolves against the local registry and a path resolves against loaded objects. A runtime object that is neither a tracked entity nor an asset has no portable identity and is sent as null with a warning. :::caution[A class or asset reference resolves by path on the receiver, and the receiver only finds what is already resident.] By default an asset that is not loaded on the receiving client decodes to null with a warning naming it. `crowdy.rpc.allowObjectLoad 1` opts into loading it on demand; leave that off in a shipping build and make sure the asset is loaded on every client that can receive the call. ::: ## What happens on the wire The call is packed into an `FCrowdyRpcCall`: the declaring class id, a function id hashed from the full signature (class path, name, and the ordered canonical parameter types), and the parameter bytes prefixed with a format version (`CrowdyRpcParamBlobVersion`). The receiving `UCrowdyEventRouter` looks the pair up in the registry `UCrowdyAutoRegistry` built at startup and invokes the `_Implementation` on the local instance of that entity. A signature that drifted between two builds, or a class that is not loaded, drops with `[CrowdyEventRouter] RPC dropped - no function for ClassID=... FunctionID=...`. A receiver that holds the entity as data rather than as an object reads the same parameters by name through `FCrowdyEventParams`, not through a live object. The Crowdy Mass Rendering Backend works this way, and runs your event's body on a stand-in; see [Run your code on a crowd row](../mass/run-your-code-on-a-crowd-row.md). Tracing: ```text crowdy.rpc.trace 1 every send and receive: function, entity, addressing, parameter bytes crowdy.rpc.reliable.trace 1 the channel transport only crowdy.rpc.loopback 1 deliver your own sends to your own receiver, for a single-client test crowdy.rpc.dumpfn BP_Hero_C MyEvent print one function's routing info ``` ## Gotchas - `SpatialMulticast` runs the body on the caller too, so guard the call with `IsLocallyOwned()`; a proxy's copy of the trigger must not send a second one. - A receiver declared with no `CrowdyRecipient` is `SpatialMulticast`. On a subsystem that default is rejected at send; see [Replicated subsystems](./replicated-subsystems.md). - A `SpatialMulticast` reaches only clients with a fresh actor of their own on the map. A client whose pawn is not sending updates is at no position, so it receives none and its own sends reach nobody; see the [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity). - A late joiner receives nothing from a past call. If it matters after the moment, it is state. - Delivery is on the game thread, always. A handler may touch actors freely. ## Related - [RPC events in Blueprint](./rpc-events-blueprint.md): the same system with a checkbox instead of a macro. - [Recipients and routing](./recipients-and-routing.md): the four recipients, chunks, decay, and targeted sends. - [Channels](./channels.md): where a `Multicast` call travels. - [RPC types](../reference/rpc-types.md) and [RPC meta keys](../reference/rpc-meta-keys.md). # RPC Events in Blueprint Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/rpc-events-blueprint # RPC Events in Blueprint A Blueprint sends and receives CrowdyEvents with no C++: tick one checkbox on a Custom Event, choose a recipient, and call the event by name. The body of the event is the receiver. ## When to use one The same cases as [RPC events in C++](./rpc-events-cpp.md): a moment other clients should see, not a value a late joiner needs. The Blueprint needs a **Crowdy Entity Component** on the actor (or to be a [replicated subsystem](./replicated-subsystems.md)); a Custom Event on an actor without one has no entity to route by. ## Mark the event Select the Custom Event node. Its Details panel gains a **Crowdy SDK** category with a **Crowdy Replicates** checkbox; this is the node's own Details panel, not the actor's. ![The Crowdy Replicates checkbox in a Custom Event's Details panel](/img/unreal-sdk/bp-crowdy-replicates-checkbox.png) Ticking it writes the `CrowdyReplicates` marker onto the event and clears any Unreal replication flag on the node, since the two cannot combine. A status line under the checkbox tells you at once whether the signature is accepted: green `Replicated (N parameters)`, or a red problem naming the parameter, using the same validator the C++ scan runs. Old assets marked with the earlier spelling `CrowdyReplicate` are still recognised. :::warning[Crowdy Replicates cannot combine with Unreal's own replication, and the event must be Public.] The status line reads `Disable Unreal replication - it cannot combine with Crowdy replication`, or `Set Access Specifier to Public - Crowdy Replicates cannot be private`, until you fix the node. A private or protected event is refused because the SDK invokes it from outside the class. ::: :::caution[Toggling the checkbox needs a recompile before it takes effect.] The compiler splices a dispatch gate into the head of a marked event at compile time. Until you press Compile the log says `Custom event 'X' Crowdy Replicates ... Recompile to activate.` and the event still runs locally only. ::: The lantern's `Sparkle` event is the Blueprint twin of the C++ page's `Sparkle_Implementation`, with the style struct flattened into two inputs, `Color` and `Intensity`. Its body is the receiver: **Set Light Color** and **Set Intensity** on `Light`, each fed by a **Get Light** node. The graph below is the whole example; the Details panel of its event node is where Crowdy Replicates is ticked and Spatial Multicast is chosen. [Blueprint snippet: rpc-mark-event, see /bp/unreal-sdk/rpc-mark-event.txt] ## Pick the recipient Once Crowdy Replicates is ticked, three more rows appear. **Recipient** is the `ECrowdyEventRecipient` the C++ page uses, with the same default: ![The Recipient dropdown on a replicated Custom Event](/img/unreal-sdk/bp-recipient-dropdown.png) - **Spatial Multicast** (the default): everyone in range, over the chunk-based path, thinned by decay. - **Multicast**: every member of the app's session channel, whatever the distance, over the channel transport. - **Owning Client**: only the target entity's owner; another client's call becomes a request to that owner. - **Host**: only the elected host; another client's call becomes a request to the host. **Decay Rate** and **Replication Distance** sit below the recipient and are greyed out unless the recipient is Spatial Multicast, because only the spatial path reads them. **Channel** appears only for Multicast: type a channel name, or press **Pick** to choose from the app's channels, which the picker fetches live from Crowdy Studio when you are signed in there; free text always works. **Is A One-Shot Action** appears only once a map profile assigned in **Project Settings, Plugins, Crowdy SDK** (a **Map Profiles** entry or the **Default Profile**) selects a backend that draws crowd rows. A project that uses only the actor pool never sees it. See [Play poses and one-shot actions](../mass/play-poses-and-one-shot-actions.md). :::warning[A channel you type here must already exist. Only the session channel is created for you.] A typo, or a channel nobody created, logs a warning at connect time and every call to the event silently reaches nobody. See [Channels](./channels.md). ::: :::caution[Host runs the call on whatever instance it holds, regardless of who owns that entity, and tells nobody else.] There is no ownership gate on the host. If the rest of the session must see the outcome of a Host event, the host sends a second, Multicast or Spatial Multicast event with the result. ::: ## Call it Call the event by name from any graph on the actor, exactly like a local Custom Event: drag off an exec pin, type `Sparkle`, and fill the `Color` and `Intensity` pins. There is no separate node kind for the call and none in the figure above; the call node is the ordinary one the editor offers for any custom event. The gate at the head of the event routes the call over the transport and skips the local body when this client is not a recipient; when it is, the body runs here as well. For a Spatial Multicast event the owner runs the body itself and sends it, so the call sits behind an **Is Locally Owned** check the way the [Quickstart](../quickstart.md) does; a proxy's copy of the trigger must not send a second one. ## Receive it There is no second node and no second graph. The same `Sparkle` body shown above is the receiver: on every recipient the SDK replays the call through the event, the gate recognises the replay and lets the body run, and `Color` and `Intensity` arrive on the event's own pins with the values the caller passed. What the checkbox generates is a call to `UCrowdyReplicatedEventLibrary::CrowdyDispatchReplicatedEvent` at the top of the event. It is marked internal-use and never placed by hand; if a marked event ever loses its gate (a manual edit, a bad merge), calling it runs the body locally and announces nothing, which looks exactly like a transport drop. Recompile. **Success signal.** With one client, `crowdy.rpc.loopback 1` and `crowdy.rpc.trace 1` show the send and the looped-back receive; the sparkle then comes from the receive rather than the local run. ## Gotchas - The checkbox lives on the Custom Event node's Details, not the actor's. - A red status line is the fastest signal that a pin type is rejected. Read it before compiling. - The Channel field is join-only. Create the channel in Crowdy Studio or at runtime first. - The Blueprint event body runs on the caller for Spatial Multicast and Multicast, and only where the recipient says for Owning Client and Host. Do not add a second local call. - Cheat-sensitive data never rides a CrowdyEvent; it belongs on the [truth plane](../concepts/two-planes.md), in your server code. ## Related - [RPC events in C++](./rpc-events-cpp.md): the parameter rules and the routing keys in full. - [Recipients and routing](./recipients-and-routing.md): what a chunk is, decay, and targeted sends. - [Channels](./channels.md): creating the channel a Multicast event names. # Recipients and Routing Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/recipients-and-routing # Recipients and Routing Every CrowdyEvent names a recipient, and the recipient decides two things: who runs the body, and which transport carries it. This page is the delivery model behind both the [C++](./rpc-events-cpp.md) and the [Blueprint](./rpc-events-blueprint.md) authoring pages. ## The four recipients `ECrowdyEventRecipient`, set with `CrowdyRecipient` in C++ or the Recipient dropdown in Blueprint. `SpatialMulticast` is the default when the key is omitted. | Recipient | Travels over | Distance and decay | Who runs the body | |---|---|---|---| | `SpatialMulticast` (default) | The spatial path, addressed by the sender's chunk | Yes: `CrowdyDistance` and `CrowdyDecay` | Every client in range, the caller included | | `Multicast` | A channel: the session channel, or the one `CrowdyChannel` names | No: every member, any distance, never thinned | Every channel member, the caller included | | `OwningClient` | A targeted delivery to one client | No | Only the target entity's owner. A non-owner's call becomes a request to the owner; the owner's own call runs locally and sends nothing. | | `Host` | A targeted delivery to one client | No | Only the elected host. A non-host's call becomes a request; the host's own call runs locally. The host runs it on whatever instance it holds, whoever owns that entity. | The decision is a pure function of the recipient, the participant kind, and the local authority facts, so it is the same on every client and in every build. "In range" is measured from the position each client's own actor updates registered, so a `SpatialMulticast` reaches only clients with a fresh actor on the map; a client whose pawn is not sending is nowhere and receives nothing. The [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity) makes the player's pawn that actor before it sends anything. :::warning[On a replicated subsystem the default recipient is rejected. Set Multicast or Host explicitly.] A subsystem has no world position, so a `SpatialMulticast` event declared on one has nowhere to be sent from. The send logs `uses SpatialMulticast, which has no location; set CrowdyRecipient=Multicast or Host. Dropping.` and consumes the call so a Blueprint caller does not run the body as a fallback. Since `SpatialMulticast` is what an unannotated event gets, every event on a subsystem needs the key written out. See [Replicated subsystems](./replicated-subsystems.md). ::: ## Spatial: chunks, distance, and decay The world is divided into chunks of `ChunkSize` units on a side; a spatial event is sent from the sender's chunk and reaches every client whose actor stands within `CrowdyDistance` chunks of it. `ChunkSize` lives on the game session (`UCrowdyGameSession::GetChunkSize`, `SetChunkSize`) and defaults to 1600 units, so `ECrowdyReplicationDistance::Four_Chunks` is 6400 units at the default. A distance is a chunk count, not a world distance: changing the chunk size changes what every distance value means. To see which chunk a point falls in, `UHelperFunctions::GetChunkCoordinateAtLocation(WorldContextObject, WorldLocation, ChunkX, ChunkY, ChunkZ)` (Blueprint: **Get Chunk Coordinate At Location**) reads the session's chunk size and returns the three `int64` coordinates; the older `GetChunkCoordinatesAtWorldLocation` without a world context is deprecated. `ECrowdyReplicationDistance` runs from `None` through `One_Chunk` to `Eight_Chunks` (the default). `ECrowdyDecayRate` thins delivery with distance: | Value | Effect | |---|---| | `No_Decay` (default) | Every client inside the distance receives the event. | | `Exponential_Decay` | Delivery probability falls off exponentially with distance. | | `Linear_50`, `Linear_25`, `Linear_10`, `Linear_5` | Linear falloff, from the mildest to the most aggressive. | Decay is a server-side filter applied before the event reaches remote clients. Use it for high-rate cosmetic events where a distant client missing some of them is fine. ## Multicast: what reliable means A `Multicast` event rides a [channel](./channels.md). "Reliable" here means coverage: every member of the channel is a recipient, at any distance, with no decay. It does not mean guaranteed delivery. The transport is still UDP with no acknowledgement, no retransmit, and no duplicate suppression. :::warning[Reliable is coverage, not delivery. A late joiner gets nothing from a past Multicast call.] Treat a Multicast event like any other fire-and-forget call for anything that must not be silently missed. If a value has to be right on a client that missed the event or joined afterwards, put it in a [Crowdy State](./crowdy-state.md) property, or in your server code if it is [truth](../concepts/two-planes.md); an event has no history to replay. ::: The payload of a Multicast call is capped at 1024 bytes (see the [C++ page](./rpc-events-cpp.md#containers-and-their-bounds)); a large container belongs on `SpatialMulticast`. ## Targeted: OwningClient and Host Both are delivered by the server as a single-actor message to exactly one client, never as a broadcast. On receipt an actor participant drops a broadcast that claims to carry an owner-only or host-only function, since a real one never arrives that way. On a subsystem there is no single-actor transport, so the same events legitimately arrive as channel broadcasts and are gated by identity on receipt. :::caution[A Host send needs the host's own entity within the sender's known range.] The targeted message is addressed to the host's avatar entity and the chunk it stands in. If this client holds no record of that entity, the send logs `Run-On-Host '' dropped ... the host's avatar is not in range` and the call is gone; it is not queued until the host comes into range. ::: An `OwningClient` event aimed at a world entity nobody owns has no destination and drops; use `Host` for a world entity. The lantern world uses `Host` for a relight: a client whose lantern has gone dark asks the elected host to relight it, and the host acts on whichever `ALantern` instance it holds. The lantern the host holds is a proxy of another client's entity, so setting `bLit` on it would stay on the host; the receiver marks the property dirty as well, which on an entity the host does not own is the [host push](./crowdy-state.md#on-an-entity-you-do-not-own-the-host-push) that makes the owner adopt the value. `Flicker` from the [Quickstart](../quickstart.md) is the `SpatialMulticast` row; the other two recipients are described in the table above and need no new member. ```cpp // Lantern.h UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "Host")) void RequestRelight_Implementation(); CROWDY_EVENT(RequestRelight) // Lantern.cpp void ALantern::RequestRelight_Implementation() { Light->SetVisibility(true); bLit = true; // The host does not own this lantern; the mark is the host push that makes the owner adopt it. CrowdyEntity->MarkStateDirty(TEXT("bLit")); } ``` A `RequestRelight` custom event with Crowdy Replicates ticked and **Recipient** set to **Host**; its body is **Set Visibility** on `Light`, fed by **Get Light**. Call it from any client whose lantern is dark and it runs on the host alone. The figure is the visible half only. To make the owner adopt the value as the C++ does, add two nodes after it: **Set Lit** to true, then **Mark Crowdy State Dirty** with `Lit` as the property name; without them the relight stays on the host's proxy. [Blueprint snippet: recipient-each, see /bp/unreal-sdk/recipient-each.txt] ## Receiving raw notifications in Blueprint `UCrowdyBlueprintReceptionLayer` (**Crowdy Blueprint Reception Layer**) is a Blueprintable object that receives the wire-level notifications behind the systems on this page, for a game that wants to see them before the SDK's own routing does: subclass it in Blueprint, list the payload structs it claims in `SupportedEvents` (empty means unclaimed events only) and the actor update types in `SupportedActorUpdateTypes`, create it with **Create and Register Layer** (`CreateAndRegisterLayer(WorldContextObject, Class)`), and bind its two events. `OnEventNotificationReceived` (`FOnGameEventNotificationReceived`) delivers an `FGameEventNotificationBP` (the sender `UUID`, the chunk, the `Timestamp`, and the `Event` as an instanced struct); `OnActorUpdateReceived` (`FOnActorUpdateReceived`) delivers an `FActorUpdateNotificationBP` with the same envelope around an `ActorUpdate` struct. A game that uses CrowdyEvents and Crowdy State never needs one; it exists for custom payloads sent with `SendCrowdyEvent` and for diagnostics. ## Delivery is on the game thread Every routing and dispatch entry point in `FCrowdyRPC` and `UCrowdyEventRouter` is game-thread only, and every handler runs there. A handler may spawn, destroy, and touch components without marshalling. ## Sending without an actor Two lower-level entry points exist for callers the ordinary path cannot serve. **A CrowdyEvent for an entity you hold only by identity.** `FCrowdyRPC::SendToTarget(World, Target, Fn, Frame)` sends a receiver function to an entity described by an `FCrowdyRpcTarget`: its `NetID`, an optional `Location`, its `OwnerID`, and this client's `Role` toward it. It routes by the function's own recipient, all four of them, and where this client already holds a registration for the entity the record's owner, role, and position win over the target's fields. It returns false only when there was no transport at all; every drop returns true, so a caller must not run a local body as a fallback for a refused send. Read the location from the entity's own live state, never from a caller's argument, because the location decides which region the message is addressed to. A plain `UObject` that stands for an entity with no actor can also implement `ICrowdyEventSource` and use the ordinary `CROWDY_EVENT` call; the send path asks the interface for the identity and the position it would otherwise read from an actor. The five overrides are `GetEventEntityID` (the entity's NetID), `GetEventLocation` (where it stands right now, or false when unknown), `GetEventOwnerID` (the owning player, invalid for a world entity), `GetEventRole`, and `IsEventLocallyOwned` (only an owner, or the host for a world entity, may originate). Answer for the entity you stand for, read live; never hand back a location a caller passed in, because the location decides which region the event is addressed to. **A struct payload with no declared receiver.** `UCrowdyUtilities::SendCrowdyEvent(WorldContextObject, TargetEntity, Payload, Recipient, DecayRate, ReplicationDistance)` in C++, **Send Crowdy Event** (`K2_SendCrowdyEvent`, a wildcard struct pin) in Blueprint, broadcasts any `USTRUCT` at a target actor with an explicit recipient. It goes through `UCrowdyEntitySubsystem::DispatchGameEvent`, whose `ECrowdyTarget` (`Everyone`, `Entity`, `Owner`, `Host`, `AllExceptSender`) is the wire-level addressing the recipient enum maps onto; `DispatchGameEventAt` is the same send from an explicit location, `DispatchGameEventView` and `DispatchGameEventViewAt` borrow the payload instead of copying it, and `DispatchSingleActorMessage` and `DispatchSingleActorMessageTo` are the targeted delivery behind OwningClient and Host. A handler receives the struct, and may declare an `FCrowdyEventContext` as an optional second parameter to read the `SenderID`, the `TargetID`, the `Target`, and `bSentByLocalPlayer`. `UCrowdyEntityComponent::SendEvent(Payload, Scope)` is the same send addressed at the component's own entity, with `ECrowdyEventScope::Everyone` or `OwnerOnly`. :::caution[The struct send has none of the compile-time checking a CrowdyEvent receiver gets.] Any struct is accepted, there is no `_Implementation` naming convention to tie a sender to a receiver, and a mismatch is found at runtime or not at all. Prefer a `CrowdyEvent` unless you are building a system on top of the SDK. ::: ## Gotchas - Omitting `CrowdyRecipient` means `SpatialMulticast`. Write the key out on anything that is not an actor. - Decay and distance are read only for `SpatialMulticast`. Setting them on a `Multicast` event does nothing. - `OwningClient` on a host-owned world entity drops. Use `Host`. - The Host recipient has no ownership gate. The host may act on any entity it holds. - Changing `ChunkSize` changes the meaning of every distance in the project. ## Related - [RPC events in C++](./rpc-events-cpp.md) and [in Blueprint](./rpc-events-blueprint.md). - [Channels](./channels.md): what the Multicast path rides, and the session channel. - [Host authority](./host-authority.md): who the Host recipient reaches. - [Replicated subsystems](./replicated-subsystems.md): the non-spatial participant. - [Enums](../reference/enums.md). # Crowdy State Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/crowdy-state # Crowdy State Mark a `UPROPERTY` with `meta=(CrowdyState)` and the client that owns the entity diffs it at the map's replication cadence and ships only the changed values to every proxy, which write them back onto their own copy of the actor. No wire struct, no executor, no manual send unless you ask for one. It replicates single values: POD leaves and `USTRUCT`s made of them, never containers or object references, and it says so once at startup for each property it drops. ## When to use it For view state that has to be the same on every client, late joiners included: a lantern's lit flag, an animation stance, a cosmetic colour. It works in either entity mode, Static or Dynamic, and on a [replicated subsystem](./replicated-subsystems.md). :::danger[Authoritative or cheat-sensitive state never belongs in Crowdy State.] This is the view plane. The owner writes a value and every other client believes it; the host's precedence below is a convention the receiver honours, not a check. Hit points, currency, inventory: your server code, always. See [The Two Planes](../concepts/two-planes.md). ::: ## Prerequisites The actor carries a `UCrowdyEntityComponent`, and the map profile's `bUseStateReplicator` is on (the default). A property the class declares is discovered at startup by the auto registry, which builds the class's replication layout once; the layout is baked into the [registry](../studio/inspector-and-registry.md) for packaged builds, so no metadata is read at runtime. ## The metadata keys All on the `UPROPERTY`, all spelled exactly as shown. | Key | Value | Effect | |---|---|---| | `CrowdyState` | none | The marker. The property replicates on the view plane. | | `CrowdyOnRep` | a function name | A parameterless `UFUNCTION` run after the value is written, on the receiver and on the sender. Read the property for the new value; there is no previous-value argument. | | `CrowdyOwnerOnly` | none | Delivered only to the owning client, over the targeted path, never on the spatial broadcast. Scope, not secrecy. | | `CrowdyHeartbeat` | none | Opt in to the periodic keyframe: the property is re-sent every `StateKeyframeIntervalSeconds` even when unchanged, so a late or desynced observer converges. Off by default in C++; the Blueprint dropdown turns it on. | | `CrowdyManualDirty` | none | Never auto-diffed. The value ships only when you mark it, with `MarkStateDirty`. For a property that is expensive to compare every tick. See [Marking state from outside the actor](#marking-state-from-outside-the-actor). | :::warning[An enum without CrowdyHeartbeat can show a stale value to a late joiner.] An enum is held state: sent on change, never re-sent. A client that starts observing after the last change, or that loses the one datagram carrying it, shows the wrong value until it changes again. The startup scan warns once per such property; `crowdy.state.heartbeat.advisories` lists every one it found. Add `CrowdyHeartbeat`, or tick Keyframe heartbeat in the variable's Crowdy Replication settings, unless the value is meant to be transient. ::: ## Mark a property The Quickstart's `bLit` is the plain form: assign, and the change ships. This page adds a second property to `ALantern`, `TimesLit`, marked `CrowdyManualDirty`, so the count is pushed once per lighting instead of compared every tick. `RecordLighting` (a plain `void RecordLighting();` member you declare in the header) is called from the owner-gated overlap of the Quickstart's step 3; it increments the count and marks it, and every proxy's `OnRep_TimesLit` scales the light by the count. ```cpp // Lantern.h // A manual-dirty property ships only when you schedule it, not every tick. UPROPERTY(meta = (CrowdyState, CrowdyManualDirty, CrowdyOnRep = "OnRep_TimesLit")) int32 TimesLit = 0; UFUNCTION() void OnRep_TimesLit(); // Lantern.cpp void ALantern::RecordLighting() { ++TimesLit; CrowdyEntity->MarkStateDirty(TEXT("TimesLit")); } void ALantern::OnRep_TimesLit() { Light->SetIntensity(2000.f * TimesLit); } ``` Add an Integer variable `TimesLit`, set its **Replication** dropdown under **Crowdy Replication** to **Replicated**, and tick **Update manually** there (the other two toggles are **Keyframe heartbeat** and **Only send to owner**). From **Event ActorBeginOverlap**, the pure **Is Crowdy Entity Locally Controlled** feeds a **Branch**; on the true side **Get TimesLit** feeds a **+**, **Set TimesLit** stores the result, and the static **Mark Crowdy State Dirty** schedules it with `TimesLit` on the Property Name pin and `Target` left as self (the member form on the component would need a component getter as well). Seven nodes, one of them the owner gate the lantern's C++ overlap handler has: on a proxy the mark is not ignored, it becomes the [host push](#on-an-entity-you-do-not-own-the-host-push) described below. [Blueprint snippet: state-mark, see /bp/unreal-sdk/state-mark.txt] ## The notify `CrowdyOnRep` names a parameterless function, GAS-style. It fires wherever the value genuinely moved since the last send: on every receiver after the write, and on the sender itself, because a sent delta never comes back to its sender and the owner would otherwise never run it. A keyframe re-sending an unchanged value fires nothing, and a manual dirty mark on a value that did not move fires nothing either. Each slot fires at most once per tick, and a change the notify makes to its own property is folded into the sent baseline rather than re-sent. The [Quickstart](../quickstart.md) lantern is the `state-onrep` example: `bLit` with `CrowdyOnRep = "OnRep_Lit"`, and a notify that sets the light's visibility. ```cpp // Lantern.h // View state: a wrong value is a visual glitch, not a cheat, so it belongs on the Crowdy State plane. UPROPERTY(meta = (CrowdyState, CrowdyOnRep = "OnRep_Lit")) bool bLit = true; UFUNCTION() void OnRep_Lit(); // Lantern.cpp void ALantern::OnRep_Lit() { Light->SetVisibility(bLit); } ``` Select the variable, set **Crowdy Replication** to **Replicated**, and a **RepNotify** field appears; the editor creates the `OnRep_` function. The variable's Get and Set nodes then carry the replication badge: ![The replication badge on a Crowdy-replicated variable's Get and Set nodes](/img/unreal-sdk/bp-replication-badge.png) The body of the notify, drawn here as a custom event named `OnRep_Lit` because a generated graph cannot draw the editor-made function itself: **Get bLit** feeds **Set Visibility** on `Light`. [Blueprint snippet: state-onrep, see /bp/unreal-sdk/state-onrep.txt] :::caution[The notify runs on the sender too, at the next replication tick, and only on a real change.] Write the notify as the one place the value is applied to the world, the way `OnRep_Lit` drives the light, and keep it idempotent. The Quickstart calls `OnRep_Lit` itself right after the assignment for an immediate local response; the replicator runs it once more on the tick that ships the change, which is harmless for a notify that only applies the current value and wrong for one that counts its own calls. ::: ## What ships, and when Only the owning client diffs. A remote proxy never diffs; it applies. The cadence is the map profile's `ReplicationIntervalHz` (1 to 10, default 10), the same clock the continuous channel uses, though the two send independently. Each tick the replicator compares every auto-diffed property of every entity this client drives against a shadow copy of the last sent value and encodes the changed ones into one `FCrowdyStateDelta`; owner-only properties go out as a second, targeted delta. Every `StateKeyframeIntervalSeconds` (default 2, on the map profile; 0 turns it off) each entity also emits a keyframe carrying its `CrowdyHeartbeat` properties, unless the entity's `StateHeartbeat` is Off. A delta travels as far as the profile's `StateRelevanceDistance` (Four Chunks by default), which is tighter than the continuous channel. A struct with a native net serializer (`FVector_NetQuantize`, `FRotator`) rides the engine's own quantized serializer automatically; a plain `FVector` stays exact. Quantization is chosen by type, not by a key. See [What a state property may be](#what-a-state-property-may-be). Every delta carries a layout hash that folds each property's name and canonical type in declaration order. A receiver whose own hash for the class differs drops the delta cleanly rather than reading bytes by position, so adding, removing, reordering, or retyping a replicated property on one build and not another costs you deltas, not corruption. An enum property is carried as its underlying integer (blob version 2, `CrowdyStateBlobVersion`); a peer on the older format drops these deltas rather than misparsing them, so every client of an app runs one build. ## What a state property may be You marked a property `CrowdyState` and nothing arrives, or you are about to mark one and want to know if it will. The classifier every surface shares, `FCrowdyStateLayoutBuilder::ClassifyStateProperty` (with `IsStateReplicatable` as the yes-or-no form and `DescribeStateSupport` for the sentence), decides at discovery; the Blueprint compiler's variable check and the variable-details dropdown reuse it, so every surface reports the same reason. ### Accepted | Kind | Examples | Notes | |---|---|---| | Numeric | `int32`, `int64`, `uint8`, `float`, `double` | Diffed and sent as their bytes. | | Boolean | `bool` | | | Enum | `enum class E : uint8`, `TEnumAsByte` | Carried as the underlying integer. Consider `CrowdyHeartbeat`; see [The metadata keys](#the-metadata-keys). | | Name and string | `FName`, `FString` | | | Plain struct | any `USTRUCT` whose fields, at every depth, hold no container | One slot in the layout; diffed and sent as one unit. | | Net-serialized struct | `FVector`, `FRotator`, `FVector_NetQuantize`, any struct with a native net serializer | Rides its own `NetSerializeItem`, quantized as the engine defines it, and exempt from the nested-container check because its serializer bounds the decode. | ### Rejected :::warning[Containers and object references are rejected at discovery, in any position, including inside a struct. Use a CrowdyEvent, or your server code.] The error names the escape hatch: `CrowdyState: property 'Players' on '/Script/MyGame.Lantern' is a container; CrowdyState does not replicate containers; use a CrowdyEvent RPC or a Server Object. Omitting it.` A list of who lit the lantern is a `TArray` parameter on a [CrowdyEvent](./rpc-events-cpp.md) if it is a moment, or a [Server Object](../exec/overview.md) if it is [truth](../concepts/two-planes.md). Never re-attempt it here. ::: | Rejected | The reason discovery logs | |---|---| | `TArray`, `TSet`, `TMap` | `is a container; CrowdyState does not replicate containers; use a CrowdyEvent RPC or a Server Object` | | A plain struct that holds a container at any depth | `is a USTRUCT that transitively contains a container (TArray/TSet/TMap); such nested containers are rejected because a forged element count would drive an unbounded allocation on decode` | | A fixed-size C array (`float Values[4]`) | `is a fixed-size array; CrowdyState replicates only single-value POD and USTRUCT properties (its positional diff would miss changes past element 0)` | | `UObject*`, `TSubclassOf`, soft object and class references, interfaces, delegates | `is an object/interface/delegate reference; CrowdyState replicates only POD and USTRUCT values` | | Anything else, `FText` for one | `has an unsupported type 'X'; CrowdyState replicates only POD and USTRUCT values` | A rejected property is omitted from the class's layout, so it never corrupts the positional wire order of the ones that were accepted; the rest of the class replicates normally. The line is an error and is not gated by `crowdy.state.trace`, so it is in the log whether or not tracing is on. ### Structs A plain `USTRUCT` is a single slot. The whole struct is compared with the engine's `Identical` and, when any field differs, the whole struct is sent; on the receiver every field lands together, before the notify runs. That is the reason to use one: two fields that must always be seen together, a colour and an intensity, cannot arrive half-applied the way two separate properties can. The lantern's glow is such a pair. `FLanternGlow` holds a colour and an intensity; `WarmGlow` on the owner changes both, and `OnRep_Glow` applies both to the light in one step. ```cpp // Lantern.h // A plain struct rides CrowdyState as one value; its fields always arrive together. USTRUCT() struct FLanternGlow { GENERATED_BODY() UPROPERTY() FLinearColor Color = FLinearColor::White; UPROPERTY() float Intensity = 1.f; }; UPROPERTY(meta = (CrowdyState, CrowdyOnRep = "OnRep_Glow")) FLanternGlow Glow; UFUNCTION() void OnRep_Glow(); // Lantern.cpp void ALantern::WarmGlow() { Glow.Color = FLinearColor(1.f, 0.6f, 0.2f); Glow.Intensity += 500.f; } void ALantern::OnRep_Glow() { Light->SetLightColor(Glow.Color); Light->SetIntensity(Glow.Intensity); } ``` A Blueprint variable of a C++ struct type replicates the same way: set its **Crowdy Replication** dropdown to **Replicated** and give it a RepNotify, exactly as for a scalar. Authoring a new struct type in Blueprint for this is unusual, so this section has no Blueprint graph of its own; the mechanics of marking and notifying are under [Mark a property](#mark-a-property) and [The notify](#the-notify). :::caution[A struct diffs and ships as one unit.] Changing one field of a five-field struct re-sends all five. Five scalar properties diff independently and each ships alone. Group fields into a struct when they must land together; keep them separate when they change at different times. ::: ### Where the list you wanted belongs | You wanted | Put it on | |---|---| | The names of the players who lit the lantern this match | Your server code, if it is truth the server should hold; otherwise a `TArray` parameter on a `Multicast` CrowdyEvent when it changes. | | A ring buffer of recent positions | The [continuous state](./continuous-state.md) snapshot, as fixed fields, or nothing: the proxy interpolates for you. | | A reference to another actor | Its NetID as an `FGuid`, resolved with `FindEntity` on the receiver. | | A list of active effects | Your server code. | ## Marking state from outside the actor `UCrowdyStateBlueprintLibrary` has two static nodes that schedule a `CrowdyManualDirty` property to ship without first fetching the actor's Crowdy Entity Component: **Mark Crowdy State Dirty** and **Mark All Crowdy States Dirty**. Each resolves the target actor's component and forwards to its `MarkStateDirty` or `MarkAllStateDirty`. Use them whenever the caller does not already hold the component: a graph on another actor, a function library, a UI widget. From the actor's own graph either form works; the static one needs no component reference wired in. ### The two nodes | Node | C++ | What it does | |---|---|---| | **Mark Crowdy State Dirty** | `UCrowdyStateBlueprintLibrary::MarkCrowdyStateDirty(Target, PropertyName)` | Schedules one manual-dirty property on the target's entity for the next replication tick. | | **Mark All Crowdy States Dirty** | `UCrowdyStateBlueprintLibrary::MarkAllCrowdyStatesDirty(Target)` | Schedules every manual-dirty property on that entity. | Both are `DefaultToSelf` on `Target`: from an actor's own graph you wire nothing and pick the property. From a component graph or a plain object graph, self is not an actor, so wire the actor in; the node shows a "Wire an Actor into Target" hint until you do. Both are safe no-ops, never a crash, when the target is null, has no entity component, or the map runs no state replicator. On an entity this client does not drive the two differ: **Mark All Crowdy States Dirty** is ignored, while **Mark Crowdy State Dirty** is not a no-op at all; it becomes the host push described below. On an entity this client does drive, marking a property that is auto-diffed anyway is a no-op logged at verbose level as `ignored: not a manual-dirty property`, never an error. The example is the same `TimesLit` member [Mark a property](#mark-a-property) added; this is the alternate call site, not a second gameplay moment. ```cpp // Lantern.cpp #include "Replication/State/CrowdyStateBlueprintLibrary.h" void ALantern::PushTimesLit() { // Schedule the same property from anywhere, without fetching the component first. UCrowdyStateBlueprintLibrary::MarkCrowdyStateDirty(this, TEXT("TimesLit")); } ``` From **Event ActorBeginOverlap**, the pure **Is Crowdy Entity Locally Controlled** feeds a **Branch**, and the true side calls **Mark Crowdy State Dirty** with `Target` left unwired (self) and `TimesLit` picked from the Property Name dropdown. No component reference is needed. The branch is the owner gate the C++ caller has: on a proxy the same mark becomes the push described below, which is not what an overlap on a proxy means. [Blueprint snippet: state-static-dirty, see /bp/unreal-sdk/state-static-dirty.txt] ### The property picker The Property Name pin carries a dropdown, supplied in the editor by `FCrowdyStatePropertyPinFactory`, listing the resolved target class's properties that are marked `CrowdyManualDirty` and pass the type check. The dropdown is editor-only metadata scanning; the name you pick bakes into the compiled Blueprint as a literal `FName`, so a packaged build never reads metadata for this node. :::note[If your property is not in the dropdown, it is either auto-diffed already or it failed the type check.] An auto-diffed property does not need marking; assign it and it ships. A property that failed the check was omitted from the layout at startup with an error; see [What a state property may be](#what-a-state-property-may-be). ::: ### On an entity you do not own: the host push **Mark Crowdy State Dirty** on an entity another client owns is not a no-op. It becomes a one-shot super-user push: the live value of the named property is read on the next replication tick and broadcast once, with no shadow and no ongoing tracking, stamped host-sourced when the caller is the elected host. It works for any Crowdy State property on that entity, not only the manual-dirty ones, because a host override may need to correct anything. An `OwnerOnly` entity refuses it at the source: the push is dropped before it is sent, and a receiver drops one that arrives anyway. A host-owned world entity is the other case. On the host it is tracked with auto-diff off, so a plain `CrowdyState` property on it never ships on change; the host's mark works only for a `CrowdyManualDirty` property (the `CrowdyHeartbeat` keyframe still restates the rest). Mark a host-owned entity's changing properties manual-dirty, or give them `CrowdyHeartbeat`. The receiver honours a push by the entity's `HostOverride` policy: `Allow` (the default) adopts a host-sourced value into the owner's baseline. A push from a client that is not the host is not stamped, so the real owner drops it, and every other proxy applies it, so the proxies disagree with the owner until the owner's next change. That is the hazard behind the owner gate above: a mark issued on a proxy by mistake is a push. :::warning[A push on an entity you do not own is a live correction with no undo.] The value on the calling client is broadcast as it stands. There is no shadow to revert to and no acknowledgement. Read the current value first, set what you mean, then mark. ::: :::caution[Anyone can call this on any entity. Precedence is a convention, not enforcement.] A modified client can stamp its own pushes host-sourced. This orders who wins on the view plane and nothing more; see [Host authority](./host-authority.md) and [The Two Planes](../concepts/two-planes.md). ::: ## A field that silently does nothing The first thing to check. Discovery logs an unconditional error for every marked property it cannot carry, of the shape `CrowdyState: property 'X' on '/Script/MyGame.Lantern' is a container; CrowdyState does not replicate containers; use a CrowdyEvent RPC or a Server Object. Omitting it.` The reasons are a container, a struct that buries a container, a fixed-size array, an object or interface or delegate reference, or an otherwise unsupported type such as `FText`; [Rejected](#rejected) lists them. One more line to know: a property whose name and type also appear in the actor's continuous-state executor struct is dropped with `also lives in its executor state struct ... Dropping it from the CrowdyState layout`, so a field lives on exactly one channel. These fire at class registration and are not gated by the trace variable. Grep the log for `CrowdyState: property`, which all three lines share, before turning anything on. When the property is in the layout but the change is not arriving, that is the other question: ```text crowdy.state.trace 1 per delta: entity, changed-property count, byte size, spatial or owner-only or keyframe crowdy.state.loopback 1 decode your own deltas onto a local mirror entity, for a single-client test ``` Two read-only diagnostics on `UCrowdyStateReplicator` answer the same questions from code or Blueprint: `IsStateReplicated(Actor)` is true when the actor's class carries at least one accepted property, whichever client drives it, and `GetLastSentStateBytes(Actor)` is the blob size this client last sent for the actor, or -1 when this client does not drive it. `crowdy.state.scopes` adds a CPU trace scope around each delta decode for profiling. ## Host precedence When the local client is the elected host, every delta it sends is stamped host-sourced. A receiver that owns the entity adopts a host-sourced value into its own baseline instead of reverting it on the next diff, provided the entity's `HostOverride` is `Allow` (the default); `OwnerOnly` drops even a host correction. A host-owned world entity is driven by the host alone, and only by explicit pushes: its manual-dirty marks and its keyframe, never a background diff. See [Host authority](./host-authority.md). :::warning[Host precedence is a convention with no server-side check. A forged host-sourced flag is honoured as real.] It orders who wins on the view plane; it does not make either value trustworthy. If the value matters, it belongs in your server code and the question does not arise. ::: ## Gotchas - Grep for `CrowdyState: property` first. A property that is not in the layout replicates nothing and says so once, at startup, as an error. - The notify has no parameters and no old value. Keep the previous value yourself if you need it. - A Blueprint variable that is both natively Replicated and Crowdy Replicated fails to compile. In C++, keep `Replicated` off a `CrowdyState` property. - `CrowdyOwnerOnly` is delivery scope, not secrecy. The value still travels in the clear to the owner. - `StateKeyframeIntervalSeconds` at 0 stops the baseline re-send only; on-change replication is unaffected. - The loopback and trace variables are test aids. Leave them off in a normal session. - `FText` is not accepted on this plane, though it is on a CrowdyEvent. Use an `FString` or an `FName`. - A struct that is net-serialized is exempt from the nested-container check only because its serializer bounds the decode. A plain struct with a `TArray` inside is rejected however small the array. - Quantization is decided by the struct's type. To ship a quantized position, declare `FVector_NetQuantize`, not `FVector`. - `Target` on the static nodes defaults to self only where self is an actor. In a component or widget graph, wire the actor. - The push is scheduled, not sent. It goes out on the next replication tick with the value the property holds then. - Marking twice in one tick sends once. - A host-owned world entity is written only through the host's manual-dirty marks and the keyframe; assigning a plain property on it and waiting does nothing. See [Host authority](./host-authority.md). ## Related - [Continuous state](./continuous-state.md): the sibling channel for many fields that move together. - [Map profiles](./map-profile.md): the cadence, the relevance distance, and the keyframe interval. - [State meta keys](../reference/state-meta-keys.md). - [RPC events in C++](./rpc-events-cpp.md): containers and object references as event parameters. - [The Two Planes](../concepts/two-planes.md): what belongs on the truth plane instead. - [Host authority](./host-authority.md): host-owned entities and the host override. - [Ownership transfer](./ownership-transfer.md): making another client the owner instead of pushing over it. # Continuous State Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/continuous-state # Continuous State Continuous state is how a moving entity moves on every other client. Set the entity component to Dynamic mode and, every replication interval, the owner sends a snapshot of the actor's location and rotation; each other client in range applies it to a proxy through the [rendering backend](./rendering-backends.md). There is nothing to write for that: the SDK's default executor takes the snapshot for you. :::note[What is coming] A later SDK release simplifies this further: continuous state will happen by itself, with location and rotation, and need no executor at all, and it adds an extension point for sending your own data alongside them. The aim is to take this setup out of your hands. Everything this page teaches keeps working. ::: ## When to use it Almost always the default is enough: a pawn, a player, an AI that moves. Write a custom snapshot only when more fields must travel with the transform at the movement cadence, a pose, say. For many small independent values that change at different times, prefer [Crowdy State](./crowdy-state.md) properties: they diff per property and ship only what changed. The two are independent channels, not alternatives; an entity can use both. :::warning[Dynamic mode is unrelated to ownership and to Crowdy State property replication.] `Mode` on the entity component chooses whether the continuous channel runs. It does not decide where your replicated properties go, and Static mode does not disable them; both work in either mode. Set Dynamic for a moving entity and nothing else changes. ::: ## The default: no executor at all Set `Mode` to `ECrowdyEntityMode::Dynamic` and leave `StateExecutor` unset. The SDK's `UCrowdyDefaultActorUpdateExecutor` snapshots the actor's transform into `FCrowdyActorState`, a `Location` and a `Rotation` and deliberately nothing else. With `bAutoRegister` on (the default) the component joins the channel at `BeginPlay`; off, you call `StartReplication` yourself, and `StopReplication` leaves the channel. The lantern world's mover is `ALanternPlayer`, the player character from the [Quickstart](../quickstart.md): a `UCrowdyEntityComponent` in Dynamic mode with PlayerDerived identity and LocalClient ownership, and a `Torch` point light, so a moving light is what every other client sees. ```cpp // LanternPlayer.h #include "Components/PointLightComponent.h" #include "Replication/Components/CrowdyEntityComponent.h" #include "LanternPlayer.generated.h" UCLASS() class ALanternPlayer : public ACharacter { GENERATED_BODY() public: ALanternPlayer(); virtual void BeginPlay() override; UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Torch; UPROPERTY(VisibleAnywhere, Category = "Crowdy") TObjectPtr CrowdyEntity; }; // LanternPlayer.cpp #include "LanternPlayer.h" ALanternPlayer::ALanternPlayer() { Torch = CreateDefaultSubobject(TEXT("Torch")); Torch->SetupAttachment(RootComponent); // Dynamic sends the transform every interval, Player Derived names the entity after the signed-in user, Local Client makes this client its sender. CrowdyEntity = CreateDefaultSubobject(TEXT("CrowdyEntity")); CrowdyEntity->Mode = ECrowdyEntityMode::Dynamic; CrowdyEntity->IdentityPolicy = ECrowdyIdentityPolicy::PlayerDerived; CrowdyEntity->Ownership = ECrowdyOwnership::LocalClient; } ``` On the Crowdy Entity Component set **Mode** to **Dynamic** in the Details panel and leave **State Executor** empty. There is no graph to build: **Auto Register** joins the channel at BeginPlay. Start the pawn only once the SDK reports the connection up (`OnUDPConnectionSuccess`, or `GetUDPConnectionState()` at `Connected`); the Quickstart spawns it from that event, and the Gotchas say why. **Success signal.** On a second client the player's torch moves with the owner. `crowdy.entity.trace 1` logs the registration; if nothing moves, check [the backend config](./map-profile.md) before the executor. :::caution[A host-owned Dynamic entity leaves BeginPlay sending nothing.] `bAutoRegister` starts the channel only for an entity this client owns outright. A level-placed Dynamic entity with `Ownership = Host` has no owner at `BeginPlay`, so nothing is sent until the host is known; the component logs this once. A moving world entity is driven by the host through `StartReplication` once it knows it is the host, or by an owner after an [ownership transfer](./ownership-transfer.md). ::: ## A custom executor Subclass `UActorUpdateExecutor` when the snapshot needs more than a transform. Two functions: `GetActorState(UpdateComponent)` returns the `FInstancedStruct` snapshot (`UpdateComponent` is the entity component; `GetOwner()` on it is the actor), and `GetStateStruct()` returns the `UScriptStruct` it is made of, which registers the type for the wire; the struct needs no annotation. :::danger[Override the `_Implementation` functions, with `const`, never the bare event names.] Both are `BlueprintNativeEvent`s, so the bare names are not virtual in the generated C++. A native subclass declares `GetActorState_Implementation(const UActorComponent*) const override` and `GetStateStruct_Implementation() const override`; `UCrowdyDefaultActorUpdateExecutor` is the shipped example of the correct shape. Marking `GetActorState` itself `override` does not compile, because there is nothing virtual to override. ::: `ULanternPlayerExecutor` snapshots `FLanternPlayerState`: the transform the default carries plus one field of the player's own, `bTorchLit`, so a proxy's torch can follow its owner's. The struct is declared next to the executor, and the executor is assigned to `StateExecutor` in the character's constructor. On the other clients the snapshot is applied by the backend's [replication application policy](./rendering-backends.md#the-replication-application-policy), which is what moves a remote player's proxy: the shipped one applies `Location` and `Rotation`, and a policy of your own replaces it, so write one only when your executor sends more than the transform, and then apply the transform yourself first. ```cpp // LanternPlayer.h #include "Replication/Executor/ActorUpdateExecutor.h" // The custom snapshot: the transform the default carries, plus one field of your own. USTRUCT() struct FLanternPlayerState { GENERATED_BODY() UPROPERTY() FVector Location = FVector::ZeroVector; UPROPERTY() FRotator Rotation = FRotator::ZeroRotator; UPROPERTY() bool bTorchLit = true; }; UCLASS() class ULanternPlayerExecutor : public UActorUpdateExecutor { GENERATED_BODY() public: virtual FInstancedStruct GetActorState_Implementation(const UActorComponent* UpdateComponent) const override; virtual UScriptStruct* GetStateStruct_Implementation() const override; }; // LanternPlayer.cpp CrowdyEntity->StateExecutor = CreateDefaultSubobject(TEXT("Executor")); FInstancedStruct ULanternPlayerExecutor::GetActorState_Implementation(const UActorComponent* UpdateComponent) const { FLanternPlayerState State; if (const ALanternPlayer* Player = UpdateComponent ? Cast(UpdateComponent->GetOwner()) : nullptr) { State.Location = Player->GetActorLocation(); State.Rotation = Player->GetActorRotation(); State.bTorchLit = Player->Torch->IsVisible(); } return FInstancedStruct::Make(State); } UScriptStruct* ULanternPlayerExecutor::GetStateStruct_Implementation() const { return FLanternPlayerState::StaticStruct(); } ``` Create a Blueprint with **Actor Update Executor** as its parent class. In **My Blueprint**, under Functions, press **Override** and pick the two functions: ![Get Actor State and Get State Struct in the override list of an Actor Update Executor Blueprint](/img/unreal-sdk/bp-executor-overrides.png) **Get State Struct** returns the struct type; **Get Actor State** builds the snapshot with **Make InstancedStruct** from the owning actor's transform. Then set the character's **State Executor** to the new class in the Details panel. An override on an executor subclass is not an actor event graph, so the screenshot above is the reference. :::caution[Only the owner runs GetActorState. Keep it a read-only snapshot.] A remote proxy is fed from the network and never calls it, so a side effect inside the override happens on one client only; a value the proxy needs comes from the backend's policy. Read the actor, fill the struct, return it. ::: The owner evaluates `GetActorState` every interval (`ReplicationIntervalHz`, 10 by default, 1 to 10 Hz, one clock shared with Crowdy State; `bUseAutoReplicator` off means no polling at all). With `bSendActorStateOnlyOnChange` on, the default, an unchanged snapshot does not repeat: a full keyframe goes out every `ActorKeyframeIntervalSeconds` (3 s) so a late or lossy observer converges, and a heartbeat every `ActorHeartbeatIntervalSeconds` (1 s) so an idle entity is not reaped; off, the snapshot goes out every interval whether it changed or not. The [map profile](./map-profile.md) page lists each field. A field lives on exactly one channel: a `CrowdyState` property whose name and canonical type also match a field of the executor struct is dropped from the Crowdy State layout at discovery, with an error mentioning `also lives in its executor state struct`. ## Gotchas - `Mode` is orthogonal to `Ownership` and to Crowdy State. Do not set Dynamic hoping to make properties replicate; they already do. - `_Implementation`, `const`, `override`. The shipped default is the template. - Nothing is held until the connection is up. `OnUDPConnectionSuccess` already includes the server's readiness wait after assignment, so you never time anything; but an actor update sent before it is neither held nor replayed, so it is lost, mostly silently, until the next interval's update or keyframe repairs it. A Dynamic entity you own that begins play early heals that way; spawn a player pawn from the event instead, as the Quickstart does. - An unchanged actor is not silent. Set both intervals to 0 and an idle entity sends nothing after its first snapshot, and its observers time it out after the tracker's `ActorTimeoutThreshold`. - The policy is what moves the proxy. With no Backend Config the shipped `UCrowdyTransformRepPolicy` applies `FCrowdyActorState`, interpolating across a ring of recent samples with bounded extrapolation; a custom struct like `FLanternPlayerState` is skipped until the profile names a policy that reads it, and that policy must apply the transform itself, or the proxy stands still while the owner moves. - Class identity travels on every update, so several entity classes may share one state struct; the receiver never derives the class from the struct. This is what lets the default executor serve every entity in a project. - `RegisterStateClass` on `UCrowdyActorManager` is filled for you at `BeginPlay`; you never call it. ## Related - [Quickstart](../quickstart.md): the player pawn as a Dynamic entity, spawned once connected, and your own reflection as the first thing you see. - [Entity component](./entity-component.md): `Mode`, `StateExecutor`, `bAutoRegister`, `StartReplication`. - [Rendering backends](./rendering-backends.md): the policy that moves a proxy from the snapshot. - [Map profiles](./map-profile.md): the cadence and the keyframe and heartbeat intervals. - [Connection and reconnect](./connection-and-reconnect.md): what `OnUDPConnectionSuccess` and `Connected` mean. - [Crowdy State](./crowdy-state.md): the per-property alternative. # Host Authority Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/host-authority # Host Authority One client of your app is the elected host, and the SDK tells every client who it is. Use it to decide who runs shared view-plane logic: who drives the world entities, who spawns the things that belong to nobody. Why election is a convention and not a security boundary is on [The Host Is a Convention](../concepts/host-is-a-convention.md); this page is the calls. ## Checking authority `UCrowdyUtilities::GetCrowdyHasAuthority(WorldContextObject)` returns true when the local client is the host. It is a pure node, **Crowdy Has Authority**, in Blueprint; **Switch Crowdy Has Authority** (`CrowdyHasAuthority`) is the same answer as two exec pins. `CrowdyGetHostID` returns the host's id and whether one is set, and `CrowdyIsConnectedToServer` says whether there is a connection to ask through at all. The lantern world gates a one-time action on it: at `BeginPlay` an `ALanternPost` turns its light up on the host alone, and a client that is not the host returns early and leaves host work to the host. ```cpp // LanternPost.h void RelightIfHost(); // LanternPost.cpp #include "Utils/CrowdyUtilities.h" void ALanternPost::RelightIfHost() { if (!UCrowdyUtilities::GetCrowdyHasAuthority(this)) { return; // Not the host; the host's work replicates to us instead. } Light->SetIntensity(5000.f); } ``` In the post's Blueprint, at **Event BeginPlay** the pure **Crowdy Has Authority** feeds a **Branch**; the true side calls **Set Intensity** on `Light` from a **Get Light**. [Blueprint snippet: host-gate, see /bp/unreal-sdk/host-gate.txt] :::danger[Host is a convention, not enforcement. Never gate cheat-sensitive state on it.] Nothing on the wire is rejected for not coming from the host, and a modified client can claim to be it. Use the check to pick who runs shared logic; anything a player could gain by winning that check is decided by your server code, on the [truth plane](../concepts/two-planes.md). ::: **Success signal.** With two clients, the post is bright on exactly one of them, the host. The effect is local by design: a post the whole village must see bright makes the intensity a [Crowdy State](./crowdy-state.md) property the host pushes. `crowdy.entity.trace 1` shows the election result on both clients. ## The host subsystem `UCrowdyHostSubsystem` is a world subsystem that tracks the election result and broadcasts it. `IsHost()` is the same answer as `GetCrowdyHasAuthority`; `IsHostSet()` says whether an election result has arrived at all, so an early `IsHost()` false does not mean "someone else is host"; `GetHostID()` is the host's entity id, the id the host's own entities are owned under, and `GetHostUserID()` its raw user id, a different number. The member table, the thread-safety notes, and the server-validated check it carries are on [Host election](../services/host-election.md). Bind `OnHostElected` (`FOnCrowdyHostElected`, `HostID`, `PreviousHostID`, on the game thread) rather than caching the answer at `BeginPlay`: the host changes when the current one leaves, and a host-owned entity's authority moves with it. A world subsystem of your own that lives next to it must null-check `GetGameInstance()` in `Initialize`; [Replicated subsystems](./replicated-subsystems.md#inherit-a-base) says why. ## World entities and the host override A level-placed entity with `Ownership = ECrowdyOwnership::Host` is `HostOwned`: whichever client is host at the moment is its one authority, and its `IsLocallyOwned()` is true there alone. Its Crowdy State is written by the host through explicit pushes (`MarkStateDirty`, `MarkAllStateDirty`, or the [static nodes](./crowdy-state.md#the-two-nodes)) and the keyframe, never by a background diff, so an implicit and an explicit write cannot race. On a host change the entity is re-tracked by the new host without any code of yours. A client-owned entity (`Ownership = LocalClient`) has a `HostOverride` (`ECrowdyHostOverride`): `Allow`, the default, lets the host correct its Crowdy State as a super-user and the owner adopts the value; `OwnerOnly` refuses even a host correction. This is precedence by convention. It orders who wins on the view plane; it does not make either value trustworthy. Two more client-side helpers on `UCrowdyUtilities` answer ownership questions without a round trip: `IsCrowdyEntityHost(Entity)` is true when the entity is the host player's own entity (its NetID equals the host id), and `DoesCrowdyEntityOwn(OwnerActor, TargetActor)` is true when the first actor's player owns the second, resolving a host-owned side to the concrete host id on both ends. `IsCrowdyEntityLocallyControlled(Entity)` is the same question as the component's `IsLocallyOwned`, as a static. ## The server-validated check For the moment a local answer is not enough, ask the server. `UCrowdyHostSubsystem::CheckEntityIsHost(Entity, Callback)` in C++, or the latent **Is Crowdy Entity Host (Server)** node in Blueprint, asks the server whether an entity's owner is the elected host and answers Is Host, Is Not Host, or Failed, where Failed means unknown, never a confirmed no. The lantern world uses it where a wrong answer costs something: a claim on a lantern post is granted only after a definite server yes. The check, its pins, its failure cases, and that example in both languages are on [Host election](../services/host-election.md#the-server-validated-check). The server-validated check is still not enforcement. It tells you the current truth about who the host is; what you do with the answer runs on an unenforced plane. ## Gotchas - `IsHostSet()` before `IsHost()`. Until an election result arrives, every client reads false. - `GetHostID()` and `GetHostUserID()` are different ids. Compare `GetOwnerID()` with the first only for an entity a player spawned; a host-owned entity carries no owner id at all, so ask `GetRole() == ECrowdyRole::HostOwned`, `IsLocallyOwned()`, or `DoesCrowdyEntityOwn` instead. - A host-owned entity is written by explicit pushes. Assigning a property on it and waiting does nothing. A [replicated subsystem](./replicated-subsystems.md) the host owns is the exception: it is auto-diffed. - `HostOverride` is a view-plane tie-breaker. If the value matters, it belongs in your server code. - The server check needs the target to exist on the server. A freshly spawned, never-updated entity answers Failed. ## Related - [The Host Is a Convention](../concepts/host-is-a-convention.md): election as a convention, and the two checks compared. - [Host election](../services/host-election.md): the host subsystem and the server-validated check in full, from the service side. - [Ownership transfer](./ownership-transfer.md): the grant flow the server check feeds. - [Entity component](./entity-component.md): `Ownership`, `HostOverride`, and `IsLocallyOwned`. - [Crowdy State](./crowdy-state.md#host-precedence): what a host-sourced delta does on the receiver. # Ownership Transfer Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/ownership-transfer # Ownership Transfer An entity spawned at runtime belongs to its spawner and a level-placed one to whoever its `Ownership` says. Ownership transfer moves it: a client asks, the entity's current authority grants or ignores, and the grant lands on every client. `UCrowdyOwnershipTransfer` is a static Blueprint library with the four verbs. ## When to use it A player picks up a lantern post that belonged to the level, a vehicle changes driver, a world creature becomes a player's pet. Anything where "who simulates this" should change hands without destroying and respawning. :::warning[Ownership is a coordination convention on the view plane, not enforcement, and a transfer is transient.] No server checks who may request or grant. And a client that first observes the entity after a grant sees the spawn-time owner, or re-baselines from proxy defaults, not necessarily the transferred one. Cheat-sensitive ownership belongs on the [truth plane](../concepts/two-planes.md), in your server code. ::: ## The flow 1. A client that does not own the entity calls **Request Ownership Transfer**. The request travels as a Multicast `CrowdyEvent` on the session channel (`UCrowdyEntityComponent::RequestOwnership_Implementation`), so every client sees it, and only the entity's current authority reacts: the owning client, or the host for a host-owned world entity. 2. On the authority, either `bAutoApproveOwnershipRequests` on the component is on and the grant happens at once, or `UCrowdyEntitySubsystem::OnOwnershipRequested` fires for game code to decide. 3. Game code approves by calling one of the three grant nodes, or ignores the request. There is no deny node; silence is the rejection. 4. The grant travels the same way (`GrantOwnership_Implementation`), every client re-points the entity's record through `UCrowdyEntitySubsystem::ReassignOwnership`, and `OnEntityOwnershipChanged` fires everywhere once ownership has actually moved. ## The four nodes | Node | C++ | Who calls it | |---|---|---| | **Request Ownership Transfer** | `RequestOwnershipTransfer(WorldContextObject, TargetEntity)` | Any client that does not own the entity. A no-op when the target is not a registered entity or this client is already its authority. | | **Grant Ownership Transfer** | `GrantOwnershipTransfer(WorldContextObject, TargetEntity, NewOwner)` | The current authority. `NewOwner` is any entity the new owner's player owns, typically their avatar. | | **Grant Ownership Transfer To Player** | `GrantOwnershipTransferToPlayer(WorldContextObject, TargetEntity, NewOwnerPlayerID)` | The current authority, with the requester's id straight from `OnOwnershipRequested`; no local actor of theirs is needed. | | **Grant Ownership To Host** | `GrantOwnershipToHost(WorldContextObject, TargetEntity)` | The current authority, to make the entity host-owned again. The one verb an actor reference cannot express, because the host is not an actor. | Every node resolves the target's entity component and subsystem and is a safe no-op, never a crash, on a null or unregistered target. All three grants funnel through the component's C++ `GrantOwnershipTo(NewOwnerID)`, which is also a no-op with a warning unless this client is the current authority. :::caution[Grant Ownership Transfer refuses a NewOwner that is not client-owned. It does not fall back to the host.] Passing a host-owned or unregistered actor logs a warning and does nothing, and the log tells you to use **Grant Ownership To Host** instead. The choice between a player and the host is explicit. ::: The lantern world's transfer is a player claiming a lantern post. The post is level-placed and host-owned; the player asks for it, and the host approves. The request: the post's overlap, when this client does not own the post, calls `TryClaim`: ```cpp // LanternPost.h // A player who walks into a post they do not own asks to carry it. virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; void TryClaim(); // LanternPost.cpp #include "Replication/Ownership/CrowdyOwnershipTransfer.h" void ALanternPost::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); if (!CrowdyEntity->IsLocallyOwned()) { TryClaim(); } } void ALanternPost::TryClaim() { UCrowdyOwnershipTransfer::RequestOwnershipTransfer(this, this); } ``` The grant, on whichever client is the authority: `HandleOwnershipRequested`, bound to `OnOwnershipRequested` in `BeginPlay`, ignores requests for other entities and hands the requester's id to `VerifyThenGrant`, which grants behind the server-validated host check on [Host authority](./host-authority.md#the-server-validated-check): ```cpp // LanternPost.h UFUNCTION() void HandleOwnershipRequested(AActor* TargetEntity, AActor* RequesterActor, const FGuid& RequesterID); // LanternPost.cpp #include "Replication/Subsystems/CrowdyEntitySubsystem.h" GetWorld()->GetSubsystem()->OnOwnershipRequested.AddDynamic(this, &ALanternPost::HandleOwnershipRequested); void ALanternPost::HandleOwnershipRequested(AActor* TargetEntity, AActor* RequesterActor, const FGuid& RequesterID) { if (TargetEntity != this) { return; } VerifyThenGrant(RequesterID); } ``` From **Event ActorBeginOverlap**, **Request Ownership Transfer** with `Target Entity` wired to a Self reference; the "not already mine" check the C++ makes is a no-op inside the node anyway, since a request for an entity you already own does nothing: [Blueprint snippet: own-request, see /bp/unreal-sdk/own-request.txt] The grant: at **Event BeginPlay**, a **Crowdy Entity Subsystem** getter feeds **Bind Event to On Ownership Requested**, whose custom event `OnClaimRequested` compares its `Target Entity` with a Self reference (**==** into a **Branch**, because the event fires for every entity this client is the authority of) and, when they match, passes its `Requester ID` to **Grant Ownership Transfer To Player** with `Target Entity` wired to self. Eight nodes; the compare is the guard the C++ has. The [server-checked version](./host-authority.md#the-server-validated-check) puts the host check between the event and the grant: [Blueprint snippet: own-grant, see /bp/unreal-sdk/own-grant.txt] **Success signal.** `OnEntityOwnershipChanged` fires on both clients with the new owner's id, the post's `IsLocallyOwned()` turns true on the requester and false on the host, and `OnCrowdyOwnershipAssigned` on the post's component fires with `bIsLocallyOwned` matching. ## The two events On `UCrowdyEntitySubsystem`, both Blueprint-assignable, both on the game thread: | Event | Parameters | Fires | |---|---|---| | `OnOwnershipRequested` (`FOnCrowdyOwnershipRequested`) | `TargetEntity`, `RequesterActor`, `RequesterID` | On the entity's current authority only, when another client requests it. `RequesterActor` is the requester's avatar when this client holds it, else null; `RequesterID` is always valid. | | `OnEntityOwnershipChanged` (`FOnCrowdyEntityOwnershipChanged`) | `TargetEntity`, `NetID`, `NewOwnerID`, `PreviousOwnerID` | On every client once ownership has actually changed. `NewOwnerID` is invalid when the result is host-owned. | `bAutoApproveOwnershipRequests` is authored config on the component, read on whichever client is the authority when a request arrives. Changing it at runtime on one client affects only the requests that client receives while it holds authority. ## What happens to in-flight state The entity record is the single source of truth for both planes, so re-pointing its owner is the whole transfer: RPC routing and Crowdy State tracking follow. The old owner stops sending the moment it relinquishes; the new owner builds a fresh shadow of the entity's properties and starts diffing; bystanders are untouched. The actor's NetID never changes, so every address stays valid. :::caution[There is a brief window with no writer.] Between the old owner relinquishing and the reliable grant landing on the new owner, nobody diffs the entity. There is never a double writer, but for that short window there is no writer at all, by design. A Dynamic entity's continuous channel moves with the authority the same way: the new owner joins it and the old one leaves. ::: A grant carries the previous owner's id, and `ReassignOwnership` compares and swaps on it, so a stale or duplicate grant is dropped rather than applied twice. ## Gotchas - Silence is the rejection. If you want the requester to know, send them an `OwningClient` event yourself. - The request short-circuits on the component's `IsLocallyOwned()`, so the host of a host-owned entity cannot accidentally request its own world entity into personal ownership. - Granting to a player id with no local presence orphans the entity until it is granted again. Check the requester is still in the session before you grant. - A new owner that never observed the entity re-baselines from proxy defaults. The durable truth lives in your server code, not on the view plane. - Two Multicast events on the component carry all of this. Never give them a `CrowdyChannel`; see [Entity component](./entity-component.md). ## Related - [Host authority](./host-authority.md): the host as the authority for a world entity, and the server-validated check before a grant. - [Entity component](./entity-component.md): `bAutoApproveOwnershipRequests`, `OnCrowdyOwnershipAssigned`, and the two receivers. - [Crowdy State](./crowdy-state.md): the shadow and the diff the transfer restarts. - [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md). # Replicated Subsystems Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/replicated-subsystems # Replicated Subsystems A UE subsystem is not an actor, but it can take part in both view-plane mechanisms: `meta=(CrowdyState)` properties diff and replicate on it exactly as on an actor, and `meta=(CrowdyEvent)` functions ride the reliable channel. Two abstract bases do the enrollment; a function library exists for the cases they do not cover. One difference from a host-owned world actor, whose Crowdy State moves only on explicit marks and the keyframe: a subsystem is auto-diffed on the host, so assign the property and the change ships. Its deltas ride the reliable channel with no spatial filter. ## When to use one For a world-level value that belongs to nobody in particular and every client should agree on: a village clock, a weather phase, a match state on the view plane. A subsystem is host-owned, a singleton every client resolves by class path with no handshake, so it is the natural home for "the one of these". :::danger[Host-owned only, on the view plane. A replicated subsystem is not where a score lives.] Host precedence is a convention here as everywhere on this plane. A match score, a winner, anything a client could gain by lying about, belongs on the [truth plane](../concepts/two-planes.md); a subsystem is for the shared cosmetic state around it. ::: ## Inherit a base | Base | Extends | Enrolls | |---|---|---| | `UCrowdyReplicatedWorldSubsystem` | `UWorldSubsystem` | Once, in its own `Initialize`; unenrolls in `Deinitialize`. Created only in Play in Editor and game worlds. | | `UCrowdyReplicatedGameInstanceSubsystem` | `UGameInstanceSubsystem` | Per world: into each new world's registry when that world's actors are initialized, out again as the world is cleaned up. It outlives worlds, and the entity registry it enrolls into is per world. | Both have one protected hook, `GetReplicatedOwnership()`, which returns `ECrowdyOwnership::Host` and is the only supported answer for a singleton: a per-owner proxy makes no sense for a class every client holds exactly once. The lantern village's night clock is a world subsystem: `ULanternNightSubsystem` holds one Crowdy State property, `NightPhase`, marked `CrowdyHeartbeat` so a late joiner converges, with a notify, `OnRep_NightPhase`, that sets every lantern's intensity from the phase, one CrowdyEvent, `AnnounceDawn`, a Multicast that resets the phase on every client, and a `BlueprintCallable`, `AdvanceNight`, the host calls to step it. ```cpp // LanternNight.h #include "Replication/RPC/CrowdyEvent.h" #include "Replication/Subsystems/CrowdyReplicatedSubsystem.h" #include "LanternNight.generated.h" // A world subsystem replicates both planes at once: a CrowdyState property and a CrowdyEvent. UCLASS() class ULanternNightSubsystem : public UCrowdyReplicatedWorldSubsystem { GENERATED_BODY() public: UPROPERTY(meta = (CrowdyState, CrowdyHeartbeat, CrowdyOnRep = "OnRep_NightPhase")) uint8 NightPhase = 0; UFUNCTION() void OnRep_NightPhase(); UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "Multicast")) void AnnounceDawn_Implementation(); CROWDY_EVENT(AnnounceDawn) UFUNCTION(BlueprintCallable, Category = "Lantern") void AdvanceNight(); }; // LanternNight.cpp #include "LanternNight.h" #include "EngineUtils.h" #include "Lantern.h" void ULanternNightSubsystem::OnRep_NightPhase() { for (TActorIterator It(GetWorld()); It; ++It) { It->Light->SetIntensity(NightPhase * 1000.f); } } void ULanternNightSubsystem::AnnounceDawn_Implementation() { NightPhase = 0; OnRep_NightPhase(); } void ULanternNightSubsystem::AdvanceNight() { ++NightPhase; OnRep_NightPhase(); } ``` The game-instance twin is `ULanternCalendarSubsystem` on the other base: a `DayCount` declared the same way, which survives travel between village levels and needs no override of `Initialize` at all. The block is the declaration only; a notify and a caller are yours to add: ```cpp // LanternNight.h // A game instance subsystem outlives worlds, so a day count survives travel between village levels. UCLASS() class ULanternCalendarSubsystem : public UCrowdyReplicatedGameInstanceSubsystem { GENERATED_BODY() public: UPROPERTY(meta = (CrowdyState, CrowdyHeartbeat)) int32 DayCount = 0; }; ``` A replicated subsystem is defined in C++; a Blueprint may not subclass one. Once the class exists, its Blueprint surface appears by itself: a **Get Lantern Night Subsystem** getter node (the editor offers one for every `BlueprintType` world or game-instance subsystem), and off it every `BlueprintCallable` and `BlueprintPure` the class declares, `Advance Night` here. A `CrowdyState` property with `BlueprintReadWrite` is readable the same way. There is no pasted graph on this page because a graph would name your class, not one the site can ship; the shape is Event BeginPlay, the getter, the call, behind a **Crowdy Has Authority** branch (the [host-gate figure](./host-authority.md#checking-authority)), since only the host's write to a host-owned subsystem ships. :::warning[If you override Initialize or Deinitialize, call Super. Enrollment lives inside the base's override.] There is no separate hook and no runtime check. A subclass that overrides `Initialize` without calling `Super::Initialize` is never enrolled, and nothing it replicates ever leaves the machine. ::: :::danger[A world subsystem must null-check GetGameInstance() in Initialize.] Any further work you add to `Initialize` after `Super::Initialize` runs on a transient game world the engine creates at startup, whose game instance is set only after the world exists. `GetGameInstance()` returns null there, and reaching through it crashes with an access violation on launch that Play in Editor never reproduces. The SDK's own world subsystems guard for it; so must yours. ::: **Success signal.** On the host, call `AdvanceNight` and every client's `OnRep_NightPhase` runs at the next tick; `crowdy.state.trace 1` logs the delta. `AnnounceDawn` shows up in `crowdy.rpc.trace 1` as a reliable send over the session channel. ## What a subsystem cannot do Two consequences of having no actor. :::warning[Every CrowdyEvent on a subsystem needs an explicit CrowdyRecipient of Multicast or Host.] The default recipient, `SpatialMulticast`, needs a world position to send from. A subsystem has none, so the send is rejected with `uses SpatialMulticast, which has no location; set CrowdyRecipient=Multicast or Host. Dropping.` `OwningClient` also works, delivered over the channel and gated on receipt, but on a host-owned singleton the owner is the host, so `Host` says what you mean. ::: - A `CrowdyOwnerOnly` property never transmits on a subsystem: the targeted path addresses an actor's owner, and a host-owned singleton has none. The replicator logs `is non-spatial; owner-only propert(ies) not emitted` once and moves on. - There is no spawn event and no `InitialState`. A subsystem's first values on a late joiner come from its class defaults and then the keyframe, which is why `CrowdyHeartbeat` belongs on every property a newcomer must see. ## The library, for cases the bases do not cover `UCrowdyReplicatedSubsystemLibrary` is what the bases call. **Register Replicated Subsystem** (`RegisterReplicatedSubsystem(Subsystem, Ownership)`, `Subsystem` defaults to self) resolves the entity subsystem from the caller's world and enrolls it, returning the participant's NetID or an invalid `FGuid` on failure; **Unregister Replicated Subsystem** reverses it. In a world with no entity subsystem, which is any editor world, unregister is silent and register logs a warning and enrolls nothing. :::caution[Do not call Register Replicated Subsystem from a game-instance subsystem.] Its world is the ambiguous "current" world. Inherit `UCrowdyReplicatedGameInstanceSubsystem`, which enrolls per world, instead. ::: Underneath both is the two-call pattern on `UCrowdyEntitySubsystem`, `RegisterParticipant(Participant, Ownership)` and `UnregisterParticipant(Participant)`, which any `UObject` may use. If you write it by hand in a world subsystem's `Initialize`, the order below is not optional. :::warning[Initialize both dependencies before enrolling: the entity subsystem, then the state replicator.] `Collection.InitializeDependency(UCrowdyEntitySubsystem::StaticClass())` and then `Collection.InitializeDependency(UCrowdyStateReplicator::StaticClass())`, before the register call. The replicator binds the registration event in its own `Initialize`; enroll first and the broadcast fires before anyone is listening, the subsystem is never tracked, and no later rescan recovers it. Depending on the entity subsystem alone does not pull the replicator in. ::: ## Gotchas - Both bases are `Abstract`. Your concrete subclass is what the engine instantiates. - A subsystem's NetID is the hash of its class path. Rename the class and every client agrees on the new id at the same time, because they all run one build. - The world base is created only in PIE and game worlds. In an editor world it does not exist, and that is correct. - The game-instance base ignores worlds owned by another game instance, so two PIE clients in one process each enroll their own. - A subsystem's traffic shares the session channel with the two ownership-transfer events. Its reliable sends have the same 1024-byte payload cap as any [Multicast call](./rpc-events-cpp.md#containers-and-their-bounds). ## Related - [Crowdy State](./crowdy-state.md): the properties a subsystem replicates. - [RPC events in C++](./rpc-events-cpp.md): the events it declares. - [Recipients and routing](./recipients-and-routing.md): why the default recipient is rejected here. - [Channels](./channels.md): the transport a subsystem's deltas and events ride. - [Host authority](./host-authority.md): who drives a host-owned subsystem. # Rendering Backends Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/rendering-backends # Rendering Backends A rendering backend turns the [continuous state](./continuous-state.md) a remote entity's owner sends into something on screen for everyone else. The SDK ships two: the actor pool, which is the default, and the Crowdy Mass Rendering Backend, which draws remote entities as crowd rows (see [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md)). A project customises the actor pool through two policy classes, or replaces the backend by implementing four methods. ## When you touch this When a Dynamic entity moves on its owner and not on anyone else, when you want your own state struct applied to a proxy, or when pooled actors of the entity's class are the wrong way to draw a crowd; see [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md). A Static entity never comes through here: its proxy is the actor its spawn event made, driven by Crowdy State and events, and a client that joined after the spawn builds it from a repeat of that event. ## The default: the actor pool `FCrowdyActorManagementConfigStruct::BackendClass` on the map profile defaults to `UCrowdyActorPoolBackend`, so a profile that never chose a backend gets this one. It uses `UCrowdyActorPoolSubsystem` to keep a pool of pre-spawned actors per entity class, checks one out when a remote entity appears, registers it in the entity registry so entity-targeted events reach it like any actor, and hands every update to a `UCrowdyRepApplicationPolicy`: the shipped `UCrowdyTransformRepPolicy` unless you provide one. ![The Actor Management category of a map profile: Backend Class and Backend Config](/img/unreal-sdk/map-profile-backend-select.png) The screenshot is the SDK's shipped default profile, and its **Backend Config** reads None: the backend runs on a built-in config in that case. :::note[No Backend Config means the built-in one: the actor pool with the transform policy.] When `BackendConfig` is empty, `UCrowdyActorPoolBackend::InitializeBackend` creates a transient **Actor Pool Backend Config** and, since it names no policy, instantiates `UCrowdyTransformRepPolicy`: it reads the default executor's `FCrowdyActorState` and sets the pooled actor's location and rotation, interpolated across a ring of recent samples with bounded extrapolation. A `BackendConfig` of another backend's class is refused: the backend logs `Backend Config is a , but this backend needs a CrowdyActorPoolBackendConfig` and returns false, the actor manager logs `could not initialize, so no remote entity will be drawn on this map`, and remote entities arriving as continuous-state updates are tracked with nothing on screen. Spawn-event proxies and Crowdy State are unaffected either way. See [Map profiles](./map-profile.md). The fallback arrived in 2.15.0. On 2.14.0 and earlier an empty Backend Config, or one with no Replication Policy Class, tracks remote entities and draws nothing, and the fix there is an **Actor Pool Backend Config** naming `UCrowdyTransformRepPolicy`. See [What's Changed](../guides/whats-changed.md#2026-09-22-sdk-v2150). ::: ### Configure it `UCrowdyActorPoolBackendConfig`, set as the profile's `BackendConfig`: | Field | Default | Effect | |---|---|---| | `ReplicationPolicyClass` | none | Your `UCrowdyRepApplicationPolicy` subclass. Unset means `UCrowdyTransformRepPolicy`, which moves the proxy from `FCrowdyActorState`; set it when your executor sends more than the transform, and move the proxy yourself in it. | | `PoolPolicyClass` | none, optional | A `UCrowdyActorPoolPolicy` subclass. Unset means the concrete base, which already hides pooled actors, shows them on activation, and strips proxy movement. | | `DefaultPoolSizePerClass` | 8 | Pools are created lazily per entity class with this many actors made up front. | | `PerClassPoolOverrides` | empty | A per-class up-front size; a class listed here is also pre-warmed at map load. | | `MaxPoolSizePerClass` | 256 | How far a class's pool may grow. When every actor is in use, the pool spawns one more, up to this cap (never below the up-front size). | | `bRenderLocalOwner` | off | Also draw a pooled copy of entities this client owns, driven by the server's echo of its own updates. Off, the owner sees only its real actor. Turn it on to watch the round trip on a single client. | When an entity's spawn event already made an actor of the entity's class, the pool draws that actor and takes nothing from the pool, so what `OnCrowdySpawned` set on it stays. When ownership of a drawn entity moves to this client, the pool lets go of the actor, which this client now moves; when it moves away, the pool draws this client's actor from the new owner's updates. A pool that is full grows by one actor at a time, spawned during play with collision off until it is activated. At the cap, a new entity of that class waits: it is activated as soon as another entity of the class leaves, or, if its spawn event already made an actor for it, that actor is kept and drawn instead of being replaced. Nothing is dropped. The cap is there so a flood of made-up entity ids from a misbehaving peer cannot make your client spawn without limit; raise it if you legitimately expect more than 256 live remote entities of one class. :::caution[A custom executor needs a matching policy. The default policy reads FCrowdyActorState and refuses anything else.] `UCrowdyTransformRepPolicy` skips an update whose struct is not `FCrowdyActorState`, so a Dynamic entity whose executor sends its own struct stands still until its profile names a policy that reads it. `UCrowdyRepApplicationPolicy` has two pure virtuals and requires a subclass; `UCrowdyActorPoolPolicy` ships full bodies and is meant to be instantiated as is. ::: ### The replication application policy The policy is what moves a remote player's proxy. Continuous state is how pawns, players, and AI move, so a policy's first job is to take the location and rotation out of each update and put the proxy where the owner was at `RenderTimeMs`. The shipped `UCrowdyTransformRepPolicy` does exactly that for `FCrowdyActorState`, and for a pawn that only moves it is all you need. You write your own only when your executor sends more than the transform, and then you apply the transform yourself first and your extra fields after, because a custom policy replaces the shipped one rather than adding to it: `UCrowdyActorPoolBackend::ResolvePolicyClass` instantiates the config's class instead of `UCrowdyTransformRepPolicy`, so a policy that never sets location and rotation leaves every proxy under it standing still. `UCrowdyRepApplicationPolicy` reads a state struct out of each update and applies it to the pooled actor each frame: | Override | Called | What you do | |---|---|---| | `ExtractFields(State, ServerTimestampMs, SlotId)` | Per update, per slot | Read your struct out of `State` into your own per-slot buffers. Return false when the struct type is wrong and the update is skipped. | | `ApplyToActor(Actor, SlotId, RenderTimeMs)` | Per frame, per active slot | Apply the buffered state, interpolated to `RenderTimeMs`, to the actor. | | `OnInstanceActivated(Actor, InitialState)` | Once, when a slot activates | A `BlueprintNativeEvent`: set initial state on the actor. | | `OnInstanceDeactivated(SlotId)` | When the entity leaves | Clean up per-slot state. | | `GetExpectedSlotCount()` | At startup | How many slots to pre-allocate; 64 by default. Match your expected player count. | #### The sample ring Both the shipped policy and the example below keep their per-slot history in `TInterpolatedField`, a small ring buffer the SDK ships in `Data/TInterpolatedField.h`, 32 samples deep by default. `TInterpolatedField::Push(Value, TimestampMs)` stores one sample with the server timestamp the update carried. `TInterpolatedField::Sample(RenderTimeMs, LerpFn)` finds the two samples that bracket the render time by binary search and blends between them with the function you pass (`FMath::Lerp` for a location; a quaternion slerp for a rotation). When the render time runs past the newest sample, `Sample` extrapolates from the last two by calling the same function with an alpha above 1, for at most 0.2 seconds beyond the newest; after that it holds the value extrapolated to 0.2 seconds. The step is measured over at least the average gap of the last four samples (50 ms when only two are held), so two updates that arrive almost together after a sender hitch extrapolate at the sender's normal pace instead of throwing the proxy far ahead for a frame. A render time older than the oldest sample holds the oldest sample; a slot is in that state only briefly after activation, until the render time, which runs one interpolation delay behind, catches up with the samples already held. A location follows that extrapolation in full; the rotation slerp clamps its alpha to 1.2, so a rotation extrapolates by at most a fifth of a step. With one sample it returns that sample; with none, the default you pass. `PhysicalIndex` is the ring's own index arithmetic, nothing you call. The reason a proxy built on it does not jitter is the render time itself: the actor manager renders at the estimated server time minus the interpolation delay (100 ms), so the proxy is always drawn a little behind the newest sample, between two samples it already holds, and never snaps to the latest one as it lands. Extrapolation only runs when an update is late, and its cap keeps a lost stream from sliding a proxy across the map. #### The example: the same shape, with your own field added `ULanternRepPolicy` is what the shipped policy does, plus one field. `FLanternPlayerState`, the struct the [continuous state](./continuous-state.md) page's executor sends for `ALanternPlayer`, carries `Location`, `Rotation`, and `bTorchLit`. The policy keeps one `TInterpolatedField` and one `TInterpolatedField` per slot: `ExtractFields` refuses any other struct type and pushes the location and rotation with the update's server timestamp; `ApplyToActor` samples both at `RenderTimeMs`, calls `SetActorLocationAndRotation` on the proxy, and only then applies the torch flag to the proxy's `Torch` light. The flag is applied as the latest value: a boolean is not something to blend. `OnInstanceDeactivated` resets the slot so a reused slot never starts from a departed player's history. The policy header includes `LanternPlayer.h` for the struct, so this snippet depends on the [`executor-override` block](./continuous-state.md#a-custom-executor) being in your project first. ```cpp // LanternRepPolicy.h #include "Data/CrowdyRepApplicationPolicy.h" #include "Data/TInterpolatedField.h" #include "LanternPlayer.h" #include "LanternRepPolicy.generated.h" // Replaces the shipped transform policy, so it moves the proxy itself and then applies the one field of its own. UCLASS() class ULanternRepPolicy : public UCrowdyRepApplicationPolicy { GENERATED_BODY() public: virtual bool ExtractFields(const FInstancedStruct& State, int64 ServerTimestampMs, int32 SlotId) override; virtual void ApplyToActor(AActor* Actor, int32 SlotId, int64 RenderTimeMs) override; virtual void OnInstanceDeactivated(int32 SlotId) override; private: TArray> Positions; TArray> Rotations; TArray bTorchLitBySlot; void EnsureSlot(int32 SlotId); }; // LanternRepPolicy.cpp #include "LanternRepPolicy.h" #include "Components/PointLightComponent.h" bool ULanternRepPolicy::ExtractFields(const FInstancedStruct& State, int64 ServerTimestampMs, int32 SlotId) { const FLanternPlayerState* Player = State.GetPtr(); if (!Player) { return false; } // Each slot keeps a ring of timestamped samples; Sample reads them back at whatever render time the pool asks for. EnsureSlot(SlotId); Positions[SlotId].Push(Player->Location, ServerTimestampMs); Rotations[SlotId].Push(Player->Rotation, ServerTimestampMs); bTorchLitBySlot[SlotId] = Player->bTorchLit; return true; } void ULanternRepPolicy::EnsureSlot(int32 SlotId) { if (SlotId >= Positions.Num()) { Positions.SetNum(SlotId + 1); Rotations.SetNum(SlotId + 1); bTorchLitBySlot.SetNumZeroed(SlotId + 1); } } ``` ```cpp // LanternRepPolicy.cpp void ULanternRepPolicy::ApplyToActor(AActor* Actor, int32 SlotId, int64 RenderTimeMs) { if (!Positions.IsValidIndex(SlotId)) { return; } const FVector Location = Positions[SlotId].Sample(RenderTimeMs, [](const FVector& A, const FVector& B, float T) { return FMath::Lerp(A, B, T); }); const FRotator Rotation = Rotations[SlotId].Sample(RenderTimeMs, [](const FRotator& A, const FRotator& B, float T) { return FQuat::Slerp(A.Quaternion(), B.Quaternion(), FMath::Clamp(T, 0.f, 1.2f)).GetNormalized().Rotator(); }); // The transform first, then the field of your own: the proxy's torch follows the owner's. Actor->SetActorLocationAndRotation(Location, Rotation); if (const ALanternPlayer* Player = Cast(Actor)) { Player->Torch->SetVisibility(bTorchLitBySlot[SlotId]); } } void ULanternRepPolicy::OnInstanceDeactivated(int32 SlotId) { if (!Positions.IsValidIndex(SlotId)) { return; } Positions[SlotId] = {}; Rotations[SlotId] = {}; bTorchLitBySlot[SlotId] = false; } ``` `ExtractFields` and `ApplyToActor` are C++ only, so the policy class is C++. Set it as **Replication Policy Class** on the Actor Pool Backend Config in the map profile's Details panel. `OnInstanceActivated` is the one hook a Blueprint subclass of the policy may implement. ### The pool policy `UCrowdyActorPoolPolicy` decides what a pooled actor does at three moments, each a `BlueprintNativeEvent` a Blueprint subclass can implement without C++: `OnActorPooled(Actor)` once per actor right after it is spawned into the pool, `OnActorActivated(Actor, InitialState)` when it is checked out (its `InitialState` is empty in the shipped pool, and the identity lands on the component right after it returns), and `OnActorDeactivated(Actor)` when it goes back. The base hides and shows the actor and disables proxy movement; subclass only to add per-actor behaviour at those moments. Pooled actors are genuinely spawned (`SpawnActorDeferred` and `FinishSpawning`), so their constructors and Blueprint-added components run. Before `BeginPlay` the pool marks any entity component on them dormant, so a pre-warmed actor registers no entity and no listener while it stands for nothing. `UCrowdyActorPoolSubsystem::RegisterPool(FCrowdyPoolConfig)` (`ActorClass`, `PoolPolicyClass`, `PoolSize`, `MaxPoolSize`) is what the backend calls to create a pool, a no-op for a class that already has one, and `HasPool(Class)` says whether one exists; `AcquireActor` checks an actor out, growing the pool up to `MaxPoolSize` when every actor is in use, and `ReleaseActor` checks it back in. `crowdy.pool.trace 1` logs spawn, release, reuse, and growth. ## What a pooled proxy does and does not do :::warning[A pooled Dynamic proxy never sees the spawn event's InitialState.] The spawn event creates a proxy actor and broadcasts `OnCrowdySpawned` with the payload; the actor pool then treats that actor as an orphan, destroys it, and swaps in a pre-warmed pooled actor that never received it. Anything a proxy needs to look right must be a class default or a field of the replicated state struct, never a one-time spawn value. ::: :::warning[The owner of a client-spawned Dynamic entity sees two visuals.] The pool is fed only by inbound network state, so the owner ends up with the local actor it drives directly and the pooled proxy the server echoes back, interpolation-delayed. There is no per-entity flag to choose. Hide one side yourself, in the entity's own code: `SetActorHiddenInGame` on the local actor when `IsLocallyOwned()` if the round-tripped proxy is the one that should show. With **Enable Owner Tracking** on (the default) the local player's own avatar is echoed and drawn the same way; turning it off suppresses that one echo and no other owned entity's. ::: - A proxy's `GetNetID()` and `GetRole()` work: the backend assigns the identity to the pooled actor's component when it checks it out. `RemoteProxy` is the role it reports. ## Swapping at runtime `UCrowdyActorManager::SetBackend(NewBackend)` (Blueprint: **Set Backend**) swaps the active backend: the previous one is deinitialized, slot state is preserved, and the new one receives `ApplyInterpolation` from the next tick. The new backend must already be initialized, and `InitializeBackend` is C++ only, so this is a C++ operation in practice. `GetActiveBackend()` is the live backend, null when none initialized; a backend that refused to initialize is absent here and present in the profile, which is how to tell the two apart from code. ## Writing a backend Subclass `UCrowdyRenderingBackend` and pair it with a `UCrowdyRenderingBackendConfig` subclass carrying whatever your backend needs (an instanced mesh, a pool size). Four overrides are required and three optional: | Override | Required | Contract | |---|---|---| | `ActivateInstance(SlotId, UUID, EntityClass, InitialState)` | yes | A remote instance became visible. Acquire the rendering resource for `SlotId`. `EntityClass` is never null. | | `DeactivateInstance(SlotId, UUID)` | yes | It left. Release the resource and the per-slot state. | | `ExtractUpdate(State, ServerTimestampMs, SlotId)` | yes | An update arrived. Parse the struct into your interpolation buffers. | | `ApplyInterpolation(SlotId, RenderTimeMs)` | yes | Apply the interpolated state; `RenderTimeMs` is already offset by the interpolation delay. | | `InitializeBackend(World, Config)` | no | Acquire subsystems, read the config. Return false, after logging the reason and the remedy, when you cannot draw with what you were given; the manager refuses you rather than installing you. | | `DeinitializeBackend()` | no | Release what you held. | | `IsInstanceActive(SlotId)` | no | Whether `SlotId` holds something drawn. Return false when `ActivateInstance` could not acquire a resource (a full pool), and the actor manager calls `ActivateInstance` again once capacity may have freed or the entity registers. The default, true, never retries. | `DrawsEntitiesAsCrowdRows()` is one more, for a backend that represents entities without spawning an actor of the entity's class; the editor reads it off the class default object to decide whether to show authoring surfaces that only make sense on that representation. The default is false. The Crowdy Mass Rendering Backend that ships in the SDK is the one backend that returns true; choose it as the map profile's **Backend Class** and see [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md). `SlotId` is a stable index the actor manager assigns for the instance's lifetime; keep your per-instance state in arrays indexed by it. :::danger[A custom backend must never subscribe to the actor tracker itself.] The actor manager is already the single caller of `ActivateInstance` and `DeactivateInstance`. A backend that also subscribes to `UCrowdyActorTracker` activates every entity twice and spawns everything double. The four overrides are your only entry points. ::: ## Gotchas - `BackendClass` alone draws the default executor's movement through `UCrowdyTransformRepPolicy`. A custom state struct needs `BackendConfig` with a `ReplicationPolicyClass` that reads it, and that policy replaces the shipped one: apply `Location` and `Rotation` yourself before anything else, or every proxy stands still. - A policy built on `TInterpolatedField` is the smooth choice for anything that moves. Sample it at `RenderTimeMs`, never at the newest sample: the delay is what hides the network. - `bUseCrowdyActorTracker` off on the profile means no tracker, no manager, and no backend at all. - A pooled proxy's spawn-time look comes from class defaults or the state struct, never the spawn payload. - A pool at its cap warns once per class, naming the class and `MaxPoolSizePerClass`; each entity it could not draw is a `crowdy.pool.trace` line, not a warning, since it is retried. Raise `MaxPoolSizePerClass` if you see the warning. On 2.16.0 and earlier, a pool never grew past `DefaultPoolSizePerClass` (8), every entity past that was dropped with a warning and never retried, and its spawn-event actor was destroyed first; see [What's Changed](../guides/whats-changed.md#2026-09-26-sdk-v2170). - A class that arrives over the wire before it is loaded cannot be drawn; see Preloaded Entity Classes on [Entities and spawning](./entities-and-spawning.md). ## Related - [Map profiles](./map-profile.md): where `BackendClass` and `BackendConfig` live. - [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md): the Crowdy Mass Rendering Backend, which draws remote entities as crowd rows. - [Continuous state](./continuous-state.md): the executor that produces what the policy consumes. - [Entities and spawning](./entities-and-spawning.md): the actor tracker and the spawn event. - [Entity component](./entity-component.md): the identity a pooled actor is given. # Channels Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/channels # Channels A channel is a named message group within one app: create it, join it, publish to it, and every member receives the message wherever they stand. It is also the transport a `Multicast` CrowdyEvent and a [replicated subsystem](./replicated-subsystems.md)'s state deltas ride, and every client joins one channel, the session channel, whether or not it ever creates another. `UCrowdyChannels` is a game-instance subsystem. Every server call on it is asynchronous and answers through exactly one of two delegates, success or `FOnChannelError`, including when the request never reaches the server. Publishing is not one of those calls: it goes out over UDP and is not acknowledged. ## When to use one Guild chat, a party's coordination, a village-wide announcement, trade broadcasts: anything that must reach a group regardless of distance. For a moment that only nearby players need, a `SpatialMulticast` event is cheaper; for a value, [Crowdy State](./crowdy-state.md), or your server code if it is [truth](../concepts/two-planes.md). ## The session channel Every app has one channel every client joins on connect, named `__crowdy_session_`, created on demand when it does not exist and the app's creation policy lets a member create one (otherwise create it once in Crowdy Studio, or the log warns that default-channel events will drop). It is an SDK-owned transport, not a place for your own messages. It carries every `Multicast` CrowdyEvent with no `CrowdyChannel` of its own and every replicated subsystem's Crowdy State delta. :::warning[A message you publish on the session channel, or on any channel a Multicast event names, never reaches OnChannelMessageReceived.] Every channel the connect-time bootstrap joins (the session channel and each channel a Multicast event names) and every channel you hand to `RegisterReliableRpcChannel` is an RPC transport: a payload arriving on it goes to the RPC decoder and is never broadcast to the game. Publish your own notices on a channel of your own, one channel per purpose. ::: `GetSessionChannelId()` is its resolved id, 0 until joined or created, and `AreReliableChannelsReady()` turns true once the connect-time bootstrap has joined every channel the app's Multicast events name plus the session channel. Both are pure nodes you can poll. :::warning[A channel a Multicast event names must already exist. Only the session channel is created for you.] The bootstrap joins named channels; it never invents one, because a missing name is usually a typo. It logs `[CrowdyChannels] Multicast channel 'X' was not found for this app - RPCs targeting it will drop. Create it (or fix the name) in Crowdy Studio.` A channel you create at runtime after the bootstrap has run needs `RegisterReliableRpcChannel(ChannelId, Name)` once you have joined it, or events naming it still drop. Registering it makes it an RPC transport: raw `PublishChannelMessage` payloads on it are decoded as RPC frames and are not delivered to `OnChannelMessageReceived`, so keep one channel per purpose. ::: ## Create and join `CreateChannel(Name, Description, MembershipPolicy, bMembersCanSend, OnSuccess, OnError)` creates one; the creator becomes its owner with the system `leader` role, which holds every permission including `send_messages`. `ECrowdyChannelMembershipPolicy` decides how others get in: `Open` (join at once), `Request` (a join request awaits approval), `Invite` (managers add members), `Admin` (app admins only). `bMembersCanSend` true creates a default `member` role granting `send_messages` to everyone who joins; false makes an announce-only channel where joiners receive but cannot post until given a role. The app's `FCrowdyChannelPolicy` (read with `GetChannelPolicy`) holds who may create channels at all (`ECrowdyChannelCreationPolicy`: `Admin`, `Member`, `Anyone`), the default membership policy, and the caps `MaxMembers` and `MaxChannelsPerUser` (0 means none); `SetChannelPolicy` sets the two policies, and the caps are set in Crowdy Studio. The lantern village creates a `village` channel for lit notices once the connection is up: `CreateVillageChannel`, called from the game instance's connected handler (the create needs the game token), and `HandleVillageChannelCreated` stores the id behind `GetVillageChannelId`, the one place the id lives. ```cpp // LanternGameInstance.h #include "Queries/Data/Channels/Types/FCrowdyChannel.h" UFUNCTION(BlueprintPure, Category = "Lantern") int64 GetVillageChannelId() const { return VillageChannelId; } private: int64 VillageChannelId = 0; void CreateVillageChannel(); UFUNCTION() void HandleVillageChannelCreated(FCrowdyChannel Channel); // LanternGameInstance.cpp #include "Subsystem/CrowdyChannels.h" void ULanternGameInstance::CreateVillageChannel() { // Called once connected (the create needs the game token); one client creates, the rest join. UCrowdyChannels* Channels = GetSubsystem(); FOnChannelSuccess Created; Created.BindDynamic(this, &ULanternGameInstance::HandleVillageChannelCreated); Channels->CreateChannel(TEXT("village"), TEXT("Lantern lit notices"), ECrowdyChannelMembershipPolicy::Open, true, Created, FOnChannelError()); } void ULanternGameInstance::HandleVillageChannelCreated(FCrowdyChannel Channel) { VillageChannelId = Channel.ChannelId; } ``` In the Game Mode Blueprint, at **Event BeginPlay** a **Crowdy SDK Subsystem** getter feeds **Bind Event to On UDP Connection Success**, whose custom event `OnConnected` calls **Create Channel**, a latent node with `Name`, `Description`, `Membership Policy`, and `Members Can Send` inputs and **On Success** (a `Channel` struct) and **On Error** pins. Binding to the connection is what keeps the create after sign-in; at BeginPlay there may be no token yet and the create would answer on **On Error**. The Game Mode belongs to one map, so keep the id in a variable on the Game Instance if the next map needs it. (Do not start from the Game Instance's **Event Init**: it runs before the SDK's subsystems exist, see [Authentication](../services/authentication.md#signing-in).) To keep the id, drag a **Break Crowdy Channel** off the `Channel` pin on the success branch and set an Integer64 variable `VillageChannelId` from `Channel Id`. [Blueprint snippet: ch-create, see /bp/unreal-sdk/ch-create.txt] Every connecting client running this asks the server to create the channel. A real game has one client create it, the host for instance, or calls `GetChannels` first and creates only on a miss; the server's creation policy is the real gate on who may. `JoinChannel(ChannelId, ...)` joins an open channel and `RequestToJoinChannel` files a request on a `Request` channel; `LeaveChannel` leaves. The creator is already a member and needs neither, though the local cache (`HasCachedChannels`, `IsPlayerInChannel`) does not show the new channel until `GetMyChannels` has run. ## Ban a member A manager removes a member for good with `BanChannelMember(ChannelId, UserId, Reason, OnSuccess, OnError)`. It takes the user out of the channel and refuses every later join, request to join and add of that user until a manager lifts the ban with `UnbanChannelMember(ChannelId, UserId, OnSuccess, OnError)`. The caller needs the manage-members permission, and the channel's owner and the caller themselves cannot be banned. A user who is not a member 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, whose `Status` is `banned`; unban answers `bWasBanned`, which is false when the user had no ban. The village's elder bans a member from the `village` channel. `VillageChannelId` is the id the game instance kept when it created the channel. A Ban button on the member list calls `BanFromVillageChannel` with the member picked. `HandleChannelBanned` receives the member record, and `HandleChannelBanFailed` the error. ```cpp // LanternGameInstance.h #include "Queries/Data/Channels/Types/FCrowdyChannelMember.h" #include "Queries/Data/Channels/Types/FCrowdyChannelError.h" public: // A Ban button on the member list calls this for the member picked. void BanFromVillageChannel(int64 UserId, const FString& Reason); UFUNCTION() void HandleChannelBanned(FCrowdyChannelMember Member); UFUNCTION() void HandleChannelBanFailed(FCrowdyChannelError Error, FString Message); // LanternGameInstance.cpp #include "Subsystem/CrowdyChannels.h" void ULanternGameInstance::BanFromVillageChannel(int64 UserId, const FString& Reason) { UCrowdyChannels* Channels = GetSubsystem(); FOnChannelMemberSuccess Banned; Banned.BindDynamic(this, &ULanternGameInstance::HandleChannelBanned); FOnChannelError Failed; Failed.BindDynamic(this, &ULanternGameInstance::HandleChannelBanFailed); // Needs the manage-members permission; the owner and yourself cannot be banned. A Reason over 500 characters fails before it is sent. Channels->BanChannelMember(VillageChannelId, UserId, Reason, Banned, Failed); } void ULanternGameInstance::HandleChannelBanned(FCrowdyChannelMember Member) { UE_LOG(LogTemp, Log, TEXT("User %lld is banned from the village channel"), Member.UserId); } void ULanternGameInstance::HandleChannelBanFailed(FCrowdyChannelError Error, FString Message) { UE_LOG(LogTemp, Warning, TEXT("The ban failed: %s"), *Message); } ``` **Ban Channel Member** is a latent node with `Channel Id`, `User Id` and `Reason` inputs. The figure bans the user that the `OnBanPressed` event passes in, for a fixed reason. **On Success** carries the member record; **On Error** carries `Error` and `Message`, and the figure opens `Error` with **Break Crowdy Channel Error** to print its `Code` and the `Message`. [Blueprint snippet: ch-ban-node, see /bp/unreal-sdk/ch-ban-node.txt] ## Handle a ban on join A banned user's `JoinChannel`, `RequestToJoinChannel`, or a manager's `AddChannelMember` for them, fails with the error code `Banned` in `ECrowdyChannelErrorCode`. The code 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. A Join the Village button calls `JoinVillageChannel`. Its failure handler checks the code first. ```cpp // LanternGameInstance.h #include "Queries/Data/Channels/Types/FCrowdyChannelError.h" public: // A Join the Village button calls this. void JoinVillageChannel(); UFUNCTION() void HandleVillageJoinFailed(FCrowdyChannelError Error, FString Message); // LanternGameInstance.cpp #include "Subsystem/CrowdyChannels.h" void ULanternGameInstance::JoinVillageChannel() { FOnChannelError Failed; Failed.BindDynamic(this, &ULanternGameInstance::HandleVillageJoinFailed); GetSubsystem()->JoinChannel(VillageChannelId, FOnChannelMemberSuccess(), Failed); } void ULanternGameInstance::HandleVillageJoinFailed(FCrowdyChannelError Error, FString Message) { // A ban stays until a manager lifts it, so trying again is pointless; tell the player instead. if (Error.Code == ECrowdyChannelErrorCode::Banned) { UE_LOG(LogTemp, Warning, TEXT("You are banned from the village channel: %s"), *Message); return; } UE_LOG(LogTemp, Warning, TEXT("Could not join the village channel: %s"), *Message); } ``` On the **Join Channel** node's **On Error** pin, open `Error` with **Break Crowdy Channel Error** and compare `Code` with **Banned** in a **Branch**; the figure prints the `Message` on the banned branch. [Blueprint snippet: ch-banned-join, see /bp/unreal-sdk/ch-banned-join.txt] ## Publish and receive `PublishChannelMessage(ChannelId, Payload)` sends raw bytes to every active member except the sender. The caller must already be a member holding `send_messages`; an open channel's default member role grants it. The server caps a channel message payload at 1024 bytes. `PublishChannelMessageAs(ChannelId, SenderUuid, Payload)` sends under a sender id you choose, as the Game Kit's chat does, since the session's own id is empty until the player's entity resolves; a receiver sees that id, which proves nothing. `CanPublishChannelMessage()` is true once the game session exists and the realtime connection is routing. :::warning[Publishing is unacknowledged UDP: no ordering, no delivery guarantee, and the caller is never told if it was dropped.] It is the right tool for a notice, not for a value. Put anything that must be right on every client in a state property, or in your server code if it is truth. ::: The lantern's `PublishLit`, called from the owner-gated overlap, publishes a one-byte notice, its lit flag, on the village channel, reading the id from the game instance's `GetVillageChannelId`: ```cpp // Lantern.cpp #include "LanternGameInstance.h" #include "Subsystem/CrowdyChannels.h" void ALantern::PublishLit() { // Tell every village member the lantern changed, over the channel the game instance created. const ULanternGameInstance* Game = GetGameInstance(); UCrowdyChannels* Channels = Game ? Game->GetSubsystem() : nullptr; if (Channels) { Channels->PublishChannelMessage(Game->GetVillageChannelId(), { uint8(bLit) }); } } ``` From **Event ActorBeginOverlap**, behind an **Is Crowdy Entity Locally Controlled** branch (the owner gate the C++ has): a **Crowdy Channels** subsystem getter feeds the `Target`, **Get VillageChannelId** (the Integer64 variable the create graph set) the `Channel Id`, and a one-element **Make Array** the `Payload` byte array of **Publish Channel Message**. The array's one element is the lit byte, 0 in the figure; wire your lit flag into it. It is a plain call with no result pins. [Blueprint snippet: ch-publish, see /bp/unreal-sdk/ch-publish.txt] `OnChannelMessageReceived` fires on the game thread for every delivery, with the channel id, the sender's `SenderUUID`, and the payload. The village's `HandleVillageNotice`, bound by `WatchVillageChannel` from `Init`, reads the one byte and sets every lantern's visibility from it; the check on the payload's length before the read is the validation the caution below asks for. ```cpp // LanternGameInstance.h void WatchVillageChannel(); UFUNCTION() void HandleVillageNotice(int64 ChannelId, FString SenderUUID, const TArray& Payload); // LanternGameInstance.cpp #include "EngineUtils.h" #include "Lantern.h" #include "Subsystem/CrowdyChannels.h" void ULanternGameInstance::WatchVillageChannel() { GetSubsystem()->OnChannelMessageReceived.AddDynamic(this, &ULanternGameInstance::HandleVillageNotice); } void ULanternGameInstance::HandleVillageNotice(int64 ChannelId, FString SenderUUID, const TArray& Payload) { // A channel message is not server-signed; validate it before acting. Here the one byte is the lit flag. const bool bLit = Payload.Num() > 0 && Payload[0] != 0; for (TActorIterator It(GetWorld()); It; ++It) { It->Light->SetVisibility(bLit); } } ``` There is no Blueprint figure for the receive side: `OnChannelMessageReceived` carries a byte-array `Payload`, and the generated graphs on this site cannot express a bound event with an array parameter. In your own Blueprint, drag off a **Crowdy Channels** subsystem getter, choose **Bind Event to On Channel Message Received**, and create the matching custom event from the `Event` pin; it receives `Channel Id`, `Sender UUID`, and `Payload` as an array of bytes. :::caution[A channel message is not signed on the way down. Validate the payload before acting on it.] The server does not sign the delivery, and any member with `send_messages` could have sent it. Treat the bytes as untrusted input: bound every length, clamp every value, and never let one decide an outcome that must hold against a modified client. The SDK does not filter payloads on a channel: every payload you are sent reaches your handler, whatever text it begins with. ::: ## The full surface Cache and queries, on `UCrowdyChannels`: `HasCachedChannels` (has `GetMyChannels` ever completed), `GetCachedMyChannels` (the memberships, no round trip), `IsPlayerInChannel`, `GetMyChannelById`, `IsInAnyChannel`, `HasPermissionInChannel` (an `ECrowdyChannelPermission`: `SendMessages`, `ManageChannel`, `ManageMembers`, `ManageRoles`, `InviteMembers`), `GetMyChannels`, `GetChannel`, `GetChannels`, `GetChannelMembers`, `GetChannelRoles`, `GetChannelPolicy`, `GetPendingJoinRequests`. `OnMyChannelsCacheChanged` fires when the cached membership list changes. Mutations: `CreateChannel`, `UpdateChannel`, `DeleteChannel`, `JoinChannel`, `RequestToJoinChannel`, `LeaveChannel`, `AddChannelMember`, `RemoveChannelMember`, `BanChannelMember`, `UnbanChannelMember`, `CreateChannelRole`, `UpdateChannelRole`, `DeleteChannelRole`, `SetChannelMemberRoles`, `SetChannelPolicy`. Every query and mutation also exists as a latent Blueprint node with **On Success** and **On Error** pins, one class each: `UCrowdyChannels_GetMyChannels` (**Get My Channels**), `UCrowdyChannels_GetChannel`, `UCrowdyChannels_GetChannels` (**Get All Channels**), `UCrowdyChannels_GetChannelMembers`, `UCrowdyChannels_GetChannelRoles`, `UCrowdyChannels_GetChannelPolicy`, `UCrowdyChannels_GetPendingJoinRequests`, `UCrowdyChannels_CreateChannel`, `UCrowdyChannels_UpdateChannel`, `UCrowdyChannels_DeleteChannel`, `UCrowdyChannels_JoinChannel`, `UCrowdyChannels_RequestToJoinChannel` (**Request to Join Channel**), `UCrowdyChannels_LeaveChannel`, `UCrowdyChannels_AddChannelMember`, `UCrowdyChannels_RemoveChannelMember`, `UCrowdyChannels_BanChannelMember` (**Ban Channel Member**), `UCrowdyChannels_UnbanChannelMember` (**Unban Channel Member**), `UCrowdyChannels_SetChannelMemberRoles`, `UCrowdyChannels_CreateChannelRole`, `UCrowdyChannels_UpdateChannelRole`, `UCrowdyChannels_DeleteChannelRole`, `UCrowdyChannels_SetChannelPolicy`. A list result arrives wrapped (`FCrowdyChannelsResult`, `FCrowdyMyChannelsResult`, `FCrowdyChannelMembersResult`, `FCrowdyChannelRolesResult`) so a **Break** node unwraps it. `PublishChannelMessage` has no latent twin; it is a plain call on the subsystem. **On Success** and **On Error** of a node share one set of data pins: the result (`Channel`, `Member`, `Channels`, and so on; a node that returns nothing has none), then `Error` and `Message`. On **On Success**, `Error` and `Message` are empty. On **On Error** the result is empty, `Error` is an `FCrowdyChannelError` (break it to read its `Code`) and `Message` is the text to show. The types: | Type | Holds | |---|---| | `FCrowdyChannel` | `ChannelId`, `AppId`, `Name`, `Description`, `OwnerUserId`, `MembershipPolicy`, `Status`, `CreatedAt`. | | `FCrowdyChannelMember` | `ChannelMemberId`, `ChannelId`, `UserId`, `Status` (`active`; `pending` while a request waits; `banned` in the answer to a ban), `Roles` sorted by rank, `CreatedAt`. | | `FCrowdyChannelMembership` | Your own membership of one channel: `Channel`, `Roles`, the effective `Permissions` across them, `JoinedAt`. | | `FCrowdyChannelRole` | `ChannelRoleId`, `ChannelId`, `RoleName`, `Rank`, `bIsSystem`, `Permissions`, `CreatedAt`. Every channel has a non-deletable `leader` role. | | `FCrowdyChannelPermissions` | The five flags: `bSendMessages`, `bManageChannel`, `bManageMembers`, `bManageRoles`, `bInviteMembers`. | | `FCrowdyChannelPolicy` | `AppId`, `CreationPolicy`, `DefaultMembershipPolicy`, `MaxMembers`, `MaxChannelsPerUser`. | | `FCrowdyChannelError` | An `ECrowdyChannelErrorCode` (`Unknown`, `NotFound`, `Forbidden`, `PolicyViolation`, `AlreadyMember`, `NotMember`, `NetworkError`, `ServerError`, `Banned`) and the `Message` to show. All but `Banned` are a best-effort classification of the message text; `Banned` comes from the server's own error code. | Every id is an `int64`; the server's ids are 64-bit. In C++ each call takes a pair of dynamic delegates: a success type shaped for its result (`FOnChannelSuccess` with an `FCrowdyChannel`, `FOnChannelsSuccess` with the array, `FOnChannelMemberSuccess`, `FOnChannelMembersSuccess`, `FOnChannelRoleSuccess`, `FOnChannelRolesSuccess`, `FOnMyChannelsSuccess` with the memberships, `FOnChannelPolicySuccess`, `FOnChannelVoidSuccess` for the calls that return nothing, `FOnChannelUnbanSuccess` with a `bWasBanned` for an unban) and `FOnChannelError`. The two events are `FOnMyChannelsCacheChanged` and `FOnChannelMessageReceived`. The latent nodes expose the same shapes as multicast pins that also carry `Error` and `Message`: `FChannelAsyncResult`, `FChannelsAsyncResult`, `FMyChannelsAsyncResult`, `FChannelMemberAsyncResult`, `FChannelMembersAsyncResult`, `FChannelRoleAsyncResult`, `FChannelRolesAsyncResult`, `FChannelPolicyAsyncResult`, `FChannelUnbanAsyncResult`, and `FChannelAsyncOnError` for the nodes that return nothing. ## What rides a channel from the SDK itself A `Multicast` CrowdyEvent is encoded into a channel payload and published over the session channel, or the channel its `CrowdyChannel` names once this client has joined it. A replicated subsystem's Crowdy State delta travels the same way. Both are capped at 1024 bytes by the SDK's own codec and dropped loudly, never truncated, when they would exceed it; a Multicast event whose parameters can never fit is refused at registration. "Reliable" on this path means every member is a recipient, not that delivery is guaranteed; see [Recipients and routing](./recipients-and-routing.md#multicast-what-reliable-means). `crowdy.rpc.reliable.trace 1` logs the reliable sends and receives on the channel transport. ## Gotchas - The session channel is always joined, even in a project with no Multicast event. - A named channel is join-only from the SDK's side. Create it in Crowdy Studio or at runtime, then `RegisterReliableRpcChannel` if you created it after connecting. - `send_messages` gates publishing, not membership. An announce-only channel is a channel whose joiners lack it. - The sender never receives an echo of its own message. - Channel management is a Game API call and needs the app-scoped token the sign-in gives you; see [Authentication](../services/authentication.md). The wire protocol and the GraphQL calls are on the [Game API channels page](/game-api/channels). ## Related - [Recipients and routing](./recipients-and-routing.md): the Multicast recipient. - [RPC events in C++](./rpc-events-cpp.md): the `CrowdyChannel` key and the payload cap. - [Replicated subsystems](./replicated-subsystems.md): deltas over the session channel. - [Connection and reconnect](./connection-and-reconnect.md): when the channel bootstrap runs. - [Teams and channels in Studio](../studio/teams-and-channels.md): authoring channels without code. - [Play together](../services/play-together.md): chat rooms and party chat as text, with Send Chat and On Chat Message. # Connection and Reconnect Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/connection-and-reconnect # Connection and Reconnect The realtime connection opens after sign-in, re-assigns itself a server when it loses one, and tells the game only when that fails. This page is what a game can read about the connection, what it should react to, and the two knobs that shape the traffic. ## When you need this A connection indicator in the HUD, a "reconnecting" overlay, a decision about what to show while the world is not being updated. Everything else on the view plane depends on the connection and needs nothing from this page. ## The states `UCrowdySDKSubsystem::GetUDPConnectionState()` (Blueprint: **Get UDP Connection State**) returns an `EUDPConnectionState`: | State | Meaning | |---|---| | `Disconnected` | Not connected, and no reconnect in progress. | | `Connecting` | The connection is being opened. | | `Connected` | Up and carrying traffic. | | `Reconnecting` | The connection lost its server and is assigning another. Reported, not acted on: it recovers on its own. | | `GateKeep` (shown as Gate Kept) | The server is gatekeeping this client because the app is full. Nothing retries; the player has already been told. | :::note[Poll it. No delegate is needed for a status indicator.] `GetUDPConnectionState` is a pure node, safe to read every frame. Reserve the events below for the moments you must react to. ::: The events on `UCrowdySDKSubsystem`, all Blueprint-assignable: **On UDP Connection Success** (`OnUDPConnectionSuccess`, an `FOnUDPConnectionSuccess`) whenever the connection comes up, including after a re-assignment, since that is a new session; **On UDP Timed Out** (`OnUDPTimedOut`, an `FOnUDPTimedOut`) when the transport gave up on a re-assignment, with the state at `Reconnecting`; **On UDP Address Notify** (`OnUDPAddressNotify`, an `FOnUDPAddressNotify` with `bSuccess` and `GateKeep`) when the server answers the access request. `RequestUDPAccess()` asks for the connection; the [Quickstart](../quickstart.md) never calls it because the sign-in does. Underneath sits `UCrowdyUDPSubsystem`, whose `GetConnectionState()` is the C++ read that needs no game thread and whose `GetUDPNetworkStats()` returns an `FUDPNetworkStatistics` (bytes and datagrams each way, messages per second, `Ping`, the client-notify counters and loss percentage); `ResetUDPNetworkStats()` zeroes it. Coming up does more than flip the state: the same handler that broadcasts **On UDP Connection Success** joins the reliable-RPC channels, which is what makes the [session channel](./channels.md) "always joined". :::note[Connected means the server is ready for your traffic, not only that the socket is open.] After the server assigns this client, it needs a moment to install the session before it accepts spatial traffic. The SDK waits that out itself: the state stays `Connecting` until the assignment plus the readiness wait have elapsed, and only then does it turn `Connected` and broadcast **On UDP Connection Success**. You never time anything. What the SDK does not do is hold your sends for that moment: an actor update or a spatial event sent while the state is still `Connecting` goes out, or is dropped, and is never replayed once connected; the next replication interval is the recovery. So gate the thing that starts sending, the pawn's spawn or the entity component's activation, on this event or on `Get UDP Connection State` reading `Connected`, as the [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity) does. A `Multicast` event is the one exception, queued until the channel bootstrap finishes (see the Gotchas). ::: ## When it drops The low-level connection re-assigns a server on its own; the SDK only surfaces it as `Reconnecting`. A failed re-assignment is where the game hears about it: the SDK marks the state `Reconnecting`, broadcasts **On UDP Timed Out**, and immediately asks for the connection again itself, so a handler that polls the state inside that event reads `Reconnecting`, never `Disconnected`. `Disconnected` is what you read only after that request fails too. If the failure was the app being full, the state is `GateKeep` instead and nothing retries. The timeout that starts a re-assignment is `UDPTimeoutSeconds` in the project settings, and the address family is `UDPProtocol` (`ECrowdyUDPProtocol`: `Auto` takes the IPv4 address, `IPv4` records that choice deliberately, `IPv6` takes the IPv6 address and does not fall back). Both are managed by Crowdy Studio and shown read-only in Project Settings; see [Config Sync](../studio/config-sync.md). `StopNetworkOperations()` reports the session as disconnected for the UI to read; it does not close the socket or interrupt a recovery already under way, deliberately. `ToggleNetworkMessageProcessing()` discards received messages for development. :::caution[StopNetworkOperations changes what the UI reads. It does not stop the connection.] The connection belongs to the transport and recovers on its own. Asking the UI to show a disconnect must not be able to cancel that. ::: ## Reacting to a drop There is no reconnect code to write. The SDK asks for the connection again itself, and the game hears two events: **On UDP Timed Out** when a re-assignment failed and the SDK is opening the connection again, and **On UDP Connection Success** when it is back. Bind both once, in your Game Instance's `Init` after `Super::Init()` in C++ or from your Game Mode's Event BeginPlay in Blueprint, and do the visible thing in each. :::note[Do not call RequestUDPAccess from the timeout handler.] The SDK calls it itself as soon as the event has been broadcast. A second request from the game is a second reconnect in flight for the same client. ::: The lantern village dims every placed lantern while the connection is down and restores them when it is back: `WatchConnection`, called from `Init`, binds the two events to `HandleConnectionLost` and `HandleConnectionRestored`. ```cpp // LanternGameInstance.h void WatchConnection(); UFUNCTION() void HandleConnectionLost(); UFUNCTION() void HandleConnectionRestored(); // LanternGameInstance.cpp #include "EngineUtils.h" #include "Lantern.h" void ULanternGameInstance::WatchConnection() { // The SDK reconnects by itself after a timeout; a game only reacts to the two events. UCrowdySDKSubsystem* Sdk = GetSubsystem(); Sdk->OnUDPTimedOut.AddDynamic(this, &ULanternGameInstance::HandleConnectionLost); Sdk->OnUDPConnectionSuccess.AddDynamic(this, &ULanternGameInstance::HandleConnectionRestored); } void ULanternGameInstance::HandleConnectionLost() { for (TActorIterator It(GetWorld()); It; ++It) { It->Light->SetIntensity(0.f); } } void ULanternGameInstance::HandleConnectionRestored() { for (TActorIterator It(GetWorld()); It; ++It) { It->Light->SetIntensity(5000.f); } } ``` In the Game Mode Blueprint, from **Event BeginPlay**, a **Crowdy SDK Subsystem** getter feeds **Bind Event to On UDP Timed Out** and then **Bind Event to On UDP Connection Success**; the custom events `OnConnectionLost` and `OnConnectionRestored` are theirs. Dimming every lantern needs a loop, which is more than a six-node figure shows, so the graph binds the events and stops. [Blueprint snippet: conn-events, see /bp/unreal-sdk/conn-events.txt] ## When the server refuses a send A send leaves this client before the server judges it, so a refusal arrives later, on its own. **On Send Refused** on the realtime subsystem (`UCrowdyCppReplicationSubsystem::OnSendRefused`; in Blueprint, **Get Game Instance**, **Get Subsystem (Crowdy Cpp Replication Subsystem)**, **Bind Event to On Send Refused**) fires on the game thread for each one (an `FOnCrowdySendRefused`), with an `FCrowdySendRefusal`: the **Code** and **Code Name**, whether it was a channel message and its **Channel Id**, the **Message Type**, **What** was refused in words and a **Remedy**. The same line goes to the log as a Warning. C++ that reads the bridge's own send errors can turn one into the same struct with `UCrowdyCppReplicationSubsystem::DescribeSendRefusal(Error, OutRefusal)`, which returns false when the server's error names no send this client has on record. The usual one is **Unauthorized** for a permission the player's access tier does not grant. The permissions belong to each access tier of the app: `use_text_chat` for channel messages (party and guild chat, chat rooms, reliable CrowdyEvents such as Multicast events, and CrowdyState sent on a channel) and proximity text, and `use_direct_messages` for a message to one actor (an Owning Client or Host event, and CrowdyState only its owner reads); without them the server refuses every such send from that tier's players. The tiers an app had when these permissions arrived were given both; check a tier's permissions on the web console when its players' sends are refused. A refused channel message can also mean the player is not a member with the send messages permission, and the remedy says both. The server names a refused send only by a one-byte sequence number, so on a busy connection a refusal can be put down to the wrong send. Show it to the player or the log; do not undo game state because of it. ## Bundled sends Outbound replication messages of one network pass are packed into bundle datagrams. `crowdy.net.send.bundle` (default 1) controls it; 0 sends one datagram per message, a diagnostic switch rather than a setting a shipping build needs. The variable is read when a connection opens, so flipping it takes effect on the next connect, not on the live socket. ## Signed inbound bundles Once the connection is up, the transport tells the replication server what this client can read (a `CLIENT_CAPABILITIES` message), and repeats it every 15 seconds so a token refresh or a server-side move, both of which reset the server's record without saying so, never leaves it stale. A server at v0.30.0 or later then packs the notifications it sends you into `MESSAGE_BUNDLE_SIGNED` datagrams: the members inside carry no signature of their own, and one HMAC at the end covers the whole datagram. The transport checks that one signature, drops the datagram if it fails, and delivers the members exactly as it delivers any other notification. Your handlers see no difference; a datagram that used to cost one verification per member costs one. An older server ignores the message and keeps signing every member, which is also what the tagged 2.14.0 plugin receives, since it never announces itself. `crowdy.net.recv.signedbundles` (default 1) is the switch; 0 stops the announcement and keeps the per-member form, for a before-and-after comparison rather than for shipping. Like `crowdy.net.send.bundle` it is read when a connection opens. ## The receive budget Inbound messages are delivered on the game thread, per frame, under two budgets: `crowdy.net.receive.maxmessages` (default 3072) caps how many messages one frame may deliver, and `crowdy.net.receive.maxdrainms` (default 4.0) caps the milliseconds one frame may spend delivering them. Whichever binds first ends that frame's drain and the rest waits for the next frame. The budget covers every message the connection receives, channel notifications and video fragments included, not only actor updates. :::note[Raise the message count first.] If drains keep ending on the time budget instead, delivery is costing more per message than the frame can afford, and the count is not the lever. ::: `crowdy.net.receive.poolactorupdates` (default 1) reuses actor update message objects instead of allocating one per message; `crowdy.net.routes` prints the network router's subscription table; `crowdy.net.trace 1` logs socket, send, and receive activity, and `crowdy.serialize.trace 1` the encode and decode of each payload. `crowdy.serialize.scopes` adds CPU trace scopes inside decode for profiling. ## Deprecated calls Five functions on `UCrowdySDKSubsystem` are deprecated (the compiler warns on each call) and do nothing useful: `StartUDPTimeoutMonitoring`, `StopUDPTimeoutMonitoring`, `TriggerUdpHeartbeat`, `DeregisterAllReceptionLayers`, and `SetQueryEndpoint`. The connection tracks its own liveness now; remove the calls. ## Gotchas - `Reconnecting` needs nothing from you. Only `Disconnected` after a timeout does. - `GateKeep` is an answer, not a failure. Retrying against a full app asks a server with no room whether it has room. - Coming up joins the channels. A Multicast event sent before **On UDP Connection Success** is queued and flushed when the bootstrap finishes; see [Channels](./channels.md). - `crowdy.net.send.bundle` and `crowdy.net.recv.signedbundles` changes take effect on the next connect. ## Related - [Channels](./channels.md): what connecting joins. - [Video frames](./video-frames.md): the largest consumer of the receive budget. - [Authentication](../services/authentication.md): the sign-in that requests the connection. - [Console variables](../reference/console-cvars.md). # Video Frames Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/video-frames # Video Frames `UCrowdyVideoFrameReceiver` turns the video fragments the server fans out into whole frames and hands each one to you once, as an encoded image. The SDK does not decode it, does not know its dimensions, and has not checked that it is a valid image; turning the bytes into a texture is your code. This SDK receives only. It has no public call, node, or subsystem that sends a video frame; the frames come from a client on the same app that publishes them through another Crowdy client, such as a browser on the JavaScript SDK ([Webcam video](/crowdyjs/webcam-video)). A game built on this SDK alone never produces one. ## When you need it A picture-in-picture of a nearby player, a video wall in the village square, any feature that shows another client's camera. If the game never shows video it pays for none of this: with no listener bound a fragment is dropped the moment it reaches the receiver, with no copy and no partial frame held. ## Receive a frame The receiver is a world subsystem, created for Play in Editor and game worlds only. Bind `OnVideoFrameAssembled` (an `FOnCrowdyVideoFrameAssembled`) and it fires on the game thread once per completed frame with an `FCrowdyVideoFrame`. C++ code may bind `OnVideoFrameReady` instead, a plain multicast delegate (`FOnCrowdyVideoFrameReady`) at the same firing point with no dynamic-delegate overhead. The lantern village's player character binds at `BeginPlay`, and its `HandleVideoFrame` sets the torch's intensity to the byte count of each frame: a visible stand-in for the decode the example deliberately leaves out, because decoding is not the SDK's, as the page says below. ```cpp // LanternPlayer.h #include "Replication/Subsystems/CrowdyVideoFrameReceiver.h" UFUNCTION() void HandleVideoFrame(const FCrowdyVideoFrame& Frame); // LanternPlayer.cpp void ALanternPlayer::BeginPlay() { Super::BeginPlay(); if (UCrowdyVideoFrameReceiver* Video = GetWorld()->GetSubsystem()) { Video->OnVideoFrameAssembled.AddDynamic(this, &ALanternPlayer::HandleVideoFrame); } } void ALanternPlayer::HandleVideoFrame(const FCrowdyVideoFrame& Frame) { // The bytes are an encoded image the SDK never decodes; a consumer turns them into a texture. Torch->SetIntensity(static_cast(Frame.Bytes.Num())); } ``` In Blueprint the receiver is reached like any world subsystem: a **Crowdy Video Frame Receiver** getter node, **Bind Event to On Video Frame Assembled** off it, and a custom event with a `Frame` pin; **Break Crowdy Video Frame** opens the struct. The twin below, in the player character's Blueprint, binds at **Event BeginPlay**: a **Crowdy Video Frame Receiver** getter feeds **Bind Event to On Video Frame Assembled**, whose custom event `OnVideoFrame` calls **Set Visibility** on `Torch` (a Point Light component you add). Break the `Frame` pin to reach `Bytes`, `Sender Id`, and `Codec`. [Blueprint snippet: video-receive, see /bp/unreal-sdk/video-receive.txt] :::caution[Binding late costs you the frame already in flight, and nothing else.] Reassembly is turned on by the first bind. The one frame that was assembling when you bound may be missed; every frame after it is delivered. Nothing before the bind was kept. ::: **Success signal.** With a publishing client (not one built on this SDK) sending video into the same app, the torch's intensity jumps with every completed frame, and `GetPendingSenderCount()` reads above zero while a frame is half arrived. ## The frame `FCrowdyVideoFrame`, all fields read-only: | Field | What it is | |---|---| | `SenderId` (`FString`) | The sender's 32 wire octets as text. Exact and unambiguous; empty for an unset id. | | `SenderUUID` (`FGuid`) | The key derived from those octets, the id the rest of the SDK addresses an actor by. Lossy: the derivation reads the octets as hexadecimal, so two senders whose ids differ only outside that alphabet derive the same key. `USerializationFunctionLibrary::ToGuid` (Blueprint: **To Guid**) derives the same key from a `SenderId` string, and returns an invalid guid for text that is not an actor id. | | `Codec` (`ECrowdyVideoCodec`) | `Jpeg` or `WebP`. `Unknown` is what a byte this build has no entry for maps to; a fragment carrying one is dropped before it reaches you. | | `FrameId` (`int32`) | The sender's own counter. It wraps, orders that sender's frames, and means nothing across two senders. | | `Bytes` (`TArray`) | The encoded image, JPEG or WebP according to `Codec`. | | `ChunkX`, `ChunkY`, `ChunkZ` (`int64`) | The chunk the last fragment was broadcast over. | | `ServerTimestamp` (`int64`) | When the server stamped that last fragment, in milliseconds since the epoch. | :::warning[Use SenderId, not SenderUUID, to tell two senders apart for certain.] `SenderUUID` is the convenient key for matching a frame to an entity you already track by `FGuid`. It can name more than one sender. When correctness depends on which camera this is, compare `SenderId`. ::: :::danger[There is no decode anywhere in the SDK.] `Bytes` is a JPEG or WebP file in memory. Turning it into a `UTexture2D` is your own code, with the engine's image wrapper module or a decoder of your choice, and validating it as an image is part of that job: the bytes came off the network from another client. ::: ## Housekeeping | Call | What it answers or does | |---|---| | `GetPendingSenderCount()` | Senders with a frame half arrived. Zero while nothing is listening, since nothing is ingested. | | `GetDroppedFragmentCount()` | Fragments dropped for a malformed header, or for a frame their sender had already moved past. | | `GetAbandonedFrameCount()` | Frames abandoned incomplete: superseded by a newer frame, timed out, or the sender left. | | `ForgetSender(SenderGuid)` | Drops whatever is half assembled for one sender and forgets its frame counter. Exposed because a sender the server never announced as gone is otherwise never released, and a sender that rejoins and restarts at frame zero has every fragment dropped as a straggler until its counter climbs back past the old value. One key can name more than one sender, and every sender it names is forgotten. | The receiver releases a sender itself when the server announces its departure or the tracker times it out, using the same lossy key. ## What is not delivered - A client's own fragments. The server fans a video packet out over the chunk it was sent to, which includes the sender, and a frame of your own camera is one you already have. They are discarded, never reassembled. - A frame that never completes. A fragment for a frame the sender has moved past is dropped; the incomplete frame is abandoned and counted. Video fragments share the per-frame receive budget with every other message the connection receives; see [Connection and reconnect](./connection-and-reconnect.md#the-receive-budget). ## Gotchas - The receiver is a world subsystem. There is none in an editor world, and a new world has a new receiver with nothing bound. - Nothing is reassembled until something is bound. A diagnostic that reads the counters with no listener sees zeros. - `FrameId` is per sender. Do not use it to order frames from two cameras. - `SenderUUID` is the SDK's actor key and is lossy by construction; `SenderId` is exact. ## Related - [Connection and reconnect](./connection-and-reconnect.md): the receive budget these fragments share. - [Voice chat](../services/voice-chat.md): the sibling communication feature. - [Entities and spawning](./entities-and-spawning.md): the actor tracker whose departures release a sender. # Draw a crowd with Mass Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/draw-a-crowd-with-mass # Draw a crowd with Mass :::note[Part of the SDK since v2.19.0.] Since v2.19.0 CrowdyMass is part of the SDK; on v2.18.0 and earlier it was the separate CrowdyMass plugin, as [What's Changed](../guides/whats-changed.md#2026-10-12-sdk-v2190) explains. ::: Crowds with Mass is a way to draw many remote players, or other Dynamic entities, without spawning an actor for each one. The other players on a map become rows of a crowd that the engine draws and animates in bulk. This page tells you what that means, when it is the right choice, and where to read next. If you learn by doing, follow [Market night in Oakford](./examples/market-night-in-oakford.md). ## What it is Crowds with Mass is a [rendering backend](../runtime/rendering-backends.md). A rendering backend decides what a machine puts on screen for an entity that belongs to someone else. The default backend, the actor pool, spawns a real actor of the entity's class. The **Crowdy Mass Rendering Backend** (`UCrowdyMassRenderingBackend`) draws the same entity as one row of a crowd instead: a position, an animation state and a few numbers in a table that Unreal's Mass framework updates in bulk and draws as instanced skinned meshes. No actor exists for that row. ![A crowd of pale animated mannequins walking and jogging across a tiled floor, every one of them drawn as a Mass crowd row rather than as an actor](/img/unreal-sdk/mass-crowd-rows.png) You do not need to know Mass to use it. Everything here is Blueprint and assets except three jobs that need C++: registering the fields a graph reads by name, writing your own fragment, trait and processor, and registering an event handler that adds an effect while the row keeps playing its action. [Add your own crowd data](./add-your-own-crowd-data.md), [Read and write its fields, from Blueprint](./from-blueprint/read-and-write-its-fields.md) and [Receive its events, from C++](./from-cpp/receive-its-events.md) say where. Crowds with Mass is part of the CrowdySDK plugin. There is nothing to install and nothing to enable beyond the plugin you already use. A map opts in by choosing the backend on its map profile. :::note[Your own player is always a real actor.] Every player is a real actor on their own machine, whatever backend the map uses. A crowd row is how someone else's machine draws that player, never how you see yourself. ::: ## Why you would use it The actor pool spawns one actor for every remote entity in range. Each actor carries its components, its animation graph and its tick, which is the right cost for a few players and the wrong one for a square full of them. A crowd row costs a table entry instead of an actor. It draws through an instanced skinned mesh. It plays its animation from a bank shared by every row of the same character. A few hundred to a few thousand remote players can stay on screen at a steady frame rate. On market night in Oakford the village square fills with players, far more than the actor pool should draw. The Oakford map profile selects Mass, and every other player in the square is a crowd row. ## When not to use it - **You expect few remote entities.** With a dozen or so players in range, the actor pool is simpler and costs you no extra authoring. - **Every remote entity must be a full actor everywhere.** A crowd row has no components, no physics body of its own and no animation graph. Code that lives on the actor does not run for a row. An event body runs on a stand-in instead, and it has limits; see [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md). - **You want full actors only up close.** That is possible: a character can opt into a near tier, and the closest entities, up to a ceiling you set, are drawn as real actors. See [Promote close entities to actors](./promote-close-entities-to-actors.md). Everything beyond the ceiling stays a row. | Question | Actor pool | Crowds with Mass | |---|---|---| | How you choose it | It is the default; set nothing | Set **Backend Class** on the map profile | | How a remote entity is drawn | A pooled actor of its class | A crowd row; an actor only when close, and only for a class that opts in | | What the cost grows with | The number of actors | The number of rows, each far cheaper than an actor | | Animation | The actor's own animation graph | An anim set and bank you author, shared by every row | | Your code on a remote entity | Runs on the actor as usual | An event body runs on a per-class stand-in; component code does not run | | A good fit for | Tens of remote entities | Hundreds to thousands | | Extra work | None | A flagged material, an anim set, optionally a near tier shell | ## Where it sits The backend is chosen per map, by the **Backend Class** on the map profile. A map that chooses nothing runs on the actor pool, and a project can use the actor pool on one map and Mass on another. Choosing Mass changes how remote entities are drawn; it does not change how they are sent, found or owned. Entity identity, [Crowdy State](../runtime/crowdy-state.md), [events](../runtime/rpc-events-blueprint.md) and [continuous state](../runtime/continuous-state.md) work the same way under both backends. [Turn Mass on for a map](./turn-mass-on-for-a-map.md) shows the setting. ## Three ways one entity is drawn One entity can be drawn three ways on one machine, and every call you author has to be correct on all three. | Representation | When | What runs your code | |---|---|---| | A real actor | Always for the player on their own machine; for every entity everywhere, on an actor pool map | Your actor, as usual | | A near tier actor | A row that comes within **Near Tier Distance** of the camera, for a class that opted in | The per-class stand-in, as for a row; the near tier actor only draws the entity and plays its actions | | A crowd row | The default for a remote entity under Mass | Fragments and processors, and the per-class stand-in for event bodies; no actor exists | A near tier actor is a pooled copy of the shell you build, bound to one entity while it is close. The stand-in is an object the SDK makes for each class to run your event bodies on when no real actor exists. ## Author once, on the actor class You never author a "Mass version" of your character. You author the character, and on another machine the SDK draws it as a row. Everything is declared once, on your own actor class: - the Crowdy entity component and the state it sends, on `ALanternPlayer`; - the replicated `Stance` property that drives a pose, and the `WaveTorch` event that drives an action; - the anim set the crowd animates from, named by a component on the class; - the near tier shell, if you want close players drawn as actors. ## Two planes, kept apart The SDK keeps two kinds of state and Crowds with Mass does not mix them. | | View state | Server values | |---|---|---| | What it is | Crowdy State, continuous state and events | A Server Object, with a record per entity | | Owned by | The client being observed | The server | | Carries | Movement, stance, animation, anything cosmetic | Health, stats, inventory, anything that decides an outcome | | Trust | Forgeable | Authoritative | | Reaches a crowd row by | A field you register, then a fragment on the row | A read from the entity's record, on demand | :::caution[View state decides what is drawn. Server values decide what is true.] Anything a peer sends about itself can be forged, so a crowd row may use it to look right and never to decide an outcome. Put anything a peer must not be able to influence in a Server Object. A row reads those values in place; see [Read its server values, from Blueprint](./from-blueprint/read-its-server-values.md) and [Read its server values, from C++](./from-cpp/read-its-server-values.md), and [Keep a record per entity](../exec/keep-a-record-per-entity.md) for the Server Object itself. ::: ## The example in this section The pages follow one world, the village of Oakford. Players are `ALanternPlayer`, a character that sends `FLanternPlayerState` through its own executor, `ULanternPlayerExecutor`; `BP_LanternPlayer` is your Blueprint subclass of it. On market night every other player in the square is drawn as a crowd row. The raiders on the north road, from [Raiders on the north road](../exec/examples/raiders-on-the-north-road.md), show server values on rows. [Market night in Oakford](./examples/market-night-in-oakford.md) puts the whole thing together. ## Before you start You need a signed-in project with a Dynamic Crowdy entity, usually the player pawn, and a character with a skeletal mesh: a row is built from that mesh, so a bare Character with no mesh produces no row. The [Entity Component](../runtime/entity-component.md) page lists the entity settings, and [Continuous State](../runtime/continuous-state.md#a-custom-executor) builds the custom executor that `ALanternPlayer` uses. The [Quickstart](../quickstart.md) gets a project connected first. ## Read this section in order 1. [Turn Mass on for a map](./turn-mass-on-for-a-map.md): choose the backend, set its options and project settings, flag the material. 2. [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md): the entity component, the state struct, the mesh and where the entity config comes from. 3. [Set up the animation set](./set-up-the-animation-set.md): the bank, the **Crowdy Mass Anim Set**, gaits and the rule that an index must change. 4. [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md): a replicated enum becomes a held pose, and an event becomes a one-shot action. 5. [Promote close entities to actors](./promote-close-entities-to-actors.md): the near tier shell, pooling and distances. 6. [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md): what an event body does when the observer holds a row. 7. [Add your own crowd data](./add-your-own-crowd-data.md): your own fragment, trait and processor, and the state fields that feed them. 8. From Blueprint: [Target an entity](./from-blueprint/target-an-entity.md), [Call an event on it](./from-blueprint/call-an-event-on-it.md), [Receive its events](./from-blueprint/receive-its-events.md), [Read and write its fields](./from-blueprint/read-and-write-its-fields.md), [Read its server values](./from-blueprint/read-its-server-values.md). 9. From C++: the same five pages, [Target an entity](./from-cpp/target-an-entity.md), [Call an event on it](./from-cpp/call-an-event-on-it.md), [Receive its events](./from-cpp/receive-its-events.md), [Read and write its fields](./from-cpp/read-and-write-its-fields.md) and [Read its server values](./from-cpp/read-its-server-values.md). 10. [Fix a crowd that does not look right](./troubleshooting.md): symptoms first, then the diagnostics you may use. 11. [Market night in Oakford](./examples/market-night-in-oakford.md): the worked example from end to end. ## Related - [Rendering Backends](../runtime/rendering-backends.md): the actor pool, and what a backend is. - [Map Profiles](../runtime/map-profile.md): where **Backend Class** and **Backend Config** live. - [Crowdy State](../runtime/crowdy-state.md): the view state that reaches a row. - [The Two Planes](../concepts/two-planes.md): why view state and server values stay apart. - [Keep a record per entity](../exec/keep-a-record-per-entity.md): the server side of a row's values. # Turn Mass on for a map Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/turn-mass-on-for-a-map # Turn Mass on for a map Mass is a rendering backend, so you turn it on per map, on the map profile. This page sets **Backend Class**, shows the options the backend reads, lists the project settings you are likely to touch, and walks the material change every crowd needs. By the end the Oakford map draws every other player as a crowd row. ## When you need this When a map should draw its remote players as a crowd. A map that sets nothing keeps the actor pool, and nothing on this page changes it. ## Pick the backend on the map profile The switch is on a **Crowdy Map Profile** data asset, under **Actor Management**. For Oakford: 1. In the Content Browser, **Add**, **Miscellaneous**, **Data Asset**, pick **Crowdy Map Profile**, and name it `DA_OakfordProfile`. 2. Open it. Under **Actor Management**, set **Backend Class** to **Crowdy Mass Rendering Backend**. 3. Leave **Backend Config** empty for now. The backend then reads everything from the project settings below. 4. Open **Project Settings, Plugins, Crowdy SDK**, find **Map Profiles**, and add the Oakford map with `DA_OakfordProfile` as its profile. Use **Default Profile** instead to draw every map through Mass. ![The DA_OakfordProfile Details panel with Actor Management expanded, Backend Class set to CrowdyMassRenderingBackend, Backend Config set to None, and Enable Networking ticked](/img/unreal-sdk/mass-map-profile-backend.png) The map profile page covers the asset and how a profile is found: [Create the asset](../runtime/map-profile.md#create-the-asset) and [Assign the profile](../runtime/map-profile.md#assign-the-profile). | Field | Set it to | |---|---| | **Backend Class** | **Crowdy Mass Rendering Backend**. The default is **Actor Pool Rendering Backend**. | | **Backend Config** | A **Crowdy Mass Backend Config**, or leave it empty. | :::caution[Nothing draws as a crowd until a profile selects the Mass backend.] Installing or updating the SDK changes no map. A project that sets no profile runs on the profile the SDK ships, and that profile draws actors. ::: :::caution[The crowd nodes work only on a map that selects the Mass backend.] The Mass subsystems are created only in a Play In Editor or game world whose map profile has **Backend Class** set to **Crowdy Mass Rendering Backend**. On an actor pool map, the Crowdy Mass nodes and C++ calls that need them do nothing, quietly. A **Crowdy Mass Area Spawner** spawns nothing and warns once. **Set Crowdy Mass Hit Zone Table** warns once and hits use the default zone. **Receive Crowdy Event on Entity** and **Receive Server Values Changed on Entity** log once and receive nothing. **Play Crowdy Mass Action** still works, because it reads the actor's class. The rest of the Mass module stays out of the way on such a map too. Its two per-frame processors sit out that world, and a **Crowdy Mass Near Tier** component does not tick there until something binds it. ::: In C++, `UCrowdySDKDeveloperSettings::ProfileDrawsCrowdRows(Profile)` tells you whether a profile selects a backend that draws crowd rows. **Backend Class** and **Backend Config** are greyed out when `bUseCrowdyActorTracker` is off on the profile. Without the tracker there is no backend at all. :::caution[Backend Config must match the Backend Class.] The Mass backend ignores a **Backend Config** of another class without a word and reads the project settings instead, so none of its overrides apply. (The actor pool backend is the one that refuses a mismatched config and then draws nothing.) Pair **Crowdy Mass Rendering Backend** with a **Crowdy Mass Backend Config** or with nothing. ::: **Success signal.** Two clients on the Oakford map: the other player appears as a crowd row and moves. If nothing appears, start at "Nothing appears at all" on [Fix a crowd that does not look right](./troubleshooting.md). ## Start from the shipped profile `/CrowdySDK/Mass/Data/DA_CrowdyMassDefaultProfile` is a ready-made profile that selects the Mass backend with an empty **Backend Config** and **Enable Networking** on. Use it as a starting point, or to try Mass before you author your own. Nothing registers it for you. Point **Map Profiles** at it, as the **Default Profile** to draw every map through Mass or as the entry for one map, and that map is on Mass. ## Set the backend's options A **Crowdy Mass Backend Config** (`UCrowdyMassRenderingBackendConfig`) is created inline on the profile's **Backend Config** field. Its properties are under **Mass Backend**: | Label | Default | What it does | |---|---|---| | **Remote Entity Config Override** (`RemoteEntityConfigOverride`) | unset | The Mass entity config for every class this backend does not bind by name. Unset means the project-wide **Remote Entity Config**. It must include the Crowdy Remote Entity trait. | | **Per Class Entity Configs** (`PerClassEntityConfigs`) | empty | Rows of **Entity Class** and **Entity Config** (`FCrowdyMassEntityConfigBinding`). A class in a row is built from the config beside it. Subclasses inherit the row. | | **Smoothing Duration Override** (`SmoothingDurationOverride`) | -1.0 | Seconds a movement segment is replayed over. A negative value uses the project's **Default Smoothing Duration**. | | **Render Local Owner** (`bRenderLocalOwner`), under **Mass Backend, Debug** | off | Also draw your own entity as a crowd row, overlapping your real pawn. A single-client check; leave it off in a shipped game. | Which config a class is built from, and how to author one, is on [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md#where-the-entity-config-comes-from). Most projects leave all of this at its default. :::caution[A per-class row with one half filled in is not skipped silently.] A row with only **Entity Class** or only **Entity Config** logs `per-class entity config entry ignored, class '' and config '' must both resolve`, and that class falls back to the next config. A row with both empty is skipped without a word. Swapping the backend replaces the whole set of rows; they are never merged. ::: ## Tune the project settings **Project Settings, Plugins, Crowdy Mass** (`UCrowdyMassDeveloperSettings`) holds the project-wide defaults, saved to `Config/DefaultGame.ini` under `[/Script/CrowdyMass.CrowdyMassDeveloperSettings]`. The backend config above overrides a few of them per map profile. The ones you are likely to touch: ![The Crowdy Mass page in Project Settings showing the Remote Entities, Near Tier, Trace and Cache categories with their default values](/img/unreal-sdk/mass-settings.png) **Remote Entities** | Label | Default | What it does | |---|---|---| | **Remote Entity Config** (`RemoteEntityConfig`) | unset | The Mass entity config for every class no per-class row or backend override answers. Unset means each class is built from itself. | | **Default Smoothing Duration** (`DefaultSmoothingDuration`) | 0.1 s | Seconds a movement segment is replayed over until an arrival interval has been measured, and for every update when adaptive smoothing is off. | | **Adaptive Smoothing** (`bAdaptiveSmoothing`) | on | Replay each segment over the interval measured between arrivals instead of a fixed time. | | **Smoothing Interval Overshoot** (`SmoothingIntervalOvershoot`) | 1.2 | How much longer than the measured interval a segment is replayed over. Above 1 removes the stall at the cost of sitting slightly behind. Only used with adaptive smoothing. | | **Min Smoothing Duration** (`MinSmoothingDuration`) | 0.05 s | Shortest span a measured segment may be replayed over. | | **Max Smoothing Duration** (`MaxSmoothingDuration`) | 0.5 s | Longest span any segment may be replayed over, including one a sender asked for. | **Near Tier** **Near Tier Distance** (`NearTierDistance`), **Near Tier Visible Distance** (`NearTierVisibleDistance`) and **Max Near Tier Actors** (`MaxNearTierActors`) decide when a close row becomes a real actor. They are described, with their defaults, on [Promote close entities to actors](./promote-close-entities-to-actors.md). **Trace** Traces and overlaps against the crowd find candidates through a spatial grid first, then test exact shapes. The defaults suit a ground-based crowd. | Label | Default | What it does | |---|---|---| | **Enable Spatial Grid** (`bEnableSpatialGrid`) | on | Use the grid. Off, every trace scans all tracked entities. | | **Spatial Grid Mode** (`SpatialGridMode`, `ECrowdyMassSpatialGridMode`) | **2D (X/Y)** | Split the grid on X and Y only, or on X, Y and Z (**3D (X/Y/Z)**). | | **Spatial Grid Cell Size** (`SpatialGridCellSize`) | 1600 cm | Smaller cells give fewer candidates but make a long trace visit more cells. | | **Spatial Grid Entity Bounds Radius** (`SpatialGridEntityBoundsRadius`) | 250 cm | The radius used to place an entity in the grid. Exact mesh bounds still decide hits. | | **Spatial Grid Trace Query Radius** (`SpatialGridTraceQueryRadius`) | 0 cm | Extra radius around a ray when collecting cells. | | **Spatial Grid Max Ray Cells** (`SpatialGridMaxRayCells`) | 4096 | The most cells one trace or overlap visits, and the most one entity may cover. A straight trace longer than this, or a sweep whose radius or end points are not finite or are out of range, is not answered from the grid: it tests every tracked entity instead, which is slower but finds far entities it used to miss. | | **Spatial Grid Max Entity Sweep Radius** (`SpatialGridMaxEntitySweepRadius`) | 12800 cm | Ceiling on the radius one position update may claim, so a forged jump cannot cover unbounded cells. | | **Enable Trace Instance Cache** (`bEnableTraceInstanceCache`) | off | Cache each entity's visual instance lookup, at a small memory cost per entity. | | **Enable Physics Asset Trace Metadata Cache** (`bEnablePhysicsAssetTraceMetadataCache`) | off | Cache trace metadata per mesh and physics asset pair. Turn it off while you edit physics assets live. | The four settings under **Trace, Debug** (`SpatialGridDebugColor`, `SpatialGridDebugLineThickness`, `SpatialGridDebugTwoDimensionalHeightPadding`, `SpatialGridDebugTwoDimensionalCornerLineHalfLength`) only change how the grid is drawn when a trace node's **Draw Spatial Grid Cells** is on. In a Development build, `crowdy.mass.interp.adaptive`, `crowdy.mass.interp.overshoot`, `crowdy.mass.interp.min` and `crowdy.mass.interp.max` change the four smoothing values in a running session. A negative value (for `crowdy.mass.interp.adaptive`, -1) leaves the setting in charge. See [Fix a crowd that does not look right](./troubleshooting.md). ## Flag the material This is the most common reason a crowd looks wrong, and it is a material setting, not a Mass one. A material draws through the instanced skinned mesh path only if its base material carries the instanced skinned usage flag. Without it the engine silently draws the default material: the row spawns, animates and can be hit, and it is grey. 1. Open the parent **Material** of the character's materials, not a material instance. Instances inherit the flag. 2. Under **Usage**, tick **Used with Instanced Skinned Mesh**. 3. Save the material. A flag that is set in the editor and never saved is gone on the next launch. ![The Usage category of the M_LanternPlayer material with Used with Skeletal Mesh and Used with Instanced Skinned Mesh ticked and every other usage flag clear](/img/unreal-sdk/mass-material-usage-flag.png) The SDK checks the flag for you. When it builds a crowd row from a class it logs, once per material, how many material slots are not flagged and which base material to flag, and `crowdy.mass.derive` prints the same verdict. The flag's evidence is in `LogMaterial`; no crowd log line reports the substitution itself, because the engine does it quietly. If you author the entity config yourself, its mesh was chosen for a crowd and its material was prepared with it. The check matters most for a class the SDK derives a config from, since an ordinary character material has no reason to carry the flag. ## The engine's animation pass The SDK hands each row's animation to the renderer itself, in place of the engine's own pass. With both running, every row is handed over twice and the animation of one row can land on another. The CrowdyMass module turns the engine's pass off when the game starts, so your project does not need a `DefaultMass.ini` for it. If the project uses the MetaHuman crowd plugin, that plugin's own pass is left alone. To turn it off as well, add this to `Config/DefaultMass.ini`, with the editor closed: ```ini [/Script/MetaHumanCrowd.MetaHumanMassSimpleAnimationProcessor] bAutoRegisterWithProcessingPhases=False ``` Write the file rather than unticking the setting in the editor: the checkbox is not saved and is back on at the next launch. **Success signal.** With `crowdy.mass.anim.dump 1`, `LogCrowdyMass` reports as many animation entries as instances on each crowd mesh. If it warns, once a second, that more than one processor is filling the batch, something turned the engine's pass back on; see [Fix a crowd that does not look right](./troubleshooting.md). :::caution[A Mass crowd of your own that stands still gets no animation.] The SDK's pass animates moving skinned rows. The engine's pass, now off, also animated skinned entities Mass keeps as stationary instances. If your project runs its own Mass crowd of stationary skinned characters, they hold their last pose. Crowdy crowd rows are not affected. ::: ## Add the module dependency A Blueprint-only project needs no Build.cs change. A C++ module that uses the Mass types of Crowds with Mass lists the `CrowdyMass` module: ```csharp PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "CrowdySDK", "CrowdyNet", "CrowdyReplication", "CrowdyServices", "CrowdyMass", }); ``` `CrowdyMass` brings `MassCore`, `MassEntity` and `MassSpawner` with it. List them yourself only if your module declares Mass types of its own; see [Add your own crowd data](./add-your-own-crowd-data.md). ## Check it with one client The Mass backend skips your own entity on purpose, so on one client you see nothing. Two ways to see a crowd: **Draw yourself as a row.** Give the profile a **Crowdy Mass Backend Config** and tick **Render Local Owner**. Your own pawn is then also drawn as a crowd row, overlapping it. Render Local Owner exists only on the backend config, so with an empty **Backend Config** the option is off. **Place a Crowdy Mass Area Spawner.** Drop a **Crowdy Mass Area Spawner** (`ACrowdyMassAreaSpawner`) in the level of a map that selects Mass. It spreads entities on an even grid over its **Spawn Area** box, a box with extent 1000 x 1000 x 100 (2000 by 2000 by 200 cm) by default. They stand where they are placed; they do not move. Set **Entity Class** to the actor class they stand for. A spawner entity with no class renders from the fallback config, and no Crowdy event can ever be sent to it. | Label | Default | What it does | |---|---|---| | **Spawn Area** (`SpawnArea`) | box extent 1000 x 1000 x 100 | The box entities are spread over. Resize and move it in the level. | | **Entity Count** (`EntityCount`) | 100 | How many to spawn. | | **Entity Class** (`EntityClass`) | none | The actor class the entities stand for. Set it for anything you intend to trace or send events to. | | **Spawn on Begin Play** (`bSpawnOnBeginPlay`) | on | Spawn when the level begins play. | | **Destroy Spawned on End Play** (`bDestroySpawnedOnEndPlay`) | on | Destroy the entities when play ends. | | **Clear Before Spawn** (`bClearBeforeSpawn`) | on | Destroy what this spawner made before spawning again. | | **Position Jitter** (`PositionJitter`), under **Distribution** | 0.0 | Moves each entity off its grid point by up to this fraction of a cell, from 0 to 1. | | **Use Deterministic Seed** (`bUseDeterministicSeed`) | off | Use **Random Seed** so the layout is the same every run. | | **Random Seed** (`RandomSeed`) | 1337 | The seed, when **Use Deterministic Seed** is on. | | **Randomize Yaw** (`bRandomizeYaw`) | off | Give each entity a random facing. | | **Project to Ground** (`bProjectToGround`), under **Grounding** | off | Drop each entity onto the first surface a line trace finds. | | **Ground Trace Distance** (`GroundTraceDistance`) | 10000 | How far above and below each entity the ground trace reaches. | | **Ground Trace Channel** (`GroundTraceChannel`) | **WorldStatic** | The collision channel the ground trace uses. | The spawner also has Blueprint-callable functions. **Spawn Entities** and **Respawn Entities** return the number spawned, **Destroy Spawned Entities** removes them, and **Get Spawned UUIDs** and **Get Spawned Entities** list them. **Success signal.** In play, a grid of crowd rows stands in the spawn box, drawn from the entity class you set. ## Gotchas - Map profile matching is by the map's short name. `/Game/Maps/Oakford` and `/Game/Test/Oakford` both match the key `Oakford`; give two maps different names. - A **Default Profile** or map row that names an asset that fails to load does not fall through to the shipped default. It resolves to nothing and the SDK is inactive on that map; see [Assign the profile](../runtime/map-profile.md#assign-the-profile). - The Mass entity subsystem exists only in a Play In Editor or game world whose map profile selects the Mass backend, so nothing here runs in the editor viewport or on an actor pool map. ## Related - [Map Profiles](../runtime/map-profile.md): the asset, the Project Settings entries and how a profile is found. - [Rendering Backends](../runtime/rendering-backends.md): the actor pool, and what a backend is. - [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md): the entity, its payload and its mesh. - [Promote close entities to actors](./promote-close-entities-to-actors.md): the Near Tier settings and what they apply to. - [Fix a crowd that does not look right](./troubleshooting.md): the crowd is grey, nothing appears, and the diagnostics. # Make a character draw as a crowd row Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/make-a-character-draw-as-a-crowd-row # Make a character draw as a crowd row A character becomes a crowd row on other machines without a second version of it. You make the class a Dynamic Crowdy entity, send a state struct whose field names the crowd can read, give the class a skinned mesh, and check the result with one console command. By the end `ALanternPlayer` appears in Oakford's square as a moving row on every other player's machine. ## When you need this When a map selects Mass (see [Turn Mass on for a map](./turn-mass-on-for-a-map.md)) and you want a class, usually the player pawn, drawn as a row. This page starts from `ALanternPlayer`, a character whose `ULanternPlayerExecutor` sends `FLanternPlayerState`, built as in [Continuous State](../runtime/continuous-state.md#a-custom-executor). `BP_LanternPlayer` is your Blueprint subclass of `ALanternPlayer`. ## Make the class a Dynamic entity Add a **Crowdy Entity Component** to `ALanternPlayer`, or to `BP_LanternPlayer` if you work in Blueprint, and set: | Setting | Value | Why | |---|---|---| | **Mode** | **Dynamic** | Streams the executor's snapshot every replication interval. **Static** is event-only and moves nothing. | | **Identity Policy** | **Player Derived** | For a player pawn: the id comes from the signed-in account, so only one actor per client can use it, the locally controlled primary pawn. | | **Ownership** | **Local Client** | This client owns the entity. The options are **Local Client** and **Host**, and **Host** is the default. | | **State Executor** | An executor whose state struct has the fields below | Builds the snapshot sent every interval. Unset means the SDK's transform-only default. | | **Auto Register** | on | Joins the continuous channel at `BeginPlay`. | Every setting is described on [Entity Component](../runtime/entity-component.md), and the executor on [Continuous State](../runtime/continuous-state.md#a-custom-executor). ## Send a payload the crowd can read The crowd binds a row's position by reflecting over the field names of the struct your executor sends. The names are the contract: - a field called `Transform`, or - a field called `Location` (or `Position`) plus a field called `Rotation`. The default executor's `FCrowdyActorState` already has `Location` and `Rotation`. A struct of your own needs the same names. `FLanternPlayerState` below keeps them and adds `bTorchLit`. ```cpp // LanternPlayer.h #include "Replication/Executor/ActorUpdateExecutor.h" // The custom snapshot: the transform the default carries, plus one field of your own. USTRUCT() struct FLanternPlayerState { GENERATED_BODY() UPROPERTY() FVector Location = FVector::ZeroVector; UPROPERTY() FRotator Rotation = FRotator::ZeroRotator; UPROPERTY() bool bTorchLit = true; }; UCLASS() class ULanternPlayerExecutor : public UActorUpdateExecutor { GENERATED_BODY() public: virtual FInstancedStruct GetActorState_Implementation(const UActorComponent* UpdateComponent) const override; virtual UScriptStruct* GetStateStruct_Implementation() const override; }; // LanternPlayer.cpp CrowdyEntity->StateExecutor = CreateDefaultSubobject(TEXT("Executor")); FInstancedStruct ULanternPlayerExecutor::GetActorState_Implementation(const UActorComponent* UpdateComponent) const { FLanternPlayerState State; if (const ALanternPlayer* Player = UpdateComponent ? Cast(UpdateComponent->GetOwner()) : nullptr) { State.Location = Player->GetActorLocation(); State.Rotation = Player->GetActorRotation(); State.bTorchLit = Player->Torch->IsVisible(); } return FInstancedStruct::Make(State); } UScriptStruct* ULanternPlayerExecutor::GetStateStruct_Implementation() const { return FLanternPlayerState::StaticStruct(); } ``` 1. Create a Structure asset with a **Vector** variable named `Location` and a **Rotator** variable named `Rotation`. Add any extra fields after them. 2. Create a Blueprint with **Actor Update Executor** as its parent class. Override **Get State Struct** to return your structure, and override **Get Actor State** to build it from the owning actor with **Make InstancedStruct**. 3. Set the character's **State Executor** to the new executor class. :::caution[A payload whose fields have other names binds nothing, and the row stands at the world origin.] `LogCrowdyMass` names the struct by its full path once, lists both accepted spellings with the types they need, and says what each lookup found. Read that warning before you suspect anything else about the row's position. ::: :::caution[Add fields at the end of the struct and never reorder it.] The struct is read in declared order. Appending keeps an older client's shorter payload readable, and reordering silently reads bytes as the wrong field. Give every field a safe default, because a truncated or older payload keeps it. ::: That is movement done. Poses, actions and your own fields are separate steps: [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md) and [Add your own crowd data](./add-your-own-crowd-data.md). ## Give the class a mesh A row is built from a skinned mesh found on the class's default object. The class needs: - a `USkinnedMeshComponent`, which a **Skeletal Mesh Component** is, - with a mesh asset assigned, - and a base material that carries **Used with Instanced Skinned Mesh**; see [Flag the material](./turn-mass-on-for-a-map.md#flag-the-material). The component can be declared in C++, added in the Blueprint, or inherited from a parent class that the Blueprint overrides. All three work, and the check below names which one answered. Components are not read most-derived first. Every component declared in C++ is read before any Blueprint component, and the most-derived rule only applies among the Blueprint routes. A component that lists no mesh is skipped, so a C++ base class can expose a mesh slot that a Blueprint child fills. ## Check it before you run `crowdy.mass.derive` says what a row of a class would be made of, without spawning anything: ``` crowdy.mass.derive BP_LanternPlayer ``` It takes a short class name, an asset path or a full object path, and it logs one line on `LogCrowdyMass` naming: - the mesh and which of the three routes supplied it, - the collision object type the row is traced as, - the material verdict: how many slots are not flagged, and which base material to flag, - the anim set and where it came from, - and whether an authored config would be used instead of any of it. The line is a warning when the mesh is missing, when any material is not flagged, or when an authored config wins. Otherwise it is an ordinary log line. The editor world can answer the mesh, route, collision and material parts. The config and animation parts need a Play In Editor or game world, on a map whose profile selects the Mass backend, because the Mass entity subsystem exists only there; without it the line says the config answer is unknown. When no mesh resolves, the line gives the reason and the fix. The mesh reasons are: - no class was given, - the class is not an Actor class, - the class has no class default object yet because it is still loading, - it has a skinned mesh component with no mesh assigned, - it has no skinned mesh component at all. The command can also fail before the class is known: the name is not found, the name is ambiguous (it lists the paths that match), the asset registry is still scanning, or the path will not load. **Success signal.** For `BP_LanternPlayer`, one line that names the mesh, shows no unflagged material, and names an anim set once you have one. ## Where the entity config comes from Every crowd row is built from a Mass entity config (`UMassEntityConfigAsset`): what the row is made of, its mesh, its animation and its collision. There are five places a class's config can come from. They are consulted in this order. The first one that is set answers; if what it names does not load, the class is derived from itself rather than trying the next place: 1. A per-class binding in the **Per Class Entity Configs** of the map profile's **Backend Config**, matching the class exactly. 2. An inherited per-class binding: the nearest bound ancestor of the class. 3. The backend's **Remote Entity Config Override**. 4. The project-wide **Remote Entity Config** in **Project Settings, Plugins, Crowdy Mass**. 5. A config the SDK works out from the class itself, called derivation. Most characters use the fifth and never author a config. ### Per-class bindings are inherited A binding on a base class also covers every class that derives from it, and the nearest bound ancestor wins. Bind a base character and also one of its Blueprint children, and the child uses its own row while every other child uses the base's row. Two consequences follow. If you bind a broad base class, you have bound its whole subtree. And if one variant should look different, give it its own row. Do not delete the ancestor's row to get there, because that changes every class beneath it. When a warning says an authored config is overriding something your class declares, it names the class that actually carries the row. If nothing binds your class, it says so and names what answered instead. ### What derivation builds With nothing authored, the SDK builds a config in memory from your class, in this order: 1. `UCrowdyMassRemoteEntityTrait`: transform, actor, velocity, viewer information, identity and interpolation. 2. `UCrowdyMassViewPoseTrait`: the fragment a replicated pose lands in. 3. `UMassMovableVisualizationTrait` with the mesh found above, every level of detail set to a skinned mesh instance. The mesh is placed where the class places it: the mesh component's location and rotation become the mesh entry's **Local Transform**, so a character whose mesh sits 90 cm below its capsule centre, turned a quarter, stands on the ground and faces forward as a row too. A mesh nested under other components is placed by the whole chain up to the actor's root, and a mesh that is the root gets no placement of its own. When the class's anim set names a **Bank**, the mesh gets that bank as its transform provider. 4. `UCrowdyMassTraceCollisionTrait`, with an object type taken from a capsule component or from the mesh's physics asset. 5. `UCrowdyMassAnimationTrait`, only if the class names an anim set. 6. The traits your class declares, last. See [Add your own crowd data](./add-your-own-crowd-data.md). The **Crowdy Mass Trace Collision** trait keeps its settings in an `FCrowdyMassTraceCollisionSettings` under **Trace Collision**: | Label | Default | What it does | |---|---|---| | **Enable Trace Collision** | on | Lets traces hit the row. | | **Object Type** | **WorldDynamic** | What sort of thing the row is, for traces that look for one object type. | | **Default Response** | **Ignore** | The response to a channel with no override. | | **Response Overrides** | **Camera** blocks | A list of `FCrowdyMassCollisionResponseOverride`, each a **Channel** and a **Response**. | A config you assemble in code can set only the object type, with `UCrowdyMassTraceCollisionTrait::SetObjectType`. The result is cached per class, and one summary line names the mesh, the route and the material verdict. If no mesh resolves, nothing is derived and the row does not appear. The log says `nothing configures a Mass entity config for` the class, `and one could not be worked out from the class, so entities of it will not appear`, followed by what is missing and what to change. A class with a **Crowdy Mass Anim Set** component whose set names a **Bank** animates with nothing else authored. A class with no anim set, or a set with no **Bank**, is not a failure: the log says it was built from the class itself with no animation, and its rows render in their bind pose. To give them a set and a bank, see [Set up the animation set](./set-up-the-animation-set.md); if rows stand in bind pose when they should animate, see [Fix a crowd that does not look right](./troubleshooting.md). ### Author a config by hand You rarely need to. Author one when you want level-of-detail distances, spawn counts or representation types that a derived config does not give you. Make a Mass Entity Config asset with these traits: **Crowdy Remote Entity**, **Crowdy Mass Trace Collision** if the row must be traceable, and **Crowdy Mass Animation** to animate it. Then bind it on the **Backend Config** or set it as the **Remote Entity Config**. An authored config keeps control of its own mesh description. In its visualization trait, the **Skinned Mesh Instance Desc** entry under **Meshes** names the **Transform Provider** itself, and it should be the same bank as the anim set's **Bank**, because the set's rows and its validation index into that **Bank**. See [Connect the bank to the crowd's mesh](./set-up-the-animation-set.md#connect-the-bank-to-the-crowds-mesh). The entry's **Local Transform** is yours to set too: give it the location and rotation the class's mesh component has, or the rows float by the mesh's drop and face a quarter turn away. :::caution[An authored config takes over completely.] The **Crowdy Mass Entity Traits** component on the class is not read at all, and you get one warning per class saying so. The Near Tier project settings do not apply; the config's own level-of-detail settings win. ::: :::caution[A config with no animation trait draws in bind pose, even when the class has an anim set.] The class's **Crowdy Mass Anim Set** component is still found, so poses and actions resolve their names, but nothing renders the set. Rows stand in bind pose and the log says `nothing renders that set` and names the set. `crowdy.mass.derive` reports the same. Add a **Crowdy Mass Animation** trait that names the set to the config. ::: To compare a derived row against your setup without unwinding it, set the console variable: ``` crowdy.mass.derive.force 1 ``` With it on, every class is built from itself. It pushes aside per-class bindings, the backend override, the project-wide config, and the anim set that an authored config's animation trait names; the class's own **Crowdy Mass Anim Set** component is used instead. Rows already spawned keep the config they were built from, and every class's animation answer is worked out again. ## Gotchas - The Mass backend skips your own locally owned entity. Use **Render Local Owner** or an area spawner to see a row on one client; see [Check it with one client](./turn-mass-on-for-a-map.md#check-it-with-one-client). - An update names its class by id, and an observer resolves only classes that were loaded when the SDK started. List the class under **Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes**, or a spectator, or an observer that has not spawned one itself, may not resolve it. The log warns when an id does not resolve, and the entity is not drawn; see [A Dynamic entity can appear with no spawn event at all](../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all) on Entities and Spawning. Check this before suspecting a pose or a field. - `crowdy.mass.derive` and `crowdy.mass.derive.force` are compiled out of Shipping builds; see [Fix a crowd that does not look right](./troubleshooting.md). ## Related - [Turn Mass on for a map](./turn-mass-on-for-a-map.md): the backend, its config and the material flag. - [Set up the animation set](./set-up-the-animation-set.md): the anim set a row animates from. - [Add your own crowd data](./add-your-own-crowd-data.md): your own traits on a derived config. - [Entity Component](../runtime/entity-component.md): every setting of the component used above. - [Continuous State](../runtime/continuous-state.md): the executor and the snapshot it sends. - [Fix a crowd that does not look right](./troubleshooting.md): a row at the origin, in bind pose, or missing. # Set up the animation set Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/set-up-the-animation-set # Set up the animation set A crowd row has no animation graph, so it cannot play a clip the way an actor does. Instead you author one asset, a Crowdy Mass Anim Set, that lists what the rows of your character class can look like: how they move at each speed, which held poses they can take, and which one-shot actions they can play. This page builds `DA_LanternPlayerAnims`, the anim set for the Oakford player, and `TP_LanternPlayer`, the bank of clips it indexes into. ## What a crowd row can show A row shows one of three things at a time, and the set has a section for each. | Kind | What it is | What chooses it | |---|---|---| | Gait | Locomotion by speed: idle, walk, run | The observer, from how fast the row is moving. You send nothing. | | Pose | A held state: sitting, fallen, mounted | A replicated enum on your character. See [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md). | | Action | A one-shot that plays once and returns: a wave, a swing | An event, or Send Crowdy Action. See [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md). | A class with no set, or a set with no **Bank**, stands in bind pose. That is the not-yet-authored state, not a failure. ## The set and the bank You author two assets. **The anim set** is a Data Asset of class `UCrowdyMassAnimSet`. In the Content Browser choose **Add**, **Miscellaneous**, **Data Asset**, then pick **Crowdy Mass Anim Set** and name it `DA_LanternPlayerAnims`. It holds rows that name clips. **The bank** is a `UAnimSequenceTransformProviderData`, the engine asset the crowd's mesh draws through. It is a flat list of animation clips, and `TP_LanternPlayer` is the one the Oakford crowd uses. The renderer does not take a clip. It takes an index into that list. [Create the bank](#create-the-bank) below builds it and sets it as the set's **Bank**. The set's **Bank** field names the bank the crowd's mesh draws through. A config the SDK builds from your class gives its mesh that bank, so a class whose set names a **Bank** animates with nothing else authored. The editor also checks your rows against the same **Bank** while you work. ### The renderer only reacts when the index changes This one engine behavior shapes how you author everything else. :::caution[Hand the renderer the index it already has and nothing happens.] The renderer starts a new animation only when the index it is given changes. Ask for the same index again and the animation does not restart and does not blend. It stays where it is. ::: Three authoring rules follow from it. - **An action needs two bank slots holding the same clip.** Playing the action alternates between them, so the index always changes and the clip always restarts. Playing a wave twice in a row only works because of the second slot. - **A pose needs a bank slot of its own.** If a pose shares a slot with the animation the row returns to, the renderer is handed the index it already has when the pose ends. The row stays frozen in the pose. - **Two gaits may not share a clip.** They would resolve to the same slot, and switching between them would change nothing. [Sync actions into the bank](#sync-actions-into-the-bank) does the first rule for you. [Validation](#check-the-rows-against-the-bank) checks all three. ## Create the bank The bank is an asset you create in the editor, fill with clips, and set as the anim set's **Bank**. Do these three steps once for each character mesh. ### Create the asset 1. In the Content Browser choose **Add**, **Miscellaneous**, then **Transform Provider Data**. The editor opens a class picker titled **Pick Transform Provider Data Class**. 2. Choose **Anim Sequence Transform Provider Data** and name the asset `TP_LanternPlayer`. 3. Open it and set **Skinned Asset** to the Skeletal Mesh your character class draws with. The Skeletal Mesh must have **Optimize for Instancing** switched on in its build settings. Validating the bank reports an error naming the mesh when it does not. ### Add the clips Leave **Sequence List** empty and add one entry under **Sequences** for each clip a gait or pose will use. For `TP_LanternPlayer` those are the Idle, Walk, Run, Sitting and Fallen clips. Each entry has a **Sequence**, a **Play Rate** and a **Position**. - An entry with no **Sequence** is a validation error, and the engine treats a bank with such an entry, or with no entries, as switched off. - Every clip must be for the mesh's skeleton, or for one the engine can map onto it. Validation reports a clip whose skeleton it cannot map. - A clip is found by its position in the list, so add new clips at the end. Removing or reordering entries changes the slot number every action row already holds. You do not add the action clips by hand. [Sync actions into the bank](#sync-actions-into-the-bank) appends them. ### Connect the bank to the crowd's mesh Open `DA_LanternPlayerAnims` and set **Bank**, in the set's **Bank** section, to `TP_LanternPlayer`. That is the only step a class needs. The bank reaches the crowd through the mesh description of the class's Mass entity config. Each entry of the description has an **Asset**, the Skeletal Mesh, and a **Transform Provider**, the bank. When the first row of a description is drawn, Mass creates one instanced skinned mesh component for each entry, gives it the **Asset**, and sets the **Transform Provider** as the component's bank. Every row that shares the description shares that bank. A config the SDK builds from your class fills in the mesh description for you. It gives the crowd's mesh the set's **Bank** as its transform provider, so a class with a **Crowdy Mass Anim Set** component whose set names a **Bank** animates with nothing else authored. A set with no **Bank**, or a class with no anim set, stands in bind pose, and the log says the mesh carries no bank so nothing can be animated. An authored Mass Entity Config keeps control of its own mesh description, so it names the bank itself: 1. Author a Mass Entity Config for the class, as [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md) describes, and give it a visualization trait. 2. In the trait's **Skinned Mesh Instance Desc**, add one entry under **Meshes**. Set its **Asset** to the character's Skeletal Mesh and its **Transform Provider** to `TP_LanternPlayer`, the same bank as the set's **Bank**. 3. Add a **Crowdy Mass Animation** trait to the config and set its **Anim Set** to `DA_LanternPlayerAnims`. 4. Bind the config to the class on the **Backend Config**, or set it as the **Remote Entity Config**. :::caution[An authored config's mesh should name the same bank as the set's Bank.] The set's rows and its validation index into the set's **Bank**. If the mesh entry's **Transform Provider** is a different bank, those indices point into a list the crowd never draws with, and validation reports no problem, because the set cannot see which mesh description will render it. ::: The animation pass reads the bank back from the mesh description. If a description carries two different banks it warns and uses the first, because a slot number would mean a different clip in each. The first time the animation pass pairs the set with the bank in a running game, it runs the validation checks and logs each failure. ## Bind the set to the class A set does nothing until a class names it. Add a **Crowdy Mass Anim Set** component (`UCrowdyMassAnimSetComponent`) to `BP_LanternPlayer` and point its **Anim Set** property at `DA_LanternPlayerAnims`. | Label | Default | What it does | |---|---|---| | **Anim Set** | None | The set this class's crowd rows render through. Read off the class, before anything spawns. The component has no runtime behavior and no tick. | ![The Crowdy Mass Anim Set component on the character with its Anim Set property set to DA_LanternPlayerAnims](/img/unreal-sdk/mass-anim-set-component.png) The entity config the SDK works out from your class gains a Crowdy Mass Animation trait from this component. The trait names the set, and the config's mesh gets the set's **Bank**. See [Connect the bank to the crowd's mesh](#connect-the-bank-to-the-crowds-mesh). - A component with an empty **Anim Set** is reported as a component with nothing in it. That is a different message from a class with no component at all: one is a slot to fill, the other is a component to add. - If you authored an entity config for the class by hand, name the set on that config's Crowdy Mass Animation trait, and name the same bank on its mesh entry. The trait wins. The component is still found when the config carries no animation trait, but nothing renders the set, so rows stand in bind pose and the log says so. ## The Oakford set `DA_LanternPlayerAnims` holds these rows. The sections below explain each setting. | Section | Rows | |---|---| | Gaits | `Idle`, `Walk`, `Run` | | Poses | `Sitting` (Below Actions), `Fallen` (Above Actions, Hold Last Frame) | | Actions | `Wave` (id 1), `Cheer` (id 2) | `TP_LanternPlayer` holds the five clips the gaits and poses name, as [Create the bank](#create-the-bank) builds it. You do not add the action slots by hand, because [Sync actions into the bank](#sync-actions-into-the-bank) appends them. ![DA_LanternPlayerAnims in the Details panel with the Sync Actions Into Bank button, the bank TP_LanternPlayer, and three gaits: Idle at 0 and 0, Walk at 100 and 60, and Run at 400 and 330, each with its clip, followed by the smoothing and blend settings at their defaults](/img/unreal-sdk/mass-anim-set-gaits.png) ## The asset, section by section Every setting is on the asset itself or on one of the three row structs, `FCrowdyMassGaitEntry`, `FCrowdyMassActionEntry` and `FCrowdyMassPoseEntry`. | Section | Label | Default | What it does | |---|---|---|---| | Gaits | **Gaits** | Empty | Rows of `FCrowdyMassGaitEntry`. Author them slowest first. Row order decides which gait a row speeds up into. | | Gaits | **Speed Smoothing Time Seconds** | `0.3` | How long the smoothed speed takes to follow a change in real speed. Keep it above one position-update interval. The default suits the 10 Hz updates remote players arrive on. | | Gaits | **Max Smoothing Step Seconds** | `0.1` | The longest frame the smoothing treats as real. Without it, one long frame drops the whole crowd to the slowest gait. | | Gaits | **Blend Seconds** | `0.2` | Blend time when a gait or a pose changes. Does not apply to actions. Zero snaps. | | Actions | **Actions** | Empty | Rows of `FCrowdyMassActionEntry`. Action id N is row N, counting from 1. Id 0 means no action. | | Bank | **Bank** | None | The bank the crowd's mesh draws through, a `UAnimSequenceTransformProviderData`. A config the SDK builds from the class gives its mesh this bank. Validation and **Sync Actions Into Bank** work against it. | | Bank | **Sync Actions Into Bank** (`UCrowdyMassAnimSet::SyncActionsIntoBank`) | Button | Gives each action row its two bank slots in the **Bank**. | | Pose State | **Pose State Enum Is** | **Native By Name** | Whether the state enum is declared in C++ (**Native By Name**) or is a Blueprint Enumeration asset (**Blueprint Asset**). Chosen with `ECrowdyMassPoseStateEnumSource`. | | Pose State | **Pose State Enum** | Empty | The enum the pose rows name. A dropdown of every byte-wide enum compiled into the build for **Native By Name**, or an object picker for **Blueprint Asset**. Only one of the two shows. | | Pose State | **Poses** | Empty | Rows of `FCrowdyMassPoseEntry`. Authored in [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md#hold-a-pose-from-replicated-state). | One set may hold at most `CrowdyMassMaxGaits` gaits and `CrowdyMassMaxPoses` poses, both 255. Ids are one byte and 0 means none, so row 256 would take an id that wraps to 0 and clear instead of set. Rows past the limit are refused and validation reports it. :::note[Row 0 of Gaits is the floor.] The first gait row is what every other gait falls back to. Its **Enter Speed** and **Exit Speed** are never read. ::: ## Check the rows against the bank With a **Bank** set, saving or validating the asset checks what would otherwise stay invisible until a crowd is on screen: - an action whose two slots are identical, out of range, or hold two different clips - a gait or pose whose clip is not in the bank - an action whose Sequence (the clip an actor plays) disagrees with the clip its slots render for the crowd - a pose that shares a bank slot with a gait, another pose, or an action The last one is easy to miss. Every row involved is correct on its own, so only a check that sees the bank can catch it. The message says what will happen: ```text so an entity leaving that pose is handed the index it already has and stays frozen in the pose. Give the pose a sequence of its own in the bank. ``` With no **Bank**, validation gives one warning, and only when the set has action rows. It says the slot pair on each action row cannot be checked, because a slot pair is raw numbers that mean nothing without the bank they index. Gait and pose rows name clips as assets, so their row-level checks still run without a bank, but the slot collision checks do not. A named bank that fails to load is an error, not a warning. ## Sync actions into the bank An action is authored as a name plus a clip. The **Sync Actions Into Bank** button works out the two slots. 1. Set **Bank**. 2. Give the action row an **Action Name** and a **Sequence**. Leave **Primary Index** and **Alternate Index** alone. 3. Press **Sync Actions Into Bank** on the asset. The button follows four rules. - It only appends. It never reorders or removes bank entries, so every index your gaits and poses already use keeps its meaning. - It never hands an action the slot a gait or pose already resolves to, even when the clip is the same. It appends a fresh pair instead. - Running it twice changes nothing the second time, even when a row already names its pair in the other order. - Ctrl+Z restores both the set and the bank. It leaves alone the rows it cannot help and names them in the log. A row with no **Sequence** is a crowd-only action whose slots you author yourself. A row whose clip will not load is reported as a fault. The button finishes with a line like this: ```text CrowdyMassAnimSet DA_LanternPlayerAnims: Sync Actions Into Bank appended 4 slot(s) to 'TP_LanternPlayer' and moved the slot pair on 2 action row(s). ``` For `Wave` and `Cheer` that is two new slots each, four in all, and both rows moved. The button writes to the bank asset, so it runs only when you press it. The same function can be called from an editor Python script or a Blueprint editor utility, for a batch of sets or a build step. In Python it is `anim_set.sync_actions_into_bank()`, on the loaded set. It follows the same four rules and logs the same line. It refuses while a game or a Play in Editor world is running, so stop Play first. ## Gaits A gait is locomotion selected by speed. The animation pass smooths each row's 2D velocity into one speed and picks the gait whose band contains it. Selection is local to each observer. Nothing on the sending side changes, and no gait is ever replicated. Each row of `FCrowdyMassGaitEntry` has these settings: | Label | Default | What it does | |---|---|---| | **Gait Name** | None | A name for authoring and messages. Selection uses the row's position and never searches by name. | | **Sequence** | None | The clip. It must already be in the bank. The row matches the clip by asset and does not add it. | | **Enter Speed** | `0.0` | The smoothed speed above which this gait may be entered. Ignored on row 0. | | **Exit Speed** | `0.0` | The speed below which a row leaves this gait for the one below it. Author it below **Enter Speed**. | | **Play Rate** | `1.0` | Applied once, when the gait is entered. The renderer reads playback settings only when the index changes, so a rate cannot track speed continuously. | The gap between **Enter Speed** and **Exit Speed** is the hysteresis band. It stops a row hovering at a boundary from swapping clips every frame. Make it wider than the ripple the smoothing leaves behind, because a remote player's speed arrives as bursts of movement between position updates. The Oakford player uses three rows. Speeds are in Unreal units per second. | Row | **Gait Name** | **Enter Speed** | **Exit Speed** | |---|---|---|---| | 0 | `Idle` | 0 | 0 | | 1 | `Walk` | 100 | 60 | | 2 | `Run` | 400 | 330 | ### What validation rejects These are errors, reported when the asset is saved or validated. - A gait row with no **Gait Name**, with a duplicate name, or with no **Sequence**. - A **Sequence** that does not load, such as a deleted asset. - Two gaits sharing a **Sequence**. Both resolve to the same slot and the per-gait **Play Rate** is discarded. - On any row after row 0, an **Exit Speed** above the **Enter Speed**, or equal to it once the **Enter Speed** is above zero. - From row 2 on, an **Enter Speed** below the row before it. The row can never be shown. Author slowest first. One case is a warning. A row after row 0 with an **Enter Speed** of zero or less is entered at any speed, so every slower row, the floor row included, can never be shown. ## Look rows up from code The set carries lookups for tools and tests. None of them belongs in a per-row loop. | Blueprint name | What it returns | |---|---| | **Find Pose Id** (`FindPoseId`) | The id of a pose by name. 0 for an unauthored name or `None`. | | **Find Pose Id by State Value** (`FindPoseIdByStateValue`) | The id a state enum value selects. A value no row names gives 0, which clears the pose. | | **Find Action Id** (`FindActionId`) | The id of an action by name. 0 for an unauthored name. | | **Find Action Sequence** (`FindActionSequence`) | The clip an actor plays for an action id, or none for a crowd-only row. | | **Get Action Duration Seconds** (`GetActionDurationSeconds`) | An action's length, read through its **Primary Index** slot of a bank you pass in. | | **Resolve Gait** (`ResolveGait`), **Resolve Pose** (`ResolvePose`) | An `FCrowdyMassResolvedSequence` for a row against a bank you pass in. | | **Validate Action** (`ValidateAction`), **Validate Against Bank** (`ValidateAgainstBank`) | The bank checks above, at runtime. Both return false if anything failed. **Validate Against Bank** logs each failure. **Validate Action** checks one action's slot pair and does not log. | An action or pose id is a position in one class's set, so it means nothing across two sets. Resolve it against the set of the class you are asking about. ## Gotchas - **The set is shared.** One asset serves every entity of every class that names it. Two classes can map the same enum value to different clips by naming different sets. - **Rows past the limit are lost.** Ids wrap, so the 256th gait or pose is refused rather than risk clearing. - **A wide enum cannot carry a pose.** A pose value rides one byte. A pose row that names an enum entry outside 0 to 255 is an error, and a property declared over a wider enum is not read as the pose. The log says to declare the enum as `enum class` with `: uint8`. ## Related - [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md): drive the pose and action rows from your character. - [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md): the mesh and entity config the bank belongs to. - [Promote close entities to actors](./promote-close-entities-to-actors.md): how the same set drives a near tier actor. - [Fix a crowd that does not look right](./troubleshooting.md#rows-stand-in-bind-pose): rows that stand in bind pose or stay frozen in a pose. # Play poses and one-shot actions Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/play-poses-and-one-shot-actions # Play poses and one-shot actions A crowd row has no animation graph, so you cannot tell it to sit or wave from a graph of its own. You tell it from your character class instead. A held pose rides a replicated enum property. A one-shot action rides a Crowdy event. Both are authored once on the actor class. Every observer that draws the player as a crowd row animates it from the rows of your anim set; [Set up the animation set](./set-up-the-animation-set.md) builds it. This page makes the Oakford player sit, fall, wave and cheer. ## Pose or action | | Pose | Action | |---|---|---| | What it is | A held state: sitting, fallen, mounted | A one-shot: a wave, a swing, a flinch | | Driven by | A replicated enum property | An event, or Send Crowdy Action | | Late joiners | See it, once the heartbeat re-sends it | Miss it. An action is a moment. | | Row in the set | **Poses**, matched by enum entry name | **Actions**, matched by id | :::danger[Everything on this page is view state, and a client can send any value.] A pose value, an action id and a play rate come from the observed client. They decide what is drawn, never what is true. Whether a player is really fallen is a hit-point question that belongs to your server code. The owning client sets `Stance` from that truth. See [The Two Planes](../concepts/two-planes.md). ::: ## Hold a pose from replicated state A pose is not derived from motion, so something has to say which pose a player is in. That something is one replicated enum property on your character. The SDK matches the arriving value against the enum your anim set names and shows the row that value selects. ### Declare the enum and the property The Oakford stance is `ELanternStance`, with the entries `Standing`, `Sitting` and `Fallen`. The `Stance` property on `ALanternPlayer` is marked `CrowdyState` and `CrowdyHeartbeat`. ```cpp // LanternPlayer.h UENUM(BlueprintType) enum class ELanternStance : uint8 { Standing, Sitting, Fallen }; UPROPERTY(BlueprintReadOnly, Category = "Lantern", meta = (CrowdyState, CrowdyHeartbeat)) ELanternStance Stance = ELanternStance::Standing; ``` The type is the contract and the name is yours. The SDK binds its pose writer to whichever `CrowdyState` property is declared as the enum your anim set names. 1. Add a variable named `Stance` to `BP_LanternPlayer`. Make its type the `ELanternStance` enum. A project with no C++ enum creates a Blueprint Enumeration asset and uses that. 2. In the variable's details, set **Replication** under **Crowdy Replication** to **Replicated**. **Keyframe heartbeat** is ticked by default for a newly replicated variable. Leave it ticked. 3. Compile. ![The Details panel for the Stance variable, with Replication set to Replicated under Crowdy Replication and the variable type set to ELanternStance](/img/unreal-sdk/mass-stance-variable.png) :::caution[The enum must be one byte wide.] Declare it as `enum class ELanternStance : uint8`. The value rides one byte on the wire, so a wider enum cannot carry a pose. A Blueprint Enumeration is always one byte. ::: Declare only one property of that enum on the class. Two properties ask for the same pose rows: the first declared drives the pose, the other is never read, and the log names both. A class whose anim set names no enum, or that has no property of that enum, binds no pose, and the log says which of the two it is. ### Keep the heartbeat on `CrowdyHeartbeat` is not optional in practice. Crowdy State rides unreliable multicast, and an enum is held state: it is sent when it changes and never again. One dropped delta leaves a fallen player standing on every other screen until something else changes the value. The heartbeat re-sends the value on a timer so observers converge. Two other settings must allow it: - The map profile's **State Keyframe Interval (Seconds, 0 = off)** must not be 0. It defaults to `2.0`. - The entity component's **State Heartbeat** must not be **Off**. The default, **Inherit**, follows the map profile. The SDK warns once per property for any Crowdy State enum outside the heartbeat. `crowdy.state.heartbeat.advisories` lists them all. See [The metadata keys](../runtime/crowdy-state.md#the-metadata-keys), [What ships, and when](../runtime/crowdy-state.md#what-ships-and-when) and [Settings](../runtime/entity-component.md#settings) for the keys and the cadence. ### Name the enum on the set and author the rows Open `DA_LanternPlayerAnims`. In **Pose State**, leave **Pose State Enum Is** on **Native By Name**, the default and the right answer for a C++ enum, and pick `ELanternStance` from the **Pose State Enum** dropdown. For a Blueprint Enumeration, switch the first field to **Blueprint Asset** and pick the asset. Then add rows to **Poses**. Each row is an `FCrowdyMassPoseEntry`: | Label | Default | What it does | |---|---|---| | **Pose Name** | None | A dropdown of the enum's entries. The row shows when the arriving value is that entry. | | **Sequence** | None | The clip. It must already be in the bank, in a slot no gait, other pose or action uses. | | **Rank** | **Below Actions** | `ECrowdyMassPoseRank`. **Above Actions** makes the pose beat a one-shot action while it is held. See [Pose rank](#pose-rank). | | **Hold Last Frame** | Off | Stops on the last frame instead of looping. Tick it for a fall or a corpse. | | **Play Rate** | `1.0` | Playback rate. Cannot be negative. | The Oakford set authors two rows: | **Pose Name** | **Rank** | **Hold Last Frame** | |---|---|---| | `Sitting` | **Below Actions** | Off | | `Fallen` | **Above Actions** | On | ![DA_LanternPlayerAnims showing the Wave action with Primary Index 5 and Alternate Index 6 and the Cheer action with 7 and 8, each with its clip, then Pose State Enum Is set to Native By Name, Pose State Enum set to ELanternStance, and the Sitting pose ranked Below Actions and the Fallen pose ranked Above Actions with Hold Last Frame ticked](/img/unreal-sdk/mass-anim-set-poses-actions.png) Row order is free. A value's meaning comes from your enum, and the row's id is worked out for you. A value no row names clears the pose and drops the row through to locomotion. That is why `Standing` has no row: a standing player walks, runs or idles. The class must also name the set, with a **Crowdy Mass Anim Set** component on `BP_LanternPlayer`. See [Bind the set to the class](./set-up-the-animation-set.md#bind-the-set-to-the-class). A class without it, or whose set names no **Bank**, stands in bind pose. ### What validation rejects - **Poses** has rows but **Pose State Enum** is empty. The error reads: `Poses has rows but no state enum is set, so no replicated state value can select any of them and this set cannot pose a crowd entity at all.` - A **Pose State Enum** name that no enum matches, or a Blueprint Enumeration named as a string instead of picked under **Blueprint Asset**. A string leaves nothing referencing the asset, so a cooked game would not find it. - A **Pose Name** that is not an entry of the enum, or left empty. - Two rows on the same entry, or on two entries that share one value. - A row with no **Sequence**, or a **Sequence** that does not load. - A pose entry whose value falls outside 0 to 255. Two cases are warnings. A row on enum value 0 means entities in the resting state never walk or run, because a pose that resolves always outranks locomotion. A pose ranked **Above Actions** without **Hold Last Frame** loops, which is rarely what a fall or a stun wants. ### Set the pose Write the property on the machine that owns the player. The owning client diffs it and ships the change, and every observer applies it. There is no pose id to look up and no set to have loaded. The Oakford sit key, X, toggles `Stance` between `Standing` and `Sitting`. The C++ `OnSit` below also sets the player's torch brightness, 0.3 while seated and 1.0 standing. That part belongs to [Add your own crowd data](./add-your-own-crowd-data.md), and the Blueprint steps leave it out. ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" void ALanternPlayer::OnSit() { Stance = Stance == ELanternStance::Sitting ? ELanternStance::Standing : ELanternStance::Sitting; // A seated player's torch burns low, here and on every crowd row that draws this player. TorchBrightness = Stance == ELanternStance::Sitting ? 0.3f : 1.f; Torch->SetIntensity(TorchBrightness * 2000.f); } ``` 1. In `BP_LanternPlayer`, add a key event for the **X** key, the sit key. 2. From the event, call **Is Crowdy Entity Locally Controlled** and feed a **Branch**, so only the owner writes. 3. On the true pin, feed a second **Branch** from an **Equal (Enum)** that compares **Get Stance** to `Sitting`. 4. On its true pin call **Set Stance** with `Standing`. On its false pin call **Set Stance** with `Sitting`. [Blueprint snippet: mass-sit-stance, see /bp/unreal-sdk/mass-sit-stance.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. On an observer drawing the player as a row, the byte arrives, is matched against `ELanternStance` because that is the enum the set names, becomes a pose row id, and lands on the row's pose. The animation pass picks it up on its next run. The observer's own machine holds a real actor, so it does not need any of this to see its own player sit. ### Set a pose locally, by name **Set Crowdy Mass Entity Pose** (`UCrowdyMassTargetLibrary::SetEntityPose`) puts one row in a pose by name and replicates nothing. It is for presentation on your own machine: a held pose in a menu, a cutscene, a test. The next update that carries a pose overwrites it. | Pin | What it takes | |---|---| | **Target** | The row, from a Crowdy Mass trace or an event. See [Target an entity, from Blueprint](./from-blueprint/target-an-entity.md). | | **Pose Name** | An authored pose name. `None` clears the pose. | The name is resolved against the anim set of the class the target stands for, never the caller's class. The node returns false when it refused: off the game thread (an error in the log), while Mass is processing, or when the target's class is not yet known and you named a pose. Add the class to **Preloaded Entity Classes** under **Project Settings, Plugins, Crowdy SDK, Replication**; see [Entities and Spawning](../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all). `UCrowdyMassEntitySubsystem::SetEntityPose` does the same thing addressed by entity UUID. :::caution[A pose name the class's set does not author clears the pose.] It resolves to id 0, and 0 is the value that clears. The node still returns true. An entity with no recorded class is refused instead, so a misspelled name on a known class is the case to watch. ::: ## Play a one-shot action An action is a row in the set's **Actions** array. **Action ids start at 1 and follow the row order, and 0 means no action.** In `DA_LanternPlayerAnims`, `Wave` is id 1 and `Cheer` is id 2. Only the id travels on the wire. Each observer resolves it against the set of the class the player is, so ids mean nothing across two different sets. ### Author the action rows Each row is an `FCrowdyMassActionEntry`: | Label | Default | What it does | |---|---|---| | **Action Name** | None | The name **Send Crowdy Action** resolves to an id. | | **Primary Index** | `-1` | A bank slot. Filled in by Sync Actions Into Bank. | | **Alternate Index** | `-1` | The second slot, holding the same clip. | | **Play Rate** | `1.0` | The row's rate. A crowd row plays at the rate the event carries, not this one. | | **Sequence** | None | The clip an actor plays. Unset means crowd only. | **Primary Index** and **Alternate Index** must be two different slots holding the same clip. An unsynced row has both at `-1`, which is the same error as identical slots: `retriggering it will not restart playback`. Press **Sync Actions Into Bank** on the asset, as [Set up the animation set](./set-up-the-animation-set.md#sync-actions-into-the-bank) describes. Set **Sequence** too if actors should play the action, as [Play it on an actor](#play-it-on-an-actor) explains. ### Send an action by name If all you want is a row to play a named action on everyone's screen, you do not need to declare an event. **Send Crowdy Action** takes a target and an action name. The Oakford player aims at another player's row and presses C to cheer it on. `TracedTarget` is the member the trace in [Target an entity, from C++](./from-cpp/target-an-entity.md) fills in. ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" #include "Target/CrowdyMassTargetLibrary.h" void ALanternPlayer::OnCheer() { if (!UCrowdyMassTargetLibrary::IsTargetLive(TracedTarget)) { return; } UCrowdyMassTargetLibrary::SendEntityAction(TracedTarget, TEXT("Cheer"), 0, 1.0f); } ``` 1. Get a **Target** with **Crowdy Mass Line Trace Single**, and keep the hit's **Target** in a variable named `TracedTarget` when **Hit Mass Entity** is true. See [Target an entity, from Blueprint](./from-blueprint/target-an-entity.md). 2. Add a key event for the **C** key, the cheer key. Call **Is Crowdy Mass Target Live** with `TracedTarget` and feed its result into a **Branch**, so a stale target sends nothing. 3. On the true pin, call **Send Crowdy Action** (`UCrowdyMassTargetLibrary::SendEntityAction`). Plug `TracedTarget` into **Target** and type `Cheer` into **Action Name**. 4. Under the node's advanced pins, **Variant** defaults to 0 and **Play Rate** to 1.0. [Blueprint snippet: mass-send-action, see /bp/unreal-sdk/mass-send-action.txt] The name is resolved here against the set of the class the target is recorded as, and only the resolved id is sent. Everyone who draws that player as a crowd row or as a near tier actor sees the action, this client included. :::caution[Send Crowdy Action does not reach a machine that holds the player as a real actor.] A player's own machine holds a real `ALanternPlayer`, not a row, so the cheered player does not see the cheer on themselves. It reaches only machines that draw the player as a row or a near tier actor. For an action the sender's own pawn also shows, declare an event. ::: The node returns false and logs when it did not send. It logs each of these reasons once per class and name: - The class is not yet known on this client. List the class under **Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes**, as [Entities and Spawning](../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all) describes. - The class names no anim set. - The set authors no action with that name. It refuses in two more cases and logs an error: when it is called off the game thread, and while Mass is processing. A target that names no live entity returns false without a log, because a held target hits that every frame. There is no ownership check: any client may play an action on any row. It is a visual. ### Declare an event an action To run your own code with the action as well, such as a sound or a spark, declare a Crowdy event and mark it an action. Then a client that draws the player as a row starts the matching action with nothing to register and no graph to author. The Oakford event is `WaveTorch`. It is a `SpatialMulticast` event on `ALanternPlayer` taking `ActionId`, `Variant` and `PlayRate`. The `CrowdyAction` meta key joins the [routing keys](../runtime/rpc-events-cpp.md#routing-keys) on the receiver. ```cpp // LanternPlayer.h #include "Replication/RPC/CrowdyEvent.h" UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "SpatialMulticast", CrowdyAction)) void WaveTorch_Implementation(int32 ActionId, int32 Variant, float PlayRate); CROWDY_EVENT(WaveTorch) ``` 1. Add a Custom Event named `WaveTorch` to `BP_LanternPlayer` and tick **Crowdy Replicates**. 2. Leave **Recipient** on **Spatial Multicast**. See [Pick the recipient](../runtime/rpc-events-blueprint.md#pick-the-recipient). 3. Tick **Is A One-Shot Action**, the row below **Crowdy Replicates**. 4. Press **Add Parameters**. It creates the three pins the SDK reads: `ActionId`, `Variant` and `PlayRate`. 5. Compile. ![The Details panel for the WaveTorch custom event with Crowdy Replicates ticked and showing Replicated (3 parameters), Is A One-Shot Action ticked, Recipient set to Spatial Multicast, and the inputs ActionId, Variant and PlayRate](/img/unreal-sdk/mass-one-shot-action.png) **Is A One-Shot Action** is visible only once a map profile assigned in **Project Settings, Plugins, Crowdy SDK** (a **Map Profiles** entry or the **Default Profile**) selects a backend that draws crowd rows, and **Crowdy Replicates** is ticked. A project whose maps all use the actor pool never sees it. An observer drawing the player as a row reads `ActionId` off the arriving call and starts the matching row of that class's set. The parameters are read by name. Like every `FName` match, the match ignores case. | Parameter | Type | Rules | |---|---|---| | `ActionId` | `int32` input | Required. A missing one, an `int64`, or a value of 0 or less starts nothing. | | `Variant` | `int32` | Clamped to 0 to `CrowdyMassMaxActionVariant` (63). A missing one reads 0. It selects nothing. Pass 0. | | `PlayRate` | `float` or `double` | Clamped to `CrowdyMassMinActionPlayRate` (0.1) to `CrowdyMassMaxActionPlayRate` (4.0). A value that is not a finite number becomes 1.0. A missing one reads 1.0. | The clamps exist because every one of these values came off the network from the observed client and is forgeable. `CrowdyMassClampActionPlayRate` applies the same clamp if you write your own handler. An event that spells the parameters differently still works, but needs a registered handler built with `CrowdyMassMakeActionEventHandler` and the three names. A handler registered in C++ replaces both the event body and the automatic action write. A handler you register for an action event, such as `WaveTorch`, must write the action itself. A bound **Receive Crowdy Event on Entity** node replaces the body only, and the row still plays the declared action. See [Receive its events, from C++](./from-cpp/receive-its-events.md), [Receive its events, from Blueprint](./from-blueprint/receive-its-events.md) and [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md). An event marked an action that carries no `int32` `ActionId` starts nothing, and the SDK reports that once per function. From the outside it looks exactly like an event nobody sent. :::caution[In Blueprint, recompile after ticking Is A One-Shot Action.] The marker lives on the node and is read from the compiled function. If the box is ticked but the class was not recompiled, the event replicates and its body runs, and no crowd row ever animates from it. Check with `crowdy.rpc.dumpfn BP_LanternPlayer_C WaveTorch` on the sender and on the receiver. Both should print `action=1`. ::: ### Play it on an actor Marking an event an action animates the crowd. It does not animate an actor, because an actor has a body and the body has to play the same clip, or the two representations of one event drift apart. `UCrowdyMassActionLibrary::PlayCrowdyMassAction` closes that. It takes the clip from the same authored row the crowd renders. The `WaveTorch` body calls it, and the input handler that waves calls `WaveTorch`. ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" #include "Mass/CrowdyMassActionLibrary.h" void ALanternPlayer::WaveTorch_Implementation(int32 ActionId, int32 Variant, float PlayRate) { UCrowdyMassActionLibrary::PlayCrowdyMassAction(GetMesh(), ActionId, Variant, PlayRate); } void ALanternPlayer::OnWave() { // Action ids are one-based rows of the anim set, and Wave is its first action. WaveTorch(1, 0, 1.f); } ``` 1. In the `WaveTorch` event, call **Play Crowdy Mass Action** (`PlayCrowdyMassAction`). Plug the event's pins into **Action Id**, **Variant** and **Play Rate**, and the character's `Mesh` into **Mesh**. 2. For the sender, add an input action event for the wave key. Call **Is Crowdy Entity Locally Controlled** into a **Branch** so only the owner sends. 3. On the true pin, call `WaveTorch` with **Action Id** set to 1, `Wave`'s id. To look it up by name instead, call **Find Action Id** on `DA_LanternPlayerAnims` with `Wave`. The figure shows the body from step 1: the `WaveTorch` event feeds **Play Crowdy Mass Action**, with its three pins plugged in and the character's `Mesh` as the target mesh. The sender in steps 2 and 3 is not in the figure. [Blueprint snippet: mass-wave-body, see /bp/unreal-sdk/mass-wave-body.txt] That one call is right on every machine: | The machine holds the player as | What plays the action | |---|---| | Its own pawn, or any real actor | The body runs, and this call plays the clip. | | A crowd row | The row starts the same action from the arriving event. | | A near tier actor drawn in place of the row | The body runs on the stand-in, as for a row, and the row starts the action. The near tier actor then plays it through its animation Blueprint. See [Promote close entities to actors](./promote-close-entities-to-actors.md). | On a machine that draws the player as a row or as a near tier actor, your body runs on a stand-in with no animation instance, so the call finds nothing to play on and does nothing. That is expected. See [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md). An action id is a row position in one class's set, so the call resolves the set of the class of the actor it was given and never a class-less lookup. The full signature quoted from the library: ```text PlayCrowdyMassAction(USkeletalMeshComponent* Mesh, int32 ActionId, int32 Variant = 0, float PlayRate = 1.0f, FName SlotName = TEXT("DefaultSlot"), float BlendInSeconds = 0.1f, float BlendOutSeconds = 0.1f, float StartSecondsIntoAction = 0.0f) ``` | Pin | Default | What it does | |---|---|---| | **Mesh** | Self | The skeletal mesh to play on. The action's set comes from this mesh's owning class. | | **Action Id** | | The row to play. | | **Variant**, **Play Rate** | `0`, `1.0` | As above. The rate is clamped. | | **Slot Name** (advanced) | `DefaultSlot` | The slot node the montage plays through. | | **Blend in Seconds**, **Blend Out Seconds** (advanced) | `0.1`, `0.1` | Blend times. | | **Start Seconds Into Action** (advanced) | `0.0` | Seeks the clip forward before it starts. | `UCrowdyMassActionLibrary::PlayCrowdyMassActionFromSet(Mesh, AnimSet, ActionId, Variant, PlayRate, SlotName, BlendInSeconds, BlendOutSeconds, StartSecondsIntoAction)` is the C++ twin that takes the `UCrowdyMassAnimSet` as an argument instead of working it out from the mesh's owner. Use it when the actor stands in for an entity of a different class, so the action id is looked up in the set it was authored in. It returns the `UAnimMontage` it started, or null, so you can stop it again. Both calls read the actor's class, not the Mass subsystem, so they work on an actor pool map too. The action plays as a dynamic montage into a slot, never through `PlayAnimation`, which would put the component into single-node mode and discard your animation Blueprint for good. :::caution[The mesh's animation graph needs a Slot node named like **Slot Name**.] The engine default is `DefaultSlot`, which the stock mannequin animation Blueprint already has. Without a slot of that name you get a warning of this shape: `'' refused action ; its animation graph has no 'DefaultSlot' slot node.` ::: The call returns false, with one warning per action id, when the class names no anim set, when the id names no row, or when the row authored no **Sequence**. For an unauthored row the log says the crowd may still render it from its slot pair. If a row's **Sequence** disagrees with the clip its **Primary Index** slot holds, validation reports an authoring error, because that would show one player a wave and another an emote for the same event. `FindCrowdyMassActionSequence` returns the clip an actor of a class would play, for a body that does something other than play it into a slot. ### Pose rank `ECrowdyMassPoseRank` decides what happens when an action starts while a pose is held. A pose ranked **Below Actions**, the default, lets the action play over it and the pose returns after. A pose ranked **Above Actions** overrides actions for as long as it is set. The composition order on a crowd row is: an **Above Actions** pose, then an active action, then any pose, then the gait. So `Sitting` lets a seated player wave. `Fallen` does not, so a fallen player cannot wave from the ground. :::caution[A near tier actor does not apply pose rank.] On a near tier actor the action plays into its slot whatever the pose, so a pose that should suppress actions there has to be handled in the animation Blueprint itself. See [Promote close entities to actors](./promote-close-entities-to-actors.md). ::: ## Check it in Oakford Run two clients on a map whose profile selects the Crowdy Mass Rendering Backend. Each client draws the other as a row. 1. On client A, press X to sit. Client B sees A's row take the `Sitting` pose and hold it. Press it again and the row returns to idle, walk or run. 2. On client A, press the wave key. Client B sees A's row play `Wave` once. A's own screen plays it through the `WaveTorch` body. 3. Aim at B's row from A and press C to cheer. Only machines that draw B as a row or a near tier actor see `Cheer`, so A sees it and B does not, because B holds its own player as a real actor. ## Gotchas - **A pose and a field cannot share one property.** A `CrowdyState` property can feed your own state fragment field or the pose, not both. The log says so. See [Add your own crowd data](./add-your-own-crowd-data.md). - **An action is a moment.** A player who arrives after the wave sees nothing. Use a pose for anything a late joiner must see. - **Unauthored means no animation.** A class with no set, a set with no **Bank**, or a set with no row for an arriving value, shows locomotion or bind pose. That is not an error. ## Related - [Set up the animation set](./set-up-the-animation-set.md): the set, the bank and **Sync Actions Into Bank**. - [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md): what an event body does on a machine that draws a row. - [Promote close entities to actors](./promote-close-entities-to-actors.md): the animation Blueprint side of poses and actions. - [Call an event on it, from Blueprint](./from-blueprint/call-an-event-on-it.md) and [Call an event on it, from C++](./from-cpp/call-an-event-on-it.md): **Send Crowdy Action** and **Call Crowdy Event on Entity**. - [Crowdy State](../runtime/crowdy-state.md) and [RPC Events in C++](../runtime/rpc-events-cpp.md): the properties and events these use. # Promote close entities to actors Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/promote-close-entities-to-actors # Promote close entities to actors A crowd row has no bones you can reach, no animation Blueprint and no attachments. That is fine at a distance and wrong up close. The near tier promotes a crowd row that comes close to the camera into a real, pooled actor, the near tier actor, and turns it back into a row when it moves away. On this page you build the shell, `BP_LanternPlayer_Shell`, point `ALanternPlayer` at it, and write the animation Blueprint it uses. ## When you need this Use a near tier when players that come within a few metres of the camera should look like a full character: a real animation graph, a held torch attached to a hand socket, a material you change at runtime. Skip it when a crowd row is good enough at every distance. Promotion is opt-in per class. A class that declares no shell is drawn as a row at every distance and costs nothing extra. Everything on this page is an observer-side picture. The entity stays the thing that exists: it keeps its identity, its state and its place in the crowd, and the near tier actor only shows it. It never owns state and never sends anything. It does not receive events either. An event aimed at a promoted entity runs on the per-class stand-in exactly as it does for a row, and the near tier actor shows any action through its animation Blueprint. See [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md). ## The three pieces 1. **The class declares a shell.** `BP_LanternPlayer` gets a **Crowdy Mass Near Tier Shell** component (`UCrowdyMassNearTierShellComponent`) whose **Near Tier Actor Class** names the actor to promote to. 2. **The shell is an actor class you build.** The easy path is a Blueprint subclass of **Crowdy Mass Near Tier Actor** (`ACrowdyMassNearTierActor`), which already carries everything the shell needs. 3. **The shell carries a Crowdy Mass Near Tier component** (`UCrowdyMassNearTierComponent`). It joins the spawned actor to its entity and drives it every frame. `ACrowdyMassNearTierActor` has one already, called `NearTier`. The shell declaration is read from the class defaults before anything is spawned, so the answer is worked out once per class, not once per entity. ## Build the shell 1. In the Content Browser, create a Blueprint whose parent class is **Crowdy Mass Near Tier Actor**. Name it `BP_LanternPlayer_Shell`. 2. Open it. It already has three components: | Component | Type | What it is | |---|---|---| | `Root` | `USceneComponent` | The pivot the actor is placed by. It draws nothing. | | `Mesh` | `USkeletalMeshComponent` | What the actor draws, under the pivot. | | `NearTier` | `UCrowdyMassNearTierComponent` | The component that follows the entity. | 3. Select `Mesh`. Set its **Skeletal Mesh** to the mesh `BP_LanternPlayer` uses, and set its **Anim Class** to `ABP_LanternPlayer_Shell`, the animation Blueprint you build for the shell (see [What to put in the AnimBP](#what-to-put-in-the-animbp)). Do not use the character's own animation Blueprint. 4. Leave the event graph empty. An empty shell is the normal shape. 5. Compile and save. 6. Open `BP_LanternPlayer`. In the Components panel choose **Add**, then **Crowdy Mass Near Tier Shell**. Select it and set **Near Tier Actor Class** to `BP_LanternPlayer_Shell`. 7. Compile and save. ![The BP_LanternPlayer_Shell Blueprint editor with the components Root, Mesh and Near Tier listed, the Mesh component selected, and its Details showing Anim Class set to ABP_LanternPlayer_Shell and a Skeletal Mesh assigned](/img/unreal-sdk/mass-near-tier-shell.png) ![The Crowdy Mass Near Tier Shell component on BP_LanternPlayer with Near Tier Actor Class set to BP_LanternPlayer_Shell](/img/unreal-sdk/mass-near-tier-shell-component.png) The class named on the shell component is loaded once per class, when its entity configuration is first worked out. The component itself does nothing at runtime. :::caution[The shell is a picture, not a second copy of your character.] Its near tier actors are created and released constantly as players walk in and out of range. Give it a mesh and the things that decorate a mesh, and nothing that owns state or talks to the network. - **Do not add a movement component or a colliding capsule.** The entity's transform is written onto the near tier actor every frame, and a movement component would fight that write. - **Do not add a Crowdy Entity Component, and do not implement the Crowdy event source interface.** The entity already has its own registered participant, and a second one for the same id is refused. - **Do not add a Crowdy Mass Anim Set component to the shell.** If the shell's class names a different anim set from the entity's, actions are refused for that pairing and the log names both sets. Motion and pose keep working. A shell that names no anim set of its own is the correct case. ::: :::caution[A shell with no mesh asset draws nothing, and the player vanishes until it has one.] The crowd stops drawing a row the moment a promotion is decided, so a near tier actor that is not ready leaves a hole, not a fade. Assign a skeletal mesh asset on the shell before you test. If your shell class adds its own skeletal mesh instead of filling in `Mesh`, readiness, hiding and action playback all use whichever skeletal mesh actually has an asset. Only `Mesh` is placed to match the crowd. ::: ### Entity transform offset **Entity Transform Offset** (`EntityTransformOffset`, in the shell's Class Defaults) says where the shell's root belongs relative to the entity. It points from the entity to the root, in the entity's own space, so it turns with the entity. The default is zero, and zero is right for the shape you built above. The entity's transform is the location the owning client replicates, which for a Character is the middle of its capsule. The shell's root sits there, and its `Mesh` takes the crowd's placement, which already carries the character's drop to its feet. Set an offset only for a shell whose root belongs somewhere other than that location. If a promoted player floats or sinks by a fixed amount while its row stands correctly, this is the value to look at. A C++ shell can override `GetEntityTransformOffset`, which is also a pure node (**Get Entity Transform Offset**), when the offset is computed rather than fixed. ### Correct the mesh, not the placement When a near tier actor is bound to an entity, its `Mesh` is placed to match the crowd's own placement of that character, so a promoted player faces the same way the row did. What you set on the shell's `Mesh` transform is a correction on top of that placement, not the whole placement. Author only the difference. An unauthored `Mesh` takes the crowd's placement unchanged. If a promoted player turns or drops compared with its row, the difference is what to author on `Mesh`. To read both halves, run `crowdy.mass.neartier.trace 1`: the log prints where the near tier actor placed its mesh, where the crowd places it, and what the actor added on top. ## Pooling `ACrowdyMassNearTierActor` is already pooled. Promotions reuse near tier actors instead of spawning and destroying one each time, and you set nothing to get that. If you promote to an actor class of your own instead of a subclass of **Crowdy Mass Near Tier Actor**, that class needs a Crowdy Mass Near Tier component. It should also implement the engine's `IMassActorPoolableInterface` (`CanBePooled`, `PrepareForPooling`, `PrepareForGame`). Without it every promotion is a real spawn and a real destroy, with no warning, which is the cost the near tier exists to avoid. `crowdy.mass.neartier.trace 1` prints a line when an actor is taken from the pool, when one had to be spawned because the pool had none to give, and when one goes back. That log is the only evidence that pooling is happening. ## Distances and counts Three settings in **Project Settings, Plugins, Crowdy Mass** (`UCrowdyMassDeveloperSettings`), under **Near Tier**, decide when a row becomes an actor. | Label | Default | What it does | |---|---|---| | **Near Tier Distance** | 1000 | How close an entity must be, in centimetres, before it is promoted while it is not on screen. | | **Near Tier Visible Distance** | 2000 | How close an entity the camera can see must be before it is promoted. Normally larger than **Near Tier Distance**, because a visible crowd is worth more. | | **Max Near Tier Actors** | 50 | The most entities that may be near tier actors at once. The nearest win. The rest stay crowd rows however close they get. | Only classes that declare a shell are ever promoted. If you ask for distances that reach past the band beyond them, the SDK cuts them back and logs a warning naming the values it used. Lower them to remove the warning. :::note[These settings apply only when the entity configuration is built from your class.] If you authored a Mass entity configuration for the class yourself, its own level distances and ceiling decide when promotion happens, and the near tier settings above are left alone. The log says so when it builds the configuration. See [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md) for where the configuration comes from. - A configuration that already promotes to an actor wins. The shell declaration is never read, and the log warns once per class. - A configuration that names an actor while its nearest level is still drawn as an instance promotes nothing by itself. The declared shell replaces that actor in a per-class copy of the configuration, the class does promote, and the log warns. If the configuration names another representation actor management class, the SDK also swaps in `UCrowdyMassRepresentationActorManagement` and warns, because only that class hides, places and readies a near tier actor the way the handover needs. - The engine also logs `Overriding with another instance` as that copy builds. That line is this working. ::: ## How a near tier actor animates Nothing pushes variables into your animation Blueprint. The SDK does not set AnimBP variables, does not call an event on it and does not need an interface or a special base class. The near tier component reads the entity's data every frame, after physics, and exposes it as read-only getters. Your animation Blueprint reads them. Every getter is a pure node in the **Crowdy Mass|Near Tier** category and is safe to call from an animation graph's thread-safe update. | Node | C++ name | Returns | Notes | |---|---|---|---| | **Is Bound to Entity** | `IsBoundToEntity` | bool | False means there is no answer yet. It is not an error. | | **Get Entity UUID** | `GetEntityUUID` | `FGuid` | The id of the entity this near tier actor stands for. | | **Get Smoothed Speed** | `GetSmoothedSpeed` | float | Drive locomotion from this. | | **Get World Velocity** | `GetWorldVelocity` | vector | | | **Get Local Velocity** | `GetLocalVelocity` | vector | The same velocity in the near tier actor's own space, for a strafing blend space. | | **Get Gait Id** | `GetGaitId` | integer | One-based row in the anim set's gaits. 0 means no gait. | | **Get Gait Name** | `GetGaitName` | name | The authored gait name, `None` when there is no gait. | | **Get Pose Id** | `GetPoseId` | integer | One-based row in the anim set's poses. 0 means no pose. | | **Get Pose Name** | `GetPoseName` | name | The authored pose name, `None` when there is no pose. | | **Has Pose** | `HasPose` | bool | True when the pose id is not 0. | | **Get Action Id** | `GetActionId` | integer | 0 while no action is playing. | | **Get Action Variant** | `GetActionVariant` | integer | | | **Get Action Play Rate** | `GetActionPlayRate` | float | | | **Get Action Serial** | `GetActionSerial` | integer | Goes up on every trigger, including a repeat of the same action. | | **Get Action Elapsed Seconds** | `GetActionElapsedSeconds` | float | How far into the action the entity is. | :::caution[Drive blend spaces from Get Smoothed Speed, never from the length of the velocity.] A remote entity's velocity comes from interpolating between position updates, so it reads the speed of one segment and then exactly zero until the next update arrives. Fed into a blend space, that makes a near tier actor stutter between walking and standing while the crowd behind it moves smoothly. ::: :::note[A gait id or pose id of 0 means no gait or no pose.] The component does not nudge a missing gait to row 1. Treat 0, and **Is Bound to Entity** being false, as "no answer yet". An id is a row position in one anim set. It means nothing across two classes. Branch on **Get Gait Name** and **Get Pose Name** when one animation Blueprint serves more than one character. ::: The one thing the near tier does push into your graph is a one-shot action. It plays the action as a dynamic montage in the slot named by **Action Slot Name**, as described below. ### The near tier component's settings Select the `NearTier` component in the shell, or any Crowdy Mass Near Tier component you add yourself. | Label | Default | What it does | |---|---|---| | **Action Slot Name** | `DefaultSlot` | The Slot node your animation graph exposes for one-shot actions. A graph with no slot of this name plays nothing and says so once. | | **Action Blend in Seconds** | 0.1 | How long an action blends in. An action picked up already running gets no blend. | | **Action Blend Out Seconds** | 0.1 | How long an action blends out. | | **Drive Owner Transform** | on | Moves the near tier actor to the entity's transform every frame. Leave it on. | | **Apply Trace Collision** | on | Lets the entity's trace collision setting decide the actor's collision object type and channel responses, so a row and a near tier actor answer traces the same way. The promoted shell takes that collision only on its character mesh and on components tagged `CrowdyTraceCollision`; a held weapon, a hat or other decoration is left as authored and cannot be hit. Tag a hand-made hit box with `CrowdyTraceCollision` to keep it hittable. | **Drive Owner Transform** is what keeps a near tier actor with its entity. Nothing else moves a near tier actor after it is placed, so turning it off leaves the actor standing where it was born while the entity walks away. Turn it off only for an actor that moves itself from somewhere else and wants only the animation inputs. The component also carries **Get Server Value** and the **On Server Values Changed** event for reading an entity's record. See [Read its server values, from Blueprint](./from-blueprint/read-its-server-values.md). ## What to put in the AnimBP The shell's `Mesh` uses an animation Blueprint of its own, `ABP_LanternPlayer_Shell`. It reads the near tier getters above, not a character. **Get Owning Actor** returns the near tier actor, which is not your character and has no character movement component. A copy of the character's animation Blueprint that casts to the character fails the cast, and the graph stands in its default pose. Create a new animation Blueprint for the same skeleton and build it from the getters. If the class uses one-shot actions, the graph needs exactly one thing: a **Slot** node whose slot name matches **Action Slot Name**. The default is `DefaultSlot`, which is also the engine's own default and what the stock mannequin animation Blueprint already has. For `ABP_LanternPlayer_Shell`, build the graph like this. 1. In **Event Blueprint Initialize Animation**, call **Get Owning Actor**, then **Get Component By Class** with **Crowdy Mass Near Tier**, and keep the result in a variable named `NearTier`. The component belongs to the near tier actor, so it is the same for every player that actor stands for. 2. In **Blueprint Thread Safe Update Animation**, set two variables from `NearTier`: a `Speed` float from **Get Smoothed Speed**, and a `bFallen` bool that is true when **Has Pose** is true and **Get Pose Name** equals `Fallen`. Drive the locomotion blend space from `Speed`. For a strafe axis, use **Get Local Velocity**. 3. Choose the locomotion state from **Get Gait Name**, comparing against `Idle`, `Walk` and `Run`, or ignore gaits and use `Speed` alone. 4. Choose the held pose from **Get Pose Name**, comparing against `Sitting` and `Fallen`. Do not compare ids. 5. Treat **Is Bound to Entity** false, and a gait id of 0, as "no answer yet". Do not read them as the first gait. 6. Route the locomotion output through a **Slot** node named `DefaultSlot`. Actions such as `Wave` and `Cheer` play over whatever the graph shows. 7. `Fallen` is authored **Above Actions**, so it should suppress actions. The near tier component does not apply that rank for you. Add a **Blend Poses by bool** after the Slot node. Plug the Slot node into **False Pose**, a sequence player for the `Fallen` clip into **True Pose**, and drive **Active Value** from `bFallen`. Then plug the blend into **Output Pose**. While the player is down the graph shows the fallen clip and skips the Slot node, so no action plays over it. Set `bFallen` in the update step, as step 2 does, because reading the component inside the AnimGraph itself is not thread safe and the compiler warns about it. ![The AnimGraph of ABP_LanternPlayer_Shell: the Speed variable feeds the Speed input of the BS_Idle_Walk_Run blend space player, which goes through Slot 'DefaultSlot' into the False Pose of a Blend Poses by bool node, an A_LanternFallen sequence player feeds its True Pose, a Fallen bool variable feeds Active Value, and the blend goes to Output Pose](/img/unreal-sdk/mass-shell-animgraph.png) A C++ animation instance does the same: find the component with `FindComponentByClass()` once, then call the getters. ### An action that is already running A player does not wait to be promoted before it waves. When a promotion happens in the middle of an action, the near tier component starts the montage at the position the action has already reached. The near tier actor then shows the same frame the row was showing. An action that joins mid-clip gets no blend, because easing into a mid-swing pose from the graph's default pose would show a pose the character never holds. An action that starts at its beginning keeps its authored blend. ## What you see at the handover When a row becomes a near tier actor, it appears already in the right pose: its speed, gait, pose and any running action are applied before its first drawn frame. An actor taken from the pool starts from a fresh animation graph, so you never see a bind pose or the previous player's pose for a frame. You author nothing for this. The actor is shown as a whole, so a component you deliberately left hidden stays hidden. ## If a class does not promote Promotion needs a class that declares a shell, a shell that loads as an actor and carries a Crowdy Mass Near Tier component, and a free place under **Max Near Tier Actors**. A class with a shell but no assigned **Near Tier Actor Class** is drawn as a row at every distance. For the ordered checklist and the log line each cause prints, see the section "A class never promotes" in [Fix a crowd that does not look right](./troubleshooting.md). ## Demotion and release When an entity moves out of the near band, a class whose configuration the SDK builds from the class releases its near tier actor back to the pool: the actor is hidden, unbound and has its collision off. If the entity comes back, it takes an actor from the pool again, so it may not be the same one. An authored configuration follows its own level settings. A release also stops the montage with no blend and gives back the trace collision the actor was pushing. An entity destroyed while promoted goes straight to the pool the same way, so a hidden actor never sits at the dead entity's last position and blocks traces and camera sweeps through a point where nothing is drawn. ## C++ members Everything above is available from Blueprint. A C++ shell or game system can also use these. | Member | What it does | |---|---| | `UCrowdyMassNearTierComponent::BindToEntity`, `UnbindFromEntity` | Join or leave an entity by hand, for an actor class of your own. Promotion calls them for you. | | `GetBoundEntity`, `GetResolvedAnimSet` | Read the bound entity and the anim set it renders through. | | `ReadServerValueInto`, `OnServerValuesChanged` (`FCrowdyMassNearTierServerValuesChanged`) | Read a server value of the bound entity, and listen for changes. | | `ACrowdyMassNearTierActor::IsReadyForRepresentation` | Virtual. Answer true as soon as there is something to draw. | ## Related - [Set up the animation set](./set-up-the-animation-set.md): the gaits, poses and actions the near tier actor reads by name. - [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md): how a pose or an action reaches the entity. - [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md): what an event body does for a row and for a promoted entity. - [Read its server values, from Blueprint](./from-blueprint/read-its-server-values.md) and [Read its server values, from C++](./from-cpp/read-its-server-values.md): the near tier component's server value nodes. - [Fix a crowd that does not look right](./troubleshooting.md): symptoms first, with the diagnostic commands. # Run your code on a crowd row Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/run-your-code-on-a-crowd-row # Run your code on a crowd row A Crowdy event body is where you write what a reaction looks like: a swing, a flinch, a flare of light. On a machine that holds the entity as a real actor of its class, the body runs on that actor. On a machine that draws it as a crowd row, or as a promoted near tier actor, there is no such actor, so the SDK runs the same body on a stand-in. This page tells you what that stand-in is, what a body can and cannot do on it, and when to register a handler of your own instead. ## When you need this You wrote a Crowdy event on `ALanternPlayer`, such as `WaveTorch`, and the map selects the **Crowdy Mass Rendering Backend**. The player who waves runs the body on a real actor. Everyone else may hold that player as a row. Read this page to know that the body is correct in both places. Declaring and calling events works the same under every backend. That is covered once, in [RPC Events in Blueprint](../runtime/rpc-events-blueprint.md), [RPC Events in C++](../runtime/rpc-events-cpp.md) and [Recipients and Routing](../runtime/recipients-and-routing.md). This page covers only what is different on a row. ## Which events reach a row Only the two multicast recipients ever reach a crowd row. An entity drawn as a promoted near tier actor is routed the same way. | Recipient | Reaches a row? | |---|---| | `SpatialMulticast` (the default) | Yes. | | `Multicast` | Yes. | | `OwningClient` | No. It is delivered to the entity's owner, who holds the entity as a real actor. | | `Host` | No. It is delivered to the elected host as a request addressed to an actor. | An `OwningClient` or `Host` event that arrives for an entity a client holds as a row is dropped, and the log says so at most once a second. The routing rules for all four recipients are on [Recipients and Routing](../runtime/recipients-and-routing.md#the-four-recipients). ## What runs when one arrives When a multicast event arrives for a row, the SDK decides what to do in this order. | What you did | The stand-in runs your body | The action is started on the row | |---|---|---| | Nothing. The event is not marked a one-shot action. | Yes. | Not applicable. | | The event is marked a one-shot action (`CrowdyAction`, or **Is A One-Shot Action** in Blueprint). | Yes. | Yes. | | You registered a handler for the event, or bound **Receive Crowdy Event on Entity** to it. | No. | No. Your handler decides. | The body and the action are different halves of one event. The action is the animation. The body is everything else you do in world space. A marked event with no handler gets both, which is what one body on a real actor does. See [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md) for the action half. A promoted near tier actor changes none of this. The body still runs on the stand-in, and the near tier actor plays the action from the row's data through its animation Blueprint. It runs no body of its own. ## The stand-in The stand-in is an object of the entity's own class that exists only to run bodies. It is built once per class, not once per entity: three character classes mean three stand-ins whether two players or two thousand are on screen. It is never spawned into the level, so nothing about it exists in the world. What your body sees on it: - **Position.** Before each body runs, the stand-in is moved to the entity's transform. `Get Actor Location` and `Get Actor Rotation` return where the row is, so a body that works in world space at its own location works. - **Parameters.** They arrive on the event's pins as usual. - **A world.** Nodes that need one, such as spawning an effect or playing a sound, work. - **Class defaults.** Every variable on the class is reset to its class default before each body runs. A body reads defaults, not the values the owner's actor holds, and anything it writes is discarded before the next event. - **No Blueprint components.** Only spawning runs a construction script, so components you add in the Blueprint do not exist on the stand-in. Native components, such as a C++ mesh, exist but are never registered. Because of that, a body that depends on its own components or on its own earlier state cannot do its work there, and this page lists exactly which kinds. ## A body written for both `WaveTorch` is the Oakford event: a `SpatialMulticast` event marked as a one-shot action, taking `ActionId`, `Variant` and `PlayRate`. Its body calls `UCrowdyMassActionLibrary::PlayCrowdyMassAction` on the actor's mesh. The declaration and the body are on [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md). On the owner's machine, and on any machine that holds the player as a real actor, the call plays the clip on the mesh. On a row, the stand-in has no animation graph, so the call plays nothing and says nothing. The row's clip comes from the action half, which the SDK starts for you. One body, one declaration, and each representation plays the same clip. To add a flare at the player's position, add **Play Sound at Location** (or **Spawn System at Location**) after the call and feed **Get Actor Location** into the location pin. In C++ that is `UGameplayStatics::PlaySoundAtLocation` with `GetActorLocation()`. It works on the stand-in because it needs only a world and a point. To see the row path with one client, use the single-client check on [Turn Mass on for a map](./turn-mass-on-for-a-map.md). ## What cannot work on the stand-in Two kinds of work cannot survive on the stand-in. **The Blueprint compiler warns you about both.** **Work that resumes after the body returns.** A **Delay** or any other latent call, a **Timeline**, an async action, or **Set Timer** on self. The stand-in is reset and moved to the next entity as soon as the body returns, so the continuation would resume against a stand-in for somebody else. Do the work in a single pass. **A call on one of the actor's own components.** A crowd row is drawn from data by processors and owns no per-entity components. The call reaches either a native component that was never registered or one the construction script never made. It succeeds and changes nothing. Present in world space at the entity's location instead, with a particle system, a sound or a decal, and drive animation through a one-shot action. :::caution[These are warnings, not errors.] The same body is correct wherever the entity is a real actor: always on its owner, always under the actor pool, and in every other place a real actor exists. Which representation an observer uses is chosen by the map profile, not by anything on your class, so the compiler cannot know whether your class will be drawn as a row. It warns, and you decide. ::: The warnings are scoped to one event's body, followed along execution from the event node. A **Delay** in an ordinary function beside a multicast event is correct and is not reported. An event with no recipient chosen is in scope, because the default recipient is Spatial Multicast. The **Is A One-Shot Action** checkbox, and the warnings on a class with a Crowdy Entity component, appear once a map profile assigned in **Project Settings, Plugins, Crowdy SDK** (a **Map Profiles** entry or the **Default Profile**) selects the **Crowdy Mass Rendering Backend**. A project whose maps all use the actor pool sees neither. :::danger[An event declared on a component, or on the entity avatar object, cannot run on a stand-in.] The stand-in is an object of the entity's own class, so an event declared on a component of that class, or on `UCrowdyMassEntity`, has nothing to run on there. It lands on a machine that holds the entity as a real actor, which is why this is easy to miss, and nothing lands on a row. The log reports it once per declaring class. Declare the event on the entity's own class, or register a handler for it. ::: An abstract class cannot have a stand-in at all. The log reports it once per class, and its events do not run their bodies on rows. ## Register a handler instead If you would rather decide what an event does to a row yourself, register a handler. `RegisterEventHandler` takes the event's receiver function and a function to run, and **Receive Crowdy Event on Entity** is the Blueprint version. See [Receive its events, from Blueprint](./from-blueprint/receive-its-events.md) and [Receive its events, from C++](./from-cpp/receive-its-events.md). `CrowdyMassMakeActionEventHandler` builds a ready-made handler for an action event, and `CrowdyMassWriteAction` is the call that starts an action on a row. :::caution[A registered C++ handler replaces the body and the automatic action write.] The stand-in is a fallback. It runs only when nothing you registered took the call, and a bound **Receive Crowdy Event on Entity** node counts as a registered handler for the body. A game that registered something has already said what the event means for a row, and running your body as well would do the work twice. For a C++ handler on a marked action, a second writer would also start the action twice from one event, so your handler writes the action itself. A bound Blueprint node does not replace the action: the row still plays it. ::: In Oakford, the C++ handler for `WaveTorch` flares the row's torch (`TargetBrightness = 1`) and writes the wave action with `CrowdyMassWriteAction`, action id 1. The Blueprint node does the flare and the row still waves on its own. The same applies to a promoted entity: a C++ handler that does not write the action leaves the near tier actor with nothing to play. A registration belongs to one exact function object and one world. A Blueprint recompiled in the editor makes a new function, so register again. Registering the same function twice adds a second handler, and both run, in registration order. ## The rate limit Inbound events are capped at 30 per second for each target entity, with a burst of 60. The cap is applied before the event's parameters are decoded, because decoding is the expensive half. A cap that is reached drops the event and logs it at a low level. This is CPU protection. It is not anti-cheat. The limit sits well above normal play, so only a flood of events reaches it. Two checks happen before the cap and cost no allowance: whether the event is meant for this kind of entity at all, and, when nothing is registered and the event is not an action, whether a body could run at all. An event aimed at the wrong kind of entity cannot spend the allowance of a right one. ## What does not belong in an event - **Fire-and-forget visuals.** Hit reactions, swings, deaths, flares, sounds. Anything that is fine to miss. - **Anything that decides an outcome.** Every parameter is chosen by the sender, and a body runs with those parameters on every observer. Any peer can aim an event at any entity, so neither the values nor the claim that the owner sent them can be trusted. A body is for deciding what is drawn. - **Persistent truth.** Health, stats, inventory and anything cheat-sensitive belong on the server, in a Server Object (see [Keep a record per entity](../exec/keep-a-record-per-entity.md)), and a row reads them from there. Never carry them in an event parameter, and never in a fragment. See the two planes on [Draw a crowd with Mass](./draw-a-crowd-with-mass.md). ## Measure or switch off the stand-in `crowdy.mass.event.proxy 0` turns the stand-in dispatch off. With it off, an event nothing registered a handler for lands nothing on a row, beyond the action write a marked action gets. Use it to measure what the stand-in costs, or to find a body that misbehaves on observers only. The default is 1. It exists in Editor and Development builds. `crowdy.rpc.trace 1` prints one line per event delivered to the stand-in, naming the entity, the stand-in, where it was moved to and whether the world will run a body. If the body never seems to run, read that line first. [Fix a crowd that does not look right](./troubleshooting.md) lists the diagnostic commands. ## Gotchas - A body that works in a one-player test can fail on observers only, because the owner always holds a real actor. Test with a second player or the single-client check. - A **Delay** or a timer in the body is the usual cause of an effect that fires on the owner and never on a row. - An event with a typo in `CrowdyRecipient` silently takes the default recipient. The Blueprint dropdown cannot get this wrong. C++ can. ## Related - [RPC Events in Blueprint](../runtime/rpc-events-blueprint.md) and [RPC Events in C++](../runtime/rpc-events-cpp.md): declaring and calling events. - [Recipients and Routing](../runtime/recipients-and-routing.md): the four recipients and how far a spatial event travels. - [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md): the action half of a marked event, and the `WaveTorch` declaration and body. - [Promote close entities to actors](./promote-close-entities-to-actors.md): what a close entity draws as. - [Receive its events, from Blueprint](./from-blueprint/receive-its-events.md) and [Receive its events, from C++](./from-cpp/receive-its-events.md): handlers. - [Fix a crowd that does not look right](./troubleshooting.md): symptoms first, with the diagnostic commands. # Add your own crowd data Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/add-your-own-crowd-data # Add your own crowd data A crowd row carries the data the SDK needs to draw it: a transform, a pose, an action. It has no place for a value that belongs to your game. This page adds one. You write a small Mass fragment to hold the value, a trait that puts the fragment on the row, and a processor that acts on it each frame. Then you land a replicated Crowdy State property on the fragment, so the value an actor would read from its own property arrives on the row too. :::note[This page needs C++.] The fragment, the trait, the processor and the field registration are C++. A Blueprint graph can read the result afterwards; see [Read and write its fields, from Blueprint](./from-blueprint/read-and-write-its-fields.md). ::: The page is about crowd data, not Mass in general. For the engine model of fragments, traits and processors, read Epic's [Mass Entity documentation](https://dev.epicgames.com/documentation/en-us/unreal-engine/mass-entity-in-unreal-engine). ## When you need this On a machine that holds a remote player as a real actor, a replicated view value is free. The SDK writes it onto the actor's property and your code reads the property. On a machine that holds the same player as a crowd row there is no actor, so the value has nowhere to go. It is decoded and then dropped, unless you registered a field for it and the row carries a fragment to land it in. Reach for this page when: - A Crowdy State property such as `TorchBrightness` on `ALanternPlayer` should change how the row looks, and the row has no pose, action or transform to carry it. - You want something per row that a processor eases, counts down or draws from, such as a torch that fades toward the replicated brightness instead of snapping. Do not use it for anything the game must trust. Every value that reaches a row this way came off the observed player's own client, so it is a visual and can be a lie. Health, loot and the like belong in a record the server keeps; see [Read its server values, from Blueprint](./from-blueprint/read-its-server-values.md) and [Keep a record per entity](../exec/keep-a-record-per-entity.md#crowd-rows). The Oakford example: on market night every other player in the square is a crowd row of `ALanternPlayer`. Each player's torch has a brightness, replicated as the `float` property `TorchBrightness`. You will put it on `FLanternTorchFragment`, ease it with `ULanternTorchFadeProcessor`, and add the fragment through `ULanternTorchTrait`. ## Write the fragment, the trait and the processor A game module that declares Mass types lists `"MassCore"`, `"MassEntity"` and `"MassSpawner"` next to `"CrowdyMass"` in its `Build.cs`: ```csharp PublicDependencyModuleNames.AddRange(new string[] { "CrowdyMass", "MassCore", "MassEntity", "MassSpawner" }); ``` ### The fragment A fragment is plain data. `TargetBrightness` is the value the replicated property lands on. `Brightness` is the value the row shows now, and the processor eases it toward the target. ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "Mass/EntityElementTypes.h" #include "LanternCrowd.generated.h" // Wider members come first: this fragment is read for every crowd row every frame. USTRUCT() struct FLanternTorchFragment : public FMassFragment { GENERATED_BODY() UPROPERTY() float TargetBrightness = 1.f; UPROPERTY() float Brightness = 1.f; }; static_assert(sizeof(FLanternTorchFragment) == sizeof(float) + sizeof(float), "FLanternTorchFragment has gained padding. Declare wider members before narrower ones."); ``` - **Derive from `FMassFragment`.** It comes from `Mass/EntityElementTypes.h`, which lives in MassCore, not MassEntity. `FTransformFragment` comes from `Mass/EntityFragments.h`. - **Declare wider members before narrower ones** and pin the layout with a `static_assert` written in terms of the members. A processor reads this struct for every row every frame, so padding is a cost on every row. - **Mark a member `UPROPERTY()` if you want to reach it by name.** A plain C++ member of a `USTRUCT` has no reflection entry, so nothing can find it by name. Registering one is refused up front. You need this for the Blueprint field nodes below. - **Never make "is this active" a Mass tag.** Adding or removing a tag moves the row to another archetype, which is the dominant per-row cost. Use a sentinel value in a field instead, the way the SDK's action fragment treats an action id of 0 as no action. ### The trait A trait is what puts the fragment on a row. ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "MassEntityTraitBase.h" #include "LanternCrowd.generated.h" UCLASS(EditInlineNew, CollapseCategories, meta = (DisplayName = "Lantern Torch")) class ULanternTorchTrait : public UMassEntityTraitBase { GENERATED_BODY() public: virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const override; // Copied onto every entity built from this trait. UPROPERTY(EditAnywhere, Category = "Lantern") float StartingBrightness = 1.f; }; // LanternCrowd.cpp #include "LanternCrowd.h" #include "MassEntityTemplateRegistry.h" void ULanternTorchTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const { FLanternTorchFragment& Torch = BuildContext.AddFragment_GetRef(); Torch.TargetBrightness = StartingBrightness; Torch.Brightness = StartingBrightness; } ``` `BuildTemplate` adds the fragment. Two rules to know: - The trait's `.cpp` includes `MassEntityTemplateRegistry.h`, which is where `FMassEntityTemplateBuildContext` is defined (module MassSpawner). The header only needs `MassEntityTraitBase.h`. - **Add what you own, require what another trait owns.** `AddFragment` puts your fragment on the row. `RequireFragment` asks for a fragment another trait adds, and a missing requirement fails template validation with a message naming the trait and the type. Adding a fragment another trait already added only logs at Info. Adding the same tag from several traits is fine. The trait class needs `EditInlineNew` so it shows up in the picker described in the next section, and a `DisplayName` meta if you want a friendlier label than the class name. Properties you put on the trait are authored once and copied onto every row built from it. ### The processor A processor is what makes the fragment do something each frame. This one eases `Brightness` toward `TargetBrightness`. ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "MassEntityQuery.h" #include "MassProcessor.h" #include "LanternCrowd.generated.h" UCLASS() class ULanternTorchFadeProcessor : public UMassProcessor { GENERATED_BODY() public: ULanternTorchFadeProcessor(); protected: virtual void ConfigureQueries(const TSharedRef& EntityManager) override; virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override; private: FMassEntityQuery EntityQuery; }; // LanternCrowd.cpp #include "LanternCrowd.h" #include "MassExecutionContext.h" ULanternTorchFadeProcessor::ULanternTorchFadeProcessor() : EntityQuery(*this) { // A dedicated server has nothing to draw. ExecutionFlags = static_cast(EProcessorExecutionFlags::Client | EProcessorExecutionFlags::Standalone); ProcessingPhase = EMassProcessingPhase::PrePhysics; bRequiresGameThreadExecution = false; } void ULanternTorchFadeProcessor::ConfigureQueries(const TSharedRef& EntityManager) { EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); } void ULanternTorchFadeProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) { EntityQuery.ForEachEntityChunk(Context, [](FMassExecutionContext& Context) { const TArrayView Torches = Context.GetMutableFragmentView(); const float DeltaTimeSeconds = Context.GetDeltaTimeSeconds(); for (FMassExecutionContext::FEntityIterator EntityIt = Context.CreateEntityIterator(); EntityIt; ++EntityIt) { FLanternTorchFragment& Torch = Torches[EntityIt]; Torch.Brightness = FMath::FInterpConstantTo(Torch.Brightness, Torch.TargetBrightness, DeltaTimeSeconds, 2.f); } }); } ``` The processor follows these rules in UE 5.8: - **Override `ConfigureQueries(const TSharedRef& EntityManager)`.** The no-argument overload is `final`, so overriding it does not compile. - **Build the query in the constructor's initializer list**, as `EntityQuery(*this)`. - **Set `ExecutionFlags` to `Client | Standalone`** for a processor that only feeds a visual. A dedicated server has nothing to draw. - **Set `bRequiresGameThreadExecution` to true** for anything that touches a world subsystem or the representation subsystem. This processor touches only its own fragment, so it leaves it false. - **Never change an archetype inline.** Adding or removing a tag or fragment from inside the loop goes through the command buffer: `Context.Defer()`. - **Do per-row work per chunk.** Fetch the fragment view once for the chunk and index it. No allocation and no `Cast` in the per-row loop. Processor ordering and query pruning are engine behavior; see Epic's Mass Entity documentation above. ## Put the trait on the entity You do not have to author a whole Mass entity config to add one trait. The SDK works out a config from the actor class (mesh, transform, trace collision, animation), and the **Crowdy Mass Entity Traits** component (`UCrowdyMassEntityTraitsComponent`) adds your traits to it. It is read off the class, so every row of that class gets them. 1. Open `BP_LanternPlayer`. 2. In **Components**, choose **Add**, then **Crowdy Mass Entity Traits**. 3. In the **Details** panel, under **Crowdy Mass|Traits**, find **Traits** and add an element. 4. Pick your trait class in the element's dropdown, here **Lantern Torch**, and set its properties inline. ![The Crowdy Mass Entity Traits component with its Traits array holding one element, Lantern Torch, expanded inline to show Starting Brightness at 1.0 and Valid Target Config set to All Net Modes](/img/unreal-sdk/mass-traits-component.png) | Label | Default | What it does | |---|---|---| | **Traits** | empty | The traits added to the config worked out for this class, in the order listed. Each row is an inline object with its own settings, not an asset reference. | :::caution[A class with an authored Mass entity config ignores this component.] A class whose config you authored by hand is built from that config alone, and the component is not read. The log says so once per class. If you author the config, add your trait to it directly. ::: How the component combines with the derived config: - **Your traits are added last.** Mass combines a config's traits by class and keeps the last of each class, so a trait of the same class as a derived one replaces it. That is how you change a derived trait as well as extend the config. - **The config holds a copy of your trait**, not the instance on the component. Editing the trait in the editor drops the cached config so the next one picks the change up. - **A second visualization trait of a different class is refused**, with a message. Both would build, and only the first to write the representation parameters keeps them. A visualization trait of the same class as the derived one is allowed and warned about, because it takes the mesh, the level-of-detail table and near tier promotion with it. - **Put every trait on one component.** The first component that lists anything answers and the rest are counted and warned about. A component declared in C++ is read before one added in a Blueprint, whatever the inheritance order. - **An empty list and a row set to none are different mistakes** and get different messages. A class with no component at all says nothing, because that is the ordinary case. ## Land a Crowdy State property on the fragment The fragment is on the row. Now the replicated value has to reach it. ### Mark the property Declare the property on `ALanternPlayer` and mark it `CrowdyState`. Add `CrowdyHeartbeat` so a player who arrives late is sent the current value on the next keyframe instead of waiting for it to change. The keyframe needs the map profile's **State Keyframe Interval (Seconds, 0 = off)** to be non-zero. [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md) covers the heartbeat in full. In Oakford the sit key (`OnSit`) is the only thing that sets `TorchBrightness`: 0.3 while the player sits and 1.0 while they stand. :::caution[A `CrowdyOwnerOnly` property never reaches a crowd.] An owner-only value goes only to the entity's own owning client, and an observer drawing a row is never the owner. Registering a field for it reads nothing. Drop `CrowdyOwnerOnly` if the crowd should see the value, or read it on the actor representation. The log says this once per class and property. ::: ### Register the field Register once per world, from a world subsystem, because the registration lives on the world's `UCrowdyMassEntitySubsystem` and a new world starts empty. `ULanternCrowdSubsystem` does it in `Initialize`, when the world's subsystems start. It asks the collection for the Mass entity subsystem with `Collection.InitializeDependency()` first, so that subsystem exists when the call is made. The entity subsystem exists only in a world whose map profile selects the **Crowdy Mass Rendering Backend**. Gate your own subsystem the same way, in `ShouldCreateSubsystem`: return `Super::ShouldCreateSubsystem(Outer) && GetDefault()->ShouldCreateSubsystem(Outer)`. Your subsystem then exists only on maps that draw crowd rows. On an actor pool map it is never created, and the log stays free of the engine's "Failed to initialize subsystem dependency" line, which a bare `InitializeDependency` would print there. The snippet still checks the result for null, as a guard. ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "LanternCrowd.generated.h" UCLASS() class ULanternCrowdSubsystem : public UWorldSubsystem { GENERATED_BODY() public: virtual bool ShouldCreateSubsystem(UObject* Outer) const override; virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void OnWorldBeginPlay(UWorld& InWorld) override; }; // LanternPlayer.h UPROPERTY(BlueprintReadOnly, Category = "Lantern", meta = (CrowdyState, CrowdyHeartbeat)) float TorchBrightness = 1.f; // LanternCrowd.cpp #include "LanternCrowd.h" #include "Subsystems/CrowdyMassEntitySubsystem.h" #include "Target/CrowdyMassFieldAccess.h" bool ULanternCrowdSubsystem::ShouldCreateSubsystem(UObject* Outer) const { // Only where the Mass subsystem is created: a play world whose map profile selects the Mass backend. return Super::ShouldCreateSubsystem(Outer) && GetDefault()->ShouldCreateSubsystem(Outer); } void ULanternCrowdSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); // The transient world at engine start has no game instance yet. if (!GetWorld()->GetGameInstance()) { return; } UCrowdyMassEntitySubsystem* Mass = Collection.InitializeDependency(); if (!Mass || Mass->IsStateFragmentFieldRegistered(TEXT("TorchBrightness"))) { return; } Mass->RegisterStateFragmentField(TEXT("TorchBrightness"), ECrowdyMassStateFieldType::Float, [](const void* PropertyValuePtr, const FCrowdyMassViewStateContext& Context) { FLanternTorchFragment* Torch = Context.GetFragment(); if (!Torch || !PropertyValuePtr) { return; } Torch->TargetBrightness = *static_cast(PropertyValuePtr); }); CrowdyMassFields::RegisterField(TEXT("TorchBrightness"), TEXT("TargetBrightness")); } ``` The ingestor writes `TargetBrightness`, the value that arrived. `ULanternTorchFadeProcessor` eases `Brightness` toward it every frame. The same `ULanternCrowdSubsystem` also registers the `WaveTorch` handler in `OnWorldBeginPlay`, shown on [Receive its events, from C++](./from-cpp/receive-its-events.md). Declare both functions on the one class. `RegisterStateFragmentField(PropertyName, FieldType, Ingestor)` takes the name of the Crowdy State property, the `ECrowdyMassStateFieldType` it is declared as, and an ingestor: a `FCrowdyMassViewStateIngestor` that runs for each arriving value with a pointer to it and an `FCrowdyMassViewStateContext`. The pointer already has the property's offset applied, so the ingestor never sees the containing struct. The context's `GetFragment` returns the row's fragment of that type, or null if the row does not carry it. - **Check `IsStateFragmentFieldRegistered` first.** A registration cannot be removed. A second one under the same name is accepted, warned about with a line saying the earlier one keeps the field, and never called. - **Hold your owner weakly** if the ingestor captures one. The registration lives on a world subsystem, and a game-instance object can be shut down before the world is cleaned up while its pointer still looks alive. - **Register before values arrive if you can.** Registering later works, because it drops the cached per-class resolution. It does not replay values already received, so the fragment holds what arrives from then on. - **The name must match the property name**, and the type must match the property's declared type. Names are claimed per world, across every class, and each carries one type. ### Types that can land `ECrowdyMassStateFieldType` has six values. An enum is read as the type it is declared over, so `enum class EMyStance : uint8` is a `UInt8` field. | Registered as | A property declared as | Notes | |---|---|---| | `UInt8` | `uint8`, `TEnumAsByte`, or `enum class E : uint8` | One byte exactly. An enum declared over something wider is refused. | | `Int32` | `int32`, or `enum class E : int32` | | | `Int64` | `int64` | | | `Float` | `float` | | | `Double` | `double` | | | `Bool` | a plain `bool` member | A bitfield `bool` is not accepted here. A Blueprint field read by name does accept one. | ### What is refused - **A name match with the wrong type.** The property is skipped and the log names the registered type, what the property is declared as, and the fix. Where the property reads as an accepted type, the line tells you to register for that one. - **A bitfield.** It shares its byte with the flags beside it. Declare it as a plain `bool` member, or read it on the actor representation. - **Anything else**, such as a struct, an `FName`, an `FString`, an `FVector` or an `int16`. Read it on the actor representation, or split it into properties that do land. - **Two classes that declare the same name as different types.** Only one binds. Give the other property a name of its own. ### The pose collision :::caution[Registering a field for your pose property takes the pose away.] A field you register by name is offered every property before the SDK's own pose field, whatever the registration order. If the property is the enum your anim set names as its pose state, such as `Stance`, your field claims it and nothing poses rows of that class. The log reports it. Register your field against a second property, or drop it and let the pose do its job. ::: If a class replicates two properties of the pose enum, only one drives the pose and the other is never read. The log names both. See [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md). ### The rules every ingestor obeys An ingestor runs per arriving value, on the game thread, for a value that came from another player's machine. - **Never change the row's archetype.** No adding or removing a fragment or a tag, no destroying the row, no trace collision changes. The context remembers the fragment pointers for the rest of the update. After an archetype move a later field writes into the archetype the row just left, and after a destroy the next lookup crashes. - **Never keep the fragment pointer past the call.** Mass moves a row's data whenever its archetype changes. - **Only produce a visual.** Every byte is forgeable. A player who lies about `TorchBrightness` decides what their torch looks like on your screen and nothing else. Nothing mechanical may be decided from it. - **Keep it small.** Do not loop over other rows. ### Check it with `crowdy.mass.state.verdict` A property that no registered field claims is decoded and then dropped. The log warns once per class and property, names the call to make and the type to give it. Ignore the warning if the property is only meant for actors. To see the whole picture, run this in a play session, on a client that holds rows: ``` crowdy.mass.state.verdict ``` It prints one block. Paste the whole block when you report a problem. - Every Crowdy State property no fragment field claims, with its class and how many times it was decided. A property you registered a field for leaves the list, so you confirm the fix by its absence. - How many rows are held, how many have a recorded class, and how many fields are registered. - Deltas claimed, applied and delivered, and how many were not claimed. - Each drop reason: class id mismatch, no layout, no buffer, decode failed. - Deltas claimed but refused because the row has no recorded class. This is the one refusal that does not mean something is broken. - The pose writes, and how many carried a real pose rather than the 0 that means no pose. `IsStateFragmentFieldRegistered` answers whether your own registration ran at all, which is otherwise silent. The command is a diagnostic for development builds; [Fix a crowd that does not look right](./troubleshooting.md) says which builds. Landing a value makes it available, not visible. Acting on it takes a processor like the one above, or a Blueprint field read. ## Read a value that rides your position payload `RegisterViewStateField(PropertyName, Ingestor)` is the other way in. It reads a named byte off the wire struct your state executor sends, such as `FLanternPlayerState`, instead of off a Crowdy State property. It takes no type: the field must be a single byte (`uint8`, `TEnumAsByte`, or an `enum class` over `uint8`), and a property of that name that is anything else is skipped with a warning. It matches per struct, `IsViewStateFieldRegistered` is its check, and it obeys every rule above. Use it when the value already rides the position payload and a second property would be a second thing to replicate. Use `RegisterStateFragmentField` for everything else: it is typed and it works per class. The SDK's own pose arrives on the Crowdy State road, so a byte called `PoseId` on your wire struct is ignored unless you register that name yourself. ## Read the field from a Blueprint A graph reads and writes the fragment through the Blueprint field nodes, which find a field by name. Register that name in C++ beside the ingestor with `CrowdyMassFields::RegisterField`, which binds a name to one `UPROPERTY` of one fragment. The snippet above binds `TorchBrightness` to `TargetBrightness`, the member the ingestor writes. A graph that reads `TorchBrightness` gets the target. A graph that writes it sets a new target, and the crowd eases toward that until the next replicated value replaces it. The rules for what can be registered, and the fact that a registered field gives a graph write access, are on [Read and write its fields, from C++](./from-cpp/read-and-write-its-fields.md). [Read and write its fields, from Blueprint](./from-blueprint/read-and-write-its-fields.md) has the nodes and their pins. ## Gotchas - A fragment is local. The same field on the same row holds different values on two machines, because each machine runs its own processors. - A value you land is only as fresh as the last delta. A property that rarely changes needs `CrowdyHeartbeat` so a late joiner is not left on the fragment's default. ## Related - [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md): the entity component, the mesh and the config the traits are added to - [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md): the pose road and the heartbeat - [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md): what an event does to a row - [Read and write its fields, from Blueprint](./from-blueprint/read-and-write-its-fields.md): the Blueprint field nodes - [Read and write its fields, from C++](./from-cpp/read-and-write-its-fields.md): the rules for registering a field - [Read its server values, from Blueprint](./from-blueprint/read-its-server-values.md): values the server owns - [Fix a crowd that does not look right](./troubleshooting.md): symptoms and the diagnostics reference # Fix a crowd that does not look right Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/troubleshooting # Fix a crowd that does not look right Find the symptom, then work down its list in order. Each list is ordered so a step rules out the layer below it, and each step points to the page that explains the setting. The last two sections list the console commands and switches that help, and the order in which to check a new setup. ## The crowd is grey The entities spawn, move and can be hit, but they draw in the engine's default grey material. 1. Flag the character's parent material, not a material instance: tick **Used with Instanced Skinned Mesh** under **Usage**. [Turn Mass on for a map](./turn-mass-on-for-a-map.md) has the steps. 2. Resave the material. A flag the editor sets on the fly and nobody saves is gone on the next launch. 3. Run `crowdy.mass.derive` on the class. When a slot is not flagged, the line says how many checked slots are not flagged for instanced skinned meshes and names the slot, the material instance and the base material to flag. 4. Look in `LogMaterial`. The engine substitutes the default material there, so no Crowdy Mass line mentions it. See [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md) for `crowdy.mass.derive`. ## Nothing appears at all 1. Check that a map profile for this map selects **Crowdy Mass Rendering Backend** as its **Backend Class** and has **Enable Networking** ticked. Check that the profile is assigned under **Project Settings, Plugins, Crowdy SDK, Map Profiles**. See [Assign the profile](../runtime/map-profile.md#assign-the-profile). 2. Check that the Mass entity subsystem is running. It exists in Play In Editor and game worlds whose map profile selects the Mass backend, not in the editor world and not on an actor pool map. See [Crowd nodes do nothing on this map](#crowd-nodes-do-nothing-on-this-map). 3. Run `crowdy.mass.derive` on the class and check that a mesh resolves. 4. Check whether you are the only client. The backend does not draw your own entity as a row, so on one client you see nothing. Tick **Render Local Owner**, which lives under **Mass Backend, Debug** on the **Crowdy Mass Backend Config**, or place a **Crowdy Mass Area Spawner** in the level. See [Turn Mass on for a map](./turn-mass-on-for-a-map.md). 5. Search the log for `nothing configures a Mass entity config`. That line says no config could be worked out from the class, and it names what is missing. 6. Search the log for a warning that an entity named a class id this client cannot resolve to a loaded class. If it appears, add the class to **Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes**. See [Entities and Spawning](../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all). 7. If the rows stand at the world origin instead, name the transform fields of the state struct your executor sends `Transform`, or `Location` (or `Position`) plus `Rotation`. `LogCrowdyMass` names the struct once and lists the accepted spellings. See [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md). ## Crowd nodes do nothing on this map A Crowdy Mass node or call runs without an error, but nothing happens. A **Crowdy Mass Area Spawner** spawns nothing, a trace finds no entity, or a bound event never fires. 1. Open the map's profile and check **Backend Class**. The Mass subsystems are created only when it is **Crowdy Mass Rendering Backend**. On **Actor Pool Rendering Backend** they do not exist, and the nodes and calls that need them do nothing. See [Pick the backend on the map profile](./turn-mass-on-for-a-map.md#pick-the-backend-on-the-map-profile). 2. Check that the profile is the one the map uses: a **Map Profiles** entry for the map's short name, or the **Default Profile**. A map with no entry runs on the profile the SDK ships, which draws actors. See [Assign the profile](../runtime/map-profile.md#assign-the-profile). 3. Look in the log for the one-time line. A **Crowdy Mass Area Spawner** warns once that it spawned nothing, **Set Crowdy Mass Hit Zone Table** warns once and hits keep the default zone, and **Receive Crowdy Event on Entity** and **Receive Server Values Changed on Entity** log once and receive nothing. 4. Remember what still works on an actor pool map. **Play Crowdy Mass Action** and the action id lookup read the actor's class, so they work there. 5. In C++, get the entity subsystem with `World->GetSubsystem()` and check for null before you use it. A world subsystem of your own that needs it should gate itself in `ShouldCreateSubsystem`, so it exists only on maps that draw crowd rows; see [Add your own crowd data](./add-your-own-crowd-data.md). `UCrowdySDKDeveloperSettings::ProfileDrawsCrowdRows(Profile)` answers whether a profile draws crowd rows. ## Rows stand in bind pose The rows are drawn, but they stand in the mesh's bind pose and do not walk, pose or play actions. Work down the list in order. 1. Run `crowdy.mass.derive` on the class in a Play In Editor or game world on a Mass map. Check what it says for the anim set. An answer of nothing means the class names no set. 2. Check that the class has a **Crowdy Mass Anim Set** component whose **Anim Set** is filled in, and that the set names a **Bank**. Without the component the log says the class names no animation set and its entities render in their bind pose. See [Set up the animation set](./set-up-the-animation-set.md). 3. Check the log for a line saying the set did not load. Fix the reference it names; until then the class renders in bind pose. 4. If you authored a Mass entity config for the class, check that its visualization trait's mesh entry (**Skinned Mesh Instance Desc**, **Meshes**) names a **Transform Provider**, the same bank as the set's **Bank**. When the mesh carries no bank the log says so, and nothing can be animated. See [Set up the animation set](./set-up-the-animation-set.md#connect-the-bank-to-the-crowds-mesh). 5. If you authored a config, also add a **Crowdy Mass Animation** trait that names the set. The class's own **Anim Set** component is still found, so pose and action names resolve, but nothing renders the set. The log says `nothing renders that set` and names it. See [Make a character draw as a crowd row](./make-a-character-draw-as-a-crowd-row.md). 6. Check that the **Bank** holds the clips the set indexes; saving the set runs its checks. See [Set up the animation set](./set-up-the-animation-set.md). 7. Set `crowdy.mass.anim.force` to a bank index to force that clip on every row, then set it back to -1. Turn on `crowdy.mass.anim.trace 1` to log every animation index written, per row. ## Rows float, sink or face sideways The rows animate, but they stand above or below the ground by a fixed amount, or face a quarter turn away from where they walk. 1. A row draws its mesh where the class places it. Open the class and check the mesh component's location and rotation: a character's mesh usually sits half the capsule's height below the capsule's centre, turned -90 degrees. A config the SDK builds from the class copies that placement, so fix it on the class. 2. If you authored a Mass entity config, set its mesh entry's **Local Transform** to the same location and rotation. An authored config is not given the class's placement. See [Author a config by hand](./make-a-character-draw-as-a-crowd-row.md#author-a-config-by-hand). 3. For a near tier actor that floats or sinks while the rows look right, see **Entity Transform Offset** on [Promote close entities to actors](./promote-close-entities-to-actors.md#entity-transform-offset). ## Animation lands on the wrong row Rows play each other's clips, or `LogCrowdyMass` warns once a second that a mesh batched more animation entries than instances, so more than one processor is filling its animation batch. The SDK turns the engine's own animation pass off at startup, so something has turned it back on. 1. Open **Project Settings, Mass** and check that the engine's instanced skinned mesh animation pass is not ticked. 2. Open `Config/DefaultMass.ini` if the project has one. If it sets `bAutoRegisterWithProcessingPhases=True` for `MassConsumeInstancedSkinnedMeshAnimationProcessor`, remove that line. 3. If the project uses the MetaHuman crowd plugin, its own pass is left alone and also fills an animation batch. To turn it off, set `bAutoRegisterWithProcessingPhases=False` under `[/Script/MetaHumanCrowd.MetaHumanMassSimpleAnimationProcessor]` in `Config/DefaultMass.ini`. See [The engine's animation pass](./turn-mass-on-for-a-map.md#the-engines-animation-pass). ## The crowd moves but never poses 1. Name a pose state enum on the anim set. **Poses** with rows and no **Pose State Enum** is a validation error. 2. Declare a property of that enum on your class and mark it `CrowdyState`. It must be that enum, not any enum. The type is the contract; the name is yours. 3. Declare the enum as `enum class : uint8`. A wider enum cannot ride the one byte a pose uses. 4. Check that no fragment field is registered under the name of that same property. A field claimed by name takes the property from the pose, and the log says nothing poses entities of that class. Register the field against a second property, or drop it. See [Add your own crowd data](./add-your-own-crowd-data.md). 5. Drop `CrowdyOwnerOnly` from the property. A crowd observer is never the owner, so it never receives the value. 6. Add the property to the heartbeat. An enum is held state, sent when it changes. Without `CrowdyHeartbeat`, one dropped change leaves the pose wrong until something else changes. The heartbeat also needs a non-zero **State Keyframe Interval (Seconds, 0 = off)** on the map profile and a **State Heartbeat** that is not **Off** on the entity component. 7. Run `crowdy.mass.state.verdict` in a play session. Check whether the property is in the dropped list, and whether the pose writes from CrowdyState move at all. 8. Check that the observer resolves the class. An update names its class by id, and a client resolves only the ids of classes that were loaded when the SDK started. Add the class to **Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes**. Until then the log warns that the entity named a class id this client cannot resolve, and the client falls back to guessing from the state struct. The guess works only once a component of that class has begun play on that machine, and it fails for two classes that share one struct. See [Entities and Spawning](../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all). Do this before you suspect the pose path. See [Play poses and one-shot actions](./play-poses-and-one-shot-actions.md) and [Set up the animation set](./set-up-the-animation-set.md). ## An event arrives and nothing animates The body of the event runs, which makes it look as if everything worked, but the row does not play the action. 1. Mark the event an action. In C++ that is the `CrowdyAction` meta key. In Blueprint it is the **Is A One-Shot Action** box on the custom event. Recompile after you tick it, because the marker is read off the compiled function. 2. Run `crowdy.rpc.dumpfn ` and look for `action=1`. Run it on the receiving machine as well as the sender. A Blueprint class name ends in `_C`. 3. Give the event an `int32` input named `ActionId`. Without it nothing starts, and the log says so once per function. The names `ActionId`, `Variant` and `PlayRate` are read by name, and case does not matter; an event that spells them differently needs a registered handler. 4. Check whether something registered a C++ handler for the event with `RegisterEventHandler`. It replaces both the event's body and the automatic action write on a row, so the handler must write the action itself with `CrowdyMassWriteAction`. A bound **Receive Crowdy Event on Entity** node replaces the body only, and the row still plays the action. See [Run your code on a crowd row](./run-your-code-on-a-crowd-row.md). 5. If the **Is A One-Shot Action** box is missing, assign a map profile that selects the Crowdy Mass Rendering Backend under **Project Settings, Plugins, Crowdy SDK**, as a **Map Profiles** entry or the **Default Profile**. The box stays hidden until one does. See [Mark the event](../runtime/rpc-events-blueprint.md#mark-the-event). ## An action does not restart on a repeat The first play works, and sending the same action again while it plays does nothing. 1. Open the anim set and check the action row. **Primary Index** and **Alternate Index** must be two different bank slots that hold the same clip. Equal values, or both still `-1`, are a validation error. 2. Set the set's **Bank**, then press **Sync Actions Into Bank**. It adds the clip to the bank twice and writes both indices. 3. If the class uses an authored config, check that its mesh entry names the same bank as the set's **Bank**. Nothing can catch a different one: validation checks the indices against the set's **Bank**. See [Set up the animation set](./set-up-the-animation-set.md). ## An entity is stuck in a pose An entity keeps its pose after the replicated value changes back. 1. Check whether the pose shares a bank slot with a gait, another pose or an action. Leaving the pose then hands the renderer the index it already has, and nothing changes on screen. 2. Set the set's **Bank**. Validation names the collision and tells you to give the pose a sequence of its own in the bank. 3. If you set the pose with **Set Crowdy Mass Entity Pose**, use a pose name the class's anim set authors. A name it does not author clears the pose instead of setting it. See [Set up the animation set](./set-up-the-animation-set.md). ## A replicated variable does nothing on the crowd `CrowdyState` alone lands nothing on a crowd row, because there is no actor to write onto. The value is decoded and then dropped unless a fragment field claims it. 1. Run `crowdy.mass.state.verdict`. It lists every replicated property that no field claims, and the log message for each names the call to make. 2. Call `IsStateFragmentFieldRegistered` with the property name. If it answers false, your registration never ran. 3. Register the field with the type the property is declared as. A name match with a wrong type is refused, with a line naming both. 4. Check whether the property is a struct, an `FName`, an `FString`, an `FVector`, an `int16` or a bitfield `bool`. None of these can be bound to a fragment field. Read them on the actor representation, or declare a plain `bool`. 5. Drop `CrowdyOwnerOnly` from the property if the crowd should see it. A crowd observer never receives an owner-only value. 6. Check that the name is not already claimed in this world for another type. Names are claimed per world and carry one type. 7. Check that the row carries a fragment to land the value in, and that something reads it. A landed value is stored, not shown. Showing it takes a processor, a near tier AnimBP, or a Blueprint field read. See [Add your own crowd data](./add-your-own-crowd-data.md) and [A field that silently does nothing](../runtime/crowdy-state.md#a-field-that-silently-does-nothing). ## A row gets no server values **Get Server Value** answers false for a crowd row, or **Receive Server Values Changed on Entity** never runs. 1. Check that the entity's actor class carries a **Crowdy Server Object** component whose type holds **A Record per Entity**. The class is read once, from the class default object. 2. Set the component's **Instance Mode** to one the rows can follow: **Only One Instance** (a setting of the type, and it wins over every Instance Mode), **Instance Id** with an id typed, or **This Actor's Area**. Any other mode logs one warning for the class and the rows get nothing. 3. Check that the server holds a record whose id is the entity's id, as 32 hex digits. A row finds its record by that id and by nothing else. 4. With **This Actor's Area**, check that the local player has a pawn or a camera to follow. With none, no area is followed. 5. Check that the variable is visible to the player. **Owner Only** and **Team Only** values reach only a player who may see them, and they arrive later than **Everyone** values. 6. Check that the name is the variable's name in the Server Object asset, and that the type you connect to **Value** matches the variable's type. 7. Ask later if you ask too early. A row drawn the moment a player enters an area reads nothing until the object has been read. See [Read its server values, from Blueprint](./from-blueprint/read-its-server-values.md) and [Keep a record per entity](../exec/keep-a-record-per-entity.md#crowd-rows). ## A class never promotes A class has a near tier shell, and its entities stay crowd rows however close they get. Reasons 1 to 4 are logged once per class, except a class with no shell component at all, which is the ordinary case and says nothing. 1. Declare the shell on the class. It needs a **Crowdy Mass Near Tier Shell** component with **Near Tier Actor Class** filled in. A component with an empty slot is reported differently from no component. 2. Give the shell class a **Crowdy Mass Near Tier** component. A shell without one is refused, because nothing would drive it from the entity. A Blueprint subclass of **Crowdy Mass Near Tier Actor** has one already. 3. Check whether an authored Mass entity config already promotes. If it names a high-resolution actor and its nearest level spawns it, that decision wins, the shell declaration is never read, and you get one warning. 4. Check whether the config carries more than one visualization trait of different classes. That is refused and named. 5. Check that the entity is inside **Near Tier Distance** (off screen) or **Near Tier Visible Distance** (on screen), and that fewer than **Max Near Tier Actors** are already promoted. The nearest entities win the limited places. These settings apply only when the config was derived from the class. This reason is not logged. 6. Turn on `crowdy.mass.neartier.trace 1` to see each promotion, pool checkout and release. A config that names an actor while its nearest level still draws an instance is not a reason. It promotes nothing by itself, so the declared shell replaces the actor it names, the class is promoted after all, at the config's own distances, and the log warns. See [Promote close entities to actors](./promote-close-entities-to-actors.md). ## A near tier actor stands still Turn **Drive Owner Transform** back on if it is off on the shell's **Crowdy Mass Near Tier** component. Nothing else moves a near tier actor. Also check that the shell has no movement component or colliding capsule: the transform is written every frame, and a movement component fights it. See [Promote close entities to actors](./promote-close-entities-to-actors.md). ## A near tier actor plays no actions 1. Check that the shell's Mesh uses its own animation Blueprint, such as `ABP_LanternPlayer_Shell`, that reads the near tier getters. A copy of the character's animation Blueprint casts to the character, the cast fails on the near tier actor, and the graph stays in its default pose. 2. Add a Slot node to the shell's animation graph whose name matches **Action Slot Name** on the near tier component. The default is `DefaultSlot`. Without one, a single warning says the graph has no such slot. 3. Remove any **Crowdy Mass Anim Set** component from the shell that names a different set from the entity's. Actions are refused for that pairing, and the message names both sets. 4. Check the pose that should suppress actions. If it is ranked **Above Actions**, the near tier does not honour that rank, so the graph has to bypass the slot itself while **Has Pose** is true. See [Promote close entities to actors](./promote-close-entities-to-actors.md). ## A Blueprint field read always answers false **Get Crowdy Mass Field** or **Set Crowdy Mass Field** returns false every time. 1. Register the name in C++, then call **Is Crowdy Mass Field Registered** with the **Field Name** once at startup. An unregistered name fails every read and write. The log warns `Nothing is registered as ''` at most once every five seconds per name. 2. Check **Is Crowdy Mass Target Live** first, and do not keep a stored target across a respawn. 3. Match the node's type to the member. **Integer** is exactly `int32`, **Float** takes `float` or `double`, **Byte** takes `uint8` or a byte-wide enum, and **Boolean** takes `bool` including a bitfield. 4. Do not access a field while Mass is processing. That is refused and logged, an Error for a write and a Warning for a read, at most once every 5 seconds per field name. Move the work to a phase-end delegate or a deferred broadcast. See [Read and write its fields, from Blueprint](./from-blueprint/read-and-write-its-fields.md). ## The event dropdown is empty The **Event** dropdown on **Call Crowdy Event on Entity** or **Receive Crowdy Event on Entity** lists nothing. 1. Open the dropdown again. While the asset registry is still scanning, the list is incomplete. 2. Check that the class you want is one the list holds: the classes the Mass rendering backend is configured for, loaded classes that carry a Crowdy Entity component, and the SDK's own events under **Any Crowd Entity**. 3. Turn on `crowdy.mass.nodes.trace 1` in the editor. It logs where each entry came from. See [Call an event on it, from Blueprint](./from-blueprint/call-an-event-on-it.md). ## Commands and switches Every `crowdy.mass.*` command and variable is compiled out of Shipping builds, so none of them exists in a shipped game. The four `CrowdyMass.` commands in the first table are also left out of Shipping builds, so they exist in development builds only, and they print their results as ordinary log lines, not warnings. They help when rows draw but their shadows or animation look wrong, and when you are asked for their output in a bug report. ### Commands | Command | Takes | What it tells you | |---|---|---| | `crowdy.mass.derive` | A class: a short name, an asset path or a full object path | What a Mass entity config built from that class would contain, without spawning anything. It names the mesh, which of the three routes supplied it, the trace collision object type and the material verdict. In a Play In Editor or game world on a Mass map it also names the anim set and whether an authored config would be used instead. It logs a warning when the mesh cannot be resolved, a material is not flagged, or an authored config wins. In the editor world with no play session, the animation answer is left out and the config answer says it is unknown. | | `crowdy.mass.state.verdict` | Nothing; needs a play session | One block saying whether Crowdy State is reaching crowd entities: every replicated property no fragment field claims, how many deltas were claimed and applied, every drop reason, and which road wrote the pose. A property you fix leaves the list. Paste the whole block when you report a problem. | | `CrowdyMass.LogSkinnedMeshShadowDiagnostics` | Nothing | Logs the shadow and render flags of the crowd's instanced skinned mesh components in the current world. | | `CrowdyMass.RecreateSkinnedMeshRenderState` | Nothing | Marks the render state of those components dirty, so they rebuild. | | `CrowdyMass.ForceSkinnedMeshAlwaysAnimate` | Nothing | Sets those components to always animate at the component level and recreates their render state. | | `CrowdyMass.LogRepresentationAnimationDiagnostics` | Nothing | Logs the Mass representation, level of detail, velocity and animation state of the crowd's remote entities. | ### Commands from the rest of the SDK that this page uses These belong to the SDK as a whole and are listed on [Console Variables](../reference/console-cvars.md). | Command or variable | Default | What it does here | |---|---|---| | `crowdy.rpc.dumpfn ` | none | Prints how this process routes one event, including `action=` for a function marked an action. | | `crowdy.state.heartbeat.advisories` | none | Lists every Crowdy State enum property that is outside the keyframe heartbeat. | | `crowdy.rpc.loopback` | 0 | Delivers your own sends back to you, so one Play In Editor client can exercise a receive path. | ### Switches | Variable | Default | What it does | |---|---|---| | `crowdy.mass.neartier.trace` | 0 | Logs when a crowd entity gains a near tier actor, whether the actor was built or taken from the pool, and when it goes back. Also prints where the near tier actor placed its mesh. | | `crowdy.mass.event.proxy` | 1 | Runs an inbound event's own body on a per-class stand-in for an entity held as a row. Set 0 to compare: an event nothing registered a handler for then lands only the action write a declared action gets. | | `crowdy.mass.derive.force` | 0 | Builds every remote entity from a Mass entity config made out of its own class. It pushes aside per-class bindings, the backend override, the project **Remote Entity Config**, and the anim set an authored config's animation trait names; the class's own **Crowdy Mass Anim Set** component is used instead. Entities already spawned keep their config. | | `crowdy.mass.anim.force` | -1 | Forces one animation bank index on every Mass entity. A negative value turns forcing off. | | `crowdy.mass.anim.trace` | 0 | Logs every animation sequence index written, per entity. | | `crowdy.mass.anim.dump` | 0 | While 1, logs once a second a summary of the anim sets in use and of the instance batches handed to each instanced skinned mesh component. | | `crowdy.mass.interp.adaptive` | -1 | Replays each remote segment over the observed arrival interval. 1 on, 0 off for a fixed duration, -1 follows **Adaptive Smoothing** in the project settings. | | `crowdy.mass.interp.overshoot` | -1.0 | How much longer than the observed arrival interval a segment is replayed over. Negative follows **Smoothing Interval Overshoot**. | | `crowdy.mass.interp.min` | -1.0 | Shortest span, in seconds, a segment may be replayed over. Negative follows **Min Smoothing Duration**. | | `crowdy.mass.interp.max` | -1.0 | Longest span, in seconds, a segment may be replayed over. Negative follows **Max Smoothing Duration**. | | `crowdy.mass.nodes.trace` | 0 | Editor only. Logs where the entries of the **Call Crowdy Event on Entity** dropdown came from. | ## Check a new setup in this order Each step rules out the layer below it. 1. Run `crowdy.mass.derive` on your class. Check that it resolves the mesh you expect and reports every material flagged. 2. On one client with **Render Local Owner** on, check that your own pawn draws as a row, stands on the ground, moves, and takes its pose when you change the pose property. 3. On two clients, check that the other player's row is visible, moving and posing on the other machine. 4. On two clients, send an event to an owning client and check that it arrives there. 5. On two clients, send a multicast event and check that it runs your body on the observer's stand-in and that the declared action animates the row. 7. On two clients, call **Get Server Value** on a row and check that it answers with the value the server holds. ## Related - [Draw a crowd with Mass](./draw-a-crowd-with-mass.md): what the backend does and when to use it - [Turn Mass on for a map](./turn-mass-on-for-a-map.md): the profile, the settings and the material flag - [Market night in Oakford](./examples/market-night-in-oakford.md): a worked example that exercises each step above - [Console Variables](../reference/console-cvars.md): the SDK's own `crowdy.*` switches # Target an entity Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-blueprint/target-an-entity # Target an entity This page has a C++ twin: [Target an entity, from C++](../from-cpp/target-an-entity.md). On a Mass map you draw every other player as a crowd row, so there is no actor to drag a reference to. A **Crowdy Mass Entity Target** (the struct `FCrowdyMassEntityTarget`) names one of those players instead: which entity, and which world it lives in. You get one by tracing at the crowd, by overlapping it, or from an event, and every node on the pages after this one takes it. This page shows how to get a target and how to check it is still good. ## When you need a target - To play an animation on a player you are looking at: [Send Crowdy Action](./call-an-event-on-it.md#send-a-one-shot-action-by-name). - To send a Crowdy event to them: [Call Crowdy Event on Entity](./call-an-event-on-it.md#call-a-crowdy-event-on-an-entity). - To read or write a field of their row: [Read and write its fields](./read-and-write-its-fields.md). - To read a value the server keeps about them, such as a raider's health: [Read its server values](./read-its-server-values.md). ## The three ways to get one A target has two read-only fields, so a graph cannot build one by hand. There are three ways to get one: 1. The **Target** pin inside **Out Hit** from a Crowdy Mass trace node. 2. The **Target** pin inside each element of **Out Overlaps** from a Crowdy Mass overlap node. 3. The **Target** output pin of a **Receive Crowdy Event on Entity** node, or of a **Receive Server Values Changed on Entity** node. A target dies with the entity. If the player leaves and comes back, a target you stored before they left does not name the new entity. Check it with **Is Crowdy Mass Target Live** before you use it, as [below](#check-it-before-you-use-it). ## Trace for it A Crowdy Mass trace works like the engine's trace nodes, and it also tests the crowd. It reports a hit on a crowd row and a hit on a near tier actor the same way. You do not branch on how the player is drawn. The nodes are in the category **Crowdy Mass|Trace**. To let a player look at another and mark them: 1. In `BP_LanternPlayer`, add a variable named `TracedTarget` of type **Crowdy Mass Entity Target**. The Mass pages that follow read this variable. 2. In the event graph, add the **I** key event and drag off **Pressed**. 3. Add **Crowdy Mass Line Trace Single** and connect **Pressed** to it. 4. For **Start**, add **Get Player Camera Manager** (leave **Player Index** on 0), then drag off it and add **Get Camera Location**. 5. Drag off the same camera manager and add **Get Camera Rotation**, then drag off that and add **Get Forward Vector**. 6. Multiply the forward vector by 3000, add **Start** to the result, and connect the sum to **End**. 7. Set **Trace Channel** to **Visibility**, and leave **Trace Complex** off. Leave **Actors to Ignore** unwired. Set **Draw Debug Type** to **For Duration** while you test. 8. Drag off **Out Hit** and add the break node for the struct, so the fields show as pins. 9. Add a **Branch** after the trace and connect **Hit Mass Entity** to its **Condition**. This is the flag to branch on: it is true for a crowd row and for a near tier actor alike. 10. From **True**, add **Set TracedTarget** and connect the **Target** pin of the broken struct to it. [Blueprint snippet: mass-trace-target, see /bp/unreal-sdk/mass-trace-target.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. ### The shapes and modes There are 24 trace nodes. A name is built from three choices, in this order: **Crowdy Mass** + shape + **Trace** + **Single** or **Multi** + how the trace picks what it hits. | How it picks | The end of the name | The pin that picks | |---|---|---| | By channel | nothing | **Trace Channel** | | By object type | **For Objects** | **Object Types** | | By collision profile | **By Profile** | **Profile Name** | | Shape | Its own pins | |---|---| | **Line** | none beyond **Start** and **End** | | **Sphere** | **Radius** | | **Box** | **Half Size**, **Orientation** | | **Capsule** | **Radius**, **Half Height** | So **Crowdy Mass Sphere Trace Multi For Objects** and **Crowdy Mass Capsule Trace Single By Profile** are both real nodes, and **Crowdy Mass Line Trace Single** is the plain form. These are all 24, by their exact names in the palette: | Node | By channel | For objects | By profile | |---|---|---|---| | Line, first hit | **Crowdy Mass Line Trace Single** | **Crowdy Mass Line Trace Single For Objects** | **Crowdy Mass Line Trace Single By Profile** | | Line, every hit | **Crowdy Mass Line Trace Multi** | **Crowdy Mass Line Trace Multi For Objects** | **Crowdy Mass Line Trace Multi By Profile** | | Sphere, first hit | **Crowdy Mass Sphere Trace Single** | **Crowdy Mass Sphere Trace Single For Objects** | **Crowdy Mass Sphere Trace Single By Profile** | | Sphere, every hit | **Crowdy Mass Sphere Trace Multi** | **Crowdy Mass Sphere Trace Multi For Objects** | **Crowdy Mass Sphere Trace Multi By Profile** | | Box, first hit | **Crowdy Mass Box Trace Single** | **Crowdy Mass Box Trace Single For Objects** | **Crowdy Mass Box Trace Single By Profile** | | Box, every hit | **Crowdy Mass Box Trace Multi** | **Crowdy Mass Box Trace Multi For Objects** | **Crowdy Mass Box Trace Multi By Profile** | | Capsule, first hit | **Crowdy Mass Capsule Trace Single** | **Crowdy Mass Capsule Trace Single For Objects** | **Crowdy Mass Capsule Trace Single By Profile** | | Capsule, every hit | **Crowdy Mass Capsule Trace Multi** | **Crowdy Mass Capsule Trace Multi For Objects** | **Crowdy Mass Capsule Trace Multi By Profile** | - **Single** returns the first blocking hit in **Out Hit**. **Multi** returns every hit along the trace in **Out Hits**, an array. - Every trace node returns a bool. - The pins after the shape pins are the same on every node: **Trace Complex**, **Actors to Ignore**, **Draw Debug Type**, then the output. - Under **Advanced**: **Ignore Self** (on), **Trace Color**, **Trace Hit Color**, **Draw Time** (5 seconds), **Draw Mass Candidate Shapes** and **Draw Spatial Grid Cells**. The last two draw the crowd's broad-phase shapes and grid cells for debugging. ## Overlap for it An overlap node finds the entities inside a volume. The nodes are in the category **Crowdy Mass|Overlap**, and each returns a bool and fills **Out Overlaps** with an array of `FCrowdyMassOverlapResult`. There are ten, listed here by their exact names: | Shape | Actors | Components | |---|---|---| | Sphere | **Crowdy Mass Sphere Overlap Actors** | **Crowdy Mass Sphere Overlap Components** | | Box | **Crowdy Mass Box Overlap Actors**, **Crowdy Mass Box Overlap Actors With Orientation** | **Crowdy Mass Box Overlap Components**, **Crowdy Mass Box Overlap Components With Orientation** | | Capsule | **Crowdy Mass Capsule Overlap Actors**, **Crowdy Mass Capsule Overlap Actors With Orientation** | **Crowdy Mass Capsule Overlap Components**, **Crowdy Mass Capsule Overlap Components With Orientation** | The shape pins are **Sphere Pos** and **Sphere Radius**, **Box Pos** and **Box Extent**, or **Capsule Pos**, **Radius** and **Half Height**. The **With Orientation** forms add **Orientation**. All of them take **Object Types**, **Actors to Ignore**, and either **Actor Class Filter** (the Actors forms) or **Component Class Filter** (the Components forms). Loop over **Out Overlaps** with a **For Each Loop**. Branch on **Overlap Mass Entity**, then use **Target**. Each overlapping entity is reported once, whichever way it is drawn. :::caution[A class filter applies to actors only, never to crowd entities.] Every crowd entity inside the volume that passes **Object Types** is reported whatever **Actor Class Filter** or **Component Class Filter** says, including a close player whose near tier actor the filter rejected. Branch on **Overlap Mass Entity** rather than relying on the filter to exclude the crowd. ::: ## Read the hit **Out Hit** is a `FCrowdyMassTraceHit`. | Field | What it holds | |---|---| | **Blocking Hit** | The trace hit something that blocks. | | **Hit Mass Entity** | The hit landed on a Crowdy Mass entity, drawn as a crowd row or as a near tier actor. Branch on this. | | **Hit Crowdy Entity** | The hit landed on a Crowdy entity. | | **Hit Result**, **Actor**, **Component** | The engine's hit result and the actor and component it hit. | | **UUID** | Who was hit. It is read from the entity, never from the near tier actor drawing it. | | **Entity** | The Mass entity handle. | | **Target** | The same hit as a send target. It is set only when **Hit Mass Entity** is true. | | **Bone Name** | The bone that was hit. | | **Hit Zone Name** | The zone the bone belongs to in the active hit zone table, or the table's default zone. | | **Damage Multiplier** | The hit zone's multiplier. For a flourish only; see below. | | **Instance Index** | Which crowd row was hit, or -1 for a hit on a near tier actor. | An `FCrowdyMassOverlapResult` has **Overlap Mass Entity**, **Overlap Crowdy Entity**, **Actor**, **Component**, **UUID**, **Entity**, **Target** and **Instance Index**, with the same meanings, except that **Instance Index** is always -1 on an overlap. **Get Entity** (pure, in **Crowdy Mass|Trace**) takes a hit and returns the Crowdy Mass entity object for it, or nothing if the hit was not a Crowdy Mass entity. Getting **Target** does not create that object and **Get Entity** does, so do not call **Get Entity** on every hit, and do not keep what it returns across frames. The entity object has two pure nodes in **Crowdy Mass**, **Get Entity UUID** and **Is Entity Valid**. **Is Entity Valid** is true while the entity the object stands for still exists, and false once it is gone or its world has gone away. :::caution[Never work out damage from Damage Multiplier.] The multiplier comes from a table on the machine that ran the trace. The server never reads it. Use it for a hit marker or a damage number that the server's own result will replace. ::: ## Check it before you use it **Is Crowdy Mass Target Live** (pure, in **Crowdy Mass|Target**) takes a target and returns true while it still names a live entity that is ready to be read or written. It asks the entity manager, so it is stricter than checking that the struct is filled in. Run it on a stored target each time you use it, and do nothing when it is false. ## Hit zones Assign a hit zone table once, and every Crowdy Mass trace fills **Hit Zone Name** and **Damage Multiplier** from it. 1. Create a **Crowdy Mass Hit Zone Table** data asset (`UCrowdyMassHitZoneTable`). Each row of **Entries** has a **Bone Name**, a **Zone Name** and a **Damage Multiplier**. **Default Zone Name** (`Default`) and **Default Damage Multiplier** (1.0) answer for a bone with no row. 2. At startup, call **Set Crowdy Mass Hit Zone Table** with the asset. Call it once for the world, not on every pawn's Begin Play. A table lasts for that world only. **Set Crowdy Mass Entity Trace Collision Enabled** switches one entity in or out of traces at run time. It takes the **Entity** handle from a hit and a bool, and returns false for an entity that was never given a trace collision. ## Gotchas - **Nothing is traceable without the trace collision trait.** An entity with no trace collision never blocks a trace and never passes a filter. A config the SDK derives for you has it. A config you author yourself must include the **Crowdy Mass Trace Collision** trait, or the whole crowd stays untraceable and looks like a trace that is aimed wrong. The log names the fix once. - **A very long or malformed trace is slower, not wrong.** A trace whose sweep radius or end points are not finite or are out of range, or a straight trace longer than the grid's **Spatial Grid Max Ray Cells** (see [Turn Mass on for a map](../turn-mass-on-for-a-map.md)), is no longer answered partly from the grid. It tests every tracked entity, which costs more but finds far entities the grid used to miss. - **Targets exist in a play session only.** The Mass subsystem is created in Play In Editor and in a game, not in the editor world, and only when the map profile selects the **Crowdy Mass Rendering Backend**. On an actor pool map the trace and target nodes find nothing. ## Related - [Call an event on it](./call-an-event-on-it.md): send an action or an event to a target - [Read and write its fields](./read-and-write-its-fields.md): fields of a row - [Read its server values](./read-its-server-values.md): values the server keeps per entity - [Target an entity, from C++](../from-cpp/target-an-entity.md): the same calls in code - [Make a character draw as a crowd row](../make-a-character-draw-as-a-crowd-row.md): what gives a class a crowd row and trace collision # Call an event on it Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-blueprint/call-an-event-on-it # Call an event on it This page has a C++ twin: [Call an event on it, from C++](../from-cpp/call-an-event-on-it.md). Once you have a [target](./target-an-entity.md), two nodes act on the player it names. **Send Crowdy Action** plays a one-shot animation by name, with nothing declared. **Call Crowdy Event on Entity** sends any Crowdy event of your own, so your own code runs on the machines that receive it. Both work whether the player is drawn as a crowd row or as a near tier actor. ## Choose between the two nodes | | Send Crowdy Action | Call Crowdy Event on Entity | |---|---|---| | Use it for | "Everyone sees this player cheer" | An event of your own that also runs your code | | What you declare | Nothing | A Crowdy event on the entity's actor class | | What you pass | An action name from the class's anim set | The event's own parameters | | Who it reaches | Everyone drawing the entity as a row or a near tier actor | The recipients the event's own setting names | ## Send a one-shot action by name **Send Crowdy Action** (category **Crowdy Mass|Anim**) takes a **Target** and an **Action Name** from the class's anim set, and everyone drawing that entity as a crowd row or as a near tier actor sees the action, including you. The pins, the reasons it returns false and the Oakford cheer are on [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md). ## Call a Crowdy event on an entity Use this for an event you declared on the entity's actor class, in Blueprint with **Crowdy Replicates** ticked on a custom event, or in C++ with `CROWDY_EVENT`. See [RPC Events in Blueprint](../../runtime/rpc-events-blueprint.md). In the Oakford square, `ALanternPlayer` declares `WaveTorch` (`ActionId`, `Variant`, `PlayRate`), a `SpatialMulticast` event marked an action. `BP_LanternPlayer` keeps a `TracedTarget` from [Target an entity](./target-an-entity.md#trace-for-it). To make the traced player wave their torch: 1. Add the **V** key event. From **Pressed**, check `TracedTarget` with **Is Crowdy Mass Target Live** and a **Branch**, then on **True** add **Call Crowdy Event on Entity** (category **Crowdy Mass|Events**). 2. In the **Event** dropdown on the node, pick `WaveTorch`. The node is now titled **Call WaveTorch on Entity**, with the class name under it. 3. Wire `TracedTarget` into **Target**. 4. Set the pins the node grew, one per parameter of the event: **Action Id** to 1 (the first action of the anim set, `Wave`), **Variant** to 0 and **Play Rate** to 1.0. [Blueprint snippet: mass-call-event, see /bp/unreal-sdk/mass-call-event.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. The **Event** dropdown lists: - the Crowdy events of the entity classes your map profile's Crowdy Mass backend binds; - the Crowdy events of loaded classes that carry a Crowdy Entity component; - the SDK's own events, under **Any Crowd Entity**, which can be sent to an entity of any class. The events are grouped by class, and the grouping shows whenever more than one group has events, which on this node includes **Any Crowd Entity**. Picking an event picks the class that declares it. Defaults on the pins come from the event's own defaults. A parameter that is an array, set or map must be wired, because it cannot hold a typed-in value. The event goes out as if it had been called on that player's actor, so its recipient setting decides who runs it. A `SpatialMulticast` event reaches everyone near the entity. See [Recipients and Routing](../../runtime/recipients-and-routing.md). On a machine that draws the player as a crowd row or as a near tier actor, the event's body runs on the per-class stand-in, and a near tier actor only draws the entity and plays its actions; see [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md). :::caution[Nothing tells the graph whether the event arrived.] The node has no output for the result. A target that cannot be resolved, a class mismatch and a refused call all fail quietly, with a line in the log. A client that cannot resolve the entity's class yet also fails this way: add the class to **Preloaded Entity Classes** under **Project Settings, Plugins, Crowdy SDK, Replication**, as [Entities and Spawning](../../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all) describes. Check **Is Crowdy Mass Target Live** first. ::: ### When the node will not compile The node reports an error, on the node, when: - no event is picked in the **Event** dropdown; - the class that declared the event, or the event itself, does not exist: pick it again; - the picked function is not a Crowdy event you can send, for example a handler that takes a payload; - **Target** is not wired; - a container parameter is not wired. If you reshape the event later, for example by adding a parameter, the call logs an error telling you to recompile the Blueprint that calls it. Recompile it. ## Gotchas - **An empty dropdown can mean "not yet".** While the asset registry is still scanning, a class bound only by a map profile can be missing. Wait for the scan, or turn on `crowdy.mass.nodes.trace 1` to see where the entries came from. - **The event must be on the entity's own class.** An event declared on a component cannot run on a crowd row; see [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md). - **Anyone can call it.** Any client can send an event at any entity. Treat what arrives as a request to draw something, and keep anything that must be true on the server. ## Related - [Target an entity](./target-an-entity.md): get the target these nodes take - [Receive its events](./receive-its-events.md): react to an event on the machines that draw the entity - [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md): author the actions Send Crowdy Action plays - [Set up the animation set](../set-up-the-animation-set.md): where the action names come from - [Call an event on it, from C++](../from-cpp/call-an-event-on-it.md): the same calls in code # Receive its events Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-blueprint/receive-its-events # Receive its events This page has a C++ twin: [Receive its events, from C++](../from-cpp/receive-its-events.md). When a Crowdy event arrives for a player you draw as a crowd row or a near tier actor, the SDK already runs the event's own body for you on the per-class stand-in; see [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md). **Receive Crowdy Event on Entity** lets a graph of your own take the event instead, with the event's parameters as pins and the entity as a **Crowdy Mass Entity Target**. It is the receiving half of [Call Crowdy Event on Entity](./call-an-event-on-it.md#call-a-crowdy-event-on-an-entity). ## When you need this - To draw something from one place, for every player, when an event arrives: a counter, a toast, a flare on the row. - To handle an event in a graph that is not on the entity's own class, such as a manager or the game state. If the event's own body already does what you want, you do not need this node. Binding it changes what runs; see [what a bound graph replaces](#what-a-bound-graph-replaces). ## Bind a graph to an event In the Oakford square, the market wants a lantern flare on the row of any player who waves. `ALanternPlayer` declares `WaveTorch`, and `ULanternCrowdSubsystem` registered a `TorchBrightness` field on the crowd row (see [Read and write its fields](./read-and-write-its-fields.md)). 1. Open an **event graph**. The node cannot go in a function or macro graph, and pasting it there is refused. 2. Add **Receive Crowdy Event on Entity** (category **Crowdy Mass|Events**). 3. In the **Event** dropdown on the node, pick `WaveTorch`. The node is now titled **Receive WaveTorch on Entity**, with the class name under it, and it grows an output pin for each parameter of the event. 4. Wire **Event BeginPlay** into **Bind**. Do this from one object per machine, such as the game state or a manager actor. Each bound object runs for every event, so binding from every pawn runs your graph once per pawn. 5. From **On Event**, add **Is Crowdy Mass Target Live** on the node's **Target**, then a **Branch**. 6. On **True**, add **Set Crowdy Mass Field (Float)**. Set **Target** to the node's **Target**, **Field Name** to `TorchBrightness` and **Value** to 1.0. [Blueprint snippet: mass-receive-event, see /bp/unreal-sdk/mass-receive-event.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. The write sets a new target brightness, and the crowd eases the torch toward it. The next `TorchBrightness` that arrives from the player replaces that target, and the torch eases back to what the player is really showing. If nothing is wired to **Bind**, the compiler warns that the node is never reached and listens for nothing. :::note[The row still waves.] The graph above flares the torch, and the row still plays the wave, because `WaveTorch` is marked an action and a bound graph keeps that action. See [what a bound graph replaces](#what-a-bound-graph-replaces). ::: **Unbind** is optional. The binding also ends when the object that bound it, or its world, goes away. Binding the same object twice is the same binding, not a second one. ## What the pins give you | Pin | Direction | What it is | |---|---|---| | **Bind** | in, exec | Start listening for this event on crowd entities. | | **Unbind** | in, exec | Stop listening. | | **Then** | out, exec | Continues as soon as the bind or unbind is done, so several of these chain. | | **On Event** | out, exec | Runs once for every arriving event, on the game thread. | | **Event** | in, dropdown | The Crowdy event to listen for. It is picked on the node and cannot be wired. | | **Target** | out | The entity the event arrived for. It is valid for this run only and does not survive that entity being respawned. | | One pin per parameter | out | The event's parameters, matched by name and type. | :::caution[Everything On Event carries came from the player being watched.] The parameters were chosen by the client you are observing and can be anything at all. Use them to decide what is drawn, never what is true. Keep health, loot and the like in a Server Object (see [Keep a record per entity](../../exec/keep-a-record-per-entity.md)) and read it with [Get Server Value](./read-its-server-values.md). ::: ## What a bound graph replaces Binding this node counts as registering a handler for the event. While a handler is registered for an event, a crowd row does not run the event's own body. The stand-in body is the fallback for an event nothing took. The action is not replaced. If the event is marked an action, such as `WaveTorch`, the row still plays it, so you do not need to start the animation yourself. A handler registered in C++ is different: it replaces the action write too, and writes the action itself if it wants one; see [Receive its events, from C++](../from-cpp/receive-its-events.md#what-a-bound-function-replaces). ## Which events you can receive The **Event** dropdown lists the Crowdy events of the entity classes your map profile's Crowdy Mass backend binds and of loaded classes that carry a Crowdy Entity component. It does not list the events the crowd applies itself, such as `PlayCrowdyAction`. Those run their own body on every machine and hand nothing to a graph, and picking one is a compile error. An event that has a parameter named `CrowdyMassTarget`, `Then` or `OutputDelegate` cannot be received, because those names are used by the node's own pins. Rename the parameter. ## Gotchas - **Inbound events are rate limited.** A target can receive 30 events a second, with a burst of 60. See [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md) for what the limit is for. - **Recompile after reshaping an event.** A bound graph is compiled against the event as it was. If you add or change a parameter, the bind is refused with an error that tells you to recompile the binding Blueprint. - **The event still needs a recipient that reaches rows.** Only the two multicast recipients reach a machine that draws the player as a crowd row or a near tier actor. An `OwningClient` or `Host` event lands on one machine, which holds the player as a real actor. See [Recipients and Routing](../../runtime/recipients-and-routing.md). ## Related - [Call an event on it](./call-an-event-on-it.md): the sending half - [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md): what the event's own body does on a crowd row - [Read and write its fields](./read-and-write-its-fields.md): the field nodes the graph above uses - [Receive its events, from C++](../from-cpp/receive-its-events.md): the same binding in code # Read and write its fields Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-blueprint/read-and-write-its-fields # Read and write its fields This page has a C++ twin: [Read and write its fields, from C++](../from-cpp/read-and-write-its-fields.md). A crowd row keeps the values your game draws it with, such as the brightness of a torch, in fragments, which are small records of data that Mass owns. The field nodes read and write one of those values by name, for a **Crowdy Mass Entity Target**, with no C++ in the graph. A field is a visual: it holds what this machine draws, and it can differ from machine to machine. :::note[Registering a field takes C++.] The nodes work only on names your game registered in C++. The fragment, the trait and the registration are on [Add your own crowd data](../add-your-own-crowd-data.md). A Blueprint-only project can still read and write every field a programmer registered. ::: ## When you need this - To show something on the screen about the player you are looking at, from a value the row already holds. - To make a row do something visible from a graph, such as flaring its torch. For a value that has to be true, such as health, use [Read its server values](./read-its-server-values.md). ## Before you start: the name must be registered No field is registered for you. The nodes see only the names your game registered in C++, once, under the name of the `CrowdyState` property or view-state field that lands there. In the Oakford square, `ULanternCrowdSubsystem` registers `TorchBrightness`, a float that reads and writes the torch's target brightness: a read returns the target, and a write sets a new target the crowd eases toward. See [Add your own crowd data](../add-your-own-crowd-data.md) and [Read and write its fields, from C++](../from-cpp/read-and-write-its-fields.md). An unregistered name fails every read and write. The log warns `Nothing is registered as ''` at most once every five seconds for a name, so a typo is easy to miss. Call **Is Crowdy Mass Field Registered** (pure, category **Crowdy Mass|Fields**) with the name once at startup and log a warning if it is false. ## The nodes All of them are in the category **Crowdy Mass|Fields**. Each takes a **Target** and a **Field Name**, and returns a bool. | Node | The field's type | |---|---| | **Get Crowdy Mass Field (Byte)**, **Set Crowdy Mass Field (Byte)** | `uint8`, and an enum member that is one byte wide | | **Get Crowdy Mass Field (Integer)**, **Set Crowdy Mass Field (Integer)** | `int32` exactly | | **Get Crowdy Mass Field (Float)**, **Set Crowdy Mass Field (Float)** | `float` or `double` | | **Get Crowdy Mass Field (Boolean)**, **Set Crowdy Mass Field (Boolean)** | `bool`, including a bitfield | | **Get Crowdy Mass Field (Vector)**, **Set Crowdy Mass Field (Vector)** | `FVector` | | **Is Crowdy Mass Field Registered** | pure; takes a name only | | **Is Crowdy Mass Target Live** | pure; takes a target only | The getters have an **Out Value** pin. The setters have a **Value** pin. Unlike a replicated state field, a field read by name accepts a bitfield `bool`. ## Read a field 1. Get a [target](./target-an-entity.md) and check it with **Is Crowdy Mass Target Live**. 2. Add **Get Crowdy Mass Field (Float)**. Set **Target**, and type `TorchBrightness` into **Field Name**. 3. Use **Out Value** only when the node returns true. ## Write a field 1. Add **Set Crowdy Mass Field (Float)** after the live check. 2. Set **Target**, type `TorchBrightness` into **Field Name**, and set **Value**. 3. The node returns true when the write happened. ## When a read answers false The node returns false for any of these: - The name is not registered. Check it with **Is Crowdy Mass Field Registered**. - The entity is gone, or its target is stale. - The entity has no fragment of the registered type. An entity built from a different config can lack a fragment another one has. - The field is not of the type the node reads. Read an `int32` with the Integer node, not the Float node. - Mass is processing right now. See the caution below. ## What a write means :::caution[A write is a visual, and it is local.] The same field on the same entity holds different values on two machines. Nothing mechanical may be decided from one. Registering a field also gives every graph write access to it, so register only fields that are yours and safe to draw from. Anything a peer must not be able to influence belongs in a Server Object; see [Keep a record per entity](../../exec/keep-a-record-per-entity.md). ::: A write lasts only until the next value arrives. In the Oakford square, `TorchBrightness` lands on the torch's target brightness, so your write sets a new target, and the next `TorchBrightness` that arrives from the player replaces it. :::caution[An access made while Mass is processing is refused.] A graph reached from inside a Mass processing step would race the processors that read the same fragment. So would an ordinary tick that overlaps a Mass phase. The access is refused and logged: an error for a write, a warning for a read. The log line repeats at most once every five seconds for a field name. A refusal from an ordinary tick comes and goes, because Mass phases run alongside the game thread. Doing the work from a phase-end delegate, or from a broadcast deferred to the end of the frame, avoids it entirely; that is C++ work, described on [the C++ page](../from-cpp/read-and-write-its-fields.md#when-a-read-answers-false). ::: ## Set a pose by name The pose of a row has its own node, because it is not a plain field. **Set Crowdy Mass Entity Pose** (category **Crowdy Mass|Anim**) takes a **Target** and a **Pose Name**, and puts the row in the pose of that name from its class's anim set. - A **Pose Name** of `None` clears the pose. - A name the anim set does not author also clears the pose, and says so in the log. - It returns false, and does nothing, if the entity is gone, has no pose fragment, Mass is processing, or you named a pose for an entity whose class this client cannot resolve yet. Add the class to **Preloaded Entity Classes** under **Project Settings, Plugins, Crowdy SDK, Replication**; see [Entities and Spawning](../../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all). - The pose is a visual. An arriving pose replaces one you set. See [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md). ## Gotchas - **A name is claimed across the process.** Two different fragment fields cannot share a name. - **Reads are values, not references.** A node hands back a copy, never a pointer into the row, because Mass moves an entity's data when its archetype changes. ## Related - [Target an entity](./target-an-entity.md): get the target these nodes take - [Add your own crowd data](../add-your-own-crowd-data.md): the fragment, trait and registration behind a field - [Read its server values](./read-its-server-values.md): the values that decide what is true - [Read and write its fields, from C++](../from-cpp/read-and-write-its-fields.md): the same calls and the registration in code # Read its server values Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-blueprint/read-its-server-values # Read its server values This page has a C++ twin: [Read its server values, from C++](../from-cpp/read-its-server-values.md). The values the server owns about a player or a creature, such as health, loot or a team's orders, are what is true. They cannot live on an actor for a crowd row, because a crowd row has no actor. They live in a Server Object that keeps **a record per entity**, and a row reads its own record in place. Nothing is copied into a fragment, and nothing runs per row per frame: the work is one pass over a batch of changed records. The Server Object type and its server code are on [Keep a record per entity](../../exec/keep-a-record-per-entity.md), and this page does not repeat them. It covers what a crowd adds: the setup on the entity's class, and the nodes a row uses. ## When you need this - To show a value on a player or creature you are looking at: a health bar over a raider. - To react when a value changes: a bar that moves, a marker that appears. The pose, the torch and the wave are view state. A modified client can lie about them, and the worst it can do is change what you see. A server value is the truth, and a player may only read what the server chose to send them. ## Set up the class On the Oakford north road, the raiders are `BP_Raider`, keeping a record each in `CSO_Raider` (see [Raiders on the north road](../../exec/examples/raiders-on-the-north-road.md)). On a Mass map the players who watch the road draw each raider as a crowd row, and a row reads the same record. 1. **Make the type.** A Server Object definition with **Holds** set to **A Record per Entity**, whose server code adds a record for each entity, keyed by the entity's id. Each variable is **Server Only**, **Everyone**, **Owner Only** or **Team Only**. `CSO_Raider` has `Health` for **Everyone**, `Loot` for **Owner Only** and `Hideout` for **Team Only**. 2. **Put the component on the class.** The entity's actor class carries a **Crowdy Server Object** component whose **Definition** is that type. It can be native, in the Blueprint, or inherited. The SDK reads it once for the class, from the class default object. `BP_Raider` has one with **Definition** `CSO_Raider`. See [Add the component](../../exec/from-blueprint/get-a-server-object.md#add-the-component). 3. **Pick the object with the component's Instance Mode.** Rows follow only a mode that is the same for the whole class: | Instance Mode | What the rows read | |---|---| | **Only One Instance** (a setting of the type) | The type's one object. It wins over every Instance Mode. | | **Instance Id**, with an id typed | The object with that id. | | **This Actor's Area** | The objects of the areas around the local view. | With **Signed-In Player**, **This Actor**, **Player's Team** or **From Server Value**, the rows get no server values, and the log says why once for the class. 4. **Use the entity's id as the record id.** A row's record is the one whose id is the entity's UUID as 32 hex digits, the form a census of the world reports as `uuid`. With **This Actor's Area**, the game follows the areas within the type's **Areas Followed Around a Player** of the local player's pawn or camera, and moves them as the view moves. A value of 1 means the 3 by 3 areas around the view. It lets go of an area only when the view is more than that radius plus one away, so a border does not flap. With no pawn or camera to follow, no area is followed and the rows read nothing. ## Read a value **Get Server Value** is in the category **Crowdy Mass|Server Values**. Its pins are **Target**, **Name** and **Value**. 1. Get a [target](./target-an-entity.md) of a raider with a trace. You may check it with **Is Crowdy Mass Target Live** first; the graph below leaves that out to stay short. 2. Add the **H** key event, and from **Pressed** add **Get Server Value**. Wire `TracedTarget` into **Target**. 3. Type `Health` into **Name**. It is the variable's name in the asset. 4. Add an Integer variable named `Health` to the Blueprint, then drag it in and choose **Set Health**. Connect **Value** to it. The pin takes the type of whatever you connect. 5. Connect the node's return value to a **Branch**. Use **Value** only when the node returns true. 6. From **True**, add **Set Light Color** with the `Torch` light component of the player as its target. Connect a **Select Color** to **New Light Color**, with red in **A** and white in **B**. 7. Get `Health`, connect it to a `<=` node with 10 in its second input, and connect the result to the **Pick A** pin of **Select Color**. The torch turns red at 10 health or less, and white otherwise. [Blueprint snippet: mass-server-health, see /bp/unreal-sdk/mass-server-health.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. The node returns true only for a value the server sent that fits the type of **Value**. The server never sends a value the player may not receive, such as an **Owner Only** value of an entity they do not own. The node returns false for it. ## React to a change **Receive Server Values Changed on Entity** is the twin of [Receive Crowdy Event on Entity](./receive-its-events.md). It runs part of a graph whenever the server values of a row change. 1. Open an **event graph**. The node cannot go in a function or macro graph. 2. Add **Receive Server Values Changed on Entity** (category **Crowdy Mass|Server Values**). 3. Set **Entity Class** to `BP_Raider`. The node listens to rows of that class and its subclasses. The node is an error without a class, and a warning if the class has no Crowdy Server Object component that reads a record per entity. 4. Wire **Event BeginPlay** into **Bind**. If nothing runs into **Bind** the compiler warns that the node is never reached. 5. From **On Changed**, use **Target** and **Changed**. Call **Get Server Value** on **Target** for each variable you care about. | Pin | Direction | What it is | |---|---|---| | **Bind** | in, exec | Start listening. | | **Unbind** | in, exec | Stop listening. The binding also ends when the object or its world goes away. | | **Entity Class** | in | The actor class whose crowd rows are listened to, with its subclasses. | | **Then** | out, exec | Continues as soon as the bind or unbind is done. | | **On Changed** | out, exec | Runs for each change of a row of that class. | | **Target** | out | The row. | | **Changed** | out | An array of strings: the variables that changed, as **Get Server Value** takes them. Empty when the entity's record was removed. | A record can arrive before its row exists. The record is kept, and the event runs once, with the current values, when the row appears. A row that moves from one followed area into the next does not see its record removed and added again. ## A near tier actor A class that promotes to the near tier (see [Promote close entities to actors](../promote-close-entities-to-actors.md)) reads and hears the same values through its **Crowdy Mass Near Tier** component, `UCrowdyMassNearTierComponent`. Both answer for the entity the near tier actor stands in for, so the actor and its row never disagree. - **Get Server Value** on the component has the pins **Name** and **Value**. It reads the bound entity's variable. - **On Server Values Changed** is an event on the component with a **Changed** array. In `BP_LanternPlayer_Shell`, select the **Crowdy Mass Near Tier** component and click the plus beside the event in the Details panel. ## Gotchas - **Read to draw.** The values are the server's. Change them by calling a Server Function, not from here. - **Owner Only and Team Only values arrive later** than **Everyone** values, because they are fetched, at most once a second for each followed area. - **The first values are not instant.** A row drawn the moment a player enters an area reads nothing until the object has been read. - **A class stays followed for the life of the world** once a row of it has been seen. - **A row gets no values.** Work through the checklist on [Fix a crowd that does not look right](../troubleshooting.md). ## Related - [Keep a record per entity](../../exec/keep-a-record-per-entity.md): the type, the audience of a variable and the server code - [Raiders on the north road](../../exec/examples/raiders-on-the-north-road.md): the record per raider this page reads - [Target an entity](./target-an-entity.md): get the target **Get Server Value** takes - [Promote close entities to actors](../promote-close-entities-to-actors.md): the near tier - [Fix a crowd that does not look right](../troubleshooting.md): the checklist for a row that gets no values - [Read its server values, from C++](../from-cpp/read-its-server-values.md): the same reads in code # Target an entity Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-cpp/target-an-entity # Target an entity This page has a Blueprint twin: [Target an entity, from Blueprint](../from-blueprint/target-an-entity.md). On a Mass map you draw every other player as a crowd row, so there is no actor to hold a pointer to. An `FCrowdyMassEntityTarget` names one of those players instead: which entity (an `FMassEntityHandle`), and which world it lives in. You get one by tracing at the crowd, by overlapping it, or from an event, and every function on the pages after this one takes it. This page shows how to get a target and how to check it is still good. Your module needs `CrowdyMass` in its dependencies. It brings in the Mass modules it uses. ```csharp PublicDependencyModuleNames.Add("CrowdyMass"); ``` | You need | Include | |---|---| | `FCrowdyMassEntityTarget` | `Target/CrowdyMassEntityTarget.h` | | `UCrowdyMassTraceBlueprintLibrary`, `FCrowdyMassTraceHit`, `FCrowdyMassOverlapResult` | `Trace/CrowdyMassTraceBlueprintLibrary.h` | | `UCrowdyMassTargetLibrary` | `Target/CrowdyMassTargetLibrary.h` | ## When you need a target - To play an animation on a player you are looking at: [`SendEntityAction`](./call-an-event-on-it.md#send-a-one-shot-action-by-name). - To send a Crowdy event to them: [`SendEventToMassTarget`](./call-an-event-on-it.md#call-a-crowdy-event-on-an-entity). - To read or write a field of their row: [Read and write its fields](./read-and-write-its-fields.md). - To read a value the server keeps about them, such as a raider's health: [Read its server values](./read-its-server-values.md). ## The three ways to get one 1. `FCrowdyMassTraceHit::Target` from a Crowdy Mass trace. 2. `FCrowdyMassOverlapResult::Target` from a Crowdy Mass overlap. 3. The first parameter of a function you bind with `UCrowdyMassEntitySubsystem::RegisterObjectEventHandler`, and of one you bind for a server values change. See [Receive its events](./receive-its-events.md) and [Read its server values](./read-its-server-values.md). The two members, `Entity` and `World`, are public. Code that already holds a live entity handle, in a Mass processor for example, can fill them in. The usual way is still a trace. A target dies with the entity. Mass bumps a slot's serial when it is reused, so a target you stored before a player left does not name whoever respawns into the same slot. Check it with `IsTargetLive` before you use it, as [below](#check-it-before-you-use-it). ## Trace for it A Crowdy Mass trace works like the engine's trace functions, and it also tests the crowd. It reports a hit on a crowd row and a hit on a near tier actor the same way. You do not branch on how the player is drawn. The functions are static members of `UCrowdyMassTraceBlueprintLibrary`. A handler for an inspect key, with the target kept on the pawn: ```cpp // LanternPlayer.h #include "Target/CrowdyMassEntityTarget.h" FCrowdyMassEntityTarget TracedTarget; // LanternPlayer.cpp #include "LanternPlayer.h" #include "Engine/EngineTypes.h" #include "GameFramework/Controller.h" #include "Trace/CrowdyMassTraceBlueprintLibrary.h" void ALanternPlayer::OnInspect() { const AController* ViewController = GetController(); if (!ViewController) { return; } FVector Eye; FRotator Look; ViewController->GetPlayerViewPoint(Eye, Look); FCrowdyMassTraceHit Hit; UCrowdyMassTraceBlueprintLibrary::CrowdyMassLineTraceSingle(this, Eye, Eye + Look.Vector() * 3000.f, UEngineTypes::ConvertToTraceType(ECC_Visibility), false, TArray(), EDrawDebugTrace::ForDuration, Hit); if (!Hit.bHitMassEntity) { return; } TracedTarget = Hit.Target; } ``` `UEngineTypes::ConvertToTraceType` turns the collision channel into the `ETraceTypeQuery` the function takes. Branch on `bHitMassEntity`: it is true for a crowd row and for a near tier actor alike, and `Target` is set only then. ### The shapes and modes There are 24 trace functions. A name is built from three choices, in this order: `CrowdyMass` + shape + `Trace` + `Single` or `Multi` + how the trace picks what it hits. | How it picks | The end of the name | The parameter that picks | |---|---|---| | By channel | nothing | `ETraceTypeQuery TraceChannel` | | By object type | `ForObjects` | `const TArray>& ObjectTypes` | | By collision profile | `ByProfile` | `FName ProfileName` | | Shape | Its own parameters, after `Start` and `End` | |---|---| | `Line` | none | | `Sphere` | `float Radius` | | `Box` | `FVector HalfSize`, `FRotator Orientation` | | `Capsule` | `float Radius`, `float HalfHeight` | So `CrowdyMassSphereTraceMultiForObjects` and `CrowdyMassCapsuleTraceSingleByProfile` are both real functions, and `CrowdyMassLineTraceSingle` is the plain form. In the Blueprint palette the same functions read **Crowdy Mass Line Trace Single**, **Crowdy Mass Sphere Trace Multi For Objects** and so on. These are all 24: | Function | By channel | For objects | By profile | |---|---|---|---| | Line, first hit | `CrowdyMassLineTraceSingle` | `CrowdyMassLineTraceSingleForObjects` | `CrowdyMassLineTraceSingleByProfile` | | Line, every hit | `CrowdyMassLineTraceMulti` | `CrowdyMassLineTraceMultiForObjects` | `CrowdyMassLineTraceMultiByProfile` | | Sphere, first hit | `CrowdyMassSphereTraceSingle` | `CrowdyMassSphereTraceSingleForObjects` | `CrowdyMassSphereTraceSingleByProfile` | | Sphere, every hit | `CrowdyMassSphereTraceMulti` | `CrowdyMassSphereTraceMultiForObjects` | `CrowdyMassSphereTraceMultiByProfile` | | Box, first hit | `CrowdyMassBoxTraceSingle` | `CrowdyMassBoxTraceSingleForObjects` | `CrowdyMassBoxTraceSingleByProfile` | | Box, every hit | `CrowdyMassBoxTraceMulti` | `CrowdyMassBoxTraceMultiForObjects` | `CrowdyMassBoxTraceMultiByProfile` | | Capsule, first hit | `CrowdyMassCapsuleTraceSingle` | `CrowdyMassCapsuleTraceSingleForObjects` | `CrowdyMassCapsuleTraceSingleByProfile` | | Capsule, every hit | `CrowdyMassCapsuleTraceMulti` | `CrowdyMassCapsuleTraceMultiForObjects` | `CrowdyMassCapsuleTraceMultiByProfile` | - Every function takes a world context object first, then `Start`, `End`, the shape parameters and the picker from the tables, then `bTraceComplex`, `const TArray& ActorsToIgnore` and an `EDrawDebugTrace::Type`, then the output, then the defaulted debug parameters. - `Single` fills one `FCrowdyMassTraceHit& OutHit`. `Multi` fills a `TArray& OutHits`. Every function returns a bool. - The defaulted parameters are `bIgnoreSelf` (true), `TraceColor`, `TraceHitColor`, `DrawTime` (5 seconds), `bDrawMassCandidateShapes` and `bDrawSpatialGridCells`. The last two draw the crowd's broad-phase shapes and grid cells for debugging. ## Overlap for it An overlap function finds the entities inside a volume. Each returns a bool and fills `TArray& OutOverlaps`. | Shape | Actors | Components | |---|---|---| | Sphere | `CrowdyMassSphereOverlapActors` | `CrowdyMassSphereOverlapComponents` | | Box | `CrowdyMassBoxOverlapActors`, `CrowdyMassBoxOverlapActorsWithOrientation` | `CrowdyMassBoxOverlapComponents`, `CrowdyMassBoxOverlapComponentsWithOrientation` | | Capsule | `CrowdyMassCapsuleOverlapActors`, `CrowdyMassCapsuleOverlapActorsWithOrientation` | `CrowdyMassCapsuleOverlapComponents`, `CrowdyMassCapsuleOverlapComponentsWithOrientation` | The shape parameters are `SpherePos` and `SphereRadius`, `BoxPos` and `BoxExtent`, or `CapsulePos`, `Radius` and `HalfHeight`. The `WithOrientation` forms add `FRotator Orientation`. All of them take `ObjectTypes`, then a `UClass*` filter (`ActorClassFilter` for the Actors forms, `ComponentClassFilter` for the Components forms), then `ActorsToIgnore`, then the output. Each overlapping entity is reported once, whichever way it is drawn. :::caution[A class filter applies to actors only, never to crowd entities.] Every crowd entity inside the volume that passes `ObjectTypes` is reported whatever `ActorClassFilter` or `ComponentClassFilter` says, including a close player whose near tier actor the filter rejected. Branch on `bOverlapMassEntity` rather than relying on the filter to exclude the crowd. ::: ## Read the hit An `FCrowdyMassTraceHit` has these fields, all read-only in Blueprint. | Field | What it holds | |---|---| | `bBlockingHit` | The trace hit something that blocks. | | `bHitMassEntity` | The hit landed on a Crowdy Mass entity, drawn as a crowd row or as a near tier actor. Branch on this. | | `bHitCrowdyEntity` | The hit landed on a Crowdy entity. | | `HitResult`, `Actor`, `Component` | The engine's hit result and the actor and component it hit. | | `UUID` | Who was hit. It is read from the entity, never from the near tier actor drawing it. | | `Entity` | The `FMassEntityHandle`. | | `Target` | The same hit as a send target. Set only when `bHitMassEntity` is true. | | `BoneName` | The bone that was hit. | | `HitZoneName` | The zone the bone belongs to in the active hit zone table, or the table's default zone. | | `DamageMultiplier` | The hit zone's multiplier. For a flourish only; see below. | | `InstanceIndex` | Which crowd row was hit, or `INDEX_NONE` for a hit on a near tier actor. | An `FCrowdyMassOverlapResult` has `bOverlapMassEntity`, `bOverlapCrowdyEntity`, `Actor`, `Component`, `UUID`, `Entity`, `Target` and `InstanceIndex`, with the same meanings, except that `InstanceIndex` is always `INDEX_NONE` on an overlap. `UCrowdyMassTraceBlueprintLibrary::GetEntity(Hit)` returns the `UCrowdyMassEntity` avatar object for a hit, or null if the hit was not a Crowdy Mass entity. The template form `GetEntity(Hit)` casts it to `T`. Reading `Target` does not create the avatar and `GetEntity` does, so do not call it on every hit, and do not keep the pointer across frames. On the object, `GetUUID()` returns the entity's id and `IsValid()` (the Blueprint node **Is Entity Valid**) is true while the entity still exists, and false once it is gone or its world has gone away. :::caution[Never work out damage from DamageMultiplier.] The multiplier comes from a table on the machine that ran the trace. The server never reads it. Use it for a hit marker or a damage number that the server's own result will replace. ::: ## Check it before you use it `FCrowdyMassEntityTarget::IsSet()` only says both members are filled in. `UCrowdyMassTargetLibrary::IsTargetLive(Target)` asks the entity manager, so it also answers false for an entity that has gone and for a handle whose slot another entity now holds. Call it on a stored target each time you use it, and do nothing when it is false. The snippets on the next pages do. ## Hit zones Assign a hit zone table once, and every Crowdy Mass trace fills `HitZoneName` and `DamageMultiplier` from it. 1. Create a **Crowdy Mass Hit Zone Table** data asset (`UCrowdyMassHitZoneTable`). Each row of **Entries** is an `FCrowdyMassHitZoneEntry` with a **Bone Name**, a **Zone Name** and a **Damage Multiplier**. **Default Zone Name** (`Default`) and **Default Damage Multiplier** (1.0) answer for a bone with no row. 2. At startup, call `UCrowdyMassTraceBlueprintLibrary::SetHitZoneTable(WorldContextObject, HitZoneTable)`. Call it once for the world, not on every pawn's `BeginPlay`. A table lasts for that world only, and null goes back to the built-in default zone. `UCrowdyMassTraceBlueprintLibrary::SetEntityTraceCollisionEnabled(WorldContextObject, Entity, bEnabled)` switches one entity in or out of traces at run time. It returns false for an entity that was never given a trace collision. ## Gotchas - **Nothing is traceable without the trace collision trait.** An entity with no `FCrowdyMassTraceCollisionFragment` never blocks a trace and never passes a filter. A config the SDK derives for you has it. A config you author yourself must include the **Crowdy Mass Trace Collision** trait, or the whole crowd stays untraceable and looks like a trace that is aimed wrong. The log names the fix once. - **A very long or malformed trace is slower, not wrong.** A trace whose sweep radius or end points are not finite or are out of range, or a straight trace longer than the grid's `SpatialGridMaxRayCells` (see [Turn Mass on for a map](../turn-mass-on-for-a-map.md)), is no longer answered partly from the grid. It tests every tracked entity, which costs more but finds far entities the grid used to miss. - **Targets exist in a play session only.** The Mass subsystem is created in Play In Editor and in a game, not in the editor world, and only when the map profile selects the **Crowdy Mass Rendering Backend**. On an actor pool map the trace and target calls find nothing. ## Related - [Call an event on it](./call-an-event-on-it.md): send an action or an event to a target - [Read and write its fields](./read-and-write-its-fields.md): fields of a row - [Read its server values](./read-its-server-values.md): values the server keeps per entity - [Target an entity, from Blueprint](../from-blueprint/target-an-entity.md): the same nodes in a graph - [Make a character draw as a crowd row](../make-a-character-draw-as-a-crowd-row.md): what gives a class a crowd row and trace collision # Call an event on it Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-cpp/call-an-event-on-it # Call an event on it This page has a Blueprint twin: [Call an event on it, from Blueprint](../from-blueprint/call-an-event-on-it.md). Once you have a [target](./target-an-entity.md), two calls act on the player it names. `SendEntityAction` plays a one-shot animation by name, with nothing declared. `CROWDY_MASS_SEND_EVENT` sends any Crowdy event of your own, so your own code runs on the machines that receive it. Both work whether the player is drawn as a crowd row or as a near tier actor. Both are game thread only. | You need | Include | |---|---| | `UCrowdyMassTargetLibrary` | `Target/CrowdyMassTargetLibrary.h` | | `UCrowdyMassEventLibrary` | `Events/CrowdyMassEventLibrary.h` | ## Choose between the two calls | | `SendEntityAction` | `CROWDY_MASS_SEND_EVENT` | |---|---|---| | Use it for | "Everyone sees this player cheer" | An event of your own that also runs your code | | What you declare | Nothing | A Crowdy event on the entity's actor class | | What you pass | An action name from the class's anim set | The class, the event, and its arguments | | Who it reaches | Everyone drawing the entity as a row or a near tier actor | The recipients the event's own setting names | ## Send a one-shot action by name `static bool SendEntityAction(const FCrowdyMassEntityTarget& Target, FName ActionName, int32 Variant = 0, float PlayRate = 1.0f)` looks the name up in the anim set of the target's class, and everyone drawing that entity as a crowd row or as a near tier actor sees the action, including you. The cheer in the Oakford square, the reasons the call returns false and the rules around it are on [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md). ## Call a Crowdy event on an entity Use this for an event you declared on the entity's actor class, in C++ with `CROWDY_EVENT` or in Blueprint with **Crowdy Replicates** ticked on a custom event. See [RPC Events in C++](../../runtime/rpc-events-cpp.md). In the Oakford square, `ALanternPlayer` declares `WaveTorch` (`ActionId`, `Variant`, `PlayRate`), a `SpatialMulticast` event marked an action. `ALanternPlayer` keeps a `TracedTarget` from [Target an entity](./target-an-entity.md#trace-for-it). To make the traced player wave their torch: ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" #include "Events/CrowdyMassEventLibrary.h" #include "Target/CrowdyMassTargetLibrary.h" void ALanternPlayer::OnSignal() { if (!UCrowdyMassTargetLibrary::IsTargetLive(TracedTarget)) { return; } // ActionId, Variant, PlayRate: checked against WaveTorch's parameters when this compiles. CROWDY_MASS_SEND_EVENT(TracedTarget, ALanternPlayer, WaveTorch, 1, 0, 1.0f); } ``` `CROWDY_MASS_SEND_EVENT(Target, Class, Event, Args...)` calls the `CROWDY_EVENT` called `Event` that is declared on `Class`, for the row that `Target` names. The arguments are checked against the event's parameters when your code compiles, so a wrong count or type is a compile error, and each one is placed by the event's reflected parameters. It expands to a call of `UCrowdyMassEventLibrary::SendEventToMassTarget(Target, TEXT("Event_Implementation"), &Class::Event_Implementation, Args...)`. :::note[The untyped call still exists.] `SendEventToMassTarget(const FCrowdyMassEntityTarget& Target, UFunction* Fn, const void* Frame)` is still there for code that already holds the event's `UFunction` and a frame it built. The frame holds the event's input parameters in declaration order, laid out as the reflected function lays them out, and the call does not construct or destroy anything in it. Nothing checks it against the function, so prefer the macro. ::: The event goes out as if it had been called on that player's actor, so its recipient setting decides who runs it. A `SpatialMulticast` event reaches everyone near the entity. See [Recipients and Routing](../../runtime/recipients-and-routing.md). On a machine that draws the player as a crowd row or as a near tier actor, the event's body runs on the per-class stand-in, and a near tier actor only draws the entity and plays its actions; see [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md). The return value is not a delivery receipt. It is false only when the target's world is gone, since then there is nothing to route through. A target that cannot be resolved, a function that is not a Crowdy event and a class mismatch all return true, with a line in the log. Check `IsTargetLive` first. ### When a call is refused The macro and `SendEventToMassTarget` log a refusal and return true when: - the function is null or is not a Crowdy event; - the target does not resolve to a live entity with an identity; - this client cannot resolve the entity's class yet: add the class to **Preloaded Entity Classes** under **Project Settings, Plugins, Crowdy SDK, Replication**, as [Entities and Spawning](../../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all) describes; - the declaring class of the function is not the entity's class or one of its bases. ## Gotchas - **The event must be on the entity's own class.** An event declared on a component cannot run on a crowd row; see [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md). - **Anyone can call it.** Any client can send an event at any entity. Treat what arrives as a request to draw something, and keep anything that must be true on the server. ## Related - [Target an entity](./target-an-entity.md): get the target these calls take - [Receive its events](./receive-its-events.md): react to an event on the machines that draw the entity - [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md): author the actions `SendEntityAction` plays - [Set up the animation set](../set-up-the-animation-set.md): where the action names come from - [Call an event on it, from Blueprint](../from-blueprint/call-an-event-on-it.md): the same calls as nodes # Receive its events Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-cpp/receive-its-events # Receive its events This page has a Blueprint twin: [Receive its events, from Blueprint](../from-blueprint/receive-its-events.md). When a Crowdy event arrives for a player you draw as a crowd row or a near tier actor, the SDK already runs the event's own body for you on the per-class stand-in; see [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md). `UCrowdyMassEntitySubsystem::RegisterEventHandler` lets your own code take that event instead: you get the event's parameters and the entity, and you write what the row does. It is the receiving half of [`SendEventToMassTarget`](./call-an-event-on-it.md#call-a-crowdy-event-on-an-entity). | You need | Include | |---|---| | `UCrowdyMassEntitySubsystem`, `FCrowdyMassEventIngestor`, `FCrowdyMassViewStateContext` | `Subsystems/CrowdyMassEntitySubsystem.h` | | `CrowdyMassWriteAction`, `CrowdyMassMakeActionEventHandler` | `Mass/CrowdyMassActionEvents.h` | | `FCrowdyMassActionFragment` | `Fragments/CrowdyMassAnimFragments.h` | ## When you need this - To drive a crowd row yourself when an event arrives: write an action, set a field, count something. - To handle an event from code that is not on the entity's own class, such as a subsystem. If the event's own body already does what you want, or the event is marked an action and uses the names `ActionId`, `Variant` and `PlayRate`, you do not need a handler. Registering one changes what runs; see [what a bound function replaces](#what-a-bound-function-replaces). ## Bind a function to an event In the Oakford square, `WaveTorch` is a `SpatialMulticast` event marked an action. `ULanternCrowdSubsystem` is a world subsystem that registers a handler for it when the world begins play. The handler does two things. It flares the row's torch by setting `TargetBrightness` on `FLanternTorchFragment` to 1, and it writes the wave into the row's action fragment itself with `CrowdyMassWriteAction` (action id 1, the first action of the anim set). ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "LanternCrowd.generated.h" // LanternCrowd.cpp #include "LanternCrowd.h" #include "Fragments/CrowdyMassAnimFragments.h" #include "LanternPlayer.h" #include "Mass/CrowdyMassActionEvents.h" #include "Subsystems/CrowdyMassEntitySubsystem.h" void ULanternCrowdSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); UCrowdyMassEntitySubsystem* Mass = InWorld.GetSubsystem(); const UFunction* WaveFunction = ALanternPlayer::StaticClass()->FindFunctionByName(TEXT("WaveTorch_Implementation")); if (!Mass || !WaveFunction) { return; } // A registered handler replaces the event body and the automatic action write on a crowd row, so it writes the action itself. Mass->RegisterEventHandler(WaveFunction, [](const FCrowdyEventParams&, const FCrowdyMassViewStateContext& Context) { if (FLanternTorchFragment* Torch = Context.GetFragment()) { Torch->TargetBrightness = 1.f; } FCrowdyMassActionFragment* Action = Context.GetFragment(); if (!Action) { return; } CrowdyMassWriteAction(*Action, 1, 0, 1.f); }); } ``` It writes the wave itself because a registered handler replaces the event's body and the automatic action write; see [what a bound function replaces](#what-a-bound-function-replaces). `RegisterEventHandler(const UFunction* Function, FCrowdyMassEventIngestor Ingestor)` takes the reflected receiver, which for a `CROWDY_EVENT` is the `_Implementation` function on the declaring class. The ingestor is `TFunction`. It returns an `FCrowdyMassEventHandlerHandle`, whose `IsValid()` is false only for a null function or a null ingestor. `UnregisterEventHandler(Handle)` takes the handler off again, and is safe to call from inside the handler. - Register where the world exists. `UCrowdyMassEntitySubsystem` is created in Play In Editor and game worlds only, and only when the map profile selects the **Crowdy Mass Rendering Backend**, so a world subsystem's `OnWorldBeginPlay` is a good place. On an actor pool map there is no subsystem, and a registration made through the Blueprint node logs once and receives nothing. - A registration is per exact `UFunction` and per world. A Blueprint class that is recompiled in the editor makes a new function object, so register it again. - Registering the same function twice adds a second handler. Both run, in registration order, and neither may depend on that order. - `IsEventHandlerRegistered(Function)` and `NumEventHandlersRegistered(Function)` let a startup check notice that your registration never ran. `RegisterObjectEventHandler(Function, HandlerObject, HandlerFunctionName)` is the call the Blueprint node **Receive Crowdy Event on Entity** compiles to. The handler function's first parameter must be an `FCrowdyMassEntityTarget`, and its other parameters are matched to the event's parameters by name, and only where the types are exactly the same. It may have no output parameter and no return value. The object is held weakly, and binding the same object and function name again is the same binding. `UnregisterObjectEventHandler(Function, Object, FunctionName)` removes it. ## What the parameters give you The ingestor receives two things. - `const FCrowdyEventParams&` holds the event's parameters, which you read by name. See [RPC Events in C++](../../runtime/rpc-events-cpp.md#what-happens-on-the-wire). - `const FCrowdyMassViewStateContext&` holds `EntityManager`, the `Entity` handle and `EntityClass`. `EntityClass` is always null for an event handler, because it is set only for state fields; call `GetEntityClass` on the subsystem with the entity's UUID if you need the class. `Context.GetFragment()` returns a pointer to one of the entity's fragments, or null if it has none. The pointer is good for this call only. `CrowdyMassWriteAction(Action, ActionId, Variant, PlayRate)` is the one write behind every one-shot action. It clamps the play rate to 0.1 to 4.0 and the variant to 0 to 63, and it returns false and writes nothing for an `ActionId` of zero or less. If your action event uses other names for its parameters, you do not need a handler of your own. `CrowdyMassMakeActionEventHandler(ActionIdParam, VariantParam, PlayRateParam)` returns a ready-made ingestor that reads the names you give it. An ingestor may change what the row looks like and nothing else. It must not add or remove a fragment or tag, destroy the entity, or touch its trace collision. :::caution[Everything the ingestor receives came from the player being watched.] The parameters were chosen by the client you are observing and can be anything at all. Use them to decide what is drawn, never what is true. Keep health, loot and the like in a Server Object (see [Keep a record per entity](../../exec/keep-a-record-per-entity.md)) and read it with [`GetServerValueAs`](./read-its-server-values.md). ::: ## What a bound function replaces A registered handler counts as the game having said what the event means for a row. While one is registered for an event, a crowd row does not run the event's own body. It also skips the automatic action write that an event marked an action would do. The stand-in body is the fallback for an event nothing took. A bound **Receive Crowdy Event on Entity** node counts as a handler for the body only. It replaces the event's body on a row, but the declared action is still written, so the row still plays the action. That is why the handler in the snippet writes the action itself. Without `CrowdyMassWriteAction`, registering a handler for `WaveTorch` would stop the row waving. An event marked an action with no handler registered gets both: the action write, and the body running on the stand-in. ## Which events you can receive Any Crowdy event declared on your own actor class, with a recipient that reaches rows. Only the two multicast recipients reach a machine that draws the player as a crowd row or a near tier actor. An `OwningClient` or `Host` event lands on one machine, which holds the player as a real actor. See [Recipients and Routing](../../runtime/recipients-and-routing.md). The events the SDK declares on `UCrowdyMassEntity`, such as `PlayCrowdyAction`, run their own body on every observer and are not offered to a handler. A handler registered for one never runs. ## Gotchas - **Inbound events are rate limited.** A target can receive 30 events a second, with a burst of 60. See [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md) for what the limit is for. - **A handler is world scoped.** Two worlds can be alive at once during travel. Register in each world that needs one. ## Related - [Call an event on it](./call-an-event-on-it.md): the sending half - [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md): what the event's own body does on a crowd row - [Read and write its fields](./read-and-write-its-fields.md): fields an ingestor can set - [Receive its events, from Blueprint](../from-blueprint/receive-its-events.md): the same binding as a node # Read and write its fields Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-cpp/read-and-write-its-fields # Read and write its fields This page has a Blueprint twin: [Read and write its fields, from Blueprint](../from-blueprint/read-and-write-its-fields.md). A crowd row keeps the values your game draws it with, such as the brightness of a torch, in fragments, which are small records of data that Mass owns. A field is a name that points at one `UPROPERTY` of one fragment. You register it once, and then `UCrowdyMassTargetLibrary` reads and writes it by name for an `FCrowdyMassEntityTarget`. The same calls are Blueprint nodes. A field is a visual: it holds what this machine draws, and it can differ from machine to machine. | You need | Include | |---|---| | `CrowdyMassFields::RegisterField`, `EAccessResult` | `Target/CrowdyMassFieldAccess.h` | | `UCrowdyMassTargetLibrary` | `Target/CrowdyMassTargetLibrary.h` | | `UCrowdyMassEntitySubsystem::RegisterStateFragmentField` | `Subsystems/CrowdyMassEntitySubsystem.h` | ## When you need this - To show something on the screen about the player you are looking at, from a value the row already holds. - To make a row do something visible from code that has a target, such as flaring its torch. For a value that has to be true, such as health, use [Read its server values](./read-its-server-values.md). ## Before you start: the name must be registered No field is registered for you. Registering is two calls under one name: what an arriving property does, and where it lives. In the Oakford square, `ULanternCrowdSubsystem` registers both when the world's subsystems initialize. It lands the `TorchBrightness` property of `ALanternPlayer` on `TargetBrightness` of `FLanternTorchFragment`, and it makes the name `TorchBrightness` read and write that same member. A read returns the target. A write sets a new target, which `ULanternTorchFadeProcessor` eases the torch toward until the next replicated value replaces it. ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "LanternCrowd.generated.h" UCLASS() class ULanternCrowdSubsystem : public UWorldSubsystem { GENERATED_BODY() public: virtual bool ShouldCreateSubsystem(UObject* Outer) const override; virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void OnWorldBeginPlay(UWorld& InWorld) override; }; // LanternPlayer.h UPROPERTY(BlueprintReadOnly, Category = "Lantern", meta = (CrowdyState, CrowdyHeartbeat)) float TorchBrightness = 1.f; // LanternCrowd.cpp #include "LanternCrowd.h" #include "Subsystems/CrowdyMassEntitySubsystem.h" #include "Target/CrowdyMassFieldAccess.h" bool ULanternCrowdSubsystem::ShouldCreateSubsystem(UObject* Outer) const { // Only where the Mass subsystem is created: a play world whose map profile selects the Mass backend. return Super::ShouldCreateSubsystem(Outer) && GetDefault()->ShouldCreateSubsystem(Outer); } void ULanternCrowdSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); // The transient world at engine start has no game instance yet. if (!GetWorld()->GetGameInstance()) { return; } UCrowdyMassEntitySubsystem* Mass = Collection.InitializeDependency(); if (!Mass || Mass->IsStateFragmentFieldRegistered(TEXT("TorchBrightness"))) { return; } Mass->RegisterStateFragmentField(TEXT("TorchBrightness"), ECrowdyMassStateFieldType::Float, [](const void* PropertyValuePtr, const FCrowdyMassViewStateContext& Context) { FLanternTorchFragment* Torch = Context.GetFragment(); if (!Torch || !PropertyValuePtr) { return; } Torch->TargetBrightness = *static_cast(PropertyValuePtr); }); CrowdyMassFields::RegisterField(TEXT("TorchBrightness"), TEXT("TargetBrightness")); } ``` - `RegisterStateFragmentField` says what an arriving `CrowdyState` property does to the row. See [Add your own crowd data](../add-your-own-crowd-data.md). - `CrowdyMassFields::RegisterField(TEXT("TorchBrightness"), TEXT("TargetBrightness"))` says which `UPROPERTY` of which fragment the name reads and writes. The non-template form takes the `UScriptStruct*` instead. Use the same name as the matching `RegisterStateFragmentField`, so a graph asking for the name and the ingestor landing it cannot drift apart. - `IsStateFragmentFieldRegistered` and `CrowdyMassFields::IsFieldRegistered` answer whether each registration ran. An unregistered name fails every read and write, and the log warns `Nothing is registered as ''` at most once every five seconds, so check once at startup. The rules for `RegisterField`: - The property must be a `UPROPERTY`. A plain C++ member has no reflection entry and is refused. - The fragment must derive from `FMassFragment`. - `FCrowdyRemoteIdentityFragment` is always refused. It carries the entity's id, owner and role. - Registering the same name with the same binding again succeeds and does nothing, which is what makes it safe to call once per world. The same name with a different binding is refused, and the first one is kept. - A name from `RegisterField` is claimed across the whole process. A name from `RegisterStateFragmentField` is claimed per world, across every class. - Registering grants a graph write access to the field, so register only fields that are yours and safe to draw from. - In editor builds a native recompile re-resolves the bindings by itself. `CrowdyMassFields::RebindRegisteredFields()` does it by hand. ## The functions All of them are static members of `UCrowdyMassTargetLibrary`, game thread only, and return a bool. | Function | The field's type | |---|---| | `GetFieldByte`, `SetFieldByte` | `uint8`, and an enum member that is one byte wide | | `GetFieldInt`, `SetFieldInt` | `int32` exactly | | `GetFieldFloat`, `SetFieldFloat` | `float` or `double`; the value is a `double` | | `GetFieldBool`, `SetFieldBool` | `bool`, including a bitfield | | `GetFieldVector`, `SetFieldVector` | `FVector` | | `IsFieldRegistered` | takes a name only | | `IsTargetLive` | takes a target only | Their Blueprint display names are **Get Crowdy Mass Field (Float)**, **Set Crowdy Mass Field (Float)** and so on, with **Is Crowdy Mass Field Registered** and **Is Crowdy Mass Target Live**. ## Read a field A getter takes the target, the field name and an out value: `static bool GetFieldFloat(const FCrowdyMassEntityTarget& Target, FName FieldName, double& OutValue)`. Check the target with `IsTargetLive` first, and use the out value only when the call returns true. ## Write a field A setter takes the target, the field name and the value: `static bool SetFieldFloat(const FCrowdyMassEntityTarget& Target, FName FieldName, double Value)`. It returns true when the write happened. ## When a read answers false The call returns false for any of these: - The name is not registered. - The entity is gone, or its target is stale. - The entity has no fragment of the registered type. An entity built from a different config can lack a fragment another one has. - The field is not of the type the call reads. - Mass is processing right now. `CrowdyMassFields::AccessField(Target, FieldName, bWrite, Visitor)` is the lower-level door, and it says which reason it was. It returns an `EAccessResult`: `Ok`, `NoWorld`, `InvalidEntity`, `UnknownField`, `NoFragment`, `TypeMismatch`, `DuringMassProcessing` or `OffGameThread`. `CrowdyMassFields::DescribeAccessResult` turns one into text. The visitor gets the field's `FProperty` and a pointer to its value, and the pointer is valid only during the call. ## What a write means :::caution[A write is a visual, and it is local.] The same field on the same entity holds different values on two machines. Nothing mechanical may be decided from one. Anything a peer must not be able to influence belongs in a Server Object; see [Keep a record per entity](../../exec/keep-a-record-per-entity.md). ::: If an ingestor or a processor writes the same member, your write lasts only until it next runs. In the Oakford square the ingestor writes `TargetBrightness` whenever a `TorchBrightness` arrives from the player, so a write there holds until the next value lands. :::caution[An access made while Mass is processing is refused.] Code reached from a delegate broadcast inside a Mass chunk loop would race the processors that read the same fragment. So would an ordinary tick that overlaps a Mass phase, because Mass phases run alongside the game thread. The access is refused and logged: an error for a write, a warning for a read, at most once every five seconds for a field name. Doing the work from a phase-end delegate, or from a broadcast deferred to the end of the frame, puts it outside the schedule and takes the refusal away. ::: ## Set a pose by name The pose of a row has its own call, because the pose fragment is not a plain field. `UCrowdyMassTargetLibrary::SetEntityPose(Target, PoseName)` puts the row in the pose of that name from its class's anim set. - `NAME_None` clears the pose. - A name the anim set does not author also clears the pose, and says so in the log. - It returns false, and does nothing, if the entity is gone, has no pose fragment, Mass is processing, or you named a pose for an entity whose class this client cannot resolve yet. Add the class to **Preloaded Entity Classes** under **Project Settings, Plugins, Crowdy SDK, Replication**; see [Entities and Spawning](../../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all). - The pose is a visual. An arriving pose replaces one you set. See [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md). ## Gotchas - **A name is claimed once.** Two different fragment fields cannot share a name. - **Reads are values, not references.** A function hands back a copy, never a pointer into the row, because Mass moves an entity's data when its archetype changes. ## Related - [Target an entity](./target-an-entity.md): get the target these functions take - [Add your own crowd data](../add-your-own-crowd-data.md): the fragment, trait and processor behind a field - [Read its server values](./read-its-server-values.md): the values that decide what is true - [Read and write its fields, from Blueprint](../from-blueprint/read-and-write-its-fields.md): the same calls as nodes # Read its server values Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/from-cpp/read-its-server-values # Read its server values This page has a Blueprint twin: [Read its server values, from Blueprint](../from-blueprint/read-its-server-values.md). The values the server owns about a player or a creature, such as health, loot or a team's orders, are what is true. They cannot live on an actor for a crowd row, because a crowd row has no actor. They live in a Server Object that keeps **a record per entity**, and a row reads its own record in place. Nothing is copied into a fragment, and nothing runs per row per frame: the work is one pass over a batch of changed records. The Server Object type and its server code are on [Keep a record per entity](../../exec/keep-a-record-per-entity.md), and this page does not repeat them. It covers what a crowd adds: the setup on the entity's class, and the calls a row uses. | You need | Include | |---|---| | `UCrowdyMassServerValuesLibrary` | `Target/CrowdyMassServerValuesLibrary.h` | | `UCrowdyMassServerRecordsSubsystem`, `FOnCrowdyMassServerValuesChanged` | `Subsystems/CrowdyMassServerRecordsSubsystem.h` | | `UCrowdyMassNearTierComponent` | `Components/CrowdyMassNearTierComponent.h` | ## When you need this - To show a value on a player or creature you are looking at: a health bar over a raider. - To react when a value changes: a bar that moves, a marker that appears. The pose, the torch and the wave are view state. A modified client can lie about them, and the worst it can do is change what you see. A server value is the truth, and a player may only read what the server chose to send them. ## Set up the class On the Oakford north road, the raiders are `BP_Raider`, keeping a record each in `CSO_Raider` (see [Raiders on the north road](../../exec/examples/raiders-on-the-north-road.md)). On a Mass map the players who watch the road draw each raider as a crowd row, and a row reads the same record. 1. **Make the type.** A Server Object definition with **Holds** set to **A Record per Entity**, whose server code adds a record for each entity, keyed by the entity's id. Each variable is **Server Only**, **Everyone**, **Owner Only** or **Team Only**. `CSO_Raider` has `Health` for **Everyone**, `Loot` for **Owner Only** and `Hideout` for **Team Only**. 2. **Put the component on the class.** The entity's actor class carries a `UCrowdyServerObjectComponent` whose **Definition** is that type. It can be native, in the Blueprint, or inherited. The SDK reads it once for the class, from the class default object. `BP_Raider` has one with **Definition** `CSO_Raider`. See [One object per area](../../exec/from-cpp/get-a-server-object.md#one-object-per-area). 3. **Pick the object with the component's Instance Mode.** Rows follow only a mode that is the same for the whole class: | Instance Mode | What the rows read | |---|---| | **Only One Instance** (a setting of the type) | The type's one object. It wins over every Instance Mode. | | **Instance Id**, with an id typed | The object with that id. | | **This Actor's Area** (`ECrowdyServerObjectInstanceMode::ThisActorsArea`) | The objects of the areas around the local view. | With **Signed-In Player**, **This Actor**, **Player's Team** or **From Server Value**, the rows get no server values, and the log says why once for the class. 4. **Use the entity's id as the record id.** A row's record is the one whose id is the entity's UUID as 32 hex digits, the form a census of the world reports as `uuid`. `UCrowdyMassServerValuesLibrary::ClassGetsServerValues(const UClass*)` answers whether rows of a class can get server values at all: whether it carries such a component, reading a record per entity, in a mode rows can follow. Use it in a startup check. With **This Actor's Area**, the game follows the areas within the type's **Areas Followed Around a Player** of the local player's pawn or camera, and moves them as the view moves. A value of 1 means the 3 by 3 areas around the view. It lets go of an area only when the view is more than that radius plus one away, so a border does not flap. With no pawn or camera to follow, no area is followed and the rows read nothing. ## Read a value `UCrowdyMassServerValuesLibrary::GetServerValueAs(const FCrowdyMassEntityTarget& Target, FName Name, T& Out)` reads the variable called `Name` into `Out`. `T` must be exactly the type of the variable: a number of the same width, a `bool`, an `FString`, `FName` or `FText`, a `UENUM`, or a `USTRUCT`. The Blueprint node is **Get Server Value**. The call returns false, and resets `Out` to `T()`, when the value has not arrived yet, when the variable does not exist, or when `T` is not the variable's type. The snippet reads a raider's health into an `int32` and uses the player's torch to show it: a raider on its last legs turns the torch red. ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" #include "Target/CrowdyMassServerValuesLibrary.h" void ALanternPlayer::OnCheckHealth() { int32 Health = 0; if (!UCrowdyMassServerValuesLibrary::GetServerValueAs(TracedTarget, TEXT("Health"), Health)) { return; } // A raider on its last legs turns the torch red. Torch->SetLightColor(Health <= 10 ? FLinearColor::Red : FLinearColor::White); } ``` The server never sends a value the player may not receive, such as an **Owner Only** value of an entity they do not own. The call returns false for it. :::note[The property-based calls still exist.] `GetServerValueInto(Target, Name, const FProperty* DestProperty, void* Dest)` reads into a property you already hold and returns true only for a value that fits it. `FindServerValueProperty(Target, Name)` gives you the variable's property, and `PropertyHolds(Property)` tells you whether a `T` fits it. Use them when the type is only known at run time. ::: `UCrowdyMassServerValuesLibrary::FindServerValues(Target)` gives the whole record at once: a `const FInstancedStruct*` holding the record's value struct, or null when the entity has no record. Read it with `GetPtr()` for the struct your type uses, and do not keep the pointer. ## React to a change `UCrowdyMassServerRecordsSubsystem` is a world subsystem, created in Play In Editor and game worlds only, and only when the map profile selects the Crowdy Mass Rendering Backend. Get it with `World->GetSubsystem()`. The delegate type is `FOnCrowdyMassServerValuesChanged`, a multicast delegate of `(const FCrowdyMassEntityTarget& Target, TConstArrayView Changed)`. `Changed` names the variables that changed, as `GetServerValueAs` takes them, and is empty when the entity's record was removed. | Call | What it does | |---|---| | `AddClassHandler(const UClass*, FOnCrowdyMassServerValuesChanged::FDelegate)` | Runs the listener after each change of the values of a row of that class or a subclass. Returns an `FDelegateHandle`. | | `RemoveClassHandler(FDelegateHandle)` | Takes it off. | | `BindObjectHandler(const UClass*, UObject*, FName FunctionName)` | The same, for a function that takes the entity target and a `TArray`. This is what **Receive Server Values Changed on Entity** compiles to. Binding again changes nothing. | | `UnbindObjectHandler(const UClass*, const UObject*, FName)` | Takes it off. | | `WatchEntity(FMassEntityHandle, FOnCrowdyMassServerValuesChanged::FDelegate)` | Follows one row. It also runs once at the next flush if the record is already there. | | `UnwatchEntity(FMassEntityHandle, FDelegateHandle)` | Stops following it. | A record can arrive before its row exists. The record is kept, and the event runs once, with the current values, when the row appears. A row that moves from one followed area into the next does not see its record removed and added again. ## A near tier actor A class that promotes to the near tier (see [Promote close entities to actors](../promote-close-entities-to-actors.md)) reads and hears the same values through its `UCrowdyMassNearTierComponent`. Both answer for the entity the near tier actor stands in for, so the actor and its row never disagree. - `ReadServerValueInto(FName Name, const FProperty* DestProperty, void* Dest) const` reads the bound entity's variable. The Blueprint node is **Get Server Value**. - `OnServerValuesChanged` is a dynamic multicast delegate, `FCrowdyMassNearTierServerValuesChanged`, with one parameter, `const TArray& Changed`. Bind it with `AddDynamic`. ## Gotchas - **Read to draw.** The values are the server's. Change them by calling a Server Function, not from here. - **Owner Only and Team Only values arrive later** than **Everyone** values, because they are fetched, at most once a second for each followed area. - **The first values are not instant.** A row drawn the moment a player enters an area reads nothing until the object has been read. - **A class stays followed for the life of the world** once a row of it has been seen. - **A row gets no values.** Work through the checklist on [Fix a crowd that does not look right](../troubleshooting.md). ## Related - [Keep a record per entity](../../exec/keep-a-record-per-entity.md): the type, the audience of a variable and the server code - [Raiders on the north road](../../exec/examples/raiders-on-the-north-road.md): the record per raider this page reads - [Target an entity](./target-an-entity.md): get the target `GetServerValueAs` takes - [Promote close entities to actors](../promote-close-entities-to-actors.md): the near tier - [Fix a crowd that does not look right](../troubleshooting.md): the checklist for a row that gets no values - [Read its server values, from Blueprint](../from-blueprint/read-its-server-values.md): the same reads as nodes # Market night in Oakford Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/mass/examples/market-night-in-oakford # Market night in Oakford On market night the village square fills with players, far more than the actor pool should draw as real actors. So the Oakford map draws every other player as a row of a crowd. This page builds that, step by step. You switch the map, give the crowd a look, add a sitting pose, a wave and a cheer, promote the players standing close, dim and brighten a torch, and read the health of the raiders on the north road from the server. Each step shows the values and the order. [Draw a crowd with Mass](../draw-a-crowd-with-mass.md) and the pages linked from each step explain why. Every player is still a real actor on their own machine. A row is how another player's machine draws them. ## What you start from The player is `ALanternPlayer`, a character class with a `Torch` point light and a `ULanternPlayerExecutor`. The executor sends `FLanternPlayerState`, which holds `Location`, `Rotation` and `bTorchLit`. Build it as [Continuous State](../../runtime/continuous-state.md#a-custom-executor) does, not as the [Quickstart](../../quickstart.md) leaves it. The crowd reads `Location` and `Rotation` to place a row. See [Make a character draw as a crowd row](../make-a-character-draw-as-a-crowd-row.md). `BP_LanternPlayer` is your own Blueprint subclass of `ALanternPlayer`. It needs a skeletal mesh and the movement and input you already have, not a bare Character. Its Crowdy Entity Component is set to **Dynamic**, **Player Derived** and **Local Client**. You also need an idle, walk, run, sitting, fallen, wave and cheer clip for its skeleton. Two parts need C++: the `Build.cs` lines, and the torch data under [Brighten the torch](#brighten-the-torch), which is a fragment, a trait, a processor and a field registration. Every other step has a Blueprint half and a C++ half that do the same thing. The rules of the night: - Players walk, run and stand at the stalls. One key (X) sits a player down and the same key stands them up again. A seated player's torch burns low. - A player who trips on the cobbles lies on the ground until they are helped up. - A wave of the torch (V) greets everyone in range. The inspect key (I) marks the player you are looking at, and the cheer key (C) sends a cheer at them. - The players standing close to you get a real actor with the full animation graph. The rest of the square is rows. - You can read how bright another player's torch is. - The raiders camped on the north road are drawn as rows too. The health key (H) shows you how hurt the one you marked is. ## Add the module The game module lists `CrowdyMass` in its `Build.cs`. This example declares fragments, a trait and a processor of its own, so it lists the engine's Mass modules too. ```csharp PublicDependencyModuleNames.AddRange(new string[] { "CrowdySDK", "CrowdyNet", "CrowdyReplication", "CrowdyServices", "CrowdyMass", "MassCore", "MassEntity", "MassSpawner" }); ``` There is no plugin to enable. See [Turn Mass on for a map](../turn-mass-on-for-a-map.md). ## Switch the Oakford map to Mass 1. In the Content Browser, click **Add**, then **Miscellaneous**, then **Data Asset**, and pick **Crowdy Map Profile**. Name it `DA_OakfordProfile`. 2. Open it. Under **Actor Management**, set **Backend Class** to **Crowdy Mass Rendering Backend**. 3. Leave **Enable Networking** ticked and **Backend Config** empty. The backend then reads **Project Settings, Plugins, Crowdy Mass**. 4. In **Project Settings, Plugins, Crowdy SDK**, open **Map Profiles** and add an entry that maps the Oakford map to `DA_OakfordProfile`. See [Assign the profile](../../runtime/map-profile.md#assign-the-profile). You could also start from the shipped profile `/CrowdySDK/Mass/Data/DA_CrowdyMassDefaultProfile`, which already selects the Mass backend. Every other map keeps the actor pool. From now on, on the Oakford map, a player you see on your screen is a crowd row, not a spawned `BP_LanternPlayer`. Your own pawn is still a real actor. See [Turn Mass on for a map](../turn-mass-on-for-a-map.md). ## Flag the material 1. Open the parent material of the lantern player's body, `M_LanternPlayer`. Open the parent, not a material instance. 2. Under **Usage**, tick **Used with Instanced Skinned Mesh**. 3. Save the material. Skip this and the whole square is grey. [Fix a crowd that does not look right](../troubleshooting.md) has the checklist. ## The engine's animation pass There is nothing to do here. The SDK hands each lantern player's animation to the renderer itself, and it turns the engine's own animation pass off for you when the game starts. Oakford needs no `DefaultMass.ini` for it. See [The engine's animation pass](../turn-mass-on-for-a-map.md#the-engines-animation-pass). ## Check the class with `crowdy.mass.derive` Before you run anything, in the editor console: ``` crowdy.mass.derive BP_LanternPlayer ``` It prints the mesh it found on `BP_LanternPlayer`, which of the three routes supplied the mesh, the trace collision object type, and whether every material slot is flagged. It warns if the mesh is missing, if a material is not flagged, or if an authored config would win. The editor world has no Mass subsystem, so the anim set and the config answer appear only when you run the same command in Play In Editor, after the anim set below. Nothing here needs an entity config of your own: Mass works one out from the class. See [Make a character draw as a crowd row](../make-a-character-draw-as-a-crowd-row.md). ## Look at the square On one machine you see no crowd, because the backend does not draw your own entity as a row. Pick one of these: - Give `DA_OakfordProfile` a **Backend Config** of **Crowdy Mass Backend Config** and tick **Render Local Owner** under **Mass Backend, Debug**. Your pawn then also draws as a row, overlapping the real one. Turn it off when you are done. - Or place a **Crowdy Mass Area Spawner** in the square, with **Entity Class** `BP_LanternPlayer` and **Entity Count** 200. See [Turn Mass on for a map](../turn-mass-on-for-a-map.md) for both. With two Play In Editor windows you see the real thing: each window shows the other player as a row, moving. ## Give the crowd legs 1. In the Content Browser, click **Add**, then **Miscellaneous**, then **Data Asset**, and pick **Crowdy Mass Anim Set**. Name it `DA_LanternPlayerAnims`. 2. Create the bank `TP_LanternPlayer`, the asset the crowd renders through, and add the idle, walk, run, sitting and fallen clips to it, each in its own slot. [Set up the animation set](../set-up-the-animation-set.md) shows how. Then set `TP_LanternPlayer` as `DA_LanternPlayerAnims`'s **Bank**. The config the SDK builds from `BP_LanternPlayer` draws its mesh through that bank, so nothing else needs authoring. 3. Under **Gaits**, add three rows, slowest first. Speeds are in Unreal units per second. | Gait Name | Sequence | Enter Speed | Exit Speed | |---|---|---|---| | `Idle` | the idle clip | 0 | 0 | | `Walk` | the walk clip | 100 | 60 | | `Run` | the run clip | 400 | 330 | 4. Add a **Crowdy Mass Anim Set** component to `BP_LanternPlayer` and set its **Anim Set** to `DA_LanternPlayerAnims`. Which gait a row plays comes from how fast it moves on your screen. Nothing about gaits is sent. See [Set up the animation set](../set-up-the-animation-set.md). ## Sit and fall A pose is held state, so something must say which one. On `ALanternPlayer` that is the `Stance` property of the enum `ELanternStance : uint8`, with the values `Standing`, `Sitting` and `Fallen`. The property is marked `CrowdyState` and `CrowdyHeartbeat`. A dropped change would otherwise leave a player sitting on every other screen until something else changed. ```cpp // LanternPlayer.h UENUM(BlueprintType) enum class ELanternStance : uint8 { Standing, Sitting, Fallen }; UPROPERTY(BlueprintReadOnly, Category = "Lantern", meta = (CrowdyState, CrowdyHeartbeat)) ELanternStance Stance = ELanternStance::Standing; ``` 1. Add a variable named `Stance` to `BP_LanternPlayer`, of the `ELanternStance` enum. 2. In the variable's details, set **Replication** under **Crowdy Replication** to **Replicated**, and leave **Keyframe heartbeat** ticked. 3. Compile. In `DA_LanternPlayerAnims`, under **Pose State**, leave **Pose State Enum Is** on **Native By Name** and pick `ELanternStance` as the **Pose State Enum**. Add two **Poses** rows: | Pose Name | Sequence | Rank | Hold Last Frame | |---|---|---|---| | `Sitting` | the sitting clip | **Below Actions** | off | | `Fallen` | the fallen clip | **Above Actions** | on | `Standing` is value 0 and has no row. A value with no row clears the pose and the row walks or idles again. `Sitting` is ranked **Below Actions**, so a wave plays over a player who is sitting. `Fallen` is ranked **Above Actions**, so nobody waves from the ground. The sit key toggles `Stance`. It also sets `TorchBrightness` to 0.3 while the player sits and 1.0 while they stand, and sets the intensity of the local `Torch` light to match. `TorchBrightness` is the property you add under [Brighten the torch](#brighten-the-torch). The sit key is the only thing that sets it. ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" void ALanternPlayer::OnSit() { Stance = Stance == ELanternStance::Sitting ? ELanternStance::Standing : ELanternStance::Sitting; // A seated player's torch burns low, here and on every crowd row that draws this player. TorchBrightness = Stance == ELanternStance::Sitting ? 0.3f : 1.f; Torch->SetIntensity(TorchBrightness * 2000.f); } ``` Bind `OnSit` to the sit key. 1. In `BP_LanternPlayer`, add the **X** key event. From **Pressed**, add **Is Crowdy Entity Locally Controlled** and a **Branch**, so only the pawn you control writes. 2. On true, add a second **Branch** on an **Equal (Enum)** that compares **Get Stance** to `Sitting`. On true, call **Set Stance** with `Standing`. On false, call **Set Stance** with `Sitting`. [Blueprint snippet: mass-sit-stance, see /bp/unreal-sdk/mass-sit-stance.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. Then, after either **Set Stance**, call **Set TorchBrightness** with a **Select Float** on whether **Get Stance** now equals `Sitting`: 0.3 if so, 1.0 if not. Call **Set Intensity** on the `Torch` component with **Get TorchBrightness** times 2000. Press the key in one window. In the other, the row sits within a moment. Press it again and the row stands and walks. Set `Stance` to `Fallen` from your own knock-down code in the same way. See [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md). Each pose needs its own slot in `TP_LanternPlayer`. If a pose shares a slot with a gait, a player who leaves the pose stays frozen in it, and the anim set's validation says so when the set has a **Bank**. ## Wave the torch and cheer An action is a one-shot that plays once and returns to walking. The anim set gets two rows under **Actions**, in this order: | Action Name | Sequence | |---|---| | `Wave` | the wave clip | | `Cheer` | the cheer clip | Press **Sync Actions Into Bank**. It adds each clip to `TP_LanternPlayer` twice and fills in **Primary Index** and **Alternate Index** for both rows. Action ids start at 1 and follow the row order, so `Wave` is 1 and `Cheer` is 2. ### The wave: an event marked an action `WaveTorch` is a Crowdy event on `ALanternPlayer`. It goes to everyone in range, it is marked an action, and it takes `ActionId`, `Variant` and `PlayRate`. Marked like that, a machine that holds the player as a row starts the matching action row with no code written for it. ```cpp // LanternPlayer.h #include "Replication/RPC/CrowdyEvent.h" UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "SpatialMulticast", CrowdyAction)) void WaveTorch_Implementation(int32 ActionId, int32 Variant, float PlayRate); CROWDY_EVENT(WaveTorch) ``` The body plays the same action on a machine that holds the player as a real actor. The wave key calls `WaveTorch` with the id of `Wave`: ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" #include "Mass/CrowdyMassActionLibrary.h" void ALanternPlayer::WaveTorch_Implementation(int32 ActionId, int32 Variant, float PlayRate) { UCrowdyMassActionLibrary::PlayCrowdyMassAction(GetMesh(), ActionId, Variant, PlayRate); } void ALanternPlayer::OnWave() { // Action ids are one-based rows of the anim set, and Wave is its first action. WaveTorch(1, 0, 1.f); } ``` Bind `OnWave` to the wave key. 1. In the Event Graph of `BP_LanternPlayer`, add a **Custom Event** named `WaveTorch`. 2. In Details, tick **Crowdy Replicates**, set **Recipient** to **Spatial Multicast** and tick **Is A One-Shot Action**. 3. Press **Add Parameters**. It creates the `ActionId`, `Variant` and `PlayRate` pins. 4. Compile. The marker is read off the compiled event, so recompile after you tick the box. 5. From the event, add **Play Crowdy Mass Action**. Wire the **Mesh** component to its **Mesh** pin, and `ActionId`, `Variant` and `PlayRate` to the matching pins. [Blueprint snippet: mass-wave-body, see /bp/unreal-sdk/mass-wave-body.txt] Then, on the **V** key, call `WaveTorch` with `ActionId` 1, `Variant` 0 and `PlayRate` 1. One call, three outcomes. On a machine that holds the player as a real actor, the body plays the clip. On a machine that holds a crowd row, or a near tier actor, the body runs on a stand-in and the declared action plays on the row. The near tier actor shows the same action through its animation Blueprint. The mesh's animation graph needs a Slot node named `DefaultSlot`, which the stock mannequin graph already has. [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md) owns the declaration and the body, and [Run your code on a crowd row](../run-your-code-on-a-crowd-row.md) explains the stand-in. ### The cheer: send an action by name A cheer needs no event of its own. The inspect key traces from the camera and keeps the hit's `Target` when it hit a Mass entity. The cheer key sends the action by name to the player you kept. ```cpp // LanternPlayer.h #include "Target/CrowdyMassEntityTarget.h" FCrowdyMassEntityTarget TracedTarget; // LanternPlayer.cpp #include "LanternPlayer.h" #include "Engine/EngineTypes.h" #include "GameFramework/Controller.h" #include "Trace/CrowdyMassTraceBlueprintLibrary.h" void ALanternPlayer::OnInspect() { const AController* ViewController = GetController(); if (!ViewController) { return; } FVector Eye; FRotator Look; ViewController->GetPlayerViewPoint(Eye, Look); FCrowdyMassTraceHit Hit; UCrowdyMassTraceBlueprintLibrary::CrowdyMassLineTraceSingle(this, Eye, Eye + Look.Vector() * 3000.f, UEngineTypes::ConvertToTraceType(ECC_Visibility), false, TArray(), EDrawDebugTrace::ForDuration, Hit); if (!Hit.bHitMassEntity) { return; } TracedTarget = Hit.Target; } ``` ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" #include "Target/CrowdyMassTargetLibrary.h" void ALanternPlayer::OnCheer() { if (!UCrowdyMassTargetLibrary::IsTargetLive(TracedTarget)) { return; } UCrowdyMassTargetLibrary::SendEntityAction(TracedTarget, TEXT("Cheer"), 0, 1.0f); } ``` Bind `OnInspect` to the inspect key and `OnCheer` to the cheer key. To mark a player: 1. Add a variable named `TracedTarget` of type **Crowdy Mass Entity Target** to `BP_LanternPlayer`. 2. Add the **I** key event, and from **Pressed** add **Crowdy Mass Line Trace Single**. Set **Trace Channel** to **Visibility** and **Draw Debug Type** to **For Duration**. 3. For **Start**, add **Get Player Camera Manager** and drag off it to **Get Camera Location**. Drag off the camera manager again for **Get Camera Rotation**, then **Get Forward Vector**. Multiply the forward vector by 3000, add **Start**, and connect the sum to **End**. 4. Break **Out Hit** and branch on **Hit Mass Entity**. It is true for a row and for a near tier actor alike. On true, add **Set TracedTarget** from the **Target** pin. [Blueprint snippet: mass-trace-target, see /bp/unreal-sdk/mass-trace-target.txt] To cheer at them: 1. Add the **C** key event. From **Pressed**, add **Is Crowdy Mass Target Live** on `TracedTarget` and a **Branch**. 2. On true, add **Send Crowdy Action**. Wire `TracedTarget` into **Target** and type `Cheer` as the **Action Name**. [Blueprint snippet: mass-send-action, see /bp/unreal-sdk/mass-send-action.txt] The name is resolved against the anim set of the class the target stands for, and only the id travels. Everyone who draws that player as a row or a near tier actor sees the cheer, you included. The cheered player holds themselves as a real actor, so they do not see it. The action is a visual, so any client may send it. See [Target an entity, from Blueprint](../from-blueprint/target-an-entity.md) and [Target an entity, from C++](../from-cpp/target-an-entity.md), then [Call an event on it, from Blueprint](../from-blueprint/call-an-event-on-it.md) and [Call an event on it, from C++](../from-cpp/call-an-event-on-it.md). ## Give the players close by a real actor A row has no animation graph, no bones to attach to and no real components. The players standing close to you should have them. That is the near tier. [Promote close entities to actors](../promote-close-entities-to-actors.md) explains each step. 1. Create a Blueprint class from **Crowdy Mass Near Tier Actor** and name it `BP_LanternPlayer_Shell`. 2. Create an Animation Blueprint for the character's skeleton and name it `ABP_LanternPlayer_Shell`. It is its own graph and not a copy of the character's, because **Get Owning Actor** returns the near tier actor and a cast to the character fails. Build it like this: - In **Event Blueprint Initialize Animation**, call **Get Owning Actor**, then **Get Component By Class** with **Crowdy Mass Near Tier**, and keep the result in a variable named `NearTier`. - In **Blueprint Thread Safe Update Animation**, set a `Speed` float from **Get Smoothed Speed** on `NearTier`, and drive the locomotion blend space from `Speed`. - Choose the sitting and fallen states from **Get Pose Name**, comparing against `Sitting` and `Fallen`. - Route the output through a Slot node named `DefaultSlot`, then to **Output Pose**. `Wave` and `Cheer` play over whatever the graph shows. - Add a **Blend Poses by bool** before the Slot node, driven by **Has Pose** and **Get Pose Name** equal to `Fallen`, so the graph skips the slot while the player is down. The near tier does not apply **Above Actions** for you. 3. On the shell's `Mesh` component, set the same **Skeletal Mesh** as `BP_LanternPlayer` and set **Anim Class** to `ABP_LanternPlayer_Shell`. Author nothing else. An empty event graph is the normal shape. 4. In `BP_LanternPlayer`, add a **Crowdy Mass Near Tier Shell** component and set **Near Tier Actor Class** to `BP_LanternPlayer_Shell`. 5. In **Project Settings, Plugins, Crowdy Mass**, under **Near Tier**, keep **Near Tier Distance** at 1000 and **Near Tier Visible Distance** at 2000. Set **Max Near Tier Actors** to 12, lower than the default of 50, so you can see the ceiling. The twelve nearest players win a real actor, and the rest of the square stays rows. A C++ animation instance can read the same getters instead of a Blueprint graph. Do not add a movement component, a colliding capsule or a Crowdy Entity component to the shell. Turn on `crowdy.mass.neartier.trace 1` and walk up to another player: the log says when the player gains a near tier actor and whether it came from the pool. ## Brighten the torch Every player has a torch, and the crowd has to know how bright it is. That is crowd data of the game's own. The recipe is a replicated property, a fragment, a trait, a processor and a registered field. On `ALanternPlayer`, `TorchBrightness` is a `float` property marked `CrowdyState` and `CrowdyHeartbeat`. It is declared in the snippet further down. In a Blueprint class, add a `float` variable of that name, set **Replication** to **Replicated** and leave **Keyframe heartbeat** ticked. The sit key is the only thing that sets it. A row has no actor to write it to, so the value is decoded and dropped unless a field claims it. The fragment holds `TargetBrightness`, the value that arrived, and `Brightness`, the value the processor eases toward it, widest member first: ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "Mass/EntityElementTypes.h" #include "LanternCrowd.generated.h" // Wider members come first: this fragment is read for every crowd row every frame. USTRUCT() struct FLanternTorchFragment : public FMassFragment { GENERATED_BODY() UPROPERTY() float TargetBrightness = 1.f; UPROPERTY() float Brightness = 1.f; }; static_assert(sizeof(FLanternTorchFragment) == sizeof(float) + sizeof(float), "FLanternTorchFragment has gained padding. Declare wider members before narrower ones."); ``` The trait adds the fragment to the entity's template: ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "MassEntityTraitBase.h" #include "LanternCrowd.generated.h" UCLASS(EditInlineNew, CollapseCategories, meta = (DisplayName = "Lantern Torch")) class ULanternTorchTrait : public UMassEntityTraitBase { GENERATED_BODY() public: virtual void BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const override; // Copied onto every entity built from this trait. UPROPERTY(EditAnywhere, Category = "Lantern") float StartingBrightness = 1.f; }; // LanternCrowd.cpp #include "LanternCrowd.h" #include "MassEntityTemplateRegistry.h" void ULanternTorchTrait::BuildTemplate(FMassEntityTemplateBuildContext& BuildContext, const UWorld& World) const { FLanternTorchFragment& Torch = BuildContext.AddFragment_GetRef(); Torch.TargetBrightness = StartingBrightness; Torch.Brightness = StartingBrightness; } ``` Add `ULanternTorchTrait` to the **Traits** of a **Crowdy Mass Entity Traits** component on `BP_LanternPlayer`. The processor eases `Brightness` toward `TargetBrightness` each frame: ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "MassEntityQuery.h" #include "MassProcessor.h" #include "LanternCrowd.generated.h" UCLASS() class ULanternTorchFadeProcessor : public UMassProcessor { GENERATED_BODY() public: ULanternTorchFadeProcessor(); protected: virtual void ConfigureQueries(const TSharedRef& EntityManager) override; virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override; private: FMassEntityQuery EntityQuery; }; // LanternCrowd.cpp #include "LanternCrowd.h" #include "MassExecutionContext.h" ULanternTorchFadeProcessor::ULanternTorchFadeProcessor() : EntityQuery(*this) { // A dedicated server has nothing to draw. ExecutionFlags = static_cast(EProcessorExecutionFlags::Client | EProcessorExecutionFlags::Standalone); ProcessingPhase = EMassProcessingPhase::PrePhysics; bRequiresGameThreadExecution = false; } void ULanternTorchFadeProcessor::ConfigureQueries(const TSharedRef& EntityManager) { EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); } void ULanternTorchFadeProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) { EntityQuery.ForEachEntityChunk(Context, [](FMassExecutionContext& Context) { const TArrayView Torches = Context.GetMutableFragmentView(); const float DeltaTimeSeconds = Context.GetDeltaTimeSeconds(); for (FMassExecutionContext::FEntityIterator EntityIt = Context.CreateEntityIterator(); EntityIt; ++EntityIt) { FLanternTorchFragment& Torch = Torches[EntityIt]; Torch.Brightness = FMath::FInterpConstantTo(Torch.Brightness, Torch.TargetBrightness, DeltaTimeSeconds, 2.f); } }); } ``` `ULanternCrowdSubsystem` is a world subsystem. When the world's subsystems initialize, it registers the state field, which lands an arriving `TorchBrightness` on `TargetBrightness`, and the Blueprint field, which makes the same name readable and writable from a graph. Both registrations point at `TargetBrightness`, so a Blueprint read returns the value that arrived, and a Blueprint write sets a new target the crowd eases toward until the next replicated value replaces it. ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "LanternCrowd.generated.h" UCLASS() class ULanternCrowdSubsystem : public UWorldSubsystem { GENERATED_BODY() public: virtual bool ShouldCreateSubsystem(UObject* Outer) const override; virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void OnWorldBeginPlay(UWorld& InWorld) override; }; // LanternPlayer.h UPROPERTY(BlueprintReadOnly, Category = "Lantern", meta = (CrowdyState, CrowdyHeartbeat)) float TorchBrightness = 1.f; // LanternCrowd.cpp #include "LanternCrowd.h" #include "Subsystems/CrowdyMassEntitySubsystem.h" #include "Target/CrowdyMassFieldAccess.h" bool ULanternCrowdSubsystem::ShouldCreateSubsystem(UObject* Outer) const { // Only where the Mass subsystem is created: a play world whose map profile selects the Mass backend. return Super::ShouldCreateSubsystem(Outer) && GetDefault()->ShouldCreateSubsystem(Outer); } void ULanternCrowdSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); // The transient world at engine start has no game instance yet. if (!GetWorld()->GetGameInstance()) { return; } UCrowdyMassEntitySubsystem* Mass = Collection.InitializeDependency(); if (!Mass || Mass->IsStateFragmentFieldRegistered(TEXT("TorchBrightness"))) { return; } Mass->RegisterStateFragmentField(TEXT("TorchBrightness"), ECrowdyMassStateFieldType::Float, [](const void* PropertyValuePtr, const FCrowdyMassViewStateContext& Context) { FLanternTorchFragment* Torch = Context.GetFragment(); if (!Torch || !PropertyValuePtr) { return; } Torch->TargetBrightness = *static_cast(PropertyValuePtr); }); CrowdyMassFields::RegisterField(TEXT("TorchBrightness"), TEXT("TargetBrightness")); } ``` The class declaration in that snippet lists both overrides. The next section defines `OnWorldBeginPlay`. ### Flare the torch when a wave arrives This step is optional. It shows what a registered handler does. A registered C++ handler for `WaveTorch` replaces both the event's body and the automatic action write on a crowd row. The row still waves only if the handler writes the action itself. The Blueprint node replaces the body only, so the row still waves. The handler flares the torch and writes the wave action itself. It is the `OnWorldBeginPlay` that `ULanternCrowdSubsystem` declares above. ```cpp // LanternCrowd.h #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "LanternCrowd.generated.h" // LanternCrowd.cpp #include "LanternCrowd.h" #include "Fragments/CrowdyMassAnimFragments.h" #include "LanternPlayer.h" #include "Mass/CrowdyMassActionEvents.h" #include "Subsystems/CrowdyMassEntitySubsystem.h" void ULanternCrowdSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); UCrowdyMassEntitySubsystem* Mass = InWorld.GetSubsystem(); const UFunction* WaveFunction = ALanternPlayer::StaticClass()->FindFunctionByName(TEXT("WaveTorch_Implementation")); if (!Mass || !WaveFunction) { return; } // A registered handler replaces the event body and the automatic action write on a crowd row, so it writes the action itself. Mass->RegisterEventHandler(WaveFunction, [](const FCrowdyEventParams&, const FCrowdyMassViewStateContext& Context) { if (FLanternTorchFragment* Torch = Context.GetFragment()) { Torch->TargetBrightness = 1.f; } FCrowdyMassActionFragment* Action = Context.GetFragment(); if (!Action) { return; } CrowdyMassWriteAction(*Action, 1, 0, 1.f); }); } ``` 1. In an event graph that runs once per machine, such as the game state, add **Receive Crowdy Event on Entity** and pick `WaveTorch` in its **Event** dropdown. 2. Wire **Event BeginPlay** into **Bind**. 3. From **On Event**, add **Is Crowdy Mass Target Live** on the node's **Target** and a **Branch**. 4. On true, add **Set Crowdy Mass Field (Float)** with **Target** from the node, **Field Name** `TorchBrightness` and **Value** 1.0. [Blueprint snippet: mass-receive-event, see /bp/unreal-sdk/mass-receive-event.txt] :::note[The row still waves.] A bound Receive Crowdy Event on Entity node replaces the event's body on a row, but the declared action is still written, so the row plays the wave. The C++ handler replaces the action write too, so it writes the wave itself. See [Receive its events, from Blueprint](../from-blueprint/receive-its-events.md) and [Receive its events, from C++](../from-cpp/receive-its-events.md). ::: ### Read the brightness Landing a value makes it available, so a graph reads it by name. Look at a player with the inspect key. Each frame the HUD checks that the traced target is still live, reads the field and puts the value into **Set Percent** on a Progress Bar named `TorchMeter`. Call `UCrowdyMassTargetLibrary::IsTargetLive(TracedTarget)`. When it is true, call `UCrowdyMassTargetLibrary::GetFieldFloat(TracedTarget, TEXT("TorchBrightness"), Brightness)` and pass `Brightness` to the meter. On the HUD's tick, add **Is Crowdy Mass Target Live** on the traced target and a **Branch**. On true, add **Get Crowdy Mass Field (Float)** with the **Field Name** `TorchBrightness`. Wire its **Out Value** into **Set Percent** on `TorchMeter`. When the other player sits, the meter drops to 30 percent. When they stand it returns to 100. Run `crowdy.mass.state.verdict` if the meter never moves. A `TorchBrightness` that no field claims shows in its dropped list. See [Add your own crowd data](../add-your-own-crowd-data.md), [Read and write its fields, from Blueprint](../from-blueprint/read-and-write-its-fields.md) and [Read and write its fields, from C++](../from-cpp/read-and-write-its-fields.md). ## The raiders on the north road The warband from [Raiders on the north road](../../exec/examples/raiders-on-the-north-road.md) is far more than a village can draw as actors, and each raider already keeps a record on the server. On the Oakford map `BP_Raider` is drawn as a row too, and a row reads its raider's record. `BP_Raider` is a Crowdy entity with a **Crowdy Server Object** component whose **Definition** is `CSO_Raider` and whose **Instance Mode** is **This Actor's Area**, as [the actors](../../exec/examples/raiders-on-the-north-road.md#the-actors) section of that page sets up. For a row, `BP_Raider` needs the same treatment as the player in the steps above: a flagged material and a `crowdy.mass.derive` check. A client that never spawns a `BP_Raider` itself cannot resolve the class from the network. Add `BP_Raider` to **Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes**. See [Entities and Spawning](../../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all). With **This Actor's Area**, the game follows the areas around the local player's pawn or camera, as many as **Areas Followed Around a Player** says, which is 1 on `CSO_Raider`. A raider outside those areas has no values to read. The raider's `Health` is visible to Everyone, so every row can read it. Its `Loot` is Owner Only and its `Hideout` Team Only, so a row yields them only for a player who may see them. Mark a raider with the inspect key (I). The health key (H) reads its `Health` and turns your own torch red when the raider is down to 10 or less, and white again otherwise. ```cpp // LanternPlayer.cpp #include "LanternPlayer.h" #include "Target/CrowdyMassServerValuesLibrary.h" void ALanternPlayer::OnCheckHealth() { int32 Health = 0; if (!UCrowdyMassServerValuesLibrary::GetServerValueAs(TracedTarget, TEXT("Health"), Health)) { return; } // A raider on its last legs turns the torch red. Torch->SetLightColor(Health <= 10 ? FLinearColor::Red : FLinearColor::White); } ``` Bind `OnCheckHealth` to the health key. 1. Add an Integer variable named `Health` to `BP_LanternPlayer`. 2. Add the **H** key event. From **Pressed**, add **Get Server Value** with `TracedTarget` as **Target** and the **Name** `Health`. Connect **Value** to **Set Health**. 3. Branch on the node's return value. On true, call **Set Light Color** on the `Torch` component. 4. Feed **New Light Color** from a **Select Color** with red in **A** and white in **B**. Connect `Health` to a `<=` node with 10 in its second input, and connect the result to **Pick A**. The torch turns red at 10 or less, and white otherwise. [Blueprint snippet: mass-server-health, see /bp/unreal-sdk/mass-server-health.txt] **Get Server Value** returns true only for a value the server sent that fits the type of **Value**. The pose, the wave and the torch are view state: a modified client can lie about them, and the worst it can do is change what you see. `Health` is a server value, so it is the truth. Decide what is drawn from view state, and what is true from server values. See [Read its server values, from Blueprint](../from-blueprint/read-its-server-values.md), [Read its server values, from C++](../from-cpp/read-its-server-values.md) and [Crowd rows](../../exec/keep-a-record-per-entity.md#crowd-rows). A record can take a moment to arrive for a raider that has just come into view, and Owner Only values arrive later than Everyone values. The first press can answer false. ## Test with two players in Play In Editor 1. Play the Oakford map. In **Advanced Settings** under **Multiplayer Options**, set **Number of Players** to 2 and click **Play**. The map you play must be the one mapped to `DA_OakfordProfile`. Sign both windows in. 2. In each window, the other player stands in the square as a row of `BP_LanternPlayer`, in its own colours and not grey. Walk one: the other window shows `Walk` at a walk and `Run` at a run. 3. Press the sit key (X) in the first window. The other window shows the player sit. Press it again and they stand. 4. Press the wave key (V). The other window shows the wave, even while the player sits. Press it twice quickly: the wave restarts. 5. In the second window, look at the first player, press the inspect key (I), then the cheer key (C). The second window shows the cheer on that row. The first window does not: it holds its own player as a real actor, and Send Crowdy Action only reaches machines that draw the player as a row or a near tier actor. A third window shows the cheer too. 6. Walk the first player to within 1000 cm (10 metres) of the second. The second window now shows a near tier actor with the full animation graph. Walk away and it goes back to a row. 7. With the first player marked in the second window, press the sit key (X) in the first window. The `TorchMeter` drops to 30 percent. Stand up and it returns to 100. 8. In the second window, look at a raider, press the inspect key (I), then the health key (H). The raider starts at 40 health and loses 5 every 5 seconds, so your torch turns red once it is down to 10 or less. At 0 health it retreats and returns at full health. If one of these does not happen, [Fix a crowd that does not look right](../troubleshooting.md) has a checklist for it. A good first order is `crowdy.mass.derive`, then one client with **Render Local Owner**, then two clients. ## Related - [Draw a crowd with Mass](../draw-a-crowd-with-mass.md): what the backend is and when to use it - [Turn Mass on for a map](../turn-mass-on-for-a-map.md): the profile, the settings and the material flag - [Make a character draw as a crowd row](../make-a-character-draw-as-a-crowd-row.md): the entity component, the mesh and `crowdy.mass.derive` - [Set up the animation set](../set-up-the-animation-set.md): the bank, gaits and the slot pairs - [Play poses and one-shot actions](../play-poses-and-one-shot-actions.md): Stance, `WaveTorch` and Send Crowdy Action - [Promote close entities to actors](../promote-close-entities-to-actors.md): the shell and the animation graph - [Add your own crowd data](../add-your-own-crowd-data.md): the torch fragment, trait and processor - [Raiders on the north road](../../exec/examples/raiders-on-the-north-road.md): the Server Object behind the raiders - [Fix a crowd that does not look right](../troubleshooting.md): symptoms and the console commands # What a Server Object is Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/overview # What a Server Object is :::note Server Objects run on [ck-exec](/exec/intro). You can use them from Blueprint or from C++; see [From Blueprint](./from-blueprint/get-a-server-object.md) and [From C++](./from-cpp/get-a-server-object.md). ::: A Server Object is a piece of game state that lives on the server and that every player sees the same way. The server keeps its values, saves them, and is the only thing that changes them. Your game reads the values it is allowed to see, is told each time they change, and asks for a change by calling one of its **Server Functions**. The server decides, and every player watching sees the result. That makes a Server Object the place for state a player could gain by lying about: a boss's health, a shared stockpile, a match's score, a player's inventory. It is the SDK's door into the [truth plane](../concepts/two-planes.md). Fast view state (movement, animation, a light flickering) stays on the view plane, in [Crowdy State](../runtime/crowdy-state.md). ## In the example world The village square has a beacon every villager feeds with oil. How much oil it holds is the kind of value a client must not decide for itself, so it is a Server Object: one per village. When a player walks into the beacon, the game calls the beacon's `FeedBeacon` Server Function; the server adds the oil (or refuses), and every player in the village sees the beacon's light change with the new amount. The pages in this section build that beacon, `AVillageBeacon`, one piece at a time. ## How the parts fit - **A definition asset** (`UCrowdyServerObjectDefinition`) describes one type of Server Object: the variables the server keeps, which of them players may see, and the functions they may call. You make one asset per type, in an editor laid out like the Blueprint editor. See [Create a Server Object type](./create-a-type.md). - **Access, members and timers** are settings on the asset that the server enforces before your code runs: who may read and call, how often, members and a leader, timers, and calling other types. See [Access, members and timers](./access-members-and-timers.md), and [Relate Server Objects to each other](./relate-server-objects.md) for which to use when one object belongs to a player, lists others or is shared by many. - **Messages** are what the server code sends to everyone listening when something happens, such as a boss falling. Players hear them in a game, and other Server Object types hear them in their own server code. A message is an event: nothing keeps it. See [Server messages](./create-a-type.md#server-messages). - **A Server Function Library** is a type with functions and nothing to remember, like a Blueprint Function Library: logic that many callers share, with no instance of its own, answered by several instances at once. You set the asset's **Kind** to make one. See [Make a Server Function Library](./server-function-libraries.md). - **A record per entity** is a setting on the asset, **Holds**, for a crowd: the same variables kept once for each entity, with some visible to everyone, some to the entity's owner and some to its team. See [Keep a record per entity](./keep-a-record-per-entity.md). - **The server code** is a Rust crate the editor generates from the definition asset; you fill in what each Server Function does. See [Write its server logic](./write-server-logic.md), then [Deploy it with Server Compute](./deploy-with-server-compute.md). - **An Instance Id** picks one Server Object of that type. The beacon uses the village's name, so each village has its own. - **From Blueprint**, the **Crowdy Server Object** component and the typed nodes get the object, read and follow its variables, hear its messages and call its functions. See [Get a Server Object](./from-blueprint/get-a-server-object.md), [Read and follow its variables](./from-blueprint/read-and-follow-variables.md), [Hear its messages](./from-blueprint/hear-server-messages.md), [Call its functions](./from-blueprint/call-functions.md), [Call a Server Function Library](./from-blueprint/call-a-library.md) and [Pins for every type](./from-blueprint/pins-for-every-type.md). - **From C++**, `UCrowdyServerObjectSubsystem`, a game instance subsystem, hands out Server Objects. Every actor or object that acquires the same definition and Instance Id shares one `UCrowdyServerObject`, and the subsystem gives it back a short while after the last of them is gone. The variables players can see arrive in a copy of the state struct and raise one change event per server change, and Server Functions are called with a params struct and answer with a reply struct or an outcome that says what went wrong. See [Get a Server Object](./from-cpp/get-a-server-object.md), [Read and follow its variables](./from-cpp/read-and-follow-variables.md), [Hear its messages](./from-cpp/hear-server-messages.md), [Call its functions](./from-cpp/call-functions.md) and [Call a Server Function Library](./from-cpp/call-a-library.md). The platform has its own words for all of this (hubs, keys, methods, topics). You do not need them to use the SDK, but the platform's pages and logs use them; [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) maps one to the other. ## What you need 1. **Your app on the Dev backend.** Server Objects connect with the signed-in player's app token, like the rest of the runtime. See [Player sign-in](../runtime/player-sign-in.md). 2. **The module dependency, for C++.** Blueprint needs none. Add `CrowdyExec` to your game module's `Build.cs`, next to the modules from [Installation](../installation.md#step-4-add-the-module-dependencies): ```csharp PrivateDependencyModuleNames.Add("CrowdyExec"); ``` 3. **A definition asset per type**, described on [Create a Server Object type](./create-a-type.md). 4. **The type's server code, deployed to your app on ck-exec.** The editor generates it from the definition asset, so the server and your game agree on every name and field, and you write only what each Server Function does. The Server Compute page in Crowdy Studio sends it to your app. See [Deploy it with Server Compute](./deploy-with-server-compute.md). ## What is next Describe the beacon's state and functions in [Create a Server Object type](./create-a-type.md), set who may do what in [Access, members and timers](./access-members-and-timers.md), see how types relate in [Relate Server Objects to each other](./relate-server-objects.md), write what its functions do in [Write its server logic](./write-server-logic.md), and [deploy it](./deploy-with-server-compute.md). Then use it [from Blueprint](./from-blueprint/get-a-server-object.md) or [from C++](./from-cpp/get-a-server-object.md). When something does not behave, [Troubleshooting](./troubleshooting.md) has the checks, and [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) maps the SDK's words to the platform's. If your project used the SDK's Game Models, which were removed in v2.19.0, [Move from Game Models](./move-from-game-models.md) says what to do before you upgrade and what to use instead. A crowd of entities, each with values of its own, is [Keep a record per entity](./keep-a-record-per-entity.md). The worked examples, a tip jar, guild halls and an arena, a boss every player hits, a warband of raiders, and a loot roller for the boss and a chest, are together under [Examples](./examples/tip-jar.md). # Create a Server Object type Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/create-a-type # Create a Server Object type :::note Server Objects run on [ck-exec](/exec/intro). ::: Each type of [Server Object](./overview.md) is described by one data asset of the class `UCrowdyServerObjectDefinition`. The asset names the variables the server keeps, which of them players may see, the functions players may call, and the messages the server sends to everyone listening. The SDK reads it at runtime to encode and decode every value, and the type's server code is generated from it, so both sides agree on every name. A type that needs functions only, with nothing to remember between calls, sets its **Kind** to **Server Function Library**; see [Make a Server Function Library](./server-function-libraries.md). The rest of this page is about the default, **Server Object**. ## Make the asset In the Content Browser, choose **Add**, then **Crowdy**, then **Server Object**. The new asset is named `CSO_NewServerObject`; rename it after the type, for example `CSO_VillageBeacon`, and double-click it. Server Object assets take the prefix `CSO_` (Crowdy Server Object), the way Blueprints take `BP_`; an older asset named `DA_` works the same. **Add**, **Miscellaneous**, **Data Asset**, then **Crowdy Server Object Definition** makes the same asset. It opens in an editor laid out like the Blueprint editor (see [The asset editor](#the-asset-editor)), with the type's variables, functions and messages in the **Server Object** panel and whatever you select there in **Details**. The village beacon keeps its oil, whether it is lit, and who fed it last; feeding it sends an amount of oil and gets back how much the beacon holds afterwards. 1. **Add the variables.** In the **Variables** section of the Server Object panel, clear **Use Struct** in its header if it is ticked, then click **+**. A new variable is an Integer named `NewVar`, and it is selected, so Details shows it. Set its **Variable Name** to `Oil` and its starting value, under **Default Value**, to 50. Click **+** twice more for `bLit`, a Boolean that starts ticked, and `LastFedBy`, an Integer64 that starts at 0; change each one's type with **Variable Type**. 2. **Choose what players see.** Each variable has an eye at the end of its row. Leave it open on `Oil` and `bLit`: **Visible to Players**, so players can read and watch them. Click it on `LastFedBy` to close it: **Server Only**, so it never leaves the server. 3. **Add the function.** Click **+** in the **Functions** section. The new function, `NewFunction`, is selected; set its **Name** to `FeedBeacon` and leave **Callable By** on **Players**. Click **+** in the **Inputs** header, rename the new `NewParam` to `Oil`, leave it an Integer and give it the default value 10. Click **+** in the **Outputs** header and name that one `Oil` too. 4. **Name the type.** Select **Type Settings**, the first row of the Server Object panel, and Details shows the type's own settings: set **Type Name** to `village_beacon`, the **Description**, and **Readable By**. The panel now reads: | Server Object panel | The beacon | |---|---| | Variables | `Oil` Integer (open eye), `bLit` Boolean (open eye), `LastFedBy` Integer64 (closed eye) | | Functions | `FeedBeacon` with `(Oil) returns Oil` beside it | and, with **Type Settings** selected, Details: | Setting | The beacon's value | |---|---| | Type Name | `village_beacon` | | Description | Keeps the village beacon's oil and whether it is lit. | | Readable By | Every Player | ![With Type Settings selected, Details shows Type Name, Kind, Description, Save Every and Stop When Idle For, then the Access, Platform Data (folded), Members, Areas, Timers & Events, Can Call and Listen To categories](/img/unreal-sdk/server-object-type.png) Then pick the asset in the beacon's **Beacon Definition** slot in the level's Details panel; see [Get a Server Object](./from-cpp/get-a-server-object.md). ## Variables, inputs and outputs, or a struct The variables, and each function's inputs and outputs, can each be added right in the asset, as above, or taken from a `USTRUCT` of your own. Tick **Use Struct** in the section's header to take a struct: for the Variables, pick it as **State Struct** in Details with **Type Settings** selected; for a function's Inputs or Outputs, the list gives way to an **Input Struct** or **Output Struct** picker, where None sends or replies nothing. The beacon's structs: ```cpp // VillageBeacon.h // Everything the server keeps for one village's beacon; only the watched fields ever reach a player. USTRUCT(BlueprintType) struct FVillageBeaconState { GENERATED_BODY() UPROPERTY() int32 Oil = 50; UPROPERTY() bool bLit = true; UPROPERTY() int64 LastFedBy = 0; }; USTRUCT(BlueprintType) struct FFeedBeaconParams { GENERATED_BODY() UPROPERTY() int32 Oil = 10; }; USTRUCT(BlueprintType) struct FFeedBeaconReply { GENERATED_BODY() UPROPERTY() int32 Oil = 0; }; ``` With **Use Struct** ticked, the Server Object panel lists the struct's fields as the variables, each with its type and eye, but they are renamed and retyped in the struct, not in the asset. Both travel exactly alike, so the server code and the saved data do not care which you chose. The generated server code gives the ones added in the asset the names the structs would have, `VillageBeaconState`, `FeedBeaconParams` and `FeedBeaconReply`, so the beacon's `logic.rs` is the same either way. The C++ on the following pages uses these structs. With variables, inputs and outputs added in the asset there is no struct, so C++ reads and sends them by name instead: see [Read and follow its variables](./from-cpp/read-and-follow-variables.md#read-the-values) and [Call its functions](./from-cpp/call-functions.md#call-one-with-inputs-added-in-the-asset). Keep a struct when the same values are shared by several types or functions, when you may rename a field and want to keep its saved value with [Field Names](#every-setting), or when your C++ uses the struct elsewhere. ## The asset editor Double-click a definition in the Content Browser and it opens in its own editor, laid out like the Blueprint editor and using its words, not as a plain property list. The editor has a toolbar and three tabs. ![The CSO_VillageBeacon editor: Server Code on the left, the Server Object panel (Type Settings, Variables, Functions and Server Messages) and the Details of FeedBeacon on the right, Generate and Deploy in the toolbar](/img/unreal-sdk/server-object-editor.png) ### The toolbar After the usual **Save** and **Browse** buttons the toolbar holds: - **Type Settings**, a toggle that selects the **Type Settings** row of the Server Object panel from anywhere, so Details shows the type's own settings. - **Generate** writes the type's server code, the same as the Content Browser command. Its tooltip reads "Writes this type's server code under `Server/`; your logic file is never replaced". - **Deploy** builds and deploys every Server Object type in the project together, since a deploy replaces the app's whole version, and asks first. See [Server Compute](./deploy-with-server-compute.md#deploy). - A small arrow beside Deploy opens a menu with two entries. **Open Server Compute** opens [Crowdy Studio](../studio/overview.md) on the Server Compute page itself (it opens on Sign In first when nobody is signed in, and shows the page after). **Show build output** opens a **Build Output** window with what the last build of this session printed; it is disabled until a build has printed something. - A **status readout** at the right: a coloured dot and one line saying where the type stands. Hover it for the reason. Generate is disabled until the Type Name is valid. Deploy is disabled until the Type Name is valid, while another definition has the same Type Name, while another operation runs, and while nothing in the project has changed since the live version. While a deploy runs, its progress line takes the place of the status readout. The readout says: | Readout | Meaning | |---|---| | **Ready** | The server code on disk matches this definition. It is compared with the live version once [Server Compute](./deploy-with-server-compute.md) has read it, which needs you signed in to Crowdy Studio. | | **No changes**, then "live in version 7" (green dot) | The live version runs this code with these settings. | | **Changed since version 7 (server code, save interval)** (yellow dot) | Version 7, the live one, runs something else. The brackets say what: server code, save interval or idle timeout. | | **Not deployed yet** (blue dot) | The live version does not run this type. Deploy adds it. | | **Live code unknown**, then "app version 7" (yellow dot) | The live version runs code this project has no record of, for example deployed from another copy of the project. | | **Needs generating**, **Out of date** (yellow dot) | The type's server code has to be generated, or generated again; see [Write its server logic](./write-server-logic.md#the-server-code-tab). | | **Problem: ...** (red dot) | The definition fails its checks, or another definition has the same Type Name (see [Two definitions with one Type Name](#two-definitions-with-one-type-name)). The reason follows the colon. | ### The three tabs - **Server Code**, the large tab on the left: the path of the Server Functions file, the Code Source choice (**Generated** or **My Own File**), the deployed revisions, and the code itself with line numbers, Rust colouring, **Save**, **Revert** and an external-editor button. It is described on [Write its server logic](./write-server-logic.md#the-server-code-tab). - **Server Object**, top right, where My Blueprint is in the Blueprint editor: the type's variables, functions and messages. - **Details**, bottom right: the settings of whatever is selected in the Server Object panel. The tabs dock and rearrange like any editor tab, for example to put the code above the rest; the editor remembers the layout. ### The Server Object panel The panel starts with a pinned row, **Type Settings**, with a gear icon. It is selected when the asset opens, and selecting it shows the type's own settings in Details. Below it are three sections. - **Variables**, with a **Use Struct** check-box and a **+** button in its header. Each row shows the variable's type as a coloured pill, the way a Blueprint variable does, its name, its type name (Integer, Boolean, String) and an eye. An open eye is **Visible to Players**: players can read and watch it. A closed eye is **Server Only**: it never leaves the server. Click the eye to switch it. With Use Struct ticked the + button is hidden and the rows are the struct's fields. - **Functions**, with a **+** button in its header. Each row shows the function's name and its pins the way its node reads, inputs in brackets, then "returns" and the outputs, so `FeedBeacon` reads `(Oil) returns Oil`; a struct shows as its name. A **Server Only** tag follows when only server code may call it (see [Callable By](#callable-by)). Double-click a function to jump the Server Code tab to its `fn` in `logic.rs`. When **Members From** is **This Object**, a **Built-in** section lists **Join**, **Leave**, **Add Member**, **Remove Member**, **Make Leader** and **Set Open for Joining**, greyed out: they cannot be edited or deleted. Each shows its inputs the way a function row does, such as **Add Member (Player)**. See [Members](./access-members-and-timers.md#members). - **Server Messages**, with a **+** button in its header, below the functions. Each row shows the message's name and its payload the way its Received node reads, so `BossDefeated` reads `(Winner, Hits)` beside it; a message that players cannot listen to carries "(server only)". See [Server messages](#server-messages). Right-click a section's header for **Add Variable** (while Use Struct is clear), **Add Function** or **Add Message**, a function or a message for **Duplicate** and **Delete**, and a variable added in the asset for **Delete**. The Delete key deletes the one selected. New variables are Integers named `NewVar`, then `NewVar_1` and on; new functions are named `NewFunction`, new messages `NewMessage`, and new inputs and outputs `NewParam`. A warning icon after a name means the variable, function or message fails a [check made when you save](#checks-when-you-save); hover it to read the problem. ### The Details tab Details follows the selection in the Server Object panel. **With Type Settings selected** it shows the type's own settings: **Type Name**, **Kind** (**Server Object** or **Server Function Library**; a library shows only **Type Name**, **Kind**, **Description**, **Platform Data**, **Can Call**, **Scaling** and **Server Limits**, and Details rebuilds when you change it), **Description**, **State Struct** (only when the Variables use a struct), **Save Every** and **Stop When Idle For**, both always in seconds (such as 300 s), and a collapsed **Advanced** group with **Server Names** (**Field Names** and **Enum Value Names**) and **Baked**. Six categories hold the settings for who may do what: **Access** (**Readable By**: **Every Player**, **Owner Only** or **Members**; **Only One Instance**), **Platform Data** (**Reads Players**, **Changes Players' App State**, **Reads World Data**, **Changes the World**, **Reads Grids**, **Manages Grids**, **Grants Grid Permissions**, **Reads Players' Access**, **Manages Players' Access**), **Members** (**Members From**, **Max Members**, **Show Members to Players**, **Remove Members Who Leave**), **Timers & Events**, **Can Call** and **Listen To**. They are described on [Access, members and timers](./access-members-and-timers.md). **Holds** sits with the type's other settings, and two more categories belong to [a record per entity](./keep-a-record-per-entity.md): **Records** (**Max Records**, **Send Changes Every**, **Team From**), shown only when **Holds** is **A Record per Entity**, and **Areas** (**Area Size**, **Areas Followed Around a Player**). A collapsed **Server Limits** category holds what the server allows each Server Object of the type: **Memory per Instance**, **Time per Call**, **Fuel per Call (Millions)** and **Calls Waiting**. When the type has [messages](#server-messages) that players can listen to and some players may read less than every player can, a **Server Messages** category with a warning follows. See [Every setting](#every-setting). **A variable** shows **Variable Name**, where you rename it, **Variable Type**, and **Visible to Players**, the same switch as the eye. In a type whose **Holds** is **A Record per Entity**, **Visible to Players** is a drop-down of who receives the variable: **Server Only**, **Everyone**, **Owner Only** or **Team Only**; the eye then switches between **Server Only** and **Everyone**. See [Who receives a value](./keep-a-record-per-entity.md#who-receives-a-value). Variable Type is Unreal's Blueprint type picker, offering only the types a Server Object can carry (see [Field types](#field-types)). Below them a **Default Value** section holds the variable's starting value, where a new Server Object starts. A variable from a struct shows its name and type read only; change them in the struct. ![Oil selected: Variable Name, Variable Type, Visible to Players and its Default Value](/img/unreal-sdk/server-object-variable.png) **A function** shows: - a **Function** section whose header reads the node line, for example `FeedBeacon (Oil) returns Oil`, with **Name**, the name your C++ passes to `Call`, **Callable By** (**Players**, **Members**, **Leader**, **Server Only** or **Developers Only**) and **Cooldown**, in seconds, and for a Server Only or Developers Only function a **Call as Developer...** button (see [Call a function as the developer](./deploy-with-server-compute.md#call-a-function-as-the-developer)); - **Inputs**, what the caller sends, and **Outputs**, what the server answers with. Each has a **Use Struct** check-box and a **+** button in its header, and lists its pins the Blueprint way: type pill, name and default value on each row, with a drop-down to change the type, and rename, delete and drag to reorder. An empty one says "No inputs: the caller sends nothing. Add one with the + button." or "No outputs: the function replies nothing. Add one with the + button." Ticking Use Struct swaps the list for an **Input Struct** or **Output Struct** picker. A number input also gets a **Value Range** under Inputs, two boxes labelled **Min** and **Max** that the server holds callers to; see [Value Range](./access-members-and-timers.md#value-range); - a collapsed **Advanced** section with the **Server Name**, the [name the function has on the server](#every-setting). **A message** shows a **Message** section whose header reads the node line, for example `BossDefeated (Winner, Hits)`, with the message's **Name** and **Players Can Listen**; a **Payload** section with a **Use Struct** check-box and a **+** button, listing the values the message carries the way a function's Inputs are listed (an empty one says "No payload: listeners are told it happened and receive nothing. Add a value with the + button."); and a collapsed **Advanced** section with the **Server Name**, the [topic the message has on the server](#every-setting). **Code Source** and the logic file are not in Details: they are on the Server Code tab, beside the code they choose. ### Type Name The Type Name box checks what you type as you type it and says what is wrong: "Needed: the type's name on the server.", "At most 48 characters.", "Start with a lowercase letter." or "Use only lowercase letters, digits and underscores." The problem appears under the box with a warning icon. The value is stored when you press Enter or leave the box. When there is a problem and the asset's name gives a better idea, a **Use village_beacon** link beside it sets the Type Name from the asset name ("Sets the Type Name from the asset's name"): `CSO_VillageBeacon` becomes `village_beacon`, dropping the `CSO_` (or the `DA_` of an older asset) and putting an underscore where each word starts. ### Callable By **Callable By**, in a function's Details, decides whether a player may call it. | Choice | What happens | |---|---| | **Players** | Players can call it, and so can other server code and developer tools. | | **Members** | Members of the Server Object can call it. Needs **Members From**; see [Members](./access-members-and-timers.md#members). | | **Leader** | Only its leader can. Needs **Members From**. | | **Server Only** | Only other server code and developer tools can. **A player's call is refused**, and the caller's `OnDone` sees the `Denied` outcome, with a `Reason` such as "players may not call this function"; see [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code). The function's row in the Server Object panel carries a **Server Only** tag. | | **Developers Only** | Only developer tools can, such as [Call as Developer](./deploy-with-server-compute.md#call-a-function-as-the-developer): **players and other server code are refused**, with the `Reason` "only developer tools may call this function". The row carries a **Developers Only** tag. | Other server code and developer tools may call every function whatever it says, except that a Developers Only function refuses other server code too. Server Only is for functions that should never be reachable from a game client, such as a payout your own server code triggers or an administrative reset. Developers Only is for the ones only a person should run, such as a repair or a data fix you call by hand. The check is made on the server, so a modified game client cannot get around it. ### Two definitions with one Type Name Two definitions cannot use the same Type Name, since both would write the same `Server//` folder and become the same type on the server. When another definition has the Type Name, the Type Name row says "Another definition already uses this Type Name; give this one its own.", the status readout reads **Problem** followed by the same text, **Generate** and **Deploy** are disabled, and the list of deployed revisions is hidden, until one of them is renamed. [Server Compute](./deploy-with-server-compute.md#deploy) refuses to deploy too, naming both assets: "Two Server Object types are named village_beacon (...) and (...); each needs its own Type Name". ## Server messages A Server Function answers the player who called it, and the variables tell everyone what a value is now. A **message** is for the third thing: telling everyone listening that something happened. Your server code sends it, and each player listening to that Server Object, and each Server Object type that listens to this one, hears it once. A message is not a value and nothing keeps it: a player who was not listening, or whose connection was closed at that moment, never hears it, so a fact players must be able to look up later belongs in a variable. The Hollow Giant, the [shared boss](./examples/shared-boss.md), announces its fall with a `BossDefeated` message that names the player who landed the last hit and the hits it took. On `CSO_Boss`, in the **Server Messages** section of the Server Object panel: 1. Click **+**. The new message, `NewMessage`, is selected. Set its **Message** to `BossDefeated`. 2. Leave **Players Can Listen** ticked. 3. Under **Payload**, click **+** twice. Name the first value `Winner`, an Integer64, and the second `Hits`, an Integer. To carry a struct of your own instead, tick **Use Struct** and pick it as the **Payload Struct**. The row reads `BossDefeated (Winner, Hits)`. A message with no values is allowed: listeners are told it happened and receive nothing, such as a `BossRespawned` message. | Setting | The boss's value | |---|---| | Message | `BossDefeated` | | Players Can Listen | ticked | | Payload | `Winner` Integer64, `Hits` Integer | The message is sent from the type's server code; see [Send a message](./write-server-logic.md#send-a-message). It is heard from Blueprint with **On BossDefeated Received** and **Watch Message**, and from C++ with `WatchMessage`; see [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md). The message is the struct `FCrowdyServerMessage` in the definition's `Messages`. ### Who hears a message - **Players Can Listen**, ticked, the default: any player who can connect to the type may listen to the message, whatever the type's **Readable By** says. - **Players Can Listen** cleared: only the server code of a type that [listens to this one](./access-members-and-timers.md#listen-to-other-server-objects) hears it. Watching it from Blueprint or C++ logs a warning and does nothing, and a **Received** node for it does not compile. A message players cannot listen to does not open the type to players: a type whose only messages are these is still [closed to players](./access-members-and-timers.md#a-type-players-cannot-reach). **Readable By** and the Owner Only and Team Only variables decide who may read the variables. They do not decide who hears a message. So when the type has a message players can listen to and **Readable By** is not **Every Player**, or any variable is Owner Only or Team Only, the Server Messages section and the type's settings show a warning: "Every player who can connect to this type hears its messages, whoever may read its variables. Keep private data out of messages." Put what only some players may know in a variable, and send a message only for what anyone may hear. ### What a message carries The payload takes the same types as a variable (see [Field types](#field-types)) and is held to the same limits as a reply: nothing a client could not read, at most 1 MiB and 65,536 list, set and map entries. A message that breaks them is not sent: the send returns an error, which fails the function that made it when its code passes the error on; see [What the generated code does for you](./write-server-logic.md#what-the-generated-code-does-for-you). A message's **Name** is what Blueprint and C++ use and must be unique in the type. Its **Server Name** in the **Advanced** section is its topic on the server: empty uses the name in snake case, so `BossDefeated` becomes `boss_defeated`. A topic is lowercase letters, digits and underscores, starts with a letter, is at most 64 characters, is not `state` (the topic the variables use), and is not used by another message. After you add, rename or change a message, [generate the server code](./write-server-logic.md#after-you-change-the-definition) and deploy the type again, since the server code carries it. ## Every setting The platform term is in brackets, for when you read the platform's own [ck-exec pages](/exec/intro) or its logs. Where the editor labels a setting differently from its C++ name, the label is given too. **Server Object** - **Type Name** [node type]: the type's name on the server. Lowercase letters, digits and underscores, starting with a letter, at most 48 characters, and unique in your project. - **Kind** (`ECrowdyServerObjectKind`): **Server Object**, the default, keeps variables and has one instance for each Instance Id. **Server Function Library** (`FunctionLibrary`) has functions only, saves nothing between calls and is answered by several instances at once. The settings a library cannot have stay on the asset, hidden and ignored. See [Make a Server Function Library](./server-function-libraries.md). - **Description**: what this type is for, in a sentence. It is shown beside the type on the [Server Compute](./deploy-with-server-compute.md#overview) page. It is for you and your team: it is not sent to the server, and it is not part of a packaged game. - **Visibility** (`ECrowdyServerObjectVisibility`, labelled **Readable By**): who may see the values. - **Public** (**Every Player**): every player can read the variables visible to players and is told of each change. - **Owner Only**: each Server Object belongs to one player. Its Instance Id is that player's user id, and only that player can read its values or call its functions. See [Owner Only types](./from-cpp/get-a-server-object.md#owner-only-types). - **Members**: only the Server Object's members read its values; anyone can still see who the members are and join. Needs **Members From**. See [Readable By](./access-members-and-timers.md#readable-by). - **Only One Instance** (`bOnlyOneInstance`): one shared instance for every player, like a registry or a world event; nothing picks an instance. See [Only One Instance](./access-members-and-timers.md#only-one-instance). - **Platform Data**: **Reads Players** (`bReadsPlayers`), **Changes Players' App State** (`bWritesPlayerState`), **Reads World Data** (`bReadsWorld`), **Changes the World** (`bWritesWorld`), **Reads Grids** (`bReadsGrids`), **Manages Grids** (`bManagesGrids`), **Grants Grid Permissions** (`bGrantsGridPermissions`), **Reads Players' Access** (`bReadsAccess`) and **Manages Players' Access** (`bManagesAccess`): what the type's server code may read and change of players, the world, grids and players' access to the app. All off by default. Deploy asks the platform for a permission [scope] for each one ticked, the way **Required Feature** asks for `players.read`; without it the platform refuses those calls. They work for any type. See [Platform data](./access-members-and-timers.md#platform-data) and, for reading who stands in an area, [Count who stands in the area](./keep-a-record-per-entity.md#count-who-stands-in-the-area). - **Holds** (`ECrowdyServerObjectHolds`): **One Set of Variables**, the default, keeps the variables once for the Server Object. **A Record per Entity** (`RecordPerEntity`) keeps them once for each entity, keyed by the entity's id, such as the health of every member of a crowd. See [Keep a record per entity](./keep-a-record-per-entity.md). - **Visible to Players** (`OwnerFields` and `TeamFields` hold the Owner Only and Team Only variables): who receives each variable of a record, **Server Only**, **Everyone**, **Owner Only** or **Team Only**. A variable is in at most one of the two lists, and must be Visible to Players. - **Max Records** (`MaxRecords`): the most records one Server Object keeps. 2048 unless you change it, 1 to 4096. - **Send Changes Every** (`PushIntervalMs`): how often the server sends what changed, in milliseconds. 250 unless you change it, 50 to 5000. Changes in between are sent together, and the last value wins. - **Team From**: where a record's team comes from. **This Object's Records**, the default: a player's team is the team of a record they own in the same Server Object. **Crowdy Team**: a record's team is a Crowdy Team id in digits, and the server checks the player's membership through the platform, so deploying asks for `players.read`. - A record type needs **Readable By** set to **Every Player** and **Members From** set to **None**. - **Area Size** (`AreaSizeChunks`) and **Areas Followed Around a Player** (`InterestRadiusAreas`): for a component or a crowd row in **This Actor's Area** mode, in any type (see [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md)). Area Size is how many chunks one area measures on each side of the ground, 4 unless you change it, 1 to 64. Areas Followed Around a Player is how many areas out from a player's own a crowd follows, 1 unless you change it, 0 to 3. See [Pick the object](./keep-a-record-per-entity.md#pick-the-object). - **Members From**, **Max Members**, **Show Members to Players**, **Remove Members Who Leave**, **Timers**, **On Player Joined**, **On Player Left**, **Can Call**, **Can Call Any Type** (`bCanCallAnyType`) and **Listen To**: the settings on [Access, members and timers](./access-members-and-timers.md). Members From is **None**, **This Object** or **Crowdy Team**. **Can Call Any Type** lets the server code call every type in the app and deploys `calls: ["*"]`; see [Call any type](./access-members-and-timers.md#call-any-type). - **State Form** (`StateForm`, an `ECrowdyServerValuesForm`, shown as the Variables' **Use Struct** check-box): **Struct** (Use Struct ticked) or **Variables** (`List` in C++), added in the asset. The one not chosen is kept but not used. - **State** (labelled **State Struct**) [the hub's state, kept in its snapshots]: the struct holding everything the server keeps for one Server Object, when the Variables use a struct. - **State List** (`StateList`, an `FInstancedPropertyBag`, labelled **Variables**): the variables added in the asset, each with its default value, where a new Server Object starts. In the generated server code they are a struct named after the Type Name in PascalCase plus `State`, so `village_beacon` gives `VillageBeaconState`. - **Watched Fields** [the `state` topic] (labelled **Visible to Players**; the eyes): the variables players can read and watch, by the name the editor gives them. The others never leave the server. In a record type, the Owner Only and Team Only variables are listed here too. - **Functions** [methods]: what players, other server code and developer tools can ask the Server Object to do. Each, an `FCrowdyServerFunction`, has: - **Name**: the name your C++ passes to `Call`. - **Server Name** (in the function's **Advanced** section) [method name]: the function's name on the server. Empty uses Name in snake case, so `FeedBeacon` becomes `feed_beacon`. Lowercase letters, digits and underscores, starting with a letter, at most 64 characters; `read` is reserved. - **Params Form** (`ParamsForm`, shown as the Inputs' **Use Struct** check-box): **Struct** or **Variables**, added in the asset. - **Params** (labelled **Input Struct**): the struct the caller sends, when the Inputs use a struct. None sends nothing. - **Params List** (`ParamsList`, labelled **Inputs**): the inputs the caller sends, with their default values. No inputs sends nothing. In the generated server code they are a struct named after the function plus `Params`, such as `FeedBeaconParams`, with the default values as its defaults. - **Reply Form** (`ReplyForm`, shown as the Outputs' **Use Struct** check-box): **Struct** or **Variables**. - **Reply** (labelled **Output Struct**): the struct the server answers with, when the Outputs use a struct. None answers nothing. - **Reply List** (`ReplyList`, labelled **Outputs**): the outputs the server answers with. No outputs answers nothing. In the generated server code they are a struct named after the function plus `Reply`, such as `FeedBeaconReply`. - **Who Can Call** (`ECrowdyServerFunctionCaller`, labelled **Callable By**): **Players** lets players call it; **Members** and **Leader** limit it to the members or the leader; **Server Only** refuses a player's call, and only other server code and developer tools can call it; **Developers Only** lets only developer tools call it. See [Callable By](#callable-by). - **Cooldown** (`CooldownSeconds`): how long one player waits between two calls that succeeded; 0 for none. See [Cooldown](./access-members-and-timers.md#cooldown). - **Messages** [topics other than `state`]: what the server code sends to everyone listening. Each, an `FCrowdyServerMessage`, has: - **Name**: the name Blueprint and C++ use, such as the `Message` that `WatchMessage` takes. Unique in the type. - **Server Name** (in the message's **Advanced** section) [topic]: the message's name on the server. Empty uses Name in snake case, so `BossDefeated` becomes `boss_defeated`. Lowercase letters, digits and underscores, starting with a letter, at most 64 characters; `state` is reserved. - **Players Can Listen** (`bPlayersCanListen`): on by default. Off, only a type that Listens To this one hears the message. See [Who hears a message](#who-hears-a-message). - **Payload Form** (`PayloadForm`, shown as the Payload's **Use Struct** check-box): **Struct** or **Variables**, added in the asset. - **Payload** (labelled **Payload Struct**): the struct the message carries, when the payload uses a struct. None carries nothing. - **Payload List** (`PayloadList`, labelled **Payload**): the values the message carries, with their default values. No values carries nothing. In the generated server code they are a struct named after the message plus `Message`, such as `BossDefeatedMessage`. - **Listen To** (`ListenTo`) [`calls`]: other Server Object types whose messages this type's server code listens to. Each is also in Can Call. See [Listen to other Server Objects](./access-members-and-timers.md#listen-to-other-server-objects). A function added with **+** starts with its inputs and outputs added in the asset, and none of either. A message added with **+** starts with its payload added in the asset, and no values. Variables, inputs and outputs added in the asset can be any type the Blueprint type picker offers there: booleans, integers, floats and doubles, names, strings, enums, structs, soft object and soft class paths, and arrays, sets and maps of those. Text, object, class and interface references, `FInstancedStruct` and `FGameplayTagContainer` are not offered, since a Server Object cannot carry them. Variables, inputs or outputs share one struct in the generated server code when they have the same names, types and order and one was copied from the other, which keeps each value's hidden id. Lists you built separately in the editor never share, even if they look alike. The first of the sharing Lists keeps the struct, counting the variables first, then the functions in order with inputs before outputs. Each of the others that starts with the same values gets its own name as a type alias, so `logic.rs` can still use it: in the [tip jar](./examples/tip-jar.md), the Tip output (`Total`, starting at 0) is `pub type TipReply = TipJarState;`, because the jar's `Total` also starts at 0. A List that has the same shape but starts with different values gets its own struct with its own defaults. In the village beacon, the beacon's `Oil` starts at 50, so the FeedBeacon input (`Oil`, starting at 10) and output (`Oil`, starting at 0) are each their own struct, and code that used one for the other no longer builds. Generate shows a notice when a List switches between the two, such as "TipReply is now its own struct, since its starting values differ from those of TipJarState; code that uses one for the other no longer builds." In Unreal each function keeps its own default values: **Make Inputs**, the call nodes and a reply that leaves a value out use that function's. In the generated server code the defaults matter only when an older client leaves out a value it does not know yet. **Save and stop** (with the type's own settings) - **Save Interval Seconds** [`persist_every_ms`] (labelled **Save Every**): how often the server saves a running Server Object, 5 to 60 seconds, default 30. After a crash it starts again from its last save, so at most this much is lost. - **Idle Timeout Seconds** [`evict_after_ms`] (labelled **Stop When Idle For**): how long a Server Object nobody uses keeps running before the server stops it, 1 to 1800 seconds, default 300. The next use starts it again from its last save. **Scaling** (a Server Function Library only) - **Min Instances** (`MinInstances`) [`replicas`]: how many instances run all the time, 1 to 64, default 1. - **Max Instances** (`MaxInstances`) [`max_replicas`]: the most instances the server adds when calls have waited 2 seconds, 1 to 64, default 1. It removes an added one after about 30 seconds nearly idle. It must be at least Min Instances. - **Parallel Calls per Instance** (`ParallelCallsPerInstance`) [`concurrency`]: how many calls one instance answers at the same time, 1 to 256, default 16. Each running instance counts toward the app's limit on running Server Objects. See [Scaling](./server-function-libraries.md#scaling). **Server Limits** (collapsed; the defaults suit most types) - **Memory Megabytes** [`memory_mb`] (labelled **Memory per Instance**): how much memory each running Server Object may use, 1 to 512 MB, default 64. Each one reserves it from the app's memory limit, so a type that needs less fits more Server Objects. An app with no funds has 1 GB in all, so at 512 MB only two run at once and the next is answered **Unavailable**; a funded app has 64 GB. An app with no funds can also run only 16 Server Objects at once, a funded app 16,384. - **Call Time Limit Ms** [`deadline_ms`] (labelled **Time per Call**): how long one Server Function call may run before the server stops it and the caller gets **Timeout**, 10 to 10,000 ms, default 2,000. A player's connection waits up to 20 seconds for an answer: the longest **Time per Call** (10 seconds) plus time the call waits behind others at the Server Object. The limit covers only the function itself, not that waiting. A call that runs past it is answered `DeadlineExceeded` (the SDK reports **Timeout**, with a reason such as "AttackBoss ran past its Time per Call on the server: handler exceeded its deadline"; if no answer reaches the game at all, the reason is "The server sent no answer in time; the connection may be slow or lost"), and the platform also restarts that Server Object from its last save, so changes since the last save are lost. Keep functions short, and set **Save Every** low on a type whose recent changes matter. - **Fuel Per Call Millions** [`fuel_per_call`] (labelled **Fuel per Call (Millions)**): how much work one call may do before the server stops it, 1 to 1,000 million, default 50 million. - **Max Waiting Calls** [`mailbox`] (labelled **Calls Waiting**): how many calls may wait at one Server Object, 1 to 4,096, default 1,024. Past that a call is answered **Busy**. Deploy sends a limit only once you change it, so a type that keeps the defaults deploys exactly as before. A value outside its range is brought into it. **Server Names** (in the Advanced group) - **Field Names**: keeps a field's name on the server when you rename the field in Unreal. Each entry, an `FCrowdyServerFieldName`, names the struct, the field as the struct now names it, and the Server Name to keep. You pick the struct, then the field from a list of that struct's fields, so there is nothing to spell. For variables, inputs and outputs added in the asset, use **List Value Names**. - **Enum Value Names**: the same for an enum value, each entry an `FCrowdyServerEnumValueName`: pick the enum, then the value from a list of its values. - **List Value Names**: the same for a variable, input or output added in the asset, each entry an `FCrowdyServerListValueName`: pick the List (the variables, or a function's inputs or outputs), then the value, and the Server Name to keep. An entry follows its value through later renames, since it records the value's id, not its name. You rarely add one yourself: Generate Server Code adds it when you choose to keep a renamed value's old name (see [After you change the definition](./write-server-logic.md#after-you-change-the-definition)). An entry stops applying when its function is renamed; Generate then asks again. In both, the second list stays empty until the first is chosen ("Choose the struct first", "Choose the enum first"), a list with nothing to offer says "Nothing to pick", an entry still to be filled in reads "None", and one whose struct, field, enum, value or List is gone reads its name followed by "(missing)" ("Unknown value (missing)" for a List value, which is kept by its id). A Blueprint enum's values are listed by the names the editor shows. **Server Code** (set on the editor's Server Code tab, not in Details) - **Code Source**: where this type's Server Functions come from. **Generated** (the default) uses `Server//src/logic.rs`, the file Generate Server Code writes once and never replaces. **My Own File** uses a `.rs` file of your own instead, for example one shared in your repo. It is the combo beside the file's path on the tab. In C++ the choice is the enum `ECrowdyServerCodeSource`, with the values `Generated` and `OwnFile`, and a third, `GameKitStarter` (**Game Kit Starter**), which only **Add starter** sets; see [Use the Game Kit starters](./use-game-kit-starters.md). - **Logic File**: your `.rs` file, relative to the project folder, chosen with the folder button on the Server Code tab. The button is shown, and the file used, only when Code Source is My Own File; with My Own File and no file chosen, the type cannot be generated or deployed, and the messages say "Code Source is My Own File but no file is chosen". The tab shows the file's state and code; see [Write its server logic](./write-server-logic.md#the-server-code-tab) and [Use your own logic file](./write-server-logic.md#use-your-own-logic-file). The Logic File is editor-only, like the Description; Code Source is kept in a packaged game, which reaches a Game Kit starter differently from other types. **Baked** (in the Advanced group, read only) - **Baked Structs** and **Baked Enums**: the field and value tables the SDK uses at runtime, rebuilt every time you save the asset. You never edit them. ## Checks when you save Saving the asset, and the editor's **Validate Data**, check the whole definition and list every problem at once, naming the struct and field (or the function or message) each is about: a Type Name or Server Name that breaks the rules, two functions or two fields with the same name on the server, a message with no Name, two messages with one Name or one topic, a topic that is `state`, a variable visible to players that is not in the State struct, and any field of a type a Server Object cannot carry. For a record type it also names an Owner Only or Team Only variable that is not a field of the State or not Visible to Players, a variable that is both Owner Only and Team Only, **Readable By** other than **Every Player**, **Members From** other than **None**, a function named `changes`, **Max Records** or **Send Changes Every** out of range, and **Area Size** or **Areas Followed Around a Player** out of range. The Server Object panel puts a warning icon on the variable, function or message a problem is about. A definition with a problem cannot be used until it is fixed. A Server Function Library is checked on what it shows only, and needs no State struct or variable. It is also refused when a function is **Members** or **Leader**, when a function has a **Cooldown**, and when **Max Instances** is below **Min Instances**; see [Checks when you save](./server-function-libraries.md#checks-when-you-save). Variables, inputs and outputs added in the asset are checked the same way, named by the name the server code gives them, for example "FeedBeaconParams.Label: text is not supported; use FString". Two more checks are about them only: - "Add at least one variable, or use a State struct": the Variables are added in the asset and there are none. - "Two lists are named FeedBeaconParams; give their functions different names": two functions with the same Name both have inputs (or outputs) added in the asset, which would give the server two structs with one name. Field names on the server are your C++ member names (`Oil`, `bLit`), or a Blueprint struct's field names with every character other than letters, digits and underscores removed (`Max Health` becomes `MaxHealth`). Two fields whose names differ only in case are refused. :::warning[After you change a struct, save the definition again.] The tables in the asset describe the structs as they were when it was saved. If a struct gains or loses a field and the asset is not saved again, acquiring a Server Object of that type fails with a message saying the struct has changed since the definition was saved. Every cook saves it again, so this bites in the editor, not in a packaged game. ::: ## Field types | Type | Notes | |---|---| | `bool`, `uint8`, `int32`, `int64`, `float`, `double` | C++ also allows `int8`, `int16`, `uint16`, `uint32`, `uint64`. | | `FString`, `FName` | Use `FString` for text the server makes up or players choose (names, messages). | | Enums | Travel by the value's name, so reordering values is safe. | | Nested `USTRUCT`s | | | `TArray`, `TSet`, `TMap` | Set elements and map keys must be strings, names, integers or enums. | | `TOptional` | C++ only. | | `FVector`, `FVector2D`, `FIntPoint`, `FIntVector`, `FRotator`, `FQuat`, `FLinearColor`, `FColor` | | | `FDateTime`, `FTimespan` | To the millisecond. | | `FGuid`, `FGameplayTag` | A tag this build does not know fails to decode. | | `TSoftObjectPtr`, `TSoftClassPtr`, `FSoftObjectPath` | The path only; nothing is loaded. | :::warning[Keep server-made text in FString, not FName or a soft path.] Every `FName` and soft object path the server sends becomes a permanent name in the engine, never freed, so a client accepts at most 1 Mi (1,048,576) characters of new names (Name values and soft object path parts it has not seen before) from the server per run. Past that, a new Name reads as None and an object path made of new names reads as empty; the rest of the message still applies, a map or set entry whose key was skipped is dropped, and the client logs one warning. The text "None" never counts. A variable holding player-chosen names or free text should be a String (`FString`). Keep `FName`, enums, gameplay tags and soft paths for values drawn from a fixed set your game already knows. ::: Refused: object, class, weak and interface references, `FText`, `FInstancedStruct`, `FGameplayTagContainer` (use a `TArray` of `FGameplayTag`), fixed-size arrays, delegates, and field paths. Editor-only, deprecated and `Transient` properties never travel. ## Changing a type later A Server Object's state outlives the code that wrote it, so some changes are safe and some are not. | Change | What happens | |---|---| | Add a field | Safe. Older saves and older clients leave it at its default. | | Remove a field | Safe. The server drops it from its saves. | | Rename a field | The server sees one field removed and another added, and the old value is lost. Keep the old name under **Field Names** instead. | | Rename a variable, input or output added in the asset | Generate Server Code notices the rename and asks whether to keep the old name on the server. Keep it and saved values carry over (a **List Value Names** entry holds the old name); rename it on the server and the old value is lost, as with a field. | | Change a field's type | Refused when the value is read, and a save holding the old type cannot be loaded. Add a new field instead. | | Reorder enum values | Safe: values travel by name. | | Change **Kind** | Safe both ways. To a Server Function Library the saved data is kept, unused, and comes back if the type becomes a Server Object again; the deploy says so. See [Switch Kind](./server-function-libraries.md#switch-kind). | | Rename or remove a message | A **Received** node for the old name no longer compiles ("has no message ... any more"), and `WatchMessage` of it logs a warning and does nothing. Other types that listen to it need their server code generated again. | | Add an enum value | An older client reads the new value as the field's default and logs a warning; older server code refuses it. | | Remove or rename an enum value | A save holding it cannot be loaded. Keep the old name under **Enum Value Names**, or stop using the value before you remove it. | After any change, the server code has to be generated and deployed again for the server to know it; see [After you change the definition](./write-server-logic.md#after-you-change-the-definition). ## Related - [Make a Server Function Library](./server-function-libraries.md): a type with functions only, answered by several instances at once - [Access, members and timers](./access-members-and-timers.md): who may read and call, members, timers, and calling other types - [Keep a record per entity](./keep-a-record-per-entity.md): a Server Object that holds values for every entity of a crowd - [Write its server logic](./write-server-logic.md): generating the type's server code from this asset, and the Server Code tab - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md) and [from C++](./from-cpp/get-a-server-object.md): using the type in a game - [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md): listening to the messages defined above - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) - [ck-exec overview](/exec/intro): hubs, snapshots and the manifest these settings become # Access, members and timers Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/access-members-and-timers # Access, members and timers :::note Server Objects run on [ck-exec](/exec/intro). ::: Some rules are the same in every game: only the players of a team may spend the team's gold, a match starts once and only its leader starts it, a purchase can be made once a minute. You can write each of them in `logic.rs`, or you can set them in the [definition asset](./create-a-type.md) and let the server code the editor generates enforce them before your code runs. Then `logic.rs` holds only the rules that are truly yours. The settings fall into five groups in the asset's Details tab, each in its own category: - **Access**: who can read a Server Object, who can call each Server Function, how often, and with what values. - **Members**: who belongs to a Server Object. - **Timers & Events**: things the server does on a clock, and when a player arrives or leaves. - **Can Call** and **Listen To**: the other Server Object types this one's server code may call, or any type with **Can Call Any Type**, and the ones whose messages it listens to; see [Call other Server Objects](#call-other-server-objects) and [Listen to other Server Objects](#listen-to-other-server-objects). A [Server Function Library](./server-function-libraries.md) has only **Can Call**, **Platform Data** and the function rules **Callable By** (without Members and Leader), **Required Feature** and **Value Range**; the rest of this page is about Server Objects. After you change any of these, [generate the server code](./write-server-logic.md#after-you-change-the-definition) and deploy it again, since the server enforces them. Two examples in Oakford's world use every group; see [Guild halls and arena lobbies](./examples/guild-halls-and-arenas.md). ## Access ### Readable By **Readable By** is on the type: select **Type Settings** in the Server Object panel. It says who can read the variables that are [Visible to Players](./create-a-type.md#the-details-tab) and is told when they change. | Choice | Who reads the variables | |---|---| | **Every Player** | Any player. | | **Owner Only** | The one player who owns it. Its Instance Id is that player's user id. See [Owner Only types](./from-cpp/get-a-server-object.md#owner-only-types). | | **Members** | The members of the Server Object. Anyone can still see who the members are and whether joining is open, so a player who is not a member can find it and join. **Members** needs **Members From**. | With **Members** or **Owner Only** the server does not send a change's values to everyone. It tells the watchers that something changed, and each reads the values again. That is fine for a team of five; for a big group, use **Every Player**. See [Large groups](#large-groups). ### Callable By **Callable By** is on each function, in its Details. | Choice | Who may call it | |---|---| | **Players** | Any player. | | **Members** | A player who is a member of this Server Object. | | **Leader** | The leader of this Server Object. | | **Server Only** | No player. Only other server code and developer tools. | | **Developers Only** | Only developer tools, such as [Call as Developer](./deploy-with-server-compute.md#call-a-function-as-the-developer); no player and no other server code. | Other server code and developer tools may call every function whatever it says, except a Developers Only one, which only developer tools reach. **Members** and **Leader** need **Members From**. The check is made on the server, so a modified game client cannot get around it. ### Required Feature **Required Feature**, beside **Callable By**, names a feature key the caller's access tier must grant, such as `vip`. Define features and grant them to tiers on the [Access page](../studio/overview.md#the-access-page) in Crowdy Studio. A player whose tier does not grant it is refused before your code runs, with the outcome `Denied`, not retryable, and the reason "needs the vip feature". A refused call does not start the **Cooldown**. Leave it empty for no check. It checks players only, like **Callable By**: other server code and developer tools are let through. A key is 1 to 64 letters, digits, underscores, hyphens and dots, matched exactly, capitals included. A **Server Only** or **Developers Only** function has no player to check, so a Required Feature there does nothing: the function shows a warning under it ("Cooldown and Required Feature have no effect here: they limit only players' calls, and only developer tools or server code may call this function.") and Deploy lists it ("village_beacon Refill: Required Feature has no effect, since only players are checked."). A type with a Required Feature on a function that players may call asks the platform for the `players.read` scope when it is deployed; nothing else is needed. ### Cooldown **Cooldown**, in seconds, sits beside **Callable By**. It is how long one player waits between two calls of that function that succeeded; 0 means no wait. A call inside the wait is refused, and the refusal is retryable: "Feed can be called again in 4 s". A call that failed does not start the wait. Each player has their own wait, and the server remembers the last calls of up to 10,000 players per function. Only players wait: on a **Server Only** or **Developers Only** function a Cooldown does nothing, and the function shows the same warning as for a Required Feature, which Deploy lists as "village_beacon Refill: Cooldown has no effect, since only players' calls are limited." ### Value Range **Value Range** limits a number a caller sends. Select an **Input** of a number type in the function's Details and fill in the two boxes under **Value Range**, **Min** and **Max**, both optional. A call with a number outside them is refused before your code runs: `bad_params: Amount must be 1 to 100`. ![guild_hall's Deposit: Callable By Members, Cooldown 2 s, and the Amount input's Value Range of 1 to 100](/img/unreal-sdk/server-object-function-rules.png) For an input taken from a struct, set the range on the struct's field with `meta = (ClampMin = 1, ClampMax = 100)`; the two ways read the same. The range is read when the server code is generated, so generate and deploy again after changing it. Only number inputs can have one. ### Only One Instance **Only One Instance** is on the type, in **Access**, beside **Readable By**. Tick it for one shared instance for every player, like a registry or a world event: a list of the world's open arenas, the day's festival. Nothing picks an instance, so no Instance Id is needed anywhere. - In Blueprint, the [typed nodes](./from-blueprint/get-a-server-object.md#where-the-object-comes-from) for the asset need neither a **Target** nor a **Find By Asset** choice. Place the node and use it. A **Crowdy Server Object** component with the asset joins the one object whatever its **Instance Mode** says. - From C++, acquire it with any Instance Id, or an empty one; the subsystem uses the one object. - The generated server code refuses a call for any other instance with `denied`, so a stray tool cannot make a second one. - It cannot be combined with **Readable By** set to **Owner Only**, whose Instance Id is each player's user id, or with **Members From** set to **Crowdy Team**, whose Instance Id is the team id. The asset says so when you save it. Ticking or clearing it changes which object players reach, so generate and deploy the type again. Data saved under the old Instance Id is not moved to the new one. A [Server Function Library](./server-function-libraries.md) is reached the same way, with no Instance Id, and never needs this setting. ### Who receives a variable of a record In a type whose **Holds** is **A Record per Entity**, each variable has its own audience instead of the single **Visible to Players** switch. Select the variable and set **Visible to Players** in its Details: | Choice | Who receives it | |---|---| | **Server Only** | Nobody. It never leaves the server. | | **Everyone** | Every player who watches the object. | | **Owner Only** | The player who owns the record's entity. | | **Team Only** | Players whose own record in the same object has the same team, or who are on the record's Crowdy Team, as **Team From** says. | The record's owner and team are set by server code, not by a variable. A player who owns no record, or no matching team, receives the Everyone variables only; the others read as their defaults and never raise a change. Everyone values arrive in the server's batches. Owner Only and Team Only values are fetched when the server says they changed, at most once a second for each object, so they arrive a moment later. See [Who receives a value](./keep-a-record-per-entity.md#who-receives-a-value). **Readable By** must be **Every Player** for such a type, and **Members From** must be **None**: who sees what is decided per variable. ### Platform data The **Platform Data** category says what the type's server code may read and change outside its own Server Objects: players, the world and grids. Deploy asks the platform for a permission (a scope) for each setting you tick, and the platform refuses the server code's calls into anything else with `Denied`. All are off by default; tick only what the code uses. | Setting | C++ | Scope | Lets the server code | |---|---|---|---| | **Reads Players** | `bReadsPlayers` | `players.read` | Read a player's name, the features their access tier grants, their group membership and permissions, and their app state. | | **Changes Players' App State** | `bWritesPlayerState` | `players.write` | Replace a player's app state. | | **Reads World Data** | `bReadsWorld` | `world.read` | Read a chunk, the voxels written into it, and which actors are in or near it. | | **Changes the World** | `bWritesWorld` | `world.write` | Write voxels and a chunk's state. | | **Reads Grids** | `bReadsGrids` | `grids.read` | Find the grid over a chunk, read a grid and its owner, and check a player's permission on it. | | **Manages Grids** | `bManagesGrids` | `grids.write` | Create and delete grids, give a group keys on one, and open one to every player. | | **Grants Grid Permissions** | `bGrantsGridPermissions` | `permissions.write` | Grant and revoke a player's permissions on the app's grids. | | **Reads Players' Access** | `bReadsAccess` | `access.read` | Read a player's access to the app: status, tier, expiry and suspension. | | **Manages Players' Access** | `bManagesAccess` | `access.write` | Grant, revoke and suspend a player's access to the app. | **Manages Grids** and **Manages Players' Access** give the server code what an app admin can do in Crowdy Studio over grids and players' access, for your app only; tick them only on the types that need them. Crowdy Team members, records whose **Team From** is **Crowdy Team** and a **Required Feature** on a function players may call ask for `players.read` on their own, so **Reads Players** changes nothing for such a type. The box then shows ticked and greyed, and its tooltip names why, for example "Needed because Members From is Crowdy Team; Required Feature is set on Feed, so deploy asks for players.read whatever this says." (a Required Feature on a Developers Only or Server Only function does not count). The calls themselves are on the platform's [World and platform data](/exec/world-and-platform-data) page. Before it sends anything, Deploy looks through the type's server code for calls into these, and lists each one whose setting is off, such as "village_beacon: its server code calls world().set_voxels(), which needs world.write; turn on Changes the World or the platform refuses that call". It is a hint, not a check: it sees a call written as `ctx.world().set_voxels(...)`, but not one made through a variable that holds `ctx.world()` or through `ctx.node`. It ignores code inside Rust raw strings and inside `#[cfg(test)]` items (tests), so a call there raises no hint. A changed setting takes a deploy, like changed code. ### A type players cannot reach A type whose functions are all **Server Only**, with no variable **Visible to Players**, **Holds** set to **One Set of Variables**, **Members From** set to **None**, no message with **Players Can Listen** ticked, and neither **On Player Joined** nor **On Player Left**, gives players nothing to do. (A type that holds a record per entity always reaches players: each record carries its entity, owner and team.) Deploy tells the platform to keep players out of it altogether, so only other server code and developer tools reach its Server Objects, and Server Compute describes it as "closed to players". Acquiring one in a game is refused at once, with "`` is closed to players: only server code and developer tools reach it". Give the type any of those and the next deploy opens it to players again. ## Members **Members From** is on the type. It decides who belongs to the Server Object. | Choice | Members are | |---|---| | **None** | Nobody. There are no members, and saving refuses **Members** in **Readable By** and **Members** or **Leader** in **Callable By**. | | **This Object** | Players the Server Object keeps itself. Players **Join** and **Leave**. | | **Crowdy Team** | The members of a [Crowdy Team](../services/teams.md) whose id is the Server Object's Instance Id. | ### This Object The Server Object keeps its members, their order of joining, its leader and whether joining is open, and saves them with its variables. Set: - **Max Members**: the most members it takes. It is 100 unless you change it. 0 means no limit, and needs **Show Members to Players** off. - **Show Members to Players**: on by default. Players can read who the members are. Turn it off for a large group: players then get only the number of members, and **Max Members** may be 0 or any size. With it on, **Max Members** must be 1 to 4,096. - **Remove Members Who Leave**: on by default. A member who leaves the game, once their last connection to the Server Object closes, stops being a member. Turn it off to keep a member listed while they are away. When a Server Object restarts (switched off and on, or moved), the platform does not tell it about players whose connection closed while it was stopped, so see [Players who left while it was stopped](#players-who-left-while-it-was-stopped) below. The panel's **Built-in** section then lists six functions, greyed out, each with its inputs as a function row shows them, such as **Add Member (Player)**: you cannot edit or delete them, and you cannot make a function of your own with the same name. They work like any Server Function: call them from Blueprint with **Make Inputs** and **Call Server Function**, or from C++ with `Call`. ![arena_match: the greyed Built-in functions, Readable By Members, Members From This Object, Max Members 8 and one Timer](/img/unreal-sdk/server-object-members.png) | Function | Inputs | Who may call it | What it does | |---|---|---|---| | **Join** | none | Any player | Adds the caller. Refused with "It is full" at **Max Members** and "It is not open for joining" while joining is closed. A player who is already a member succeeds. | | **Leave** | none | Any player | Removes the caller. A player who is not a member succeeds. | | **Add Member** | `Player` (Integer64) | Server code only | Adds that player, with the same limits as Join. | | **Remove Member** | `Player` | The leader, or server code | Removes that player. | | **Make Leader** | `Player` | The leader, or server code | Makes that member the leader. Refused for a player who is not a member. | | **Set Open for Joining** | `bOpen` (Boolean) | The leader, or server code | Opens or closes joining. | The inputs of **Add Member**, **Remove Member** and **Make Leader** are the struct `FCrowdyServerMemberInputs`, whose one field is `Player`. The input of **Set Open for Joining** is `FCrowdyServerOpenInputs`, whose one field is `bOpen`. **Add Member** exists so that one type can seat players in another, as a registry does; see [Guild Halls](./examples/guild-halls-and-arenas.md#guild-halls). You can also call it by hand as the developer, with the play button on its row in the Built-in list; see [Call a function as the developer](./deploy-with-server-compute.md#call-a-function-as-the-developer). The inputs of **Add Member**, **Remove Member** and **Make Leader** are the struct `FCrowdyServerMemberInputs`, whose one field is `Player`. The input of **Set Open for Joining** is `FCrowdyServerOpenInputs`, whose one field is `bOpen`. ### The leader - The first member to join is the leader. - When the leader leaves or is removed, the member who has been a member longest becomes the leader. - An object with no members has no leader. - **Make Leader** hands the lead to another member. ### Open for joining A Server Object with members kept by itself also has **Open For Joining**, which starts true. While it is false, **Join** and **Add Member** are refused. The leader closes it with **Set Open for Joining**, and your code closes it with `members::set_open(false)`, for example when a match starts. It opens again by itself when the last member leaves, so nobody can close an empty object for good. Everyone can read it, and a registry reads it to skip a match that has already started. ### What a player who is not a member sees With **Readable By** on **Members**, a player who is not a member can still acquire the Server Object. They read the member count, the leader, **Open For Joining** and, with **Show Members to Players** on, the list of members, so they can decide to join. The variables stay at their defaults, **Is Member** is false, and any function whose **Callable By** is **Members** or **Leader** is refused. With **Show Members to Players** off they see the count, not the list. After a successful **Join** the game reads again and they see everything. ### Crowdy Team With **Crowdy Team**, the platform's teams own joining, roles and the size of the team. The Server Object keeps no members. Players join and leave through the **Teams** nodes ([Create Team, Join Team, Request to Join Team, Leave Team](../services/teams.md)); the Server Object asks the platform who belongs. - The Instance Id is the team's id, written in digits. Anything else is refused, as is **Crowdy Team** with **Readable By** on **Owner Only**. - **Members** means the team's members. - **Leader** means a member who can manage the team, which the team's leader role can. - The six built-in functions do not exist, and there are no `Members`, `MemberCount`, `Leader` or `OpenForJoining` variables. **Is Member** and **Is Leader** work as they do for This Object. - If the platform cannot answer who a player is, the call is refused: "membership could not be checked", with the reason after it. Give an actor the Server Object of the player's team with the component's **Player's Team** mode; see [Add the component](./from-blueprint/get-a-server-object.md#add-the-component). ## Timers and events Under **Timers & Events**, a **Timer** is something your Server Object does on a clock, each with: - **Name**: what it is called. Not empty, unique, and not the name of a function. - **Runs**: **Every**, which runs again and again, **Time** apart; **Once After**, which runs once, **Time** after it starts; or **On Schedule**, which runs at the times a **Cron** expression names. In C++ this is the enum `ECrowdyServerTimerRepeat`, with the values `Every`, `After` and `Schedule`. - **Time**: in seconds, at least 0.01. Not shown for **On Schedule**. - **Cron** and **UTC Offset**: for **On Schedule** only. **Cron** has five fields (minute, hour, day of month, month, day of week), or six with seconds first, or is one of `@hourly`, `@daily`, `@weekly`, `@monthly` and `@yearly`; `0 */6 * * *` is every six hours, on the hour. Times are UTC; **UTC Offset** reads them that many minutes ahead of UTC, from -840 to 840, with no daylight saving: 60 runs `0 9 * * *` at 09:00 in UTC+1. Saving checks each field against its range (an hour of 25 is refused, naming the field), with the syntax in [Timers and presence](/exec/timers-and-presence#cron-schedules). - **Start Automatically**: on by default. It starts when the Server Object first starts. With it off, your code starts it. Each timer's row is titled with its name and when it runs, such as "Respawn: Once After 10 s". A schedule runs on the platform's own clock. Each time it fires, the generated code arms its next run before your function is called, so an error in your function does not end the schedule. A run that came due while the Server Object was stopped runs once when it starts again; the other missed runs are skipped. Changing a timer's **Runs** reaches instances that are already running too, once they start on the new code: a deploy does not restart them, so that is when they next start, or at once if you press **Restart** on the type in Server Compute (see [Restarting a type](./deploy-with-server-compute.md#restarting-a-type)). Each Server Object saves its timers' Runs with its values, and when it starts after a deploy that changed one, a timer that was still pending is armed again under the new Runs. The Server Object arms the changed timer without stopping it first, so if arming fails the timer keeps its old schedule, and the server log says "timer EndMatch keeps any old schedule: the changed one could not be armed: " followed by the error. A timer you did not change keeps its clock. A timer that had stopped, or a Once After that already ran, stays so. A timer with **Start Automatically** that you add later starts in running instances as well, and a timer you remove or rename stops under its old name. A changed timer starts its new Runs from that moment, so a pending Once After waits its full new time. Server code generated by an earlier SDK did not save the Runs, so the first deploy of code generated by this one re-arms nothing; every deploy after it does. Each timer is a function in `logic.rs`, named after it in snake case, so a timer named `EndMatch` is `end_match`. It changes the variables the same way a Server Function does, and its changes reach the watchers. ```rust fn end_match(&mut self, _ctx: &Ctx) -> Result<()> { self.Phase = "Ended".into(); Ok(()) } ``` Each timer's name is also a constant in the `timers` module, its function's name in capitals: `EndMatch` is `timers::END_MATCH`, which holds `"EndMatch"`. Start and stop one from your code with `timers::start(ctx, timers::END_MATCH)` and `timers::stop(ctx, timers::END_MATCH)`. Use the constant rather than typing the name: the name is matched exactly, capitals included, so `"end_match"` or a misspelling is only refused when the code runs, while a misspelt constant does not compile. A timer whose constant would be `QUEUED` is refused when you generate, since the timers code uses that name itself. Start uses the period set in the asset. Inside a Server Function they take effect only when that function succeeds. If the platform refuses a timer the function starts, the whole call is refused: nothing it changed is kept or published, any timer it already started in that call is cancelled, and the player sees `ServerError` with a reason such as `the change was not kept: timer EndMatch could not start: `. The two **Events** are tick-boxes, **On Player Joined** and **On Player Left**. Each adds a function to `logic.rs`, `on_player_joined` and `on_player_left`, which get the player's user id. They count a player's connections to the Server Object, not its members. A player who opens the Server Object twice counts once: the first open runs `on_player_joined` and the last close runs `on_player_left`. When **Remove Members Who Leave** is on, the member is removed first and then `on_player_left` runs; it runs even when the player was never a member. ### Players who left while it was stopped When a Server Object restarts, whether it was switched off and on or moved, the platform does not tell it about players whose connection closed while it was stopped. The generated code keeps the connection counts it restored as unconfirmed. Right after the restart it tells the players watching to read again, and any call from a player confirms them. A player who is not heard from within two minutes is treated as having left: **Remove Members Who Leave** removes them, and `on_player_left` runs. A player who reconnects within that time just continues, and `on_player_joined` does not run again. ## Call other Server Objects **Can Call**, in the **Can Call** category, is a list of other Server Object definitions. Each is a type this one's server code may call, from `logic.rs`. Add the definition assets of the types, never the type's own. The types deploy together, so the callee is there when the caller runs. Each function of a listed type becomes a typed function in `logic.rs`, under `calls::::`. It takes the `Ctx`, the callee's Instance Id and its inputs, and returns its outputs: ```rust let hall = format!("hall-{}", self.Halls); calls::guild_hall::add_member(ctx, &hall, &calls::guild_hall::MemberInputs { Player: player })?; ``` A call from server code is not stopped by **Callable By**, so this is also how a **Server Only** function gets called. When the call fails, its error comes back to your code with the type and function in front, "guild_hall.add_member failed: It is full". Return it with `?` and the caller's player sees it as the reason their call failed. What the callee already changed stays changed even if your function then fails. The first call to an instance that is just starting, such as a new match, can take longer than a call is allowed, and fails with "call deadline passed" although the callee may still finish it. A function that is safe to repeat, like **Add Member**, can simply be called again once: ```rust let inputs = calls::guild_hall::MemberInputs { Player: player }; calls::guild_hall::add_member(ctx, &hall, &inputs).or_else(|_| calls::guild_hall::add_member(ctx, &hall, &inputs))?; ``` ### Call a library A [Server Function Library](./server-function-libraries.md) can be listed under **Can Call** like any other type. It has no Instance Id, so its typed helpers take no key: ```rust let loot = calls::loot_roller::roll_loot(ctx, &calls::loot_roller::LootRollInputs { Seed: 7, Tier: 1 })?; ``` Everything else is as above, and a call to a library that has been idle can take over a second; see [A call to a library from server code fails](./troubleshooting.md#a-call-to-a-library-from-server-code-fails). A library can list types under **Can Call** too, and calls them with their keys; see [A library that changes a Server Object](./write-server-logic.md#a-library-that-changes-a-server-object). ### Call any type **Can Call Any Type**, a check-box in the **Can Call** category of any type, lets the type's server code call and listen to every type in the app, including types added later. Deploy then writes `"calls": ["*"]` for the type in place of the names. It keeps the list: **Can Call** and **Listen To** still decide which types get typed helpers in `logic.rs`, so the types you name keep their `calls::::` functions, and **Listen To** keeps its handlers. For a type that is not in the list, there is one more function, `calls::any`. It is made only when **Can Call Any Type** is on. It calls a type by its Type Name, with a payload you encode yourself, and returns the reply bytes undecoded: ```rust #[derive(Serialize)] struct Roll { Seed: i64, Tier: i32, } #[derive(Deserialize, Default)] #[serde(default)] struct Rolled { Item: String, } let payload = encode(&Roll { Seed: 7, Tier: 1 })?; let reply = calls::any(ctx, "loot_roller", "", "roll_loot", &payload).map_err(|error| Error::new(&format!("loot_roller.roll_loot failed: {error}")))?; let loot: Rolled = decode(&reply)?; ``` The arguments are the type's Type Name, the Instance Id (empty for a [Server Function Library](./server-function-libraries.md)), the function's **Server Name** and the encoded inputs. A function with no inputs takes `&[0x80]`. You write the structs for the inputs and the reply yourself, with the field names the type has on the server, since only the listed types get them generated. Use it when the type to call is not known when you write the code, for example a plug-in type another team deploys. Otherwise list the types: a typed helper checks the name and the inputs when the crate builds, while `calls::any` finds a wrong name only when it runs, as a call that fails with "app N has no node type". Deploy does not check that the type exists. ## Listen to other Server Objects **Listen To**, in the **Listen To** category, is a list of other Server Object definitions, the way **Can Call** is. Each is a type whose [messages](./create-a-type.md#server-messages) this one's server code hears. Add the definition assets of the types, never the type's own. Checks and naming are those of Can Call. - **Each listened type is also callable.** **Listen To** adds its types to the ones the server code may call, so the `calls::::` functions above exist for them too; there is no need to list a type in both. - **Every message of the listened type is heard**, including those with **Players Can Listen** off: that setting is what tells a type's messages apart from the ones players hear, and a Server Object type is not a player. - **Hearing is chosen in server code.** The setting only makes the messages available. The code starts listening to one Server Object of the listened type, by its Instance Id, with a `listen::::` function, and handles what it hears in a function the generator adds, `on__`; see [Listen to a message](./write-server-logic.md#listen-to-a-message). A Server Object can have up to 256 listens at once, and each one takes effect only when the function that made it succeeds. Messages are not replayed: one sent while the listener is not running is missed. ## What a refused player sees A refused call reaches the caller's `OnDone`, or **On Failed** in Blueprint, with its **Outcome** and **Reason**. See [Troubleshooting](./troubleshooting.md#outcomes). | Why | Outcome | Reason | |---|---|---| | **Server Only** function | Denied | players may not call this function | | **Developers Only** function | Denied | only developer tools may call this function | | **Members** function, caller not a member | Denied | only members may call Deposit | | **Leader** function, caller not the leader | Denied | only the leader may call StartMatch | | Owner Only, not the owner | Denied | only the owner may use this Server Object | | Team membership could not be checked | Denied | membership could not be checked, and the reason | | **Cooldown** not over | Denied, **Retryable** | Feed can be called again in 4 s | | The game's server, or this type, is switched off or paused | Paused, **Retryable** | The server is switched off or paused, so it refused the call to Feed: `village_beacon` is switched off in app 123 | | **Value Range** | ServerError | `bad_params: Amount must be 1 to 100` | | Join refused | ServerError | It is full, or It is not open for joining | Denied is not retryable except for a cooldown: calling again will be refused the same way until something changes. A cooldown is retryable, and the number of seconds is rounded up. ## Large groups A guild of hundreds or a match of thousands works, with care: - Set **Readable By** to **Every Player** and **Show Members to Players** off, with **Max Members** as high as you need or 0. Players are then sent the member count, the leader and whether joining is open along with each change, and nobody re-reads. With **Members** or **Owner Only**, every change to a large group makes every member read again. - With the list hidden, **Is Member** and **Is Leader** follow the player's own **Join** and **Leave** and the next read, not a list. - Keep busy per-player actions, such as every player's score or purse, off the one shared object. Every call on one Server Object goes through that one object, and every player watching it is told of each change. For a handful of players, give each an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) object for them, and let the shared one hold what the group shares. For many players, prefer one shared object that holds a record per player (or per area, match or team) over one object per player; see [Keep a record per entity](./keep-a-record-per-entity.md). - Know the platform's limit on running Server Objects per app. An app with no funds can run 16 at once, within 1 GB of memory; a funded app can run 16,384, within 64 GB. Past the limit nothing new starts, and calls to an object that is not running come back **Unavailable**. One object per player runs into it quickly. ## Checks when you save Saving refuses the asset, and lists every problem at once, when: - **Members** or **Leader** is used in **Callable By**, or **Readable By** is **Members**, and **Members From** is **None**. - A function of your own has the name of a built-in one while **Members From** is **This Object**. - **Max Members** is below 0, or is 0 or above 4,096 while **Show Members to Players** is on. - A **Value Range** has a minimum above its maximum, or is on an input that is not a number. - A timer's name is empty, is used twice, is not a usable name once written in snake case, or is a function's name, or is `crowdy_flush` (the name a record type uses to send its changes) or `crowdy_sessions` (the name the generated code uses to keep track of connected players) in snake case, or its **Time** is under 0.01 seconds. - **Can Call** or **Listen To** has an empty entry, the type itself, or a type without a Type Name. - **Listen To** names a Server Function Library ("loot_roller is a Server Function Library and sends no messages."). Put it under **Can Call** instead. - **Members From** is **Crowdy Team** and **Readable By** is **Owner Only**. - **Only One Instance** is ticked with **Readable By** on **Owner Only**, or with **Members From** on **Crowdy Team**. - **Holds** is **A Record per Entity** and **Readable By** is not **Every Player** ("needs Readable By Every Player; make the variables others must not see Owner Only or Team Only"), or **Members From** is not **None** ("cannot use Members From; each record carries its own owner and team"), or a function is named `changes`. - A variable is Owner Only or Team Only but is not a field of the State or not Visible to Players, or is both ("'Gold' is both Owner Only and Team Only; choose one"). - **Max Records**, **Send Changes Every**, **Area Size** or **Areas Followed Around a Player** is outside its range ("Max Records is 5000; it must be from 1 to 4096"). ## For logic.rs The generated server code gives `logic.rs` these, when the definition uses the setting. | Name | What it does | |---|---| | `members::list()` | The members' user ids, in the order they joined. This Object only. | | `members::leader()` | The leader's user id, if any. | | `members::is_member(player)` | Whether a player is a member. | | `members::is_open()` | Whether joining is open. | | `members::set_open(open)` | Opens or closes joining. | | `members::add(player)` | Adds a member, and errors when full or closed. The first member leads. | | `members::remove(player)` | Removes a member. | | `members::make_leader(player)` | Makes a member the leader, and errors for a player who is not one. | | `members::is_member_of_team(ctx, player)` | Whether a player is in the team. Crowdy Team only. | | `timers::start(ctx, timers::END_MATCH)`, `timers::stop(ctx, timers::END_MATCH)` | Starts or stops a timer with the asset's period. | | `timers::END_MATCH` | A timer's name, one constant per timer. | | `calls::::(ctx, key, &inputs)` | Calls a function of a Can Call or Listen To type and returns its outputs. For a Server Function Library there is no `key`. | | `calls::any(ctx, node_type, key, method, &payload)` | With Can Call Any Type: calls any type by its Type Name with an encoded payload, and returns the reply bytes. | | `messages::send_boss_defeated(&payload)`, `messages::BOSS_DEFEATED` | Sends one of the type's messages, and the message's topic as a constant; one of each per message. | | `listen::::(ctx, key)`, `listen::::stop_(ctx, key)` | Starts or stops listening to a message of one Server Object of a Listen To type. | The `members` functions change the Server Object's own members, so a function that fails leaves them as they were, like its variables. ## Related - [Create a Server Object type](./create-a-type.md): the type's other settings and the asset editor - [Relate Server Objects to each other](./relate-server-objects.md): which of these settings to use for one-to-one, one-to-many and many-to-many relations - [Guild halls and arena lobbies](./examples/guild-halls-and-arenas.md): a registry of teams of five, and a lobby with a leader and a timer - [Write its server logic](./write-server-logic.md): what the generated code does for you, and where `logic.rs` goes - [Server messages](./create-a-type.md#server-messages): the messages a Listen To type hears - [Make a Server Function Library](./server-function-libraries.md): a type with functions and no instance, which any Can Call list can name - [Troubleshooting](./troubleshooting.md#outcomes): outcomes and refusals - [Read and follow its variables, from Blueprint](./from-blueprint/read-and-follow-variables.md#by-name): **Get Members**, **Is Member** and **Is Leader** - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md#add-the-component): the component's **Player's Team** and **From Server Value** modes - [Teams](../services/teams.md): the platform's teams, for **Crowdy Team** # Relate Server Objects to each other Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/relate-server-objects # Relate Server Objects to each other :::note Server Objects run on [ck-exec](/exec/intro). ::: A game rarely has one Server Object on its own. A player has a stash, a guild has a hall, a hall sits in a registry, a match reports to a leaderboard. This page shows how to model those relations with the settings you already have: the Instance Id, **Readable By**, **Members From**, **Only One Instance** and **Can Call**. Nothing here is a separate feature. The rule under all of it: **an object refers to another by its Instance Id.** A Server Object's type and Instance Id are all it takes to reach it, from Blueprint, from C++ or from another type's server code. ## One to one One thing in your game has exactly one Server Object of its own. | Relation | Settings | Instance Id | |---|---|---| | A player and their own object, such as a locker or a stash | **Readable By**: **Owner Only** | The player's user id | | An actor placed in the level and its object | The component's **Instance Mode**: **This Actor** | None to type: the actor's placement | | A team and its object | **Members From**: **Crowdy Team** | The team's id | An **Owner Only** object belongs to one player: only that player reads it or calls it. In Blueprint, the component's **Signed-In Player** mode, or **Find By Asset** with **Signed-In Player**, finds it with no id to type. See [Owner Only types](./from-cpp/get-a-server-object.md#owner-only-types). **This Actor** gives each placed actor its own object, the same on every player's machine; the [shared boss](./examples/shared-boss.md#the-actor) uses it. An actor spawned at run time needs an id of its own instead, set with **Instance Id**. A **Crowdy Team** object is the team's: the platform's teams decide who belongs, and the component's **Player's Team** mode finds it. See [Crowdy Team](./access-members-and-timers.md#crowdy-team). ## One to many One object keeps a list of others, such as a registry of the village's guild halls. The registry holds each hall's Instance Id, never the hall's values. | Part | Settings | |---|---| | The registry | **Only One Instance** when there is truly one per app, or one Instance Id per region; a String, Array or Map variable of Instance Ids; **Can Call** with the listed type, if it seats players or changes them | | Each listed object | Its own type and Instance Id, such as `hall-1`, with its own **Readable By** and members | To reach a listed object, take its Instance Id from the registry: - give the component **From Server Value**, pointing **Source Variable** at a variable that holds one Instance Id, and it follows the id as it changes; - or pass the id to **Find By Asset**'s **Instance Id** pin, or to **Get Server Object**. In [Guild halls](./examples/guild-halls-and-arenas.md#guild-halls), `hall_registry` seats each player in a hall with room, writes the hall's id into the player's own `player_team` object, and the hall's actor follows that id with **From Server Value**. ## Many to one Many players, or many objects, share one Server Object. | Relation | Settings | |---|---| | Many players on one shared object, such as a boss | **Readable By**: **Every Player**; a **Cooldown** and a **Value Range** on the functions players call | | Many objects reporting to one, such as matches to a leaderboard | **Can Call** on the reporting type, listing the shared one; the shared function's **Callable By** on **Server Only** | | Many entities, such as a crowd, each with values of its own | **Holds**: **A Record per Entity**, in one object for an area, a match or a team; see [Keep a record per entity](./keep-a-record-per-entity.md) | The [shared boss](./examples/shared-boss.md) is the first: every player hits one boss, and the **Cooldown** and **Value Range** keep each player to a fair rate and a fair hit, enforced on the server before your code runs. **Every Player** sends each change to every watcher; with **Members** or **Owner Only**, every watcher reads again after every change. See [Large groups](./access-members-and-timers.md#large-groups). For the second, an arena match calls the leaderboard's function from its own server code, as `calls::::`. A call from server code is not stopped by **Callable By**, so a **Server Only** function on the leaderboard takes reports from matches and refuses players. See [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). ## Many to many Players belong to several groups, and each group has several players. | Relation | Settings | |---|---| | Players and lobbies | **Members From**: **This Object** on the lobby type, with **Max Members**; one Instance Id per lobby | | Teams and their objects | **Members From**: **Crowdy Team** on each type a team has, all keyed by the team's id | Each lobby keeps its own members, so a player can be a member of several lobbies at once, and each lobby takes up to its **Max Members**. Players **Join** and **Leave**, and **Is Member** and **Get Members** answer per lobby. See [This Object](./access-members-and-timers.md#this-object) and [The arena lobby](./examples/guild-halls-and-arenas.md#the-arena-lobby). With **Crowdy Team**, the platform keeps the relation: a team can have a hall, a banner and a stash, each a type with **Members From** set to **Crowdy Team** and the team's id as its Instance Id. A player in several teams picks one with **Team Id**. See [Crowdy Team](./access-members-and-timers.md#crowdy-team). ## Good practice **Refer by Instance Id; never copy another object's values.** A copy goes stale the moment its owner changes. The registry keeps `hall-1`, not the hall's gold; the game reads the gold from the hall. **Give each fact one owner.** One type changes a value, and the others read it or call that type's function. Only `guild_hall` changes its `Gold` and its members; the registry seats a player by calling the hall's **Add Member**, and the hall does the seating. **Keep calls between types few, and plan for a timeout.** Each call from server code is one more thing that can fail. The first call to an instance that is just starting can run out of time and fail with "call deadline passed", although the callee may still finish it. So call again only a function that is safe to run twice, such as **Add Member**, or make yours safe, as `Assign` is when it gives a seated player back the same hall. What the callee already changed stays changed even if your function then fails. From the game, a **Timeout** outcome means the call may or may not have taken effect; watch the values before you call again. See [Outcomes](./troubleshooting.md#outcomes). **Keep a registry within the limits.** An Unreal client refuses a list, set or map of more than 4,096 entries, more than 65,536 entries in all the lists of one message, and a message over 1 MiB, and a change that would put one into a variable players see is not kept. See [What the generated code does for you](./write-server-logic.md#what-the-generated-code-does-for-you). A list that can grow without bound needs splitting: one registry per region or per day, each its own Instance Id, or the detail kept in the members' own objects, as each player's `player_team` holds their hall. **Choose Readable By per object, not per relation.** Related objects need not share it. A registry readable by every player can list lobbies whose values only their members read. **Use Only One Instance only for what is truly one per app.** It cannot be combined with **Owner Only** or **Crowdy Team**, and ticking or clearing it later changes which object players reach without moving the saved data. A registry that may one day be per region is better off with an Instance Id from the start. See [Only One Instance](./access-members-and-timers.md#only-one-instance). ## Related - [Access, members and timers](./access-members-and-timers.md): every setting used here - [Guild halls and arena lobbies](./examples/guild-halls-and-arenas.md): a registry, team objects and a lobby, built step by step - [Shared boss fight](./examples/shared-boss.md): many players on one object - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md#add-the-component): the component's instance modes - [Get a Server Object, from C++](./from-cpp/get-a-server-object.md): acquiring an object by its Instance Id # Write its server logic Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/write-server-logic # Write its server logic :::note Server Objects run on [ck-exec](/exec/intro). ::: A Server Object's values change only when its server code says so. That code is a small Rust crate, one per type, which the editor writes from the type's [definition asset](./create-a-type.md). Most of it is generated and kept in step with the definition; the part that decides what a Server Function does is yours, and the generator never touches it again once it exists. ## Generate it There are two places to do it, and they do the same thing: in the Content Browser, right-click the definition asset, for example `CSO_VillageBeacon`, and choose **Generate Server Code**; or open the asset and press **Generate** in its editor's toolbar (see [The Server Code tab](#the-server-code-tab)). The editor checks the definition the same way saving it does and lists any problem instead of generating. When it succeeds, a notification names the folder it wrote: ```text Server/ village_beacon/ Cargo.toml src/ lib.rs types.rs logic.rs ``` The folder is `Server//` in your project, next to the `.uproject` file and outside `Content`, so nothing in it is cooked into the game. The crate is named after the Type Name. | File | What it holds | Written | |---|---|---| | `Cargo.toml` | The crate's name, its two dependencies, `ckx-sdk` (pinned to `0.12.1`; it was `0.7.0`, so a crate generated earlier must be generated again before Deploy takes it) and `serde`, and the definition asset it was generated for. | Every time | | `src/types.rs` | A Rust struct for every struct the definition uses and a Rust enum for every enum, with the same fields and the same defaults as in Unreal. | Every time | | `src/lib.rs` | The glue between the platform and your code, and the `Functions` trait listing your Server Functions. | Every time | | `src/logic.rs` | Your code: what each Server Function does. | Once, only when it does not exist, and only when [Code Source](#use-your-own-logic-file) is Generated | A deploy adds one more file to the folder, `revisions.json`, which records what each deploy sent; see [What a deploy records](#what-a-deploy-records). Do not edit `Cargo.toml`, `types.rs` or `lib.rs`: the next generation replaces them. That includes loading a module file of your own from `lib.rs` (a top-level `mod name;` other than `logic` and `types`, with or without `pub` or `#[path]`). Declare your own modules at the top of `logic.rs` instead: `mod helpers;` there loads `src/logic/helpers.rs`. When `lib.rs` has such a line, Generate asks first: "... has lines Generate does not write and would remove: mod helpers;. Generate rewrites lib.rs; declare your own modules at the top of logic.rs instead, where `mod name;` loads src/logic/name.rs. Regenerate anyway?" The default is **No**. `logic.rs` is yours from the moment it is written. When you later add a Server Function, timer or event, Generate offers to add an empty version of it to `logic.rs` (see [After you change the definition](#after-you-change-the-definition)). To get a fresh starting point instead, delete `logic.rs` and generate again. Generate writes only into a folder that is missing or that the SDK generated: its `Cargo.toml` records a definition asset, or the first line of its `src/lib.rs` or `src/types.rs` is the generator's header ("... generated by the Crowdy SDK from its definition. Regenerate it; do not edit."). A `Server//` folder that holds a crate you wrote yourself, for example one you deploy with the platform's API, is refused instead of overwritten: "Server/village_beacon holds server code the Crowdy SDK did not generate, so Generate would overwrite it. Choose another Type Name, or move that folder." The type shows **Problem** on the [Server Compute](./deploy-with-server-compute.md#overview) page with the same remedy. A crate copied from a generated one keeps its record or header, so it counts as generated and Generate rewrites its `Cargo.toml`, `types.rs` and `lib.rs`. Generate also refuses two definitions that have no server code of their own to write. The Crowdy SDK's own root definition (`CSO_PlayersOnline`) says "This is the Crowdy SDK's own root; its server code is built into every deploy, so there is nothing to generate." A project type whose Type Name is `root` says "root is the name of the platform's root Server Object type; choose another Type Name." ### The Server Code tab The definition asset opens in its own [editor](./create-a-type.md#the-asset-editor), and the large tab on its left is **Server Code**, so you can generate, edit and deploy the type's server code without leaving the asset. ![The Server Code tab: the logic.rs path, the Generated choice, revisions, Save and Revert, and the beacon's logic.rs](/img/unreal-sdk/server-code-tab.png) Generate and Deploy are in the editor's toolbar, after Save and Browse, not on the tab. **Generate** is the same as the Content Browser command, including the warning before a change would drop saved data (see [After you change the definition](#after-you-change-the-definition)). **Deploy** builds and deploys every Server Object type in the project, and the small arrow beside it opens **Open Server Compute** and **Show build output**. At the right of the toolbar a coloured dot and one line say where the type stands: **Ready**, **Needs generating**, **Out of date** or **Problem**, or how it compares with the live version (**No changes**, **Changed since version 7**, **Not deployed yet**, **Live code unknown**), the same states as on the [Server Compute](./deploy-with-server-compute.md#overview) page. Hover the line to read the reason. Along the top of the tab, from left to right: - **The file.** The path of the Server Functions file this type uses, relative to the project folder, `Server/village_beacon/src/logic.rs` for Generated. Until the Type Name is valid it reads "`Server//src/logic.rs`, once the Type Name is valid", and for My Own File with no file chosen "No file chosen: choose your .rs file with the folder button". "Unsaved changes" appears beside it while you have edits. - **The source.** A **Code Source** combo, **Generated** or **My Own File**; see [Use your own logic file](#use-your-own-logic-file). With My Own File, a folder button beside it ("Chooses your .rs file") picks the file. - **Deployed revisions.** A combo of the code this type had at earlier deploys, starting with **Your code**; see [View, compare and restore an earlier deploy](#view-compare-and-restore-an-earlier-deploy). - **Three icon buttons.** **Save** ("Writes the code below to the logic file") is enabled only while you have edits, and keeps the file's line endings. **Revert** ("Shows the logic file as it is on disk again"). **Open in external editor** opens the file in the program your computer uses for `.rs` files; it is disabled until the file exists. Below is **the code**: an editor with line numbers down its left edge, and Rust colouring for keywords, types, strings, comments and numbers. Long lines scroll sideways instead of wrapping. When the file does not exist yet, a line above the editor says so ("logic.rs does not exist yet. Generate writes it.") with a **Generate** button for a generated `logic.rs`. When the code lacks a function the definition now has, or defines one it no longer has, a line above the editor says so as you type, for example "logic.rs is missing test_function, on_player_joined, which this type now has." Its **Add Stubs** button adds empty versions of the missing ones just before the closing brace of your `impl Functions` block, as unsaved changes: look them over, then **Save**. When the code has no `impl Functions for ` block, the button reads **Copy Stubs** and copies them for you to paste where they belong. It reads **Copy Stubs** too when that block uses a macro directly inside it, such as `my_macro!(...)`, since the macro may already define some of the functions: the line adds "logic.rs also uses my_macro, which may define some of these; check before adding them.", and nothing is inserted for you. If another program saves the file without the functions Add Stubs or Generate added, the tab says so: "logic.rs was saved by another program without the functions Generate added: test_function, on_player_joined." A function the type no longer has is only named ("logic.rs defines join, which this type no longer has: rename or delete it."); the editor never deletes your code. The paths and the Type Name decide what the card can do: - **Generate and Save are disabled until the Type Name is valid**, and their tooltips say why: "Set a valid Type Name first: lowercase letters, digits and underscores, starting with a letter, at most 48 characters". They are also disabled when the logic file would be outside the project folder ("The logic file must be inside the project folder"), and when another definition has the same Type Name. **Save** is also disabled when the file exists but could not be read ("The logic file could not be read, so saving could replace what it holds"). With no valid path the code editor is read-only, and says "Set a valid Type Name to edit this type's server code here." - **Save asks before it could lose anything.** If the file changed on disk since the editor opened it: "`logic.rs` changed on disk since it was opened here. Overwrite it with your edits?" Yes overwrites the file; No discards your edits and shows the file from disk, after a second question, since they cannot be recovered; Cancel keeps your edits unsaved. If the definition now points at another logic file: "This definition's logic file is no longer `logic.rs`. Save your edits to `logic.rs` anyway?" If the file is not UTF-8 text, it warns that saving writes it as UTF-8, which can change characters that are not plain ASCII. If the file cannot be written: "Could not save `logic.rs`. Close any program using it and try again." - **Revert** shows the file as it is on disk again, and asks first ("Discard your unsaved changes to `logic.rs`?") when you have edits. - **Edits made in another editor show up by themselves.** The tab checks the file every second while it is visible. Saved in VS Code or another editor, the new code appears in the tab within a second when you have no unsaved changes here. If you do, the tab keeps them and says "`logic.rs` changed on disk, for example in another editor, while you have unsaved changes here." with **Reload**, which drops your changes after asking, and **Keep Mine**, after which Save still asks before it overwrites the file. - **Generate** with unsaved edits asks "Save your changes to `logic.rs` first? Generating never replaces an existing logic file." - **Deploy**, in the editor's toolbar, with unsaved edits asks "Save your changes to `logic.rs` first? A deploy sends the files as they are saved." - **Changing the Type Name, Code Source or Logic File** while you have edits asks "Save your changes to `logic.rs`? If not, they stay unsaved and come back when this editor shows `logic.rs` again." Unsaved edits are kept if the panel closes or refreshes. They are not on disk, so [Server Compute's Deploy](./deploy-with-server-compute.md#deploy) warns you about them before it sends the saved files, and closing the editor asks: > These server code files have edits that are not saved: ... Quit anyway and lose the edits? ## Use your own logic file The **Code Source** combo at the top of the Server Code tab, **Generated** or **My Own File** (the definition's Code Source), says where the type's Server Functions come from. - **Generated**, the default, uses `Server//src/logic.rs`, the file Generate writes once. - **My Own File** keeps them somewhere else, for example in a file shared in your repo. A folder button appears beside the combo; choose your `.rs` file with it, relative to the project folder. Choose **Generated** again to go back to `logic.rs`; the file you chose is remembered but not used. The choice is explicit: an empty **Logic File** no longer means "use the generated file". With My Own File and no file chosen, nothing can be generated or deployed for the type, and the messages say so: - The toolbar's status line and the type's Server Compute row read **Needs generating** with "Code Source is My Own File but no file is chosen; choose one, or switch Code Source to Generated". - **Generate** still writes the generated files, then shows "Code Source is My Own File but no file is chosen. Choose one, or switch Code Source to Generated." - The tab says "Choose your .rs file above, or switch to Generated to use the generated logic.rs.", and Save says "Choose your .rs file first". With your own file chosen: - **Generate** writes only the generated files, `Cargo.toml`, `src/lib.rs` and `src/types.rs`. It never creates or overwrites your file. - **Deploy** sends your file as the type's `logic.rs`, whatever it is called on disk. - **One file per type.** Each type builds its logic file against its own State, so two types cannot use the same file, nor one type the generated `logic.rs` of another. Deploy refuses it with "`` and `` use the same Logic File ...; each type needs its own". - **If the file is missing**, the tab says "This file does not exist. Choose an existing file, switch to Generated, or type code below and Save to create it.", the type reads **Needs generating** with the reason "its Logic File ... does not exist; choose an existing file, or switch Code Source to Generated", and Generate says "The Logic File ... does not exist. Choose an existing file, or switch Code Source to Generated." Your file implements the same `Functions` trait as the default one; see [Write logic.rs](#write-logicrs). ## View, compare and restore an earlier deploy Every deploy [records](#what-a-deploy-records) the code it sent for each type, so the type's Server Code tab can show you what was live before. The revisions combo in the tab's header lists them, newest first. Its first entry is **Your code**, the editor as you have been using it. The others give the version, how long ago it was deployed ("just now", "5 min ago", "3 h ago", "2 d ago"), and the word "live" for the revision the live version runs, so the beacon might list Version 7, live, 3 h ago, then Version 6, 2 d ago. When the type has never been deployed, the list says "No deployed revisions yet" and is disabled. How many revisions are kept is set on the Server Compute page's [Settings tab](./deploy-with-server-compute.md#settings). Pick a revision and the editor shows that revision's `logic.rs`, read only, under a line such as "Viewing version 6's code (deployed 2 d ago)". Three buttons act on it: - **Compare with yours** shows a line-by-line difference against the code in your editor, unsaved edits included: lines the revision has that yours does not in red with a minus, lines yours adds in green with a plus. The button then reads **Show its code**, to switch back. - **Restore** puts the revision's code in your editor **as unsaved changes**. Nothing is written until you press **Save**, and nothing is live until you **Deploy** afterwards, so you can look at it, change it, or press **Revert** to throw it away. If you already have unsaved edits it asks "Replace your unsaved changes to `logic.rs` with version 6's code?" first. Restore replaces only the `logic.rs` (or your own file); the generated files stay as **Generate** writes them from the definition today. It is disabled, with the reason on hover, when there is no file to put it in (the Type Name is not valid, or My Own File has none chosen), when the file cannot be read, or when the revision has no `logic.rs`. - **Back to your code** returns to the editor. A revision may have been built from an older definition: if you added a field or a function since, the old `logic.rs` no longer fits the types Generate writes now. The banner then warns "This revision was built from a different definition, so its code may not compile with the types Generate writes now." You can still restore it; expect to fix compile errors before it deploys. To go back to an earlier version as it ran, without editing anything, use **Make active** on the [Versions tab](./deploy-with-server-compute.md#versions) instead: it switches the app to that whole version at once. Restore is for when you want that code back in your project. ## The Rust names - **Structs** are named after the C++ struct without its `F`: `FVillageBeaconState` becomes `VillageBeaconState`. A Blueprint struct uses its name as the editor shows it, with every character other than letters, digits and underscores removed. - **Enums** drop a leading `E`: `EBeaconColor` becomes `BeaconColor`. Each value keeps its name on the server. - A type name that Rust or the generated code already uses (`Vec`, `Error`, `Functions` and the like), or that another of your types already has, gets `Type` added, then a number if that is taken too. - **Fields** keep their names on the server exactly, so the beacon's code reads `self.Oil` and `self.bLit`. A field named like a Rust keyword, such as `type`, is written `r#type`. - **Server Functions** become methods named by their Server Name: `FeedBeacon` is `feed_beacon`. A few names cannot be written in Rust at all: a field or enum value named `self`, `Self`, `super`, `crate` or `_`, and a Server Function whose Server Name is `on_timer`, `on_topic`, `on_presence`, `on_session`, `on_player_joined` or `on_player_left`. Generation stops and names the one it hit; give it a different name on the server under the definition's **Server Names**, or the function a different **Server Name**. Field types map like this: | In Unreal | In Rust | |---|---| | `bool` | `bool` | | `int8`, `int16`, `int32`, `int64` | `i8`, `i16`, `i32`, `i64` | | `uint8`, `uint16`, `uint32`, `uint64` | `u8`, `u16`, `u32`, `u64` | | `float`, `double` | `f32`, `f64` | | `FString`, `FName`, `FGuid`, `FGameplayTag`, soft object and class paths | `String` | | `FDateTime`, `FTimespan` | `i64` | | `TArray` | `Vec` | | `TSet` | `BTreeSet` | | `TMap` | `BTreeMap` | | `TOptional` | `Option` | | `FVector`, `FVector2D`, `FRotator`, `FQuat` | generated structs `Vector`, `Vector2D`, `Rotator`, `Quat` with `f64` fields | | `FIntPoint`, `FIntVector` | generated structs `IntPoint`, `IntVector` with `i32` fields | | `FLinearColor`, `FColor` | generated structs `LinearColor` (`f32` fields) and `Color` (`u8` fields) | The generated structs keep the Unreal field names too: `X`, `Y`, `Z`, `Pitch`, `Yaw`, `Roll`, `R`, `G`, `B`, `A`. Each field in `types.rs` ends with a comment naming its Unreal type, since several Unreal types share one Rust type. For the beacon, `types.rs` holds the state struct with the defaults `FVillageBeaconState` gives it in C++: ```rust #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[serde(default)] pub struct VillageBeaconState { pub Oil: i32, // Int32 pub bLit: bool, // Bool pub LastFedBy: i64, // Int64 } impl Default for VillageBeaconState { fn default() -> Self { Self { Oil: 50, bLit: true, LastFedBy: 0, } } } ``` A new beacon on the server starts from these values, the same ones a client shows before the first values arrive. A default must be the same every time the struct is made: a field initialized with `FGuid::NewGuid()` or the current time cannot be written as server code, and Generate Server Code names it. ## Write logic.rs `lib.rs` declares a trait, `Functions`, with one method per Server Function. For the beacon it reads: ```rust pub trait Functions { fn feed_beacon(&mut self, ctx: &Ctx, call: &Call<'_>, params: FeedBeaconParams) -> Result; fn on_timer(&mut self, ctx: &Ctx, name: &str) -> Result<()> { Ok(()) } fn on_topic(&mut self, ctx: &Ctx, msg: TopicMsg<'_>) -> Result<()> { Ok(()) } fn on_presence(&mut self, ctx: &Ctx, presence: &Presence) -> Result<()> { Ok(()) } } ``` You implement it on the State struct in `logic.rs`. The first generation writes a version whose functions all refuse with "not written yet"; replace them with your own. Here is the beacon's, which takes the oil it is offered up to a limit, relights itself, and remembers who fed it: ```rust use crate::*; use ckx_sdk::prelude::*; const MAX_OIL: i32 = 100; impl Functions for VillageBeaconState { fn feed_beacon(&mut self, _ctx: &Ctx, call: &Call<'_>, params: FeedBeaconParams) -> Result { if params.Oil < 1 { return Err(Error::new("offer at least one unit of oil")); } if self.Oil >= MAX_OIL { return Err(Error::new("the beacon is full")); } self.Oil = self.Oil.saturating_add(params.Oil).min(MAX_OIL); self.bLit = true; if let Ok(player) = call.player() { self.LastFedBy = player as i64; } Ok(FeedBeaconReply { Oil: self.Oil }) } } ``` Each method gets: - **`self`**: the Server Object's state, to read and change. - **`ctx`**: the platform's context for this Server Object. `ctx.key` is its Instance Id, and `ctx.log` writes to the [logs](/exec/operations#logs). - **`call`**: who is calling. `call.player()` is the calling player's user id, or an error when the caller is not a player (a developer tool or other server code). - **`params`**: the function's inputs, already decoded into their struct (`FeedBeaconParams`). A function with no inputs has no `params` argument. It returns its outputs' struct (`FeedBeaconReply`), or `Ok(())` for a function with no outputs. To refuse, return `Err(Error::new("..."))`. The state is then put back exactly as it was before the call, nothing is published, and the caller's `OnDone` sees `ServerError` with your message as its `Reason` (see [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code)). So a function can change fields as it goes and still refuse halfway. Do not start your own messages with `unknown_method`, `denied` or `bad_params`: those words mean the generated code refused the call. The beacon never trusts the amount it is offered: the params are a request, and the reply says what the server did with it. ### The optional hooks `on_timer`, `on_topic` and `on_presence` do nothing unless you implement them in the same `impl`. They are the platform's hooks, described on [Timers, subscriptions and presence](/exec/timers-and-presence): `on_timer` runs when a timer your code set with `ctx.timer_after` or `ctx.timer_every` comes due, `on_topic` when another instance publishes on a topic your code subscribed to (the messages of a [Listen To](./access-members-and-timers.md#listen-to-other-server-objects) type reach their own `on__` function instead; see [Listen to a message](#listen-to-a-message)), and `on_presence` only on the app's root hub. A hook changes the state the same way a Server Function does: its changes reach watchers, and an `Err` puts the state back. ### A record per entity For a type whose **Holds** is **A Record per Entity**, `Functions` is implemented on `Records`, not on the state struct, and the state struct (`RaiderState`) is one record's values. `Records` has `get`, `get_mut`, `insert`, `remove`, `set_owner`, `set_team`, `ids`, `len`, `owner`, `team`, `owned_by` and `Records::area_of(chunk_x, chunk_y)`. A function that returns an error puts back only the records it touched. The names `changes` (for a function) and `crowdy_flush` (for a timer) are reserved. See [Write the server code](./keep-a-record-per-entity.md#write-the-server-code). ## Send a message A type that defines [messages](./create-a-type.md#server-messages) gets a `messages` module in `lib.rs`: one function for each message, `send_` followed by its server name, that takes the payload (no argument for a message with no values), and one constant for each, its topic in capitals, such as `messages::BOSS_DEFEATED`. Call it from any Server Function, timer or hook of the type. The shared boss announces its fall from `hit`, and its return from `respawn`. Add `messages::send_boss_defeated` to the `Hit` code from [Shared boss fight](./examples/shared-boss.md#the-server-code), and a `BossRespawned` message with no values for the second: ```rust fn hit(&mut self, ctx: &Ctx, call: &Call<'_>, params: BossHitInputs) -> Result { // ... clamp, count, set the phase, as before if self.Health == 0 { timers::start(ctx, timers::RESPAWN)?; messages::send_boss_defeated(&BossDefeatedMessage { Winner: self.LastHitBy, Hits: self.Hits })?; } Ok(BossHitOutputs { Health: self.Health, Hits: self.Hits }) } fn respawn(&mut self, _ctx: &Ctx) -> Result<()> { if self.Phase != BossPhase::Defeated { return Ok(()); } // ... restore the health, as before messages::send_boss_respawned()?; Ok(()) } ``` `BossDefeatedMessage` is the name the generated code gives a payload added in the asset: the message's name plus `Message`. A payload taken from a struct uses the struct's name without its `F`, so `FBossDefeated` is `BossDefeated`. What a send does: - **It waits for the function to succeed.** The message is queued and published only when the function returns `Ok`, after the change to the watched variables has been published, and in the order your code sent them. A function that refuses, or whose change is not kept, sends nothing, the way it publishes nothing and starts no timer. - **It reaches everyone listening.** Every player who listens to this Server Object hears it, and so does every Server Object that listens to it. Nobody is named: the message cannot be aimed at one player. - **It is not kept.** Nothing is saved or replayed. A listener that is not listening, or whose connection is closed, at that moment misses it. - **A message that cannot be sent returns an error.** The payload may hold nothing a client cannot read and at most 1 MiB and 65,536 list, set and map entries, and the error names the message, such as `the change was not kept: message boss_defeated: it is larger than 1 MiB`. With `?` the function fails and sends nothing; without it the message is simply not sent. ## Listen to a message A type listens to another type's messages when the other type is in its **Listen To** list, and its code then chooses which Server Object to listen to. The village beacon lights up when the Hollow Giant falls. On `CSO_VillageBeacon`, add `CSO_Boss` under **Listen To**, add a function `FollowBoss` with **Callable By** on **Developers Only** and one String input, `BossId`, and generate the server code. The generated code adds, for each message `CSO_Boss` defines, a handler to the `Functions` trait and two functions to a `listen::boss` module: | Generated | For `BossDefeated` | |---|---| | The handler in `Functions` | `on_boss_boss_defeated(&mut self, ctx, key, payload)`: the type's name and the message's server name, joined | | Start listening | `listen::boss::boss_defeated(ctx, key)` | | Stop listening | `listen::boss::stop_boss_defeated(ctx, key)` | The handler gets the Instance Id of the Server Object that sent the message as `key`, and the payload decoded into the sender's payload type, which `calls::boss` holds. A message with no values has no `payload` argument. The beacon's code: ```rust fn follow_boss(&mut self, ctx: &Ctx, _call: &Call<'_>, params: FollowBossParams) -> Result<()> { listen::boss::boss_defeated(ctx, ¶ms.BossId) } fn on_boss_boss_defeated(&mut self, _ctx: &Ctx, _key: &str, payload: calls::boss::BossDefeatedMessage) -> Result<()> { self.Oil = MAX_OIL; self.bLit = true; self.LastFedBy = payload.Winner; Ok(()) } ``` Deploy, then select `FollowBoss` in the Server Object panel and press **Call as Developer...** to call it on the beacon `oakford` with `boss-1`, the boss's Instance Id; see [Call as Developer](./deploy-with-server-compute.md#call-a-function-as-the-developer). From then on, each time boss `boss-1` falls the beacon refills, lights and credits the player who landed the last hit, and every player watching the beacon sees the change. What listening does: - **It lasts.** A listen is saved with the Server Object and renewed whenever it starts again, so call `listen::...` once, not every time it starts. `stop_boss_defeated(ctx, key)` ends it. A Server Object can listen to at most 256 messages at once. - **It takes effect when the function succeeds.** Like a message sent, a listen or stop is applied after the function's change is kept. If the platform refuses one, the function fails with `could not listen to boss.boss_defeated:` followed by the reason, its state is put back, and any listen the same call already made is undone. - **The handler is a handler like any other.** Its changes reach every watcher, an `Err` puts the state back, and it can send messages of its own, start timers, listen or stop. A payload that cannot be decoded fails the handler with `bad_message: boss.boss_defeated:` and the reason. - **One handler for each message.** The handler's name must not be the name of a function or timer of the type, or of another handler; Generate refuses it, naming the clash, and you rename one of them. A message whose server name cannot be a Rust name is refused too; set another Server Name for it on the type that sends it. Any other topic the Server Object receives still reaches `on_topic`. - **Only from a function, timer or hook.** Starting or stopping a listen anywhere else fails with "listening can only be started or stopped from a handler". The type that sends does not know who listens. It sends the same message whether nobody, a hundred players or three Server Objects hear it. ## A Server Function Library The server code of a [Server Function Library](./server-function-libraries.md) is generated the same way, with the same four files, but it has no state to hold. `lib.rs` implements the platform's `Spoke` instead of `Hub` and has no snapshot, no watched values, no saving, no timers, no sessions, no messages, no members, no cooldowns and no instance check. Your functions go on a struct with no fields that the generator names `Library` (`LibraryType` when one of your structs is already called `Library`), and the loot roller's `RollLoot` is: ```rust impl Functions for Library { fn roll_loot(&mut self, _ctx: &Ctx, _call: &Call<'_>, params: LootRollInputs) -> Result { // ... turn the seed and the tier into an item, as in Loot for the Hollow Giant } } ``` It is the signature of a Server Object's function without the state: `self` is the empty `Library`, `ctx` is the platform's context, `call` says who is calling, `params` holds the decoded inputs, and an `Err` is the caller's `Reason`. The full function is in [Loot for the Hollow Giant](./examples/hollow-giant-loot.md#the-server-code). What the generated code still does for you, before your function runs: **Callable By** (**Players**, **Server Only** and **Developers Only**; **Members** and **Leader** are refused when you save), **Required Feature**, decoding the inputs, the **Value Range**, and the limits on what a reply may hold; see [What the generated code does for you](#what-the-generated-code-does-for-you). A library keeps nothing, and the platform runs each instance in several copies with their own memory. So: - **Do not keep a value in a variable and expect it on the next call.** It may be there or not, and another instance never has it. If the answer has to be remembered, give it to a Server Object. - **`ctx.key` is the name of the instance that answered,** such as `r0` or `r1`, and not an Instance Id. Do not compare it with anything; it is useful in a log line. - **No timers, no listening and no platform events.** `ctx.timer_after` and `ctx.timer_every` are refused when the code runs, and so is subscribing to a topic, so a library cannot use `timers::`, `listen::` or `messages::`; it has no `on_player_joined` or `on_player_left`. ### A library that changes a Server Object A library changes game truth by calling a Server Object. List the object's definition under **Can Call** on the library and call it with the typed helper, which takes the object's Instance Id like any call between types. This function turns coins into oil and feeds the village's beacon: ```rust fn donate(&mut self, ctx: &Ctx, _call: &Call<'_>, params: DonateParams) -> Result { let oil = (params.Coins / 10).max(1); let fed = calls::village_beacon::feed_beacon(ctx, ¶ms.Village, &calls::village_beacon::FeedBeaconParams { Oil: oil })?; Ok(DonateReply { Oil: fed.Oil }) } ``` The library keeps no oil: the beacon does, and everyone watching the beacon sees it change. As for any call between types, an error comes back with the type and function in front, and what the beacon already changed stays changed; see [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). ### A Server Object that calls a library Put the library's definition under **Can Call** on the Server Object, or on another library. A library has one instance, so its typed helper takes no key: ```rust let roll = calls::loot_roller::LootRollInputs { Seed: self.LastHitBy, Tier: 3 }; let loot = calls::loot_roller::roll_loot(ctx, &roll)?; ``` The first call to a library that has been idle can take over a second and fail with "call deadline passed" although the library may still finish; see [A call to a library from server code fails](./troubleshooting.md#a-call-to-a-library-from-server-code-fails). To call a type that is not in the list, tick **Can Call Any Type** and use `calls::any`; see [Call any type](./access-members-and-timers.md#call-any-type). ## What the generated code does for you `lib.rs` handles everything the definition asset already says, so `logic.rs` holds only your rules: - **Watched values.** It answers the SDK's request for the watched values, and after every Server Function or hook that succeeds it compares the watched fields with what they were before and tells every watcher about the ones that changed, all in one notice. A change to a field that is not watched, like `LastFedBy`, sends nothing. - **Records.** On a type that keeps a record per entity, it collects the changes of the records, sends them every **Send Changes Every** and answers each player's request for the records they may see, with the Owner Only and Team Only variables left out for everyone else; the refusals it adds are `records_full` (past **Max Records**) and `bad_record` (an id or team that is empty, too long or not allowed, or a record that does not exist). - **Owner Only.** On an Owner Only type, a player may read or call a Server Object only when its Instance Id is that player's user id. An Instance Id that is not a user id written plainly in digits (`007`, `abc`) starts no Server Object at all. - **Values every client can read.** An Unreal client refuses some values: - a number that is not finite; - a list, set or map of more than 4,096 entries, or more than 65,536 entries in all the lists, sets and maps of one message; - a text over 65,535 bytes, or a message over 1 MiB; - two map keys or set values Unreal counts as one (it ignores the case of the letters A to Z, and reads an empty name as None); - a text in an `FGuid` field that is not a GUID, a name or gameplay tag over 1,023 characters, a soft object or class path that is not an object path; - a date outside the years 1 to 9999, or a time span longer than Unreal holds. Params holding one are refused with `bad_params`. A change that would put one into a watched field, or a reply holding one, is not kept: the state goes back to what it was and the call fails with a message starting `the change was not kept`. This keeps one bad call from breaking the Server Object for everyone watching it. The starting values of a new Server Object are checked too, and a bad one refuses to start it (`bad_seed`). A value that is already in the state, for example one loaded from a save made by older server code, makes reads fail with `unreadable` and the field at fault: players' Server Objects wait in Connecting, with that reason, until your server code replaces the value. Only the client knows which gameplay tags exist and which content roots are mounted, so keep tags and paths valid in your code. - **Callable By.** A player may call only functions their **Callable By** allows: **Players**, **Members** for a member, **Leader** for the leader; a **Server Only** function refuses a player's call with `denied`. Developer tools and other server code may call every function. - **Members, Cooldown, Value Range, timers and Can Call.** The settings on [Access, members and timers](./access-members-and-timers.md) are enforced here, in order: who may call, the params, the **Value Range**, the **Cooldown**, then your function. Members, timers and calls to other types reach `logic.rs` as the `members`, `timers` and `calls` names listed under [For logic.rs](./access-members-and-timers.md#for-logicrs). - **Refusals.** An unknown function is refused with `unknown_method`, a call the checks above do not allow with `denied` or `cooldown`, and params that cannot be read as the function's inputs, or are outside a **Value Range**, with `bad_params` and the field at fault. These are the reasons listed on [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code). - **Messages and listening.** It queues each `messages::send_...` and each `listen::...` call your code makes and applies them only when the function succeeds, after the change to the watched values is published; it sends the messages in order, and it routes each message the type listens to into its `on__` handler. See [Send a message](#send-a-message) and [Listen to a message](#listen-to-a-message). - **Saving and loading.** It saves the whole state on the definition's Save Interval and loads it when the Server Object starts again. For a change that should not wait for the next save, call `ctx.persist_now()` from your function. ## After you change the definition Generate the server code again after any change to the definition asset or to a struct it uses, then [build and deploy it](./deploy-with-server-compute.md). `Cargo.toml`, `types.rs` and `lib.rs` are rewritten to match; `logic.rs` is not. - **A new Server Function, timer, event or listened message** (a message added to a type in your Listen To list) appears in the `Functions` trait, and the crate does not compile until `logic.rs` has its method. After writing the other files, Generate Server Code compares `logic.rs` with the trait and, when a method is missing, says which and asks: "Add empty versions of the missing functions?" **Yes** adds each one just before the closing brace of your `impl Functions for ` block, the way the first generation writes it: a Server Function refuses with "not written yet" until you write it, and a timer or event does nothing. The question ends "This writes them into logic.rs on disk. If another program has it open with unsaved changes, save or close it there first, or its next save removes them." **No**, the default, leaves the file as it is. If the file has no such block, is not UTF-8 text, or the block uses a macro such as `my_macro!(...)` that may define some of the functions, nothing is added and the empty versions are copied to the clipboard for you to paste. If the file changes while the question is open, for example saved from another editor, Yes adds the stubs to the file as it is then, or, when they no longer fit, adds nothing and asks you to generate again. If an open Server Code tab has unsaved edits to the file, Generate leaves it alone and the tab's [Add Stubs](#the-server-code-tab) line shows what is missing. - **A renamed or removed Server Function, timer or event** leaves its old method in `logic.rs`, which no longer compiles against the trait. Generate Server Code names it ("logic.rs defines join, which this type no longer has: rename or delete it.") and never deletes it: rename it to the new name or delete it yourself. A rename is offered as a missing function plus a left-over one, so move your code from the old method into the new one. - **A renamed variable, input or output added in the asset** is noticed, since each one keeps an id of its own. Before it writes anything, Generate Server Code lists each one, such as "Gold was renamed to Coins (VillageBeaconState)", and asks: **Yes** keeps the old names on the server, so saved values carry over and `logic.rs` keeps using `Gold` (it adds a [List Value Names](./create-a-type.md#every-setting) entry for each); **No** renames them on the server, and saved values under the old names are dropped; **Cancel** stops. Server code generated by an older SDK has no ids, so the first generation after updating reports a rename as the value being gone. - **A removed or renamed field or enum value** is something your saved Server Objects may still hold. Before it writes anything, Generate Server Code compares the new types with the ones already in `types.rs` and, if a struct, field or enum value is gone or a field changed type, lists each one (for example `VillageBeaconState.Oil is gone`) and asks whether to regenerate anyway. Answer **No** and keep the old name on the server with a Server Name under **Field Names** or **Enum Value Names** on the definition, or keep the value instead of deleting it. See [Changing a type later](./create-a-type.md#changing-a-type-later) for what each change does to existing data. - **Code in `logic.rs` that used a removed field** stops compiling; fix it there. - **A new type name** (the definition's Type Name) goes into a new folder, `Server//`. Each generated `Cargo.toml` records the definition asset it was generated for, so Generate Server Code finds the folder it wrote under the old name and asks: **Yes** moves `Server//` to `Server//`, keeping your `logic.rs` and `revisions.json`, then generates; **No** generates into a fresh folder with a new `logic.rs` and leaves the old one as it is; **Cancel** stops. A folder generated for another asset, such as the one you duplicated this definition from, is never offered. If you move or rename the definition asset in the Content Browser, saving it updates the record in its crate's `Cargo.toml`, so a later Type Name change still finds the folder. Either way, to the server it is a new type, so Server Objects saved under the old name are not carried over (see [What a deploy records](#what-a-deploy-records)). A Type Name another definition already uses is refused. ## Keep it in version control The crate folder is source: commit `Server//` with the rest of your project, `logic.rs` above all, and `revisions.json` with it (see the next section). Building it locally creates a `target/` folder inside it; leave that out, for example with a `target/` line in your ignore file. ## What a deploy records The platform keeps only a fingerprint of what it built for each type (its build digest), never the code. So every deploy from the editor or the command line writes a record beside each type's crate, `Server//revisions.json`, and that record is what lets the editor tell you what changed. Per deployed revision of the type it holds: - the files as they were sent: `Cargo.toml` (without the definition asset it records, which stays on your machine) and the `src` files, with your own file's code as `logic.rs` if you use one, - a fingerprint of those files that ignores line endings and spaces at the end of a line, so opening a file in an editor that changes them does not make a new revision, - the versions that ran it and the platform's build digests for it, and - when it was deployed. Deploying the same code again does not add a revision: it moves the existing one to the top and adds the version. The newest revisions are kept, as many as **Revisions to keep** on the [Settings tab](./deploy-with-server-compute.md#settings) says, 5 by default. The file is written the same way on every platform, so it only changes in version control when a revision does. A type's state on the [Server Compute](./deploy-with-server-compute.md#overview) page is worked out from it. The platform tells the editor which build digest the live version runs for the type. If `revisions.json` has a revision with that digest, the type is compared with it: the fingerprint of the crate on disk now against the recorded one, and Save every and Stop when idle for against the live version's. Same means **No changes**; different means **Changed**, naming what differs (server code, save interval, idle timeout). If no revision has that digest, the type reads **Live code unknown**: the live version runs something this project has no record of. If the live version's manifest cannot be read at all, every type reads **Live code unknown**, never **No changes**. **Commit `revisions.json` with the crate**, so your teammates see the same states you do. If a teammate deploys and you do not have their `revisions.json` yet, the type reads **Live code unknown** on your machine until you do, and Deploy's confirmation says it replaces live code you have no record of. A `revisions.json` that cannot be read (someone edited it, or a merge left markers in it) is reported in the editor's log as "... is not a deploy record; fix or delete it", and its type reads **Live code unknown**. Deleting it forgets that type's history. The next deploy cannot add to a file it cannot read: it still deploys, then says "Deployed as version 8. Its record could not be saved, so its changes cannot be compared: ..." in the warning color, and the command line lists the same problems as `recordErrors`. Fix the file and deploy again. **Deploy starter pack** records nothing, since it does not send your code. ## Build and deploy it Open Crowdy Studio, choose **Server Compute** in its navigation, and press **Deploy** in its title row, or press **Deploy** in a definition asset editor's toolbar. Deploy is offered only when something changed since the live version; see [Server Compute](./deploy-with-server-compute.md#deploy). The editor sends every Server Object type in the project to the platform, builds it there (no Rust toolchain needed on your machine), and deploys the result as your app's new active server code. The same page shows the build output, the versions you can make active again, and the logs and call activity of what is running, and `-run=CrowdyServerCompute` does it from the command line for CI. See [Server Compute](./deploy-with-server-compute.md). A Server Object acquired before its type is deployed waits in Connecting and comes up once it is; see [Waiting is not failing](./troubleshooting.md#waiting-is-not-failing). ### By hand The platform's own build and deploy do the same work without the editor, for a setup the Server Compute page does not cover: 1. **Build.** Send the crate's `Cargo.toml`, without its `[package.metadata.crowdy]` section (the platform's build accepts only `[package]`, `[lib]` and `[dependencies]`), and its three files under `src/` to the platform's build; see [Builds and starter packs](/exec/builds#building). CrowdyJS and CrowdyCPP wrap it: see [From an SDK](/exec/builds#from-an-sdk). You can also build locally with `cargo build --release --target wasm32-unknown-unknown` and deploy the module yourself; see [Deploying](/exec/intro#deploying). 2. **Deploy.** Give the type an entry in your app's [manifest](/exec/intro#the-manifest), named by its Type Name, and deploy the build with it; see [Deploying a build](/exec/builds#deploying-a-build). A deploy replaces the app's whole manifest, so it lists every type your app runs. The beacon's entry, as the editor writes it under its `root` type: ```json "village_beacon": { "kind": "hub", "parent": "root", "crate": "village_beacon", "client": true, "persist_every_ms": 30000, "evict_after_ms": 300000 } ``` `client` must be `true` for players to reach it. `persist_every_ms` and `evict_after_ms` are the definition's **Save Interval Seconds** and **Idle Timeout Seconds** in milliseconds. A [Server Function Library](./server-function-libraries.md)'s entry has `"kind": "spoke"` and `replicas`, `max_replicas` and `concurrency` from its **Scaling** instead, and neither of those two. Building and deploying need your organization's `manage_compute` permission. [Operations](/exec/operations#instances-and-versions) explains how to move a running Server Object to new code at once, and where a crashed call's log is. ## Related - [Deploy it with Server Compute](./deploy-with-server-compute.md) - [Create a Server Object type](./create-a-type.md) - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md) - [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md): what a player's game does with the messages sent here - [Make a Server Function Library](./server-function-libraries.md): the asset behind [A Server Function Library](#a-server-function-library) - [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code): the refusals this code produces - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) - [ck-exec overview](/exec/intro): hubs, the manifest, and the `ckx-sdk` your code runs on # Deploy it with Server Compute Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/deploy-with-server-compute # Deploy it with Server Compute :::note Server Objects run on [ck-exec](/exec/intro). ::: Once a type's [server code](./write-server-logic.md) is generated and its `logic.rs` written, the server still has to run it. **Server Compute** is the page in [Crowdy Studio](../studio/overview.md) that builds your project's server code on the platform, deploys it to your app, and then lets you watch it work: which version is live, what it logged, how players' calls to each Server Function went, and a switch to stop one type if it misbehaves. There is nothing to install: the build happens on the platform, so no Rust toolchain is needed on your machine. ## Open the page Open Crowdy Studio (**Tools, Crowdy SDK, Crowdy Studio**) and choose **Server Compute** under **COMPUTE** in its navigation. It uses the sign-in and the app you already set in Studio: [sign in](../studio/sign-in.md), then pick your app on the [Project page](../studio/projects-and-apps.md). If you are not signed in, or no app is set, the page says so ("Sign in to Crowdy Studio first" or "Set the app in Crowdy Studio first", then "Sign in and choose an app on the Project page.") and shows a **Try Again** button; fix it in Studio and press that. The header holds the title, the line "Deploy your Server Objects' server code, then watch how players' calls to it go.", a line naming the app and backend ("App 12345 on Dev"), one status badge, and **Refresh**. The badge shows the first of these that applies: | Badge | Meaning | |---|---| | **Budget paused** | The app's Server Compute is paused by billing, so players' calls are refused until it is resumed. Hover it for the reason the platform gives, such as `insufficient_funds` or `spend_cap`. | | **Switched off** | The app's Server Compute is switched off, so players' calls are refused. A **Switch on** button sits beside the badge. | | **Nothing deployed** | The app has no active server code yet. | | **Live: version 7** | Version 7 is the app's active server code. | Beside the badge a small line shows how much of the app's room the Server Objects use in this datacenter, for example "3 of 16 Server Objects · 192 of 1,024 MB": how many are placed against the app's limit, and the memory they reserve (each reserves its type's **Memory per Instance**, see [Server Limits](./create-a-type.md#every-setting)) against the app's memory limit. Both limits come from your organization's funding: an app with no funds can run 16 Server Objects at once within 1 GB, a funded app 16,384 within 64 GB. Past either, no new Server Object starts until some stop, and a call to one that is not running is answered **Unavailable** with the platform's reason. Each running instance of a [Server Function Library](./server-function-libraries.md#scaling) counts as one. The line is missing when the platform does not report it. Hover a badge for a one-line explanation. **Refresh** reads the app's status, versions, logs and activity again and checks the project's Server Object types. If you change the app in Studio, the header line adds "The project's app has changed since; press Refresh to show it." The page has five tabs: **Overview**, **Versions**, **Logs**, **Activity** and **Settings**. Building, deploying, making a version active and switching on or off need your organization's `manage_compute` permission; reading needs `view_compute_diagnostics`. A problem from the platform, such as a missing permission, is written in red under the tab it came from, for example "Could not read the app's status: ..."; a busy server is retried for you first. ## Overview The left side lists every definition asset in the project, sorted by Type Name, then any type that is on the server but no longer in the project, each with its Type Name and **one badge**. **Search types** narrows the list by name, and the tabs above it filter it: **All**, **To deploy** (types that are new, changed, running code not recorded here, or removed) and **Problems** (types whose code cannot be built, or with a Server Object the platform holds back). Click a type to show it on the right; the first type is shown when the page opens. The right side shows the selected type: its Type Name (hover it to see its asset), its badge, the running switch and a **...** menu, then the type's description, if its [definition](./create-a-type.md#every-setting) has one, and a small line with how many functions it has, how many variables are visible to players ("2 variables visible to players"), who can read it ("readable by every player", "readable by its members", "only its owner can read it", or "closed to players: only server code and developer tools reach it" for [a type players cannot reach](./access-members-and-timers.md#a-type-players-cannot-reach)) and, once the Activity numbers are in, how many calls it had over the Activity tab's period, for example "1.2k calls in the last hour", and how many of its Server Objects are running or starting now ("2 running"). The parts are separated by dots. A [Server Function Library](./server-function-libraries.md) says "Server Function Library" first, and in place of the variables and who can read it says how many instances it runs, "1 to 4 instances" for a Min Instances of 1 and a Max Instances of 4, or "4 instances" when the two are equal. When the platform will not place one of the type's Server Objects, for example after it crashed again and again, a red line under it says which and why: "village is held back: ...". Last, a box says what the badge means for this type, such as "Not live yet. Deploy to add it." or "Changed since version 7: server code. Deploy to make it live." ![The Server Compute page: the deploy strip under the tabs with Add starter and Deploy starter pack, five of the project's Server Object types on the left, and the selected type's details on the right](/img/unreal-sdk/server-compute-overview.png) The badge is the first of these that applies. What stops the type's server code from being built comes first: | Badge | Meaning | |---|---| | Needs generating | There is no folder for the type yet, `logic.rs` is missing, or the type uses [My Own File](./write-server-logic.md#use-your-own-logic-file) and no file is chosen or the file is missing. The reason is written under the Type Name. | | Out of date | The definition or a struct it uses changed since the code was generated. Regenerate it with **Generate**. The reason names the file and the first line that differs, for example `... no longer matches the definition (line 7: the file has "...", Generate writes "..."); regenerate it with Generate Server Code`; a long line is cut after 80 characters. | | Problem | The definition fails its checks, or its Type Name is `root`, the name of the platform's top-level type ("root is the name of the platform's root Server Object type; choose another Type Name"), or starts with `ck_staged_`, which is kept for the stand-ins of [a deploy in parts](#a-deploy-in-parts) ("Type Names starting ck_staged_ are kept for the stand-ins a deploy in parts stores its builds with; choose another Type Name"). The reason is written under the Type Name. | Once the type's code is fine, the badge says how the type compares with the version that is live now: | Badge | Meaning | |---|---| | No changes | The live version runs this code with these settings. Hover: "Live in version 7 exactly as it is here". | | Changed | The live version runs another revision of the code, or other settings. Hover: "Changed since version 7: server code, save interval. Deploy to make it live." It can list server code, save interval, idle timeout, platform data (the [Platform Data](./access-members-and-timers.md#platform-data) settings), player access (whether [players can reach the type](./access-members-and-timers.md#a-type-players-cannot-reach)), server limits (the [Server Limits](./create-a-type.md#every-setting) settings), kind (the type is a Server Object live and a [Server Function Library](./server-function-libraries.md#switch-kind) here, or the reverse), scaling (a library's [Scaling](./server-function-libraries.md#scaling) settings), and, for a [Game Kit starter](./use-game-kit-starters.md#change-a-starter), config and parent. Limits are compared the way the platform applies them: a value out of range counts as the nearest value in range, and a limit written as a string in a manifest from another tool is read as a number, so such a manifest no longer shows "server limits" as changed on every check. | | New | The live version does not run this type: "Not live yet. Deploy to add it." | | Live code unknown | The live version runs code this project has no record of, for example deployed from another copy of the project. "Deploying replaces it." See [What a deploy records](./write-server-logic.md#what-a-deploy-records). | | Ready | The type's server code is fine, but it has not been compared yet, because the app's versions have not been read (you are not signed in, or the read failed). Press **Refresh**. | A type that the live version runs and the project no longer has is listed with a **Removed** badge ("On the server, not in this project"); selected, it shows its name, the running switch, and the line "On the server, not in this project. Your next deploy removes it." A project with no types left can still deploy: the deploy sends the Crowdy SDK's own root alone (see [Deploy](#deploy)), which removes every type from the server. The definition asset's editor shows the same state for its type in the status readout at the right of its toolbar, with **Changed since version 7** and **Not deployed yet** in place of Changed and New; see [The asset editor](./create-a-type.md#the-asset-editor). Its **Deploy** dropdown has an **Open Server Compute** entry that brings you to this page. The page does not regenerate code for you. Regenerate from the Content Browser or the asset, then open the tab again or press **Refresh**. With no definition assets it says "No Server Object types yet." and "Create a Server Object Definition asset to add one." Once the app's status is known, a type the live version runs also says **Running** or **Switched off**, with a **Switch off** or **Switch on** button (see [Switching a type or the app off and on](#switching-a-type-or-the-app-off-and-on)), and a running one has a **Restart** button before it (see [Restarting a type](#restarting-a-type)). A type that is not live yet has nothing to switch, so it shows neither. ### Remove a type The **...** menu on a type has two entries. **Open definition** opens the type's definition asset. **Remove from project...** takes the type out of the project: it deletes the definition asset and, when the folder is the type's own, its `Server//` folder. It is disabled while another operation runs, and it asks first, in a dialog titled "Remove village_beacon": > Remove village_beacon from this project? > > This deletes its definition /Game/Beacon/CSO_VillageBeacon and its Server Code folder Server/village_beacon. > > village_beacon keeps running on the server (App 12345 on Dev) until your next deploy, which removes it. The confirm changes with the situation: - **What it deletes.** Everything in the type's `Server//` folder goes with it: `logic.rs`, the generated files and `revisions.json`, so the type's revision history goes too. Get them back from version control if you need them. A logic file of your own that lives outside that folder is never touched. - **What it leaves, and why.** The folder is left alone, and the dialog says "This deletes its definition ... only." followed by the reason, when deleting it could hurt something else: "Another definition has the same Type Name and so the same Server Code folder, which is left as it is."; "Its Type Name is not a valid one, so its Server Code folder may be another type's; it is left as it is."; "Its Server Code folder is not directly inside the project's Server folder, so it is left as it is."; "It has no Server Code folder."; "Its Server Code folder holds server code the Crowdy SDK did not generate, so it is left as it is."; or "Its Server Code folder is, or holds, a link to somewhere else, and deleting it could delete what the link points to; it is left as it is, so delete it by hand." A folder that could not be deleted is reported in red under the tab: "Removed village_beacon's definition, but could not delete its Server Code folder Server/village_beacon. Delete it by hand." - **Unsaved code edits.** If the type's code has edits you have not saved, the dialog lists the files: "Its unsaved code edits are lost:". - **What it does on the server: nothing yet.** A type that is live keeps running, with players calling it, until your next deploy, which no longer includes it and removes it (the deploy's confirm says "It removes village_beacon, which this project does not have; their Server Objects stop."). Until then it shows as a **Removed** row, where you can still switch it off. The deploy stops its Server Objects and keeps their saved state, and the type's switch goes with it: a later deploy that adds a type of the same name starts those Server Objects from what they saved, switched on. A type that was never deployed ("New") says "village_beacon is not live on the server (App 12345 on Dev), so nothing changes there." - **The last type.** Removing the project's last type is no different: the next deploy sends the Crowdy SDK's root alone and removes the type from the server. To stop players' calls to it before then, use **Switch off** on this page. After you confirm, the editor's own delete runs, with its usual check of what still refers to the definition, for example a Blueprint slot holding it; it may ask again, and if you cancel there nothing else happens. If the row went stale (the asset moved, or its Type Name changed since the list was read) nothing is removed and the page says "/Game/Beacon/CSO_VillageBeacon is no longer the definition of village_beacon, so nothing was removed. The list is read again; try once more from it." For a build server or a script, `-op=removetype` does the same from the command line; see [From the command line](#from-the-command-line). ### Deploy **Deploy** sits in the page's title row, beside **Refresh**, so it stays in reach however long the list of types grows. It shows how many types the deploy adds, changes or removes ("Deploy 3"), reads **Up to date** when there is nothing to deploy, and **Working...** while an operation runs. Under the tabs, a strip holds the status line, **Add starter** (see [Use the Game Kit starters](./use-game-kit-starters.md#add-a-starter)) and **Deploy starter pack**; before anything is known it says "Deploy builds every type's server code on the server and makes it the app's live version. Players use it at once." **Generate** sits between **Refresh** and **Deploy**. It reads "Generate 3" while 3 types are out of date or have no server code yet, and it is enabled only then, and only while no other operation runs. Game Kit starters are not counted, and neither are types that need you, such as a [My Own File](./write-server-logic.md#use-your-own-logic-file) type with no Logic File. Its tooltip reads "Generates the server code of every type that is out of date or has none yet, asking what a single Generate asks, then saves the definitions it changed." It generates those types one after another through the same Generate as a definition's own button, so it asks each question that Generate asks (see [Generate it](./write-server-logic.md#generate-it)), and a cancel stops the run. It then saves the definitions it changed, refreshes, and shows a notice of what was written and what was not, and why. **Deploy is offered only when something changed.** Once every type's code is fine and the live version is known, the status line says what a deploy would do, and **Deploy** is enabled only if there is something to do: - "Since version 7: 1 changed, 1 new. Deploying sends the project's 2 types." Any of "changed", "new", "running code not recorded here" (the live version runs something this project has no record of) and "removed" can appear, each with its count. - "Nothing changed since version 7." **Deploy** reads **Up to date** and cannot be pressed. - "Nothing is deployed yet. Deploying sends the project's 2 types." for an app with no active version. - "Ready to deploy 2 Server Object types." when the live version could not be compared, for example because it has not been read yet; press **Refresh**. - "This project has no Server Object types, so deploying sends only the Crowdy SDK's root (Players Online)." when the project has no types. A project like that can still deploy: the root goes alone, and any type the app ran before is removed. A change you have just deployed keeps its "Deployed as version 8." line until something changes again, then the summary takes over. A failed or half-recorded deploy keeps its message until the next deploy. **Deploy** is disabled while any type is not fine, and the line says what to do: "Fix the 1 type marked Problem first." or "Generate Server Code for 1 type first (right-click the definition in the Content Browser)."; two definitions with one Type Name show "Two Server Object types are named village_beacon (...) and (...); each needs its own Type Name". A type whose `Server//` folder holds a crate the SDK did not generate is marked **Problem** with "... holds server code the Crowdy SDK did not generate; choose another Type Name, or move that folder" (see [Generate it](./write-server-logic.md#generate-it)). It is also disabled while another operation runs. The same rules apply to the **Deploy** button in a definition asset editor's toolbar. **Deploy** asks first, in a dialog titled Deploy. The first sentence is always there, then one sentence for each kind of change that applies, naming the types, with what it removes first: > Deploying replaces the active server code of App 12345 on Dev with this project's 2 Server Object types. > > It removes tip_jar, which this project does not have; their Server Objects stop. Their saved state is kept and is used again if you deploy them again. > > It adds lantern. > > It changes village_beacon (server code, save interval). A type whose kind changes also gets a sentence of its own, "loot_roller changes from Server Object to Server Function Library: its saved data is kept, unused, and comes back if it becomes a Server Object again." or, the other way, "loot_roller changes from Server Function Library to Server Object: it starts from the saved data it had as a Server Object, if any." A project with no Server Object types gets "Deploying replaces the active server code of App 12345 on Dev. This project has no Server Object types, so only the Crowdy SDK's root (Players Online) is deployed." as its first sentence. The other one is "It replaces live code this project has no record of in village_beacon." When what the app runs now could not be read, the dialog says "What the app runs now could not be read, so what this changes is not known." instead. Read the list before you answer Yes: it is the difference between what you have here and what players are running. "It removes" names every type the live version runs that this project does not have, however it got there: a type you removed from the project, one deployed from another copy of the project, or server code deployed another way, such as a crate written by hand and deployed with the platform's API. Deploy cannot send such code, so answering Yes takes it offline. If your app runs code like that, deploy it the same way you deployed it before, not from this page. If any server code file has edits you have not saved in the asset editor's Server Code tab, the dialog adds: > 1 Server Code file has unsaved edits; Deploy sends the saved files. and lists the file names. Answer No, save them, and deploy again if you meant to send them. When the server code of some types is exactly what an earlier deploy to this app sent, the dialog adds "The server code of lantern, village_beacon is as it was deployed before, so it is not built again." When the deploy is built in parts, it adds "The server code is built in 2 parts. Each part but the last is first stored by a version that runs what runs now, so the version history gains 1 more version."; see [A deploy in parts](#a-deploy-in-parts). If the server code calls into platform data its definition does not ask for, the dialog adds "Check before deploying:" and one line for each, such as: > village_beacon: its server code calls world().set_voxels(), which needs world.write; turn on Changes the World or the platform refuses that call Answer No, tick the setting in the definition, and deploy again; see [Platform data](./access-members-and-timers.md#platform-data). The check ignores code inside Rust raw strings and inside `#[cfg(test)]` items, so a call that only appears there is not listed. The same list holds a function that cannot use what it sets: a **Developers Only** or **Server Only** function with a **Cooldown** or a **Required Feature** gets "village_beacon Refill: Cooldown has no effect, since only players' calls are limited." or "village_beacon Refill: Required Feature has no effect, since only players are checked." Only players are limited and checked, so clear the setting, or let players call the function. See [Cooldown](./access-members-and-timers.md#cooldown). Confirm, and Studio sends the platform: - the server code of **every** Server Object type in the project, exactly as it is on disk under `Server//` (a type whose Code Source is My Own File sends that file as its `logic.rs`), and - a crate named `root` that the SDK generates for you from `CSO_PlayersOnline` in its own content. It is the top-level type every Server Object type sits under on the platform. It keeps one variable, the number of players in the app's world, which **Get Players Online** reads (see [How many players are online](./from-blueprint/read-and-follow-variables.md#how-many-players-are-online)), and you never edit it. An app whose root an older Crowdy SDK deployed keeps no count, and Server Compute offers a deploy until you make one. The root is not shown as a row, but it is tracked like a type: once a deploy from this SDK has recorded it, a later Crowdy SDK whose root code differs makes Server Compute offer a deploy again. An app whose root was deployed by an earlier SDK is compared only after your next deploy. A project with no Server Object types deploys the root alone. A project with the Game Kit's `world` starter does not send this crate: `world` is the root instead, and every other type runs under it; see [The world becomes the app's root](./use-game-kit-starters.md#the-world-becomes-the-apps-root). together with an entry for each type saying whether players may reach it and what platform data its server code may use, and giving its **Save Interval Seconds** and **Idle Timeout Seconds** from the definition (the platform's [manifest](/exec/intro#the-manifest)). A Server Function Library's entry is `"kind": "spoke"` with its **Min Instances**, **Max Instances** (only when above Min Instances) and **Parallel Calls per Instance** as `replicas`, `max_replicas` and `concurrency`, and no save interval or idle timeout; a type with **Can Call Any Type** lists `"calls": ["*"]`. A deploy replaces the app's active server code as a whole; it does not add to it. That is why every type goes together: a type left out of a deploy is a type the app no longer has in its active code, until a deploy or a version that includes it is active. That is also how [removing a type](#remove-a-type) takes effect. For the example world, a project with `CSO_VillageBeacon` and a `CSO_Lantern` sends `village_beacon`, `lantern` and `root` in one deploy, even when only the beacon's `logic.rs` changed. Only the beacon is built, though: see [Server code that is not built again](#server-code-that-is-not-built-again). The status line follows the build: "Sending the server code...", "Waiting for a builder... 4 s", "Building on the server... 12 s", "Deploying...", and finally the line below. A deploy built in parts says which part it is on, "Part 1 of 2: building on the server... 12 s", and one with nothing to build goes straight to "Deploying...". > Deployed as version 7. Once the deploy succeeds, Studio also records what it sent in each type's `Server//revisions.json`, and the Crowdy SDK's root in `Server/root/revisions.json`, which is how the page tells you next time what changed; see [What a deploy records](./write-server-logic.md#what-a-deploy-records). If a record cannot be written the deploy still stands, and the line says so in the warning color: "Deployed as version 7. Its record could not be saved, so its changes cannot be compared: ..." Commit each `revisions.json` with the crate, and `Server/root/revisions.json` too. A **Build output** button appears once a build has output, and opens it in a read-only box. A failed build deploys nothing: the line says "The build failed. See the build output below." and the box opens by itself, usually with a compiler error in a `logic.rs`. Only one operation runs at a time; the buttons are disabled until it ends. Each type's server code, its crate, may have at most 64 files and 2 MB of source; the [builds page](/exec/builds#limits) has the platform's full list. A project with a crate over either shows the reason on the status line, such as "village_beacon's server code has 70 files; each type's server code can have at most 64" or "village_beacon's server code is 2.10 MB; one build takes at most 2 MB of source", and **Deploy** stays disabled. A deploy with more crates or more source than one build takes is not refused for it: it is [built in parts](#a-deploy-in-parts). A running Server Object keeps its old code until it next starts; see [Operations](/exec/operations#instances-and-versions). So after a deploy that changed a type, the card reads the app's Server Objects, and when some of a changed type are still running it adds a line and two buttons: > 3 running Server Objects of village_beacon still run the previous version until they stop when idle. Move them to the new version now? **Move now** restarts those types (see [Restarting a type](#restarting-a-type)) without asking again; **Leave them** dismisses the line, and they move when they next start. Which Server Objects ran the previous version is decided from the Server Objects read as the deploy was sent, by their epoch, not from your computer's clock, so a clock that is off does not hide or add any. ### Server code that is not built again A type's server code is not built again when its files are exactly those of a revision in its `revisions.json` that this app already holds, because one of the app's versions ran it. The deploy names that module by its digest instead, and the confirm says "The server code of lantern is as it was deployed before, so it is not built again." So a deploy that changed one type's code builds that one crate, a change of settings alone, such as a save interval, builds none, and a deploy with nothing to build sends no build at all. This needs the app's versions, so when they could not be read every crate is built. **Deploy** on the page always reuses what it can. To build every type's server code again, including types whose code did not change, so they pick up the platform's current SDK and toolchain, open the drop-down arrow beside **Deploy** and pick **Rebuild All and Deploy**. It is available even when there is nothing to deploy, and it asks to confirm first. The command line's `-op=deploy -rebuild` does the same, and both follow one rule. ### A deploy in parts The platform builds at most 16 crates and 2 MB of source at a time. A deploy with more, such as a project with many types or with the [Game Kit starters](./use-game-kit-starters.md), is built in several parts, in order, each at most 16 crates and 2 MB. It still becomes one version, and what players run changes once: 1. **Every part is built first.** A part that fails deploys nothing, and nothing on the app changes. 2. **Each part but the last is stored.** The platform keeps a build's modules only once a version has used them, so each of those parts is deployed in a staging version: the types that run now, unchanged, plus a stand-in type for each of its crates, named `ck_staged_` and the crate's name, which nothing calls and players cannot reach. The version that was active is then made active again at once. Before the first of these, the versions are read again, and if the app's active version changed since the deploy started, it stops: "The app's active version is version 9, not version 8 as when the deploy started, so nothing was deployed; refresh, then deploy again". 3. **The deploy itself** names the stored and reused modules by their digest, with the last part's build, and its version runs no stand-in. So the [Versions](#versions) tab gains one version per extra part, before the deploy's own. The confirm says how many parts there are and how many more versions they add. On an app with nothing deployed there is nothing to make active again, so a staging version stays active until the deploy. If storing a part fails, the line says "Built, but storing the builds failed: " followed by the platform's words and what is active now, such as "Version 8 is active again, as before the deploy." A deploy in parts needs to know what runs now, so when the app's versions could not be read it refuses before building anything: "This deploy is built in parts, each first stored by a version that runs what runs now, and what runs now could not be read; refresh, then deploy again". ### Deploy starter pack **Deploy starter pack** builds and deploys the platform's example types instead of your project, after asking: > Deploying the starter pack replaces the active server code of App 12345 on Dev with the platform's example Server Object types. > > It removes village_beacon, lantern, which the active version runs, unless the starter pack has a type of the same name; that type then takes over its saved Server Objects. Their saved state is kept and is used again if you deploy them again. The second sentence names what the live version runs, the SDK's `root` left out, and is missing when nothing is live. When what the app runs could not be read, it says "What the app runs now could not be read, so what this removes is not known." instead. It is meant for trying the platform out on an empty app. It replaces your project's server code just as **Deploy** replaces the previous code, so do not press it on an app your game uses; if you do, make the earlier version active again on the Versions tab. See [Starter packs](/exec/builds#starter-packs). ## Versions Every deploy makes a new version. The **Versions** tab lists them newest first: "Version 7", an **Active** badge on the live one, and a line such as "3 h ago by Ada, 2 types" (hover it for the exact time). Under it, a second line says what that deploy changed from the version before it, for example "Since version 6: village_beacon changed", or "Since version 6: lantern added" and "lantern removed" when a type came or went, several joined by dots. A deploy that changed nothing says "Same server code as version 6". Hover it: "The types whose server code or settings differ from the version before it". The platform reports each version's types, so this works for versions anyone deployed, from any copy of the project; the oldest version has no line, and neither does one whose code cannot be compared with the one before because the platform has no build digest for it. With nothing deployed the tab says "Nothing deployed yet." and "Deploy from the Overview tab." **Make active** on any other version asks first, in a dialog titled "Make version 6 active": > Makes version 6 the active server code of App 12345 on Dev. That is a rollback: if version 7 has a bug, make 6 active and players get the earlier code, while you fix `logic.rs` and deploy again. It changes no Server Object's saved values. As with a deploy, a running Server Object moves to the code when it next starts, and the Server Objects of a type the older version lacks are stopped with their saved state kept. That type's switch goes too, so a type you switched off comes back switched on when a later version runs it again. Rolling back to a version older than a definition change can leave Server Objects saved in a newer shape; read [Changing a type later](./create-a-type.md#changing-a-type-later) before you go back across one. ## Switching a type or the app off and on Each type the live version runs has a **Switch off** button on the Overview tab, beside its name when selected, or **Switch on** once the type is off. Use it when one type is causing trouble and you want it quiet while the rest of the app carries on. Switching off asks first, in a dialog titled "Switch off village_beacon": > Players' calls to village_beacon in App 12345 on Dev are refused until it is switched on again. Its Server Objects are saved and stopped. Switching on asks nothing. While a type is off, calls to it are refused and reach the client as the `Paused` outcome, which is retryable (see [Troubleshooting](./troubleshooting.md#outcomes)), and nothing of that type runs. Switch it on and its Server Objects start again, from what was saved, the next time a call reaches them. It is not the same as removing the type in a deploy: the code stays and the type stays live. To move running Server Objects onto newly deployed code, use **Restart** instead, which refuses no calls. The buttons are disabled until the app's status has loaded and while another operation runs. When the whole app is switched off, the header badge says so and its **Switch on** button, after asking "Players can call the Server Functions of App 12345 on Dev again.", lets players call the app again. The page cannot switch the whole app off; see [the kill switch](/exec/operations#the-kill-switch) for that. ## Restarting a type **Restart** on a running type moves its running Server Objects to the live version now. It asks first, in a dialog titled Restart: > Move village_beacon's running Server Objects on App 12345 on Dev to version 8 now? > > Each is saved and stopped, and starts again on version 8 at its next call. Players watching one see it read again; a call that arrives while it stops may be answered Unavailable. Nothing is refused while it happens and no state is lost: each Server Object is saved first and starts again from what was saved. Players watching one see its values read again on the new start, the way they do after a reconnect. The deploy line then says how many it stopped, for example "3 running Server Objects stopped; each starts again on the active version at its next call." Use it after a deploy, from the **Move now** button the deploy strip offers, or whenever a fix should reach Server Objects that are running now rather than when they next stop. From the command line, `-op=restart` with no `-type` restarts every type that still has an instance on older code; see [From the command line](#from-the-command-line). ## Call a function as the developer A [Server Only or Developers Only](./create-a-type.md#callable-by) function has no player to call it, so you run it by hand from the definition asset: select the function in the Server Object panel, and in its Details press **Call as Developer...**. A window titled "Call as Developer" opens with: - **Instance Id**, the Server Object to call, such as `boss-1`. A type that is **Owner Only** takes its owner's user id, and a type with **Only One Instance** always calls its one Server Object, whatever is typed. It is needed otherwise. - the function's inputs, starting from their default values, to fill in; - **Call**, and under it the answer: "Ok" and the outputs as JSON, such as `{"total":7}`, or the outcome and the reason, such as "Denied: only members may call Deposit". If the connection itself is refused it says why, for example "Unavailable: could not connect as the developer: start failed: denied: an owner-only Server Object's Instance Id is its owner's user id". The built-in member functions that only server code may call, such as **Add Member** when **Members From** is **This Object**, can be called this way too. They sit greyed out in the **Built-in** list of the Members panel, and a play button beside the row ("Call as Developer: runs this function on a live Server Object as you, the app's developer") opens the same window. The call goes to the live app with your Crowdy Studio sign-in, as you: the server code sees `Caller::Developer` with your user id, not a player, and whatever it changes stays changed. It starts the Server Object if it is not running. If the function's inputs change in the definition while the window is open, **Call** says so; close the window and open it again. ## Logs The **Logs** tab shows the lines your server code writes with `ctx.log`. For the beacon: ```rust ctx.log(Level::Info, &format!("fed {} oil, now {}", params.Oil, self.Oil)); ``` Filter by type (type a name in the "All types" box and press Enter) and by level: **All**, **Info**, **Warnings** or **Errors**. Lines come newest first, 100 at a time; **Load older** at the bottom pages back. Each line has a colored dot (hover for Error, Warning, Info or Debug), its text, and a small line such as "3 min ago, village_beacon, 42": when, the type, and the Instance Id. Hover it for the exact time. The platform keeps lines for 24 hours; with none to show the tab says "No log lines match." and "Lines are kept for 24 hours." A **flow** is the platform's name for one call and everything it causes. **Show this call** on a line shows only the lines of that call, and a **One call** chip appears beside the level choices; its x shows every call again. That is how you follow one `FeedBeacon` from a player's request through the beacon's own lines. Lines written outside a call, such as when a Server Object starts or stops, have no flow and no button. The limits on how many lines a call and an app may log are on [Operations](/exec/operations#following-one-call). ## Activity The **Activity** tab shows "How players' calls to each Server Function went, busiest first." for the **Last hour**, **Last day** or **Last week**. Each Server Function gets a card, for example `village_beacon.FeedBeacon`: - how many calls: "1.2k calls in the last hour" (or "1 call in the last hour"), - "no failures", or "94% succeeded", - "usually answers in 12 ms (slowest 1.4 s)", - and a badge for each kind of problem there was: **N server errors** (your code answered with an error, or the call failed on the server), **N timed out** (a call past its **Time per Call** also restarts that Server Object from its last save, so see [Server Limits](./create-a-type.md#every-setting)), **N refused** (the caller was not allowed) and **N busy** (the server was busy; the caller may try again). Hover a badge for what it counts. If `FeedBeacon` is slow or refusing, this is where it shows. With no calls it says "No calls yet in this period." The numbers come from [Calls per endpoint](/exec/operations#calls-per-endpoint) on the platform. ## Settings The **Settings** tab holds Server Compute's settings for this project: "Server Compute's settings for this project. They are saved with the project, so everyone working on it shares them." It has one card so far, **Revisions**, with one setting. **Revisions to keep** is how many deployed revisions of each type's server code the project keeps, to view, compare or restore in the definition asset editor's [Server Code tab](./write-server-logic.md#view-compare-and-restore-an-earlier-deploy). It is a number from 3 to 50 and starts at 5. The hint under it reads "Per type, how many deployed revisions of its server code the project keeps to view, compare or restore (3 to 50). Saved in Config/DefaultEditor.ini; keeping fewer takes effect at the next deploy." The number is changed when you press Enter or leave the box. The setting is saved to `Config/DefaultEditor.ini` in your project, not to your own editor preferences, so commit that file and your team shares one value. If the file cannot be written the page says "Could not save to Config/DefaultEditor.ini, so the change lasts only until the editor closes. Check that the file can be written, for example that it is not read-only." Check the file out first if your version control keeps it read only. Each type's history is `Server//revisions.json`, which holds every kept revision's files in full, so a higher number makes it bigger; lowering the number trims each type the next time it is deployed. ## From the command line For a build server or a script, the same operations run without the editor window: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=deploy -yes ``` It uses Crowdy Studio's saved sign-in and app, so sign in and pick the app in the editor once on that machine first. There is no token to pass on the command line. | Operation | What it does | |---|---| | `-op=status` | The app's active version, whether the app is switched off, which types are switched off, whether its budget is paused and why, and its Server Objects and reserved memory against its limits (`instances`, `instanceLimit`, `reservedMb`, `maxReservedMb`). | | `-op=whoami` | Whether the machine is ready: `signedIn`, `scope` (`session` for a Studio sign-in, `organizationToken` for an organization token), `userId`, `appId`, `orgId` and `environment`. No token is read into the output. Without a sign-in it fails with "Sign in to Crowdy Studio on this machine", and without an app with "Choose the app in Crowdy Studio and press Sync"; both can be in one message. | | `-op=versions` | The versions, newest first. | | `-op=changes` | Which types differ from the live version, like the badges on the Overview tab, and whether a deploy would change anything. Read only. | | `-op=deploy -yes` | Assembles the project, builds what changed, in [parts](#a-deploy-in-parts) when it needs more than one build, waits, deploys it, and records what it sent in each type's `revisions.json`, like **Deploy**. `-batch=N` puts at most N crates in a build, 1 to 16 (16 unless given). `-rebuild` builds every crate instead of [reusing](#server-code-that-is-not-built-again) what the app already holds, as **Rebuild All and Deploy** does. See [The deploy's result](#the-deploys-result). | | `-op=starters -yes` | Builds and deploys the starter pack, like **Deploy starter pack**. It takes `-batch=N` too. | | `-op=addstarter -starter=session` | Adds a Game Kit starter, and the starters it needs, to the project, like **Add starter**: their crates under `Server/` and their definitions under `/Game/ServerObjects/GameKit/`. `-starter=` is one of the seventeen: `world`, `matchmaker`, `session`, `leaderboard`, `player`, `market`, `npcs`, `minigame`, `tactics`, `director`, `instance`, `territory`, `zone`, `deck`, `arena`, `race` or `ball`. `-config=` sets the named starter's **Config**, as JSON or `@` and a file, like `-inputs=`. The result's `added` lists them. See [Use the Game Kit starters](./use-game-kit-starters.md#add-a-starter). | | `-op=removestarter -starter=world -yes` | Removes a Game Kit starter from the project with every starter that runs under it or calls it: their definitions, their `Server/` folders with any edits, and the **Game Kit World** setting when it named one. The result's `removed` lists them. See [Remove starters](./use-game-kit-starters.md#remove-starters). | | `-op=generate [-type=village_beacon] [-yes]` | Generates server code, like **Generate** (see [Generate it](./write-server-logic.md#generate-it)). Without `-type` it generates every type that is out of date or has no server code. A type that needs you (a My Own File type with no Logic File, or a missing one) and a type with a problem are listed under `skipped` and do not fail the run. With `-type`, it generates that one type even when it is up to date; an unknown name fails and lists the types. A question a single Generate would ask refuses that type, and the question is the reason, unless you pass `-yes`. `-yes` answers only the safe questions: keep the old List value names on the server, move the old Type Name folder to the new one (not when the type's own Logic File is inside it), and add empty versions of the missing functions. It never accepts dropping values that saved instances hold, rewriting `lib.rs` lines it would remove, or a move when both folders exist. See [The generate result](#the-generate-result). | | `-op=exporttype`, `-op=newtype`, `-op=edittype` | Read a type's settings as a file, create a type from one, and change a type from one. See [Define a type from a file](#define-a-type-from-a-file). | | `-op=removetype -type=village_beacon [-yes]` | Removes an ordinary type the way **Remove from project...** does (see [Remove a type](#remove-a-type)). It refuses a Game Kit starter (use `-op=removestarter`), a definition outside `/Game/`, and a definition that other assets use, naming them. A redirector that is still in use is named too: "fix up redirectors in the Content Browser first". It deletes the definition asset and then, only if the asset is really gone, the type's Server Code folder when the SDK generated it, any unused redirectors to the asset, and the type's Game Kit World and Actor Starters settings. Without `-yes` it says what it would remove, changes nothing and exits 1. | | `-op=renametype -type=village_beacon -to=/Game/Beacon/CSO_Beacon [-yes]` | Moves or renames the definition asset the way the Content Browser does, which leaves a redirector. The type's Server Code folder follows it, and its Game Kit settings move with it. The Type Name does not change: a changed Type Name is a new type, which `-op=edittype` refuses. It refuses a definition or a target outside `/Game/`, and a target that is taken. Without `-yes` it says what it would do and changes nothing. | | `-op=activate -version=6 -yes` | Makes version 6 the active one, like **Make active**. | | `-op=disable -type=village_beacon -yes` | Switches one type off. | | `-op=enable -type=village_beacon -yes` | Switches it on. Without `-type`, `-op=enable -yes` switches the whole app back on, as the app's **Switch on** does. `-op=disable` still needs `-type`: switching the whole app off stays an action of the page. | | `-op=restart -type=village_beacon -yes` | Moves its running Server Objects to the active version, like **Restart**; the result's `stopped` says how many. | | `-op=restart [-yes]` | With no `-type`, restarts every type that has an instance running code older than the active version. Each instance is compared with the version that was active when it started, and an instance that started within a minute of the deploy counts as old too. It stops every running or starting instance of those types, the ones already on the active version included. Without `-yes` it lists each such type's instances that it would stop (Instance Id, phase and age), changes nothing and exits 1. After a rollback (`-op=activate` to an older version) it refuses and asks for `-type=`. | | `-op=call -type=village_beacon -fn=Refill -key=village -yes` | Calls a Server Only or Developers Only function as the developer, like **Call as Developer**. `-inputs=` gives its inputs as JSON, or `-inputs=@inputs.json` from a file, since a shell mangles quotes (a relative path is read from the folder the command runs in; a path with a space goes in quotes with its `@`, as in `-inputs="@C:\My Game\inputs.json"`, or with the quotes after the `@`, as in `-inputs=@"C:\My Game\inputs.json"`, where the whole path is read; `-config=` and `-bundle=` take the same forms); missing inputs keep their defaults, and a name the function has no input for is refused rather than ignored. `-key=` is needed unless the type has Only One Instance. `-fn=` finds a function by name, the built-in ones such as `AddMember` included, and the Developers Only functions of a [Game Kit starter](./use-game-kit-starters.md#functions-only-a-developer-calls), such as the matchmaker's `Configure`. It also calls a starter's read-only player functions as the developer, today the npcs starter's `state`. A 64-bit integer above 9007199254740991 must be written as a string, for example `{"Player": "9007199254740993"}`; a larger number written without quotes is refused with "... is above 9007199254740991; write it as a string, for example "9007199254740993"." The result gives `status`, `message` and `reply`, the outputs as JSON. A Game Kit starter's function that takes a `Ref`, such as the player's **Grant**, gets a fresh one when `-inputs=` leaves it out, and the result gives it as `ref`. | | `-op=grant -player=101910912345678 -bundle=@reward.json -yes` | Gives one player in-game soft currency and items through the [player starter](./use-game-kit-starters.md#functions-only-a-developer-calls)'s **Grant**, never real money. `-bundle=` is an `FCrowdyKitBundle` as JSON or `@` and a file, such as `{"Currency": {"gold": 100}, "Items": {"wood": 5}}`; `-xp=N` adds experience; `-ref=R` sends a `Ref` of your own, else a fresh one is made; `-type=` names the player starter when its Type Name is not `player`. The result gives `ref`, `status`, `message` and `reply`, what the grant earned the player. | | `-op=deposit -player=101910912345678 -bundle=@restore.json -yes` | Puts soft currency and items into one player's hub through **Deposit**, counting nothing toward quests or achievements, such as to give back what a bug took. It takes `-bundle=`, `-ref=` and `-type=` as `grant` does. | | `-op=instances` | Every Server Object the platform has placed: its type, Instance Id, phase (`idle`, `starting`, `running` or `stopping`), epoch, `heldBack` when the platform will not place it, with the reason, and `orphaned`, true for one whose type the active version no longer deploys (a deploy or a rollback dropped the type and stopped it; it keeps its saved state). | | `-op=logs` | Recent log lines. Optional `-type=`, `-flow=` and `-limit=`. `-level=all`, `info`, `warnings` or `errors` filters by level, like the Logs tab's choices. `-before=` pages back: pass the previous page's `nextBefore`. | | `-op=stats` | Call counts per Server Function. Optional `-type=`. `-window=hour`, `day` or `week` picks the period, like the Activity tab's. | | `-op=batch -ops=` | Runs the operations in a file, one per line, in this one editor start. See [Several operations in one start](#several-operations-in-one-start). | `-yes` stands in for the confirmation the page asks. `deploy`, `starters`, `activate`, `disable`, `enable`, `restart`, `call`, `grant` and `deposit` replace, switch, restart or run the app's live server code, so without `-yes` they refuse with "This changes the app's live server code or what it holds; pass -yes to confirm." and send nothing. The read-only operations (`status`, `versions`, `changes`, `instances`, `logs`, `stats`) do not need it. `addstarter` changes only the project and needs neither `-yes` nor a sign-in. `removestarter` changes only the project too, but deletes the starters' code with any edits, so without `-yes` it names what it would remove and refuses, for example "This deletes session, matchmaker, world: their definitions and server code, edits included; pass -yes to confirm." An unknown `-op` is refused with the list of operations. `generate` and `newtype` change only the project and need no `-yes`; `edittype`, `removetype` and `renametype` change it only with `-yes`. **Close the Unreal Editor first.** An operation that writes the project (`generate`, `newtype`, `edittype` with `-yes`, `addstarter`, `removestarter`, `removetype` and `renametype` with `-yes`) refuses while an Unreal Editor is running: "An Unreal Editor is running. Close it first: it holds this project's Server Object types in memory and would save its own copy over what this op writes. Pass -editoropen to write anyway." `-editoropen` writes anyway. **A read-only `Config/DefaultGame.ini`.** The Game Kit World and Game Kit Actor Starters settings live in that file. When `removetype`, `renametype`, `addstarter` or `removestarter`, or **Remove from project...** on the page, would change them and the file is read-only, as a file checked in to source control can be, it refuses before changing anything: "...DefaultGame.ini is read-only (checked in?), and this changes the Game Kit settings it holds; make it writable, then run again; nothing was changed". The engine would otherwise keep the change in memory only and lose it when the editor closes. The command's exit code is its own result: 0 when the operation succeeded and 1 when it did not, whatever else the editor logs. It writes one line per step to the log under `LogCrowdyExec`. Add `-out=` to also get the result as one JSON object: `op`, `ok`, `error` and `errorCode` (both empty when it succeeded), and the fields of that operation. A CI job can read it: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=deploy -yes -out=Saved/deploy.json ``` A job that runs `-op=deploy` without `-yes` fails with exit code 1 and deploys nothing, so add it once you are sure the job should replace the app's server code. `-op=deploy` refuses, as the page does, when a type's server code is not ready (it needs generating, is out of date, or has a problem), and says why. Regenerate and commit the server code before the job runs; a `Server/` folder that no longer matches its definition asset fails the job instead of deploying old code. Unlike the page, the command knows nothing about unsaved edits in an open editor; it sends what is on disk. It also does not check whether anything changed: it deploys what is on disk every time, as a new version, even when every crate is reused and nothing is built, so ask `-op=changes` first if the job should skip a deploy that would change nothing. ### Several operations in one start Starting the editor takes most of the time of a short operation. `-op=batch` runs several in one start: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=batch -ops=Saved/ops.txt ``` Each line of the file is the complete argument list one run of that operation would take. Blank lines and lines that start with `#` are skipped: ```text # read first -op=status -out=Saved/status.json -op=changes -out=Saved/changes.json -op=call -type=leaderboard -fn=Submit -key=weekly -inputs="@Saved/score.json" -yes -out=Saved/submit.json ``` The operations run in order. Each one connects on its own, has its own time limit, and writes its own `-out` file exactly as it would alone. The batch stops at the first operation that fails, and the operations after it write nothing; put `-batchcontinue` on a line to go on after that line fails. The exit code is 1 if any operation failed and 0 otherwise. The log has one line per operation, such as "Server Compute batch: 2/3 -op=changes ok". A line cannot be `-op=batch`, and a file with such a line, or with no operations, runs nothing. ### The deploy's result `-op=deploy` logs what it builds and what it reuses, such as "building 3 pieces of server code in 1 build; 15 unchanged, named by their digest", then each build as it goes ("build 1 of 2: ... queued", "build ... succeeded against ckx-sdk 0.12.1 after 41 s"), "every build succeeded; deploying", and "deployed as version 9, now the app's active server code", which names the staging versions after it when there were any. With `-out=` the JSON result adds: | Field | What it holds | |---|---| | `version` | The version the deploy made. | | `builds` | Every build sent, in order, each with `buildId`, `buildStatus`, `buildLog`, `sdkVersion` and its `artifacts`. Empty when nothing was built. | | `buildId`, `buildStatus`, `buildLog`, `sdkVersion` | The last build's, as before builds were split. `sdkVersion` is the `ckx-sdk` version the platform compiled the server code against, whatever version the crates name. | | `reused` | Each crate that was not built again: `crate` and the `digest` it was named by. | | `artifacts` | Every crate's module in this deploy, built or reused: `crate`, `digest` and `sizeBytes`. | | `stagedVersions` | The staging versions a [deploy in parts](#a-deploy-in-parts) made, oldest first. Empty for a deploy in one part. | | `restoredVersion` | The version made active again after the last staging version, or 0 when none was. | A failed deploy fills the same fields as far as it got, and its `error` says what is active now, for example "... Version 8 is active again, as before the deploy." A build that failed gives "The build ... failed, so nothing was deployed; its log is above and in buildLog". ### Changes and records `-op=changes` compares the project with the live version the way the page does, without changing anything or needing `-yes`. It logs one line per type, for example `village_beacon: changed (server code, save interval), live revision 2`, and a last line saying `nothing changed`, `a deploy would change what runs` or `not ready to deploy; see problems`. With `-out=` the JSON result adds: | Field | What it holds | |---|---| | `liveVersion` | The live version's number, or 0 when nothing is live. | | `types` | One row per type in the project, then one for each type on the server that the project no longer has: `type` (the Type Name), `change`, `what` and `liveRevision`. | | `change` | `unchanged`, `changed`, `new`, `unknown` (the live version runs code this project has no record of), `removed` (on the server, not in the project) or `not_compared` (the type's server code is not ready). | | `what` | For a changed type, what differs: any of `server code`, `save interval`, `idle timeout`, `platform data`, `player access`, `server limits`, `kind`, `scaling`, and for a Game Kit starter `config` and `parent`. Otherwise empty. | | `liveRevision` | Where the revision the live version runs sits in the type's `revisions.json`, counting from 0 for the newest, or null when there is none. | | `problems` | Why the project cannot be deployed yet, one sentence each, as the page shows them. Empty when it can. | | `warnings` | What may go wrong after a deploy without stopping it, one sentence each, such as platform data the server code calls into without asking for it. `-op=deploy` writes them too, and both log each as a `warning:` line. | | `ready` | True when `problems` is empty. | | `hasChanges` | True when a deploy would change what runs (a type is `new`, `changed`, `unknown` or `removed`), and also when `ready` is false, since a type whose code is not ready differs from what a deploy would send once it is. | A job that should deploy only when something changed can run `-op=changes`, stop when `hasChanges` is false, and otherwise run `-op=deploy -yes`. `-op=deploy` writes each type's `revisions.json` after the deploy succeeds and logs "recorded the deployed revision of 2 types". A record that cannot be written does not fail the job: the deploy stands, the log warns "the deploy record was not saved for ...", and the JSON result lists each one under `recordErrors` (an empty list when all were saved). A job that builds the project's server code in CI should commit the changed `revisions.json` files afterwards, so the next `-op=changes` and everyone's Overview tab agree with what is live. `-op=starters` records nothing. ### The generate result `-op=generate` logs one line per type. With `-out=` the JSON result adds: | Field | What it holds | |---|---| | `generated` | The types it generated. | | `refused` | The types it did not generate because a question a single Generate would ask was not answered by `-yes`: `type` and `reason`, the question itself. | | `skipped` | The types it left alone because they need you (a My Own File type with no Logic File, or a missing one) or have a problem. These do not fail the run. | | `notes` | Other lines about what the run did or did not do. | | `saved` | The definitions it saved. Generating can change a definition, for example by adding List Value Names entries. | | `logicGaps` | Per type, what `logic.rs` still needs: `logicFile` (the file), `missing` (functions to write), `leftover` (functions the file defines that the type no longer has) and `stubsAdded` (empty versions it added). | The exit code is 1 if anything was refused or the save failed. A job can generate, then read `logicGaps` to see which functions to write next. ### Define a type from a file A Server Object type is a definition asset you normally fill in with the Details panel. From the command line you write the same settings as a JSON file, a **spec**, and create or change the type from it. `-op=exporttype` shows what a spec looks like. A spec holds the settings the Details panel shows, under their C++ property names: `TypeName`, `Functions`, `bOnlyOneInstance`, `StateForm`, `StateList` and so on, exactly as `-op=exporttype` writes them. What the bake rebuilds on its own, such as the baked field tables, is not in a spec. - **Keys are matched with their capitals.** `typename` is not `TypeName`. - **An enum is its value name** as the editor writes it, such as `"List"` for a StateForm or `"Every"` for a timer. A struct or enum asset is its path, such as `/Script/MyGame.MyState`, and `null` is none. - **`CanCall` and `ListenTo` are Type Names,** not asset paths. - **A 64-bit integer is a string,** such as `"9007199254740993"`. - **A List is an array of variables.** `StateList`, `ParamsList`, `ReplyList` and `PayloadList` each hold objects with `Name`, `Type`, `TypeObject` (for Enum, Struct and object kinds), `Containers` (at most 2), `KeyType` and `KeyTypeObject` (for a map), `Id`, `MetaData` and `Default`. `ClampMin` and `ClampMax` in `MetaData` are a variable's Value Range. A spec is refused whole, and nothing changes, when it has any problem: an unknown or wrongly capitalised key, a value of the wrong kind or outside its range, a path that does not load, control characters in a name or text, a Logic File outside the project or not a `.rs` file, or a problem the bake finds. Every problem is listed in `errors` of the result, not only the first. For example, a tip jar with a Total and the last tipper, both visible to players, an `AddTip` function and a timer, saved as `tip_jar.json`: ```json { "TypeName": "tip_jar", "bOnlyOneInstance": true, "StateForm": "List", "StateList": [ { "Name": "Total", "Type": "Int64" }, { "Name": "LastTipper", "Type": "String" } ], "WatchedFields": ["Total", "LastTipper"], "Functions": [ { "Name": "AddTip", "ParamsForm": "List", "ParamsList": [ { "Name": "Amount", "Type": "Int32", "MetaData": { "ClampMin": "1", "ClampMax": "100" } } ], "ReplyForm": "List", "ReplyList": [ { "Name": "Total", "Type": "Int64" } ] } ], "Timers": [ { "Name": "Hourly", "Repeat": "Every", "Seconds": 3600 } ] } ``` Create the type, then generate its server code: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=newtype -spec=@tip_jar.json UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=generate -type=tip_jar ``` The operations: | Operation | What it does | |---|---| | `-op=exporttype [-type=T] [-spec=]` | Reads the type's settings. The spec is under `definition` in the `-out` result; with `-spec=` it also writes the bare spec to that file. Without `-type` it exports a new type's defaults, as a template to fill in. | | `-op=newtype -spec= [-path=/Game/...]` | Creates a definition asset `CSO_`, so `tip_jar` becomes `CSO_TipJar`, in `-path` (`/Game/ServerObjects` unless given; it must start with `/Game/`). A setting the spec leaves out keeps its default. It refuses a Type Name that any type in the project already has, in any capitals, and a Code Source of Game Kit Starter: add a starter with `-op=addstarter`. When another type already holds the asset name (`tip__jar` and `tip_jar` both make `CSO_TipJar`), the refusal names that type and suggests `-path=` to put the new asset somewhere else. | | `-op=edittype -type=T -spec= [-yes]` | Replaces the type's settings with the spec's. The spec must hold every top-level setting, so start from `-op=exporttype`; a partial spec cannot empty `Functions` by leaving it out. Settings inside a nested object may be left out. Without `-yes` it changes nothing: it reports `changed` (what would differ) and `renamedListValues`, and exits 1, except that a spec identical to the type succeeds with `changed` empty. With `-yes` it saves the definition, and if the save fails it puts the type back as it was. | `edittype` refuses a changed Type Name, because to the server a new name is a new type (see [After you change the definition](./write-server-logic.md#after-you-change-the-definition)), and a switch to or from a Code Source of Game Kit Starter. `newtype`, and `edittype` with `-yes`, write the project, so they refuse while an editor runs, as described above. The next step after either is `-op=generate -type=T`. ## Be careful on a shared app A deploy replaces the app's active server code for every player and every teammate on that app at once, and there is no separate step to try it first: the staging versions of [a deploy in parts](#a-deploy-in-parts) run what was already live and are not a preview. Use an app of your own for experiments, keep `Server/` in version control so a deploy can be repeated, and know how to make the previous version active before you deploy. ## Related - [Write its server logic](./write-server-logic.md) - [Create a Server Object type](./create-a-type.md) - [Use the Game Kit starters](./use-game-kit-starters.md): Add starter, world as the root, and removing starters - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md) - [Troubleshooting](./troubleshooting.md): outcomes, refusals and `crowdy.exec.trace` - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) - [Crowdy Studio](../studio/overview.md) - [Builds and starter packs](/exec/builds) and [Operations](/exec/operations) on the platform # Use the Game Kit starters Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/use-game-kit-starters # Use the Game Kit starters :::note Server Objects run on [ck-exec](/exec/intro). The starters themselves are the platform's; see [Game Kit](/game-kit/intro). ::: The platform's [Game Kit](/game-kit/intro) ships **starters**: ready-made server code for systems most games need, such as a world clock, matchmaking and a game session. In Unreal a starter is not a new kind of thing. It is an ordinary [Server Object type](./create-a-type.md) whose **Code Source** is **Game Kit Starter**: a definition asset with variables and Server Functions, and a Rust crate under `Server//` that is the starter's own code instead of code Generate writes. You add it to the project once, deploy it with your own types, and use it with the same nodes and the same C++ calls as any other Server Object. ## The starters **Add starter** offers seventeen of the platform's starters. Each becomes a type of the project with the Type Name of the starter: | Type Name | Definition | What it keeps | Instance Id | |---|---|---|---| | `world` | `CSO_Kit_World` | The game day's clock, the weather, resource nodes that grow back, liveops events with their modifiers, and the players online. While the project has it, it is the app's root. | None: it has **Only One Instance**. | | `matchmaker` | `CSO_Kit_Matchmaker` | One matchmaking queue per Instance Id, with a rating window that widens while players wait, and a match every player must accept, which reserves a session. | The queue's name, such as `ranked` or `casual`. | | `session` | `CSO_Kit_Session` | One game session per Instance Id, from lobby to result: a host, ready checks, turns with a time limit, scores and a winner. With a board in its Config, a finished session posts each player's score to that leaderboard. | The session's key, such as the one a match reserved. | | `leaderboard` | `CSO_Kit_Leaderboard` | One leaderboard per Instance Id: it ranks the scores your server code submits, with ties, seasons that end on a schedule, and the last season's standings. Players read it; they never submit to it. | The board's name, such as `weekly-score` or `fastest-lap`. | | `player` | `CSO_Kit_Player` | One hub per player: their items, in-game soft currency, crafting, loot, idle production, levels, achievements and quests. Only that player reads their inventory, wallet and progress; anyone reads their profile. | The player's user id; empty for the signed-in player. | | `market` | `CSO_Kit_Market` | One market per Instance Id: vendors with fixed prices, an order book for each item it lists, and trades between two players. It holds nothing itself: what a player puts up waits in their own `player` hub until it settles. | The market's name, such as `town` or `harbor`, at most 48 bytes. | | `npcs` | `CSO_Kit_Npcs` | One area per Instance Id: six mobs that wander, chase the nearest player and take players' hits, two NPCs that walk their routes and talk, and the pets players adopt there. They are shown to the players near them as [actors](#show-a-starters-actors). | The area's place on a grid of 4 by 4 chunks, `x,y`, such as `10,-4`: the key **This Actor's Area** gives for a player standing in it. | | `minigame` | `CSO_Kit_Minigame` | One duel per Instance Id: two players each throw rock, paper or scissors, the hub keeps both throws hidden until both are in or the round's time runs out, and the first to win a majority of the rounds wins (best of 3 by default). | Any name you choose, such as `tavern-1`. | | `tactics` | `CSO_Kit_Tactics` | One turn-based grid battle per Instance Id, up to 4 players: each brings a unit, the host starts it, every unit moves and attacks once in the players' phase, then the hub's enemies move and strike, until one side has no unit standing. | Any name you choose, such as `skirmish-1`. | | `director` | `CSO_Kit_Director` | One tower defence per Instance Id: waves of enemies walk a lane to the players' base, players build towers with the gold kills pay and strike enemies themselves, and the last wave brings a boss that changes phase as it weakens. It sends its events as one message, **EncounterEvent**. | Any name you choose, such as `gate-defense`. | | `instance` | `CSO_Kit_Instance` | One dungeon run per Instance Id for a party: floors the hub generates from the run's seed, room by room (fight, treasure, rest, boss), until the party clears the last floor, is wiped, abandons the run or leaves it idle. With a board in its Config, a cleared or wiped run posts each member's depth to that leaderboard. | Any name you choose, such as `cellar-run-7`. | | `territory` | `CSO_Kit_Territory` | One war over control points per Instance Id: players join a faction, a faction captures a point by having more members standing on it than any other, points pay income, and the first faction to the winning score wins. It reads where players are in the world, and sends its events as one message, **TerritoryEvent**. | The map's name, such as `valley`. | | `zone` | `CSO_Kit_Zone` | One shrinking circle per Instance Id, as in a battle royale: it holds, then shrinks phase by phase, and wears down every player outside it until one is left. It reads where players are in the world, and sends its events as one message, **ZoneEvent**. | The match's name, such as `match-1`. | | `deck` | `CSO_Kit_Deck` | One card table per Instance Id, two to six players, playing crazy eights: the hub keeps the deck's order and every hand, the table shows only how many cards each player holds, and each player reads their own hand with a call of their own. | Any name you choose, such as `tavern-2`. | | `arena` | `CSO_Kit_Arena` | One real-time fight per Instance Id, up to 10 players: each picks a class, warrior or mage, with abilities the hub referees, a strike on the fighter nearest the aim, a projectile or an area that goes off after a delay. Each player's client reports where they are a few times a second and the hub keeps what their class's speed allows. The fallen come back after a few seconds, and the first to ten kills, or the most kills when the time runs out, wins. It sends its events as one message, **ArenaEvent**. | Any name you choose, such as `yard-1`. | | `race` | `CSO_Kit_Race` | One race per Instance Id, up to 8 racers: a countdown on the start line, then a course of gates in order for a number of laps, with checkpoint and lap times, places and the winner's run as a ghost. Each racer's client reports where they are, and the hub times the positions it kept. With a board in its Config, a finished race posts each finisher's time to that leaderboard. It sends its events as one message, **RaceEvent**. | Any name you choose, such as `mill-loop-1`. | | `ball` | `CSO_Kit_Ball` | One ball game per Instance Id, red against blue, up to 5 a side: a ball the hub moves, claimed near it, passed and shot, a goal in each end, the first to the winning score or the better score when time runs out. Each player's client reports where they are, and the ball is shown to the players near it as an [actor](#show-a-starters-actors). It sends its events as one message, **BallEvent**. | The match's name, such as `green-1`, of at most 29 characters, with no `@` or spaces, when its ball is shown. | The `player` and `market` starters keep in-game soft currency, such as gold and gems, which is game state like a score. It is never real money, and nothing in them buys, sells or pays out real money. What each one does, function by function, is on the platform's pages: [World and liveops](/game-kit/modules/world), [Matchmaking](/game-kit/modules/matchmaking), [Sessions](/game-kit/modules/sessions), [Leaderboards](/game-kit/modules/leaderboards), [Player](/game-kit/modules/player), [Market](/game-kit/modules/market), [NPCs](/game-kit/modules/npcs), [Duels](/game-kit/modules/minigames), [Grid battles](/game-kit/modules/tactics), [Waves and bosses](/game-kit/modules/encounters), [Private runs](/game-kit/modules/instances) and [Control points and circles](/game-kit/modules/territory), which also covers the zone, [Card tables](/game-kit/modules/tables), [Arenas](/game-kit/modules/arenas) and [Races and ball games](/game-kit/modules/racing). The SDK names the functions in Unreal style, so the platform's `harvest` is **Harvest**, `state` is **GetState**, `status` is **GetStatus** and `me` is **GetMyPlace**. A starter brings the starters it needs. The matchmaker runs under the world and calls the session, and the session runs under the matchmaker, so adding `session` or `matchmaker` adds those three, and adding `world` adds only `world`. The leaderboard runs under the world, so adding it adds `world` and `leaderboard`. A session calls a leaderboard only when [its Config names a board](#post-a-sessions-scores-to-a-leaderboard), so adding the session does not add the leaderboard: add it yourself when you want one. The player runs under the world; the market runs under the world and calls the player, so adding `market` adds `world`, `market` and `player`. The npcs starter runs under the world and reads its day and night, so adding `npcs` adds `world` and `npcs`. The six game-mode starters, `minigame`, `tactics`, `director`, `instance`, `territory` and `zone`, each run under the world and call nothing that they need, so adding one adds `world` and itself. An `instance` calls a leaderboard only when [its Config names a board](#send-a-party-down-a-dungeon), as the session does. The `deck` runs under the world and calls nothing either, so adding it adds `world` and `deck`. The `arena` runs under the world and calls nothing too, so adding it adds `world` and `arena`, and the `ball` the same. The `race` runs under the world and calls a leaderboard only when [its Config names a board](#run-a-race), as the session does. **Add starter** is not **Deploy starter pack**. The starter pack deploys every starter of the platform as it is, in place of your project, to try the platform out; see [Deploy starter pack](./deploy-with-server-compute.md#deploy-starter-pack). **Add starter** makes a starter part of your project, deployed with your own types. ## Add a starter On the [Server Compute](./deploy-with-server-compute.md) page, the strip under the tabs has an **Add starter** menu beside **Deploy starter pack**. Pick any of the seventeen, from `world` to `ball`. It asks first, naming every starter it adds: > Adds world, matchmaker, session to the project as Server Object types: each one's Rust under `Server/`, which you can change, and its definition under /Game/ServerObjects/GameKit. With world, world is the app's root from the next deploy in place of the Crowdy SDK's: your own types run under it, and Get Players Online reads it. Confirm, and for each starter it: - copies the starter's crate, its `Cargo.toml` and `src/lib.rs`, from the Crowdy SDK into `Server//`, and - saves its definition under `/Game/ServerObjects/GameKit/`: `CSO_Kit_World`, `CSO_Kit_Matchmaker`, `CSO_Kit_Session`, `CSO_Kit_Leaderboard`, `CSO_Kit_Player`, `CSO_Kit_Market`, `CSO_Kit_Npcs`, `CSO_Kit_Minigame`, `CSO_Kit_Tactics`, `CSO_Kit_Director`, `CSO_Kit_Instance`, `CSO_Kit_Territory`, `CSO_Kit_Zone`, `CSO_Kit_Deck`, `CSO_Kit_Arena`, `CSO_Kit_Race` or `CSO_Kit_Ball`. A notice says "Added world, matchmaker, session; deploy to run them". The types appear on the Overview tab as **New**, and the next **Deploy** sends them with your own types. A starter the project already has is skipped, and picking one the project has whole says "The project already has the world starter." Nothing is written when something is in the way, and the message says what to do: - "The project already has a Server Object type named session (...), which the session starter needs; rename that type first" - "... already exists, so the world starter's server code would replace what it holds; move that folder first" - "/Game/ServerObjects/GameKit/CSO_Kit_World already exists; move or delete it to add the world starter" Commit the new `Server//` folders and the definitions with the rest of your project. From the command line, `-op=addstarter` does the same without asking: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=addstarter -starter=session ``` It changes only the project, so it needs no `-yes` and no sign-in. It logs "added world, matchmaker, session", and its `-out=` result has `starter` and `added`, the Type Names it added. `-config=` sets the named starter's **Config** as it is added, as JSON or as `@` and a file holding it, so `-starter=session -config=@session.json` with `{"board": "weekly-score"}` in `session.json` adds a session that posts its scores to that board; the starters it brings keep an empty Config. A Config that is not a JSON object is refused before anything is written: "the Config for session is not a JSON object: ...". So is a `world` Config whose `day_ms`, `nodes`, `node_amount` or `regrow_ms` is not a whole number without a decimal point or exponent, from 0 (`nodes` at most 65535), in the editor's **Add starter** too; [Change a starter](#change-a-starter) says more. So is a Config for a starter the project already has, which it would not use: "The project already has the session starter, so this Config would not be used; set Config in its definition's Game Kit category instead". See [From the command line](./deploy-with-server-compute.md#from-the-command-line). ### The world becomes the app's root Every app has one root type, which every other type runs under. Without starters it is the Crowdy SDK's own root, which keeps the players online for **Get Players Online**. Once the project has `world`, `world` is the root instead: Deploy does not send the SDK's root, your own types run under `world`, and the first deploy lists `root` among what it removes. Adding `world` also sets **Project Settings, Crowdy SDK, Server Objects, Game Kit World** (`GameKitWorld` in `Config/DefaultGame.ini`) to `CSO_Kit_World`. **Get Players Online** reads the count from the definition that setting names, the `Online` variable of the world, and from the SDK's root when it is empty. Commit `Config/DefaultGame.ini` too, so a packaged game reads the same root the app runs. When the setting and the project disagree, Deploy warns, for example "Get Players Online reads ..., but this deploy's root is world; set Game Kit World to ... in Project Settings, Crowdy SDK". Until the app runs a deploy with `world`, **Get Players Online** returns false and logs "This app has no Game Kit world, so Players Online has no count: deploy the app from Server Compute, or clear the Game Kit World setting if the project no longer has the world starter. It is asked again every minute." See [How many players are online](./from-blueprint/read-and-follow-variables.md#how-many-players-are-online). **Get Players Online** also returns false, with 0, for good when the world's snapshot carries no player count: a world deployed from the platform's own starter pack sends none, where the SDK's `world` starter does. The log says why once. `FCrowdyKitWorldConditions` has a `bHasOnline` flag for this, so read it before `Online` in C++, or call `UCrowdyServerObjectLibrary::ReadKitWorldOnline(Target, World, Online, bUnsent)`, which returns false with `bUnsent` set when the world sent no count. A `world` definition saved with an earlier SDK reports "has changed since ... was saved" until you save it again. ## Use it from Blueprint and C++ A starter is used like any other Server Object type, so the pages on [getting one](./from-blueprint/get-a-server-object.md), [reading its variables](./from-blueprint/read-and-follow-variables.md), [hearing its messages](./from-blueprint/hear-server-messages.md) and [calling its functions](./from-blueprint/call-functions.md) apply as they are, from Blueprint and [from C++](./from-cpp/get-a-server-object.md): - **Get the object** with **Get Server Object**, a typed node with **Find By Asset**, or the **Crowdy Server Object** component, given the starter's definition and its Instance Id. The world has **Only One Instance**, so its nodes need neither a Target nor an Instance. In C++, `Acquire` the definition with the Instance Id. - **Its variables** are what the starter shows every player: the world's conditions, a queue's counts, a session's whole state. **Get** and **On Changed** read and follow them, such as **Get Online (CSO_Kit_World)** or **On Phase Changed (CSO_Kit_Session)**. In C++, `GetState()` holds the State struct and `WatchValues` reports each change. - **Values only this player reads.** A player's inventory, wallet and progress, and a market's orders and trades for this player, are variables too: **Get Wallet (CSO_Kit_Player)**, **On Wallet Changed (CSO_Kit_Player)**, **On Orders Changed (CSO_Kit_Market)** and `GetState()` hold them. See [Values only the player reads](#values-only-the-player-reads). - **Its functions** have a **Call** node each, such as **Call Harvest (CSO_Kit_World)**, and `Call` in C++. The answer comes back as for any Server Function, with the [outcome and code](#refusals-and-their-codes) below. - **Its messages.** A hub's `event` messages reach the game as the type's [server messages](./create-a-type.md#server-messages), matched by their topic, which is the message's server name. The world, matchmaker and session starters put everything in their state and send no events, so their definitions declare no messages. The `director`, `territory` and `zone` starters send their events as [one message each](#react-to-a-directors-waves), and the `arena` sends **ArenaEvent**, described in [Fight in an arena](#fight-in-an-arena). If you change a starter to send one, add the message to its definition. What is particular to a starter: - **Its records.** A starter whose hub keeps a collection, a set of members it changes one at a time, holds them as [records](./from-blueprint/read-and-follow-variables.md), one per member, keyed by the member's id as text. The world's resource nodes are its records: **Find Record** with `3` gives node 3's values (`FCrowdyKitWorldNode`: `Id`, `Amount`, `RegrowAt`), **Get Record Ids** lists every node, **On Records Changed** and **On Records Updated** report each harvest and regrowth, and **Watch Record** follows one node. A record has no owner or team. The object is **Ready** once both its variables and its records are read, so a record is there when **On Status Changed** says Ready. The events come back from **Call GetState** (`Events`), and a player's place in a queue from **Call GetStatus**. - **Pacing and resending.** Calls to starters go through the Game Kit's own client, which keeps them under the platform's limit and sends one again when that is safe. The SDK's own retry of a busy call is not added on top. - **A starter that will not let this player read it** fails with "The server code for session refused to let this player read it (forbidden): ..." and stays **Failed**, as an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) object of someone else's does. A refusal coded `cooldown` or `unavailable` is waited out like any other. ### The values in C++ The State, inputs and outputs of the starters are C++ structs and enums the SDK declares in `CrowdyGameKitTypes.h`, in the `CrowdyExec` module, and Blueprint sees them too: | Starter | Structs and enums | |---|---| | world | `FCrowdyKitWorldConditions` (the State: `TimeOfDay`, `bNight`, `Weather`, `Modifiers`, `Online` with `bHasOnline`), `FCrowdyKitWorldView` (what **GetState** answers: `Conditions`, `ClockMs`, `Nodes`, `Events`, `Online`), `FCrowdyKitWorldNode`, `FCrowdyKitWorldEvent`, `FCrowdyKitHarvestInputs`, `FCrowdyKitHarvestReply`, `FCrowdyKitCancelEventInputs`, `FCrowdyKitCancelEventReply`, `ECrowdyKitWeather` | | matchmaker | `FCrowdyKitQueueCounts` (the State: `Waiting`, `Proposals`, `Forming`), `FCrowdyKitMatchJoinInputs`, `FCrowdyKitMatchPlace` (what **Join**, **Accept** and **GetStatus** answer), `ECrowdyKitMatchState`, `FCrowdyKitMatchmakerConfig` | | session | `FCrowdyKitSessionState` (the State, which **GetState** answers too), `FCrowdyKitSessionConfig` (with `Board`), `FCrowdyKitSessionMove`, `FCrowdyKitSessionReadyInputs`, `FCrowdyKitSessionReadyReply`, `FCrowdyKitSessionActInputs`, `FCrowdyKitSessionActReply`, `ECrowdyKitSessionPhase` | | leaderboard | `FCrowdyKitBoardView` (the State: `Season`, `SeasonStarted`, `Entries`, `Top`), `FCrowdyKitBoardPlace` (`Rank`, `Player`, `Score`; what **GetMyPlace** answers), `FCrowdyKitBoardPlaces` (what **GetTop** and **GetAround** answer), `FCrowdyKitBoardPageInputs`, `FCrowdyKitBoardAroundInputs`, `FCrowdyKitBoardSeason`, `FCrowdyKitBoardSubmitInputs`, `FCrowdyKitBoardSubmitReply`, `FCrowdyKitBoardConfig`, `FCrowdyKitBoardRollReply`, `ECrowdyKitBoardKeep`, `ECrowdyKitBoardOrder` | | player | `FCrowdyKitPlayerState` (the State: the public profile, `Player`, `Level`, `Achievements`, `QuestsDone`, and the player's own `Inventory`, `Wallet` and `Progress`), `FCrowdyKitBundle` (`Items` and `Currency`, each a `TMap`: a price, a reward, an offer), `FCrowdyKitPlayerMe` (what **GetMe** answers), `FCrowdyKitInventory` (**GetInventory**: `Slots`, `Stacks` of `FCrowdyKitStack`, `Mail`, `Held`), `FCrowdyKitWallet` (**GetWallet**: `Balances`, `Held`), `FCrowdyKitProgress` (**GetProgress**: `Level`, `Xp`, `Next`, `TotalXp`, `Achievements` of `FCrowdyKitAchievement`, `Quests` of `FCrowdyKitQuest`, each with `FCrowdyKitObjective`s), `FCrowdyKitProduction` and `FCrowdyKitProducer` (in **GetMe**), `FCrowdyKitPlayerProfile` (**GetProfile**), `FCrowdyKitPlayerRules` (**GetRules**: the `FCrowdyKitItemDef` items, soft currencies, `FCrowdyKitRecipe`s, `FCrowdyKitLootTable`s with their `FCrowdyKitLootEntry`s, `FCrowdyKitQuestDef`s and `FCrowdyKitAchievementDef`s its Config sets), `FCrowdyKitEarned`, `FCrowdyKitCraftInputs`, `FCrowdyKitCraftReply`, `FCrowdyKitOpenLootInputs`, `FCrowdyKitOpenLootReply` (with an `FCrowdyKitLootRoll`), `FCrowdyKitQuestInputs`, `FCrowdyKitStepInputs`, `FCrowdyKitDiscardInputs`, `FCrowdyKitGrantInputs`, `FCrowdyKitRecordInputs`, `FCrowdyKitDepositInputs`, `ECrowdyKitQuestState` | | market | `FCrowdyKitMarketState` (the State: `Vendors`, `Currency`, `Quotes`, `OpenTrades`, `PendingSteps`, and this player's `Orders` and `Trades`; **GetState** answers it too, without those two), `FCrowdyKitVendors` (**GetVendors**), `FCrowdyKitVendor`, `FCrowdyKitVendorOffer`, `FCrowdyKitQuote`, `FCrowdyKitBookInputs`, `FCrowdyKitBook` (**GetBook**: `Bids` and `Asks`, each a list of `FCrowdyKitPriceLevel`), `FCrowdyKitOrders` (**GetOrders**), `FCrowdyKitOrder`, `FCrowdyKitTrades` (**GetTrades**), `FCrowdyKitTrade`, `FCrowdyKitTradeSide`, `FCrowdyKitShopInputs` (**Buy** and **Sell**), `FCrowdyKitOrderInputs`, `FCrowdyKitPlacedOrder`, `FCrowdyKitCancelOrderInputs`, `FCrowdyKitOpenTradeInputs`, `FCrowdyKitTradeReply`, `FCrowdyKitTradeOfferInputs`, `FCrowdyKitTradeInputs`, `FCrowdyKitAcceptTradeReply`, `ECrowdyKitOrderSide`, `ECrowdyKitTradeState` | | npcs | `FCrowdyKitAreaState` (the State, which is empty), `FCrowdyKitAreaMember` (a record: `Id`, `Type` of `ECrowdyKitAreaMemberType`, `X`, `Y`, and the fields of its type), `FCrowdyKitAreaMembers` (what **GetState** and **GetPets** answer: `Members`), `FCrowdyKitAttackInputs` (`Mob`), `FCrowdyKitAttackReply` (`Hp`), `FCrowdyKitTalkInputs` (`Npc`; **Talk** answers the NPC's `FCrowdyKitAreaMember`), `FCrowdyKitAdoptInputs` (`Species`, `Name`, each 1 to 24 characters), `FCrowdyKitAdoptReply` (`Pet`, the new pet's id), `FCrowdyKitPetInputs` (`Pet`: **SummonPet**, **DismissPet** and **ReleasePet**), `FCrowdyKitRenamePetInputs` (`Pet`, `Name`) | | minigame | `FCrowdyKitMinigameState` (the State: `Phase`, `Seats` of `FCrowdyKitMinigameSeat`, `BestOf`, `Round`, `Deadline`, `Last` an `FCrowdyKitMinigameReveal`, `Winner`), `FCrowdyKitMinigameJoinReply`, `FCrowdyKitMinigameThrowInputs`, `FCrowdyKitMinigameThrowReply`, `ECrowdyKitMinigamePhase` | | tactics | `FCrowdyKitTacticsState` (the State: `Phase`, `Round`, `Width`, `Height`, `Obstacles` of `FCrowdyKitGridCell`, `Units` of `FCrowdyKitTacticsUnit` with their `FCrowdyKitTacticsStats`, `Players`, `Host`, `EndedTurn`, `PhaseEnds`, `Log`), `FCrowdyKitTacticsMoveInputs`, `FCrowdyKitTacticsAttackInputs`, `FCrowdyKitTacticsAttackReply`, `ECrowdyKitTacticsPhase`, `ECrowdyKitTacticsTeam` | | director | `FCrowdyKitDirectorState` (the State: `Phase`, `Players`, `Width`, `Height`, `Spawn`, `Base`, `BaseHp`, `Gold`, `Wave` with `bHasWave`, `Waves`, `NextWaveAt`, `Enemies` of `FCrowdyKitDirectorEnemy`, `Towers` of `FCrowdyKitDirectorTower`, `BossPhase`, `Kills`), `FCrowdyKitDirectorEvent` (the **EncounterEvent** message: `Type` with `bHasType`, `Wave`, `Phase`), `FCrowdyKitDirectorBuildInputs`, `FCrowdyKitDirectorBuildReply`, `FCrowdyKitDirectorHitInputs`, `FCrowdyKitDirectorHitReply`, `ECrowdyKitDirectorPhase`, `ECrowdyKitDirectorEventType` | | instance | `FCrowdyKitInstanceState` (the State: `Phase`, `Owner`, `Seed`, `Floor`, `Floors`, `Room`, `Rooms` of `FCrowdyKitInstanceRoom`, `Members` of `FCrowdyKitInstanceMember`, `Invited`, `Gold`, `Relics`, `Grid`, `Log`), `FCrowdyKitInstanceOpenInputs` (`Seed`, `bHasSeed`, `Party`), `FCrowdyKitInstanceOpenReply`, `FCrowdyKitInstanceInviteInputs`, `FCrowdyKitInstanceEnterReply`, `FCrowdyKitInstanceDescendReply`, `ECrowdyKitInstancePhase`, `ECrowdyKitInstanceRoomKind` | | territory | `FCrowdyKitTerritoryState` (the State: `Phase`, `Points` of `FCrowdyKitTerritoryPoint`, `Scores`, `Members`, `Winner`), `FCrowdyKitTerritoryEvent` (the **TerritoryEvent** message: `Type` with `bHasType`, `Point`, `Owner`, `Previous`, `Paid`, `Faction`), `FCrowdyKitTerritoryJoinInputs`, `ECrowdyKitTerritoryPhase`, `ECrowdyKitTerritoryEventType` | | zone | `FCrowdyKitZoneState` (the State: `Phase`, `Center` an `FCrowdyKitZoneCenter`, `Radius`, `Stage` with `bHasStage`, `NextRadius`, `Players` of `FCrowdyKitZonePlayer`, `Alive`, `Winner`), `FCrowdyKitZoneEvent` (the **ZoneEvent** message: `Type` with `bHasType`, `Phase`, `ToRadius`, `Radius`, `ShrinksAt`, `Player`, `Place`), `ECrowdyKitZonePhase`, `ECrowdyKitZoneEventType` | | deck | `FCrowdyKitDeckState` (the State, which **GetState** answers too, plus this player's own `Hand`: `Phase`, `Players`, `Host`, `HandCounts`, `Top`, `Suit` with `bHasSuit`, `DrawCount`, `Turn`, `TurnEnds`, `Last`, `Winner`, `Scores`), `FCrowdyKitCard` (`Id`, `Rank`, `Suit`), `FCrowdyKitDeckHand` (`Cards`, `Playable`, `bDrawn`; what **GetHand** and **Draw** answer), `FCrowdyKitDeckPlayed`, `FCrowdyKitDeckPlayInputs` (`Card`, `Suit`, `bHasSuit`), `FCrowdyKitDeckPlayReply` (`bPlaying`), `ECrowdyKitDeckPhase`, `ECrowdyKitDeckAction`, `ECrowdyKitCardSuit` | | arena | `FCrowdyKitArenaState` (the State, which **GetState** answers too: `Phase`, `Origin` an `FCrowdyKitGroundPosition`, `Width`, `Depth`, `Fighters` of `FCrowdyKitArenaFighter`, `Projectiles` of `FCrowdyKitArenaShot`, `Areas` of `FCrowdyKitArenaBlast`, `KillTarget`, `EndsAt`, `Winner`), `FCrowdyKitArenaEvent` (the **ArenaEvent** message: `Type` with `bHasType`, `Ability`, `Caster`, `X`, `Y`, `Victim`, `Damage`, `Killer`, `Player`, `What`, `Detail`, `Why`, `Winner`), `FCrowdyKitArenaJoinInputs` (`HeroClass`), `FCrowdyKitArenaMoveInputs` (`X`, `Y`), `FCrowdyKitGroundPosition` (what **Move** answers: `X`, `Y`), `FCrowdyKitArenaCastInputs` (`Ability`, `X`, `Y`), `FCrowdyKitArenaCastReply` (`bHit`), `ECrowdyKitArenaPhase`, `ECrowdyKitArenaEventType` | `FCrowdyKitMatchPlace` is the player's place in a queue, and only the fields of its `State` are filled: `Rating`, `WaitedMs` and `Window` while **Queued**; `Players`, `Accepted` and `ExpiresInMs` while **Proposed**; `Session`, the Instance Id of the session reserved for the match, once **Matched**. A session's `Scores` is a `TMap` from user id to score, and the world's `Modifiers` a `TMap`, which is why these are structs of the SDK's rather than variables added in the asset. On a leaderboard, players with the same score share a `Rank`, and **GetMyPlace** answers a `Rank` of 0 when the player is not on the board. Blueprint cannot leave an input out, so a `Limit` of 0 on **GetTop** means the board's own Top Size, and a `Player` of 0 on **GetAround** means the player calling. Blueprint has no empty value either, so where the player and market starters answer "none", the field reads a value they never use: a vendor offer's `Stock` and `PerPlayer` read -1 for no limit, a `FCrowdyKitQuote`'s `Bid`, `Ask` and `Last` read 0 for no price yet, `FCrowdyKitProgress`'s `Next` reads 0 at the top level, and an achievement's `UnlockedAt` reads 0 while it is locked. A player's **GetMe**, **GetInventory**, **GetWallet** and **GetProgress** answer only that player; anyone else is refused **Denied** with **Code** `forbidden`. When both players **AcceptTrade**, the answer's `State` is **Settling**: the market then moves each side's offer to the other player, and their wallets and inventories show it a moment later, when the trade reads **Done**. The functions a market calls on a player's hub to hold and move what is traded are not in `CSO_Kit_Player`, since only the market's server code may call them. ### Values only the player reads A starter's hub keeps some values for one player alone, which its snapshots never carry: the player starter's inventory, wallet and progress, a market's orders and trades of the player calling, and a card table's hand. The SDK reads each with its own call, so they are ordinary variables of the State, with **Get**, **On Changed** and `WatchValues` as for any other: - The object reads them after every read of its State: when it first connects, after a reconnect, after the server restarted, and after a missed snapshot. It is **Ready** only once they are read, so **On Status Changed** saying Ready brings them. - When the hub says one of them changed for the signed-in player, such as the wallet after a **Craft** or a market trade settling, the SDK reads that one again, and **On Wallet Changed** follows a moment after the change. A change for another player reads nothing. - When the object fails, as after a sign-out, they go back to their defaults, so one player's wallet is never left for the next. - After a hub restart that arrives as a pushed snapshot, they are read again. - If a customized starter's snapshot carries a field that is also an Owner Value, the snapshot's value wins. For the player starter they are read only on the signed-in player's own player. Get `CSO_Kit_Player` with an empty Instance Id for that: the SDK uses the signed-in player's user id, and **Get Server Object** refuses with "Nobody is signed in yet, so player has no player to read: sign in first, or give a player's user id as the Instance Id" before anyone signs in. A **Crowdy Server Object** component with `CSO_Kit_Player` and its **Instance Id** left empty does the same as **Signed-In Player**: it waits for the sign-in instead of failing, and joins the new player's own hub whenever another player signs in. Get it with another player's user id and you read their public profile, `Player`, `Level`, `Achievements` and `QuestsDone`, while `Inventory`, `Wallet` and `Progress` are not read: their **Has Value** pins are false and **Get** gives the default, as it does for every player's while nobody is signed in. In C++, the object's `IsUnreadOwnerValue(TEXT("Wallet"))` is true for such a value. A market reads the orders and trades of whoever is signed in, whatever its Instance Id, and so does a deck: its `Hand` is the signed-in player's own at the table the Instance Id names. See [Play a card game with a hidden hand](#play-a-card-game-with-a-hidden-hand). In `CSO_Kit_Player`, `CSO_Kit_Market` and `CSO_Kit_Deck` these are listed under **Game Kit**, **Owner Values** (`FCrowdyKitOwnerValue`: `Method`, `What`, `Variable`): each names the call that answers it (`wallet`), what the hub's changed messages call it (`wallet`) and the State variable it fills (`Wallet`). **Keyed By Player** is on for the player, which a definition's `IsKeyedByPlayer()` reads in C++. A definition whose Owner Value names no variable of its State reports the problem, such as "Owner Value Wallet is not a variable of the State", and so does one with Owner Values and no State Topic. ### Field names are snake_case The starters' Rust names its fields in snake_case, so a starter's definition does too, by default: `TimeOfDay` is `time_of_day` on the server, and a bool's `b` is dropped, so `bNight` is `night`. Enum values follow the same rule. An entry under the definition's **Server Names** still wins over it. It matters when you point a starter at a struct of your own, and in C++, where the field names `WatchValues` reports are the server names: `Fields.Contains(TEXT("night"))`, not `bNight`. ### Send Once A function of a starter has a **Send Once** setting in its Details, which no other type shows. With it on, each call carries an id of its own, and the starter runs the call once however often it is sent again, so a call resent after a lost answer does not harvest, join or score twice. Add starter turns it on for every function that changes something and off for those that only read, such as **GetState**, **GetStatus** and **GetTop**. Leave it as it is unless you change what a function does. ## Refusals and their codes A starter refuses a call with a code the game can branch on and words to show, such as `full`, `not_your_turn` or `empty`. The codes are listed on the platform's [Refusals carry a code](/game-kit/protocol#refusals-carry-a-code). In Unreal a refusal arrives on **On Failed**, or in `OnDone`, with: - **Outcome** **Refused** (`ECrowdyServerCallOutcome::Refused`) for a refusal of the game's own, such as a full match or another player's turn. A Blueprint Switch on the outcome has a **Refused** pin. - **Code**, the code itself as a name, such as `full`. On a **Call** node it is in the advanced area with **Outcome**, **Reason** and **Retryable**: click the small arrow at the bottom of the node to show it. In C++ it is `FCrowdyServerCallResult::Code`, an `FName`, and `None` for anything that is not a starter's refusal. - **Reason**, the starter's own words, such as "only the host starts the session". When it gives none, "The server refused the call to Start: " followed by the code. Three codes read as an outcome a call already has, and fill **Code** too: | Code | Outcome | Retryable | |---|---|---| | `forbidden` | Denied | No | | `cooldown` | Denied | Yes | | `unavailable` | Unavailable | Yes | So a game that already handles **Denied** and **Unavailable** handles these without change, and can still read **Code** to tell them apart. Every other code is **Refused** and not retryable. Your own types never answer **Refused**. See [Outcomes](./troubleshooting.md#outcomes). In an actor whose **Crowdy Server Object** component holds `CSO_Kit_Session` with the session's Instance Id, so the **Call** node needs no **Target**: [Blueprint snippet: kit-refused-code, see /bp/unreal-sdk/kit-refused-code.txt] ## Functions only a developer calls Some of a starter's functions are for you, not for players. Their definition makes them [Developers Only](./create-a-type.md#callable-by), and the starter's Rust refuses anyone else with `forbidden`, which a game reads as **Denied**: | Starter | Function | What it does | |---|---|---| | world | **ScheduleEvent** | Schedules or replaces a liveops event (`FCrowdyKitWorldEvent`: `Name`, `StartMs`, `EndMs` and `Modifiers`); while it runs, its modifiers apply. | | world | **CancelEvent** | Cancels one by `Name`, and answers whether there was one. | | matchmaker | **Configure** | A queue's settings (`FCrowdyKitMatchmakerConfig`): `PlayersPerMatch`, the rating window, how long players have to accept. | | leaderboard | **Submit** | Puts a player's score on the board (`FCrowdyKitBoardSubmitInputs`: `Player`, `Score`), and answers their score now. Other server code, such as a session, submits too; a player never does. | | leaderboard | **Configure** | A board's settings (`FCrowdyKitBoardConfig`): whether it keeps a player's best, sum or latest score, whether high or low ranks first (points or times), its Top Size, a cron schedule that ends each season, and how many players it holds. | | leaderboard | **Roll** | Ends the season now: the board keeps its final standings, which **GetSeason** answers as `Last`, and starts the next one empty. | | player | **Grant** | Gives a player a reward of items and in-game soft currency (`FCrowdyKitGrantInputs`: `Ref`, `Bundle`, `Xp`, `Counters`), and answers what it earned them (`FCrowdyKitEarned`). Items that do not fit wait in their mail. | | player | **Record** | Counts something the player did toward quests and achievements (`FCrowdyKitRecordInputs`: `Ref`, `Counter`, `N`). | | player | **Deposit** | Puts items and soft currency into a player's hub without counting anything (`FCrowdyKitDepositInputs`: `Ref`, `Bundle`). | | territory | **Reset** | Starts a new war: every point and score starts again and the phase reads **Open**. Players keep their factions. | Run them with **Call as Developer...** in the function's Details in the definition asset, or with `-op=call` from the command line; see [Call a function as the developer](./deploy-with-server-compute.md#call-a-function-as-the-developer). For a one-player test of the `ranked` queue: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=call -type=matchmaker -fn=Configure -key=ranked -inputs=@ranked.json -yes ``` with `{"PlayersPerMatch": 1}` in `ranked.json`; the inputs left out keep their defaults. The world has Only One Instance, so a call to it needs no `-key=`. A session's **Configure** is not Developers Only: the session's host calls it in the lobby. ![The Call as Developer window for the matchmaker's Configure: the Instance Id box, the Players Per Match, Window, Widen Per Sec, Max Window, Accept Ms and Party Ms inputs, and the Call button](/img/unreal-sdk/kit-call-as-developer.png) A player's **Grant**, **Record** and **Deposit** each take a `Ref`, up to 128 characters, that names that one change. The player's hub runs a change once per `Ref` and, for up to seven days (its last 4096 changes), answers a second call with the same `Ref` as it answered the first, giving nothing more and saying nothing about it. So give every grant its own `Ref`, such as `quest-reward:4211`, and reuse one only to send the same grant again after a lost answer. The `-key=` of a call to the player is the player's user id. ### Grant and deposit soft currency from Studio On the [Server Compute](./deploy-with-server-compute.md) page, the **...** menu of the `player` row has **Grant soft currency...** and **Deposit soft currency...**. Each opens **Call as Developer** on that function, which runs it on the live app now. Fill **Player** with the player's user id, and the `Bundle`'s `Currency` and `Items` with the amounts, such as `gold` 100. The window says what it gives is in-game soft currency, never real money. Leave `Ref` empty: each press of **Call** makes a fresh one, such as `studio-grant-` and 32 hex digits, so each press gives again, and the answer shows the `Ref` it sent. To send a grant again after its answer was lost, copy that `Ref` into the field and press once more: the player's hub answers as it did the first time and gives nothing more. From the command line, `-op=grant` and `-op=deposit` do the same: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=grant -player=101910912345678 -bundle=@reward.json -xp=50 -yes ``` with `{"Currency": {"gold": 100}, "Items": {"wood": 5}}` in `reward.json`. Without `-ref=` a fresh `Ref` is made, which the result gives as `ref`. Both refuse, before sending anything, a call that would give a player nothing they would see: a player id other than their user id as a game reads it ("007 is not a player's user id as a game reads it, ..."; a number above 0, no sign or leading zero), a grant or deposit with an empty bundle and no experience ("this gives the player nothing: fill the Bundle's Currency or Items, such as gold 100"), a `-bundle=` key other than `Items` and `Currency`, and an `-xp=` that is not a whole number. See [From the command line](./deploy-with-server-compute.md#from-the-command-line). ## Post a session's scores to a leaderboard A leaderboard ranks scores that your server code decided, never scores a player sends, so a player's **Submit** comes back **Denied** with **Code** `forbidden`. The session starter posts for you: give it a board and, when a session finishes, it submits each player's score to that leaderboard. 1. Add the `leaderboard` starter, if the project does not have it. 2. In `CSO_Kit_Session`, under **Game Kit**, set **Config** to `{"board": "weekly-score"}`, with the board's name. The session's **Calls** already lists `leaderboard`, and Deploy sends that link while the project has the leaderboard. 3. Deploy. From then on, every session that finishes, by reaching the target score, running out of rounds or losing too many players, submits each player's score once to the `weekly-score` board. A session sees its board as `Config.Board` in its variables. **Configure** leaves the board as the Config set it, so a host cannot send a session's scores elsewhere. To read the board, get `CSO_Kit_Leaderboard` with `weekly-score` as its Instance Id: its variables are the top of the board and the season, which **On Top Changed** follows, and **Call GetMyPlace**, **GetTop**, **GetAround** and **GetSeason** read more. A board keeps each player's best score by default, higher ranking first. For other rules, call its **Configure** as the developer, for example `{"Keep": "Sum"}` for a total over the season, or `{"Order": "Low"}` for times. A project with a board in a Config and no leaderboard deploys with a warning: "session: its Config posts scores to the board weekly-score, but the project has no leaderboard, so they go nowhere; add the leaderboard starter". If the leaderboard is not deployed, a finished session's scores go nowhere; when the platform refuses the post, the session's server log says "a score did not reach board weekly-score: ...". The session checks its Config as **Configure** checks its inputs: `min_players` at least 1 and at most `max_players`, `max_players` at most 64, `turn_ms` at least 1000, and every key of the right kind. A Config it refuses stops each of its sessions from starting, so test a new Config on one session before you rely on it. ## Find a match and take turns The matchmaker tells a player nothing on its own: a player in a queue asks its **GetStatus** to learn they were proposed a match, accepts it, and asks again until they are matched. **Find Match** does all of that. **In Blueprint**, add **Find Match** (Crowdy SDK, Game Kit). Its **Matchmaker** is the queue's Server Object: **Get Server Object** with `CSO_Kit_Matchmaker` and the queue's name, such as `ranked`, held by your Owner while the search runs. Set **Rating**, **Auto Accept** and **Timeout** (seconds; 0 waits for as long as it takes). Its pins: - **Queued** runs when the player waits in the queue. - **Proposed** runs when a match is proposed, and again when who has accepted it changes, with **Players** and **Accepted** (user ids). - **Matched** runs once, with **Session Id**: get `CSO_Kit_Session` with it as the Instance Id, then **Call Join**, **Call Ready** and, as the host, **Call Start**. - **Failed** runs once, with **Code** and **Reason**: the matchmaker's refusal (such as `full`), `cancelled`, `timeout`, `expired` (a proposed match this player did not accept in time) or `left` (the player left the queue another way). With **Auto Accept** the node accepts each proposed match itself. Without it, call **Accept** on the node's **Search** pin from **Proposed**, for example after the player presses a button; **Cancel** on the same pin stops the search. A match that falls through after this player accepted, because another player declined or let it expire, is searched for again. Every ending but **Matched** leaves the queue, so nobody is proposed a match with a player who stopped looking. **Cancel** and the **Timeout** wait for a call already sent, and end with **Matched** if it made the match. A matchmaker that cannot reserve a session puts its players back in the queue, so a search with no Timeout then waits on. [Blueprint snippet: kit-find-match, see /bp/unreal-sdk/kit-find-match.txt] **Is My Turn** and **Get Turn Player** read the session's turn: give them the session's Server Object. **Is My Turn** is true while the session plays and it is the signed-in player's turn; **Get Turn Player** gives the user id whose turn it is, and is false, with **Player** 0, in the lobby, once the session finished, and before it is read. Call them from **On Turn Changed (CSO_Kit_Session)** and **On Phase Changed (CSO_Kit_Session)** to enable the player's controls: **Turn** reads 0 in the lobby and on the first turn alike, so it is the phase changing to **Playing** that starts the first turn. In a widget whose `SessionId` variable holds the **Session Id** from **Matched**, and whose Play button is named `PlayButton` with **Is Variable** ticked: [Blueprint snippet: kit-session-turn, see /bp/unreal-sdk/kit-session-turn.txt] From queue to result, every player of a match runs the same steps. The matchmaker matches two players by default (`players_per_match`), and each player's **Find Match** ends **Matched** with the same **Session Id**. Each player gets that session, calls **Join** and then **Ready**. The session's **Host** variable is the first player to join: compare it with **Get User ID** on the Crowdy Game Session, and only that player calls **Start**. **Start** is refused with `not_ready` until every player is ready, so the host calls it again when **Ready** changes. The other players wait for **Phase** to read **Playing**. Then each player calls **Act** only while **Is My Turn** is true; a move out of turn is refused with `not_your_turn`. A player who runs out of the session's `turn_ms` is skipped. When a player reaches `target_score`, or the rounds run out, **Phase** reads **Finished** with the **Winner**. If the session's Config names a `board`, every player's score is posted to that leaderboard exactly once, so a board that keeps a **Sum** grows by each session's score. **In C++**, `CrowdyKit::FindMatch` in `CrowdyGameKitMatch.h` is the same search, with a function for each update: ```cpp #include "CrowdyGameKitMatch.h" UCrowdyServerObject* Queue = GetGameInstance()->GetSubsystem()->Acquire(KitMatchmaker, TEXT("ranked"), this, Error); Search = CrowdyKit::FindMatch(Queue, 1000, /*bAutoAccept*/ true, /*TimeoutSeconds*/ 60.f, [this](const FCrowdyKitMatchProgress& Progress) { if (Progress.Stage == ECrowdyKitMatchStage::Matched) { JoinSession(Progress.Session); } UE_CLOG(Progress.Stage == ECrowdyKitMatchStage::Failed, LogTemp, Log, TEXT("No match: %s (%s)"), *Progress.Code.ToString(), *Progress.Reason); }); ``` `Search` is a `TSharedPtr`, whose `Accept()` and `Cancel()` match the node's and whose `IsOver()` says whether it ended; the search keeps itself going whether or not you hold it. `CrowdyKit::IsMyTurn(Session)` and `CrowdyKit::GetTurnPlayer(Session, Player)` read the turn. The Blueprint node is `UCrowdyFindMatchAction`, whose pins are `FCrowdyFindMatchUpdate` delegates, and the turn nodes are in `UCrowdyGameKitLibrary`. ## Trade between two players Two players swap in-game soft currency and items through a market; it is game state, never real money. The market moves nothing until both players accepted both offers, and until then what each player offers waits, held, in their own `player` hub. 1. Both players get `CSO_Kit_Market` with the same market name, such as `town`, and their own `CSO_Kit_Player` with an empty Instance Id, whose **Wallet** and **Inventory** they watch. 2. One player calls **OpenTrade** with the other player's user id as **With**. Its answer's **Trade** is the trade's id. The other player's **Trades** shows the trade a moment later, so **On Trades Changed (CSO_Kit_Market)** is where that player learns of it and reads its **Id**. 3. Each player calls **Offer** with the **Trade** and a **Bundle**, such as **Currency** `gold` 5. What they offer moves to their wallet's **Held** at once. A new offer from either player takes back both acceptances, so a player accepts only once the other's offer, in **Trades**, is what they agreed to. 4. Each player calls **AcceptTrade**. The first answer's **State** is **Open**; the second's is **Settling**, and the market moves each offer to the other player. Once it is **Done** the trade leaves both players' **Trades**, and each **On Wallet Changed (CSO_Kit_Player)** brings the new balances. **DeclineTrade** calls a trade off, and so does the market when the trade is still open after its `trade_ms` (five minutes by default); either way each offer goes back to the player who made it. A player can offer only what their wallet and inventory hold; **Offer** is refused otherwise. A player is in at most 10 open or settling trades, the market's `trades_max`, so nobody can fill another player's **Trades**: past that, **OpenTrade** is refused with `limit`, whichever of the two players is full. Set another number in `CSO_Kit_Market`'s **Config**, such as `{"trades_max": 3}`. Two players have at most one open trade with each other: **OpenTrade** while one is open answers that trade's id. A trade that is done or called off is gone, so a later **Offer**, **AcceptTrade** or **DeclineTrade** on it is refused `not_found`. ## Run a game mode on a starter `minigame`, `tactics`, `director`, `instance`, `territory` and `zone` each run one kind of match. Each keeps one hub per Instance Id, which is any name you choose: a table, a battle, a map, a run. Get it as any Server Object, with **Get Server Object** and the starter's definition, and the Instance Id of the match; the players of one match all get the same one. Every player id in their variables is a user id. A match is read from its variables, which everyone in it sees, and changed with the functions below; the hub decides what a move does, and a refusal comes back with the **Code** named under each. The `deck` is a card table and has [its own section](#play-a-card-game-with-a-hidden-hand) after these, since it also keeps a value only its player reads. The `arena` is a real-time fight and has [its own section](#fight-in-an-arena) at the end, since a player's client also [reports where the player is](#report-where-the-player-is). Two of them keep a board, `tactics` and `director`, and a board keeps its server's x and z axes: a cell is **X** and **Z** (`FCrowdyKitGridCell`), and z is not height. `territory` and `zone` are the world: z is up, as in Unreal, the ground is x and y, and positions are in chunks of 1600 units, as for the [npcs](#the-npcs-starters-area). Their definitions have [Reads World Data](./access-members-and-timers.md) on, since their hubs read where players are in the world: a client never says it stands on a point or inside the circle. A player's position is the chunk the platform tracks, to one chunk, and never the actor state the player's own client writes, so a client cannot place itself inside the circle or on a point. ### Duel two players In Oakford's tavern, two villagers play rock, paper, scissors at the table `tavern-1`. - **Join** seats a player. The second to sit starts the match, and its answer's `bStarted` says so. A third player is refused `full`. **Leave** stands up before the match starts or once it is over, and is refused `started` while it is under way. A caller who is not seated is refused `not_member` in every phase, and nothing changes. - **Throw** takes a **Hand**: `rock`, `paper` or `scissors`. Nobody sees a throw before both are in, or the round's time runs out: `Seats` says only whether each player has thrown (`bThrown`). Then `Last` holds the round's `Hands` in seat order, with empty text for a throw that was not in time, and its `Winner`, a user id, or 0 for a draw. A player who does not throw in time loses the round. Another hand is refused `bad_request`. - The first to win a majority of `BestOf` rounds (3 by default) wins: `Phase` reads **Finished** and `Winner` is the user id. **Rematch** plays again at the same table once it is over, and is refused `conflict` before. - `Deadline` is when the round closes, in Unix milliseconds, and 0 while no round waits on throws. A rematch times out its round too, also when the match is a best of 1, which the platform's own `minigame` does not. ### Fight a grid battle - **Join** brings a player's unit, up to 4 players. The first to join is the `Host`, and only the host calls **Start**; anyone else is refused `forbidden`. The enemies take the far side. - In the players' phase every unit may **Move** once, to an **X** and **Z** it can reach within its `Moves` and around obstacles and other units, and **Attack** once, with the **Target** unit's `Id` within its `Range`. **EndTurn** hands over early. A call out of turn is refused `not_your_turn`, a second move or attack `conflict`, and a cell or target too far `out_of_reach`. - When every player has ended their turn, or `PhaseEnds` passes, the hub's enemies move and strike, and `Round` goes up. `Phase` reads **Won** or **Lost** when a side has no unit standing. `Log` keeps the last 24 lines of what moved and hit. ### Defend against waves - **Join** takes up to 8 players. The first to join, or a developer, calls **Start**; anyone else is refused `forbidden`. - Enemies walk one lane from `Spawn` to `Base`, and an enemy that arrives takes from `BaseHp`. **Build** puts a tower on a free cell with **X** and **Z**, for gold; its answer is the `Gold` left. A cell that is taken is refused `conflict`, a cell off the field `bad_request`, too little gold `insufficient`, and a tower that would close the lane `blocks_lane`. **Hit** strikes the enemy with the **Enemy** id for its `Hp` left, and a player may strike again only after the Config's cooldown, or is refused `cooldown`. A hit still answers with the enemy's health at once. - While an encounter runs, the hub publishes only events on a hit or a build; the next tick, at most 500 ms later, carries the snapshot. - `Wave` is the wave under way, `Waves` how many there are, and `NextWaveAt` when the next one starts. The last wave brings a boss, and `BossPhase` reads 0 to 3 as it weakens. `Enemies` and `Towers` are what is on the field, and `Kills` counts each player's kills. - Its events arrive as one message, **EncounterEvent**, described in [React to a director's waves](#react-to-a-directors-waves). ### Send a party down a dungeon - **Open** starts the run for the player calling, who becomes its `Owner`; opening it again is refused `started`. **Invite** adds a player, which only the owner does, and the invited player calls **Join** (a player who was not invited is refused `forbidden`). A developer's **Open** may name the whole party in **Party**. - **Enter** resolves the room the party is at: a fight, a treasure, a rest or the boss. The hub rolls a fight from the party's strength, so `Rooms` shows each room's `Kind` and `bDone`, `Members` each member's `Hp`, and `Log` what happened. A member who is down is refused `forbidden`, and **Enter** on a cleared floor `conflict`. The owner calls **Descend** when the floor's rooms are done, refused `not_ready` before. **Abandon** ends the run, by the owner or a developer. - The run ends **Cleared** when the party clears the last floor, **Wiped** when nobody is standing, **Abandoned**, or **Expired** when nobody touched it for the Config's `idle_ms`. A call to a run that is over is refused `finished`, and to one nobody opened `not_ready`. - **Seed** decides the floors: the same seed makes the same dungeon. It is 0 to 2^63-1, so it fits an `int64` exactly, and a larger one is refused `bad_request`. In **Open**, **Has Seed** decides whether the **Seed** is sent: with it set the run uses your seed, and with it unset the hub draws one. Either way the answer's `Seed` is the one the run uses. The seed is in `Seed` for everyone in the run, so it gives away the whole dungeon. - With a board in its Config, such as `{"board": "oakford-deepest"}`, a run that is cleared or wiped posts each member's depth, the floors reached and rooms done as one score, to that leaderboard, once. Add the `leaderboard` starter for it, as for [a session's scores](#post-a-sessions-scores-to-a-leaderboard); when the post fails, the run's server log says "the run's depth did not reach board ...". With a `volume` in its Config, `Grid` is the id of the grid of the world reserved for the run, and 0 without one. ### Fight over control points - **Join** takes a **Faction**, one of the Config's `factions` such as `red`, once: a player keeps theirs, and a different one is refused `conflict`. A faction that is not in the Config is refused `bad_request`, and a war with all the members it takes `full`. Faction names must differ by more than case. - A faction captures a point by having more members standing on it than any other faction has. Each `Points` entry has its `Owner`, the faction `Capturing` and its `Progress` from 0 to 1, and `Present`, how many of each faction stood on it at the last scan. A point sits at ground x and y with a height z in the Config, in chunks; players within one chunk of its height count. - Points pay income to the faction that holds them; `Scores` and `Members` count by faction. The first faction to the Config's `win_score` wins: `Phase` reads **Won** and `Winner` is the faction. - **Reset**, for a developer, starts a new war. ### Shrink a circle - **Join** takes players, and the first to join, or a developer, calls **Start**; anyone else is refused `forbidden`. A match takes two (`not_ready`), and a full or running match refuses a join (`full`, `started`). - The circle holds, then shrinks phase by phase to the radii of the Config's `phases`. `Center` is its ground x and y, `Radius` what it is now, and `NextRadius` what it shrinks to in the phase `Stage`. Every player outside it loses health, and `Players` shows each one's `Hp`, whether they are `bOutside` at the last scan, and their `Place` once they fall, with the winner at 1. `Alive` counts those standing, and `Winner` is the last one. - Players more than one chunk above or below the circle's height, or more than 10 chunks from its centre on either ground axis, count as outside, so the first circle's radius is at most 10 chunks. Each scan makes more world reads than the platform's `zone` does: 21 at the default radius of 10, fewer as the circle shrinks, and 1 at the end. A scan that cannot read the world carries its damage over to the next good scan, up to 5 scan periods' worth. - The circle is scanned every 2 seconds by default (it was every second), which halves its world reads. Damage scales with the time a player spends outside, so a second outside costs the same as before. Set `scan_ms` in the Config, at least 250, to scan faster. - Its events arrive as one message, **ZoneEvent**, in the same way. The realtime ZoneChange event carries `centerZ` beside `centerY`, both the ground value. The `territory` and `zone` of the platform keep y up. The SDK's keep z up, so a war or a match the platform's starter saved starts again. ### Play a card game with a hidden hand In Oakford's tavern, two to six villagers play crazy eights at the table `tavern-2`: match the top card's suit or rank, an eight is wild and names the next suit, draw when you cannot play, and the first to empty their hand wins the points left in the others'. Add `deck`, deploy, and have the players at the table get `CSO_Kit_Deck` with the Instance Id `tavern-2`. A table is one game: when it is over, open a new Instance Id for the next one. - **Join** seats a player, up to six; a seventh is refused `full`, and a table that has started refuses a new player `started`. **Leave** stands up. During a game the leaver's cards go to the bottom of the deck, and when only one player is left that player wins. Someone else leaving keeps the current player's clock and their one draw; the last seat leaving on its own turn hands the turn to the first seat. The platform's own `deck` resets the clock and the draw instead, and hands the turn back a seat. **Start** deals five cards to each player and turns the first card up. Only the host, the first to join, calls it, and anyone else is refused `forbidden`; with fewer than two players it is refused `too_few_players`. - **Play** takes the **Card**, which is a card's `Id`, and, for an eight, the suit it names: set **Suit** and **Has Suit**. An eight with **Has Suit** unset is refused `bad_request`. A card that is not in the hand is refused `not_found`, one that does not go on the top card `not_playable`, and any move out of turn `not_your_turn`. Its answer is **Playing**, false when that card emptied the hand and ended the game. - **Draw** takes one card, once a turn, and its answer is the new hand (`FCrowdyKitDeckHand`). A second draw is refused `conflict`, and `empty` when no cards are left. **Pass** ends a turn after drawing, and is refused `conflict` before. - A turn lasts 30 seconds. A player who runs out of time draws, if they had not, and passes; `Last.Action` reads **Timeout**. - **GetState** answers the table, and **GetHand** the caller's own hand, which the object reads for you. The table's variables are public, and never show a card in anyone's hand: - `Phase` reads **Lobby**, **Playing** or **Finished**. `Players` are the user ids in seat order, `Host` the first to join, `Turn` the player whose turn it is and `TurnEnds` when it runs out, in Unix milliseconds; `Turn` and `TurnEnds` read 0 outside **Playing**. - `HandCounts` says how many cards each player holds, by user id, `DrawCount` how many are left to draw, and `Top` the card on the table. - `Suit` is the suit in play, which an eight changes. It has **Has Suit** beside it, false in the lobby, as for [a value that may be none](#read-a-value-that-may-be-none). - `Last` is the last move (`FCrowdyKitDeckPlayed`): its `Player`, its `Card` (empty for a draw, a pass or a timeout) and its `Action`, **Play**, **Draw**, **Pass** or **Timeout**. `Player` reads 0 before the first move. - `Winner` is the user id of the first to empty their hand, and `Scores` holds each player's points: the winner is given what the others' hands are worth, eights 50, tens and faces 10, aces 1 and any other card its number. The `Hand` is the [value only this player reads](#values-only-the-player-reads). It holds `Cards`, every card in the signed-in player's hand, `Playable`, and `bDrawn`, whether they have drawn this turn. Nobody else's cards reach the game, and a player who is not seated reads an empty hand. The object reads it after every read of the table, and again when the table says it changed for the signed-in player: after they play or draw, and also when their turn begins and when it ends, so `Playable` is always the server's own list of the cards that may go now, and it is empty when it is not their turn. Use it for the controls and do not work the rule out in the game. Right after a turn moves, **On Turn Changed** can run before the new `Hand` arrives, and when no card in the hand can go, the `Hand` does not change at all as the turn begins, so **On Hand Changed** does not run then: refresh the controls from both. A card (`FCrowdyKitCard`) is its `Id`, the text **Play** takes, a `Rank`, 1 for an ace up to 13 for a king, with the jack 11 and the queen 12, and a `Suit`, **Spades**, **Hearts**, **Diamonds** or **Clubs**. A Blueprint can **Switch on ECrowdyKitCardSuit** and pick a card's art from its rank and suit, without reading the text. A card that is none, such as `Top` in the lobby, has an empty `Id`. **In Blueprint**, in the tavern table's widget: 1. Add **On Hand Changed (CSO_Kit_Deck)** and wire **Construct** into its **Bind**, with the Server Object from **Get Server Object** as its **Target**. From **Changed**, add **Get Hand** and show a card for each of its **Cards** with **For Each Loop**, using **Break Crowdy Kit Card** and a **Switch on ECrowdyKitCardSuit** to choose its art. 2. Add **On Turn Changed (CSO_Kit_Deck)** bound the same way, and from both events call one function that enables the buttons: **Play** only for a card that is in the hand's **Playable**, and **Draw** while it is this player's turn and **bDrawn** is false. **Pass** is for **bDrawn** true. The table's **Turn** is a user id, so compare it with **Get User ID** on the Crowdy Game Session. 3. On the button, add **Call Play (CSO_Kit_Deck)** with **Card** set to the card's **Id**. For an eight, set **Suit** to the suit the player picked and tick **Has Suit**. **Call Draw**, **Call Pass**, **Call Join**, **Call Start** and **Call Leave** are the other buttons. Under **On Failed**, **Code** says why, such as `not_playable`. [Blueprint snippet: kit-deck-play, see /bp/unreal-sdk/kit-deck-play.txt] **In C++**, with `KitDeck` a `UPROPERTY` holding `CSO_Kit_Deck`, the villager's table plays the first card the server says may go: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" FString Error; Table = GetGameInstance()->GetSubsystem()->Acquire(KitDeck, TEXT("tavern-2"), this, Error); Table->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, TConstArrayView Fields) { if (Fields.Contains(TEXT("hand")) || Fields.Contains(TEXT("turn"))) { PlayFirstPlayable(); } })); ``` ```cpp void ATavernTable::PlayFirstPlayable() { const FCrowdyKitDeckState* State = Table->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitDeckPhase::Playing || State->Turn != Table->GetSignedInUserId() || State->Hand.Playable.IsEmpty()) { return; } const FCrowdyKitCard& Card = State->Hand.Playable[0]; FCrowdyKitDeckPlayInputs Inputs; Inputs.Card = Card.Id; Inputs.Suit = ECrowdyKitCardSuit::Hearts; Inputs.bHasSuit = Card.Rank == 8; Table->Call(TEXT("Play"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("Not played: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); } ``` **Draw** and **Pass** are `Table->Call(TEXT("Draw"), FInstancedStruct())` and `Table->Call(TEXT("Pass"), FInstancedStruct())`. A draw's `Result.Reply` holds the new `FCrowdyKitDeckHand`, and the table's `Hand` changes with it. To play another game, replace `playable` and `card_points` in the crate's `Server/deck/src/lib.rs`: `playable` says which cards may go on the top card, and `card_points` what a card left in a hand is worth. Keep the function names and the `table` topic as they are, as for [any starter](#change-a-starter). The platform's [Card tables](/game-kit/modules/tables) page has the rest of the starter. **Add starter** gives the deck's **Config** a random `secret`, such as `{"secret": "6f1c..."}`, and the hub mixes it into every shuffle. Everything else a deal is seeded from, the table's name, the time and the epoch, is something a player can see, so without the secret a modified game could work out every hand. Keep it when you edit the Config, and give the deck one of your own if you add it with a Config of yours. The **Config** stays in the editor and is never cooked into the game. The player starter's loot rolls use a `secret` the same way, and **Add starter** writes one for it too. If a Config of yours leaves the `secret` out, Deploy warns about it for the `deck` and the `player`; a `secret` that is not a string is a problem that blocks Deploy. ### Read a value that may be none Blueprint cannot leave a value out, so where a variable may be none, the struct has a **Has** flag beside it, and the value means nothing while the flag is false. - An index that may be none has a flag. In the director's state, **Wave** is valid only while **Has Wave** is true, which is false before the first wave starts and after the last. In the zone's, **Stage** is valid only while **Has Stage** is true, which is false until the first warning. In C++ these are `bHasWave` and `bHasStage`. A flag changes with its value, so it has a **Get** node and no **On Changed** of its own: **On Wave Changed (CSO_Kit_Director)** runs when either changes, and **Get bHasWave (CSO_Kit_Director)** reads the flag there. - For an input, the flag decides whether the value is sent. **Open** on the instance sends **Seed** only when **Has Seed** is set; with it unset the hub draws a seed and the **Seed** you set is ignored. - Anything else that is never 0 reads 0 for none: a user id such as `Winner` or `Host`. A time reads 0 when nothing is waiting, such as `Deadline` and `NextWaveAt`, and text reads empty, such as a point's `Owner`. In C++, with `Gate` acquired as [below](#react-to-a-directors-waves) does: ```cpp const FCrowdyKitDirectorState* State = Gate->GetState().GetPtr(); if (State && State->bHasWave) { WaveText->SetText(FText::Format(INVTEXT("Wave {0} of {1}"), State->Wave + 1, State->Waves)); } ``` ### React to a director's waves `director`, `territory` and `zone` send their events as a single message each, **EncounterEvent**, **TerritoryEvent** and **ZoneEvent**, rather than a message for each kind of event. Its `Type` says which event it is, and its other fields carry what that event has: only the fields of its `Type` are filled, and the rest read 0 or empty. `Type` has a flag beside it, **Has Type** (`bHasType`): it is false when this client does not know the event's type, as when the server is newer than the game, and then you ignore the event. An event arrives just before the snapshot that shows its effect, so read the event's own fields; the State catches up on the next snapshot. The territory's `Type` is **Captured**, **Income** or **Won**; the zone's is **Warning**, **Shrinking**, **Settled**, **Final**, **Eliminated** or **Won**; the director's is **Wave Started**, **Wave Cleared**, **Boss Phase**, **Won** or **Lost**. A message is an event and not a value, so read the variables for what is true now; see [Hear its messages](./from-blueprint/hear-server-messages.md). Oakford's village gate is attacked in waves. Add `director`, deploy, and have the gate's players get `CSO_Kit_Director` with the Instance Id `gate-defense`. The gate rings its bell for each wave and shows the boss's phase. **In Blueprint**, in the gate's event graph: 1. Add **On EncounterEvent Received (CSO_Kit_Director)** and wire **Event BeginPlay** into its **Bind**, with the Server Object from **Get Server Object** as its **Target**, or the actor's **Crowdy Server Object** component. 2. From **Received**, add a **Branch** on **Has Type**, and from its true pin a **Switch on ECrowdyKitDirectorEventType** wired to the **Type** pin. 3. On **Wave Started**, ring the bell and show **Wave** plus 1, since it counts from 0. On **Boss Phase**, show **Phase**, 1 to 3. On **Won** and **Lost**, show the result. **Wave Cleared** needs nothing here: the last wave's end is **Won**, not **Wave Cleared**. Here the gate's **Crowdy Server Object** component holds `CSO_Kit_Director`, so the node's **Target** is left empty: [Blueprint snippet: kit-director-event, see /bp/unreal-sdk/kit-director-event.txt] **In C++**, with `KitDirector` a `UPROPERTY` holding `CSO_Kit_Director`: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" FString Error; UCrowdyServerObject* Gate = GetGameInstance()->GetSubsystem()->Acquire(KitDirector, TEXT("gate-defense"), this, Error); if (!Gate) { return; } Gate->WatchMessage(TEXT("EncounterEvent"), FOnCrowdyServerMessage::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, FName Message, const FInstancedStruct& Payload) { const FCrowdyKitDirectorEvent* Event = Payload.GetPtr(); if (!Event || !Event->bHasType) { return; } switch (Event->Type) { case ECrowdyKitDirectorEventType::WaveStarted: Bell->Play(); UE_LOG(LogTemp, Log, TEXT("Wave %d is coming"), Event->Wave + 1); break; case ECrowdyKitDirectorEventType::BossPhase: UE_LOG(LogTemp, Log, TEXT("The boss weakens: phase %d"), Event->Phase); break; case ECrowdyKitDirectorEventType::Won: case ECrowdyKitDirectorEventType::Lost: ShowResult(Event->Type == ECrowdyKitDirectorEventType::Won); break; default: break; } })); ``` The territory and zone work the same way with `FCrowdyKitTerritoryEvent` and `FCrowdyKitZoneEvent`: on a zone's **Warning**, `Phase`, `ToRadius` and `ShrinksAt` say how and when the circle will shrink, and on **Eliminated**, `Player` and `Place` say who fell and where they placed. ### Report where the player is Some starters decide what a player may do from where the player is: the `arena`, the `race` and the `ball`. Their definitions say so in the **Game Kit** category: **Position Function** names the Server Function that takes the position (`Move` on each of the three), and **Position Reports Per Second**, from 1 to 10 and 5 by default, says how often to send it. A definition is marked as a problem when the function is not there, is **Send Once**, takes inputs without `X` and `Y` doubles, or answers something other than nothing or a struct with `X` and `Y`. A starter with no Position Function reports nothing. You do not call that function. The **Crowdy Server Object** component on the player's pawn holding such a starter sends it for you, from the moment the Server Object is **Ready**: - **Position Report** (`PositionReport`, an `ECrowdyPositionReport`) says who is reported. **Player Pawn**, the default, reports only when the owner is a pawn this game's player controls, so an AI-controlled pawn, or an actor such as a scoreboard that holds the arena, never reports. **This Actor** reports any owner, for an actor that stands in for the player, such as a vehicle or a camera rig. **Off** turns the reports off. - **Snap To Kept Position** (`bSnapToKeptPosition`, on by default) corrects the owner when the server kept another place than the one reported. It moves the owner by the server's correction, the place the server kept minus the place that was sent, from where the owner is now, so movement made while the answer was on its way is kept. - **On Position Kept** (`OnPositionKept`, an `FOnCrowdyPositionKept`) runs when the server kept another place, with the **Location** the owner was moved to, in Unreal units, at the owner's own height. It runs after the owner was moved when Snap To Kept Position is on, so a stumble effect or a camera nudge belongs there. - In C++, `ReportsPosition()` on a definition (`UCrowdyServerObjectDefinition`) says whether it has a Position Function, so whether its components report at all. ![The Crowdy Server Object component's Position rows: Position Report set to Player Pawn and Snap to Kept Position ticked](/img/unreal-sdk/kit-position-report-component.png) When several components hold the same Server Object, one of them reports; another takes over when it leaves. How it reports: - **Positions are world metres.** The report's **X** and **Y** are the owner's location in Unreal units over 100: a pawn at 20000, 12000 reports 200, 120. Only the ground is sent. Height stays the owner's own, also after a snap. - **One report is in flight at a time, and the newest wins.** When the last report has not been answered by the time the next is due, that one is skipped, so a slow moment never leaves a queue of old places in front of the new one or in front of the starter's other calls. A player who stands still, moved by less than a centimetre, is reported once a second. - **A refusal holds the reports.** When the hub refuses a report, because the player is not a fighter yet, the fight is not running, the fighter is down or was removed, the component sends no more for at least 1 second, and after that until the object's State changes, and at most 5 seconds. When the report did not reach the server, it waits 1 second. Reports stop when the object is released or the player leaves, and start again when it is **Ready** again. - **With Player Pawn, only a pawn this game controls is reported.** A pawn another player drives never is: that player's own game reports it. - **A paused game keeps reporting.** The reports run on real time, not game time, so a player who opens a pause menu in the middle of an arena fight is not benched as idle. - **The call budget.** The Game Kit's client keeps its calls under 100 every 10 seconds. At 5 reports a second the component uses half of that, which leaves the rest for **Cast**, **Join** and the other calls; lower the rate in the definition if your game casts a lot. Snapping is what keeps a player honest and in the fight. The hub keeps what the player's class allows: it cuts a move that is too fast to the farthest place the class's speed reaches, keeps a fighter where they were after a jump of the Config's `teleport` metres or more, and answers with the place it kept. When that place is more than 5 centimetres from the one reported, the component teleports the owner by the difference. Without that the pawn would stay ahead of its fighter and every report would be cut again. A jump of `teleport` metres or more, and a cut of more than 1 metre, each count as a strike against the player. Strikes clear only after 10 seconds without one, and `remove_after_strikes` of them (8 by default) remove the fighter for the rest of the fight. Turn **Snap To Kept Position** off only if you move the owner yourself from **On Position Kept**. The `territory` and `zone` starters do not use reports: they read where players are from the world, as [above](#run-a-game-mode-on-a-starter) says. **In Blueprint**, there is nothing to wire: add a **Crowdy Server Object** component to the pawn, give it `CSO_Kit_Arena` and the arena's Instance Id, and leave **Position Report** on **Player Pawn** and **Snap To Kept Position** on. Until the player has **Join**ed, the hub refuses the reports, and the component holds them for at least a second and then until the State changes, or 5 seconds at most. To react to a snap, select the component and add **On Position Kept** from its Events. **In C++**, the component's event is a dynamic delegate: ```cpp #include "CrowdyServerObjectComponent.h" UCrowdyServerObjectComponent* Reporter = FindComponentByClass(); if (Reporter) { Reporter->OnPositionKept.AddDynamic(this, &AYardPawn::HandlePositionKept); } ``` ```cpp void AYardPawn::HandlePositionKept(FVector Location) { PlayStumbleEffect(Location); } ``` ### Fight in an arena In Oakford's practice yard, villagers fight in the arena `yard-1`. Add `arena`, deploy, and have the fighters get `CSO_Kit_Arena` with the Instance Id `yard-1`. There is one fight for each Instance Id, so a second yard is a second name. The arena is a rectangle of the world. In `CSO_Kit_Arena`'s **Game Kit** category, **Config** places it: `origin` is its corner in world metres, `width` runs along X from there and `depth` along Y, 40 each by default. The yard is a 40 by 40 metre square at 200, 120, so its Config is `{"origin": [200, 120]}`. The hub keeps fighters inside the rectangle, so build the yard's floor to match, and change the level and the Config together. A fight that is already under way keeps the Config it started with. The other keys, each with the default it keeps when the Config leaves it out: - `spawns`, offsets from the origin in metres where the fighters stand when the fight starts (the four corners, 5 metres in), and `classes`, the classes and their abilities (`warrior` with `strike` and `slam`, `mage` with `bolt` and `ignite`). - `players_max` (10), `kill_target` (10), `duration_ms` (300000, five minutes) and `respawn_ms` (3000). - `teleport` (12), the metres a reported move may jump at once, and `remove_after_strikes` (8), the strikes that remove a fighter for the rest of the fight. A strike is a jump of `teleport` metres or more, or a speed cut of more than 1 metre, and strikes clear only after 10 seconds without one. - `idle_ms` (15000), how long a running, standing fighter may go without a report before they are benched; 0 never does. A paused game keeps reporting, so a pause menu does not bench a fighter. - `realtime` (false) and `height` (0): with `realtime` on, casts, impacts and the hub's movement flags are also sent on the realtime channel to the players around the arena's centre, at `height` metres. The functions: - **Join** takes a **Hero Class**, `warrior` or `mage`. Another class is refused `bad_request`, a full arena `full`, and a different class once the fight is running `started`. A player may join a fight that is already running. - **Start** is called by the first to join, or a developer; anyone else is refused `forbidden`, and one fighter is refused `not_ready`. It puts every fighter at a spawn point. A player's next report, if it comes from farther than the player could have moved, is answered with the spawn point, a success and not a refusal, and the component moves the player there; a player already nearer stays where they are. - **Cast** takes an **Ability**, such as `strike`, and an aim **X** and **Y** in world metres, not Unreal units. A `strike` or `ignite` lands at once on the fighter nearest the aim, within 1.5 metres of it, and the answer's `bHit` says whether it hit someone; `bolt` flies on from the caster, and `slam` goes off at the aim after a short delay, so their answer is false and the hits arrive in the State and the events. A cast is refused `cooldown`, `insufficient` (the class's resource), `out_of_reach` (the aim is farther than the ability reaches), `not_found` (the class has no such ability), `conflict` while the fighter is down, `not_ready` before the fight runs and `not_member` before **Join**. - **Leave** stands the player down. **Move** is the position function the component calls for you. **GetState** answers the State. The State, which everyone in the fight sees: - `Phase` reads **Waiting**, **Running** or **Finished**. `Origin`, `Width` and `Depth` are the Config's, so a client can draw the yard. - `Fighters` has an `FCrowdyKitArenaFighter` each: `Player`, `HeroClass`, `X` and `Y` in world metres, `Hp` and `MaxHp`, `Resource`, `Statuses` (such as `burning`), `Kills`, `Deaths` and `bDown`. To place an actor at a fighter, multiply `X` and `Y` by 100. - `Projectiles` are the shots in the air (`Ability`, `Caster`, `X`, `Y`), and `Areas` the blasts waiting to go off (`Ability`, `Caster`, `X`, `Y`, `Radius` and `At`, in Unix milliseconds). - `KillTarget` is the kills that win. `EndsAt` is when the time runs out, in Unix milliseconds, and 0 outside **Running**. `Winner` is a user id, and 0 until the fight is won, and when the time ran out with the most kills tied. A fighter whose health runs out is down (`bDown`) for `respawn_ms`, and cannot cast or move meanwhile. Then they come back at their spawn point with full health and no statuses, and a burn that kills credits whoever set it. The first to `KillTarget` wins, or the most kills when `EndsAt` passes. The events arrive as one message, **ArenaEvent**, heard as the director's is: its `Type` says which event it is, with **Has Type** false for a type this client does not know, and only the fields of that `Type` are filled. **Cast** has `Ability`, `Caster`, `X` and `Y`; **Impact** `Ability`, `Caster`, `Victim` and `Damage`; **Kill** `Killer` and `Victim`; **Respawn** `Player`; **Violation** `Player`, `What` and `Detail`, for a move the hub cut or held back; **Removed** `Player` and `Why`, `movement` or `idle`; and **Finished** `Winner`. An event arrives just before the snapshot that shows its effect. Where the SDK's copy differs from the platform's [Arenas](/game-kit/modules/arenas): - **Positions are world metres, z up.** The platform's keep x and z on the ground inside `(0, 0)` to `(width, depth)`, and place an arena in the world with an `anchor` chunk. The SDK's `Move`, `Cast`, State and events are in the world's own metres, ground X and Y, and the arena sits where `origin` says. - **A NaN or infinite aim or move is refused `bad_request`,** and spends nothing, not even the cooldown. - **Leaving and joining again keeps your record.** A fighter who leaves while the fight runs is set aside with their class, health, kills, deaths, cooldowns, resource and respawn, and **Join** brings them back as they were. A fighter removed for movement is set aside as removed: their **Join** is refused `forbidden` until the next **Start**. - **The movement check is a travel budget, and its slack does not grow with the report rate.** The platform's allows half a metre more on every report, so a client that reports often could move about twice as fast as its class. The SDK's holds a sustained speed to 1.25 times the class's speed whatever the rate. A late report's unused allowance covers the next one, and that banking stops at about a second of travel, so a player on a slow connection, whose reports arrive further apart, is not cut short. Only a cut of more than 1 metre counts against a player toward being removed. - **The hub's own placements never count against a player.** Join, start, a respawn and a rejoin put the fighter somewhere. A report after one from farther than the player could have moved is answered with the placed point, a success and not a refusal, and the component moves the player there; a nearer player stays where they are. A player pressing against the arena's edge is clamped to it without a strike. - **A fighter who leaves stays a target for `respawn_ms`** for projectiles, areas and burns, at the spot they left, and then is not; a fighter benched as idle is never a target. An instant ability, such as `strike`, also needs its target within the ability's range of the caster, not only of the aim. At most four times `players_max` set-aside records are kept. - **An idle fighter is benched.** A running, standing fighter with no report for `idle_ms` is set aside with a **Removed** event whose `Why` is `idle`, and rejoins as they were. The platform's keeps them. - **`Resource` is current.** The State shows a fighter's resource with its regeneration up to now. The platform's shows the value from their last cast. **In Blueprint**, in the yard pawn's event graph, with the **Crowdy Server Object** component holding `CSO_Kit_Arena` and `yard-1`: 1. On the join button, add **Call Join (CSO_Kit_Arena)** with **Hero Class** set to `warrior`. The first player to join then presses another button wired to **Call Start**. 2. On a click in the yard, add **Call Cast (CSO_Kit_Arena)** with **Ability** `strike` and the clicked point's X and Y divided by 100 into **X** and **Y**. Under **On Failed**, **Code** says why, such as `cooldown`. 3. Add **On ArenaEvent Received (CSO_Kit_Arena)** and wire **Event BeginPlay** into its **Bind**, with the component as its **Target**. From **Received**, add a **Branch** on **Has Type**, and a **Switch on ECrowdyKitArenaEventType** on its true pin. On **Kill**, show **Killer** and **Victim**; on **Finished**, show **Winner**. 4. To draw the fighters, add **On Fighters Changed (CSO_Kit_Arena)** on the same object and place an actor at each fighter's **X** and **Y** times 100. **In C++**, with `Arena` the acquired `CSO_Kit_Arena` (`Acquire(KitArena, TEXT("yard-1"), this, Error)`), the pawn strikes the nearest fighter that is standing. Acquiring the arena this way does not report the pawn's place: the pawn still needs the **Crowdy Server Object** component holding `CSO_Kit_Arena` to report, or the game must call **Move** itself. Otherwise the fighter never moves on the hub and is benched as idle after `idle_ms`. ```cpp void AYardPawn::StrikeNearest() { const FCrowdyKitArenaState* State = Arena->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitArenaPhase::Running) { return; } const FVector2D Me(GetActorLocation().X / 100.0, GetActorLocation().Y / 100.0); const FCrowdyKitArenaFighter* Nearest = nullptr; double NearestDistance = TNumericLimits::Max(); for (const FCrowdyKitArenaFighter& Fighter : State->Fighters) { const double Distance = FVector2D::Distance(Me, FVector2D(Fighter.X, Fighter.Y)); if (Fighter.Player == Arena->GetSignedInUserId() || Fighter.bDown || Distance >= NearestDistance) { continue; } Nearest = &Fighter; NearestDistance = Distance; } if (!Nearest) { return; } FCrowdyKitArenaCastInputs Inputs; Inputs.Ability = TEXT("strike"); Inputs.X = Nearest->X; Inputs.Y = Nearest->Y; Arena->Call(TEXT("Cast"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { const FCrowdyKitArenaCastReply* Reply = Result.Reply.GetPtr(); UE_CLOG(Reply && Reply->bHit, LogTemp, Log, TEXT("Hit")); UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("Not cast: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); } ``` The ArenaEvent is heard with `WatchMessage` and an `FCrowdyKitArenaEvent`, as the director's is in [React to a director's waves](#react-to-a-directors-waves). The platform's [Arenas](/game-kit/modules/arenas) page has the rest of the starter. ### Run a race On Oakford's fair day, villagers race their carts around the mill loop, `mill-loop-1`. Add `race`, deploy, and have each racer's pawn hold `CSO_Kit_Race` with that Instance Id in a **Crowdy Server Object** component, which [reports where the racer is](#report-where-the-player-is). A race runs once for each Instance Id: the next heat is a new name, such as `mill-loop-2`. The course is a list of gates in the world. In `CSO_Kit_Race`'s **Game Kit** category, **Config** sets it: `gates` is a list of `{"x", "y", "radius"}`, each a ring on the ground in world metres, and `laps` how many times round. The first gate is the start and finish line. A racer passes the gates in order, and a gate counts when the path between two of their reports comes within its radius. The default is a triangle of three gates at 0, 0 and 30, 0 and 15, 25, 3 metres across, twice round. The other keys, each with the default it keeps when the Config leaves it out: - `course_id` (`loop`), the course's name in the realtime timing events; `countdown_ms` (3000) and `racers_max` (8). - `rules`, the movement check: `max_speed` (12 metres a second), `teleport` (20 metres at once), `bounds` (none: a `[x0, y0, x1, y1]` rectangle in world metres keeps racers inside) and `grace` (1.25). The hub allows up to `grace` times `max_speed`, so set `max_speed` to your cart's top speed over 1.25 for a strict limit. - `finish_grace_ms` (30000), how long the others have once the winner is in, and `time_limit_ms` (600000, ten minutes), how long after the start the race closes whoever is still out; 0 is no limit. - `idle_ms` (15000), how long a racer who has not finished may go without a report before they retire; 0 never does. - `board`, a leaderboard's Instance Id: with one, a closed race posts each finisher's time to it in milliseconds. Configure that board with `order: "low"`, so the fastest time ranks first, and add the `leaderboard` starter yourself. - `realtime` (false) and `height` (0): with `realtime` on, timing is also sent on the realtime channel to the players around the start line, at `height` metres. The functions: - **Join** puts the player on the start line; it is refused `started` once the countdown begins, unless the player is already racing, and `full` past `racers_max`. A player's next report from farther than they could have moved is answered with the line, a success and not a refusal, and the component moves them there. - **Start** is called by the first to join, or a developer; anyone else is refused `forbidden`, and a race with no racers `not_ready`. It begins the countdown, which holds every racer on the line: their reports are answered with it. - **Leave** takes a player off the grid in the lobby. Once the countdown begins, it retires them: they did not finish and cannot join again. - **GetGhost** answers the winner's run, `FCrowdyKitRaceGhost`: `Player`, `TotalMs`, and `Frames`, each an `FCrowdyKitRaceGhostFrame` with an `AtMs` from their start and an `X` and `Y` in world metres, to replay as a ghost cart. Before anyone has won, `Player` is 0 and `Frames` is empty. - **Move** is the position function the component calls for you. **GetState** answers the State. The State, which everyone sees: - `Phase` reads **Lobby**, **Countdown**, **Racing** or **Finished**. `CourseId`, `Gates` (`X`, `Y` and `Radius` each) and `Laps` are the Config's, so a client can draw the course. - `GoAt` is when the countdown ends, `ClosesAt` when the finish grace runs out, and `EndsAt` when the time limit does, in Unix milliseconds, each 0 when there is none. - `Racers` has an `FCrowdyKitRacer` each: `Player`, `X` and `Y` in world metres, `Lap`, `NextGate` (the index in `Gates` of the gate to pass next), `BestLapMs`, `Place` and `TotalMs` once they finish, and `bDidNotFinish`. A value that is not there yet reads 0. The events arrive as one message, **RaceEvent** (`FCrowdyKitRaceEvent`), heard as the arena's is: **Go** has `At`; **Timing** has a `Stage` (an `ECrowdyKitRaceStage`: **Started**, **Checkpoint**, **Lap** or **Finished**, with **Has Stage** false for one this client does not know), `Player`, `Checkpoint`, `Lap` and `ElapsedMs`; **Violation** has `Player`, `What` and `Detail`, for a report the hub cut or held back; **Retired** has `Player` and `Why`, `left` or `idle`; and **Finished** has `Results`, an `FCrowdyKitRaceResult` for every racer with their `Player`, `Place` and `TotalMs`, the finishers first in place order and then, with 0 for both, those who did not finish. Where the SDK's copy differs from the platform's [Races and ball games](/game-kit/modules/racing): - **Positions are world metres, z up,** for the gates, `Move`, the State, the ghost and the events. The platform's keep x and z in its own units from the course's origin. - **A NaN or infinite position is refused `bad_request`,** and changes nothing. - **The movement check is the arena's travel budget,** so a client that reports more often does not move faster, a slow connection is not cut short, and the hub's own placements on the line never count against a racer. - **No flying start.** The countdown holds racers on the line and the start sets them there with no travel saved, so a racer cannot be past the line at the go. - **A gate between two reports counts,** and its time is when the path crossed it, not when the report arrived. The platform's checks only the reported points, so a racer who reports less often could miss a gate, and is timed later. - **A race always ends.** It closes when every racer has finished or retired, `finish_grace_ms` after the winner, or at `time_limit_ms`. A racer who leaves or stops reporting retires. The platform's waits for every racer to finish. - **Snapshots are not sent for every report:** at most five a second, as the arena's, and one at once when the race starts, goes, or closes. - **Results list every racer, the finishers first in place order,** and a time reaches the board whatever the length of the race's Instance Id. The ghost covers the whole run, however long. **In Blueprint**, with the **Crowdy Server Object** component holding `CSO_Kit_Race` and `mill-loop-1`: 1. On the join button, add **Call Join (CSO_Kit_Race)**. The first to join then presses a button wired to **Call Start**. 2. Add **On RaceEvent Received (CSO_Kit_Race)**, bound at **Event BeginPlay** with the component as its **Target**. From **Received**, **Switch on ECrowdyKitRaceEventType**: on **Go**, hide the countdown; on **Timing**, show **Lap** and **Elapsed Ms** when **Player** is the signed-in player; on **Finished**, show the podium from **Results**. 3. Add **On Racers Changed (CSO_Kit_Race)**, and for the signed-in player's racer, light the gate at **Next Gate** in the State's **Gates**. **In C++**, with `Race` the acquired `CSO_Kit_Race`, the pawn finds the gate to drive to next: ```cpp bool AMillCart::NextGate(FVector& OutWhere) const { const FCrowdyKitRaceState* State = Race->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitRacePhase::Racing) { return false; } const int64 Me = Race->GetSignedInUserId(); const FCrowdyKitRacer* Mine = State->Racers.FindByPredicate([Me](const FCrowdyKitRacer& Racer) { return Racer.Player == Me; }); if (!Mine || Mine->bDidNotFinish || Mine->Place > 0 || !State->Gates.IsValidIndex(Mine->NextGate)) { return false; } const FCrowdyKitRaceGate& Gate = State->Gates[Mine->NextGate]; OutWhere = FVector(Gate.X * 100.0, Gate.Y * 100.0, GetActorLocation().Z); return true; } ``` ### Play a ball game On the village green, two teams play ball in `green-1`. Add `ball`, deploy, and have each player's pawn hold `CSO_Kit_Ball` with that Instance Id in a **Crowdy Server Object** component. The pitch is a rectangle of the world. In `CSO_Kit_Ball`'s **Game Kit** category, **Config** places it: `origin` is its corner in world metres, `width` runs along X from there (60 by default) and `depth` along Y (40), and each end wall, at `origin` and at `origin` plus `width`, has a goal `goal_width` wide (12) at its middle. Red attacks the far end, at `origin` plus `width`, and blue the near end. To draw the ball, set `realtime` to `true` and give **Kit Actors**' kind `ball` your ball's class, as [Show a starter's actors](#show-a-starters-actors) says: the ball is then shown to every player within reach of the pitch, at `height` metres (0 by default). With `realtime` on, the match's Instance Id may be at most 29 characters, with no `@` or spaces, since the ball's actor carries it. The other keys, each with its default: - `team_max` (5), `score_to_win` (3) and `duration_ms` (300000, five minutes). - `claim_radius` (1.5 metres), how near the ball a player must be to claim it, and `protect_ms` (1500), how long a new holder cannot be robbed. - `pass_speed` (18) and `shoot_speed` (28) in metres a second, and `friction` (0.8), the share of its speed a loose ball loses each second. - `max_speed` (9), `teleport` (15) and `grace` (1.25), the movement check as the race's. - `idle_ms` (15000), how long a player may go without a report while the game is played before they are taken off the pitch; 0 never does. The functions: - **Join** takes a **Team** (`FCrowdyKitBallJoinInputs`), **Red** or **Blue**, and puts the player at the middle of their half; a full team is refused `full`. Before the game and after it, Join again changes team. While it is played, a player keeps their team (another is refused `started`), and one who left comes back where they left, so leaving and joining is not a shortcut across the pitch. - **Start** is called by the first to join, or a developer, and needs a player on each team (`not_ready`). It puts every player at the middle of their half and the ball at the centre. - **Claim** takes a loose ball within `claim_radius`, or the ball from an opponent once their `protect_ms` is over. It is refused `out_of_reach`, `conflict` when a teammate has it, and `cooldown` while the holder is protected or when claiming twice within half a second. - **Pass** takes **To** (`FCrowdyKitBallPassInputs`), a teammate's user id (`bad_request` for anyone else), and **Shoot** an aim **X** and **Y** in world metres; both are refused `conflict` when the player does not have the ball. The ball runs on, slows, and turns off the side and end walls; it scores when it crosses an end wall inside the goal. - **Leave** takes the player off the pitch; a ball they held drops loose where they were. **Move** is the position function. **GetState** answers the State. The State, which everyone sees: - `Phase` reads **Lobby**, **Playing** or **Finished**; `Origin`, `Width`, `Depth` and `GoalWidth` are the Config's. - `Ball` is an `FCrowdyKitBall`: `X` and `Y` in world metres, `VX` and `VY` in metres a second, `Holder` (0 when loose), `HeldSince` and `LastTouch`. A held ball is where its holder is. - `Players` has an `FCrowdyKitBallPlayer` each: `Player`, `Team`, `X` and `Y`. `Score` is red's then blue's. `EndsAt` is when time runs out, 0 outside a game being played, and `Winner` the winning team when **Has Winner** is true (false before the end, and for a draw). The events arrive as one message, **BallEvent** (`FCrowdyKitBallEvent`, whose `Type` is an `ECrowdyKitBallEventType`): **Claimed** (`Player`, `Team`, and `From`, the user id they took it from, 0 for a loose ball), **Passed** (`Player`, `To`, `Team`), **Shot** (`Player`, `Team`), **Goal** (`Team`, `Player`, the last to touch it, and `Score`), **Violation** (`Player`, `What`, `Detail`), **Left** (`Player`, `Team`), **Removed** (`Player`, `Team`, and `Why`, `idle`) and **Finished** (`Winner` with **Has Winner**, and `Score`). Where the SDK's copy differs from the platform's [Races and ball games](/game-kit/modules/racing): - **Positions are world metres, z up,** and `origin` places the pitch. The platform's keep the pitch at its own origin, and show the ball at an `anchor` chunk. - **The ball's actor names its match.** Its **Instance Id** on the Crowdy Kit Actor component is the match's, so a client with several games near each other knows which ball is whose; it reaches every player within the pitch's size of it. - **A NaN or infinite position or aim is refused `bad_request`,** and changes nothing. - **There is a Leave, and a player may join a game being played,** at their place when they come back. The platform's has no way to leave and refuses a join once the game starts, so a player who dropped could never return. - **A player who stops reporting is taken off the pitch** after `idle_ms`, with a **Removed** event, and the ball they held drops loose. The platform's keeps them, and the ball, until the game ends. - **The movement check is the arena's travel budget,** the pitch's edge is no strike, and **Start** puts the teams at their halves. **In Blueprint**, with the component holding `CSO_Kit_Ball` and `green-1`: 1. On the team buttons, add **Call Join (CSO_Kit_Ball)** with **Team** set to **Red** or **Blue**. 2. On the kick button, add **Call Claim** when **Holder** in the State's **Ball** is not the signed-in player, and **Call Shoot** with the goal's X and Y otherwise: red's goal is at **Origin X** plus **Width**, and both goals at **Origin Y** plus **Depth** over 2. 3. Add **On BallEvent Received (CSO_Kit_Ball)**, bound at **Event BeginPlay**, and on **Goal** show **Score**. **In C++**, with `Ball` the acquired `CSO_Kit_Ball`, the pawn shoots at the goal it attacks: ```cpp void AGreenPlayer::ShootAtGoal(ECrowdyKitBallTeam MyTeam) { const FCrowdyKitBallState* State = Ball->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitBallPhase::Playing || State->Ball.Holder != Ball->GetSignedInUserId()) { return; } FCrowdyKitGroundPosition Aim; Aim.X = MyTeam == ECrowdyKitBallTeam::Red ? State->Origin.X + State->Width : State->Origin.X; Aim.Y = State->Origin.Y + State->Depth / 2.0; Ball->Call(TEXT("Shoot"), FInstancedStruct::Make(Aim), [](const FCrowdyServerCallResult& Result) { UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("No shot: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); } ``` The ball's actor is drawn and moved by the SDK; its **Crowdy Kit Actor** component's **Owner** is the holder, so a glow on the held ball listens to **On Kit Actor Changed**. The platform's [Races and ball games](/game-kit/modules/racing) page has the rest of both starters. ## Show a starter's actors Some of the platform's starters show actors their server code moves: of the ones Add starter offers, `npcs`, whose mobs, NPCs and pets are its actors, and `ball`, whose ball is one when its Config's `realtime` is on. Such an actor reaches the players near it on the realtime channel, and the SDK draws it like another player's character. 1. In the starter's definition, in the **Game Kit** category, give each kind in **Kit Actors** the actor class it is drawn as, such as `mob` and `BP_Wolf`. `CSO_Kit_Npcs` comes with the kinds `mob`, `npc` and `pet` and no classes. While Kit Actors has a kind, the definition is listed in the **Game Kit Actor Starters** project setting (**Crowdy SDK**, **Server Objects**; `GameKitActorStarters` on `UCrowdySDKDeveloperSettings`), which a packaged game reads, and which cooks the definition and its classes. A kind with no class that loads is not drawn, and the log says so once for that kind. 2. Every actor drawn for a kit actor has a **Crowdy Kit Actor** component, which the SDK adds when the class has none. Its **Kit Actor** is the actor as its hub last showed it: **Kind**, **Id** (its id in its hub, which that hub's calls take), **Instance Id** (the Instance Id of the hub that shows it, such as the area `2,-1` of an `npcs` mob: the Server Object to call for that actor; empty when the hub's actor id does not name one), **Name**, **Hp** and **Max Hp** when **Has Hp** is true, **Mode**, **Owner**, **Location** and **Rotation**. **On Kit Actor Changed** (`OnKitActorChanged`, an `FOnCrowdyKitActorChanged` with the component and the Kit Actor) runs when the actor starts drawing a kit actor and whenever anything but where it is changes, so a health bar or a nameplate listens there; the actor itself moves smoothly to where it is. To bind the event at **Begin Play**, add the component to the class yourself. 3. On a map drawn with Mass, which has no actor per entity, the Crowdy Kit Actor subsystem lists the kit actors this player sees now. **Get Kit Actor Count** (**Kind**) says how many, and **Get Kit Actor** (**Kind**, **Index**, and a **Kit Actor** output) returns true and fills the one at that index, or false when there is none. **Kind** None means every kind. **Get Kit Actors** returns them all as a copy, so a Blueprint that reads every tick, such as a Mass map's, uses the count and **Get Kit Actor**. An actor's index changes as others come and go, so follow an actor by its **Id**, not its index. A kit actor leaves when its hub says so, or after **Kit Actor Timeout** without an update: 1 second by default, a setting of the map profile's actor management beside the timeout for other actors. A kind that no starter's Kit Actors names reads as None. Positions on the wire are in chunks with z up, which the SDK turns into units by the session's chunk size (`UCrowdyGameSession::SetChunkSize`, 1600 units unless your project changes it). **Chunk Position** (`ChunkPosition`) on a **Kit Actor** holds the raw chunks the hub sent, and **Location** is in world units. In C++ the subsystem's `GetChunkSize()` gives the size it uses, and `PlaceInWorld(KitActor)` turns a Kit Actor's chunks into a place in this world. An update whose **Name** is over 128 bytes or whose **Mode** is over 32 bytes is dropped, and so is one whose **Id**, **Hp**, **Max Hp** or **Owner** is not a whole 64-bit integer. What the component holds is view state: an actor's health and mode are its hub's to decide, as with any [Server Object](/exec/intro) value. Nothing on the realtime channel proves a hub sent an update, so another player's game can send one under a kit actor's id: show what **Kit Actor** says, and never decide gameplay from it. Call the hub, which checks every call, as **Attack** does. ### The npcs starter's area A player is in an area while they stand in its 4 by 4 chunks, within one chunk (1600 units) of the area's ground height, so a **Crowdy Server Object** component on the player's pawn holding `CSO_Kit_Npcs`, with its Instance Id set to **This Actor's Area**, follows the area they are in. The ground height is 0 chunks unless the starter's **Config** says otherwise: `{"z": 3}` puts it at 3 chunks, so areas work on a map whose ground is far from Unreal Z 0. `z` is a whole number of chunks within plus or minus 4294967296. The scan counts players within 1 chunk of that height and the area's actors are drawn at it, so give their classes a way to stand on your ground if it is not flat at that height. An area keeps the height it was created with, so changing `z` affects only areas made afterwards. - Its records are the area's members, one **FCrowdyKitAreaMember** each: its **Type** (**Mob**, **Npc** or **Pet**), **Id**, **X** and **Y** in chunks, and the fields of its type (a mob's **Kind**, **Hp**, **Max Hp** and **Mode**; an NPC's **Name** and **Line**; a pet's **Species**, **Name** and **Mode**). Its State is empty. - Mobs live 1.5 chunks from the middle of the area (it was 3). So a player standing beside a mob is always in the area, and **Attack** never refuses `not_in_area` there, and one mob is always in view and within day aggro range from anywhere in the area: a player at the centre is always chased. An area saved earlier puts its mobs at the new distance when it loads and keeps its pets. - **Attack** with a mob's **Id** hits it for 10, once every half second, while the player is within 1.5 chunks of it; its answer is the mob's **Hp**, and at 0 the mob is down (refused with `down`) for 10 seconds. Mobs never hurt players. **Talk** with an NPC's **Id** answers that NPC. - **Adopt** gives the player a pet where they stand (at most 3 a player and 64 an area), and **RenamePet**, **SummonPet** (it follows them), **DismissPet** and **ReleasePet** take one of their own pets; **GetPets** answers their pets with each one's **Bond**, which only its owner reads. - A refusal's **Code** is `not_in_area`, `out_of_reach`, `cooldown`, `down`, `limit`, `full` or `forbidden`. The SDK's `npcs` keeps x and y on the ground and z up, as Unreal does, so its areas and actors are not where the platform's own `npcs` starter, which is y up, puts them; an area the platform's starter saved starts again. ```cpp #include "Replication/Components/CrowdyKitActorComponent.h" #include "Replication/Subsystems/CrowdyKitActorSubsystem.h" // In the wolf's BeginPlay, with the component on the class: its health bar follows its hub. if (UCrowdyKitActorComponent* Kit = FindComponentByClass()) { Kit->OnKitActorChanged.AddDynamic(this, &AWolf::HandleKitActorChanged); } // Anywhere in the game world: every mob this player sees now. const TArray Mobs = GetWorld()->GetSubsystem()->GetKitActors(TEXT("mob")); // Every tick, without a copy: a reference to the subsystem's own list. A Kind of None is the actors of no named kind. const TArray& Wolves = GetWorld()->GetSubsystem()->ViewKitActors(TEXT("mob")); ``` ### Hit a drawn mob Two neighbouring areas each number their mobs from 1, so a wolf's **Id** alone does not say which area it belongs to. Its **Instance Id** does: the SDK's `npcs` names each actor `M`, `N` or `P` (mob, NPC or pet), the area's key, `@` and the actor's id, such as `M2,-1@3`, and the SDK reads the area out of it. In Oakford, with `BP_Wolf` given to the `mob` kind and `CSO_Kit_Npcs` added and deployed, a villager hits the wolf they click. **In Blueprint**, in `BP_Wolf`'s event graph, on a click: 1. Add a **Crowdy Kit Actor** component to `BP_Wolf` in its Components panel, named `CrowdyKitActor`, so the graph can read it. Get its **Kit Actor**, and break it with **Break Crowdy Kit Actor State**. Its **Instance Id** is the wolf's area, such as `2,-1`. 2. Put that **Instance Id** into **Get Server Object** for `CSO_Kit_Npcs`, then **Call Attack (CSO_Kit_Npcs)** on it with **Mob** set to the Kit Actor's **Id**. (Or use a **Crowdy Server Object** component on the wolf holding `CSO_Kit_Npcs`, with its Instance Id set from the same pin.) 3. Under **On Success**, **Hp** is the wolf's health after the hit. At 0 the wolf is down: it stops being drawn and comes back after 10 seconds. Under **On Failed**, **Code** is `out_of_reach` when the villager is too far away, `cooldown` when they hit again within half a second, and `down` when the wolf is already down. [Blueprint snippet: kit-npc-attack, see /bp/unreal-sdk/kit-npc-attack.txt] **Talk** works the same with an NPC: its **Instance Id** and **Call Talk (CSO_Kit_Npcs)** with **Npc** set to its **Id**. **In C++**, with `KitNpcs` a `UPROPERTY` holding `CSO_Kit_Npcs`, in the wolf's click handler: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" #include "Replication/Components/CrowdyKitActorComponent.h" const UCrowdyKitActorComponent* Kit = FindComponentByClass(); if (!Kit || Kit->KitActor.InstanceId.IsEmpty()) { return; } FString Error; UCrowdyServerObject* Area = GetGameInstance()->GetSubsystem()->Acquire(KitNpcs, Kit->KitActor.InstanceId, this, Error); if (!Area) { return; } FCrowdyKitAttackInputs Inputs; Inputs.Mob = Kit->KitActor.Id; Area->Call(TEXT("Attack"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { if (const FCrowdyKitAttackReply* Reply = Result.Reply.GetPtr()) { UE_LOG(LogTemp, Log, TEXT("The wolf has %d left"), Reply->Hp); return; } UE_CLOG(Result.Code == FName(TEXT("out_of_reach")), LogTemp, Log, TEXT("Too far: %s"), *Result.Reason); }); ``` An empty **Instance Id** means the hub's actor id does not name one, which a kit actor of the SDK's `npcs` never does. An `npcs` added before this keeps the Rust it was added with, and sends the old ids, so **Instance Id** reads empty until you remove it and add it again, or bring the SDK's `Resources/GameKit/npcs/` changes into your `Server/npcs/`, and deploy. ## Change a starter A starter is a beginning, not a black box, and changing one is changing two things, as for a type of your own: - **Its Rust.** A starter's server code is `Server//src/lib.rs`, the starter's own, and the definition asset's [Server Code tab](./write-server-logic.md#the-server-code-tab) shows and edits that file. Its Code Source reads **Game Kit Starter**, which you cannot choose there: only Add starter makes one. **Generate** never writes a starter's code, and refuses with "world is a Game Kit starter: its server code is the starter's own Rust in .../src/lib.rs, which Generate never writes; edit that file". Keep the functions' names and the topics as they are, and the SDK keeps reaching them; the platform's [Your own version of a starter](/game-kit/deploy#your-own-version-of-a-starter) says what to keep. - **Its definition.** The functions, their inputs and outputs, and the State struct, which you can point at a struct of your own. A **Game Kit** category in Details holds what only starters have: **State Topic**, the topic whose snapshots are the variables (`world`, `queue`, `session`, `board`, `player`, `market`, `duel`, `battle`, `encounter`, `run`, `territory`, `zone`, `table`, `arena`, `race` or `ball`); **Collection Topic** and **Record Struct**, the topic whose members are the records and the struct each one fills (`nodes` and `FCrowdyKitWorldNode` on the world, `area` and `FCrowdyKitAreaMember` on the npcs, empty on the others; a field whose server name is `id` takes the member's id); **Owner Values** and **Keyed By Player**, the [values only the player reads](#values-only-the-player-reads); **Position Function** and **Position Reports Per Second**, the function the component [reports where the player is](#report-where-the-player-is) with, and how often (the arena's are `Move` and 5); **Parent**, the type it runs under (empty makes it the app's root); **Calls**, the types its Rust calls; and **Config**, a JSON object the starter reads when one of its Server Objects first starts, such as `{"day_ms": 600000}` for a ten-minute game day on the world, whose keys are `day_ms`, `nodes`, `node_amount` and `regrow_ms`. Each must be a whole number written without a decimal point or an exponent, from 0, and `nodes` at most 65535. A key in another case, such as `Nodes`, is ignored by the starter and by the check. **Add starter** and **Deploy** refuse a starter Config that holds two ids of one kind (item, currency, faction, counter or pity group) differing only by case, such as `Gold` and `gold`, since the SDK reads them as one. The session's keys are those of `FCrowdyKitSessionConfig` in snake_case (`min_players`, `max_players`, `turn_ms`, `target_score`, `max_rounds`, `lobby_idle_ms`) and `board`. The player's Config, one for every player, sets the inventory's `slots`, the `items` catalog, the soft `currencies`, the `start` bundle a new player gets, `levels`, `achievements`, `quests`, the `client_counters` a client may report with **Step**, `recipes`, `loot` tables, idle `production` and a `secret` mixed into loot rolls; the market's sets its `vendors`, the `book_items` with an order book, the `book_currency` they trade in, `orders_max` per player, `trade_ms` before an open trade is called off, and `player_type`, the Type Name of the players' hubs. The platform's [Player](/game-kit/modules/player) and [Market](/game-kit/modules/market) pages give each key's shape. The deck's Config holds one key, the `secret` mixed into every shuffle, which **Add starter** writes for you, as it writes the player's. The matchmaker and leaderboard read no Config. The other game-mode starters do: the minigame's `best_of` and `round_ms`; the tactics battle's board size, obstacles, phase time and units; the director's field, lane, towers, `kinds`, `waves` and `boss`; the instance's `floors`, `rooms_per_floor`, `party_max`, `idle_ms` and `board`; the territory's `points`, `factions`, `win_score` and income; the zone's `center`, `radius`, `phases` and damage; and the arena's `origin`, `width`, `depth`, `spawns`, `classes`, `players_max`, `kill_target`, `duration_ms`, `respawn_ms`, `teleport`, `remove_after_strikes`, `idle_ms`, `realtime` and `height`, as [Fight in an arena](#fight-in-an-arena) lists them; the race's and the ball's are in [Run a race](#run-a-race) and [Play a ball game](#play-a-ball-game). The platform's [Duels](/game-kit/modules/minigames), [Grid battles](/game-kit/modules/tactics), [Waves and bosses](/game-kit/modules/encounters), [Private runs](/game-kit/modules/instances) and [Control points and circles](/game-kit/modules/territory) and [Arenas](/game-kit/modules/arenas) pages give each one's shape. The settings a starter's Rust decides for itself are hidden: Readable By, the visibility of each variable, Members, Records, Timers and Events, Callable By, Required Feature and Cooldown. ![The Game Kit category of CSO_Kit_Npcs: its Collection Topic area and Record Struct, Kit Actors with the mob, npc and pet kinds, Parent world, Calls world and an empty Config](/img/unreal-sdk/kit-starter-definition.png) On the Overview tab a changed starter reads **Changed** like any type, and lists `config` or `parent` when those changed. A starter is marked **Problem** when its Config is not a JSON object ("its Game Kit Config is not a JSON object; correct it, or empty it to keep the starter's defaults"), when two starters have no Parent ("... an app has one root; give one of them a Parent"), when Parents run in a circle, when a Parent is a Server Function Library, when the `world` starter's Config has a `day_ms`, `nodes`, `node_amount` or `regrow_ms` that is not a whole number as described above, or when a `player` or `deck` starter's `secret` is not a string, since the starter would refuse its Config. A problem blocks Deploy. Deploy also warns, without stopping, when a `player` or `deck` starter's Config has no `secret` (exactly that key, a non-empty string), since players could then work out loot rolls and deals; see [Play a card game with a hidden hand](#play-a-card-game-with-a-hidden-hand). It is marked **Needs generating** when its crate lost a file: "its server code is missing ...; restore it, or remove the type in Server Compute and add the starter again". ## Remove starters From the command line, `-op=removestarter` takes a starter out of the project together with every starter that runs under it or calls it: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=removestarter -starter=world -yes ``` Removing `world` removes every starter, since they all run under it; removing `matchmaker` removes the matchmaker and the session; removing `session` removes the session and the matchmaker, which calls it, and leaves the leaderboard; removing `leaderboard` removes only the leaderboard, since a session or an instance calls it only when its Config names a board; removing `player` removes the player and the market, which calls it, and removing `market` removes only the market. Removing `npcs`, `minigame`, `tactics`, `director`, `instance`, `territory`, `zone`, `deck` or `arena` removes only that starter, since nothing else calls it. For each it deletes the definition and the whole `Server//` folder, your edits to its Rust included, and it clears **Game Kit World** when that named one of them. Without `-yes` it names what it would remove and changes nothing: "This deletes session, matchmaker, world: their definitions and server code, edits included; pass -yes to confirm." It logs "removing session, matchmaker, world", then "removed session, matchmaker, world", and its result has `starter` and `removed`. Before it changes anything it checks, and removes nothing when: - a starter's folder is, or holds, a link to somewhere else, since deleting it could delete what the link points to; - a definition's file is read-only, for example checked in to source control without a checkout: make it writable and run it again; - something else uses a definition, such as a Blueprint whose Server Object component holds `CSO_Kit_World`: the message names each one, so remove those uses first. If a file is held open while it deletes, for example by an IDE or `cargo`, the message ends "run -op=removestarter again to finish", which picks up where it stopped. On the Server Compute page, **Remove from project...** in a starter's **...** menu does the same, after a dialog naming every starter it removes and what the next deploy takes offline. Either way the server is not touched until your next deploy, which no longer sends the starters and removes them. Without `world`, that deploy sends the Crowdy SDK's root again, and **Get Players Online** reads it. ## In the example world Oakford's lantern posts light up at night, and villagers gather oil from the world's resource nodes. Add the `world` starter on Server Compute and deploy. **In Blueprint**, in the lantern post's event graph: 1. Add **On bNight Changed (CSO_Kit_World)** and wire **Event BeginPlay** into its **Bind**. It needs no Target, since the world has Only One Instance. From **Changed**, wire its **bNight** to **Set Visibility** on the lantern's Point Light component, named `Light`. 2. To gather oil, add **Call Harvest (CSO_Kit_World)** and set its **Node** input to a node's id. The world's resource nodes are its records: from the world's **Get Server Object**, **Get Record Ids** lists them, and **Watch Record** on the oil node's id runs with what it holds now and after each harvest, read with **Find Record** and **Get Server Value** `Amount`. 3. Under **On Success**, its **Left** output is what the node holds now. Under **On Failed**, show the arrow at the bottom of the node: **Outcome** is **Denied** with **Retryable** true and **Code** `cooldown` when the villager harvests too soon, and **Refused** with **Code** `empty` when the node is bare until it grows back. [Blueprint snippet: kit-world-night, see /bp/unreal-sdk/kit-world-night.txt] [Blueprint snippet: kit-world-harvest, see /bp/unreal-sdk/kit-world-harvest.txt] **In C++**, with `KitWorld` a `UPROPERTY` holding `CSO_Kit_World`, the lantern post follows the night and a villager harvests: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" FString Error; UCrowdyServerObject* World = GetGameInstance()->GetSubsystem()->Acquire(KitWorld, FString(), this, Error); World->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, TConstArrayView Fields) { const FCrowdyKitWorldConditions* Conditions = Object->GetState().GetPtr(); if (Conditions && Fields.Contains(TEXT("night"))) { Light->SetVisibility(Conditions->bNight); } })); FCrowdyKitHarvestInputs Inputs; Inputs.Node = NodeId; World->Call(TEXT("Harvest"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { if (const FCrowdyKitHarvestReply* Reply = Result.Reply.GetPtr()) { UE_LOG(LogTemp, Log, TEXT("The node holds %d"), Reply->Left); return; } UE_CLOG(Result.Code == FName(TEXT("empty")), LogTemp, Log, TEXT("Bare until it grows back: %s"), *Result.Reason); }); const FString OilNode = LexToString(NodeId); World->WatchRecord(OilNode, FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [OilNode](UCrowdyServerObject* Object, TConstArrayView Fields) { const FCrowdyServerRecord* Record = Object->GetRecord(OilNode); const FCrowdyKitWorldNode* Node = Record ? Record->Values.GetPtr() : nullptr; UE_CLOG(Node != nullptr, LogTemp, Log, TEXT("Oil left: %d"), Node ? Node->Amount : 0); })); ``` The world's Instance Id is ignored, since it has Only One Instance. For a match, [Find Match](#find-a-match-and-take-turns) on `CSO_Kit_Matchmaker` with the queue's name, then acquire `CSO_Kit_Session` with its **Session Id** and call **Join**, **Ready** and, as its host, **Start**; **Is My Turn** says when to let the player move. Oakford also keeps a weekly board of its lantern-lighting matches. Add the `leaderboard` starter, set `CSO_Kit_Session`'s **Config** to `{"board": "oakford-weekly"}` and deploy. Each finished match then posts its players' scores, and the village notice board, with `KitLeaderboard` a `UPROPERTY` holding `CSO_Kit_Leaderboard`, reads them: ```cpp UCrowdyServerObject* Board = GetGameInstance()->GetSubsystem()->Acquire(KitLeaderboard, TEXT("oakford-weekly"), this, Error); Board->Call(TEXT("GetMyPlace"), FInstancedStruct(), [](const FCrowdyServerCallResult& Result) { const FCrowdyKitBoardPlace* Place = Result.Reply.GetPtr(); UE_CLOG(Place && Place->Rank > 0, LogTemp, Log, TEXT("This week you are number %d with %lld"), Place ? Place->Rank : 0, Place ? Place->Score : 0LL); }); ``` In Blueprint the same is **Call GetMyPlace (CSO_Kit_Leaderboard)** on the object **Get Server Object** gives for `CSO_Kit_Leaderboard` and `oakford-weekly`, with its **Rank** and **Score** outputs; **On Top Changed (CSO_Kit_Leaderboard)** keeps the notice board's top ten current. At Oakford's market, villagers swap the planks they craft for gold. Add the `market` starter, which brings `player`, and deploy. [Trade between two players](#trade-between-two-players) is the whole exchange; in C++, with `KitMarket` and `KitPlayer` `UPROPERTY`s holding `CSO_Kit_Market` and `CSO_Kit_Player`, the villager selling planks offers them on the trade the buyer opened, and the purse follows the wallet: ```cpp UCrowdyServerObjectSubsystem* Objects = GetGameInstance()->GetSubsystem(); UCrowdyServerObject* Market = Objects->Acquire(KitMarket, TEXT("oakford"), this, Error); UCrowdyServerObject* Me = Objects->Acquire(KitPlayer, FString(), this, Error); FCrowdyKitTradeOfferInputs Offer; Offer.Trade = TradeId; Offer.Bundle.Items.Add(TEXT("plank"), 3); Market->Call(TEXT("Offer"), FInstancedStruct::Make(Offer), [](const FCrowdyServerCallResult& Result) { UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("Not offered: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); Me->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, TConstArrayView Fields) { const FCrowdyKitPlayerState* State = Object->GetState().GetPtr(); if (State && Fields.Contains(TEXT("wallet"))) { Purse->SetText(FText::AsNumber(State->Wallet.Balances.FindRef(TEXT("gold")))); } })); ``` In Blueprint the offer is **Call Offer (CSO_Kit_Market)** with the trade's id and a **Bundle** made with **Make Crowdy Kit Bundle**, and the purse is **On Wallet Changed (CSO_Kit_Player)** on the player's own `CSO_Kit_Player`. Gold here is in-game soft currency, never real money. ## Related - [Game Kit](/game-kit/intro), [The starter hubs](/game-kit/starters) and [Deploying the starters](/game-kit/deploy) on the platform - [Deploy it with Server Compute](./deploy-with-server-compute.md): deploying the starters with your types, and the command line - [Create a Server Object type](./create-a-type.md) and [Write its server logic](./write-server-logic.md) - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md): the **Code** pin and `FCrowdyServerCallResult::Code` - [Troubleshooting](./troubleshooting.md#outcomes): every outcome, **Refused** included # Keep a record per entity Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/keep-a-record-per-entity # Keep a record per entity :::note Server Objects run on [ck-exec](/exec/intro). Records per entity are new in v2.19.0; see [What's Changed](../guides/whats-changed.md#2026-10-12-sdk-v2190). ::: A crowd of raiders on Oakford's north road is hundreds of entities, and each one has values the server must own: how hurt it is, what it carries, who hired it. A Server Object with **Holds** set to **A Record per Entity** keeps those values once for each entity, in one object, and tells each player only what that player may see. ## When to use it Use a record per entity when: - you have many entities, such as a crowd, a warband or a field of crops, each with the same few values; - the values are the server's to decide, so they belong in a [Server Object](./overview.md) and not in [Crowdy State](../runtime/crowdy-state.md); - players should see some values of an entity and others only for their own entities or their team's. The object that holds the records is a shared one: one for an area of the world, for a match or for a team. It is not one object per entity. An app can run at most 1,024 instances at once (see [Operations](/exec/operations#instances-and-versions)), so a Server Object for each of two thousand raiders cannot work, while one object per area holding their records can. A thing that has one set of values, such as a boss, a beacon or a player's stash, stays a normal Server Object. See [Relate Server Objects to each other](./relate-server-objects.md). The worked version of this page is [Raiders on the north road](./examples/raiders-on-the-north-road.md). ## Make the type Make the asset as on [Create a Server Object type](./create-a-type.md), then select **Type Settings** and set: | Setting | Where | Default | What it does | |---|---|---|---| | **Holds** | Server Object | **One Set of Variables** | Choose **A Record per Entity**. The variables become the fields of each record. | | **Visible to Players** | Each variable's Details | | **Server Only**, **Everyone**, **Owner Only** or **Team Only**. See [Who receives a value](#who-receives-a-value). | | **Max Records** | Records | 2048 | The most records one object keeps, 1 to 4096. | | **Send Changes Every** | Records | 250 ms | How often the server sends what changed, 50 to 5000 ms. | | **Team From** | Records | **This Object's Records** | Where a team comes from: **This Object's Records** or **Crowdy Team**. See [Who receives a value](#who-receives-a-value). | | **Area Size** | Areas | 4 | The side of one area, in chunks, 1 to 64. See [Pick the object](#pick-the-object). | | **Areas Followed Around a Player** | Areas | 1 | How many areas out from a player's own are followed, 0 to 3. 1 follows the 3 by 3 around it. | | **Reads World Data** | Platform Data | Off | Lets the server code ask which actors are near a chunk. Deploying asks for the `world.read` permission. | ![The raider type with Type Settings selected: Holds set to A Record per Entity, then the Records category (Max Records, Send Changes Every, Team From) and the Areas category (Area Size, Areas Followed Around a Player)](/img/unreal-sdk/server-object-records.png) **Readable By** must be **Every Player**: who sees which variable is decided per variable, not for the whole object. **Members From** must be **None**, since each record carries its own owner and team. Saving the asset says so when either is wrong. **Area Size** and **Areas Followed Around a Player** work for every type, whatever it holds. **Reads World Data** does too. A type of this kind starts with no records. The server code adds them. ## Who receives a value Every variable of a record has one audience: | Audience | Who receives it | |---|---| | **Server Only** | Nobody. It stays on the server. | | **Everyone** | Every player who watches the object. | | **Owner Only** | The player who owns the record's entity. | | **Team Only** | A player whose own record in this object has the same team as this record. | ![Loot selected on the raider type: Variable Name, Variable Type, Visible to Players set to Owner Only and the Default Value](/img/unreal-sdk/server-object-variable-audience.png) Each record has an **owner**, a player's user id or 0 for none, and a **team**, a string. Your server code sets both. They are not variables of your struct, and no player can set them. Which players count as the record's team depends on **Team From**: - **This Object's Records**, the default. A player's team in an object is the team of a record that player owns in that same object. A player who owns several records in one object uses the first by id. An empty team matches nothing. The team is a string you choose, such as `ravens`. - **Crowdy Team**. A record's team is the id of a [Crowdy Team](../services/teams.md), written in digits, and a player is on it when the platform says they are a member of that team. `insert` and `set_team` refuse a team that is not digits; an empty team means no team. Deploying asks for the `players.read` permission, since the server checks membership through the platform. Choose by how far a team reaches. With **This Object's Records** a player's team is known only inside one object. When records are kept per area, that means Team Only values reach a teammate only while both players are in the same area. If teams must see each other's values across areas, use **Crowdy Team**. A player who owns no record in the object and is on no matching team sees the Everyone values and nothing else. How the values travel depends on the audience: - **Everyone values** come in batches. The server collects the changes and, every **Send Changes Every**, sends the records that changed, each with only the Everyone variables that changed. A record that changes twice in between is sent once with its last value. A record you remove is removed on every player's side with the same batch. - **Owner Only and Team Only values** are fetched. The batch carries a notice that some changed, and the owner's or team's game asks the server for them, at most once a second for each object. They arrive a moment after the Everyone values. - **A player who joins, or whose game missed a batch,** reads every record again, a page at a time. A value a player does not receive reads as the variable's default, and raises no change. When a record's owner or team changes, the Owner Only and Team Only values of that record go back to their defaults for everyone who just lost access. When a player's own team changes, their game reads the object again. ## Pick the object The component on an actor, and a crowd row, choose which object holds the records. For a crowd the usual choice is the area the entity stands in. - **Only One Instance** (a setting of the type): one object for the whole world. Only for a small number of entities. - **Instance Id**: a name you choose, such as `match-7`, for a match or a team. - **This Actor's Area**: the object for the area the actor stands in, which changes as the actor moves. **This Actor's Area** cuts the ground into squares of **Area Size** by **Area Size** chunks. The object's Instance Id is the square's `x,y`: the chunk's x and y, which are Unreal's X and Y over the session's chunk size, each divided by Area Size and rounded down, so the area at chunk (-1, 5) with an Area Size of 4 is `-1,1`. The height is not part of the key. The component checks every half second and, when the actor crosses into another area, joins that area's object and lets go of the old one after a short grace period, so an actor on a border does not flap. Moving a record from one area's object to the next is your server code's job. One approach: the census in each area adds the entities it finds standing there and removes the ones that left, as the code below does. ## Write the server code Generate the type as on [Write its server logic](./write-server-logic.md). In `logic.rs` the functions are implemented on `Records`, the object's records, and not on the state struct: ```rust impl Functions for Records { fn skirmish(&mut self, _ctx: &Ctx) -> Result<()> { let ids: Vec = self.ids().map(str::to_string).collect(); for id in ids { let Some(raider) = self.get_mut(&id) else { continue }; raider.Health -= 5; if raider.Health <= 0 { self.remove(&id); } } Ok(()) } } ``` `Records` gives your code: | Name | What it does | |---|---| | `get(id)` | The record's values, or `None`. | | `get_mut(id)` | The record's values to change, or `None`. | | `insert(id, owner, team, state)` | Adds the record, or replaces the one with that id. Returns an error past **Max Records** (`records_full`), for an id that is empty or over 64 bytes, for a team over 64 bytes, and for either holding a control character (`bad_record`). | | `remove(id)` | Removes the record and returns its values. | | `set_owner(id, owner)`, `set_team(id, team)` | Change who owns the record and which team it is on. An error (`bad_record`) when there is no such record. | | `owner(id)`, `team(id)` | The record's owner (0 for none) and team. | | `ids()`, `len()` | Every id in order, and how many records there are. | | `owned_by(player)` | The ids of the records a player owns. | | `Records::area_of(chunk_x, chunk_y)` | The Instance Id of the area holding a chunk, worked out with this type's **Area Size**: the same key the component uses. | Changes made in one call are kept or put back together, like the variables of any Server Object: a function that returns an error leaves the records as they were. Ids are text, 1 to 64 bytes, and compared exactly, capitals included. ### Count who stands in the area To add the entities standing in an area, turn on **Reads World Data** and ask the world from a timer. `actors(at)` returns the actors in one chunk, and each one's `uuid` is the id its record uses. Ask for each chunk of the area in turn: ```rust fn census(&mut self, ctx: &Ctx) -> Result<()> { let Some((x, y)) = area_origin(&ctx.key) else { return Ok(()) }; for cx in x..x + AREA_SIZE { for cy in y..y + AREA_SIZE { let Ok(actors) = ctx.world().actors(ChunkPos::new(cx, cy, 0)) else { return Ok(()) }; for actor in actors { if self.get(&actor.uuid).is_none() { let _ = self.insert(&actor.uuid, 0, "", RaiderState::default()); } } } } Ok(()) } ``` Three things to know about the census: - **It reads the heights it asks for.** The call above reads ground level, height 0, and sees nothing standing higher. Ask for more heights if your world has them. - **One call answers up to 200 actors.** A chunk with 200 in the answer may hold more. Do not remove a record because its actor is missing from an answer that is full: skip the removals for that tick. - **Do not use `actors_near` for a dense crowd.** It answers at most 500 actors across several chunks, so a crowded area is cut short, and records of live entities would look as if they had left. `area_origin` turns the `x,y` in `ctx.key` back into the area's first chunk. [Raiders on the north road](./examples/raiders-on-the-north-road.md) shows the whole census, with the removal of the entities that left. The platform's pages on these calls are [World and platform data](/exec/world-and-platform-data). ## From Blueprint Put a **Crowdy Server Object** component on the actor, with **Definition** set to the type and **Instance Mode** set to **This Actor's Area**, **Instance Id** or the type's single instance. The actor needs a **Crowdy Entity** component as well: its id names the actor's record. Without one the component fails with "needs a Crowdy Entity component on this actor". ![The Crowdy Server Object component: Definition CSO_Raider and Instance Mode This Actor's Area](/img/unreal-sdk/server-object-component-area.png) The component reads **its actor's own record**, so the nodes work as they do for any Server Object. The typed **Get** and **On Changed** nodes, **On Variables Changed**, **Get Variables** and **Get Server Value** all give that record's values, and **Changed** runs when one of them changes. See [Read and follow its variables](./from-blueprint/read-and-follow-variables.md). To reach other entities' records, take the Server Object from the component with **Get Server Object**, and use these nodes on it: | Node | What it does | |---|---| | **Find Record** | Takes an **Id** and gives that record's **Values**, its **Owner** and its **Team**. Returns false when there is no such record. Read a variable out of **Values** with **Get Server Value**. | | **Get Record Ids** | The id of every record the player can see, in no particular order. | | **Holds Records** | True when the type keeps a record per entity rather than one set of variables. | | **Num Records** | How many records the object holds now. | | **Watch Record** | Takes a **Record Id** and an event (the delegate `FCrowdyServerRecordEvent`). The event runs with the **Object**, the **Record Id** and the **Changed** variables after each change of that record, and when it is removed (**Find Record** is then false). When the record is already current it runs once at once, naming all its variables. A watch added from an **On Records Changed** or **On Records Updated** handler gets that catch-up and does not run a second time for the same change. Records dropped because the object **Failed** are not reported; **On Status Changed** is. | | **Stop Watching Record** | Takes the same **Record Id** and event, and stops that watch. | | **On Records Changed** | An event that runs after every read, batch or fetch that changed records, with the **Object**, the ids that are new or changed, and the ids that were removed. | | **On Records Updated** | Like **On Records Changed**, but each changed record comes with its changed variables: a list of **Record Id** and **Changed Variables**, then the removed ids. **Changed Variables** is empty when only the record's owner or team changed. | **Watch Record** works like **Watch Message**: get the Server Object from the component, call **Watch Record** with a **Record Id**, and wire a custom event with the delegate's pins (the **Object**, the **Record Id** and the **Changed** variables) to its **Event**. The figure follows one record, here an id `raider-1` that your server code inserted, and prints its id each time it changes. [Blueprint snippet: so-record-watch, see /bp/unreal-sdk/so-record-watch.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. **Stop Watching Record** takes the same **Record Id** and the same event: [Blueprint snippet: so-record-unwatch, see /bp/unreal-sdk/so-record-unwatch.txt] **On Records Updated** is bound from **Begin Play** like any event of the object, with **Bind Event to On Records Updated** on the Server Object. Its event gets the changed records, each with its **Record Id** and **Changed Variables**, and the removed ids. The figure prints how many records changed: [Blueprint snippet: so-records-updated, see /bp/unreal-sdk/so-records-updated.txt] **Find By Asset** and the by-name **Watch Variables** read a type of this kind as if it had no records, so they give its defaults. Use the component, or **Find Record**. ## From C++ The component works as in Blueprint, and its `GetVariables()` and `OnVariablesChanged` give the actor's own record. To read others, acquire the area's object yourself. `FindGroundChunk` gives the chunk under a location as actor updates place it, and `GetAreaKey` on the definition turns it into the Instance Id: ```cpp UCrowdyServerObjectSubsystem* Objects = GetGameInstance()->GetSubsystem(); int64 ChunkX = 0; int64 ChunkY = 0; if (!Objects || !Objects->FindGroundChunk(GetActorLocation(), ChunkX, ChunkY)) { return; } FString Error; UCrowdyServerObject* Area = Objects->Acquire(RaiderDefinition, RaiderDefinition->GetAreaKey(ChunkX, ChunkY), this, Error); if (Area) { Area->OnRecordsChanged.AddUObject(this, &ARoadWatch::HandleRecords); } ``` `OnRecordsChanged`, an `FOnCrowdyServerRecordsChanged`, runs after every read, batch or fetch that changed records. It gives the object, the ids that are new or changed, the ids removed, and for each changed id the State fields that changed, one bit per field. Read a record with `GetRecord`, which returns an `FCrowdyServerRecord` holding the `Values`, the `Owner` and the `Team`: ```cpp void ARoadWatch::HandleRecords(UCrowdyServerObject* Object, TConstArrayView Changed, TConstArrayView Removed, TConstArrayView> ChangedFields) { for (const FString& Id : Changed) { const FCrowdyServerRecord* Record = Object->GetRecord(Id); const FRaiderState* Raider = Record ? Record->Values.GetPtr() : nullptr; if (Raider) { UpdateBar(Id, Raider->Health); } } } ``` `HoldsRecords()` says whether the definition keeps records, `NumRecords()` counts them, `GetRecordIds()` lists them, and `WatchRecord(Id, Listener)` follows one record, naming its variables on each change. Ids are compared with capitals significant. `WatchValues` and `GetState()` on a records object give the type's defaults, not a record. The rest of the C++ surface for records: - `UCrowdyServerObject::FindValues` gives a record's values by id, or the object's own when the id is null. The Blueprint **On Records Changed** is the `RecordsChanged` property, an `FCrowdyServerRecordsChanged`, and **On Records Updated** is the `RecordsUpdated` property, an `FCrowdyServerRecordsUpdated` whose `FCrowdyServerRecordChange` entries hold a `RecordId` and its `ChangedVariables`. - `UCrowdyServerObjectComponent::ReadsRecord` says whether the component reads its actor's record, and `UCrowdyServerObjectComponent::GetRecordId` gives that record's id. - `UCrowdyServerObjectDefinition::GetAreaKey` and the static `UCrowdyServerObjectDefinition::MakeAreaKey` turn a chunk into an area's Instance Id, and `UCrowdyServerObjectDefinition::HasCrowdyTeamRecords` says whether the type keeps records on Crowdy Teams. `UCrowdyServerObjectSubsystem::GetChunkSize` gives the chunk size the game session uses. - The settings are typed by `ECrowdyServerObjectHolds`, `ECrowdyServerFieldAudience` (**Everyone**, **Owner Only**, **Team Only**) and `ECrowdyServerTeamSource` (**This Object's Records**, **Crowdy Team**). ## Crowd rows A crowd drawn with the Crowdy Mass Rendering Backend has no actor for most entities, only rows, but a row reads the same records. See [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md). The record's id is the entity's id, the 32 characters the census calls report as `uuid`, so a census that keys records by the actor's `uuid` lands each record on its row. - Set the **Crowdy Server Object** component's **Instance Mode** on the entity's actor class to **Only One Instance**, **Instance Id** with an id typed, or **This Actor's Area**. With any other mode the rows get no server values, and the log says why once for the class. - Read the values with [Read its server values, from Blueprint](../mass/from-blueprint/read-its-server-values.md) or [Read its server values, from C++](../mass/from-cpp/read-its-server-values.md). ## Size it What a player receives is the changed records, times the bytes of each record, times the sends a second: > 500 records changed in a send, 20 bytes each, 4 sends a second: about 40 KB a second to each player. - **Raise Send Changes Every** when records change more than once in between: the last value is the only one sent. Lower it for faster feel, at the cost of more sends. - **Keep records small.** Put a value in a record only if a player or the server logic needs it per entity. - **Max Records** is 2048 unless you change it, and 4096 at most. A call that inserts past it is refused with `records_full`. - A client refuses a message over 1 MiB. The server splits a batch at 4,096 records or 512 KiB into several sends, in order, and a player's first read comes in pages of up to 512 KiB. - A player on a slow link falls behind and catches up. Batches queue instead of dropping. - Size the area so one object holds the records you want its watchers to receive. A player follows `(2r+1)` squared objects, with `r` the **Areas Followed Around a Player**: 9 by default. ## Limits and rules - **Readable By** must be **Every Player**, and **Members From** must be **None**. - A variable's audience is set per variable, and only a variable that is **Visible to Players** can be Owner Only or Team Only. A variable cannot be both. Saving checks both. - The function name `changes` and a timer whose name in snake case is `crowdy_flush` or `crowdy_sessions` are reserved. - A record's id is 1 to 64 bytes of text with no control characters. Ids that differ only in capitals are different records. - A team is at most 64 bytes. With **Team From** on **Crowdy Team** it is a team id in digits or empty, and anything else is refused. - A type starts with no records, so a seed must be empty. - The owner and team are set by server code only. ## If a value is not what you expect - **Always the default.** A player gets Owner Only values only for records they own, and Team Only values only when their own record in the object has the same team. A player who owns no record sees neither. - **A Team Only value arrives late.** Owner Only and Team Only values are fetched, at most once a second for each object. - **The component stays Connecting or fails.** See [Troubleshooting](./troubleshooting.md#a-server-object-does-not-become-ready). A record type needs its census or other server code to add records first. - **To see the pushes and fetches,** turn on [`crowdy.exec.trace`](./troubleshooting.md#seeing-what-a-server-object-is-doing). ## Related - [Raiders on the north road](./examples/raiders-on-the-north-road.md): a warband with a record per raider - [Create a Server Object type](./create-a-type.md#every-setting): every setting - [Access, members and timers](./access-members-and-timers.md#who-receives-a-variable-of-a-record): the audience of a variable, and the checks when you save - [Write its server logic](./write-server-logic.md#a-record-per-entity): the functions in `logic.rs` - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md#add-the-component) and [from C++](./from-cpp/get-a-server-object.md#one-object-per-area): This Actor's Area - [Read and follow its variables, from Blueprint](./from-blueprint/read-and-follow-variables.md#records) and [from C++](./from-cpp/read-and-follow-variables.md#records) - [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md): the entities drawn as crowd rows that read these records # Make a Server Function Library Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/server-function-libraries # Make a Server Function Library :::note Server Objects run on [ck-exec](/exec/intro). ::: A **Server Function Library** is a Server Object type that has functions and nothing else. It is the server's counterpart of a Blueprint Function Library: a place for logic that many callers share, with no instance of its own to hold. You describe it in the same [definition asset](./create-a-type.md) as any Server Object type, choose **Kind: Server Function Library**, and the editor hides what it cannot have. | | Server Object | Server Function Library | |---|---|---| | Keeps variables, saved between calls | Yes | No | | Instances | One for each Instance Id | One name for everyone; the server runs several instances of it and picks one for each call | | Calls answered at the same time | One for each Server Object | Several for each instance, on several instances | | Players watch its values, hear its messages | Yes | No: there is nothing to watch | | Members, timers, records, messages, cooldowns | Yes | No | | Players call its functions | Yes | Yes | | Other server code calls its functions | Yes | Yes | ## When to use one Use a library for a rule or a calculation that has no memory of its own and that several callers need: turning a seed into a loot drop, pricing an item, checking whether a move is legal, scoring a result. Because several instances answer at once, a busy game keeps getting answers where one Server Object would make its callers queue. A library can also change game truth, but only by calling a Server Object: a library function that rewards a player calls the player's own Server Object and that object changes its values. Do not use one for anything that has to remember. A counter, a stockpile, a match or a record per player belongs in a [Server Object](./create-a-type.md), because a library saves nothing: each instance runs your code in several copies, each with its own memory, so a value a function leaves in a variable can be there for one call and gone for the next. A library has no [timers](./access-members-and-timers.md#timers-and-events) and cannot [listen to messages](./access-members-and-timers.md#listen-to-other-server-objects), and a player cannot watch it; whatever players should see goes in a Server Object that the library's caller changes. Two things to plan for: - **The first call after the library has been idle can take over a second.** Players wait a little; server code calling it may be told the call timed out. See [A call to a library from server code fails](./troubleshooting.md#a-call-to-a-library-from-server-code-fails). - **Each instance counts toward the app's limit on running Server Objects**, 16 for an app with no funds and 16,384 for a funded one. A library that keeps 4 instances running leaves an unfunded app room for 12 other Server Objects. See [Scaling](#scaling). ## In the example world The Hollow Giant should drop something when it falls, and a chest on the north road should hand out a trinket. Both want the same rule: turn a seed and a tier into an item name. That rule keeps nothing, so it is a library, `CSO_LootRoller`, with one function, `RollLoot`. The chest calls it from a Blueprint or from C++, and the boss's server code calls it too. [Loot for the Hollow Giant](./examples/hollow-giant-loot.md) builds all of it. This page makes the asset. ## Make the asset Make a **Crowdy Server Object Definition** as on [Create a Server Object type](./create-a-type.md#make-the-asset) and name it `CSO_LootRoller`. Select **Type Settings**, the first row of the Server Object panel. In Details, under **Server Object**: | Setting | Value | |---|---| | Type Name | `loot_roller` | | Kind | **Server Function Library** | | Description | Turns a seed and a tier into an item name. | **Kind** is the second row, after **Type Name**. It is **Server Object** until you change it, and changing it rebuilds Details at once. The Server Object panel then shows **Type Settings** and **Functions**, and no Variables or Server Messages. Add the function `RollLoot` with **+** in **Functions**, as on [Create a Server Object type](./create-a-type.md#make-the-asset): **Callable By** on **Players**, and two inputs, `Seed` (Integer64) and `Tier` (Integer, starting at 1, with a **Value Range** of 0 to 4), and one output, `Item` (String). Or use structs, as the [boss](./examples/shared-boss.md#what-the-server-keeps) does. ![The CSO_LootRoller editor: the Server Object panel lists only Type Settings and Functions, and RollLoot reads (Seed, Tier) returns Item](/img/unreal-sdk/server-function-library-editor.png) A library has these settings, and no others: - **Type Name**, **Kind** and **Description**. - **Functions**, with their **Name**, **Callable By**, **Required Feature**, inputs, outputs, **Value Range** and **Server Name**. **Callable By** can be **Players**, **Server Only** or **Developers Only**; see [Callable By](./create-a-type.md#callable-by). - **Platform Data**, as on [Access, members and timers](./access-members-and-timers.md#platform-data), for what the library's server code reads and changes of players, the world and grids. - **Can Call** and **Can Call Any Type**, the types its server code may call; see [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). - **Scaling**, below. - **Server Limits**, collapsed, and **Server Names**, in the Advanced group, as on [Every setting](./create-a-type.md#every-setting). Everything else is hidden in Details and ignored everywhere, even if the asset still holds values from when it was a Server Object: it is not checked when you save, not in the generated server code, not deployed and not used by the game. That is the variables or State struct, **Visible to Players**, **Readable By**, **Only One Instance**, **Holds** and its records, **Members From** and the other member settings, **Areas**, **Timers**, **On Player Joined** and **On Player Left**, **Server Messages**, **Listen To**, **Save Every** and **Stop When Idle For**. Nothing picks an instance of a library, so a caller gives no Instance Id. A library whose functions are all **Server Only** is [closed to players](./access-members-and-timers.md#a-type-players-cannot-reach): only other server code and developer tools reach it. That is what a library that only your Server Objects use should be. ## Scaling The **Scaling** category, shown only for a library, says how many instances the server runs and how many calls each answers at once. The platform term is in brackets. ![With Type Settings selected, Details of CSO_LootRoller shows Type Name, Kind set to Server Function Library and Description, then Platform Data, Can Call and the Scaling category: Min Instances 1, Max Instances 1 and Parallel Calls per Instance 16](/img/unreal-sdk/server-function-library-type.png) | Setting | C++ | Range | Default | What it does | |---|---|---|---|---| | **Min Instances** [`replicas`] | `MinInstances` | 1 to 64 | 1 | How many instances run all the time. | | **Max Instances** [`max_replicas`] | `MaxInstances` | 1 to 64 | 1 | The most the server adds when calls wait. Equal to Min Instances, it never adds any. | | **Parallel Calls per Instance** [`concurrency`] | `ParallelCallsPerInstance` | 1 to 256 | 16 | How many calls one instance answers at the same time. | The server adds an instance, up to **Max Instances**, once calls have waited 2 seconds for an answer, and removes an added instance after about 30 seconds in which it is nearly idle. It never goes below **Min Instances**. So the library answers at least Min Instances times Parallel Calls per Instance calls at once, and more when it has grown. Start with the defaults. Raise **Max Instances** when bursts of callers wait, **Min Instances** to have those instances ready before the burst, and **Parallel Calls per Instance** to let each instance answer more at once. The longest one call may run is still the type's **Time per Call**, 2 seconds unless you change it, and a call that runs past it is answered **Timeout** like any other; see [Server Limits](./create-a-type.md#every-setting). Calls one instance runs at the same time share its processor and Time per Call counts the time that passes, so a function that keeps the processor busy needs a Time per Call that covers Parallel Calls per Instance of them; see [Library calls time out during a burst](./troubleshooting.md#library-calls-time-out-during-a-burst). Every instance that runs counts toward the app's limit on running Server Objects, and the Server Compute header shows it ("3 of 16 Server Objects"); past the limit no new instance starts. See [Large groups](./access-members-and-timers.md#large-groups) for the limits, and [Deploy it with Server Compute](./deploy-with-server-compute.md#overview) for how a library's row reads. ![The Server Compute row of loot_roller: its description, the line Server Function Library, 1 function and 1 instance, and the New badge](/img/unreal-sdk/server-compute-library.png) With a Min Instances of 1 and a Max Instances of 4 the same line says "1 to 4 instances". ## Checks when you save Saving a library lists these problems at once, with the usual checks of the visible settings (the Type Name, the function and server names, inputs and outputs). A library needs no State struct and no variable, so "Choose a State struct" does not apply to it. - A function's **Callable By** is **Members** or **Leader**: "RollLoot: Members and Leader need members, and a Server Function Library has none." - A function has a **Cooldown**: "RollLoot: a cooldown is saved data, and a Server Function Library saves none." Clear it. A library cannot remember when a player last called. - **Max Instances** is below **Min Instances**: "Max Instances must be at least Min Instances." - Another Server Object type lists the library under **Listen To**: "loot_roller is a Server Function Library and sends no messages." Remove it from **Listen To**; a type can still list it under **Can Call** and call it. The editor shows the first two as a warning under the function's row too. ## Switch Kind You can change **Kind** on a type that is already deployed, in either direction, and nothing is lost: - **To Server Function Library.** The type's variables, members, timers, messages and the rest stay in the asset, hidden and unused. The server keeps the saved data of its Server Objects, unused, and it comes back if the type becomes a Server Object again. Server Objects of the type that were running stop answering. The deploy asks first and names the change: "loot_roller changes from Server Object to Server Function Library: its saved data is kept, unused, and comes back if it becomes a Server Object again." - **To Server Object.** The hidden settings apply again, and the server starts each Server Object from the saved data it had, if any: "loot_roller changes from Server Function Library to Server Object: it starts from the saved data it had as a Server Object, if any." Games that used the type change with it. A library has only **Call** nodes, and they take no **Target** and no Instance Id; **Get**, **On Changed** and **On Received** nodes of the type no longer apply, so remove them. See [Call a Server Function Library](./from-blueprint/call-a-library.md). The Server Compute row reads **Changed** with "kind" in its list, and **Changed** with "scaling" when only Min Instances, Max Instances or Parallel Calls per Instance differ from the live version. After you change **Kind**, **Scaling** or a function, [generate the server code](./write-server-logic.md#after-you-change-the-definition) and deploy again. ## Write and call it - **Its server code** is a `logic.rs` like a Server Object's, without a state: [A Server Function Library](./write-server-logic.md#a-server-function-library) shows the function, a library calling a Server Object, a Server Object calling a library, and what a library cannot do. - **A game calls it** with the **Call RollLoot** node, or `UCrowdyServerObjectLibrary::CallLibraryFunction` in C++: [Call a Server Function Library, from Blueprint](./from-blueprint/call-a-library.md) and [from C++](./from-cpp/call-a-library.md). - **Server code calls it** with a typed helper that takes no key, once the library is in the caller's **Can Call** list: [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). ## Related - [Create a Server Object type](./create-a-type.md): the asset editor, **Callable By** and every setting - [Loot for the Hollow Giant](./examples/hollow-giant-loot.md): the loot roller, a chest and the boss, step by step - [Access, members and timers](./access-members-and-timers.md#call-other-server-objects): Can Call and Can Call Any Type - [Write its server logic](./write-server-logic.md#a-server-function-library): the server code of a library - [Deploy it with Server Compute](./deploy-with-server-compute.md): a library's row and the deploy that changes a Kind - [Troubleshooting](./troubleshooting.md#a-call-to-a-library-from-server-code-fails): a cold first call - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md): Kind, Scaling and Can Call Any Type on the platform # Troubleshooting Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/troubleshooting # Troubleshooting :::note Server Objects run on [ck-exec](/exec/intro). ::: This page is for Blueprint and C++ alike. In Blueprint, **Get Status** and **Get Failure Reason** are the nodes for what `GetStatus()` and `GetFailureReason()` answer in C++, and **On Failed** carries the **Outcome**, **Reason**, **Retryable** and **Code** that `OnDone` receives. On a **Call** node those pins are in the advanced area: click the small arrow at the bottom of the node to show them. **Code** is filled only by a [Game Kit starter](./use-game-kit-starters.md#refusals-and-their-codes). ## A Server Object does not become Ready Read its status first; the four statuses are on [Get a Server Object](./from-cpp/get-a-server-object.md#status). Then the reason it gives. ### Waiting is not failing Most problems on the server clear up by themselves, so reading and watching a Server Object recover from every temporary one without your help. Before its first values arrive, a Server Object waits them out in **Connecting** instead of failing. It keeps trying, a little less often each time and, unless the server asks for longer, never more than 15 seconds apart, and becomes Ready on its own once the cause is gone. Every wait before a retry or a redial is stretched by a random amount, up to half again: a failed read's first retry waits 1 to 1.5 seconds, and a lost connection is redialled after 1 to 1.5 seconds, then 2 to 3, and so on up to 30 to 45. So many players do not retry in step after a server restart. While it waits, `GetFailureReason()` says what it is waiting for. It waits when: - the type is not deployed on the server yet, or the app now runs an older version without it: the reason reads "`village_beacon` is not deployed on the server"; - the type is switched off on the server, or the whole app is switched off or paused (by its owner, or because it ran out of funds): the reason reads "The server is switched off or paused, so this Server Object waits: " followed by the platform's own words, such as "`village_beacon` is switched off in app 456" or "app 456 is switched off". It reads the same whether the server says so with its pause and switch-off statuses (`AppPaused`, `AppDisabled`, `TypeDisabled`) or, on an older server, with a refusal in words, and a read made while the connection is redialling into a paused or switched-off app shows this reason too. To show the player a message instead of retrying, see [When the app is paused](../services/authentication.md#when-the-app-is-paused); - the server does not let players use the type; - the server code crashed and its Server Object is restarting; - the server did not answer in time, or failed internally; - the server code refused the read, such as `unreadable` when the state holds a value players cannot read (see [refusals from your server code](#refusals-from-your-server-code)); - the server is busy or cannot be reached; when the platform could not start the Server Object to read it, for example because the app is at its instance or memory limit, the reason reads "The server could not be reached: " followed by the platform's words; - the connection to the server could not be opened: the reason reads "Could not open the connection to the server for ``: " followed by why, and names the type the SDK tried it with. When the refusal is that the server is switched off or paused, its error ending in `(APP_PAUSED)`, `(APP_DISABLED)` or `(TYPE_DISABLED)`, or saying so in words on an older server, it reads "The server is switched off or paused for ``: " followed by the error instead. A try made with the root or a Server Function Library names no type, since the platform is not told one: "Could not open the connection to the server: " or "The server is switched off or paused: ". The next try uses another type's Server Object, so one type that is not deployed or is switched off does not hold up the others. So a game can acquire its Server Objects before their server code is deployed, and they come up as soon as it is. ### What makes it fail A Server Object moves to **Failed**, and stays there, only when waiting cannot help: - this player may not read it: an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) Server Object that belongs to someone else, or a [Game Kit starter](./use-game-kit-starters.md) that refused the read with a code, such as "The server code for session refused to let this player read it (forbidden): ..." (a starter's `cooldown` or `unavailable` is waited out instead); - the deployed server code does not serve the variables players can see, or was built for another version of the SDK: generate and deploy it again; - the values the server sends do not match the definition's State struct: the definition and the deployed code are out of step, so generate and deploy again; - the server refused the SDK's request as malformed; - the Game API named a Server Object gateway outside its own domains, so the SDK will not connect to it: the reason reads "Could not open the connection to the server: the Game API named a gateway outside its own domains (" followed by why and "); if you host the server yourself, add its domain to Allowed Exec Gateway Domains in Project Settings". It is not retried until you acquire an object again (or call Restart). See [Server Object gateway domains](../reference/project-settings.md#server-object-gateway-domains) for the setting; - the player signed out (`The player signed out`); - the player's access to the game ended (`Your access ended: ` followed by the reason); see [When access ends](#when-access-ends). To try again after a failure, call `Acquire` again with the same definition and Instance Id, from the same owner or another; in Blueprint, call **Rejoin** on the component. The same object starts over from Connecting, and its values start from the definition's defaults. ### When access ends A player's access to the game can end while they play: it is suspended, revoked or expired, the platform signs the player out, or the account is blocked. The Game Session fires **On Access Ended** (see [When the player's access ends](../services/authentication.md#when-the-players-access-ends)) and Server Objects stop: - Every Server Object fails with `Your access ended: ` followed by the reason, whatever its status was. - A call in flight, and every call made afterwards, finishes as the outcome **Access Ended** (`ECrowdyServerCallOutcome::AccessEnded`), with the same reason. It is not retryable. - Nothing reconnects or retries by itself, since the server would refuse it. Show the reason and send the player back to sign in. A successful sign-in clears the ended state: a **Crowdy Server Object** component and a typed node set to **Find By Asset** get their Server Object again on their own. Code that called `Acquire` itself calls it again. ## A value is not what you expect - **A newer server sends an enum value this build does not have.** The field keeps its default and the SDK logs a warning once, under `LogCrowdyExec`. Update the game, or keep the value's old name under **Enum Value Names** if it was a rename. - **The connection dropped.** Changes made while it was gone are not replayed. The SDK reads the values again when it reconnects and reports the fields that differ as one change. See [Get a Server Object](./from-cpp/get-a-server-object.md#sign-out-shutdown-and-dropped-connections). - **The values stopped changing, or never arrived.** Check `GetStatus()` and `GetFailureReason()`. A Server Object waiting in Connecting (its type not deployed yet, or switched off) or one that has Failed keeps the last values it had, and the reason says what it is waiting for or why it stopped. See [Status](./from-cpp/get-a-server-object.md#status). ## A message never arrives A [message](./create-a-type.md#server-messages) is heard once, when the server sends it, and nothing keeps it. When a game or a Server Object expects one and gets nothing, check these in order. **In a game, from Blueprint or C++:** - **Players Can Listen is off.** Only the server code of a type that Listens To this one hears the message. A Blueprint **Received** node for it does not compile ("heard only by other Server Object types; turn on Players Can Listen"), and **Watch Message** or `WatchMessage` logs "... is heard only by other Server Object types; turn on Players Can Listen to hear it in a game" once and does nothing. Tick it in the asset, then generate and deploy the type. Likewise a name the definition does not have logs "... has no message named ..., so nothing can listen to it". - **The deployed server does not send it yet.** A message you added, renamed or changed reaches the server only with the generated code and a deploy; see [After you change the definition](./write-server-logic.md#after-you-change-the-definition). - **The function that sends it did not succeed.** A message is published only when the function that sent it succeeds, after its changes. A refusal sends nothing; look at the caller's `Reason` and the server code's [logs](./deploy-with-server-compute.md). - **The game was not listening at that moment.** A message sent before the game started listening, or while its connection was closed, is lost and never replayed. Check `GetStatus()`: a Server Object that is not Ready announces nothing, and a message that arrives while it is still reading its values is held and announced once they are current, at most 32 of them. Past 32 the oldest is dropped and the log warns "held back 32 messages while reading its values, so the oldest was dropped". - **The platform refused to let the player listen.** The log warns once, "`` could not listen to its `` message, so it is asked again after a wait: " followed by the platform's reason. The SDK asks again after a wait that grows from 1 second to 30, and nothing else about the Server Object fails. A message sent while the listen is refused is lost. - **A limit was reached.** A connection allows 256 subscriptions, and each time the game listens it counts against the 120 calls a connection may make every 10 seconds. Each message a game listens to on each Server Object is one subscription, so many objects with several messages each can reach it: give back the objects the game no longer needs, and listen only to the messages it uses. - **The message comes before the change it follows.** A type that keeps [a record per entity](./keep-a-record-per-entity.md) sends its records in batches, so a message sent right after a record changed can arrive first. Put what the listener needs into the payload. - **The payload does not fit its struct.** The message is dropped with a warning that names it. The definition and the deployed code are out of step: generate and deploy again. **In server code, listening to another type:** - **The listen was never applied.** A `listen::...` call takes effect only when the function that made it succeeds, and the function fails, with `could not listen to boss.boss_defeated:` and the reason, when the platform refuses it. A type can listen to 256 messages at once. Check the function's result. - **The key is not the sender's.** The key is the sender's Instance Id, with capitals significant: `boss-1` and `Boss-1` are two Server Objects. - **The sender is not in Listen To.** Without it the `listen` functions and the `on__` handler do not exist; see [Listen to a message](./write-server-logic.md#listen-to-a-message). Generate and deploy after adding it. - **The message was sent too early.** One sent before the listen was applied, or while the listener was stopped, is missed. - **The handler failed.** A handler that returns an error puts its state back, and a payload it cannot read fails it with `bad_message: .: ...`; read the logs. ## Outcomes `Outcome` is an `ECrowdyServerCallOutcome`. The platform's own status is in brackets. | Outcome | What happened | What to do | |---|---|---| | Success [`Ok`] | The server code ran and answered. `Reply` holds the outputs. | Nothing. The watched values change for everyone, you included. | | ServerError [`AppError`, `Internal`] | The server code refused the call or failed, or the platform failed handling it. `Reason` carries the message. Also returned when the reply does not fit the function's outputs. | Show or log `Reason`. See [refusals from your server code](#refusals-from-your-server-code). | | Busy [`Busy`, `RateLimited`] | The server stayed busy through every retry. | Try again later, and call less often. If one Server Object takes many calls at once, raise **Calls Waiting** in the type's [Server Limits](./create-a-type.md#every-setting). | | Unavailable [`Unavailable`, `Moved`] | The server could not be reached, so the call may or may not have run; or the Server Object has Failed (`Reason` is its failure reason). When the platform says why, such as the app being at its instance limit, `Reason` adds its words after "The server could not be reached:". Not retried by the SDK. A Game Kit starter's `unavailable` refusal (another hub the call needed did not answer) arrives as Unavailable too, with **Code** `unavailable` and `bRetryable` set. | Check `GetStatus()`. Call again only if running the function twice is harmless, or after the watched values show it did not happen. | | NotDeployed [`NotFound`] | This type is not deployed on the server. The Server Object itself waits in Connecting until it is. | Deploy the type's server code to the app. | | Timeout [`DeadlineExceeded`] | Nobody answered in time, and `Reason` says which side gave up. "AttackBoss ran past its Time per Call on the server: handler exceeded its deadline" means the server's own limit stopped the call (the type's **Time per Call**, 2 seconds unless changed), followed by the server's message; for a read it reads "The server took longer than this type's Time per Call to read this Server Object" (or "watch"). "The server sent no answer in time; the connection may be slow or lost" means no answer reached the game within the project's **Server Object Call Timeout** (20 seconds unless changed; see [Project settings](../reference/project-settings.md#server-object-timeouts)): the longest **Time per Call** (10 seconds) plus time spent waiting behind other calls on the server. "No answer within 5 seconds; FeedBeacon may still have run on the server" means the call's own **Timeout** ran out first (5 seconds here); it is not retryable, and an answer that arrives later is not reported. The call may or may not have taken effect. A call past **Time per Call** also restarts that Server Object from its last save, so changes since the last save are lost. | Watch the values before you call again. If the function does real work that needs longer, raise **Time per Call** in the type's [Server Limits](./create-a-type.md#every-setting); otherwise keep functions short, and set **Save Every** low on a type whose recent changes matter. **Time per Call** counts the function only, not the time a call waits behind others. A call that gives up earlier than the project's timeout has a **Timeout** set on its Call node (`TimeoutSeconds` in C++); clear it, or raise it, if the server needs longer. | | Denied [`Denied`] | The server refused the call. Either the platform refused it, because players may not call this type at all, and `Reason` reads "The server refused the call to Feed: " followed by the platform's words; or the type's own server code refused the player by a rule in the asset, such as **Callable By**, **Owner Only** or a **Cooldown**, and `Reason` says why. A Game Kit starter's `forbidden` and `cooldown` refusals arrive as Denied too, with **Code** set and the starter's words in `Reason`; `cooldown` is `bRetryable`. A call refused because the server is switched off or paused is [Paused](#outcomes), not Denied. | Show `Reason`. A refusal by a rule stays a refusal until something changes, except a cooldown, which is `bRetryable`: call again after the seconds it names. See [refusals from your server code](#refusals-from-your-server-code). | | Paused [`AppPaused`, `AppDisabled`, `TypeDisabled`; `Denied` in words from an older server] | The game's server, or this type, is switched off or paused, by its owner or because the app ran out of funds. The platform says which with its own status: `AppPaused` for an app paused by billing, `AppDisabled` for the whole app switched off, `TypeDisabled` for one type switched off. A server from before those statuses refuses with `Denied` and says so in words, and the SDK reads that as Paused too. It is retryable, and `Reason` reads "The server is switched off or paused, so it refused the call to Feed: " followed by the platform's words, such as "`village_beacon` is switched off in app 456", "app 456 is switched off" or "app 456 is paused: ...". A call that got no answer because the connection was redialling into a paused or switched-off app reports Paused with the platform's words too, not Timeout. | Try again later, or show the player a paused message; see [When the app is paused](../services/authentication.md#when-the-app-is-paused). Code that only checked for Denied should handle Paused too, and a Blueprint Switch on the outcome has a **Paused** pin. | | AccessEnded | The player's access to the game ended: suspended, revoked, expired, signed out by the platform, or the account blocked. `Reason` reads `Your access ended: ` followed by why. Not retryable, and it applies to every Server Object until the player signs in again. | Show `Reason` and return the player to sign in; see [When access ends](#when-access-ends). A Blueprint Switch on the outcome has an **Access Ended** pin. | | Refused [`AppError` with a Game Kit code] | A [Game Kit starter](./use-game-kit-starters.md#refusals-and-their-codes) refused the call for a reason of the game's, such as a full match (`full`) or another player's turn (`not_your_turn`). **Code** (`Code` in C++) holds the code and `Reason` the starter's words. Not retryable. Your own types never answer Refused. | Branch on **Code**, and show `Reason`. The codes are on the platform's [Refusals carry a code](/game-kit/protocol#refusals-carry-a-code). A Blueprint Switch on the outcome has a **Refused** pin. | | ServerCrashed [`Trapped`] | The server code crashed handling the call. The Server Object restarts on the server from its last save, so its most recent changes may be lost; the SDK reads its values again and reports what differs. | Read the server code's log. | | BadRequest [`BadRequest`] | The call could not be sent as asked: an unknown function name, params of the wrong struct, or params that could not be encoded. `Reason` says which. | Fix the call. | | Canceled | The Server Object was given back, or the player signed out, before the call finished. | Nothing, usually; the owner is gone or the session ended. | ## Refusals from your server code A refusal whose message starts with one of these words came from the [server code generated for the type](./write-server-logic.md#what-the-generated-code-does-for-you), not from your own logic: - `unknown_method`: the deployed server code has no such function. The definition is newer than what is deployed; generate and deploy the server code again. It reaches `OnDone` as a `ServerError`. - `denied`: a player called a function they may not: **Server Only**, **Members** or **Leader** without being one, or, on an Owner Only type, a Server Object that is not theirs. It reaches `OnDone` as `Denied`, and `Reason` is the text after the word, such as "only members may call Deposit". A Server Only function is one no player may call, so a call from a game client is refused this way every time; only other server code and developer tools can call it. Set it back to **Players** in the [definition asset](./create-a-type.md#callable-by) if players should be able to. Deploy the type again afterwards, since the server code carries the choice. - `cooldown`: the player called a function inside its **Cooldown**. It reaches `OnDone` as `Denied` with `bRetryable` set, and `Reason` says how many seconds to wait, such as "Feed can be called again in 4 s". - `bad_params`: the server could not read the params, or a number is outside its **Value Range** (`bad_params: Amount must be 1 to 100`), and the rest of the reason names the field. Usually the deployed code is older than the definition. It reaches `OnDone` as a `ServerError`. - `unreadable`: the Server Object's state holds a value no Unreal client could read, such as text in an `FGuid` field that is not a GUID, often one loaded from a save that older server code made. The rest of the reason names the field, such as `unreadable: Id: the server state holds a value players cannot read: a GUID is not in a form Unreal reads`. Reads are refused until your server code replaces the value, so players' Server Objects wait in Connecting with this as their reason and become Ready by themselves once it is fixed. See [the values every client can read](./write-server-logic.md#what-the-generated-code-does-for-you). - `bad_seed`: the starting values given to a new Server Object hold a value no Unreal client could read, and the rest of the reason names the field. The Server Object does not start. - `the change was not kept`: the call or hook would have put such a value into a watched field, or the reply held one, or a message it sent held one or was larger than 1 MiB, so the state went back to what it was. It reaches `OnDone` as a `ServerError`. - `bad_message`: a message this type listens to could not be read as its payload, as in `bad_message: boss.boss_defeated: ...`. The handler fails and the state goes back; the sender's definition and this type's deployed code are out of step, so generate and deploy both again. The [full list of what a refused player sees](./access-members-and-timers.md#what-a-refused-player-sees) is on the members page. Any other `ServerError` reason is the message your server code refused with, such as the beacon saying it is full. ## A call to a library from server code fails The first call to a [Server Function Library](./server-function-libraries.md) after it has been idle can take over a second. A player's game just waits for it. A Server Object's function that calls the library is held to the platform's call deadline, and a call that outlasts it fails with "call deadline passed" although the library may still finish. A boss that rolls loot when it falls sees it as `loot_roller.roll_loot failed: call deadline passed`. - **Decide what a failed call means.** If the answer is optional, as a drop is, take it only when the call came back: `if let Ok(loot) = calls::loot_roller::roll_loot(ctx, &roll) { ... }`, as in [Loot for the Hollow Giant](./examples/hollow-giant-loot.md#the-boss-rolls-on-its-fall). - **Call again once when the function is safe to repeat.** A pure calculation is. The second call finds the library warm: ```rust let loot = calls::loot_roller::roll_loot(ctx, &roll).or_else(|_| calls::loot_roller::roll_loot(ctx, &roll))?; ``` Do not repeat a call that changes a Server Object unless repeating it is harmless; see [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). - **The helper does not exist.** `calls::loot_roller` is made only when the caller lists the library under **Can Call**, or the caller uses `calls::any` with **Can Call Any Type**. Add it, generate the caller's server code and deploy again. - **`app N has no node type`.** A call by `calls::any` named a type the app does not run, or misspelled it. Deploy does not check names given to `calls::any`. - **`denied`.** A player called a function that is **Server Only** ("players may not call this function") or **Developers Only** ("only developer tools may call this function"). Other server code can call a Server Only one, and developer tools both; see [Callable By](./create-a-type.md#callable-by). - **Unavailable, with the platform's words.** The app is at its limit on running Server Objects, and every instance of the library counts toward it; see [Scaling](./server-function-libraries.md#scaling). A function that sees a value reset between two calls is working as designed: a library keeps nothing, and each instance runs several copies of your code with their own memory. Keep the value in a Server Object. ## Library calls time out during a burst A burst of calls that each keep the processor busy, such as many loot rolls at once, can come back as **Timeout** with "ran past its Time per Call on the server", even though one call alone is quick. The calls an instance runs at the same time share its processor, and **Time per Call** counts the time that passes, not the work done. The platform adds an instance only after calls have waited 2 seconds, so the first moments of a burst all land on the instances already running. In a test with one instance running four calls at once, 300 ms of work per call, and two players each sending 24 at once, a Time per Call of 2000 ms failed some calls and 5000 ms answered all of them. - **Give the function room.** Raise **Time per Call** to cover **Parallel Calls per Instance** times the work one call does. - **Start with more instances.** Raise **Min Instances** when bursts are expected, such as when a round ends and every player opens a chest. - **Run fewer at once per instance.** A lower **Parallel Calls per Instance** gives each call more of the processor; raise **Max Instances** to keep the total. ## A Server Function Library will not save Saving shows these for a library, and they are the whole list besides the usual checks of its Type Name, functions and server names: | Message | What to do | |---|---| | "RollLoot: Members and Leader need members, and a Server Function Library has none." | Set the function's **Callable By** to **Players**, **Server Only** or **Developers Only**. | | "RollLoot: a cooldown is saved data, and a Server Function Library saves none." | Set its **Cooldown** to 0. To limit how often a player may call, put the function on a Server Object. | | "Max Instances must be at least Min Instances." | Raise **Max Instances**, or lower **Min Instances**. They are under **Scaling**. | | "loot_roller is a Server Function Library and sends no messages." | A Server Object lists the library under **Listen To**. Move it to **Can Call**. | See [Checks when you save](./server-function-libraries.md#checks-when-you-save). ## A ping fails **Ping Server Objects** (`UCrowdyServerObjectSubsystem::Ping` in C++) fails with a `Reason` instead of a round trip when: - "No Server Object connection is open": a ping never opens a connection. Acquire a Server Object first and ping once it is Ready, or ping again after the connection comes back. - "Too many pings: the platform answers at most 60 every 10 seconds": the platform drops the rest of a connection's pings without a word, so the SDK refuses them before sending. Ping on a timer of a second or more, not every frame. See [Ping the platform](./from-blueprint/call-functions.md#ping-the-platform). ## Seeing what a Server Object is doing When a Server Object never becomes Ready, stops changing, or two players disagree about it, turn on `crowdy.exec.trace`. It is off by default, and the warnings and errors print whether it is on or not. Turn it on in the editor's console (press the backtick key, in Play In Editor too): ```text crowdy.exec.trace 1 ``` or, for a packaged game or an editor started from the command line, add this to the command line: ```text -ini:Engine:[ConsoleVariables]:crowdy.exec.trace=1 ``` The lines go to the Output Log under `LogCrowdyExec`, at Log verbosity, each starting with `exec:`, then the type and Instance Id (`boss/boss-1`). An [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) Server Object prints `owner` in place of the Instance Id, so the player's user id never reaches the log. The Players Online root has no Instance Id, so it prints `-` in its place, such as `exec: root/- read applied ...`. For a shared boss being hit by two players, one player's log reads: ```text exec: boss/boss-1 status Connecting -> Ready exec: boss/boss-1 subscribe ok exec: boss/boss-1 read applied epoch=3 seq=41 changed=Health,MaxHealth,Phase,Respawns,Hits,LastHitBy exec: boss/boss-1 call Hit sent exec: boss/boss-1 push applied epoch=3 seq=42 changed=Health,Hits,LastHitBy exec: boss/boss-1 call Hit answered Success ms=63 exec: boss/boss-1 push applied epoch=3 seq=43 changed=Health,Phase,Hits,LastHitBy exec: boss/boss-1 push stale epoch=3 seq=43 exec: boss/boss-1 call Hit sent exec: boss/boss-1 call Hit answered Denied ms=41 exec: connection closed exec: redial ok exec: boss/boss-1 reread reason=reconnect exec: boss/boss-1 read applied epoch=3 seq=47 changed=Health,Hits,LastHitBy ``` What it prints: - **Status changes**, such as `status Connecting -> Ready`. - **Every read and push that was applied**, with its `epoch`, its `seq` and the names of the variables that changed (`changed=-` when none did). The `epoch` changes when the server restarts the Server Object, and `seq` orders its changes. The `seq` printed is the one the SDK applied, so if two players' logs show the same `epoch` and `seq` lines, they saw the same changes. The names come in the order Watch Variables reports them. A push that is older than what the object already has prints `push stale`, with the object's current `epoch` and `seq`, and is not applied. - **Why it read again**, as `reread reason=` followed by `gap` (a change was missed), `overflow` (so many changes arrived while a read was in flight that they could not all be held), `epoch` (the server restarted the object), `reconnect` (the connection came back) or `ownerOnly` (an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) or Members type, which tells watchers to read instead of sending the values). - **Subscribes**, as `subscribe ok` or `subscribe failed` followed by the reason. - **Calls**: `call Hit sent`, and `call Hit answered` with the [outcome](#outcomes) and how many milliseconds the answer took. A call the SDK retries for you because the server was busy prints `sent` once. - **Connection**: `exec: dial failed` with the reason when the first connection could not be made, `exec: connection closed` when an open one drops, then `exec: redial ok` or `exec: redial failed` with the reason. Every `redial ok` is preceded by a `connection closed`. A close on purpose names why, and no redial follows it: `exec: connection closed (no Server Object in use)`, `(signed out)` or `(shutting down)`. The next Server Object you get opens a fresh connection. It prints the names of the variables that changed, never their values, and never a token. Turn it off again with `crowdy.exec.trace 0` once you have your answer, since a busy fight writes a line for every hit. ## Related - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md) and [from C++](./from-cpp/get-a-server-object.md): the statuses, and what sign-out and a dropped connection do - [Read and follow its variables, from Blueprint](./from-blueprint/read-and-follow-variables.md) and [from C++](./from-cpp/read-and-follow-variables.md) - [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md): when a message is heard, held or lost - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md): what the SDK retries - [Make a Server Function Library](./server-function-libraries.md), and calling one [from Blueprint](./from-blueprint/call-a-library.md) or [from C++](./from-cpp/call-a-library.md) - [Access, members and timers](./access-members-and-timers.md#what-a-refused-player-sees): every refusal a rule in the asset can cause - [Deploy it with Server Compute](./deploy-with-server-compute.md): the Logs tab and the Activity tab - [ck-exec operations](/exec/operations): logs, including a crashed call's # Unreal and ck-exec names Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/unreal-and-ck-exec-names # Unreal and ck-exec names :::note Server Objects run on [ck-exec](/exec/intro). ::: The SDK names things the way an Unreal project would. The platform's [ck-exec pages](/exec/intro), its operations tools and its logs use their own words. This table maps one to the other. | Unreal SDK | ck-exec | |---|---| | Server Object type: one definition asset, `UCrowdyServerObjectDefinition` | hub node type | | Type Name | node type name | | Server Object: `UCrowdyServerObject`, from `UCrowdyServerObjectSubsystem` | a hub instance | | Instance Id | instance key | | Variables: the State Struct, or the variables added in the asset | the hub's state, kept in its snapshots | | Server Function | method | | Server Name of a function | method name | | Calling a Server Function: `UCrowdyServerObject::Call` | a call | | Call outcome: `ECrowdyServerCallOutcome` | reply status (mapped on [Troubleshooting](./troubleshooting.md#outcomes)) | | Variables Visible to Players, and following them: `WatchValues` | the hub's `state` topic, subscribed to, and its `read` method | | Server message, sent by `messages::send_...` | a topic the hub publishes on (any topic but `state`) | | Server Name of a message | topic | | Listening to a message: `WatchMessage` in a game, `listen::::` in server code | subscribing to a hub instance's topic | | Listen To | the types in the manifest's `calls`, which may subscribe | | Kind: Server Function Library | a spoke node type, `"kind": "spoke"` in the manifest | | Min Instances | `replicas` | | Max Instances | `max_replicas` | | Parallel Calls per Instance | `concurrency` | | Calling a library: `UCrowdyServerObjectLibrary::CallLibraryFunction`, the **Call** node with no Target | a call to the spoke with the empty key; the platform picks an instance | | Can Call Any Type | `"calls": ["*"]`; in server code, `calls::any` over `ctx.call` | | Save Interval Seconds | `persist_every_ms` | | Idle Timeout Seconds | `evict_after_ms` | | The type's server code, generated into `Server//` by **Generate Server Code** | the type's crate, built into the module its manifest entry names | | Your code in `logic.rs`, implementing `Functions` | the hub's handlers: the generated `lib.rs` implements `Hub` and calls yours. For a library, it implements `Spoke` | | Deploy on the Server Compute page, and `-op=deploy` | a build (`execBuild`) then a deploy (`execDeploy`) of a manifest that lists every type under a `root` hub type | | Version: one deploy, listed on the Versions tab of the Server Compute page, with **Make active** to go back | a version (`execVersions`); activating an earlier one is `execActivateVersion`, a rollback | | Switch off and Switch on for a type | the kill switch, `execSetEnabled` with a node type; calls are refused with `Paused` meanwhile | | A call and its log lines: click a line's flow to follow it | a flow, the 32 hex digits every line of one call carries (`execLogs`) | | The Activity tab of the Server Compute page | `execEndpointStats`, calls per endpoint | Mods and CLIENT halves have no Unreal SDK counterpart yet. A spoke is a [Server Function Library](./server-function-libraries.md). Two platform names show up where you might not expect them. `read` is the method the SDK calls to fetch the watched values, which is why no Server Function may use that name. And `state` is the topic every Server Object's server code publishes its variable changes on; a message cannot use it. ## Related - [What a Server Object is](./overview.md) - [Make a Server Function Library](./server-function-libraries.md) - [ck-exec overview](/exec/intro) # Move from Game Models Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/move-from-game-models # Move from Game Models :::note Server Objects run on [ck-exec](/exec/intro). v2.18.0 added them and deprecated the Game Model API; [v2.19.0 removes it](../guides/whats-changed.md#2026-10-12-sdk-v2190). ::: The SDK's Game Model API is removed in v2.19.0, the release after the one that adds Server Objects. It was built on a game API engine that ck-exec replaced, so it had no server to talk to. This page says what went, what to do in your project before you upgrade, what takes the place of each part, and how to move a system across. The replacement is [Server Objects](./overview.md), from Blueprint or C++. ## What was removed - **The Game Model API.** Containers and attributes (the `CrowdyModel` and `CrowdyContainer` metadata), model functions, Crowdy Effects (the assets, the effect graph editor and the Apply Effect nodes), sessions on the Game Model subsystem, automations, signals, pre-seeding, the Game Kit, container manifests, and `UCrowdyGameModelSubsystem`. **Get Local User Id** goes with it: use **Get User ID** on the Crowdy Game Session. - **Its settings and logging.** The `crowdy.gamemodel.*` console variables, the `LogCrowdyGameModel` log category and the `DefaultModelNotificationCarrier` setting. - **Server Owned in the Replication dropdown.** A Blueprint variable's **Crowdy Replication** dropdown now offers only **None** and **Replicated**. - **CrowdyMass's Game Model values.** `GetModelValue`, `GetModelTargetState`, `GetEffectTarget` and the model fields are gone. Crowd rows read Server Object values instead, from a record per entity; see [Crowd rows](./keep-a-record-per-entity.md#crowd-rows). - **Crowdy Studio's Game Model page.** The **Features** and **Tier-feature grants** cards that were on it are on the new **Access** page, under **Project** in the Configure group. ## Upgrade steps Do these in order. 1. **Compile and open the project on the release that adds Server Objects first.** That release still has the Game Model API, and every use of it is listed: each C++ use gets a compiler warning (C4996 on MSVC), and each Blueprint that uses Game Models gets one compiler warning listing its uses when it compiles, loads or cooks. Write the list down, and [move each system](#move-a-system) off Game Models while the nodes are still there to read. 2. **Search your C++ for `CrowdyModel`, and for `CrowdyContainer`.** A property or Blueprint variable that still carries either marker compiles and does nothing, and nothing warns you. Remove the marker, or the property if it was only a Game Model attribute.3. **Know what the upgrade drops without a message.** A Blueprint that still has **Apply Effect** nodes loses those nodes when it loads, with no error, so step 1 is the only place they are listed. Crowdy Effect, container manifest and Game Kit assets no longer load; note what they described and delete them. An old saved `DefaultModelNotificationCarrier` is ignored and can be deleted from your config. 4. **Fix the C++ that no longer compiles** (see [the removed C++ API](#the-removed-c-api)). 5. **Do a clean cook.** Do not cook iteratively over the upgrade: the baked registry's layout changed, so rebuild it and cook from scratch. See [Packaging](../guides/packaging.md). 6. **Check your channel code.** The SDK no longer drops channel payloads that begin `cmc:`, `csg:` or `gms|`, so a payload that starts that way now reaches your handler. See [Channels](../runtime/channels.md). ## The removed C++ API - `ICrowdyEntitySubscriber::ApplyModelChanges` is gone. A class that implements the interface and marks it `override` stops compiling; delete the function. - `UCrowdyEntitySubsystem::RegisterSubParticipant`, `RegisterSubParticipantAs` and `DeriveSubParticipantID` are gone. - `FCrowdyModelIdentity` is gone. `FCrowdyEntityIdentity` has `StableNetIDFromActorPath` and `NetIDFromBindingKey` for the same jobs. - `FCrowdyEntityRecord::AnchorNetID` is gone. - `ICrowdyBindingKeyProvider` stays. Its header now lives under `Replication/Components/`, so fix an include that names the old path. The CrowdyState layout's message for a container property now reads "use a CrowdyEvent RPC or a Server Object" where it used to name a Game Model container; see [Crowdy State](../runtime/crowdy-state.md). ## What takes the place of each part | Game Models | Server Objects | |---|---| | A container type | A Server Object definition asset: [Create a Server Object type](./create-a-type.md) | | Container attributes (`CrowdyModel`) | The state struct's variables. What players may read is the values that are **Visible to Players**, the ones they watch: [Create a Server Object type](./create-a-type.md#the-details-tab) | | Model functions and Effects | Server Functions, written in the type's `logic.rs`: [Write its server logic](./write-server-logic.md) | | Invoke policies | **Callable By** and **Readable By** on the asset, and checks in the logic: [Access, members and timers](./access-members-and-timers.md#access) | | A match session | One Server Object per match, with the match id as its Instance Id: [Relate Server Objects to each other](./relate-server-objects.md) | | Change pings, **Listen for Model Changes**, OnRep on model attributes | Following the watched variables, from [Blueprint](./from-blueprint/read-and-follow-variables.md) or [C++](./from-cpp/read-and-follow-variables.md) | | Automations: interval schedules | The type's timers: [Timers and events](./access-members-and-timers.md#timers-and-events) | | Automations: cron schedules | No setting on the asset for now; the platform's [Cron schedules](/exec/timers-and-presence#cron-schedules) | | `player_joined`, `player_left`, `player_count_changed` triggers | **On Player Joined** and **On Player Left** on the type, and presence on the server: [Presence](/exec/timers-and-presence#presence) | | Authority rules (tier features, group and grid permissions) | A tier feature: [Required Feature](./access-members-and-timers.md#required-feature) on the function. Group and grid permissions: checks in the logic against the player and the world, [World and platform data](/exec/world-and-platform-data) | In Oakford, had the beacon that a villager feeds with oil been a Game Model container, it would have had an `Oil` attribute and a `FeedBeacon` model function, guarded by an invoke policy. As a Server Object it is the `AVillageBeacon` type from [What a Server Object is](./overview.md): `Oil` is a variable players watch, and `FeedBeacon` is a Server Function that adds the oil or refuses. ## Move a system Do this for each system on your list, while the project is still on the release that has both Game Models and Server Objects. 1. **Author each system as a Server Object type**: make the [definition asset](./create-a-type.md), set [who may read and call it](./access-members-and-timers.md), [write its logic](./write-server-logic.md) and [deploy it with Server Compute](./deploy-with-server-compute.md). 2. **Switch the call sites.** Read and follow values and call functions [from Blueprint](./from-blueprint/get-a-server-object.md) or [from C++](./from-cpp/get-a-server-object.md), and remove the Game Model nodes and calls as you go. 3. **Seed any data that must persist.** Nothing moves automatically. A one-off script writes the starting values into the new type, for example by calling a [Server Only](./create-a-type.md#callable-by) Server Function, which developer tools can call and players cannot. Server Objects need ck-exec on your app's environment, so a project can finish moving once its environment has it. ## Why Server Objects are not marked on the class Game Models were declared by markers on your own classes. Server Objects are declared in one asset, on purpose: - **The truth is the server code.** A property on an actor that looks writable is only a request to the server. Keeping the state in the server code makes that plain. - **A Server Object is not an actor.** It is picked by an Instance Id, exists before any actor does and outlives them, and many actors can share one. - **One versioned contract.** The asset is the single place the type is described, so compiling a Blueprint can never silently change what the server expects. - **Marker metadata is stripped when a game is cooked,** so a property marker is not something a packaged game can read back. - **A call answers or refuses plainly,** with an outcome and a reason, which an OnRep on an attribute cannot. ## Related - [What a Server Object is](./overview.md) - [From the legacy engines](/exec/from-the-legacy-engines): the platform's full mapping from game models and automations to ck-exec - [Port a compute module](/exec/port-a-compute-module): a worked port to a hub - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md): the SDK's words next to the platform's - [Troubleshooting](./troubleshooting.md): outcomes and refusals # Get a Server Object Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-blueprint/get-a-server-object # Get a Server Object :::note Server Objects run on [ck-exec](/exec/intro). ::: Everything on the [C++ pages](../from-cpp/get-a-server-object.md) can be done from Blueprint, and most of it needs no variable, no cast and no setup of your own. Open any Blueprint graph, type the name of a variable or function from a [definition asset](../create-a-type.md), and pick the node that appears. The node reads the variable, tells you when it changes, or calls the function, with a real pin for every value. A Blueprint and a C++ class can use the same Server Object at the same time. To build one from nothing, following each click, see [Tip jar: your first Server Object](../examples/tip-jar.md). ## The typed nodes Type a variable's name in the graph's palette or in the menu that opens when you drag off a pin, and the asset offers its nodes under **Server Objects**, then the asset's name. For the village beacon's `CSO_VillageBeacon` the category is **CSO Village Beacon**: ![The Palette filtered to CSO_VillageBeacon, offering Call FeedBeacon, Get bLit, Get Oil, Get Server State, On bLit Changed and On Oil Changed](/img/unreal-sdk/server-object-palette.png) | Node | What it does | |---|---| | **Get Oil (CSO_VillageBeacon)** | Reads the variable `Oil`. A pure node: it has no execution pins. | | **On Oil Changed (CSO_VillageBeacon)** | Runs when `Oil` changes. | | **Call FeedBeacon (CSO_VillageBeacon)** | Calls the function `FeedBeacon`, with a pin for each input and each output. | | **Get Server State (CSO_VillageBeacon)** | Every variable of the object at once, one output pin each. | [Blueprint snippet: so-typed-nodes, see /bp/unreal-sdk/so-typed-nodes.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. The figure shows the four nodes for `Oil`; `bLit` has a **Get** and an **On bLit Changed** of its own, as the palette shows. There is one **Get** and one **On Changed** for each variable and one **Call** for each function of the asset, and one **On Received** for each message players can listen to; see [Hear its messages](./hear-server-messages.md). Only variables ticked **Visible to Players** are offered, since a **Server Only** variable never reaches a player. There is also a generic **Call Server Function** under **Server Objects**, with a **Function** dropdown that lists every function of every asset, for when you would rather pick than type. A [Server Function Library](../server-function-libraries.md) is different: it has no values and no instance, so its only nodes are **Call** nodes, under **Server Function Libraries** in the palette, and they need no **Target**; see [Call a Server Function Library](./call-a-library.md). There is no bookkeeping to do. A Server Object is one shared object that the SDK keeps current. **Get** reads that object's current value directly, never a copy you have to refresh. **On Changed** follows it, so you do not keep a variable holding the last value, compare it, or unbind anything. Rename or retype a variable in the asset and the nodes follow; see [When the asset changes](./pins-for-every-type.md#when-the-asset-changes). ## Where the object comes from Every typed node needs a Server Object. There are three ways to give it one. - **Target.** The default. The **Target** pin takes the Server Object, a **Crowdy Server Object** component, or an actor that has a component for that asset. Left empty it is **Self**, so a node in a Blueprint that has the component just works. Anything else cannot be connected to it, and the editor says why. - **Find By Asset.** For a widget or other Blueprint with no reference to pass in. Right-click the node and choose **Find By Asset**; the **Target** pin is replaced by the choice of **Instance**, which you can also change in the node's Details. - **Instance Id**: a pin takes the id, such as `oakford`. - **Signed-In Player**: the signed-in player's own object, for an [Owner Only](../from-cpp/get-a-server-object.md#owner-only-types) type. - **Player's Team**: the signed-in player's [Crowdy Team](../access-members-and-timers.md#crowdy-team). A **Team Id** pin picks the team when the player is in several; 0 uses the first. In C++ the choice of **Instance** is the enum `ECrowdyServerObjectFind`, with the values `InstanceId`, `SignedInPlayer` and `PlayersTeam`. The object is acquired the first time a node of this Blueprint uses it, and held while the Blueprint lives. Right-click the node and choose **Use Target** to go back. - **Only One Instance.** A type with [Only One Instance](../access-members-and-timers.md#only-one-instance) has just the one object, so its nodes need neither a Target nor an Instance. Place the node and use it. ## Add the component The typed nodes take **Self** as the default **Target**, so the simplest way to have a Server Object is an actor with a component. Open the Blueprint actor, click **Add** in the Components panel, and choose **Crowdy Server Object**. In its Details panel, under **Server Object**: - **Definition**: the definition asset, such as the `CSO_VillageBeacon` from [Create a Server Object type](../create-a-type.md). - **Instance Mode**: which Server Object of that type this actor uses. A type with Only One Instance ignores it. - **Instance Id**: the id typed in **Instance Id**. Every actor with the same id shares one Server Object. The beacon uses the village's name, `oakford`. - **Signed-In Player**: the signed-in player's user id. Use it for an [Owner Only](../from-cpp/get-a-server-object.md#owner-only-types) type, so each player gets their own. The component waits for the player to sign in, and joins again after a new sign-in or a new account. - **This Actor**: the actor's placement in the level, which is the same on every player's machine. Only for an actor placed in the level with the component already on it, not one spawned at run time or given the component at run time. - **Player's Team**: the signed-in player's [Crowdy Team](../access-members-and-timers.md#crowdy-team), for a type whose **Members From** is **Crowdy Team**. **Team Id** picks the team; 0 uses the player's only team. The component gets the player's teams if the game has not, and joins again when **On My Teams Changed** runs. A player in several teams fails with "This player is in 2 teams; set Team Id": set **Team Id**, then call **Rejoin**. - **This Actor's Area**: the Server Object of the area the actor stands in. The ground is cut into squares of the definition's **Area Size** chunks on each side, and the Instance Id is the square's `x,y`. The component checks every half second and, when the actor crosses into another area, joins that area's object and lets go of the old one. For a type that keeps [a record per entity](../keep-a-record-per-entity.md#pick-the-object), the actor also needs a **Crowdy Entity** component, since its id names the actor's record. Needs a game session, which gives the chunk size. In C++ the mode is `ECrowdyServerObjectInstanceMode::ThisActorsArea`. - **From Server Value**: the Instance Id is a variable of another Server Object, followed as it changes. Set **Source Definition**, **Source Instance** (the enum `ECrowdyServerObjectSourceInstance`: **Instance Id**, with **Source Instance Id**, or **Signed-In Player**) and **Source Variable**, a String or an integer. While the variable is empty the component waits ("Waiting for TeamId"), and when it changes the component joins the new one. See [Guild Halls](../examples/guild-halls-and-arenas.md#guild-halls). ![The Crowdy Server Object component: Definition CSO_VillageBeacon, Instance Mode Instance Id, Instance Id oakford](/img/unreal-sdk/server-object-component.png) The component joins at Begin Play and lets go at End Play. The Server Object is given back a few seconds after its last holder lets go, so a beacon that is destroyed and placed again straight away keeps its values. See [Get a Server Object](../from-cpp/get-a-server-object.md#one-object-many-owners). When the player's access to the game ends, the Server Object fails and its **Get Failure Reason** says why. After the player signs in again, the component, like a typed node set to **Find By Asset**, gets its Server Object again on its own; you do not call **Rejoin**. Show the reason from **On Access Ended**, which [Authentication](../../services/authentication.md#when-the-players-access-ends) covers. If you change **Definition** or **Instance Id** in a graph after the actor has begun play, call **Rejoin** on the component. It also tries again after a failure. While the component is joining, Rejoin does nothing, so calling it from the component's own **On Status Changed** cannot loop. ### Its events and nodes | Event or node | What it does | |---|---| | **On Variables Changed** (event) | Runs after every change of the Variables players can see, and once when they are first read. It gives the Server Object and the names that changed. The delegate is `FCrowdyServerVariablesChanged`. | | **On Status Changed** (event) | Runs when the Server Object's status changes. **Object** is None when it could not be joined. The delegate is `FCrowdyServerStatusChanged`, which gives the Server Object and its status. | | **Get Server Object** | The Server Object the component holds, or None before it joins one or after it could not. | | **Get Failure Reason** | Why there is no usable Server Object, or empty. | | **Rejoin** | Joins again. | ## Without the component The component is a convenience over the same nodes. You can use them from any Blueprint, such as a widget that shows the beacon's oil without owning it. A widget needs no component for this: either use a typed node with **Find By Asset**, or call **Get Server Object** with the widget as the **Owner** and connect the result to the typed nodes' **Target**. These nodes, and **Set Server Value** and **Get Server Value**, are in the function library `UCrowdyServerObjectLibrary`: - **Get Server Object**: takes a **Definition**, an **Instance Id** and an **Owner** (which defaults to the Blueprint itself), and returns the Server Object, or None with **Error** saying why. Owners that ask for the same one share it. It is held until the owner is destroyed. - **Release Server Object**: stops one owner holding it, for an owner that lives on after it stops needing it, such as a widget that is reused. - **Get Player Instance Id**: the signed-in player's user id, which is the Instance Id of their own [Owner Only](../from-cpp/get-a-server-object.md#owner-only-types) object. It is empty when nobody is signed in. On the Server Object itself: | Node | What it does | |---|---| | **Get Status** | Connecting, Ready, Failed or Released. See [Status](../from-cpp/get-a-server-object.md#status). | | **Get Failure Reason** | Why it Failed, or what it is waiting for. | | **Get Instance Id** | The Instance Id it was joined with. | | **Get Definition** | The definition asset the object was made from, to pass to nodes that take one. A pure node. | | **On Status Changed** | An event that runs when the status changes. | The statuses are the ones from C++; see [Status](../from-cpp/get-a-server-object.md#status). ## Related - [Get a Server Object, from C++](../from-cpp/get-a-server-object.md): the same rules, from C++ - [Read and follow its variables](./read-and-follow-variables.md): the next page, **Get** and **On Changed** - [Tip jar: your first Server Object](../examples/tip-jar.md): the component, click by click - [Create a Server Object type](../create-a-type.md): the asset these nodes come from - [Access, members and timers](../access-members-and-timers.md): Only One Instance, members and teams - [Guild halls and arena lobbies](../examples/guild-halls-and-arenas.md): the component's From Server Value mode - [Keep a record per entity](../keep-a-record-per-entity.md): the component's This Actor's Area mode, for a crowd - [Troubleshooting](../troubleshooting.md): a Server Object that stays in Connecting or Fails # Read and follow its variables Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-blueprint/read-and-follow-variables # Read and follow its variables :::note Server Objects run on [ck-exec](/exec/intro). ::: Players see the variables their [definition](../create-a-type.md) marks **Visible to Players**. The typed nodes read them and follow their changes, each with a real pin of the variable's type. The examples use the village beacon, whose `CSO_VillageBeacon` asset has `Oil`, `bLit` and a Server Only `LastFedBy`. Every node here needs a Server Object; see [Get a Server Object](./get-a-server-object.md#where-the-object-comes-from). ## Get a variable **Get Oil** has one output pin, **Oil**, typed as the variable is. It reads the object's current value each time the node is used. Before the server has sent the values it reads the variable's default from the asset. A **Get** with nothing behind it, such as a **Target** that holds no object for that asset, gives the default too. To tell a real zero from "the server has not answered yet", use the node's advanced pin, **Oil Is Ready**. It is collapsed by default: click the small arrow at the bottom of the node to show it. It is false until the server has sent this Server Object's values, and true after. A variable that is an optional in C++ has a **Has Value** pin that is always shown, and it is false when the optional is empty or the values have not arrived; see [What the pins are](./pins-for-every-type.md#what-the-pins-are). **Get Server State** is the same for every visible variable at once, with one advanced **Is Ready** pin that is false until the server has sent the values. It is not offered for a Server Object whose State has no variable visible to players, such as the Game Kit `CSO_Kit_Npcs`; a **Get Server State** already placed on one fails to compile with a message saying so. The nodes work whether the definition takes its variables from a struct or you [added them in the asset](../create-a-type.md#variables-inputs-and-outputs-or-a-struct) (the List form): **Get**, **Get Server State** and **On Changed** read and follow a variable added in the asset with the same pins. ## Follow a variable **On Oil Changed** is for an event graph. It has a **Bind** execution pin in, **Then** and **Changed** execution pins out, and the value as an output pin. Wire **Bind** from **Begin Play** once: ```text Event BeginPlay -> On Oil Changed (Bind) Changed -> Set Text (Oil) ``` - **Changed** runs right away with the current value as soon as the object is ready, and again after every change of that variable, and only of that variable. A Game Kit starter's **On Changed** and **On Variables Changed** run once when it is Ready, not also before: a snapshot pushed while it is still connecting does not announce. - Each **On Changed** node you place is its own binding, so you can use the same variable's node more than once in one graph: bind one from **Begin Play** and another from a custom event, and each runs **Changed** once per change. - Run the same node's **Bind** again, for example with another **Target**, and it replaces its own binding; it does not run twice. It runs **Changed** once at once with the current value when the object is Ready. - There is nothing to unbind. A binding goes away when the Blueprint that owns it is destroyed or the Server Object is given back. - Through a component or an actor it follows whatever object that component joins. If the player's team changes and the component joins another team's object, **Changed** runs with the new object's value. - A node whose **Bind** is not connected never runs, so the compile gives a warning. - Bound to something that is not a Server Object of that asset, nothing is watched and the log says so. ## Show the oil The village square's beacon, `BP_VillageBeacon`, has a Crowdy Server Object component with **Definition** set to `CSO_VillageBeacon`, **Instance Mode** on **Instance Id**, and **Instance Id** set to `oakford`. In its Event Graph: 1. Add **On Oil Changed (CSO_VillageBeacon)** and wire **Event BeginPlay** into its **Bind**. 2. From **Changed**, use **Oil**, for example to set a text or a light's intensity. The figure sends it through a conversion to a **Print String**. It runs once when the object is ready, and again after every change, so this one graph covers both. 3. For the lamp, add **On bLit Changed (CSO_VillageBeacon)** from the **Then** pin of the first node, and wire its **bLit** to **Set Visibility** on the light, `Light` in the figure. [Blueprint snippet: so-show-oil, see /bp/unreal-sdk/so-show-oil.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. Anywhere else in the graph, **Get Oil (CSO_VillageBeacon)** gives the current amount. `LastFedBy` is Server Only, so it has no nodes here. ## A widget that shows the oil A widget has no component, so use **Find By Asset**. In its Event Construct, add **On Oil Changed (CSO_VillageBeacon)**, right-click it, choose **Find By Asset**, leave **Instance** on **Instance Id** and type `oakford` into its **Instance Id** pin. Wire Construct into **Bind**, and **Changed** into **SetText** on the widget's `OilText`, with **To Text** turning the **Oil** into text. The widget holds the beacon while it lives. [Blueprint snippet: so-widget-oil, see /bp/unreal-sdk/so-widget-oil.txt] ## How many players are online Every app has one root Server Object, which Server Compute deploys along with your types. It counts the players in the app's world. **Get Players Online** reads that count: - It takes an **Owner**, the Blueprint itself unless you connect another object, and gives **Players Online** and a return value that is false until the count first arrives. - The first call starts following the count, and the owner keeps it followed until the owner is destroyed. So a widget can read it in a binding, or an actor on Tick. - The count is the number of players the platform's realtime servers report for your app. A player who leaves is still counted for a few minutes, until their session ends. The root's definition is `CSO_PlayersOnline`, in the Crowdy SDK's content under **Data**. Use it like any other definition, for example with **Find By Asset** on the **On Changed** node for **PlayersOnline**, to run a graph each time the count changes. Leave the asset as it is: deploying sends the root's server code as the SDK writes it. A project with the Game Kit's `world` starter has `world` as its root instead, and **Get Players Online** reads the world's `Online` variable, from the definition **Game Kit World** in Project Settings names (`CSO_Kit_World`, set when you add the starter). See [The world becomes the app's root](../use-game-kit-starters.md#the-world-becomes-the-apps-root). If the world's snapshot carries no player count, as a world deployed from the platform's own starter pack does not, **Get Players Online** returns false, with 0, for good, and the log says why once. An app last deployed with an older Crowdy SDK, or never deployed from Server Compute, has no Crowdy SDK root. Deploy once from Server Compute. Until then **Get Players Online** returns false, and the log says why once for each reason in each game instance (so once per Play In Editor client): "This app has no Crowdy SDK root, so Players Online has no count: deploy the app from Server Compute (an app last deployed with an older Crowdy SDK, or by another tool, has none). It is asked again every minute." The root waits and asks again once a minute instead of every few seconds, and once the app is deployed the count arrives without restarting the game. ## Records A type whose **Holds** is **A Record per Entity** keeps its variables once for each entity; see [Keep a record per entity](../keep-a-record-per-entity.md). A component on an entity's actor reads that actor's own record, so everything above works unchanged. **Get**, **On Changed**, **On Variables Changed** and **Get Server Value** read the record, and a variable the player may not receive reads its default and never changes. To read the other records, take the Server Object from the component with **Get Server Object**. On it: - **Find Record** takes an **Id** and gives that record's **Values**, its **Owner** (a user id, 0 for none) and its **Team**. It returns false when there is no such record. Read a variable out of **Values** with **Get Server Value**. - **Get Record Ids** gives the id of every record the player can see, in no particular order. - **Holds Records** says whether the type keeps records, and **Num Records** counts them. A Game Kit starter whose hub keeps a collection, such as the world's resource nodes, holds its members as records too, read with the same nodes on its Server Object; see [Its records](../use-game-kit-starters.md#use-it-from-blueprint-and-c). - **Watch Record** takes a **Record Id** and an event (the delegate `FCrowdyServerRecordEvent`), which runs with the **Object**, the **Record Id** and the **Changed** variables after each change of that record and when it is removed (**Find Record** is then false). If the record is already current it also runs once at once, naming all its variables. A watch added from an **On Records Changed** or **On Records Updated** handler gets that catch-up and does not run a second time for the same change. **Stop Watching Record** removes it. Records dropped because the object **Failed** are not reported; **On Status Changed** is. - **On Records Changed** is an event. Bind it with **Assign** on the Server Object. It runs after every read, batch or fetch that changed records, with the **Object**, the ids that are new or changed (**Changed Ids**) and the ids that were removed (**Removed Ids**). - **On Records Updated** is bound the same way and runs at the same times, but gives a list of **Record Id** with its **Changed Variables**, then the removed ids. **Changed Variables** is empty when only the record's owner or team changed. Ids are compared with capitals significant. **Find By Asset**, and **Watch Variables** on a Server Object of this kind, read its defaults and not a record: use the component or **Find Record**. ## By name The typed nodes cover a Server Object whose shape you know. The by-name nodes work by name and by type, for the dynamic cases: a graph that shows any variable by a name in a data table, or one that works for several types. They are the ones every Blueprint had before typed nodes, and they still work. ### Show the oil by name Select the component and add its **On Variables Changed** event. The event runs once when the Variables are first read and again after each change, so one graph covers both: 1. From **Object**, call **Get Variables**. 2. Call **Get Server Value** on that, with **Name** set to `Oil`. Its **Value** pin takes the type of whatever you connect to it, here an Integer. 3. Use the value, for example to set a text or a light's intensity. `bLit` reads the same way, into a Boolean. **Get Server Value** returns false when the Variables have no value by that name or it is of another type. Names are matched ignoring case, spaces and punctuation, so `Max Health` and `maxhealth` find the same Variable, in the editor and in a packaged game. A C++ `float` Variable reads into a Blueprint Float. A Variable that is **Server Only** is never sent to players, so it always reads its default here. ### The nodes for reading and watching | Node | What it does | |---|---| | **Get Variables** | The Variables the server last sent, to read with **Get Server Value**. They hold their defaults until the object is Ready. | | **Watch Variables** | Takes an event (the delegate `FCrowdyServerVariablesEvent`) and runs it after every change, with the names that changed. If the Variables are already current it also runs once at once, naming them all. | | **Stop Watching Variables** | Removes an event that **Watch Variables** was given. | | **Get Members** | The user ids of the members, in the order they joined. Only for a type whose **Members From** is **This Object** with **Show Members to Players** on; empty otherwise. | | **Get Member Count** | How many members there are, even when the list is not shown. | | **Get Leader** | The leader's user id, or 0 when there is none. | | **Is Open For Joining** | Whether **Join** is accepted. | | **Is Member** | Whether the signed-in player is a member. | | **Is Leader** | Whether the signed-in player is the leader. | The member nodes answer for a type with [members](../access-members-and-timers.md#members). **Get Members**, **Get Member Count**, **Get Leader** and **Is Open For Joining** are for members the Server Object keeps itself; **Is Member** and **Is Leader** also work for a **Crowdy Team**. With the list shown, **Is Member** and **Is Leader** follow it and the signed-in user id. Otherwise a successful **Join** or **Leave** makes the object read again, and each read sets both. **On Variables Changed** and **Watch Variables** name a change of these too: `Members`, `MemberCount`, `Leader` and `OpenForJoining`, so a lobby's list refreshes when someone joins. A widget that only shows the oil by name would call **Get Server Object** in its Construct event, bind **Watch Variables** to a custom event, and read `Oil` there. It needs no component. Two things to know about the by-name nodes: - **Set Server Value** and **Get Server Value** work by name and by type. A misspelled name or a wrong type gives false, not an error, so branch on the result while you are wiring a graph. The typed nodes have no such failure: a wrong name or type does not compile. - The names **On Variables Changed** and **Watch Variables** give are the ones **Get Server Value** takes, the variable's own name in the asset, even when the definition's **Server Names** gives a Variable a different name on the server. ## Related - [Read and follow its variables, from C++](../from-cpp/read-and-follow-variables.md): the same rules, from C++ - [Get a Server Object](./get-a-server-object.md): the previous page, where the object comes from - [Hear its messages](./hear-server-messages.md): the next page, the messages a type sends - [Pins for every type](./pins-for-every-type.md): which pin each variable type gets - [Tip jar: your first Server Object](../examples/tip-jar.md): **On Total Changed**, click by click - [Shared boss fight](../examples/shared-boss.md): a health bar, a phase and a respawn message - [Raiders on the north road](../examples/raiders-on-the-north-road.md): the same nodes on a record per raider, with values only some players see - [Troubleshooting](../troubleshooting.md#a-value-is-not-what-you-expect): when a value is not what you expect # Hear its messages Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-blueprint/hear-server-messages # Hear its messages :::note Server Objects run on [ck-exec](/exec/intro). ::: A message is the server telling everyone listening that something happened, such as the Hollow Giant falling. The type's [definition](../create-a-type.md#server-messages) lists its messages, and its [server code](../write-server-logic.md#send-a-message) sends them. A game hears one with a typed node, **On Message Received**, or with **Watch Message** when it has to work by name. Every node here needs a Server Object; see [Get a Server Object](./get-a-server-object.md#where-the-object-comes-from). A message is an event, not a value. **Received** runs when the server sends the message and not at any other time: not when the object becomes ready, and not for a message sent before the game was listening. To show what is true now, such as whether the boss is defeated, read a variable with **Get** and **On Changed**; see [Read and follow its variables](./read-and-follow-variables.md). ## Hear a message **On BossDefeated Received (CSO_Boss)** has a **Bind** execution pin in, **Then** and **Received** execution pins out, and one output pin for each value the message carries: `Winner`, an Integer64, and `Hits`, an Integer. Wire **Bind** from **Begin Play** once: ```text Event BeginPlay -> On BossDefeated Received (Bind) Received -> Print String (Winner, Hits) ``` - **Received** runs once for each message the server sends, with that message's values on the output pins. - Each **On Received** node you place is its own binding, so you can use the same message's node more than once in one graph, and each runs **Received** once per message. - Run the same node's **Bind** again, for example with another **Target**, and it replaces its own binding; it does not run twice. - There is nothing to unbind. A binding goes away when the Blueprint that owns it is destroyed or the Server Object is given back. - Through a component or an actor it follows whatever object that component joins. - A node whose **Bind** is not connected never runs, so the compile gives a warning. Bound to something that is not a Server Object of that asset, nothing is heard and the log says so. - It takes the object the way **On Changed** does: **Target**, the actor's component, or **Find By Asset**; see [Where the object comes from](./get-a-server-object.md#where-the-object-comes-from). The palette offers a node for each message whose **Players Can Listen** is ticked and whose payload values all have a pin; see [Pins for every type](./pins-for-every-type.md). A message with no values has a node with **Received** and no value pins. ## Announce the fall The boss actor `BP_Boss` from [Shared boss fight](../examples/shared-boss.md#the-blueprint) has a Crowdy Server Object component with **Definition** set to `CSO_Boss`. In its Event Graph: 1. Add **On BossDefeated Received (CSO_Boss)**. Wire its **Bind** from the **Then** pin of the last **On Changed** node you added, or from **Event BeginPlay** if it is the first. 2. From **Received**, add a **Print String** that builds the line with **Build String**: the **Winner** pin, " felled the Hollow Giant in ", the **Hits** pin and " hits". 3. Compile and save. The figure turns **Winner** into a string first, since it is an Integer64, and builds the line from it. [Blueprint snippet: so-boss-defeated, see /bp/unreal-sdk/so-boss-defeated.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. Play with two players, as on that page. When the boss falls, both windows print the line once, with the same **Winner**. A player who joins a moment later sees the boss already **Defeated** through **On Phase Changed**, and prints nothing: the message is gone. If the node is not in the palette, check that the message is ticked **Players Can Listen** in the asset and that you have saved it. If the graph fails to compile with "heard only by other Server Object types; turn on Players Can Listen", the message was ticked once and is not now. If it says the asset has "no message ... any more", the message was renamed or removed: place the node again from the menu, or give the message its old name back. ## When a message is heard The game listens to a message while a node or a **Watch Message** wants it and the connection is open. When the last one goes away it stops listening, and it starts again after a dropped connection comes back. - **A message that arrives while the object is reading its values is held.** It runs after the values are current, so a graph that reads the variables from **Received** sees the state the message followed. At most 32 are held; past that the oldest is dropped and the log warns. - **A message is announced only while the Server Object is Ready.** One that arrives while it is Connecting is held until it is. - **A message sent while the connection is closed is lost.** Nothing is replayed when it comes back; the game reads the variables again, and **On Changed** reports what differs, but a message is not a value and cannot be read again. - **A message in a record type can arrive before the record change it follows.** A type that keeps [a record per entity](../keep-a-record-per-entity.md) sends its records in batches, so a message sent right after a record changed may be heard before that record shows the change. Put what the listener needs into the payload, or read the record when the next batch arrives. - **A refused listen is asked again.** If the server refuses to let the game listen to a message, the log warns once with the platform's reason and the game asks again after a wait that grows from 1 second to 30. Nothing else about the Server Object fails. - **Every message listened to is a subscription.** A connection allows 256 subscriptions in all, and each subscribe counts against the 120 calls a connection may make every 10 seconds. A game that listens to a few messages on a few Server Objects never gets near either; see [A message never arrives](../troubleshooting.md#a-message-never-arrives). ## By name The typed nodes cover a message you know. **Watch Message** and **Stop Watching Message** work by name, for a graph that listens to a message from a data table, or to one of several assets. Both are on the Server Object itself, so get it first with **Get Server Object** or from a component. | Node | What it does | |---|---| | **Watch Message** | Takes a **Message**, the message's Name, and an event (the delegate `FCrowdyServerMessageEvent`), and runs the event each time the server sends that message. The event gives the **Object**, the **Message** and the **Payload**. | | **Stop Watching Message** | Removes an event that **Watch Message** was given. | Read the values from **Payload** with **Get Server Value**, as you would read the variables: 1. Call **Get Server Object** in the actor's or widget's start event, and from its result call **Watch Message** with **Message** set to `BossDefeated`. 2. Drag from the **Event** pin and choose **Add Custom Event**, named `OnBossMessage`. 3. In the event, call **Get Server Value** on **Payload** with **Name** set to `Winner`, into an Integer64, and again for `Hits`. The first figure is steps 1 and 2: **Watch Message** on the component's Server Object, and the custom event wired to its **Event** pin. The second is step 3, where the event reads both values from its **Payload** and prints the same line as the typed node. [Blueprint snippet: so-watch-message, see /bp/unreal-sdk/so-watch-message.txt] [Blueprint snippet: so-message-payload, see /bp/unreal-sdk/so-message-payload.txt] The same rules hold as for the typed node. Nothing runs for a message sent before you watched. Watching the same event twice runs it once. **Stop Watching Message** removes it, and an event whose Blueprint is destroyed stops running by itself. It takes the same **Message** and the same event that **Watch Message** was given: [Blueprint snippet: so-unwatch-message, see /bp/unreal-sdk/so-unwatch-message.txt] Watching a name the definition does not have, or one whose **Players Can Listen** is off, does nothing: the log warns "CSO_Boss has no message named ... so nothing can listen to it", or "... is heard only by other Server Object types; turn on Players Can Listen to hear it in a game", and the event never runs. The by-name nodes check at run time, not when you compile, so test the graph once. **Get Server Value** returns false when **Payload** has no value by that name or of that type; see [By name](./read-and-follow-variables.md#by-name). ## Related - [Hear its messages, from C++](../from-cpp/hear-server-messages.md): the same rules, from C++ - [Read and follow its variables](./read-and-follow-variables.md): the previous page, **Get** and **On Changed** - [Call its functions](./call-functions.md): the next page - [Server messages](../create-a-type.md#server-messages): defining a message and who hears it - [Send a message](../write-server-logic.md#send-a-message): what the server code does - [Shared boss fight](../examples/shared-boss.md): the boss these nodes announce - [Troubleshooting](../troubleshooting.md#a-message-never-arrives): a message that never arrives # Call its functions Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-blueprint/call-functions # Call its functions :::note Server Objects run on [ck-exec](/exec/intro). ::: A client never changes a Server Object's values. It calls one of the Server Functions its [definition](../create-a-type.md) lists, and the server decides. The examples use the village beacon's `FeedBeacon` function, which takes an `Oil` amount and answers with the oil the beacon now holds. Every node here needs a Server Object; see [Get a Server Object](./get-a-server-object.md#where-the-object-comes-from). ## The Call node **Call FeedBeacon** has an execution pin and a pin for each input on the left. On the right it has **Then**, **On Success** and **On Failed**, then a pin for each output. It is a latent node, so it goes in an event graph. **On Success** or **On Failed** runs, one of them and only once, when the server answers. **Outcome** and **Reason** are shown on the node, after the function's outputs. **Retryable**, **Code** and **Timeout** are in the node's advanced area: click the small arrow at the bottom of the node to show them. **Timeout** is an input, explained in [Give up sooner](#give-up-sooner). **Code** is an output that only a [Game Kit starter](../use-game-kit-starters.md#refusals-and-their-codes) fills, with its refusal code, such as `full`; it is None for every other type. A pin you have already wired stays visible when the area is collapsed. - **On Success** runs with the outputs set. Wire them straight from their pins. - **On Failed** runs with **Outcome**, **Reason**, **Retryable** and **Code**. **Outcome** is one of the [call outcomes](../troubleshooting.md#outcomes). A call the server refuses by a rule in the asset, such as **Callable By**, arrives as **Denied** with the reason to show, and a call inside a **Cooldown** as **Denied** with **Retryable** true; see [What a refused player sees](../access-members-and-timers.md#what-a-refused-player-sees). A call to a server, or a type, that is switched off or paused arrives as **Paused** with **Retryable** true, so a Switch on **Outcome** has a **Paused** pin: try again later or show a paused message. A Game Kit starter's refusal for a reason of the game's, such as a full match, arrives as **Refused** with its **Code**, so the Switch has a **Refused** pin too. A call made after the player's access ended, or still waiting when it did, arrives as **Access Ended**, not retryable, so the Switch has an **Access Ended** pin too: show **Reason** and send the player back to sign in; see [When access ends](../troubleshooting.md#when-access-ends). A call that times out says in **Reason** whether the server's own limit or the connection gave up; see [Timeout](../troubleshooting.md#outcomes). - An input you leave alone sends its default from the asset. - An input that does not fit its type, such as -1 for a `uint32` or 70,000 for an `int16`, sends nothing: **On Failed** runs as a **Bad Request** and **Reason** names the input. The beacon's oil changes on the server, not on the client when the call succeeds. **On Oil Changed** shows the new amount to every player watching, the way it does in C++. **Call** reports what the server answered; use **On Changed** to show the state. When **Members From** is **This Object**, the built-in functions **Join**, **Leave**, **Add Member**, **Remove Member**, **Make Leader** and **Set Open for Joining** are offered as **Call** nodes too. A busy server is retried for you, up to three tries, and nothing else is; see [What the SDK retries](../from-cpp/call-functions.md#what-the-sdk-retries). ## Feed the beacon When the player walks into the beacon, feed it ten units of oil: 1. Add **Call FeedBeacon (CSO_VillageBeacon)** and wire the overlap event, **Event ActorBeginOverlap**, into its execution pin. 2. Set its **Oil** input to 10. 3. Under **On Success**, use its **Oil** output, the oil the server says the beacon now holds. Under **On Failed**, show **Reason**, or try again later when **Retryable** is true; show both with the arrow at the bottom of the node first. The figure prints the oil, and the reason, with a **Print String** each. [Blueprint snippet: so-feed-beacon, see /bp/unreal-sdk/so-feed-beacon.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. The call needs no **Target**: it uses the beacon's own component. ## Give up sooner A call waits for the answer as long as the project's **Server Object Call Timeout** allows, 20 seconds unless you changed it; see [Project settings](../../reference/project-settings.md#server-object-timeouts). The advanced **Timeout** pin of a **Call** node, typed or generic, is a limit of its own for that one call, in seconds. It is counted from the call, retries and waiting to send included. It can only shorten the wait: a value above the project's timeout changes nothing, and 0, the default, leaves only the project's. When the function has an input named **Timeout** of its own, the typed **Call** node labels its limit pin **Call Timeout** so the two do not clash; otherwise the pin is **Timeout**. A graph saved with either label keeps its links. When it runs out, **On Failed** runs with **Outcome** **Timeout** and **Retryable** false, and **Reason** reads, for a **Timeout** of 5, "No answer within 5 seconds; FeedBeacon may still have run on the server". The server is not told to stop, so the call may still take effect, and an answer that arrives later is not reported. Watch the beacon's oil with **On Oil Changed** before you feed it again. ## Ping the platform **Ping Server Objects** is a latent node under **Crowdy SDK > Server Objects**. It asks the platform for an answer over the connection the Server Objects already hold and measures how long it took: - **On Success** runs with **Round Trip Ms**, how long the platform took to answer. - **On Failed** runs with **Reason**. The node is the async action `UCrowdyServerPingAction`, and both of its pins are the delegate `FCrowdyServerPingDone`. It never opens a connection. With no Server Object connection open, such as before the first Server Object is used, it fails at once with "No Server Object connection is open". The platform answers at most 60 pings every 10 seconds on a connection and drops the rest without a word, so the SDK refuses more than that itself, failing with "Too many pings: the platform answers at most 60 every 10 seconds". Ping on a timer of a second or more, not every frame. ## By name The generic **Call Server Function** under **Server Objects** has a **Function** dropdown that lists every function of every asset, for when you would rather pick than type. Its pins are laid out like a **Call** node's, with **Outcome** and **Reason** shown and **Retryable** and **Code** in the advanced area. The nodes below work by name and by type instead, for a graph that calls whichever function it is told to. ### Feed the beacon by name 1. Call **Get Server Object** on the component. Drag the result into **Is Valid** first: it is None until the object joins. 2. From the Server Object, call **Make Inputs** with **Function** set to `FeedBeacon`. It returns the function's **Inputs** with their default values. It has execution pins: wire it in before **Set Server Value**, and pass its result to both of the next two nodes. 3. Call **Set Server Value** with those Inputs, **Name** `Oil` and **Value** `10`. It changes the Inputs you pass in and returns true, or false when there is no Input by that name or the type differs. 4. Call **Call Server Function** with the Server Object, **Function** `FeedBeacon` and those Inputs. 5. Under **On Success**, call **Get Server Value** on **Outputs** with **Name** `Oil` to read the oil the server says the beacon now holds. Under **On Failed**, show **Reason**, or try again later when **Retryable** is true. [Blueprint snippet: so-untyped-feed, see /bp/unreal-sdk/so-untyped-feed.txt] The figure is steps 1 to 4 only: it has no trigger event, no **Is Valid** and nothing on **On Success** or **On Failed**, and it reads the Server Object from a component variable named `Beacon`. Add your own trigger and the **Is Valid**, and wire step 5. **Call Server Function** here is the one with no **Function** dropdown, found under **Crowdy SDK > Server Objects**; it takes a Server Object and Inputs. It is latent like the typed one, and its **On Success** and **On Failed** behave the same. Both are the delegate `FCrowdyServerCallDone`, which carries **Outputs**, **Outcome**, **Reason**, **Retryable** and **Code**; a C++ handler bound to them takes all five, see [Bind the Blueprint call in C++](../from-cpp/call-functions.md#bind-the-blueprint-call-in-c). **Make Inputs** returns a function's Inputs with their default values, and is empty when the function takes none. Two things to know about the by-name nodes: - **Set Server Value** and **Get Server Value** work by name and by type. A misspelled name or a wrong type gives false, not an error, so branch on the result while you are wiring a graph. The typed nodes have no such failure: a wrong name or type does not compile. - For a function with no Inputs, leave the **Inputs** pin of the untyped **Call Server Function** empty. ## Related - [Call its functions, from C++](../from-cpp/call-functions.md): the same rules, from C++, and what the SDK retries - [Hear its messages](./hear-server-messages.md): the previous page - [Call a Server Function Library](./call-a-library.md): the next page, the **Call** node of a type with no instance - [Pins for every type](./pins-for-every-type.md): which pin each input and output gets - [Use the Game Kit starters](../use-game-kit-starters.md): the **Refused** outcome and its **Code** - [Troubleshooting](../troubleshooting.md#outcomes): every outcome, and the refusals from your server code - [Access, members and timers](../access-members-and-timers.md#what-a-refused-player-sees): what a refused player sees - [Tip jar: your first Server Object](../examples/tip-jar.md): **Call Tip**, click by click - [Shared boss fight](../examples/shared-boss.md): **Call Hit** with a cooldown and a value range # Call a Server Function Library Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-blueprint/call-a-library # Call a Server Function Library :::note Server Objects run on [ck-exec](/exec/intro). ::: A [Server Function Library](../server-function-libraries.md) has functions and nothing else: no variables to read, no messages to hear, and no instance of its own to pick. So from Blueprint it has one kind of node, **Call**, and that node needs no **Target**, no component and no Instance Id. Place it and use it. ## The Call node Right-click in an event graph, type the function's name or open the category **Server Function Libraries**, then the asset's name, for the loot roller's `CSO_LootRoller` **CSO Loot Roller**, and choose **Call RollLoot**. The node is **Call RollLoot (CSO_LootRoller)**. [Blueprint snippet: so-library-call-node, see /bp/unreal-sdk/so-library-call-node.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. It is the same node as the [Call node of a Server Object](./call-functions.md#the-call-node), without its **Target**: - an execution pin in, and a pin for each input of the function; - **Then**, **On Success** and **On Failed**, and a pin for each output; - in the advanced area, **Outcome**, **Reason**, **Retryable** and **Timeout** (click the small arrow at the bottom of the node). **Timeout** is the limit of [Give up sooner](./call-functions.md#give-up-sooner). It is latent, so it goes in an event graph, and **On Success** or **On Failed** runs, one of them and only once, when the server answers. **On Failed** carries the same [outcomes](../troubleshooting.md#outcomes) as any call: **Denied** for a function that is **Server Only** or **Developers Only**, **Bad Request** for an input that does not fit, **Timeout**, **Paused** when the type or app is switched off or paused, and so on. A library offers only **Call** nodes, one for each function. There is no **Get**, **On Changed** or **On Received**, since a library keeps no values and sends no messages, and no **Get Server State**. If the asset is a Server Object when you place a node and you change its **Kind** later, remove the nodes that no longer apply; see [Switch Kind](../server-function-libraries.md#switch-kind). ## Roll loot from the chest The chest on the north road, `BP_Chest`, hands out a trinket when a player walks into it. In its Event Graph: 1. Add **Event ActorBeginOverlap**. 2. Add **Call RollLoot (CSO_LootRoller)** from the menu above and wire the overlap event into its execution pin. 3. Set its **Seed** pin to 7 and its **Tier** pin to 1. 4. From **On Success**, add a **Print String** and wire the **Item** output to it. From **On Failed**, add another and wire **Reason**; show it with the arrow at the bottom of the node first. The figure prints the item, and the reason, with a **Print String** each. [Blueprint snippet: so-chest-roll, see /bp/unreal-sdk/so-chest-roll.txt] Compile and save, and walk into the chest. The screen prints `SilverRing:t1`. The library keeps nothing, so the same **Seed** and **Tier** give the same item every time; a game that wants variety passes a different **Seed** each time, such as a counter that goes up. Set **Tier** to 5 and the server refuses the call before the function runs, because the input's **Value Range** is 0 to 4: **On Failed** gets the **Outcome** Server Error and the **Reason** `bad_params: Tier must be 0 to 4`. ## What to expect - **Nothing to acquire first.** A library has no values to read, so it is ready as soon as the connection opens, and a call made before then waits for it. - **The first call after the library has been idle can take over a second.** Players see the answer a moment late. A call that goes on past the project's **Server Object Call Timeout**, 20 seconds unless you changed it, ends as **Timeout**. - **Calls run side by side.** Two **Call** nodes that run together are two independent calls, and the server may answer them on different instances, in either order. A library has no state, so nothing links them; do not rely on one call's effect being there for the next. - **A library changes game truth through a Server Object.** If a roll should give the player the item, the library's server code calls the player's own Server Object and that object changes its values; the game then sees the change with **On Changed** on that object. See [A Server Function Library](../write-server-logic.md#a-server-function-library). ## Related - [Call a Server Function Library, from C++](../from-cpp/call-a-library.md): the same call, from C++ - [Call its functions](./call-functions.md): the previous page, the **Call** node of a Server Object - [Pins for every type](./pins-for-every-type.md): the next page, which pin each input and output gets - [Make a Server Function Library](../server-function-libraries.md): the asset, **Kind** and **Scaling** - [Loot for the Hollow Giant](../examples/hollow-giant-loot.md): the loot roller, the chest and the boss - [Troubleshooting](../troubleshooting.md#outcomes): every outcome # Pins for every type Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-blueprint/pins-for-every-type # Pins for every type :::note Server Objects run on [ck-exec](/exec/intro). ::: The typed nodes give every value a pin of its own type, so none needs a cast. This page is what the pins are, and what happens to them when the asset changes. ## What the pins are Every value has a pin of its own type, with no casting. | In the asset or C++ | Pin | |---|---| | `bool` | Boolean | | `uint8` | Byte | | `int32` | Integer | | `int64` | Integer64 | | `float`, `double` | Float | | `FString` | String | | `FName` | Name | | Enums | The enum | | `FVector`, `FRotator`, `FVector2D`, `FQuat`, `FIntPoint`, `FIntVector`, `FLinearColor`, `FColor`, `FDateTime`, `FTimespan`, `FGuid`, `FGameplayTag` | The struct of that name | | Soft object and soft class references | A soft reference pin | | Your own structs | The struct; right-click its pin and choose **Split Struct Pin** to get a pin for each field | | `TArray`, `TSet`, `TMap` | An Array, Set or Map pin | | `int8`, `int16`, `uint16` (C++ only) | Integer | | `uint32`, `uint64` (C++ only) | Integer64 | | `TOptional` (C++ only) | A pin for T, and a **Has Value** pin beside it | The C++-only types are carried on the nearest pin that holds every value. A value that does not fit, such as a negative Integer64 for a `uint64`, is refused: a **Call** fails through **On Failed** naming the input, and a **Get** logs a warning and reads the default. An optional input is sent only when its **Has Value** is true; an optional output's **Has Value** says whether the server set it. On a **Get**, an optional's **Has Value** is false when it is empty and also before the server has sent the values. A variable, input or output whose name is one a node uses for its own pin gets no node, and the Blueprint compile says to rename it. The names are `Team Id`, `Instance Id`, `Value Changed`, `Has Values`, `then`, `execute`, `self`, `Server Function`, `Call Outcome`, `Call Reason`, `Call Retryable`, `On Success`, `On Failed`, and another value's name followed by ` Has Value`. Rename it in the asset; the nodes follow. `Call Outcome`, `Call Reason` and `Call Retryable` are the pins a **Call** node shows as **Outcome**, **Reason** and **Retryable**; **Retryable** is in its advanced area, under the small arrow at the bottom of the node. `Has Values` and another value's name followed by ` Has Value` are the pins the **Get Server State** and **Get Server Variable** nodes label **Is Ready**; the pin is the same, only its label changed. Two other things have no pin. An Array, Set, Map or optional inside another one cannot be held in a Blueprint, and neither can a C++ struct or enum that is not marked `BlueprintType`. The compile says which variable and why, for example "the struct FRelic is not BlueprintType; mark it BlueprintType", and the node is not offered in the menu until it does. Blueprint structs and enums you made in the editor already work. ### When the asset changes The nodes follow the asset. Rename a variable or an input that you added in the asset, or a field of a Blueprint struct, and the node keeps it and its wires. Change a type and the pin changes with it; a wire that no longer fits is a compile error, and the message names the pin, so reconnect it or remove it. Remove a variable and the nodes for it say "has no variable Oil any more". A variable you untick from **Visible to Players** fails the same way, and the message tells you to tick it again or remove the node. ## Related - [Get a Server Object](./get-a-server-object.md) - [Read and follow its variables](./read-and-follow-variables.md): the **Get** and **On Changed** pins - [Call its functions](./call-functions.md): the input and output pins of a **Call** node - [Create a Server Object type](../create-a-type.md#field-types): the types a variable can have # Get a Server Object Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-cpp/get-a-server-object # Get a Server Object :::note Server Objects run on [ck-exec](/exec/intro). ::: A game never creates a `UCrowdyServerObject` itself. It asks `UCrowdyServerObjectSubsystem`, a game instance subsystem, for the one matching a [definition asset](../create-a-type.md) and an Instance Id, and names an owner: the object that needs it. The subsystem keeps the Server Object connected for as long as it has an owner, and shares it between every owner that asks for the same one. ## Acquire it The beacon acquires its Server Object in `BeginPlay`, with itself as the owner and the village's name as the Instance Id. `BeaconDefinition` is the asset from [Create a Server Object type](../create-a-type.md), picked in the Details panel. ```cpp // VillageBeacon.h #include "Components/PointLightComponent.h" #include "Components/SphereComponent.h" #include "CrowdyServerObject.h" #include "VillageBeacon.generated.h" UCLASS() class AVillageBeacon : public AActor { GENERATED_BODY() public: AVillageBeacon(); virtual void BeginPlay() override; UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Trigger; UPROPERTY(VisibleAnywhere, Category = "Lantern") TObjectPtr Light; UPROPERTY(EditAnywhere, Category = "Crowdy") TObjectPtr BeaconDefinition; UPROPERTY(EditAnywhere, Category = "Crowdy") FString VillageName = TEXT("oakford"); UPROPERTY() TObjectPtr Beacon; }; // VillageBeacon.cpp #include "VillageBeacon.h" #include "CrowdyServerObjectDefinition.h" #include "CrowdyServerObjectSubsystem.h" #include "Engine/GameInstance.h" void AVillageBeacon::BeginPlay() { Super::BeginPlay(); FString Error; Beacon = GetGameInstance()->GetSubsystem()->Acquire(BeaconDefinition, VillageName, this, Error); if (!Beacon) { UE_LOG(LogTemp, Warning, TEXT("The village beacon cannot be used: %s"), *Error); return; } } ``` `Acquire(Definition, InstanceId, Owner, OutError)` returns the Server Object, or null with `OutError` saying why: - The definition cannot be used: it is missing, it has a problem the editor reported when you saved it, or a struct changed since it was saved. - The Instance Id is not valid. It must be 1 to 256 bytes of UTF-8 with no control characters (`An Instance Id must be 1 to 256 bytes with no control characters`). - There is no owner (`Acquire needs an owner`). Instance Ids are case-sensitive: `oakford` and `Oakford` are two Server Objects. A [Server Function Library](../server-function-libraries.md) has no Instance Id and no values to follow, so a game does not acquire it: it calls its functions with `CallLibraryFunction`, which holds the library for you. See [Call a Server Function Library](./call-a-library.md). You can acquire before the player has signed in. The Server Object waits in **Connecting** until the connection opens, and the subsystem keeps trying to open it while any Server Object needs one. ## One object, many owners Every owner that acquires the same definition and Instance Id gets the same `UCrowdyServerObject`: one set of values, one stream of changes, and one connection to the server between them. Two beacons in the same village, or a beacon and a HUD widget showing its oil, share it. The subsystem holds owners weakly. You do not have to give a Server Object back: when every owner has been destroyed, a grace period starts, `UCrowdyServerObjectSubsystem::GracePeriodSeconds` (10 seconds). An owner that acquires it again inside that time gets the same object back, values and all. When the grace period runs out, the Server Object is **Released** and forgotten; the next `Acquire` starts a fresh one. - **Map travel.** The grace period does not run while the game instance is loading a map, or during a seamless travel. A beacon destroyed by leaving one map and acquired again by a beacon in the next reuses the same Server Object, with no gap in its values. - **Giving it back early.** `Release(Object, Owner)` stops one owner holding it, as if that owner had been destroyed. Use it for an owner that lives on after it stops needing the Server Object, such as a pooled widget. ## One object per area A type used for a crowd can keep one Server Object for each area of the ground; see [Keep a record per entity](../keep-a-record-per-entity.md#pick-the-object). The Instance Id is the area's `x,y`. The session's chunk size places a location in a chunk the way actor updates do: `UCrowdyServerObjectSubsystem::FindGroundChunk` gives the chunk of a location, Unreal's X and Y over the chunk size, rounded down, and `GetAreaKey` on the definition turns a chunk into the Instance Id, using the definition's **Area Size**: ```cpp int64 ChunkX = 0; int64 ChunkY = 0; if (Subsystem->FindGroundChunk(GetActorLocation(), ChunkX, ChunkY)) { const FString AreaId = RaiderDefinition->GetAreaKey(ChunkX, ChunkY); Area = Subsystem->Acquire(RaiderDefinition, AreaId, this, Error); } ``` Look again when the location moves into another area, and `Release` the old one. In Blueprint, or on an actor with a `UCrowdyServerObjectComponent`, the component's **This Actor's Area** instance mode does all of this. `FindGroundChunk` returns false without a game session. ## Status A Server Object is always in one of four statuses, `ECrowdyServerObjectStatus`. Read the current one with `GetStatus()`; `OnStatusChanged`, an `FOnCrowdyServerObjectStatusChanged`, runs on every change after you acquire. `GetDefinition()` and `GetInstanceId()` return what it was acquired with. | Status | Meaning | |---|---| | Connecting | Joining the Server Object and reading its variables for the first time, or waiting until the server can answer. | | Ready | Its variables are current and its functions can be called. | | Failed | It cannot be used. `GetFailureReason()` says why. Acquiring it again tries afresh. | | Released | Given back. Nothing on it works any more. | The beacon goes dark when its Server Object fails, rather than show oil it can no longer vouch for: ```cpp // VillageBeacon.h void HandleBeaconStatus(UCrowdyServerObject* Object, ECrowdyServerObjectStatus Status); // VillageBeacon.cpp void AVillageBeacon::BeginPlay() { Super::BeginPlay(); FString Error; Beacon = GetGameInstance()->GetSubsystem()->Acquire(BeaconDefinition, VillageName, this, Error); if (!Beacon) { UE_LOG(LogTemp, Warning, TEXT("The village beacon cannot be used: %s"), *Error); return; } Beacon->OnStatusChanged.AddUObject(this, &AVillageBeacon::HandleBeaconStatus); } void AVillageBeacon::HandleBeaconStatus(UCrowdyServerObject* Object, ECrowdyServerObjectStatus Status) { // A failed beacon goes dark instead of showing oil the server no longer vouches for. if (Status == ECrowdyServerObjectStatus::Failed) { Light->SetVisibility(false); UE_LOG(LogTemp, Warning, TEXT("The village beacon failed: %s"), *Object->GetFailureReason()); } } ``` A Server Object that waits in Connecting, or one that fails, says why in `GetFailureReason()`; see [A Server Object does not become Ready](../troubleshooting.md#a-server-object-does-not-become-ready). To see what one is doing, turn on [`crowdy.exec.trace`](../troubleshooting.md#seeing-what-a-server-object-is-doing). ## Owner Only types A definition whose **Visibility** is **Owner Only** describes a Server Object that belongs to one player, such as a player's own stash. Its Instance Id must be the owning player's user id in decimal, which you get from `UCrowdyGameSession`: `LexToString(Session->GetUserID())`. Only that player can read its values or call its functions; for anyone else it fails as soon as the server refuses to show them. For a group's shared object that only its members read, see **Members** under [Readable By](../access-members-and-timers.md#readable-by). Everything else works the same way. Each change reaches the owner a moment later than on a Public type, because the SDK fetches the new values when the server says they changed, instead of receiving them with the notice. ## Sign-out, shutdown and dropped connections - **Sign-out.** When the player signs out, every Server Object fails with `The player signed out`, and every call still waiting finishes as `Canceled`. After the player signs in again, acquire them again. - **Access ended.** When the player's access to the game ends (suspended, revoked, expired, signed out by the platform, or the account blocked), every Server Object fails with `Your access ended: ` followed by the reason, every call in flight or made later finishes as `AccessEnded`, and nothing reconnects. Show the reason from `UCrowdyGameSession::OnAccessEnded` and return the player to sign in; after a successful sign-in, acquire them again. See [When access ends](../troubleshooting.md#when-access-ends). - **Shutdown.** When the game instance shuts down, every Server Object is released quietly: no status change, no value change, and calls still waiting do not run their completion. - **A dropped connection.** The SDK reconnects by itself and starts watching again. Changes made while it was gone are not replayed, so it reads the variables again and reports whatever differs as one change. Your code sees at most one extra change event. ## Related - [Read and follow its variables](./read-and-follow-variables.md) - [Call its functions](./call-functions.md) - [Get a Server Object, from Blueprint](../from-blueprint/get-a-server-object.md): the same thing without C++ - [Troubleshooting](../troubleshooting.md): why a Server Object waits or fails, and `crowdy.exec.trace` - [Player sign-in](../../runtime/player-sign-in.md) # Read and follow its variables Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-cpp/read-and-follow-variables # Read and follow its variables :::note Server Objects run on [ck-exec](/exec/intro). ::: The variables a [definition](../create-a-type.md) marks **Visible to Players** are a Server Object's public face. The SDK keeps a copy of them current on every client that has the Server Object, and tells your code each time the server changes them. ## React to changes `WatchValues`, a function of `UCrowdyServerObject`, takes a listener and returns an `FDelegateHandle`. The beacon watches its Server Object right after acquiring it, and sets its light from the oil and the lit flag: ```cpp // VillageBeacon.h void HandleBeaconChanged(UCrowdyServerObject* Object, TConstArrayView Fields); // VillageBeacon.cpp void AVillageBeacon::BeginPlay() { Super::BeginPlay(); FString Error; Beacon = GetGameInstance()->GetSubsystem()->Acquire(BeaconDefinition, VillageName, this, Error); if (!Beacon) { UE_LOG(LogTemp, Warning, TEXT("The village beacon cannot be used: %s"), *Error); return; } Beacon->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateUObject(this, &AVillageBeacon::HandleBeaconChanged)); } void AVillageBeacon::HandleBeaconChanged(UCrowdyServerObject* Object, TConstArrayView Fields) { const FVillageBeaconState* State = Object->GetState().GetPtr(); if (!State) { return; } // Fields names what changed by server name; the state holds every watched value. Light->SetVisibility(State->bLit); if (Fields.Contains(TEXT("Oil"))) { Light->SetIntensity(State->Oil * 100.f); } } ``` The listener, an `FOnCrowdyServerValuesChanged` delegate, receives the Server Object and the names of the fields that changed. It runs: - **once when the values first arrive**, naming every variable players can see. This comes just before `OnStatusChanged` reports Ready, so by the time a status handler sees Ready the values are already applied; - **once when you start watching, if the values are already current**, naming every variable players can see. A second beacon that acquires a Server Object the first one already made Ready is set up at once, with no special case in your code. A listener added from inside a Ready status handler is called once, not twice; - **once per change on the server**, naming every field that change touched. A Server Function that changes both `Oil` and `bLit` produces one call naming both, never two calls or a moment where one is new and the other old. `UnwatchValues(Handle)` removes a listener. A listener bound to a `UObject`, as above, is skipped once that object is gone, so the beacon does not need to remove its own; a lambda or raw binding does. ## Read the values `GetState()` returns an `FInstancedStruct` holding the definition's State struct. Read it with `GetPtr` and the struct type, as the beacon does, at any time, not only inside the listener. - The variables **Visible to Players** hold what the server last sent. Before the first values arrive they hold the struct's defaults. - The other fields, the **Server Only** variables, hold the struct's defaults, always. They never leave the server, so `LastFedBy` reads `0` on every client. - The copy belongs to the SDK. Changing it changes nothing on the server, and the next change from the server overwrites it. To change a value, [call a Server Function](./call-functions.md). When the definition's variables are [added in the asset](../create-a-type.md#variables-inputs-and-outputs-or-a-struct) rather than taken from a struct, there is no struct to read them into. `GetStateList()` returns them as an `FInstancedPropertyBag` to read by name; it is empty when the variables come from a struct: ```cpp const FInstancedPropertyBag State = Beacon->GetStateList(); const TValueOrError Oil = State.GetValueInt32(TEXT("Oil")); const TValueOrError bLit = State.GetValueBool(TEXT("bLit")); if (Oil.HasValue() && bLit.HasValue()) { Light->SetIntensity(bLit.GetValue() ? Oil.GetValue() * 100.f : 0.f); } ``` The same rules hold: the variables players do not see keep their default values on every client. The typed Blueprint **Get** and **On Changed** nodes read and follow these variables too. ## How many players are online Every app has one root Server Object, which Server Compute deploys along with your types. It counts the players in the app's world: the number the platform's realtime servers report for your app, in which a player who leaves is still counted for a few minutes, until their session ends. `UCrowdyServerObjectLibrary::GetPlayersOnline(Owner, Count)` reads it. It returns false, with `Count` 0, until the count first arrives. The first call starts following the root for `Owner`, which keeps it followed until it is destroyed, so calling it every frame is fine: ```cpp int32 PlayersOnline = 0; if (UCrowdyServerObjectLibrary::GetPlayersOnline(this, PlayersOnline)) { PlayersText->SetText(FText::AsNumber(PlayersOnline)); } ``` To react to each change instead, acquire the root and watch it. `GetPlayersOnlineDefinition()` gives its definition, `CSO_PlayersOnline` in the Crowdy SDK's content, and its State struct is `FCrowdyPlayersOnlineState`. The root has only one instance, so the Instance Id is ignored: ```cpp FString Error; UCrowdyServerObject* Root = UCrowdyServerObjectLibrary::GetServerObject(this, UCrowdyServerObjectLibrary::GetPlayersOnlineDefinition(), FString(), this, Error); ``` A project with the Game Kit's `world` starter has `world` as its root instead. `GetPlayersOnline` then reads the `Online` field of the world's `FCrowdyKitWorldConditions`, from the definition the **Game Kit World** project setting names; `UCrowdyServerObjectLibrary::GetGameKitWorldDefinition()` gives that definition, or null when the setting is empty or names no Game Kit starter. `FCrowdyKitWorldConditions` has a `bHasOnline` flag beside `Online`: when the world's snapshot carries no player count, as a world deployed from the platform's own starter pack does not, `GetPlayersOnline` returns false, with 0, for good, and the log says why once. See [The world becomes the app's root](../use-game-kit-starters.md#the-world-becomes-the-apps-root). An app last deployed with an older Crowdy SDK, or never deployed from Server Compute, has no Crowdy SDK root. Deploy once from Server Compute. Until then `GetPlayersOnline` returns false, and the log says why once for each reason in each game instance (so once per Play In Editor client): "This app has no Crowdy SDK root, so Players Online has no count: deploy the app from Server Compute (an app last deployed with an older Crowdy SDK, or by another tool, has none). It is asked again every minute." The root waits and asks again once a minute instead of every few seconds, and once the app is deployed the count arrives without restarting the game. ## Records A type whose **Holds** is **A Record per Entity** keeps the variables once for each entity; see [Keep a record per entity](../keep-a-record-per-entity.md). `WatchValues` and `GetState()` on its Server Object give the type's defaults, not a record. Use the records instead: - `HoldsRecords()` says whether the definition keeps records, `NumRecords()` counts them and `GetRecordIds()` lists their ids, in no particular order. - `GetRecord(Id)` returns an `FCrowdyServerRecord`, or null when there is no such record. Its `Values` is an `FInstancedStruct` holding the State struct, its `Owner` is the owning player's user id (0 for none) and its `Team` is the team. A variable the player may not receive holds its default. Ids are compared with capitals significant. - `OnRecordsChanged`, an `FOnCrowdyServerRecordsChanged`, runs after every read, batch or fetch that changed records. It gives the Server Object, the ids that are new or changed, the ids that were removed, and for each changed id the State fields that changed, one bit per field. `GetFieldVariableNames` names the fields by index. - `WatchRecord(Id, Listener)` follows one record with an `FOnCrowdyServerValuesChanged` listener that names the variables on each change and when the record is removed, and `UnwatchRecord(Id, Handle)` stops it. ```cpp const FCrowdyServerRecord* Record = Area->GetRecord(RaiderId); const FRaiderState* Raider = Record ? Record->Values.GetPtr() : nullptr; ``` The component on an actor reads the actor's own record: `GetVariables()` and `OnVariablesChanged` give it. ## Field names The names the listener receives are the fields' names on the server: the C++ member name (`Oil`, `bLit`), the name of a variable added in the asset, a Blueprint struct field's name with the spaces and other characters removed, or the Server Name you set under **Field Names** on the definition. Compare them exactly as they are spelled there. ## Related - [Create a Server Object type](../create-a-type.md): choosing which variables players see - [Hear its messages](./hear-server-messages.md): the next page, the messages a type sends to everyone listening - [Call its functions](./call-functions.md) - [Keep a record per entity](../keep-a-record-per-entity.md): a Server Object with a record for each entity of a crowd - [Read and follow its variables, from Blueprint](../from-blueprint/read-and-follow-variables.md): **On Changed**, **Watch Variables** and **On Variables Changed** - [Troubleshooting](../troubleshooting.md#a-value-is-not-what-you-expect): when a value is not what you expect - [The Two Planes](../../concepts/two-planes.md): why these values are the server's, not the client's # Hear its messages Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-cpp/hear-server-messages # Hear its messages :::note Server Objects run on [ck-exec](/exec/intro). ::: A message is the server telling everyone listening that something happened, such as the Hollow Giant falling. The [definition](../create-a-type.md#server-messages) lists a type's messages and its [server code](../write-server-logic.md#send-a-message) sends them. A message is an event and not a value: it reaches a game once, when it is sent, and nothing keeps it. To show what is true now, such as whether the boss is defeated, read a variable; see [Read and follow its variables](./read-and-follow-variables.md). ## Listen for a message `WatchMessage`, a function of `UCrowdyServerObject`, takes the message's **Name** from the definition and a listener, and returns an `FDelegateHandle`. The boss watches its Server Object right after acquiring it, and announces the fall: ```cpp // HollowGiant.h USTRUCT(BlueprintType) struct FBossDefeated { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) int64 Winner = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Hits = 0; }; void HandleBossMessage(UCrowdyServerObject* Object, FName Message, const FInstancedStruct& Payload); UPROPERTY(EditAnywhere, Category = "Crowdy") TObjectPtr BossDefinition; UPROPERTY(EditAnywhere, Category = "Crowdy") FString BossId = TEXT("boss-1"); UPROPERTY() TObjectPtr Boss; FDelegateHandle MessageHandle; // HollowGiant.cpp void AHollowGiant::BeginPlay() { Super::BeginPlay(); FString Error; Boss = GetGameInstance()->GetSubsystem()->Acquire(BossDefinition, BossId, this, Error); if (!Boss) { UE_LOG(LogTemp, Warning, TEXT("The Hollow Giant cannot be used: %s"), *Error); return; } MessageHandle = Boss->WatchMessage(TEXT("BossDefeated"), FOnCrowdyServerMessage::FDelegate::CreateUObject(this, &AHollowGiant::HandleBossMessage)); } void AHollowGiant::HandleBossMessage(UCrowdyServerObject* Object, FName Message, const FInstancedStruct& Payload) { const FBossDefeated* Defeated = Payload.GetPtr(); if (!Defeated) { return; } UE_LOG(LogTemp, Log, TEXT("Player %lld felled the Hollow Giant in %d hits"), Defeated->Winner, Defeated->Hits); } ``` The listener, an `FOnCrowdyServerMessage` delegate, receives the Server Object, the message's name and its payload as an `FInstancedStruct`. It runs once for each message the server sends, and never at any other time: not when the values first arrive, and not when you start watching. - **The payload** is the struct the definition's **Payload Struct** names, here `FBossDefeated`, with `Winner` and `Hits`. Read it with `GetPtr`, as above. For a message with no values the `FInstancedStruct` is empty, and `GetPtr` gives null. - **One listener can serve several messages.** Check `Message` to tell them apart; each `WatchMessage` call is for one message. - **`UnwatchMessage(Message, Handle)`** removes a listener. A listener bound to a `UObject`, as above, is skipped once that object is gone, so the boss does not need to remove its own; a lambda or raw binding does. - **`WatchMessage` gives an invalid handle and does nothing** when the definition has no message by that name, when the message has **Players Can Listen** off, when the listener is not bound, or when the Server Object is Released. The first two log a warning: "... has no message named BossDefeated, so nothing can listen to it", or "BossDefeated is heard only by other Server Object types; turn on Players Can Listen to hear it in a game". Check `MessageHandle.IsValid()` while you wire it up. When the definition's payload values are [added in the asset](../create-a-type.md#variables-inputs-and-outputs-or-a-struct) instead of taken from a struct, there is no struct to read. `CrowdyExec::ToList` from `CrowdyExecCodec.h` gives the payload as an `FInstancedPropertyBag` to read by name: ```cpp // HollowGiant.cpp #include "CrowdyExecCodec.h" const FInstancedPropertyBag Values = CrowdyExec::ToList(Payload); const TValueOrError Winner = Values.GetValueInt64(TEXT("Winner")); const TValueOrError Hits = Values.GetValueInt32(TEXT("Hits")); if (Winner.HasValue() && Hits.HasValue()) { UE_LOG(LogTemp, Log, TEXT("Player %lld felled the Hollow Giant in %d hits"), Winner.GetValue(), Hits.GetValue()); } ``` ## When a message is heard The Server Object listens to a message's topic while at least one listener wants it, from C++, from a Blueprint node or from a component, and the connection is open. When the last listener goes it stops listening, and after a dropped connection comes back it listens again. - **A message that arrives while the object is reading its values is held.** It is announced after the values are current, so a listener that reads `GetState()` sees the state the message followed. At most 32 are held; past that the oldest is dropped and the log warns. A message is announced only while the status is Ready; one that arrives while it is Connecting is held until then. - **A message sent while the connection is closed is lost.** Nothing is replayed when it comes back. The Server Object reads its values again and reports what differs through `WatchValues`, but a message is not a value and cannot be read again. - **A message in a record type can arrive before the record change it follows.** A type that keeps [a record per entity](../keep-a-record-per-entity.md) sends its records in batches, so a message sent right after a record changed may be announced before `OnRecordsChanged` reports it. Put what the listener needs into the payload. - **A refused listen is asked again.** If the server refuses to let the player listen, the log warns once with the platform's reason, "... could not listen to its BossDefeated message, so it is asked again after a wait: ", and the Server Object asks again after a wait that grows from 1 second to 30. The Server Object does not fail. - **A payload that does not fit its struct is dropped,** with a warning that names the message. - **Every message listened to is a subscription.** A connection allows 256 subscriptions in all, and each subscribe counts against the 120 calls a connection may make every 10 seconds. See [A message never arrives](../troubleshooting.md#a-message-never-arrives). ## Related - [Hear its messages, from Blueprint](../from-blueprint/hear-server-messages.md): **On Message Received**, **Watch Message** and **Stop Watching Message** - [Read and follow its variables](./read-and-follow-variables.md): the previous page - [Call its functions](./call-functions.md): the next page - [Server messages](../create-a-type.md#server-messages): defining a message and who hears it - [Send a message](../write-server-logic.md#send-a-message): what the server code does - [Shared boss fight](../examples/shared-boss.md): the boss these messages announce - [Troubleshooting](../troubleshooting.md#a-message-never-arrives): a message that never arrives # Call its functions Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-cpp/call-functions # Call its functions :::note Server Objects run on [ck-exec](/exec/intro). ::: A client never changes a Server Object's values. It asks, by calling one of the Server Functions its [definition](../create-a-type.md) lists, and the server's code decides what happens. Each function says who can call it, with [Callable By](../create-a-type.md#callable-by): **Players**, **Members**, **Leader** or **Server Only**, which refuses a player's call. A function can also have a **Cooldown**, and a number input a **Value Range**; see [Access, members and timers](../access-members-and-timers.md). The answer comes back to the caller; the change, if there is one, reaches every player through the [variables they can see](./read-and-follow-variables.md). ## Call one When the local player walks into the beacon, the beacon offers it ten units of oil: ```cpp // VillageBeacon.h virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; // VillageBeacon.cpp #include "LanternPlayer.h" void AVillageBeacon::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); const ALanternPlayer* Player = Cast(OtherActor); if (!Beacon || !Player || !Player->CrowdyEntity->IsLocallyOwned()) { return; } FFeedBeaconParams Params; Params.Oil = 10; TWeakObjectPtr WeakThis(this); Beacon->Call(TEXT("FeedBeacon"), FInstancedStruct::Make(Params), [WeakThis](const FCrowdyServerCallResult& Result) { if (!Result.IsSuccess()) { UE_LOG(LogTemp, Warning, TEXT("The beacon took no oil: %s"), *Result.Reason); return; } // The reply is what the server decided, which may be less oil than was offered. const FFeedBeaconReply* Reply = Result.Reply.GetPtr(); if (WeakThis.IsValid() && Reply) { WeakThis->Light->SetIntensity(Reply->Oil * 100.f); } }); } ``` `Call(Function, Params, OnDone)` takes: - **Function**: the function's **Name** on the definition, `FeedBeacon` here. - **Params**: an `FInstancedStruct` holding the function's Input Struct, made with `FInstancedStruct::Make`. For a function with no inputs, pass an empty `FInstancedStruct()`. A function whose inputs are added in the asset takes an `FInstancedPropertyBag` instead; see [below](#call-one-with-inputs-added-in-the-asset). - **OnDone**: runs once with an `FCrowdyServerCallResult`, on the game thread. It holds the `Outcome`, the `Reply` on success (empty for a function with no outputs), a `Reason` sentence for anything but success, `bRetryable`, which says calling again later may succeed, and `Code`, an `FName` that only a [Game Kit starter](../use-game-kit-starters.md#refusals-and-their-codes) fills with its refusal code, such as `full` with the outcome `Refused`, and is `None` otherwise. Things worth knowing before you rely on it: - **OnDone can run before `Call` returns.** A call the SDK can refuse without asking the server (an unknown function, params of the wrong struct, a Server Object that is already Failed or Released) completes at once, inside `Call`, and sends nothing. - **You can call while the Server Object is still Connecting.** The call waits and is sent when the connection opens. If there is still no connection after 30 seconds, it completes as `Unavailable` with `The server could not be reached`. - **The server may do less than you asked.** The beacon's reply is how much oil it holds after the feed, as the server decided; the params are only the offer. - **Capture your actor weakly.** OnDone can run after the actor that made the call is gone; the beacon checks a `TWeakObjectPtr` before it touches its light. - **Only a game instance shutdown skips OnDone.** Calls still waiting then are dropped without running it. Sign-out and a released Server Object complete them as `Canceled`, and the end of the player's access completes them as `AccessEnded`; see [When access ends](../troubleshooting.md#when-access-ends). ## Call one with inputs added in the asset When a function's inputs are [added in the asset](../create-a-type.md#variables-inputs-and-outputs-or-a-struct) rather than taken from a struct, there is no struct to make. `MakeParams(Function)` hands you a copy of the function's inputs with their default values; set the ones you want by name and pass it to the `Call` that takes an `FInstancedPropertyBag`. Outputs added in the asset are read by name too, through `CrowdyExec::ToList` from `CrowdyExecCodec.h`: ```cpp #include "CrowdyExecCodec.h" FInstancedPropertyBag Params = Beacon->MakeParams(TEXT("FeedBeacon")); Params.SetValueInt32(TEXT("Oil"), 10); Beacon->Call(TEXT("FeedBeacon"), Params, [](const FCrowdyServerCallResult& Result) { const TValueOrError Oil = CrowdyExec::ToList(Result.Reply).GetValueInt32(TEXT("Oil")); UE_CLOG(Result.IsSuccess() && Oil.HasValue(), LogTemp, Log, TEXT("The beacon holds %d"), Oil.GetValue()); }); ``` - `MakeParams` returns an empty `FInstancedPropertyBag` for a function whose inputs are a struct, or that has none. - The getters return a `TValueOrError`, so a name the reply does not have is an error, not a crash. - The two kinds do not mix: passing a struct to a function whose inputs are added in the asset completes as `BadRequest` with "FeedBeacon takes FeedBeaconParams params". ## What the SDK retries The SDK retries a call only when the server says it did not run it: a busy server. It tries up to three times in all, with a short wait between tries that grows each time (0.25 to 0.375 seconds, then 0.5 to 0.75) and is never shorter than the wait the server asks for, up to 30 seconds. Each wait is stretched by a random amount, up to half again, so many players do not retry in step. Only when those tries are spent does OnDone see `Busy`. Nothing else is retried, because the SDK cannot tell whether the server already ran the call. A connection that drops after the server ran a purchase, for example, looks the same from the client as one that dropped before, and sending it again could buy twice. So an unreachable server reaches OnDone as `Unavailable` with `bRetryable` set, a timeout or a crash reaches it on the first answer, a server that is switched off or paused reaches it as `Paused` with `bRetryable` set, and your game decides whether calling again is safe. Where you can, write a Server Function so that a repeated call does no harm: take an amount to reach rather than an amount to add, or let the caller send an id the server remembers and ignores the second time. The platform limits how often one player may call your app's code (see [call limits](/exec/operations#call-limits)); a call over the limit is answered as busy and retried the same way. Do not call a Server Function every frame. ## Give up sooner A call waits for the answer as long as the project's **Server Object Call Timeout** allows, 20 seconds unless you changed it; see [Project settings](../../reference/project-settings.md#server-object-timeouts). Both `Call` overloads take a trailing `float TimeoutSeconds`, default 0, which is a limit of its own for that one call. It is counted from the call, retries and waiting to send included, and it can only shorten the wait: a value above the project's timeout changes nothing, and 0 leaves only the project's. ```cpp FFeedBeaconParams Params; Params.Oil = 10; Beacon->Call(TEXT("FeedBeacon"), FInstancedStruct::Make(Params), [](const FCrowdyServerCallResult& Result) { UE_CLOG(Result.Outcome == ECrowdyServerCallOutcome::Timeout, LogTemp, Warning, TEXT("%s"), *Result.Reason); }, 5.f); ``` When it runs out, OnDone gets `Timeout` with `bRetryable` false and a `Reason` such as "No answer within 5 seconds; FeedBeacon may still have run on the server". The server is not told to stop, so the call may still take effect, and an answer that arrives later is not reported. Check the watched values before you call again. ## Ping the platform `UCrowdyServerObjectSubsystem::Ping(OnDone)` asks the platform for an answer over the connection the Server Objects already hold, and runs `OnDone(bOk, RoundTripMs, Reason)` once, on the game thread. `RoundTripMs` is how long the platform took to answer; `Reason` says why when `bOk` is false. ```cpp UCrowdyServerObjectSubsystem* Objects = GetGameInstance()->GetSubsystem(); Objects->Ping([](bool bOk, float RoundTripMs, const FString& Reason) { UE_CLOG(bOk, LogTemp, Log, TEXT("The platform answered in %.0f ms"), RoundTripMs); }); ``` It never opens a connection. With no Server Object connection open it fails at once, and OnDone runs before `Ping` returns, with "No Server Object connection is open". The platform answers at most 60 pings every 10 seconds on a connection and drops the rest without a word, so the SDK refuses more than that itself, with "Too many pings: the platform answers at most 60 every 10 seconds". Ping on a timer of a second or more, not every frame. From Blueprint it is the **Ping Server Objects** node. ## Bind the Blueprint call in C++ `Call` with a lambda is the way to call from C++. Code that uses the Blueprint node's action instead, `UCrowdyServerCallAction::CallServerFunction`, binds its **On Success** and **On Failed** pins with `AddDynamic`. Both are the dynamic delegate `FCrowdyServerCallDone`, declared in `CrowdyServerObjectLibrary.h`, and it has five parameters: the outputs, the outcome, the reason, whether calling again may succeed, and a Game Kit starter's refusal code. The handler is a `UFUNCTION` that takes all five: ```cpp UFUNCTION() void HandleFeedFailed(const FInstancedStruct& Outputs, ECrowdyServerCallOutcome Outcome, const FString& Reason, bool bRetryable, FName Code); ``` ```cpp UCrowdyServerCallAction* Feed = UCrowdyServerCallAction::CallServerFunction(this, Beacon, TEXT("FeedBeacon"), FInstancedStruct::Make(Params)); Feed->OnFailed.AddDynamic(this, &AVillageBeacon::HandleFeedFailed); Feed->Activate(); ``` The delegate had four parameters before the `FName Code` was added, so a handler written for it, without `Code`, no longer compiles: add the parameter. `Code` is `None` for every type but a [Game Kit starter](../use-game-kit-starters.md#refusals-and-their-codes). ## Outcomes `Outcome` is an `ECrowdyServerCallOutcome`. Every outcome, what it means and what to do about it, and the refusals that come from the server code generated for the type, are on [Troubleshooting](../troubleshooting.md#outcomes). ## Related - [Read and follow its variables](./read-and-follow-variables.md) - [Get a Server Object](./get-a-server-object.md#status) - [Call its functions, from Blueprint](../from-blueprint/call-functions.md): the **Call** node and **Call Server Function** - [Call a Server Function Library](./call-a-library.md): the next page, a type with no instance to hold - [Use the Game Kit starters](../use-game-kit-starters.md): the **Refused** outcome and its `Code` - [Troubleshooting](../troubleshooting.md#outcomes): every outcome, and the refusals from your server code - [ck-exec operations](/exec/operations): logs, including a crashed call's # Call a Server Function Library Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/from-cpp/call-a-library # Call a Server Function Library :::note Server Objects run on [ck-exec](/exec/intro). ::: A [Server Function Library](../server-function-libraries.md) has no instance to pick and no values to read, so a game does not acquire it and hold it as it does a Server Object. It calls a function by naming the definition: `CallLibraryFunction`, a static function of `UCrowdyServerObjectLibrary` in `CrowdyServerObjectLibrary.h`, in the `CrowdyExec` module. ## Call one The chest on the north road hands out a trinket when the local player walks into it. `LootRollerDefinition` is the `CSO_LootRoller` asset from [Make a Server Function Library](../server-function-libraries.md#make-the-asset), picked in the chest's Details panel, and `RollLoot` takes the structs below: ```cpp // Chest.h USTRUCT(BlueprintType) struct FLootRollInputs { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) int64 Seed = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = 0, ClampMax = 4)) int32 Tier = 1; }; USTRUCT(BlueprintType) struct FLootRollOutputs { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) FString Item; }; virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; UPROPERTY(EditAnywhere, Category = "Crowdy") TObjectPtr LootRollerDefinition; // Chest.cpp #include "LanternPlayer.h" void AChest::NotifyActorBeginOverlap(AActor* OtherActor) { Super::NotifyActorBeginOverlap(OtherActor); const ALanternPlayer* Player = Cast(OtherActor); if (!Player || !Player->CrowdyEntity->IsLocallyOwned()) { return; } FLootRollInputs Inputs; Inputs.Seed = 7; Inputs.Tier = 1; UCrowdyServerObjectLibrary::CallLibraryFunction(this, LootRollerDefinition, TEXT("RollLoot"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { if (!Result.IsSuccess()) { UE_LOG(LogTemp, Warning, TEXT("The chest rolled nothing: %s"), *Result.Reason); return; } const FLootRollOutputs* Reply = Result.Reply.GetPtr(); if (Reply) { UE_LOG(LogTemp, Log, TEXT("The chest holds %s"), *Reply->Item); } }); } ``` Walk into the chest and the log reads `The chest holds SilverRing:t1`. The library keeps nothing, so the same `Seed` and `Tier` give the same item every time. `CallLibraryFunction(WorldContext, Definition, Function, Inputs, OnDone, TimeoutSeconds)` takes: - **WorldContext**: any object in the game's world, such as the actor. - **Definition**: the library's definition asset. - **Function**: the function's **Name** on the definition, `RollLoot` here. - **Inputs**: an `FInstancedStruct` holding the function's Input Struct, made with `FInstancedStruct::Make`. For a function with no inputs, pass an empty `FInstancedStruct()`. - **OnDone**: runs once with an `FCrowdyServerCallResult`, on the game thread. It holds the `Outcome`, the `Reply` on success, a `Reason` for anything but success, and `bRetryable`, as for [a call to a Server Object](./call-functions.md#call-one). - **TimeoutSeconds**: optional. A limit of its own for this call that can only shorten the wait, as in [Give up sooner](./call-functions.md#give-up-sooner). A function whose inputs are [added in the asset](../create-a-type.md#variables-inputs-and-outputs-or-a-struct) takes an `FInstancedPropertyBag`, the second overload. `MakeParams` of the library's Server Object hands you a copy with the default values; set the ones you want by name. The subsystem gives you that object for an empty Instance Id, and `CallLibraryFunction` shares it: ```cpp // Chest.cpp UCrowdyServerObjectSubsystem* Objects = GetGameInstance()->GetSubsystem(); FString Error; UCrowdyServerObject* Library = Objects->Acquire(LootRollerDefinition, FString(), this, Error); if (Library) { FInstancedPropertyBag Inputs = Library->MakeParams(TEXT("RollLoot")); Inputs.SetValueInt64(TEXT("Seed"), 7); Inputs.SetValueInt32(TEXT("Tier"), 1); UCrowdyServerObjectLibrary::CallLibraryFunction(this, LootRollerDefinition, TEXT("RollLoot"), Inputs, [](const FCrowdyServerCallResult& Result) { const TValueOrError Item = CrowdyExec::ToList(Result.Reply).GetValueString(TEXT("Item")); UE_CLOG(Result.IsSuccess() && Item.HasValue(), LogTemp, Log, TEXT("The chest holds %s"), *Item.GetValue()); }); } ``` The reply reads by name through `CrowdyExec::ToList` from `CrowdyExecCodec.h`, as on [Call one with inputs added in the asset](./call-functions.md#call-one-with-inputs-added-in-the-asset). ## How it is held and answered - **The world holds the library.** The first call to a library connects it, and the game instance's `UCrowdyServerObjectSubsystem` keeps it, as its own owner, until the subsystem shuts down. There is nothing to release, and calls to the same library share one connection. Calling `Acquire` yourself works too and is ready as soon as the connection opens, but `CallLibraryFunction` needs none of it. - **OnDone runs exactly once.** A call that cannot be sent completes at once, inside `CallLibraryFunction`, so `OnDone` can run before it returns. Only a game instance shutdown skips it, for calls still waiting; sign-out completes them as `Canceled`. - **A refusal is an outcome, not a crash:** | Cause | Outcome | Reason names | |---|---|---| | A null definition, or a definition that is not a Server Function Library | `BadRequest` | the asset, "... is not a Server Function Library" | | A function the definition does not have | `BadRequest` | the type and the function | | No world, or a world with no game instance | `Unavailable` | that it needs a world with a game instance | `Definition->IsFunctionLibrary()` tells you whether an asset picked at run time is a library before you call it, and `Definition->Kind` is the `ECrowdyServerObjectKind` behind it. Every other outcome is a call's, listed on [Troubleshooting](../troubleshooting.md#outcomes): `Denied` for a function that is **Server Only** or **Developers Only**, `ServerError` for an input outside its **Value Range**, `Timeout`, `Paused` and the rest. - **Calls run side by side.** Two calls that are sent together are two independent calls, and the server may answer them on different instances, in either order. A library has no state to link them. - **The first call after the library has been idle can take over a second.** Your `OnDone` simply runs later. A call that goes on past the project's **Server Object Call Timeout**, 20 seconds unless you changed it, ends as `Timeout`. - **Capture your actor weakly,** as on [Call one](./call-functions.md#call-one): `OnDone` can run after the actor that made the call is gone. The chest's lambda touches nothing of the actor, so it captures nothing. ## Related - [Call a Server Function Library, from Blueprint](../from-blueprint/call-a-library.md): the **Call** node, which needs no Target - [Call its functions](./call-functions.md): the previous page, a call to a Server Object - [Make a Server Function Library](../server-function-libraries.md): the asset, **Kind** and **Scaling** - [Loot for the Hollow Giant](../examples/hollow-giant-loot.md): the loot roller, the chest and the boss - [Troubleshooting](../troubleshooting.md#outcomes): every outcome # Tip jar: your first Server Object Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/examples/tip-jar # Tip jar: your first Server Object :::note Server Objects run on [ck-exec](/exec/intro). ::: This page builds the village square's tip jar. Every villager can drop a coin in, and everyone sees the same running total. The total is worth keeping on the server, so nobody can tip a thousand coins from a modified client. You use only the editor and Blueprints; the one piece of code is a few lines of Rust that you paste. Before you start you need: - a project with the Crowdy SDK, on the **Dev** backend, where your players can sign in (see [Player sign-in](../../runtime/player-sign-in.md)); - Crowdy Studio signed in to your app, for the Deploy step (see [Server Compute](../deploy-with-server-compute.md#open-the-page)). ## Make the Server Object 1. In the Content Browser, click **Add**, then **Crowdy**. 2. Pick **Server Object**. The new asset is named `CSO_NewServerObject`. 3. Rename it `CSO_TipJar` and double-click it. The asset editor opens. 4. In the **Variables** section of the **Server Object** panel, click **+**. 5. In **Details**, set **Variable Name** to `Total`. 6. Leave its type on **Integer** and its **Default Value** on 0. 7. Leave the eye at the end of the `Total` row open, so it is **Visible to Players**. 8. In the **Functions** section, click **+**. 9. In **Details**, set **Name** to `Tip`. 10. Leave **Callable By** on **Players**. 11. In the **Inputs** header, click **+**, and rename the new `NewParam` to `Amount`. Leave it an Integer. 12. In the **Outputs** header, click **+**, and rename the new `NewParam` to `Total`. Leave it an Integer. 13. Select **Type Settings**, the first row of the **Server Object** panel. 14. In **Details**, set **Type Name** to `tip_jar`, or click **Use tip_jar** beside it. 15. Click **Save**. The panel now shows the variable `Total` with an open eye, and the function `Tip` with `(Amount) returns Total` beside it. The [definition asset page](../create-a-type.md) explains every setting. ## Write what Tip does 1. In the asset editor's toolbar, click **Generate**. It writes the server code under `Server/tip_jar/` in your project. 2. Open the **Server Code** tab. It shows `logic.rs`, where Tip refuses with "not written yet". 3. Click in the code, press **Ctrl+A** to select it all, and paste this over it: ```rust use crate::*; use ckx_sdk::prelude::*; impl Functions for TipJarState { fn tip(&mut self, _ctx: &Ctx, _call: &Call<'_>, params: TipParams) -> Result { if params.Amount < 1 || params.Amount > 100 { return Err(Error::new("a tip is 1 to 100 coins")); } self.Total = self.Total.saturating_add(params.Amount); Ok(TipReply { Total: self.Total }) } } ``` 4. Click **Save** on the tab. Tip adds the amount to `Total` and answers with the new total. It refuses a tip of 0 or of a thousand, and the server checks that, not the game. [Write its server logic](../write-server-logic.md) says what each line does. ## Put it on the server 1. In the asset editor's toolbar, click **Deploy**. It sends every Server Object type in the project, and asks first. 2. Read the dialog, which should say it adds `tip_jar`, and click **Yes**. 3. Wait for the status line to say "Deployed as version" and a number. Deploy builds the code on the server and makes it live for your app at once. If it fails, the build output opens, usually with a mistake in the pasted code. The [Server Compute](../deploy-with-server-compute.md#deploy) page has the rest. ## Make the actor 1. In the Content Browser, click **Add**, then **Blueprint Class**, then **Actor**. 2. Name it `BP_TipJar` and double-click it. 3. In the Components panel, click **Add** and choose **Crowdy Server Object**. 4. With the component selected, in **Details**, under **Server Object**, set **Definition** to `CSO_TipJar`. 5. Leave **Instance Mode** on **Instance Id**. 6. Set **Instance Id** to `lobby`. Every `BP_TipJar` with the Instance Id `lobby`, on every player's machine, now shares one tip jar. A different id is a different jar. ## Show the total 1. Open the **Event Graph**. 2. Right-click in the graph, type `Total`, and choose **On Total Changed (CSO_TipJar)** under **Server Objects**. 3. Drag from **Event BeginPlay**'s execution pin to the **Bind** pin of the new node. 4. Drag from the node's **Changed** pin, type `Print String` and choose it. 5. Drag from the **Total** pin of the node to **In String** of Print String. The editor adds the conversion from Integer to String. [Blueprint snippet: so-tipjar-total, see /bp/unreal-sdk/so-tipjar-total.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. The node runs **Changed** once as soon as the jar is ready, with the current total, and again after every change. You wired **Bind** once and there is nothing to unbind. ## Tip from the keyboard 1. Click **Class Defaults** in the toolbar. 2. In **Details**, under **Input**, set **Auto Receive Input** to **Player 0**, so this actor hears the local player's keys. 3. Back in the **Event Graph**, right-click, type `T`, and choose the **T** key event under **Input, Keyboard Events**. 4. Drag from its **Pressed** pin, type `Tip`, and choose **Call Tip (CSO_TipJar)**. 5. Set its **Amount** pin to 1. 6. Drag from **On Success**, type `Print String`, and choose it. 7. Drag from the **Total** output pin of Call Tip to its **In String**. 8. Click the small arrow at the bottom of **Call Tip** to show its **Reason** pin. Drag from **On Failed**, add another **Print String**, and drag **Reason** to its **In String**. 9. Click **Compile**, then **Save**. [Blueprint snippet: so-tipjar-tip, see /bp/unreal-sdk/so-tipjar-tip.txt] Neither node needs a **Target** wired: they use this actor's own component. **On Success** runs when the server has added the coin, with the new total. **On Failed** runs with the reason if the server refused, or could not be reached. The [Blueprint page](../from-blueprint/call-functions.md#the-call-node) says what **Outcome** and **Retryable** tell you. ## Play it with two players 1. Drag `BP_TipJar` from the Content Browser into your level. 2. Open the dropdown beside the Play button, choose **Advanced Settings**, and under **Multiplayer Options** set **Number of Players** to 2. 3. Click **Play**. Both windows are players of your app, so each has to be signed in the way your game signs players in. 4. Watch the two screens. Each prints the jar's total, `0` the first time, as soon as the jar is ready. 5. Click in one window and press **T**. 6. The window you pressed in prints `1` twice, once from **On Success** and once from **Changed**. The other window prints `1` once, from **Changed**. 7. Press **T** in the other window, and both print `2`. Both windows read the one jar on the server, and **On Total Changed** told each of them. The total is not kept anywhere in your Blueprint; the server holds it. ## Where to go next - Next: [a shared boss](./shared-boss.md). Many players hit one boss, the server refuses hits while it is defeated and brings it back on a timer, and you switch the type off and on from Server Compute to watch everyone recover. - Make a second variable, such as `LastTipper` as a String, keep its eye open, and use **On LastTipper Changed** the same way. Add it to `logic.rs` and Deploy again. After any change to the asset, **Generate** and **Deploy** again; see [After you change the definition](../write-server-logic.md#after-you-change-the-definition). - Let only a leader call a function, with **Callable By** set to **Leader**: [Access, members and timers](../access-members-and-timers.md). - If the whole game should share one jar, with no Instance Id to choose, tick **Only One Instance** in the asset: [Only One Instance](../access-members-and-timers.md#only-one-instance). - For a widget that shows the total with no actor, use **Find By Asset**: [Where the object comes from](../from-blueprint/get-a-server-object.md#where-the-object-comes-from). # Guild halls and arena lobbies Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/examples/guild-halls-and-arenas # Guild halls and arena lobbies :::note Server Objects run on [ck-exec](/exec/intro). ::: Two things Oakford's players do in groups, built from the settings on [Access, members and timers](../access-members-and-timers.md). Each shows the settings on the asset, the few lines of `logic.rs` that are left, and the Blueprint side. ## Guild halls Oakford's players are seated in guild halls of five. A hall keeps a coffer of gold that only its five may add to. A registry seats each new player in the hall that has room, and the game shows the player their own hall. Three types do it. **`guild_hall`**, one per hall, with an Instance Id such as `hall-1`: | Setting | Value | |---|---| | Members From | This Object | | Max Members | 5 | | Readable By | Members | | Variables | `Gold`, Integer, Visible to Players | | Function `Deposit` | Callable By **Members**, Input `Amount` Integer with a **Value Range** of 1 to 100, Cooldown 2 | **`player_team`**, one per player, whose Instance Id is the player's user id: | Setting | Value | |---|---| | Readable By | Owner Only | | Variables | `TeamId`, String, Visible to Players | | Function `SetTeam` | Callable By **Server Only**, Input `TeamId` String | **`hall_registry`**, one for the village, with the Instance Id `oakford`: | Setting | Value | |---|---| | Readable By | Every Player | | Variables | `Halls` and `Seated`, Integers, and `Seats`, a Map of String to String (player to hall), all Server Only | | Function `Assign` | Callable By **Players**, no Inputs, Output `TeamId` String | | Can Call | `guild_hall`, `player_team` | ### The logic The hall and the team object need almost nothing. `Deposit` is one line, since the server already checked that the caller is a member, that `Amount` is 1 to 100 and that the player is not depositing too fast: ```rust fn deposit(&mut self, _ctx: &Ctx, _call: &Call<'_>, params: DepositParams) -> Result<()> { self.Gold += params.Amount; Ok(()) } ``` `SetTeam` on `player_team` stores `params.TeamId` in `self.TeamId`. The registry seats the player and tells their object: ```rust fn assign(&mut self, ctx: &Ctx, call: &Call<'_>) -> Result { let player = call.player()?; // A player who already has a hall keeps it, however often Assign is called. if let Some(hall) = self.Seats.get(&player.to_string()) { return Ok(AssignReply { TeamId: hall.clone() }); } if self.Halls == 0 || self.Seated >= 5 { self.Halls += 1; self.Seated = 0; } let hall = format!("hall-{}", self.Halls); calls::guild_hall::add_member(ctx, &hall, &calls::guild_hall::MemberInputs { Player: player })?; let team = calls::player_team::SetTeamParams { TeamId: hall.clone() }; calls::player_team::set_team(ctx, &player.to_string(), &team)?; self.Seated += 1; self.Seats.insert(player.to_string(), hall.clone()); Ok(AssignReply { TeamId: hall }) } ``` `Add Member` is the built-in function that only server code may call, so a player cannot seat themselves. `Seats` makes `Assign` safe to call twice: the second call gives back the same hall. ### The Blueprint side 1. On the village's hall board, `BP_HallBoard`, add a **Crowdy Server Object** component. Set **Definition** to `CSO_HallRegistry`, **Instance Mode** to **Instance Id**, and **Instance Id** to `oakford`. When a player presses the board, call **Call Server Function** on its Server Object with **Function** `Assign`. 2. On the player's character or HUD, add a second **Crowdy Server Object** component. Set **Definition** to `CSO_PlayerTeam` and **Instance Mode** to **Signed-In Player**. Its `TeamId` says which hall the player is in. 3. On the hall's own actor, `BP_GuildHall`, add a third component. Set **Definition** to `CSO_GuildHall`, **Instance Mode** to **From Server Value**, **Source Definition** to `CSO_PlayerTeam`, **Source Instance** to **Signed-In Player** and **Source Variable** to `TeamId`. While the player has no hall, `TeamId` is empty and the component waits ("Waiting for TeamId"). When `Assign` sets it, the component joins `hall-1` by itself, and follows the player if they are ever seated elsewhere. ![From Server Value: Source Definition CSO_PlayerTeam, Source Instance Signed-In Player, Source Variable TeamId](/img/unreal-sdk/server-object-component-from-value.png) 4. In the hall's **On Variables Changed**, call **Get Variables** and **Get Server Value** with **Name** `Gold` to show the coffer. **Get Member Count** and **Get Members** show who is in the hall. 5. To give gold, call **Call Server Function** with **Function** `Deposit` and Inputs made with **Make Inputs** and **Set Server Value** (`Amount`). A player from another hall gets **On Failed** with **Outcome** Denied and the Reason "only members may call Deposit", and reads no `Gold` at all. A second deposit within two seconds gets Denied with **Retryable** true. If your halls were Crowdy Teams instead, set `guild_hall`'s **Members From** to **Crowdy Team**, use **Player's Team** for the component, and let the Teams nodes seat players; the registry is no longer needed. See [Crowdy Team](../access-members-and-timers.md#crowdy-team). ## The arena lobby Oakford's arena takes eight players. They gather in a lobby, the first to arrive leads, the leader starts the match, nobody can join once it has begun, and ten minutes later it ends by itself. One type, **`arena_match`**, with the Instance Id `oakford-arena`: | Setting | Value | |---|---| | Members From | This Object | | Max Members | 8 | | Readable By | Members | | Variables | `Phase`, String, starting at `Lobby`, Visible to Players | | Function `StartMatch` | Callable By **Leader**, no Inputs | | Timer | `EndMatch`, **Runs** **Once After**, **Time** 600 seconds, **Start Automatically** off | Change **Readable By** to **Every Player** if spectators should see `Phase`. As it stands, someone who is not in the lobby sees how many are in it, who leads and whether it is open, and can **Join**. ### The logic ```rust fn start_match(&mut self, ctx: &Ctx, _call: &Call<'_>) -> Result<()> { if self.Phase != "Lobby" { return Err(Error::new("the match has already started")); } members::set_open(false); self.Phase = "Running".into(); timers::start(ctx, timers::END_MATCH) } fn end_match(&mut self, _ctx: &Ctx) -> Result<()> { self.Phase = "Ended".into(); Ok(()) } ``` Nothing checks that the caller is the leader: **Callable By** did that. Nothing counts eight players: **Max Members** did. Closing joining is `members::set_open(false)`, so **Join** is refused with "It is not open for joining" from then on. ### The Blueprint side 1. On `BP_ArenaLobby`, add a **Crowdy Server Object** component with **Definition** `CSO_ArenaMatch`, **Instance Mode** **Instance Id**, **Instance Id** `oakford-arena`. 2. A **Join** button calls **Call Server Function** with **Function** `Join` and no Inputs. Under **On Failed**, show the **Reason**: "It is full", or "It is not open for joining". In the figure, the button is a custom event, **OnJoinPressed**, that takes **Get Server Object** from the lobby's component (the figure's variable is named `CrowdyServerObject`), calls **Call Server Function** with **Function** `Join`, and prints the **Reason** from **On Failed**. [Blueprint snippet: so-arena-join, see /bp/unreal-sdk/so-arena-join.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. 3. In **On Variables Changed**, call **Get Member Count** to show "5 of 8", **Is Open For Joining** to hide the Join button once it closes, and **Is Leader** to show a **Start** button to the leader only. 4. **Start** calls **Call Server Function** with **Function** `StartMatch`. A player who is not the leader is refused with Denied, "only the leader may call StartMatch", even from a modified client. 5. When `Phase` changes to `Ended`, **On Variables Changed** runs again and the game shows the result. **Get Leader** gives the leader's user id, for a name tag. If the leader leaves the lobby, the member who has been in it longest leads, and **Is Leader** changes for both of them. A lobby for every match rather than one arena needs a registry that finds a lobby with room, as the guild halls have. ## Related - [Access, members and timers](../access-members-and-timers.md): every setting used here - [Relate Server Objects to each other](../relate-server-objects.md): registries, team objects and lobbies as relations, and good practice - [Get a Server Object, from Blueprint](../from-blueprint/get-a-server-object.md#add-the-component): the component modes - [Read and follow its variables, from Blueprint](../from-blueprint/read-and-follow-variables.md#by-name): the member nodes - [Write its server logic](../write-server-logic.md): where `logic.rs` lives # Shared boss fight Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/examples/shared-boss # Shared boss fight :::note Server Objects run on [ck-exec](/exec/intro). ::: The Hollow Giant stalks Oakford's north road. Every player who passes can hit it, and all of them see the same health bar fall. Its health is the kind of number a client must never decide, so it lives on the server as a Server Object: one boss, however many players are hitting it. This page builds it end to end. It uses what the earlier pages explain one piece at a time: the [definition asset](../create-a-type.md), [access and timers](../access-members-and-timers.md), the [server code](../write-server-logic.md), [Server Compute](../deploy-with-server-compute.md) and the [Blueprint nodes](../from-blueprint/get-a-server-object.md). If you have not made a Server Object before, start with [Tip jar: your first Server Object](./tip-jar.md). The rules of the fight: - Every hit takes 1 to 25 health off the boss, whoever lands it. - A player can hit about four times a second. - At half health the boss is **Enraged**. At zero it is **Defeated**, and hits are refused. - Ten seconds after it falls, it comes back at full health. ## What the server keeps The boss has six values, and every player may see all of them. | Value | Type | Starts at | What it is | |---|---|---|---| | `Health` | Integer | 100 | What is left | | `MaxHealth` | Integer | 100 | What a full bar is | | `Phase` | `EBossPhase` | Alive | Alive, Enraged or Defeated | | `Respawns` | Integer | 0 | How many times it has come back | | `Hits` | Integer | 0 | Hits accepted, over every life | | `LastHitBy` | Integer64 | 0 | The user id of the last player to hit it | You can keep them in structs, as the C++ pages do, or as variables added right in the asset. The server code is the same either way. The structs: ```cpp UENUM(BlueprintType) enum class EBossPhase : uint8 { Alive, Enraged, Defeated }; USTRUCT(BlueprintType) struct FBossState { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Health = 100; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 MaxHealth = 100; UPROPERTY(EditAnywhere, BlueprintReadWrite) EBossPhase Phase = EBossPhase::Alive; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Respawns = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Hits = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int64 LastHitBy = 0; }; USTRUCT(BlueprintType) struct FBossHitInputs { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = 1, ClampMax = 25)) int32 Damage = 10; }; USTRUCT(BlueprintType) struct FBossHitOutputs { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Health = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Hits = 0; }; ``` The `ClampMin` and `ClampMax` are the function's [Value Range](../access-members-and-timers.md#value-range), 1 to 25. With no C++, add the same six as **Variables** in the asset instead, with `Phase` picking your enum (a Blueprint enum works too), and give `Hit` one **Input**, `Damage`, and two **Outputs**, `Health` and `Hits`. Set the range under **Value Range** in the input's Details, **Min** 1 and **Max** 25. The rest of this page says where the two differ. ## The asset Make a **Crowdy Server Object Definition**, `CSO_Boss`, as on [Create a Server Object type](../create-a-type.md#make-the-asset). With **Type Settings** selected in the Server Object panel, in Details: | Setting | Value | |---|---| | Type Name | `boss` | | Readable By | **Every Player** | | State Struct | `FBossState`, if you use structs | Leave **Members From** on **None**: nobody is seated in a boss, and anyone may hit it. Leave every eye on the variables open, **Visible to Players**. **Every Player** suits a boss that a whole world fights. With **Members** or **Owner Only** the server tells each watcher that something changed and each one reads the values again, which is slower and means a burst of hits can be seen as fewer, bigger steps. See [Readable By](../access-members-and-timers.md#readable-by). Add a function, `Hit`: | Setting | Value | |---|---| | Callable By | **Players** | | Cooldown | 0.25 | | Inputs | `FBossHitInputs` (**Use Struct**), or one Integer, `Damage`, starting at 10, with a **Value Range** of 1 to 25 | | Outputs | `FBossHitOutputs` (**Use Struct**), or two Integers, `Health` and `Hits` | **Cooldown** is per player: one player can hit every quarter second, and another player's hits do not count against them. Add a timer under **Timers & Events**: | Setting | Value | |---|---| | Name | `Respawn` | | Runs | **Once After** | | Time | 10 seconds | | Start Automatically | **off** | Its row reads "Respawn: Once After 10 s". Start Automatically is off because the boss starts alive. The server code starts the timer when the boss falls. Save the asset. ## The server code Click **Generate** in the asset editor's toolbar, which writes `Server/boss/`. `Hit` and `Respawn` both arrive in `logic.rs` as methods that refuse with "not written yet". Replace the file with this: ```rust use crate::*; use ckx_sdk::prelude::*; impl Functions for BossState { fn hit(&mut self, ctx: &Ctx, call: &Call<'_>, params: BossHitInputs) -> Result { if self.Phase == BossPhase::Defeated { return Err(Error::new("The boss is defeated; wait for it to respawn")); } self.Health = (self.Health - params.Damage).max(0); self.Hits = self.Hits.saturating_add(1); self.LastHitBy = call.player().map(|id| id as i64).unwrap_or(0); self.Phase = match self.Health { 0 => BossPhase::Defeated, health if health * 2 <= self.MaxHealth => BossPhase::Enraged, _ => BossPhase::Alive, }; if self.Health == 0 { timers::start(ctx, timers::RESPAWN)?; } Ok(BossHitOutputs { Health: self.Health, Hits: self.Hits }) } fn respawn(&mut self, _ctx: &Ctx) -> Result<()> { // A timer that fires twice, or on a boss that is not down, changes nothing. if self.Phase != BossPhase::Defeated { return Ok(()); } self.Health = self.MaxHealth; self.Phase = BossPhase::Alive; self.Respawns += 1; Ok(()) } } ``` If you added the values in the asset rather than as structs, the generated names are `BossState` (the same), `HitParams` and `HitReply` in place of `BossHitInputs` and `BossHitOutputs`; see [The Rust names](../write-server-logic.md#the-rust-names). What it does, in the order it does it: - **It refuses while the boss is defeated.** The function returns an error, so the state is put back exactly as it was and nothing is published. The caller gets the `ServerError` outcome with that message. A second player who hits in the same moment the boss falls gets the refusal too. - **It clamps at zero.** A 25 at 10 health leaves 0, not -15. - **It counts the hit and remembers who landed it.** `Hits` goes up by one, over every life of the boss. - **It sets the phase from what is left.** Half of `MaxHealth` or less is Enraged, and zero is Defeated. - **At zero it starts `Respawn`.** The timer starts only because this call succeeded. Had the call been refused, no timer would have been armed. If the platform refuses to start the timer, the whole hit is refused instead: the boss keeps its health, nothing is published, and the player sees `ServerError` with "the change was not kept: timer Respawn could not start" and the reason. - **`respawn` restores full health, but only for a boss that is down.** It runs once, ten seconds after the boss fell, and its changes reach every watcher like a function's do. Called on a boss that is not Defeated, for example by a stale or repeated timer, it does nothing. Nothing here checks that `Damage` is 1 to 25, that the caller is a player or that they are not hitting too fast. The **Value Range** and **Cooldown** on the asset are enforced before `hit` runs. ## Put it on the server Open Crowdy Studio and choose **Server Compute**, as on [Open the page](../deploy-with-server-compute.md#open-the-page). The **Overview** tab lists `boss` with the badge **New**. Click **Deploy**, read the dialog, which should say it adds `boss`, and click **Yes**. Wait for "Deployed as version" and a number. See [Deploy](../deploy-with-server-compute.md#deploy). If you change the asset or the code later, **Generate** and **Deploy** again. ## The actor The boss is an actor with a **Crowdy Server Object** component: **Definition** `CSO_Boss`. What to set in **Instance Mode** depends on how the boss gets into the level. - **This Actor**, for a boss you place in the level. The component uses the actor's placement, which is the same on every player's machine, so every player who loads the level meets the same boss. No id to type. - **Instance Id**, for a boss your game spawns at run time, the usual case for a boss that appears when a fight starts. Type an id such as `boss-1`; every copy with that id, on every machine, is the same boss. Set it before the actor finishes spawning, or call **Rejoin** on the component afterwards. A new id is a new boss, at full health, with nothing saved. - **From Server Value**, for a boss per match. Point **Source Definition** at the match's Server Object and **Source Variable** at a String such as `MatchId`; the boss follows it, as the guild hall follows its player's team. See [Guild Halls](./guild-halls-and-arenas.md#guild-halls). For this page, place the actor in the level and use **This Actor**. Name it `BP_Boss`: in the Content Browser, **Add**, **Blueprint Class**, **Actor**, then open it, click **Add** in the Components panel, choose **Crowdy Server Object**, set **Definition** to `CSO_Boss` and **Instance Mode** to **This Actor**. Drag it into your level. ## The Blueprint Every node here is a typed node from [The typed nodes](../from-blueprint/get-a-server-object.md#the-typed-nodes), and none needs a **Target**: they use the actor's own component. **Show the health.** In the Event Graph: 1. Add **On Health Changed (CSO_Boss)** and wire **Event BeginPlay** into its **Bind**. 2. Wire **Changed** to a **Set Percent** on your health bar widget, with **Health** divided by **Get MaxHealth (CSO_Boss)**. The figure converts both to floats first. [Blueprint snippet: so-boss-health, see /bp/unreal-sdk/so-boss-health.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. 3. In the same way, bind **On Phase Changed (CSO_Boss)** from **Event BeginPlay**, and from its **Changed** show the **Phase**: the figure sends it through **Enum to String** into a **Print String**, and a tint on the bar that turns red for Enraged and grey for Defeated works as well. 4. Bind **On Respawns Changed (CSO_Boss)** as well, from the **Then** pin of **On Phase Changed**, and show "It is back." from its **Changed** with a **Print String**. [Blueprint snippet: so-boss-phase, see /bp/unreal-sdk/so-boss-phase.txt] **Changed** runs at once, with the current value, as soon as the object is ready, so the bar is right for a player who arrives in the middle of the fight. A widget with no component can read the same boss through **Find By Asset**, using **Get Health (CSO_Boss)** in its own graph; see [A widget that shows the oil](../from-blueprint/read-and-follow-variables.md#a-widget-that-shows-the-oil), which works the same way. **Hit it.** Give the actor **Auto Receive Input** on **Player 0**, as in [Tip jar: your first Server Object](./tip-jar.md#tip-from-the-keyboard), then: 1. Add the **F** key event, and from **Pressed** add **Call Hit (CSO_Boss)**. 2. Set its **Damage** pin to 10. 3. From **On Success**, add a **Print String** that builds "Health " and the **Health** output, the boss's health after your hit, with **Build String**. The call also gives **Hits**, the hit count; the figure leaves it unwired. 4. Click the small arrow at the bottom of **Call Hit** to show its **Reason** pin, then from **On Failed** add a **Print String** and wire **Reason** to it. Compile and save. [Blueprint snippet: so-boss-hit, see /bp/unreal-sdk/so-boss-hit.txt] ## Two players in Play In Editor 1. In **Advanced Settings** under **Multiplayer Options**, set **Number of Players** to 2 and click **Play**. Sign both windows in. 2. Both show a full bar. 3. Press **F** in one window. Its bar drops to 90, and it prints the success line with `Health 90`. The other window's bar drops to 90 too, a moment later. 4. Press **F** in the other window. Both go to 80. 5. Tap **F** in one window faster than four times a second. A hit sooner than that after your last one runs **On Failed** with the **Outcome** Denied and a **Reason** that says how long to wait, such as "Hit can be called again in 1 s". **Retryable** is true for that one, and the boss does not change. 6. Keep hitting from both windows. Below 50 health both windows turn Enraged. At 0 both show Defeated. While it is defeated, another **F** runs **On Failed** with the **Outcome** ServerError and the **Reason** "The boss is defeated; wait for it to respawn", and **Retryable** is false: calling again will be refused the same way until something changes. Ten seconds after it fell, both windows show a full bar, Alive, and **Respawns** 1. **Hits** is not reset: it counts every hit the boss has taken. To see the range at work, set **Damage** to 30 or 0. The server refuses it before `hit` runs: **On Failed** gets the **Outcome** ServerError and the **Reason** `bad_params: Damage must be 1 to 25`, **Retryable** is false, and the boss does not change. ## Switch it off and on Open **Server Compute** and look at the `boss` row on the **Overview** tab while both players are in the fight. 1. Click **Switch off** and confirm "Switch off boss". Its Server Objects are saved and stopped. 2. Press **F** in either window. The call is refused as **Paused**, and nothing changes for either player. Each window keeps showing the last health it had. 3. Click **Switch on**. There is no dialog. 4. Press **F** again. The boss starts again from what was saved, both windows follow it again, and the next hit lands on the same health it had before you switched off. Nothing is lost and nothing has gone back. If the boss was defeated when you switched it off, its `Respawn` timer is still pending: it fires once after the type is back, and both windows see the boss return. To move a running boss onto code you have just deployed without refusing any hits, press **Restart** on the `boss` row instead: it is saved, stopped and started again on the new code at the next hit, and both windows read it again. See [Restarting a type](../deploy-with-server-compute.md#restarting-a-type). ## What the player sees when the boss falls The hit that takes it to 0 succeeds for the player who landed it, with `Health` 0. Everyone watching gets one change with `Health` 0 and `Phase` Defeated, and their bars empty together. `LastHitBy` is that player's user id, so the game can name who landed the blow. From then until the respawn, every hit is refused, from every player, with the same message and not retryable. A game should turn the **F** prompt off on **Defeated** rather than let players fire into it. Ten seconds later the server runs `respawn` and every player gets one change: full health, Alive, `Respawns` up by one. If something does not behave, turn on [`crowdy.exec.trace`](../troubleshooting.md#seeing-what-a-server-object-is-doing) in both windows. Each accepted hit shows as one applied push with the same epoch and sequence number in both logs, which is the quick way to tell whether the two players really saw the same thing. A dropped connection shows as `exec: connection closed`, then `exec: redial ok` and a re-read. ## Related - [Tip jar: your first Server Object](./tip-jar.md): the same flow from nothing, with a tip jar - [Guild halls and arena lobbies](./guild-halls-and-arenas.md): members, a registry and a lobby - [Access, members and timers](../access-members-and-timers.md): every setting used here - [Write its server logic](../write-server-logic.md): where `logic.rs` lives and what the generated code does for you - [Server messages](../create-a-type.md#server-messages): announcing the boss's fall with a `BossDefeated` message, heard [from Blueprint](../from-blueprint/hear-server-messages.md) and [from C++](../from-cpp/hear-server-messages.md) - [Loot for the Hollow Giant](./hollow-giant-loot.md): a Server Function Library the boss calls when it falls - [Deploy it with Server Compute](../deploy-with-server-compute.md): Deploy, versions and the switch - [Get a Server Object, from C++](../from-cpp/get-a-server-object.md): the statuses and what a dropped connection does - [Troubleshooting](../troubleshooting.md#seeing-what-a-server-object-is-doing): `crowdy.exec.trace` # Raiders on the north road Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/examples/raiders-on-the-north-road # Raiders on the north road :::note Server Objects run on [ck-exec](/exec/intro). Records per entity are new in v2.19.0; see [What's Changed](../../guides/whats-changed.md#2026-10-12-sdk-v2190). ::: A warband camps on Oakford's north road, a short walk from the Hollow Giant. There are far too many raiders for a Server Object each, so the warband is one Server Object per patch of road with a record for every raider in it. Every player sees how hurt each raider is. A player can hire a raider, and then only that player sees what the raider carries. Raiders belong to one of two bands, and a player who hires a raider learns where that band hides. This page builds it end to end. It uses [Keep a record per entity](../keep-a-record-per-entity.md), which explains each setting, and the pages that earlier examples use: the [definition asset](../create-a-type.md), the [server code](../write-server-logic.md), [Server Compute](../deploy-with-server-compute.md) and the [Blueprint nodes](../from-blueprint/get-a-server-object.md). If you have not made a Server Object before, start with [Tip jar: your first Server Object](./tip-jar.md). The rules of the road: - Each raider has 40 health and loses 5 every 5 seconds to the skirmishes on the road. A raider at 0 health retreats, and comes back at full health a moment later. - A player can hire one raider at a time by pressing a key. The server picks the first raider nobody has hired. - A hired raider collects loot every 5 seconds, and only its player sees the loot. - Raiders belong to the ravens or the wolves. Each band has a hideout. A player who has hired a raider of a band can see that band's hideout, and nobody else can. ## What the server keeps Each raider has a record with four values. | Value | Type | Starts at | Who receives it | |---|---|---|---| | `Health` | Integer | 40 | Everyone | | `Loot` | Integer | 0 | Owner Only: the player who hired the raider | | `Hideout` | String | empty | Team Only: players who have hired a raider of the same band | | `Greed` | Integer | 2 | Nobody (Server Only): how much loot a hired raider collects at each skirmish | Besides these, every record has an owner, the player who hired the raider or 0, and a team, `ravens` or `wolves`. The server code sets them. They are not variables of the type. Add the four as **Variables** in the asset, as the [tip jar](./tip-jar.md) does. With a struct instead, `FRaiderState` has the same four members, and the server code is the same. ## The asset Make a **Crowdy Server Object Definition**, `CSO_Raider`, as on [Create a Server Object type](../create-a-type.md#make-the-asset). With **Type Settings** selected, in Details: | Setting | Value | |---|---| | Type Name | `raider` | | Holds | **A Record per Entity** | | Readable By | **Every Player** | | Reads World Data | On | | Area Size | 2 | Leave **Max Records** at 2048, **Send Changes Every** at 250 ms, **Areas Followed Around a Player** at 1 and **Team From** on **This Object's Records**. Leave **Members From** on **None**; a record type cannot have members. **Reads World Data** is on because the server code asks which raiders stand in the area. **Area Size** is 2 because the census reads every chunk of the area, and a small area keeps that to four chunks. With a larger one, the same code just reads more. Add the variables. For each, select it in the Server Object panel and, in Details, set **Visible to Players**: | Variable | Type | Default | Visible to Players | |---|---|---|---| | `Health` | Integer | 40 | **Everyone** | | `Loot` | Integer | 0 | **Owner Only** | | `Hideout` | String | empty | **Team Only** | | `Greed` | Integer | 2 | **Server Only** | Add a function, `Hire`: | Setting | Value | |---|---| | Callable By | **Players** | | Cooldown | 1 | | Inputs | none | | Outputs | none | Add two timers under **Timers & Events**: | Name | Runs | Time | Start Automatically | |---|---|---|---| | `Census` | **Every** | 2 seconds | On | | `Skirmish` | **Every** | 5 seconds | On | Their rows read "Census: Every 2 s" and "Skirmish: Every 5 s". Save the asset. ## The server code Click **Generate** in the asset editor's toolbar, which writes `Server/raider/`. Replace `logic.rs` with this: ```rust use crate::*; use ckx_sdk::prelude::*; use std::collections::BTreeSet; const FULL_ANSWER: usize = 200; fn area_origin(key: &str) -> Option<(i64, i64)> { let (x, y) = key.split_once(',')?; Some((x.parse::().ok()? * AREA_SIZE, y.parse::().ok()? * AREA_SIZE)) } impl Functions for Records { fn hire(&mut self, _ctx: &Ctx, call: &Call<'_>) -> Result<()> { let player = call.player()?; let free = self.ids().find(|id| self.owner(id) == Some(0)).map(str::to_string); let Some(id) = free else { return Err(Error::new("every raider here is hired")); }; self.set_owner(&id, player) } fn census(&mut self, ctx: &Ctx) -> Result<()> { let Some((x, y)) = area_origin(&ctx.key) else { return Ok(()) }; let mut found = BTreeSet::new(); let mut full = false; for cx in x..x + AREA_SIZE { for cy in y..y + AREA_SIZE { let Ok(actors) = ctx.world().actors(ChunkPos::new(cx, cy, 0)) else { return Ok(()) }; full |= actors.len() >= FULL_ANSWER; found.extend(actors.into_iter().filter(|actor| actor.player.is_none()).map(|actor| actor.uuid)); } } if !full { let gone: Vec = self.ids().filter(|id| !found.contains(*id)).map(str::to_string).collect(); for id in gone { self.remove(&id); } } for id in found { if self.get(&id).is_some() { continue; } let (team, hideout) = if self.len() % 2 == 0 { ("ravens", "the old mill") } else { ("wolves", "the quarry") }; let raider = RaiderState { Hideout: hideout.to_string(), ..Default::default() }; if self.insert(&id, 0, team, raider).is_err() { break; } } Ok(()) } fn skirmish(&mut self, _ctx: &Ctx) -> Result<()> { let ids: Vec = self.ids().map(str::to_string).collect(); for id in ids { let hired = self.owner(&id).is_some_and(|owner| owner != 0); let Some(raider) = self.get_mut(&id) else { continue }; raider.Health -= 5; if hired { raider.Loot += raider.Greed; } if raider.Health <= 0 { self.remove(&id); } } Ok(()) } } ``` If you used a struct, `RaiderState` is the struct's name without its `F`; see [The Rust names](../write-server-logic.md#the-rust-names). What it does: - **`hire` gives the caller the first raider nobody owns.** `set_owner` makes the caller its owner. The raider keeps its band as its team, so the caller is now on that band's team. If every raider is hired the call is refused with that message. - **`census` makes a record for each raider standing in the area.** It reads each of the area's four chunks at ground level, collects the actors no player owns, and adds a record, owned by nobody, for each one it does not have. Raiders alternate between the two bands as they are added. It removes the records of raiders that left the area, unless a chunk's answer was full: one call answers at most 200 actors, so a full answer may be missing some, and removing a record for it would remove a raider who is still there. - **`skirmish` wounds every raider and pays the hired ones.** A hired raider adds `Greed` to its `Loot`. A raider at 0 health is removed, and with it the hire; the next census adds a new record for the same raider, at full health and owned by nobody. - **The ids are the raiders' entity ids.** The census call reports each actor's `uuid`, and that is the id the raider's own actor uses to find its record. Nothing here checks who may hire. **Callable By** and **Cooldown** on the asset do that before `hire` runs. ## Put it on the server Open Crowdy Studio and choose **Server Compute**, as on [Open the page](../deploy-with-server-compute.md#open-the-page). The **Overview** tab lists `raider` with the badge **New**. Click **Deploy**, read the dialog, which should say it adds `raider`, and click **Yes**. Wait for "Deployed as version" and a number. See [Deploy](../deploy-with-server-compute.md#deploy). The type asks for the `world.read` permission, since **Reads World Data** is on. ## The actors Two actors use the type. **The raider.** `BP_Raider` is a Crowdy entity, so it has a **Crowdy Entity** component; see [Entities and Spawning](../../runtime/entities-and-spawning.md). Its id is the id of its record. Add a **Crowdy Server Object** component, set **Definition** to `CSO_Raider` and **Instance Mode** to **This Actor's Area**. The component reads the raider's own record and follows the raider as it walks from one area into the next. Add a health bar widget, with a Progress Bar variable named `HealthBar`. Spawn the warband from your game mode with **Spawn Crowdy Entity** and the class `BP_Raider`, on the road. **The player.** Give the player's pawn a **Crowdy Server Object** component too, with **Definition** `CSO_Raider` and **Instance Mode** **This Actor's Area**. The pawn is a Crowdy entity already, and its own area is where **Hire** is called. The census skips actors that belong to a player, so the pawn has no record of its own and the component's variables stay at their defaults. The pawn is only here to reach the area's object. Give it **Auto Receive Input** on **Player 0**, as in [Tip jar: your first Server Object](./tip-jar.md#tip-from-the-keyboard). ## The Blueprint Every node here is a typed node from [The typed nodes](../from-blueprint/get-a-server-object.md#the-typed-nodes), and none needs a **Target**: each uses its own actor's component. **Show the health.** In `BP_Raider`'s Event Graph: 1. Add **On Health Changed (CSO_Raider)** and wire **Event BeginPlay** into its **Bind**. 2. Wire **Changed** to a **Set Percent** on `HealthBar`, with **Health** divided by 40. [Blueprint snippet: so-raider-health, see /bp/unreal-sdk/so-raider-health.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. **Show what only some players see.** In the same graph: 1. Bind **On Loot Changed (CSO_Raider)** from **Event BeginPlay**, and from its **Changed** print the **Loot** with a **Print String**. 2. Bind **On Hideout Changed (CSO_Raider)** from the **Then** pin of **On Loot Changed**, and from its **Changed** print the **Hideout**. [Blueprint snippet: so-raider-private, see /bp/unreal-sdk/so-raider-private.txt] **Changed** runs at once with the current value, so each of these prints once when the raider appears. A player who may not see a value gets its default, so `Loot` prints 0 and `Hideout` prints an empty line for everyone but the raider's owner and the band's players. The value never changes for them afterwards. **Hire a raider.** In the player pawn's Event Graph: 1. Add the **H** key event, and from **Pressed** add **Call Hire (CSO_Raider)**. 2. Click the small arrow at the bottom of **Call Hire** to show its **Reason** pin, then from **On Failed** add a **Print String** and wire **Reason** to it. [Blueprint snippet: so-raider-hire, see /bp/unreal-sdk/so-raider-hire.txt] Compile and save both Blueprints. ## Two players in Play In Editor 1. In **Advanced Settings** under **Multiplayer Options**, set **Number of Players** to 2 and click **Play**. Sign both windows in, and spawn the warband from the first. 2. Within a few seconds both windows show the same raiders with full bars. A raider's bar falls a fifth of the way every 5 seconds, in both windows. 3. Press **H** in the first window. The server gives that player the first raider nobody owns. Within a second or so, on that raider, the first window prints `Loot` 2, then 4, then 6, one step every 5 seconds. The second window does not print them: it sees the same raider with `Loot` still 0. 4. In the first window, the raiders of the hired raider's band print their `Hideout`, "the old mill" for the ravens or "the quarry" for the wolves. The second window prints an empty line for all of them. 5. Press **H** in the second window. It gets the next free raider. Its own loot now climbs in the second window only, and the second window now sees the hideout of the raiders of that raider's band, which may be the same band or the other one. 6. Press **H** twice within a second. The second press runs **On Failed** with the **Outcome** Denied and a **Reason** such as "Hire can be called again in 1 s". 7. Wait. A raider at 0 health retreats, and a hired raider that retreats is lost: its player's loot stops, and, if it was their only raider, the hideout of its band goes back to an empty line. A moment later the raider is back at full health, owned by nobody. Press **H** to hire again. A team is the team of a record the player owns *in the same area's object*. A player who walks into the next area is on no team there until they hire a raider there. To keep a band's secrets across areas, set **Team From** to **Crowdy Team**; see [Who receives a value](../keep-a-record-per-entity.md#who-receives-a-value). If something does not behave, turn on [`crowdy.exec.trace`](../troubleshooting.md#seeing-what-a-server-object-is-doing) in both windows. Each batch of Everyone values shows as one applied push in both logs, and each fetch of the Owner Only and Team Only values as one read in the owner's or the team's log only. ## Related - [Keep a record per entity](../keep-a-record-per-entity.md): every setting this page uses, and how values reach players - [Shared boss fight](./shared-boss.md): one Server Object everyone hits, beside the Hollow Giant - [Access, members and timers](../access-members-and-timers.md): Callable By, Cooldown and timers - [Write its server logic](../write-server-logic.md): where `logic.rs` lives and what the generated code does for you - [Deploy it with Server Compute](../deploy-with-server-compute.md): Deploy, versions and the switch - [Troubleshooting](../troubleshooting.md#a-value-is-not-what-you-expect): when a value is not what you expect # Loot for the Hollow Giant Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/examples/hollow-giant-loot # Loot for the Hollow Giant :::note Server Objects run on [ck-exec](/exec/intro). Server Function Libraries are new in v2.20.0; see [What's Changed](../../guides/whats-changed.md#2026-10-16-sdk-v2200). ::: The Hollow Giant should drop something when it falls, and a chest on Oakford's north road should hand out a trinket to anyone who walks into it. Both want the same rule: turn a seed and a tier into an item. The rule keeps nothing between calls and needs no members, timers or values to watch, so it is not a Server Object but a [Server Function Library](../server-function-libraries.md), `CSO_LootRoller`. Players can call it, so can the boss's server code, and it answers several calls at once. This page builds it end to end, on top of the boss from [Shared boss fight](./shared-boss.md). If you have not made a Server Object before, start with [Tip jar: your first Server Object](./tip-jar.md). The rules: - A roll takes a `Seed`, an Integer64, and a `Tier`, an Integer from 0 to 4, and answers an item name such as `SilverRing:t1`. - The same `Seed` and `Tier` always give the same item. A game that wants variety passes a different seed each time. - The chest rolls tier 1. The Hollow Giant rolls tier 3 when it falls, with the user id of the player who landed the last hit as the seed, and keeps the item where every player can see it. ## What the server keeps The library keeps nothing. That is what makes it a library. The boss gains one value, `LastDrop`, a String that starts empty and is Visible to Players. The structs, if you use them: ```cpp USTRUCT(BlueprintType) struct FLootRollInputs { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) int64 Seed = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = 0, ClampMax = 4)) int32 Tier = 1; }; USTRUCT(BlueprintType) struct FLootRollOutputs { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) FString Item; }; ``` With no C++, add `Seed` and `Tier` as the function's **Inputs** in the asset, with a **Value Range** of 0 to 4 on `Tier`, and `Item` as its **Output**; the generated names are then `RollLootParams` and `RollLootReply` in place of `LootRollInputs` and `LootRollOutputs`; see [The Rust names](../write-server-logic.md#the-rust-names). ## The asset Make a **Crowdy Server Object Definition**, `CSO_LootRoller`, as on [Make a Server Function Library](../server-function-libraries.md#make-the-asset). With **Type Settings** selected, in Details: | Setting | Value | |---|---| | Type Name | `loot_roller` | | Kind | **Server Function Library** | | Description | Turns a seed and a tier into an item name. | The Server Object panel now shows only **Type Settings** and **Functions**. Add the function `RollLoot`: | Setting | Value | |---|---| | Callable By | **Players** | | Inputs | `FLootRollInputs` (**Use Struct**), or `Seed` Integer64 and `Tier` Integer starting at 1 with a **Value Range** of 0 to 4 | | Outputs | `FLootRollOutputs` (**Use Struct**), or one String, `Item` | ![The CSO_LootRoller editor: the Server Object panel lists only Type Settings and Functions, RollLoot reads (Seed, Tier) returns Item, and Details shows its Callable By, Inputs and Outputs](/img/unreal-sdk/server-function-library-editor.png) Leave **Scaling** at its defaults: 1 Min Instance, 1 Max Instance and 16 Parallel Calls per Instance. A chest and one boss never need more; see [Scaling](../server-function-libraries.md#scaling) for when to raise them. Save the asset. ## The server code Click **Generate**, which writes `Server/loot_roller/`. The function arrives in `logic.rs` as a method that refuses with "not written yet". Replace the file with this: ```rust use crate::*; use ckx_sdk::prelude::*; const ITEMS: [&str; 6] = ["RustySword", "OakShield", "IronHelm", "SilverRing", "DragonScale", "GhostCloak"]; impl Functions for Library { fn roll_loot(&mut self, _ctx: &Ctx, _call: &Call<'_>, params: LootRollInputs) -> Result { let mixed = u64::from(params.Seed as u32).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 40; let index = (mixed as usize + params.Tier as usize) % ITEMS.len(); Ok(LootRollOutputs { Item: format!("{}:t{}", ITEMS[index], params.Tier) }) } } ``` What it does: - **It is a pure calculation.** The item depends on the seed and the tier and on nothing the library remembers, so any instance gives the same answer. There is no state to read or change; `self` is the empty `Library` struct. - **It trusts the range.** `Tier` is 0 to 4 because the **Value Range** on the asset is enforced before `roll_loot` runs; the function does not check it again. - **It uses the low 32 bits of the seed,** which is plenty to spread the six items. See [A Server Function Library](../write-server-logic.md#a-server-function-library) for the parts of `logic.rs` that are different from a Server Object's. ## The boss rolls on its fall Open `CSO_Boss`. Add `CSO_LootRoller` to **Can Call**, in the **Can Call** category, and add the `LastDrop` variable. Generate the boss's server code again; it gains a `calls::loot_roller` module whose `roll_loot` takes no key, since a library has no Instance Id. Change the `hit` of [the boss's server code](./shared-boss.md#the-server-code) where it starts the respawn: ```rust if self.Health == 0 { timers::start(ctx, timers::RESPAWN)?; let roll = calls::loot_roller::LootRollInputs { Seed: self.LastHitBy, Tier: 3 }; if let Ok(loot) = calls::loot_roller::roll_loot(ctx, &roll) { self.LastDrop = loot.Item; } } ``` The roll is allowed to fail. The first call to a library that has been idle can take over a second, and a call from server code that outlasts the call deadline fails with "call deadline passed" although the library may still finish; see [A call to a library from server code fails](../troubleshooting.md#a-call-to-a-library-from-server-code-fails). A missing drop must not undo the kill, so the boss keeps the item only when the roll came back. ## Put it on the server Open **Server Compute**. The **Overview** tab lists `loot_roller` with the badge **New** and a line that reads **Server Function Library**, 1 function and 1 instance, and `boss` as **Changed**. Click **Deploy**, read the dialog, which should say it adds `loot_roller` and changes `boss`, and click **Yes**. A deploy sends every type together, so the library and the boss go out as one version. See [Deploy](../deploy-with-server-compute.md#deploy). ![The Server Compute row of loot_roller: its description, the line Server Function Library, 1 function and 1 instance, and the New badge](/img/unreal-sdk/server-compute-library.png) ## The chest The chest is an actor, `BP_Chest`, with no component: a library has no object to hold. Build it from Blueprint on [Call a Server Function Library](../from-blueprint/call-a-library.md#roll-loot-from-the-chest), or from C++ on [Call a Server Function Library, from C++](../from-cpp/call-a-library.md#call-one). Both roll `Seed` 7 at `Tier` 1 when the player walks into it. The Blueprint is this graph, with a **Print String** each for the item and the reason: [Blueprint snippet: so-chest-roll, see /bp/unreal-sdk/so-chest-roll.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. ## Show the drop On `BP_Boss` from [Shared boss fight](./shared-boss.md#the-blueprint), add **On LastDrop Changed (CSO_Boss)** and wire **Event BeginPlay**, or the **Then** pin of the last **On Changed** you added, into its **Bind**. From **Changed**, add a **Print String** that shows the **LastDrop** pin. Compile and save. [Blueprint snippet: so-boss-drop, see /bp/unreal-sdk/so-boss-drop.txt] ## Try it 1. Play with two players as on [Two players in Play In Editor](./shared-boss.md#two-players-in-play-in-editor). Walk one player into the chest. That window prints `SilverRing:t1`, and the other window prints nothing, since the call was that player's own. 2. Walk the other player into the chest. It prints `SilverRing:t1` too, and so does the first player on a second visit, since the seed and the tier have not changed. 3. Hit the boss down to 0 with either player. Both windows print an item of tier 3, such as `GhostCloak:t3`, the same in both, since the boss kept it in `LastDrop` and everyone watches that. Which item it is depends on who landed the last hit. 4. Let the boss respawn and bring it down again with the other player. A different seed usually gives a different item, and both windows print the change. If the item is empty, the boss's roll failed once, typically on the first fall after the library was idle: the boss still fell, and its next fall tries again. ## Related - [Make a Server Function Library](../server-function-libraries.md): Kind, Scaling and what a library cannot have - [A Server Function Library](../write-server-logic.md#a-server-function-library): the server code - [Call a Server Function Library](../from-blueprint/call-a-library.md) and [from C++](../from-cpp/call-a-library.md): the chest - [Shared boss fight](./shared-boss.md): the boss this page extends - [Call other Server Objects](../access-members-and-timers.md#call-other-server-objects): Can Call and the typed helpers - [Troubleshooting](../troubleshooting.md#a-call-to-a-library-from-server-code-fails): a cold first call # 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. # Best Practices Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/guides/best-practices # Best Practices Four rules decide most of the design questions you will meet with this SDK: which plane a field lives on, who decides an outcome, what code that runs once per entity may do, and what the host is for. Every feature page shows one of them on one feature; this page states them plainly so you can apply them to a feature no page covers. ## When you come here Before you add a replicated variable and are not sure whether it is a `CrowdyState` property or belongs on the server. Before you write the code that resolves a fight, a capture, or a purchase. When a loop over your networked entities is slower than it should be. When you are about to write `if (IsHost())` around something that matters. ## Where state goes Networked state lives on one of two planes, and a field lives on exactly one of them. **CrowdyState** is the view plane: movement, animation, transient data. It rides a fast client-owned UDP path, the client that owns an entity writes its state, and nothing on the server checks the value. Any client can lie about its own view state. **Your server code** is the truth plane: hit points, stats, inventory, scores, anything a client must not be able to forge. Your app's code on [ck-exec](/exec/intro) owns them; a client asks it for a change and is told the confirmed result. This is the only enforcement boundary in the system. The SDK does not wrap ck-exec yet; see [The Two Planes](../concepts/two-planes.md). The test for a new field is one question: can a malicious client benefit from lying about this value? If yes, it belongs in your server code, however tempting the low latency of CrowdyState is. If no, it is a CrowdyState property, and the truth plane never hears about it. :::warning[Authoritative or cheat-sensitive state never goes in CrowdyState.] A `CrowdyState` property is whatever its owner says it is. Put health, currency, inventory, or a quest flag there and any client can set it to any value, and every peer will apply it. Those belong in your server code. ::: Only you know whether a value gates a reward, so the SDK cannot choose the plane for you. [The Two Planes](../concepts/two-planes.md) walks through the split field by field. ## Requesting versus deciding A client requests a change and presents the confirmed result. It does not decide the authoritative outcome, not from local prediction, not from a peer's event, and not because it happens to be the host. When the client already knows the target, it asks the server's code for the change: deal damage, heal, capture a camp, change ownership, assign a team. The server validates and applies it, including any immediate dependent state. When the target or the process has to be discovered or coordinated, finding or creating a team, searching, resetting a match, fanning out work, that work belongs in server code too. The site-wide [API best practices](/overview/best-practices) page states this split for every API. CrowdyEvents, CrowdyState, and actor snapshots move presentation: poses, cosmetics, one-shot effects. Use them to look responsive, and reconcile anything competitive or persistent to the server's confirmed value when it arrives. `IsLocallyOwned()` on the entity component answers who sends an entity's pose, never who decides a rule. ## The cost of per-entity work Code that runs once per networked entity per frame or per tick pays for everything inside it at whatever scale your game reaches. A cast that is free on one actor is a thousand casts a frame on a thousand entities. Two habits keep that loop cheap. Track many entities by index, not by pointer. An index is smaller, survives a reallocation, and does not chase a cache line to be useful; a pointer in a per-entity struct is a guaranteed miss every time it is followed. Resolve once, outside the loop. Anything that allocates, copies a container, searches by `FName`, calls `Cast<>`, calls a virtual, resolves a soft object, or reads a `UObject`'s properties belongs in a per-frame or per-chunk step whose output is a flat value the loop indexes. :::warning[No allocation, `Cast<>`, or UObject read inside a per-entity loop.] Resolve the component, the ownership, and the property you need once, into a plain value, and let the loop index that. If the loop needs to know who owns each entity, that is a bool per entity computed once, not a `FindComponentByClass` and a `Cast<>` per entity per frame. ::: The entity component is built to be read this way. `GetOwnership()`, `GetRole()`, and `IsLocallyOwned()` are cheap getters over values the component already holds, and `OnCrowdyOwnershipAssigned` tells you the moment they change, so an owner flag can be computed at `BeginPlay` and updated from that event rather than re-derived every frame. The same applies on the send side: a `MarkStateDirty` or an RPC issued inside a loop over many entities is a per-entity cost your own tick pays before the SDK ever sees the data. Ship one event that names many entities before you ship many events that each name one. [The Entity Component](../runtime/entity-component.md) lists the getters. ## What to do on the host One client of your app is elected host. The host is a convenience for "which client runs shared setup once": driving world entities that belong to nobody, spawning the things a level needs, running the one-time step that should not happen on every client at once. It is not a security boundary. Election happens on the view plane and nothing on the server prevents a modified client from claiming the role or from writing as if it held it. Even the SDK's own host-owned entity path, the `HostOverride` policy read through `GetHostOverridePolicy()`, is precedence by convention: it decides whose write wins on a well-behaved client, not whether a write is true. :::warning[The host is a convention, not an anti-cheat boundary.] Do not put HP, currency, inventory, captures, or team assignment behind a host check. A host check decides who runs a piece of view-plane work; your server code decides whether a change is allowed. [The Host Is a Convention](../concepts/host-is-a-convention.md) explains why. ::: Two checks answer "am I the host", at two trust levels. `UCrowdyHostSubsystem::IsHost()`, `GetHostID()`, and `GetHostUserID()` are the local view, free and immediate, and right for choosing which client runs setup. `UCrowdyHostSubsystem::CheckEntityIsHost` is the server-validated sibling of the client-derived `UCrowdyUtilities::IsCrowdyEntityHost` compare: for the local player it asks the server directly, and for another actor it resolves that actor's owner on the server and compares it to the elected host. Use it when the caller cannot afford a spoofed or stale answer, and remember that even its answer only tells you who is host; the gate itself still lives in your server code. [Host Authority](../runtime/host-authority.md) has the calls, [Host Election](../services/host-election.md) the server-validated check in full. ## Gotchas - A field lives on one plane. Decide which one it is before you mark it. - `IsLocallyOwned()` fails closed for a host-owned entity while no host is elected yet. Bind `OnCrowdyOwnershipAssigned` instead of assuming an answer at `BeginPlay`. - `HostOverride` is authored configuration, identical on every client. It is not sent on the wire and it is not checked by the server. - A per-entity cost you cannot see in one PIE client is still there. Measure with the entity count you ship, not the one you test with; [Testing Locally](./testing-locally.md) covers the loopback switches for that. ## Related - [The Two Planes](../concepts/two-planes.md): the split, field by field. - [ck-exec overview](/exec/intro): where the truth plane's code runs. - [Host Authority](../runtime/host-authority.md): the host calls and host-owned entities. - [Host Election](../services/host-election.md): the server-validated host check. - [Testing Locally](./testing-locally.md): loopback and trace switches for a single client. # Testing Locally Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/guides/testing-locally # Testing Locally Most replication paths need two clients to see anything: one that sends, one that receives. Two console variables let you exercise the send and receive halves of RPC and Crowdy State on a single Play in Editor client, so you do not need a second window for most day-to-day iteration. This page covers those two switches, when you do need genuine two-client PIE and how to set it up without a false result, and the trace console variables you reach for once loopback confirms the send side but the far end still looks wrong. ## When you come here You are iterating on an RPC handler or a Crowdy State property and want to confirm the send and receive code paths without launching a second client, you are about to test something that only shows up with two real players (host election, ownership, targeted delivery), or something replicated is not showing up and you need to see where the SDK stopped. ## Single-client RPC loopback ```text crowdy.rpc.loopback 1 ``` With loopback on, a CrowdyEvent you send is also delivered back to your own receive path, once. This confirms, with a single client, that the send site fires, the call serializes, and the receiving `Name_Implementation` runs. A call made from inside the replayed body sends normally but does not loop back again, so loopback cannot cascade into a second delivery of its own output. :::warning[Loopback is for single-client testing only.] Turn it off before testing real two-client routing, or the extra self-delivery masks routing problems. ```text crowdy.rpc.loopback 0 ``` ::: ## Single-client Crowdy State loopback Crowdy State loopback cannot replay a delta onto the same actor the way RPC loopback replays a call, because the ownership gate drops a non-host-sourced delta for an entity the client already owns outright, and even if it did not, decoding onto the same live actor would report zero changed properties and never fire `OnRep`. So instead of replaying onto the sender, it mirrors. ```text crowdy.state.loopback 1 ``` With loopback on, a state delta you send for an owned entity is also decoded onto a lazily spawned local mirror: a second instance of the same class, in the `RemoteProxy` role, with its own distinct NetID. The mirror's `OnRep` and change delegates fire exactly as a real peer's would, so you can confirm a property change reaches a receiver without a second client. Off by default; when it is off, nothing about normal replication changes. :::note[The mirror always logs its delivery as `(targeted)`, even for a delta that actually went out spatially or as a keyframe.] This is a cosmetic label in the trace log only: `DispatchStateDelta` never gates its behavior on that flag. Do not read the trace log's delivery-kind label as fact while state loopback is on. ::: The mirror requires the entity's `OwnerID` to stay invalid and its role to stay `RemoteProxy`; that is how the SDK tells the mirror apart from the real owned entity so the two do not collide. You do not configure this yourself, it explains why a mirror exists at all rather than a same-actor replay. ## Two-PIE-client setup The loopback switches cover the common case. Some things only show up with two real clients: which one gets elected host, an ownership transfer, a targeted or spatial send that must reach one client and not the other. :::caution[Two PIE clients need two distinct real accounts.] Identity is deterministic from the signed-in account. One account signed in twice, in two PIE windows, conflates into a single player and a single NetID, so the two windows do not reproduce two-client behavior at all. This is a silent wrong result, not a crash: sign each window in with a different account. ::: Each PIE window inside the editor process keeps its own saved session, so one window does not overwrite another window's sign-in. The only setup left to do by hand is signing the two windows into two different accounts. Run the two clients as separate processes: in Editor Preferences, under Play, uncheck "Run Under One Process." Besides matching how a packaged build actually runs, it keeps any per-process editor systems (runtime generation, audio devices) from being shared between the two worlds. The first client still runs in the editor with its own session; every client the editor starts as a separate process uses the default session, so two clients work, but a third would restore the second one's account. For three or more, run the extra ones as packaged clients with `-crowdysession` (below). Once both clients are running, confirm the setup actually worked before you trust anything else you see: each client should report a different local player identity, and exactly one client should be the elected host. ### Two packaged or standalone clients Outside PIE, clients that share a `Saved` folder read the same saved session, so a second client restores the first one's account. A Development package keeps `Saved` beside the game, so every copy started from one folder shares it; a Shipping package keeps it in the Windows user's local app data, so every Shipping copy that user starts shares it, wherever it is. Start each extra client with `-crowdysession=`, for example `MyGame.exe -crowdysession=b`: it keeps a session of its own, signs in once, and restores that account on every later start with the same name. A name is 1 to 32 letters, digits, `-` or `_`, and case does not matter (`B` and `b` are one session). A name that breaks those rules is refused with a warning, and the client gets a fallback session that every refused name shares, never the default one. A client started without the switch keeps the default session, so a player's existing sign-in is untouched. Before v2.21.1 there is no switch: give each client its own `-userdir=` instead. ## Sign in from the console A headless smoke test has no sign-in screen to click through. Three console commands sign a test account in and out from the command line. They exist in development builds and are not in Test or Shipping builds. Each acts on the world it is typed in, so with several PIE clients, type it in the right client. - `crowdy.auth.signin.file ` signs in with a test account read from a JSON file, through the normal Login. The file holds the account's email and password: ```json {"email": "tester@example.com", "password": "the-test-password"} ``` It logs only the outcome and the user id. Keep that file out of source control, and never type a password on the console line itself: the console logs what you type. - `crowdy.auth.status` logs whether you are signed in, the user id, whether an app token is set and when it expires, and the pause state. It never prints a token. - `crowdy.auth.signout` is the normal logout. In a headless run, sign in, then poll `crowdy.auth.status` until it reports `SignedIn=true` before you run any player-side command. Sign-in finishes after the command returns, so a command typed straight after it can run while you are still signed out. See [Authentication](../services/authentication.md). ## Trace console variables Each SDK area exposes a `crowdy..trace` console variable that adds informational logging for that area; warnings and errors print regardless. The four you will reach for first while testing locally: - `crowdy.rpc.trace`: every CrowdyEvent send and receive, the function, entity, addressing, and whether it was owned or delegated. - `crowdy.state.trace`: owned-entity diffing, delta emission, and datagram sizes. - `crowdy.entity.trace`: registry add and remove, event routing and dispatch, actor tracking. - `crowdy.net.trace`: the UDP transport underneath both: socket open and close, sends and receives, worker pool and buffer pool churn. The full table, every trace and behavior console variable the SDK ships, is on [Console variables](../reference/console-cvars.md). The category each module logs under, and which trace console variable pairs with it, is on [Log categories](../reference/log-categories.md). :::tip[Turn a trace back off once you have your answer.] Informational trace lines add log volume and a small per-frame cost while they are on. `crowdy.serialize.scopes` and `crowdy.state.scopes` are worth calling out specifically: each adds a nested CPU trace scope around every message decode, at the cost of an extra pair of timestamps per message. Leave both off unless you are reading that specific timing. ::: ## A short debugging loop 1. Turn on the trace that matches the area you suspect: `crowdy.rpc.trace` for a call that never arrives, `crowdy.state.trace` for a property that never updates, `crowdy.entity.trace` if replication looks dead everywhere at once. 2. Reproduce the problem. 3. Read the log from the moment of the action. The trace lines show what the SDK decided and where it stopped. 4. Turn the trace back off. If nothing traces at all and replication looks completely inactive on a map, that is usually a map profile whose asset did not load, or one with networking off, not a logging problem; the warning names the asset. See [Map profile](../runtime/map-profile.md) before you dig further into traces. ## Gotchas - `crowdy.rpc.loopback` replays a call once and does not cascade; a call made from inside the replayed body sends normally. - `crowdy.state.loopback` mirrors onto a second local entity instead of replaying onto the sender, because the ownership gate and the zero-changed-properties problem rule out a same-actor replay. - The state loopback mirror's trace always says `(targeted)`; that is a label, not the delta's real delivery kind. - One account in two PIE windows produces two windows that look like they are testing two clients while actually sharing one identity. Confirm two distinct local player identities before you trust a two-client result. - `crowdy.serialize.scopes` and `crowdy.state.scopes` cost real per-message overhead while on; they are not free background logging. ## Related - [Console variables](../reference/console-cvars.md): the full table of trace and behavior console variables. - [Authentication](../services/authentication.md): the sign-in the console commands go through. - [Log categories](../reference/log-categories.md): each module's log category and its trace console variable. - [Crowdy State](../runtime/crowdy-state.md): the concepts behind the deltas state loopback mirrors. - [Troubleshooting](./troubleshooting.md): symptom-to-cause table for problems that are not a testing-setup question. # Packaging and Shipping Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/guides/packaging # Packaging and Shipping Run through this checklist before you cook and package a build. Each step covers a setup mistake that passes in the editor but breaks, or silently does nothing, once the build is packaged. Come here before a cook, and again if a symptom only shows up in a packaged build: replication that looks dead, settings that did not take, or a Shipping build that will not compile. ## 1. Rebuild the baked registry `UCrowdyBakedRegistry` is a `UDataAsset` stored at the fixed path `/Game/CrowdySDK/CrowdyBakedRegistry`. It is a cooked snapshot of the metadata the SDK's markers declare. UObject metadata (`HasMetaData` and friends) is stripped from cooked and Shipping builds, so at runtime the SDK reads this snapshot instead of the live metadata: RPC routing and CrowdyState property flags among it. :::warning[Never call `HasMetaData` at runtime in shipped code.] That metadata is gone in a cooked build. The SDK already reads the baked registry for you, for RPC and CrowdyState metadata alike. ::: The cook rebakes the asset automatically, and the editor also bakes it once at startup if the asset is missing. Run **Tools > Rebuild Crowdy Registry** yourself to guard against inspecting a stale snapshot while you are still in the editor, or use the Registry page's **Rebuild (Deep Scan)** button in [Crowdy Studio](../studio/inspector-and-registry.md), which streams tagged assets in instead of freezing the editor on a force-load. :::note[The cook re-bakes the asset automatically, so the manual rebuild is mostly a guard while you are still in the editor.] ::: ## 2. Ignore the baked asset in version control The baked asset is machine-generated and should not be committed. Add it to the ignore list in your version control system. ```text Content/CrowdySDK/CrowdyBakedRegistry.uasset ``` The cook regenerates it, so leaving it out of source control avoids merge noise and a stale asset overwriting a fresh bake. ## 3. Confirm every playable map resolves a profile Every map that ships resolves a [map profile](../runtime/map-profile.md), one of three ways: - An entry in the per-map `MapProfiles` map names it, or - `DefaultProfile` is set for unlisted maps, or - Neither is set, and the map runs on the SDK's shipped default profile (networking on, the actor pool drawing). The cook hazard is the first two: a `MapProfiles` row or a `DefaultProfile` whose asset is not packaged resolves to nothing. The SDK then logs `check that the asset still exists and is packaged`, and the entity subsystem, the auto replicator, and the actor manager do nothing on that map, so replication looks dead. :::caution[A profile asset that loads in the editor and is missing from the cook is the failure to look for.] Open **Project Settings, Plugins, Crowdy SDK, Map Profiles** and confirm every asset named there, and the `DefaultProfile` asset, is in a cooked directory or referenced by something that is. ::: ## 4. Verify project settings are synced Confirm the app settings match the environment you are shipping to. The relevant fields live under the `[/Script/CrowdyReplication.CrowdySDKDeveloperSettings]` section: - AppID - OrgId - The game API URLs To sync them, open Crowdy Studio, go to the Project page, and run **Config Sync**. It shows a before-and-after diff and writes the selected app's values into the project settings when you click **Sync to project** on the Project page's Configuration tab. :::tip See [Project settings](../reference/project-settings.md) for the full field list. ::: ## 5. What Shipping strips A Shipping target does not just strip metadata. It sets `WITH_DEV_AUTOMATION_TESTS` to 0, so a region guarded by that macro is compiled out, unless your `Target.cs` sets `bForceCompileDevelopmentAutomationTests`. If it does, a test or harness file guarded by `WITH_DEV_AUTOMATION_TESTS` alone still compiles in Shipping, and it can reach a `#if !UE_BUILD_SHIPPING` region through an included header, not just through its own code. The Shipping build then fails on code you never touched directly. :::warning[A dev-only file can fail to compile in Shipping through a header it includes, not just its own code.] If your target forces dev automation tests on, guard test and harness sources with `#if WITH_DEV_AUTOMATION_TESTS && !UE_BUILD_SHIPPING`, and check them for a `!UE_BUILD_SHIPPING` region they reach transitively before you cut a Shipping build for the first time. ::: This is a build-time hazard, not a runtime metadata fact: keep it separate in your head from the baked-registry rule above. One strips data the SDK reads back through a snapshot; the other strips code paths outright, and a file that assumes they are still there fails to compile. ## 6. CoreRedirects ship with the plugin The SDK plugin carries its own `[CoreRedirects]` in `Plugins/CrowdySDK/Config/DefaultCrowdySDK.ini`. They map the class, struct, and enum paths from before the SDK's module split, and the type and property names the SDK has renamed since (`CrowdyEntityManager` to `CrowdyEntitySubsystem`, the `FCrowdyGroup*` teams structs to `FCrowdyTeam*` and their `GroupId` properties to `TeamId`, among others), to their current homes. Unreal registers a plugin's own `Config/Default.ini` into the config hierarchy before `InitUObject` runs, so these redirects apply automatically to any project with the plugin installed. :::caution[Do not hand-add CrowdySDK's CoreRedirects to your project's `DefaultEngine.ini`.] The plugin's own ini already covers them. A duplicate entry is not harmful, but it is redundant, and it is a sign you are following guidance written before the module split. ::: Redirects only take effect on load. An old asset that references a pre-split path is not rewritten until it is loaded and saved again, or run through a resave-packages pass. A packaging pass is exactly when a reference like that would otherwise surface, so if you are migrating assets from an older SDK version, resave them before you cook. ## Checklist ```text [ ] Rebuilt the Crowdy registry in the editor (or trust the last cook's automatic bake) [ ] CrowdyBakedRegistry.uasset is listed in the ignore list [ ] Every profile asset named in MapProfiles or DefaultProfile is packaged (a map with neither runs on the shipped default) [ ] Config Sync run against the target environment [ ] Test and harness code guarded by WITH_DEV_AUTOMATION_TESTS alone also carries !UE_BUILD_SHIPPING if the target forces dev tests on [ ] No hand-added CrowdySDK CoreRedirects in DefaultEngine.ini [ ] No HasMetaData calls remain in shipped runtime code ``` ## Gotchas - The cook rebakes the registry automatically. The manual rebuild only guards against inspecting a stale snapshot while you are still in the editor. - A Shipping-only build failure will not show up in a Development or PIE cook. If code compiles everywhere except Shipping, check whether `bForceCompileDevelopmentAutomationTests` keeps your test files in the build, then look for a `!UE_BUILD_SHIPPING` region reached through an include, not through the line you changed. - CoreRedirects apply on load, not retroactively. A stale reference in an unopened asset waits until that asset is loaded and saved, or resaved in bulk, before it resolves to the new path. ## Related - [Inspector and Registry](../studio/inspector-and-registry.md): the Registry page's Rebuild (Deep Scan) button. - [Map Profile](../runtime/map-profile.md): what a profile is and what reads it. - [Project Settings](../reference/project-settings.md): the full field list, and which fields Config Sync writes. - [Testing Locally](./testing-locally.md): the dev-only loopback and trace surface, adjacent to what Shipping strips. - [Troubleshooting](./troubleshooting.md): symptom to cause to fix, including a profile asset that did not load. # Troubleshooting Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/guides/troubleshooting # Troubleshooting This page matches a symptom to its likely cause and its fix. Read the table first; each row links to a section with the full fix and the trace CVar that confirms it. | Symptom | Likely cause | Fix | Trace CVar | |---|---|---|---| | [Nothing replicates on a map](#nothing-replicates-at-all) | A `MapProfiles` row or `DefaultProfile` names an asset that did not load, or networking is off on the resolved profile | Fix the asset path the warning names, or tick Enable Networking | `crowdy.entity.trace` | | [Actors do not appear at all](#rendering-backend-not-set) | The map profile's Backend Class was cleared | Assign a `UCrowdyRenderingBackend` subclass | `crowdy.entity.trace` | | [Entities register but are never drawn](#entities-register-but-are-never-drawn) | Backend Config empty, or its Replication Policy Class unset (2.14.0 and earlier) | Set a Crowdy Actor Pool Backend Config with a policy class | `crowdy.entity.trace` | | [Only some remote entities of a class appear](#only-some-remote-entities-of-a-class-appear) | The actor pool never grew past 8 per class (2.16.0 and earlier), or it is at `MaxPoolSizePerClass` | Update to 2.17.0; raise the cap if it warns | `crowdy.pool.trace` | | [A player who just joined is invisible until they move](#a-player-who-just-joined-is-invisible-until-they-move) | A known server issue: the actor's first update is refused and later ones are not relayed to other players | Nothing on the client fixes it yet; moving recovers | `crowdy.net.trace` | | [An RPC never runs on other clients](#events-are-not-received) | Missing entity component, unregistered entity, recipient/distance settings, or a registry gap | Work through the four sub-causes in order | `crowdy.rpc.trace` | | [A remote entity shows as the wrong class](#a-remote-entity-cannot-be-targeted) | The entity's class was never loaded on this client | Add the class to Preloaded Entity Classes | `crowdy.entity.trace` | | [A CrowdyState property does nothing on a crowd-rendered actor](#a-crowdystate-property-does-nothing-on-the-crowd) | Mass-rendered entities need their own field registration | See [Add your own crowd data](../mass/add-your-own-crowd-data.md) | n/a | | [Client cannot connect, or connects on one machine and not another](#connection-fails-ipv4-versus-ipv6) | Address-family mismatch, or the realtime connection never came up | Match the family, check the UDP path | `crowdy.net.trace` | | [Thousands of warnings a second, frame rate drops](#a-refused-connection-floods-the-log) | The realtime connection was refused or never came up; every send after that logs | Confirm the connection came up before chasing anything else | `crowdy.net.trace` | | [Code will not compile against the SDK](#module-dependency-or-include-errors) | Missing module dependency or missing include | Add the module, include the header | n/a | ## Nothing replicates at all A project that configures nothing runs on the SDK's shipped default map profile, so "no profile" is not the nothing-configured state. The map is inactive when a `MapProfiles` row or the `DefaultProfile` points at an asset that did not load, or when **Enable Networking** is off on the profile that resolved. With no profile resolved for the current map, the entity subsystem, the auto replicator, and the actor manager do nothing. You get one warning in the log and otherwise no errors, so replication just looks dead. Fix: 1. Read the warning. It names the asset path a `MapProfiles` row or `DefaultProfile` points at and says it "did not load; check that the asset still exists and is packaged". Fix the path, or the cook, first. 2. Open Project Settings, Plugins, Crowdy SDK, Map Profiles, and confirm the row for this map (or `DefaultProfile`) points at a `UCrowdyMapProfile` asset that opens. 3. On that profile, confirm `bEnableNetworking` is true. Keep the other defaults: Actor Pool backend and the auto replicator on. 4. A map with no row and no `DefaultProfile` runs on the shipped default; if that map should be inactive, give it a profile of its own with **Enable Networking** off. The profile is resolved by `UCrowdySDKDeveloperSettings::ResolveProfileForWorld`. If no profile resolves, the SDK logs "map resolved no map profile, so the SDK is inactive on it: no entity registration" and stays off for that map. :::tip[If a new map replicates nothing, read the profile warning before anything else: it names the asset that failed.] ::: ## Rendering backend not set Actors never appear, even though the map profile resolves and `crowdy.entity.trace` shows entities registering. The map profile's Backend Class was cleared: it defaults to `UCrowdyActorPoolBackend`, so this state only follows an edit. The actor manager warns "BackendClass is not set in the map profile" the first time it needs one. Fix: open the map profile asset and assign a `UCrowdyRenderingBackend` subclass in the Backend Class field. The Actor Pool backend that ships with the SDK is the right default for most games. ## Entities register but are never drawn Backend Class is set, `crowdy.entity.trace` shows remote entities registering, and nothing appears. On 2.14.0 and earlier the actor pool refuses to initialize when its config is incomplete, and logs one of two warnings: - "Backend Config is not set, but this backend needs a CrowdyActorPoolBackendConfig to know what to spawn", when the profile's Backend Config is empty. - "Replication Policy Class is not set on '...', so nothing would read the state arriving for these entities", when the config exists but names no policy. Fix: update to 2.15.0, where an empty config falls back to the built-in `UCrowdyTransformRepPolicy` and this state no longer occurs. On an older plugin, set **Backend Config** on the map profile to a Crowdy Actor Pool Backend Config asset and its **Replication Policy Class** to a `UCrowdyRepApplicationPolicy` subclass; see [Rendering backends](../runtime/rendering-backends.md) and [What's changed](./whats-changed.md). ## Only some remote entities of a class appear Another client spawns twenty entities of one class and this client shows eight. On 2.16.0 and earlier the actor pool held `DefaultPoolSizePerClass` actors (8) per class and never grew: every entity past that logged `Pool exhausted for ` and was never drawn, and one whose spawn event had already made an actor lost that actor too. Fix: update to 2.17.0, where the pool grows on demand up to `MaxPoolSizePerClass` (256) and an entity it cannot draw yet is retried instead of dropped. If the log says a pool "is at its cap", raise `MaxPoolSizePerClass` on the map profile's Actor Pool Backend Config. `crowdy.pool.trace` logs each activation, each growth step, and each entity waiting at the cap; see [Rendering backends](../runtime/rendering-backends.md#configure-it). ## A player who just joined is invisible until they move A player spawns and stands still, and the other players do not see them until they take a step; this can last a minute or more. The client is sending correctly. The replication server refuses each actor's first update with `UNAUTHORIZED` while it loads the player's permissions, which is expected and logged once as "The server rejected a send: code 7 (Unauthorized) ... ACTOR_UPDATE_REQUEST". Every later update is accepted, but the server does not relay that actor to the other players, nor theirs to it, until the actor moves. This is a server issue and has been reported. Confirm it with `crowdy.net.trace 1` on both clients: each client's log shows its own actor coming back to it (`Received opcode 130 for actor `) every few seconds, and no line for the other player's actor until one of them moves. "Transport totals" should show no verification failures and no drops. Until the server is fixed, the actor becomes visible to everyone as soon as it moves. ## Events are not received An RPC event (`CrowdyEvent`) reaches the call site but never runs `Name_Implementation` on other clients. Work through these causes in order. ### The receiving actor has no entity component An event is aimed at the entity that its sender object belongs to, by NetID. The receiving actor must carry a `UCrowdyEntityComponent`. If the actor is not an entity, there is no NetID to route to and the event is dropped. Fix: add the component in the actor constructor. ```cpp CrowdyEntity = CreateDefaultSubobject(TEXT("CrowdyEntity")); ``` ### The entity is not registered, or the dispatch drops it A Dynamic entity needs to register before it can send or receive. Until it registers it has no NetID on the network, and a call aimed at an unregistered entity logs "target is not a registered entity, event dropped" from `DispatchGameEvent` and goes nowhere. - If `bAutoRegister` is off, call `StartReplication()` yourself. - Confirm registration from `GetNetID()`. A registered entity returns a valid id. ### The recipient does not include this client Check the `CrowdyRecipient` on the receiver function. The recipient decides who runs the body: - `SpatialMulticast` (default): spatial and decay-thinned. Remote clients only run the event if they are in range. - `Multicast`: a named channel, any distance, no decay. - `OwningClient`: only the entity's owner. - `Host`: only the elected host. If you used the default `SpatialMulticast` and the other client is out of range, the event is thinned away on purpose. Raise `CrowdyDistance` (values run `None` to `Eight_Chunks`, default `Eight_Chunks`) or relax `CrowdyDecay` (default `No_Decay`). Decay and distance apply only to `SpatialMulticast`. If you need delivery at any distance, switch to `Multicast` with a `CrowdyChannel`, or use `OwningClient` or `Host` for targeted delivery. :::note[The owner of a SpatialMulticast event runs the body locally and announces to the others. The owner never runs its own `Name_Implementation` from the network: an event is not delivered back to its sender, even where a Dynamic entity's owner draws its own echoed proxy (Render Local Owner on the actor pool).] ::: ### The event was never registered, or the receiving function cannot be found Two failures look the same as a dropped event but come from a cook or a rename, not from recipients or range. A `CrowdyEvent` that never made it into the baked registry logs "not registered; calls to it will be dropped" and every call to it is silently a no-op. This is a cook-time gap: cooked builds strip UObject metadata, so the SDK reads `CrowdyEvent` markers from a baked registry built ahead of time, never from `HasMetaData` at runtime. A function that shows up in the editor because the metadata is still there can be missing from that registry in a packaged build, which is why the symptom can differ between a dev session and a shipped build. Rebuild the registry before packaging if you added or renamed an event. Separately, if the function itself cannot be found on the class, `ResolveFunction` logs "not found on class" and the call is dropped. This is almost always a rename or a missing `UFUNCTION` on the `_Implementation` function. Turn on `crowdy.rpc.trace` to follow send, route, and recipient decisions for each event. ## A remote entity cannot be targeted A client falls back to guessing a remote entity's type from its state struct, and gets it wrong when two classes share one struct. The receiver resolves an incoming entity update by a class id, and it only resolves that id among classes already loaded in memory; it never loads a package to do so. A Blueprint entity class this client has never spawned itself is not loaded, so the id cannot be named and its own declaration cannot be read. The same gap makes the actor manager fall back to the state struct, which cannot tell two classes sharing one struct apart. `Preloaded Entity Classes` is empty by default in a new project. This is the default state, not an edge case you are unlikely to hit. Fix: Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes. Add every entity class a client needs to display or target when it belongs to someone else. :::note[Treat Preloaded Entity Classes as a deployment requirement, not a one-time test-rig setting.] Any client that must display or target another client's entity needs that entity's class preloaded on this client, whether or not this client ever spawns one itself. ::: ## A CrowdyState property does nothing on the crowd A `CrowdyState` property that works perfectly on a normally-spawned actor does nothing when the same actor is drawn as a crowd row by the Crowdy Mass Rendering Backend: the value is decoded and then silently dropped. A normally-spawned actor needs nothing extra. An entity drawn as a crowd row needs its own field registration before a `CrowdyState` property reaches it; see [Add your own crowd data](../mass/add-your-own-crowd-data.md). For other crowd symptoms, such as a grey crowd or no rows at all, see [Fix a crowd that does not look right](../mass/troubleshooting.md). ## Connection fails: IPv4 versus IPv6 If the client never connects, or connects on one machine but not another, the cause is often an address-family mismatch between the client and the server endpoint: one side resolves to IPv6 and the other only listens on IPv4, or the reverse. Fix: 1. Check **UDP Protocol** under Project Settings, Plugins, Crowdy SDK. It is read-only there; set it on the Crowdy Studio Project page, Connection section. `Auto (IPv4)` and `Force IPv4` connect over IPv4, `Force IPv6` over IPv6, and neither falls back to the other family. On a network without working IPv6, `Force IPv6` is the mismatch. See [Connection and reconnect](../runtime/connection-and-reconnect.md). 2. Check that no local firewall is blocking the UDP path. The realtime plane runs over UDP; a blocked port looks like a silent connection failure. 3. Verify the app is configured for the correct backend (Dev, Production, or Custom) in Crowdy Studio, then run Config Sync so the URLs and the protocol in project settings match. A UDP timeout during a live session is a different, self-healing case: the SDK logs "The UDP connection failed, reconnecting" and retries on its own. It is not an error to act on unless the reconnect never succeeds. Turn on `crowdy.net.trace` to see connection attempts, the resolved endpoint, and the UDP handshake. ## A refused connection floods the log Thousands of Warning lines a second, frame rate drops, and nothing else about the session looks unusual. The realtime connection was refused or never came up, and every send attempt after that logs one Warning: "the replication connection refused" a send, or "cannot open a replication connection without a game instance" if it never had one to open. :::caution[Count the Warning lines before you trust any other reading taken alongside them.] A refused connection can produce thousands of Warning lines a second and cut your frame rate in half by itself. If you are chasing a performance number or another symptom from the same run, a log with more than a few hundred Warning lines in it is not a clean reading; the flood is the cause, not a side effect. ::: Fix: confirm the connection actually came up. Look earlier in the log for a server assignment or a connect failure, and fix that first before treating any other symptom recorded alongside the flood as real. ## Module dependency or include errors Code does not compile after you call into the SDK, usually a missing module dependency or a missing include. The SDK is split across modules. Reference the right one for the API you call: - `CrowdyReplication`: the entity subsystem (`UCrowdyEntitySubsystem`), the entity component, RPC events. - `CrowdyServices`: high-level subsystems and statics: `UCrowdyUtilities`, `UCrowdyTeams`, `UCrowdyAvatars`, `UCrowdyChannels`, `UCrowdyHostSubsystem`. - `CrowdySDK`: `UCrowdySDKSubsystem` (including voice). Fix: 1. Add the module you need to your `Build.cs`. ```csharp PublicDependencyModuleNames.AddRange(new string[] { "CrowdyReplication", "CrowdyServices", "CrowdySDK" }); ``` 2. Include what you use. For RPC events, include the event header explicitly. ```cpp #include "Replication/RPC/CrowdyEvent.h" ``` :::caution[Keep the module dependency graph acyclic. `CrowdyServices` depends on `CrowdyReplication`, which depends on `CrowdyNet`. Your module can depend on any of them; nothing of yours should be depended on by them.] ::: ## Still stuck If a problem does not match anything above, turn on the relevant trace variable and read the log around the failing action. See [Testing locally](./testing-locally.md) for the loopback CVars and how to reach a symptom with one client, and [Log categories](../reference/log-categories.md) for the full set of `LogCrowdy` categories and their trace CVars. A `LogHttp` `ConnectionError` burst against a backend host, with otherwise clean local connectivity, points at your own network path, not the SDK. Check your route to the backend before filing a bug. ## Gotchas - A map with nothing configured runs on the shipped default profile. The "resolved no map profile" warning means a configured profile asset did not load, and it is a warning, not an error. - On 2.14.0 and earlier an empty Backend Config, or one without a Replication Policy Class, tracks entities and draws none; the warning names the field. 2.15.0 falls back to the built-in policy. - Preloaded Entity Classes is empty by default. Add every entity class a client needs to see belonging to someone else, not just the ones it spawns itself. - A `CrowdyState` property needs no setup on a normal actor, but needs its own field registration on a crowd-rendered one; see [Add your own crowd data](../mass/add-your-own-crowd-data.md). - A refused connection can flood the log by itself; rule that out before trusting any other measurement from the same run. ## Related - [Testing locally](./testing-locally.md): the loopback CVars for reproducing a symptom with one client. - [Log categories](../reference/log-categories.md): every `LogCrowdy` category and its trace CVar. - [Fix a crowd that does not look right](../mass/troubleshooting.md): a grey crowd, missing rows, and the crowd diagnostics. - [Add your own crowd data](../mass/add-your-own-crowd-data.md): field registration for crowd-rendered entities. - [Rendering backends](../runtime/rendering-backends.md): the actor pool and the backend contract. - [Authentication](../services/authentication.md): the sign-in events to gate your first calls on. - [What's changed](./whats-changed.md): renamed or removed nodes that can look like a new bug. # Sample Project Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/guides/sample-project # Sample Project `crowdy-sdk-sample` is a small, real Unreal project that demonstrates the SDK feature by feature. Each feature is a switch you walk into: the switch fires, the feature runs, and you watch the result replicate across two Play in Editor clients. :::note[This tour targets SDK 2.1.0, not the current SDK] The plugin copy bundled in the repo is 2.1.0 (`Plugins/CrowdySDK/CrowdySDK.uplugin`), the release that introduced the current `GetCrowdyHasAuthority` name. It predates this site's own CrowdyState pages. The repo's README documents a few 2.1.0 additions, property replication on CrowdyState, replicated subsystems, host and ownership fields, as already present in the bundled plugin code, but none of the switches below exercise them, so this page does not either. For the current shapes, follow the Lantern in the [Quickstart](../quickstart.md). ::: The code lives in the `CrowdySDKTest` module, under `Source/CrowdySDKTest/Public|Private/Sample`. :::tip[Most examples ship as C++ and Blueprint side by side] Read the C++ when you want the exact call site and types. Open the matching Blueprint under `Content/Interactions/` when you want to see the same wiring as nodes. Two examples, Persistence and Teams, are C++ only: there is no Blueprint counterpart for either in this repo. ::: ## The interact-switch pattern The sample keeps feature code out of the player and the HUD. Each example is a trigger volume you overlap. This keeps every example self-contained and lets you trigger one feature at a time. Two pieces drive it: - `ISampleInteractable` is an interface with one method, `Interact(APawn*)`. Anything that reacts to the player implements it. - `ASampleSwitchBase` is a trigger box. On overlap it calls `Interact` on itself, passing the overlapping pawn. Each example subclasses `ASampleSwitchBase` and overrides `Interact`. Walk into the box, and that example's code runs with a reference to your pawn. ```cpp void ASampleObjectSwitch::Interact(APawn* Instigator) { // Each switch puts its one feature here. // The base class has already detected the overlap and handed you the pawn. } ``` ## Shared data `SampleTypes.h` holds the structs the examples pass around: - `FSampleEntityState` (Location, Rotation, Velocity, bIsFalling) is the snapshot a continuous-state executor sends on the wire. - `FSampleSpawnInfo` (Tint, DisplayName) is the spawn `InitialState` for a new entity. - `FSampleProgress` (Level, LastLocation), tagged `USTRUCT(meta=(CrowdyPersistent))`, is what the Persistence example saves and loads. - `FSampleItem` (Id, Count) is a small struct declared to show an array-of-structs RPC parameter; nothing in the repo uses it yet, and it has no switch of its own. ## The examples Eight switches, each demonstrating one part of the SDK. | Example | Switch | SDK feature it shows | | --- | --- | --- | | MimicMe | `ASampleMimicSwitch` | Dynamic entity with a continuous-state executor | | Object spawn-move-rotate-destroy | `ASampleObjectSwitch` | Spawn and destroy, plus RPC events with direct parameters | | Ghost | `ASampleGhostSwitch` | Owner tracking: your own state echoed back as a proxy | | Voice | `ASampleVoiceSwitch` | Voice chat capture and playback | | Channel announce | `ASampleChannelSwitch` | Multicast RPC over a self-provisioned channel | | Host spawn | `ASampleHostSwitch` | Host authority gate | | Persistence | `ASamplePersistenceSwitch` | Push and pull saved gameplay state | | Teams | `ASampleTeamsSwitch` | Find or create a team, join it, leave it on the next interact | ### MimicMe `ASampleMimicSwitch` spawns `ASampleMimicEntity`, an invisible puppeteer built around a `UCrowdyEntityComponent`. Each tick, the entity mirrors the interacting pawn's transform, computed by `UMathOperations::GetMimicTransform`, through the `ISampleMirrorSource` interface it implements (`GetMirrorTarget`, `GetMirrorPlaneTransform`). `USampleMimicExecutor` reads that mirror source and streams the result as an `FSampleEntityState`. The same struct shape is the one the SDK's general-purpose transform executor expects. `USampleTransformExecutor`, a `UActorUpdateExecutor`, implements the two functions any continuous-state executor needs: ```cpp FInstancedStruct USampleTransformExecutor::GetActorState_Implementation(const UActorComponent* UpdateComponent) const { // Reads the owning actor's transform and packs it into an FSampleEntityState. } UScriptStruct* USampleTransformExecutor::GetStateStruct_Implementation() const { return FSampleEntityState::StaticStruct(); } ``` The AutoReplicator polls the executor about ten times a second, only on the owner. Remote clients apply the snapshot through the rendering backend. See [Continuous State](../runtime/continuous-state.md). :::tip[ProxyClass here is a split-puppeteer detail, not a general rule] `ASampleMimicSwitch` exposes a `ProxyClass` (falling back to `MimicClass` when unset) because this example splits the invisible puppeteer from the visible proxy the other clients see. Most Actor Pool entities do not need to touch `ProxyClass` at all. ::: :::warning[The replication policy base class is abstract] `USampleTransformReplicationPolicy` subclasses `UCrowdyRepApplicationPolicy` (`ExtractFields`, `ApplyToActor`, `OnInstanceDeactivated`) to interpolate position and rotation and latch velocity and the falling flag. Without a concrete subclass registered on the entity's `ReplicationPolicyClass`, the backend init bails and replication silently no-ops. If MimicMe stops moving on remote clients, this is the first thing to check. ::: ### Object spawn-move-rotate-destroy `ASampleObjectSwitch` cycles through four actions on `ASampleObjectEntity`, a Static-mode `UCrowdyEntityComponent` actor: spawn, move, rotate, destroy. - Spawn and destroy go through the entity spawn API. - Move and rotate are RPC events whose parameters arrive directly, with no payload struct. ```cpp UFUNCTION(meta=(CrowdyEvent, CrowdyRecipient="SpatialMulticast")) void SetObjectLocation_Implementation(FVector NewLocation); CROWDY_EVENT(SetObjectLocation) UFUNCTION(meta=(CrowdyEvent, CrowdyRecipient="SpatialMulticast")) void SetObjectRotation_Implementation(FRotator NewRotation); CROWDY_EVENT(SetObjectRotation) ``` Calling `SetObjectLocation(NewLocation)` runs the body on the owner and announces it to the others, who run `SetObjectLocation_Implementation`. See [Entities and Spawning](../runtime/entities-and-spawning.md) and [RPC Events](../runtime/rpc-events-cpp.md). ### Ghost `ASampleGhostSwitch` toggles `UCrowdyActorTracker::ToggleOwnerTracking`. With owner tracking on, the server echoes your own state back to you, and the backend draws it as a proxy: a true network round trip of yourself, not a local copy. :::caution[Your player pawn must be a Dynamic Crowdy entity for there to be any state to echo.] ::: ### Voice `ASampleVoiceSwitch` toggles voice chat on `UCrowdySDKSubsystem`: - `StartVoiceChat` and `StopVoiceChat` control the microphone. - `PlayVoiceChat` and `MuteVoiceChat` control hearing others. Others are muted until `PlayVoiceChat`. No wrapper is needed; the subsystem methods are BlueprintCallable. ```cpp UCrowdySDKSubsystem* Voice = GetGameInstance()->GetSubsystem(); Voice->StartVoiceChat(); ``` See [Voice Chat](../services/voice-chat.md). ### Channel announce `ASampleChannelSwitch` is a Static entity with a Stable identity policy. On interact it self-provisions its channel: it looks up `SampleWorldChat`, creates it with an Open policy if it does not exist yet, joins it, and registers for reliable RPC on it. `Announce`, its Multicast event, is then routed over that channel. The event takes four parameters directly: ```cpp UFUNCTION(meta=(CrowdyEvent, CrowdyRecipient="Multicast", CrowdyChannel="SampleWorldChat")) void Announce_Implementation(const FString& InMessage, uint8 InMood, const TArray& InScores, TSubclassOf InIcon); CROWDY_EVENT(Announce) ``` A matching `BlueprintImplementableEvent OnAnnounceReceived` carries the same four parameters for a Blueprint listener. A Multicast event reaches channel members at any distance with no decay. For raw channel sends and the membership cache, see [Channels](../runtime/channels.md). ### Host spawn `ASampleHostSwitch` spawns a world object, but only when `UCrowdyUtilities::GetCrowdyHasAuthority(this)` returns true. ```cpp if (UCrowdyUtilities::GetCrowdyHasAuthority(this)) { UCrowdyUtilities::SpawnCrowdyEntity(this, WorldObjectClass, SpawnTransform, FInstancedStruct()); } ``` :::note[The host is a convention, not an enforced server role, so this shows how to gate work to the one elected client.] ::: See [Host Authority](../runtime/host-authority.md). ### Persistence `ASamplePersistenceSwitch` pushes, then pulls, an `FSampleProgress` for the interacting player pawn. ```cpp FSampleProgress Progress; Progress.Level = 3; Persistence->PushState(Progress, PlayerActor); Persistence->PullState(PlayerActor, [](bool bSuccess, const FSampleProgress& Loaded) { // Runs on the game thread. }); ``` :::note[Push goes over UDP and is fast. Pull goes over GraphQL and is async, and the callback runs on the game thread.] ::: Persistence is being retired from the current SDK; see the Persistence section of [What's Changed](./whats-changed.md) for what replaces it and what stays working in the meantime. ### Teams `ASampleTeamsSwitch` shows both sides of teams from gameplay code, through `GameInstance->GetSubsystem()`. Its one authored property is `TeamName` (default "Sample Team"). Interacting looks the name up with `GetTeams`, joins the team if it exists and creates it (Open policy, the creator is added as a member) if not; interacting again leaves it. Membership is read from the cache with `IsPlayerInTeam` and corrected when `OnMyTeamsCacheChanged` fires. ```cpp // Found by name in the GetTeams result: join it. TeamsSub->JoinTeam(FoundId, OnJoined, OnError); // Not found: create it; the current player becomes a member. TeamsSub->CreateTeam(TeamName, TEXT("Created from the sample"), ECrowdyTeamMembershipPolicy::Open, OnCreated, OnError); ``` The 2.1.0 plugin still names these types `FCrowdyGroup` and reads `GroupId`; the current SDK calls them `FCrowdyTeam` and `TeamId` (see [What's Changed](./whats-changed.md)). Read the cache for instant UI and treat the callbacks as eventual truth. See [Teams](../services/teams.md). ## Required setup The sample project is already set up for you. Sign up for a developer account, then log in through Crowdy Studio and again through the game. Sync the app first if it is not already synced. Logging in through the game yields the identity session token; an app-scoped token for the app is minted from it before replication works. See [Authentication](../services/authentication.md). ## Gotchas - This repo's `Plugins/CrowdySDK` has no `CrowdyCppBridge` or `CrowdyNodes` module folder. If you go looking for current-SDK modules here, you will not find them; they postdate this vendored copy. - Eight examples exist in this repo, not nine. An earlier version of this page named a ninth, `ASampleChannelsSwitch`, that does not exist in `Source` or `Content`. - Persistence and Teams have no Blueprint counterpart in `Content/Interactions/`, unlike the other six examples. - `GetCrowdyHasAuthority` is already the current name in this sample; there is no rename to make here. - If the transform replication policy has no concrete subclass registered on `ReplicationPolicyClass`, replication silently does nothing. There is no error to look for. ## Related - [What's Changed](./whats-changed.md) for the gap between this sample's SDK 2.1.0 and the current SDK, and where Persistence is heading. - [Testing Locally](./testing-locally.md) for the loopback and trace CVars this sample's own README testing steps use. - [Entities and Spawning](../runtime/entities-and-spawning.md) for the entity component and the spawn API. - [Continuous State](../runtime/continuous-state.md) for the executor pattern behind MimicMe. - [RPC Events](../runtime/rpc-events-cpp.md) for the `CrowdyEvent` macro and its recipients. - [Host Authority](../runtime/host-authority.md) for what host convention means for gameplay code. - [Channels](../runtime/channels.md) for raw channel sends alongside the Multicast RPC shown here. - [Voice Chat](../services/voice-chat.md) and [Teams](../services/teams.md) for the two services switches above use directly. - [Authentication](../services/authentication.md) for the sign-in this project needs before anything else works. # What's Changed Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/guides/whats-changed # What's Changed The customer-visible changes to the Unreal SDK since 2.0 / 2.1, newest first, each with what changed and what your project does about it. The list comes from a diff of the 2.1.0 public headers against 2.15.0 plus what each release since carried; anything not listed here kept its name, its pins, and its behavior as far as that diff shows. ## When you come here You built against the SDK in July, you are updating the plugin to 2.15 or later (2.24.0 is current), and you want the list of things that compile differently, behave differently, or no longer exist, before you open the editor. Read top to bottom once. The entries that can break a C++ build are the teams rename (between 2.1.0 and 2.12.0), the removed `DevLogin` (2026-08-30), in 2.15.0 the array delegates, in 2.19.0 the removal of the Game Model API (see [its entry](#2026-10-12-sdk-v2190)), and in 2.20.0 the renamed pin delegates of the Channels, Teams, Avatars and sign-in nodes, which a C++ `AddDynamic` handler binds to (see [On Error](#2026-10-16-sdk-v2200)). In 2.21.0, a C++ `AddDynamic` handler bound to the **Call Server Function** action's **On Success** or **On Failed** needs one more parameter, `FName Code` (see [its entry](#2026-10-26-sdk-v2210)). In 2.22.0, voice chat changes its packet format, so every player needs the new SDK version for voice, and a C++ handler bound to `OnAudioNotify` or the other old voice delegates no longer compiles (see [Voice chat](#voice-chat)). :::caution[The Game Model entries below no longer reach a server, and the API is removed.] The SDK's Game Model API (containers and attributes, Effects, sessions, pre-seeding) was built on the game API's legacy game-model engine, which ck-exec replaced; the game API no longer serves it. The entries that describe it are kept as history. Do not adopt it: v2.18.0, the release that adds Server Objects, deprecates it, and v2.19.0 removes it, as described under [2026-10-12 (SDK v2.19.0)](#2026-10-12-sdk-v2190). Its replacement is [Server Objects](../exec/overview.md), described under [2026-10-07 (SDK v2.18.0)](#2026-10-07-sdk-v2180); [Move from Game Models](../exec/move-from-game-models.md) says what to change. ::: ## 2026-11-02 (SDK v2.24.0) No breaking change and nothing deprecated. From this release the plugin's `MIGRATION.md` lists the same changes with the exact steps to take. ### Sign-out, packaged installs and MIGRATION.md - **No phantom host after sign-out.** On sign-out the SDK told the host subsystem there was no host by setting the host user id to 0, and the subsystem turned that 0 into a valid-looking id. **Is Host Set** stayed true, **Get Host ID** returned an id that belongs to no player, and **On Host Elected** fired with a host that does not exist. After a level travel, or on a map without networking, sign-out could also leave the host set. Now sign-out clears the host before **On Logout** fires, and user id 0 gives an empty (invalid) id: **Is Host Set** is false, **Get Host ID** is invalid, and **Is Host** is false (as before). When a host had been elected on the current map, **On Host Elected** also fires once, on the next frame, with an invalid `HostID` and the previous host as `PreviousHostID`. The first election on each map (the host subsystem belongs to the world, so it starts again after a level travel) fires **On Host Elected** with an invalid `PreviousHostID`, where it used to carry the same made-up id. A host poll answer that arrives while nobody is signed in, such as one still in flight at sign-out, is ignored, so it can no longer elect a host again after sign-out. **What you do:** if you bind **On Host Elected**, treat an invalid `HostID` or `PreviousHostID` as "no host". See [Host Election](../services/host-election.md#after-sign-out). - **Packaged plugin zips now carry the plugin's `Config/` folder.** The plugin's `Config/FilterPlugin.ini` held only comments, so a plugin built with UAT BuildPlugin (which is what the package script runs) left `Config/` out of the zip. `Config/DefaultCrowdySDK.ini`, which holds the SDK's own CoreRedirects, was missing from those installs. The filter now includes `/Config/...`, `/MIGRATION.md`, `/README.md` and the license file. **What you do:** if you install from a packaged zip, take the 2.24.0 package. CoreRedirects you copied into your own `DefaultEngine.ini` as a workaround may stay; duplicates are harmless. See [Installation](../installation.md#step-1-get-the-release). - **The plugin builds with UAT BuildPlugin again.** Packaging the plugin with `RunUAT BuildPlugin` (as a Fab submission or a prebuilt-binaries release does) failed: UHT requires an explicit `Category` on every editor- or Blueprint-exposed property when it builds a plugin that way, and its game target compiles each file without a precompiled header, which exposed files that relied on one for engine types. Every exposed property now has a `Category`, and every file includes what it uses. The Teams and Channels permission structs' fields moved from the `Crowdy|Teams` and `Crowdy|Channels` categories to `Crowdy SDK|Teams` and `Crowdy SDK|Channels`; that only changes how the editor groups them. **What you do:** nothing; if you package the plugin yourself, `BuildPlugin` now succeeds. - **New: `MIGRATION.md` at the plugin root.** It ships in the plugin and in the public CrowdySDK-Unreal repository. For each release it lists what changed and the exact steps to migrate, laid out so a person or an AI agent can follow it. From now on every user-visible SDK change adds an entry there along with its docs. **What you do:** when you update the plugin, read the entries for the releases between your version and the new one. See [Installation](../installation.md#step-1-get-the-release). ### Spawned entities appear once - **Spawn data reaches the actor players see.** The actor pool replaced the actor a spawn event made with a pooled one, so what **On Crowdy Spawned** set on it was lost, and when an update arrived before the spawn event, **On Crowdy Spawned** never fired at all. Now the pool draws the spawn event's own actor, and **On Crowdy Spawned** fires once per entity on each client that receives the spawn event, in either arrival order. **What you do:** nothing; per-instance appearance passed in `InitialState` now shows on other clients that were there for the spawn. See [Entities and spawning](../runtime/entities-and-spawning.md#receiving-a-spawn-on-a-remote-client). - **Static entities reach late joiners, spawn data and owner included.** A Static entity spawned with **Spawn Crowdy Entity** sent nothing after its spawn event, so a player who joined later never saw it. Now its owner repeats the spawn event every 4 seconds, from where the entity is now. A late joiner builds it as if it had seen the spawn: the actor, its owner, and **On Crowdy Spawned** with the spawn data. Clients that already have it ignore the repeats, and a repeat that arrives after the entity's destroy cannot bring it back. Each repeat is about 280 bytes plus your spawn data, about 70 bytes per second per entity. **What you do:** budget the traffic if you spawn many Static entities. See [Entities and spawning](../runtime/entities-and-spawning.md#a-dynamic-entity-can-appear-with-no-spawn-event-at-all). - **No second copy of your own entities.** The client that spawned an entity also drew a pooled copy of it, driven by the server's echo of its own updates. That copy is gone by default. **What you do:** if a single-client demo relies on it, turn on **Render Local Owner** on the map profile's Actor Pool Backend Config. See [Rendering backends](../runtime/rendering-backends.md#the-default-the-actor-pool). - **A destroyed entity stays destroyed.** An update that arrived just after an entity's destroy event drew it again for about 12 seconds, and **Destroy Crowdy Entity** kept sending updates for the length of **Destroy Delay**. Now sending stops when the destroy is called, and updates in the half second after a destroy event are not drawn. **What you do:** nothing. See [Entities and spawning](../runtime/entities-and-spawning.md#destroying). - **Ownership moves redraw the entity.** When ownership of an entity moves to or from your client, the actor pool sets its drawing up again on the next update: you stop seeing an entity you handed away frozen, and an entity handed to you is no longer pulled back to the echo of your own updates. **What you do:** nothing. ## 2026-10-31 (SDK v2.23.0) ### Nearby text chat and voice distance - **Nearby text chat.** **Send Proximity Text** on the new **Crowdy Proximity Chat** subsystem sends a line to every player within a distance you give, counted in chunks from the chunk your pawn stands in, and **On Proximity Text Received** hands you every line players near you send, with the sender's actor id and entity id. **On Proximity Text Refused** reports a line the server refused. A line is plain UTF-8, so it reaches CrowdyJS and CrowdyCPP players in the same app. The proximity text message an earlier SDK sent from C++ (`FTextMessageRequest`) now carries the text alone: its `UserID` and `Username` still compile but are not sent. **What you do:** nothing, unless you want local chat. See [Nearby Text Chat](../services/nearby-text-chat.md). - **How far your voice reaches.** **Set Voice Chat Distance** and **Get Voice Chat Distance** on the Crowdy SDK subsystem set it from 0 to 8 chunks; the default stays 8, the farthest the server relays, so nothing changes until you set it. **What you do:** set it if voice should stay local. See [How far your voice reaches](../services/voice-chat.md#how-far-your-voice-reaches). - **Voice is now sent from where your pawn stands.** Earlier versions addressed every voice frame to the chunk last given to **Set Player Current Chunk Coordinates**, which the SDK never set, so voice reach was measured from the origin chunk. **What you do:** nothing; with a pawn, voice now follows the player. - **Audio that is not the microphone, for C++.** `UVoiceChatSubsystem::FeedVoicePcm` sends 48 kHz mono samples as your voice through the microphone's own gate and encoder, and `EndVoiceFeed` ends the burst. **What you do:** nothing, unless you voice a bot or play a recorded line. See [Sending audio that is not the microphone](../services/voice-chat.md#sending-audio-that-is-not-the-microphone). ## 2026-10-30 (SDK v2.22.0) :::caution[Voice breaks between SDK versions: every player needs the new version.] Voice chat has a new packet format, shared with the CrowdyJS and CrowdyCPP SDKs. Players on an earlier SDK version cannot hear players on this one, and the reverse. Update every client that should take part in voice. ::: ### Voice chat - **Voice is rewritten on the shared voice format.** It uses Opus in 20 ms frames, with reordering and loss concealment, and it needs no player pawn, so it works in any map. The microphone and the Play, Mute, owner echo and timeout settings carry across map changes. Up to 16 speakers are heard at once, and received voice plays about 120 ms behind. **What you do:** update every client that should take part in voice; your existing voice graphs keep working. See [Voice Chat](../services/voice-chat.md). - **New nodes to see the microphone and who is talking.** **Get Voice Mic State** returns Off, Starting, Capturing, No Device or Error, and **On Voice Mic State Changed** fires when it changes. **On Voice Speaker Talking** gives a **Speaker Uuid** and **Talking**: talking means voice above a silence gate, and it ends about 0.3 seconds after the speaker stops. A silent microphone sends nothing. In C++ they are `GetVoiceMicState()`, `OnVoiceMicStateChanged` and `OnVoiceSpeakerTalking` on `UCrowdySDKSubsystem`. **What you do:** nothing, unless you want a talk indicator. See [Showing who is talking](../services/voice-chat.md#showing-who-is-talking). - **Toggle Owner Echo is now an exec node.** Earlier versions never ran it from Blueprint. **What you do:** wire it into a graph if you called it there, and turn it off again after the check. See [Confirming capture with owner echo](../services/voice-chat.md#confirming-capture-with-owner-echo). - **Other players are muted until Play Voice Chat.** The six existing voice nodes keep their names. **What you do:** call **Play Voice Chat** once if your game should hear others. **Set Voice Chat Stream Timeout Threshold** now clamps its value to 0.5 seconds up to 1 hour. - **The old voice delegates are removed.** `OnAudioNotify`, `OnAudioDataGenerated`, `OnAudioDataReceived` and `OnStreamTimeout` on `UVoiceChatSubsystem` are gone, and so are the two delegates of the audio device monitor. This breaks a C++ build that binds one of them. **What you do:** bind `OnVoiceSpeakerTalking` on `UCrowdySDKSubsystem` instead of `OnAudioNotify`. See [Blueprint Delegates](../reference/delegates.md). - **Microphone handling.** A missing microphone shows No Device and voice chat opens it as soon as one is available. On Windows a changed default microphone restarts capture by itself. A microphone that delivers nothing goes to Error with a hint about the OS microphone permission. **What you do:** nothing. - **Platform notes.** Linux has no engine audio capture, so voice capture reports it is not supported there. A packaged Mac game needs `NSMicrophoneUsageDescription` in its Info.plist, which the engine's Mac template does not include. **What you do:** on Mac, add the key; see [Platforms](../services/voice-chat.md#platforms). - **Voice counters for C++.** `UVoiceChatSubsystem::GetStats()` returns packets sent and received, frames played, concealed and dropped, and the speakers heard right now. **What you do:** nothing; read it from a debug overlay or a soak test if useful. See [Watching activity](../services/voice-chat.md#watching-activity). ### Warnings where things used to fail silently - **A bad `CrowdyRecipient`, `CrowdyDecay` or `CrowdyDistance` value now warns.** A meta value that is not a valid name logs a warning that lists the valid names, and the default is used. A Blueprint replicated event gets a compiler warning on the node. **What you do:** fix the name the warning shows. See [Recipients and routing](../runtime/recipients-and-routing.md) and [RPC meta keys](../reference/rpc-meta-keys.md). - **A Multicast or SpatialMulticast event on an actor with no registered entity now warns.** The warning differs when the Crowdy Entity Component exists but has not registered yet, for example on a pawn before it is possessed: call the event after registration. If the actor has no component, add one. **What you do:** read the warning; the advice depends on which case it is. See [RPC events in Blueprint](../runtime/rpc-events-blueprint.md). - **Mark Crowdy State Dirty warns once per world.** It warns when the actor has no Crowdy Entity Component, or when the property name is not one the class has. **What you do:** add the component, or fix the name. See [CrowdyState](../runtime/crowdy-state.md). - **Get Server Value and Set Server Value warn once** when the name is not a value of the struct. **What you do:** fix the name. See [Call its functions, from Blueprint](../exec/from-blueprint/call-functions.md). ### Crash fixes - **No crash without a game instance.** **Get Chunk Coordinate At Location** and **Crowdy Is Connected To Server** no longer crash when called where there is no game instance, such as an editor world or a construction script. **What you do:** nothing. See [Recipients and routing](../runtime/recipients-and-routing.md). ### Remote movement - **No more one-frame flicks after a sender hitches.** When a player's game stalled and its next two updates arrived almost together, the actor pool's shipped policy extrapolated their tiny gap up to 200 times, so other players saw that player flick far away for one frame. Extrapolation now measures the step over the recent average gap, so it continues at the sender's normal pace. A custom policy built on `TInterpolatedField` gets the same fix. **What you do:** nothing. See [The sample ring](../runtime/rendering-backends.md#the-sample-ring). ### Quieter logs - **Fewer lines for things that are fine.** Event payload registration no longer logs a Warning per struct. A send refused while connecting is reported once per second, with a count of the ones suppressed. A map with networking off no longer logs Errors at load. "Host not set yet" and the missing host subsystem message are trace-gated. **What you do:** nothing; a log filter you wrote for the old lines will not match. See [Log categories](../reference/log-categories.md). - **The `CrowdyMass.*` diagnostic commands are for development builds.** The four commands are no longer in Shipping builds, and they print normal output, not warnings. **What you do:** nothing. See [Mass troubleshooting](../mass/troubleshooting.md#commands-and-switches). ### Clearer messages - **Server Object refusals name the Blueprint nodes.** **Get Server Object** says it needs a Definition or an Owner; an empty Instance Id is named; an unknown function lists the type's functions; wrong parameters point at **Make Inputs**. **What you do:** nothing. See [Get a Server Object](../exec/from-blueprint/get-a-server-object.md) and [Troubleshooting](../exec/troubleshooting.md). - **Definition validation says what to fix.** It names the Timer, Message or Function that has no Name, and says how to fix a reserved name. See [Create a type](../exec/create-a-type.md). - **"Payload not registered" errors name the struct** and say how it registers. See [RPC events in C++](../runtime/rpc-events-cpp.md). - **Studio connection errors say what to check.** See [Projects and apps](../studio/projects-and-apps.md). ### Blueprint - **Call Server Function shows Outcome and Reason on the node.** They no longer sit under the small arrow; **Retryable** and **Code** still do. Wires you made keep working. See [Call its functions, from Blueprint](../exec/from-blueprint/call-functions.md). - **The ready pin on Get Server Variable and Get Server State reads "Is Ready".** It is the same pin, so existing wires keep working. An optional variable's own **Has Value** pin is unchanged. See [Get a variable](../exec/from-blueprint/read-and-follow-variables.md#get-a-variable). - **Struct field tooltips show on Server Object value pins.** See [What the pins are](../exec/from-blueprint/pins-for-every-type.md#what-the-pins-are). - **Crowdy SDK nodes sit under one "Crowdy SDK" palette category.** Search works as before and no graph changes. **What you do:** nothing; the paths in the docs now read `Crowdy SDK|...`. - **More tooltips.** Dispatch Game Event's target, decay and distance values have tooltips. The network statistics tooltips state their units, and **Dropped Client Notifies** is now **Client Notifies In Flight**. **What you do:** nothing, unless a graph or a note refers to the old name. ### Deprecated - **Reload Endpoints From Settings and Set Expected Actor Update State Size.** The first was already a no-op, and the second had no effect. Both still work. **What you do:** remove them from your graphs. See [Project settings](../reference/project-settings.md#setting-endpoints-at-runtime). ### Saved state - **A level-placed actor's fallback key no longer includes the PIE prefix.** The path-based key now matches between Play In Editor and packaged builds, except on World Partition maps. State saved from PIE under the old key is not read back. **What you do:** nothing for packaged data; expect PIE to start without the state it saved before. See [Persistence](#persistence). ### Studio - **Enter submits** the sign-in form and the first step of Create App. - **More confirmations.** Revoking all of a user's or group's grid grants, revoking a tier feature, and switching a Live app to Draft now ask first. - **Tooltips and feedback.** Registry badges and grid buttons have tooltips, and **Tools > Rebuild Crowdy Registry** shows a result notification. See [Sign in](../studio/sign-in.md), [Grids](../studio/grids.md), [Projects and apps](../studio/projects-and-apps.md) and [Inspector and Registry](../studio/inspector-and-registry.md). ### Mass - **An area spawner stops at the first failed spawn** and logs one line. **What you do:** read the log line and fix what it names, then spawn again. See [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md). - **Spatial-grid settings are greyed out** when the grid is off. ### For AI agents - **New `registry` op.** `-run=CrowdyStudio -op=registry [-class=substring] [-norebuild] [-out=file]` rebuilds the baked registry and prints every entity class's CrowdyEvents (recipient, decay, distance) and CrowdyState properties (owner-only, manual dirty, heartbeat, OnRep) as JSON. **What you do:** use it instead of reading the sources when you need an event's routing. See [Sync from the command line](../studio/config-sync.md#sync-from-the-command-line). - **The docs' AI helper files come as an index plus per-section files and a slim symbol index.** See [For AI Agents](../for-ai-agents.md). ## 2026-10-29 (SDK v2.21.2) - **Read kit actors without a copy.** The Crowdy Kit Actor subsystem gains **Get Kit Actor Count** (**Kind**) and **Get Kit Actor** (**Kind**, **Index**, **Kit Actor** out, returns a bool), and C++ `ViewKitActors(FName Kind)`, which returns a const reference. **Get Kit Actors** still works but copies everything. **Kind** None means every kind for the nodes; for `ViewKitActors` it is the actors of no named kind. **What you do:** a per-tick read, such as a Mass map's Blueprint, switches to Count and Get Kit Actor; follow an actor by its id, since its index changes as others come and go. See [Show a starter's actors](../exec/use-game-kit-starters.md#show-a-starters-actors). - **Kit actors are placed by the session's chunk size.** They used a fixed 1600; they now use `UCrowdyGameSession::SetChunkSize`. `FCrowdyKitActorState` gains `ChunkPosition`, the raw chunks the hub sent, and `Location` stays in world units. **What you do:** nothing. See [Show a starter's actors](../exec/use-game-kit-starters.md#show-a-starters-actors). - **A starter's On Changed runs once at Ready.** **On X Changed** and **On Variables Changed** no longer also run before Ready, so a snapshot pushed while Connecting does not announce. **What you do:** nothing, unless a graph relied on the early run. See [Follow a variable](../exec/from-blueprint/read-and-follow-variables.md#follow-a-variable). - **Owner Values the object does not read report no value.** Inventory, Wallet and Progress of another player's `CSO_Kit_Player`, or of any while signed out, now give **Has Value** false, not an empty value with true. **What you do:** branch on **Has Value** where you showed an empty one. See [Values only the player reads](../exec/use-game-kit-starters.md#values-only-the-player-reads). - **Owner Values are read again after a pushed hub restart.** **What you do:** nothing. See [Values only the player reads](../exec/use-game-kit-starters.md#values-only-the-player-reads). - **A snapshot's value wins over an Owner Value of the same field.** In a customized starter whose snapshot carries a field that is also an Owner Value, the value used to freeze. **What you do:** nothing. See [Values only the player reads](../exec/use-game-kit-starters.md#values-only-the-player-reads). - **No Get Server State where nothing is visible.** It is not offered for a Server Object whose State has no player-visible variables, such as `CSO_Kit_Npcs`; an existing node on one fails to compile with a message. **What you do:** remove the node. See [Get a variable](../exec/from-blueprint/read-and-follow-variables.md#get-a-variable). - **Config ids that differ only by case are refused.** **Add starter** and **Deploy** refuse a starter Config with two ids of one kind (item, currency, faction, counter, pity group) that differ only by case, since the SDK reads them as one. **What you do:** rename one. See [Add a starter](../exec/use-game-kit-starters.md#add-a-starter). - **Bad kit actor updates are dropped.** An update with a **Name** over 128 bytes or a **Mode** over 32 bytes is dropped, and so is one whose id, hp, max hp or owner is not a whole 64-bit integer. **What you do:** nothing. See [Show a starter's actors](../exec/use-game-kit-starters.md#show-a-starters-actors). - **Call as Developer from the command line reaches a starter's read-only player functions.** `-op=call` calls them as the developer, today the npcs starter's `state`. **What you do:** nothing. See [Call a function as the developer](../exec/deploy-with-server-compute.md#call-a-function-as-the-developer). - **The director publishes less.** While an encounter runs it publishes only events on a hit or a build; the next tick, at most 500 ms later, carries the snapshot. The hit reply still returns health at once. **What you do:** nothing. See [Defend against waves](../exec/use-game-kit-starters.md#defend-against-waves). - **The zone's realtime event carries `centerZ`.** The ZoneChange event gains `centerZ` beside `centerY`, the same ground value. **What you do:** nothing. See [Shrink a circle](../exec/use-game-kit-starters.md#shrink-a-circle). - **A sign-in can be cancelled without signing out.** `CancelSignIn()` on the Crowdy Authentication subsystem (and on the Crowdy SDK Subsystem) stops a restore, a password sign-in, an email link or social sign-in waiting for the browser, or one held for the terms. Only the call's `OnError` answers, with `Sign-in cancelled.`, and a cancelled restore keeps the saved session, which `Logout` would delete. `IsSignInPending()` says when a Cancel button makes sense. **What you do:** if your sign-in screen's Cancel or Back button calls `Logout`, call `CancelSignIn` instead so a returning player keeps their saved session. See [Cancel a sign-in](../services/authentication.md#cancel-a-sign-in). ## 2026-10-28 (SDK v2.21.1) - **Blueprint sign-in starts from the Game Mode, not the Game Instance's Event Init.** A Blueprint Game Instance's **Event Init** runs before the SDK's subsystems exist, so the earlier Blueprint examples, which signed in and bound events there, failed: **Login** and **Restore Session** reported "subsystem not found" and **Bind Event** bound nothing. The examples now run from the Game Mode's **Event BeginPlay** and check **Is Signed In** first, since BeginPlay runs again in every map. C++ examples, which use `Init` after `Super::Init()`, are unchanged. **What you do:** if your Blueprint Game Instance signs in or binds SDK events in Event Init, move that graph to your Game Mode's Event BeginPlay. See [Signing in](../services/authentication.md#signing-in). - **Two packaged clients can sign in as different accounts.** Clients sharing a `Saved` folder (Development copies in one folder, or any Shipping copies of one Windows user) shared one saved session. Start each extra client with `-crowdysession=`, and it keeps a session of its own; with no switch the default session is used, so existing sign-ins still restore. **What you do:** nothing, unless you run several clients from one folder. See [Two packaged or standalone clients](./testing-locally.md#two-packaged-or-standalone-clients). - **A deploy leaves a dropped type's Server Objects to the platform.** The platform now stops the Server Objects of a type a deploy or a rollback drops, keeping their saved state, and drops that type's switch, so the SDK no longer restarts those types before it deploys. `-op=deploy`'s result loses `stoppedRemoved` and `stopRemovedFailed`, and `-op=instances` gains `orphaned`, true for a Server Object whose type the active version does not deploy. **What you do:** a script that reads `stoppedRemoved` or `stopRemovedFailed` stops reading them. See [From the command line](../exec/deploy-with-server-compute.md#from-the-command-line). - **CrowdyCPP 0.74.0.** The realtime library moves from 0.73.0 to 0.74.0, which reads `orphaned` on each Server Object; the SDK sends and receives as before. **What you do:** nothing. ## 2026-10-26 (SDK v2.21.0) :::caution[Updating from 2.20.0: two things to do] 1. **Run Generate Server Code once on every Server Object type** (the **Generate** button on Server Compute does all of them). Generated crates now pin ckx-sdk 0.12.1, and Server Compute refuses to deploy a crate generated before; your `logic.rs` is kept. 2. **In C++, add an `FName Code` parameter to a handler bound with `AddDynamic` to the Call Server Function action's On Success or On Failed.** The delegate has a fifth parameter, so the build breaks until you do. ::: ### Game Kit starters - **The Game Kit starters are Server Object types.** **Add starter** on Server Compute, or `-op=addstarter`, adds any of the platform's seventeen [Game Kit](/game-kit/intro) starters (`world`, `matchmaker`, `session`, `leaderboard`, `player`, `market`, `npcs`, six game modes, `deck`, `arena`, `race` and `ball`) to the project as an ordinary type, with the starters it needs: its Rust under `Server//`, its definition `CSO_Kit_*` under `/Game/ServerObjects/GameKit/`, its values as C++ structs in `CrowdyGameKitTypes.h`. A game uses it with the nodes it already knows, and the definition's new **Game Kit** category holds what only starters have. With `world` added, `world` is the app's root and the **Game Kit World** setting points **Get Players Online** at it. `-op=removestarter` removes a starter and those that need it. **What you do:** nothing, unless you want a starter. See [Use the Game Kit starters](../exec/use-game-kit-starters.md). - **Refused, and a Code on every call's answer.** `ECrowdyServerCallOutcome` has a new last value, **Refused**, for a starter's refusal of the game's own, such as a full match, and `FCrowdyServerCallResult` a `Code` (`FName`), such as `full`; your own types never answer Refused. The **Call** nodes have an advanced **Code** pin, and the Call Server Function action's delegate `FCrowdyServerCallDone` has a fifth parameter, `FName Code`. **This breaks a C++ build** that binds On Success or On Failed with `AddDynamic`. **What you do:** add the parameter; give a Blueprint Switch on the outcome that lists every pin a **Refused** pin if it calls a starter. See [Refusals and their codes](../exec/use-game-kit-starters.md#refusals-and-their-codes). - **Find Match, Is My Turn and Get Turn Player.** An async **Find Match** node joins a matchmaker queue, follows it and accepts the match; **Is My Turn** and **Get Turn Player** read a session's turn. In C++, `CrowdyKit::FindMatch` and its siblings in `CrowdyGameKitMatch.h`. **What you do:** nothing. See [Find a match and take turns](../exec/use-game-kit-starters.md#find-a-match-and-take-turns). - **A starter's actors are drawn as classes you choose.** A starter whose server code moves actors, such as the mobs of `npcs` or the ball, has them drawn as the classes its **Kit Actors** names, each with a **Crowdy Kit Actor** component holding its **Kit Actor** (with the **Instance Id** of the hub to call for it); the new **Game Kit Actor Starters** setting cooks them, and **Kit Actor Timeout** in the map profile ends one that stops updating. **What you do:** nothing. See [Show a starter's actors](../exec/use-game-kit-starters.md#show-a-starters-actors). - **The Crowdy Server Object component reports where the player is.** For a starter with a **Position Function** (the arena, race and ball), the component sends the owner's place by itself, on real time so a paused game keeps reporting; new settings **Position Report** and **Snap To Kept Position**, and the event **On Position Kept**. **What you do:** nothing. See [Report where the player is](../exec/use-game-kit-starters.md#report-where-the-player-is). - **Grant and deposit soft currency from Studio and the command line.** The `player` starter's row on Server Compute has **Grant soft currency...** and **Deposit soft currency...**, and the command line `-op=grant` and `-op=deposit`; **Call as Developer** and `-op=call` fill a starter function's empty `Ref` with a fresh one. Soft currency is game state, never real money. **What you do:** nothing. See [Grant and deposit soft currency from Studio](../exec/use-game-kit-starters.md#grant-and-deposit-soft-currency-from-studio). ### Parties, chat, emotes and spectating - **Parties, guilds, chat rooms, emotes and spectating, as the Game Kit's other clients do them.** The **Crowdy Kit Social** subsystem and its nodes make parties, guilds and chat rooms on teams and channels, with **Send Chat** and **On Chat Message**; **Send Emote** and **On Emote** on **Crowdy Kit Emotes** reach the players nearby, within two budgets (`crowdy.kit.emotes.*` console variables); the **Crowdy Spectator** component keeps a player with no avatar receiving what happens near it. A client on 2.20.0 or older logs a Warning for each emote and spectator beat it hears. A CrowdyEvent whose generated type id would be 101, which the Game Kit's emote uses, takes another and logs a Warning naming both. **What you do:** nothing; if that Warning names one of your events, players on this SDK and on an older one stop exchanging it until they all update. See [Play together](../services/play-together.md). ### Deploying and Server Compute - **A deploy builds in parts, reuses unchanged code, and stops the types it removes.** A project over 16 crates or 2 MB of source is built in parts instead of refused (staging versions with stand-ins named `ck_staged_`, a reserved prefix); a crate whose files match a revision the app holds is not built again (**Rebuild All and Deploy** and `-rebuild` build everything); and the running Server Objects of a type the deploy leaves out are stopped just before it. `-op=deploy`'s result gains `builds`, `reused`, `stagedVersions`, `restoredVersion`, `artifacts`, `stoppedRemoved` and `stopRemovedFailed`, and its top-level `buildId`, `buildStatus`, `buildLog` and `sdkVersion` describe the last build only and are absent when nothing was built. **What you do:** rename a type whose Type Name starts with `ck_staged_`; a script that reads `buildStatus` reads `ok` and `builds` instead. See [A deploy in parts](../exec/deploy-with-server-compute.md#a-deploy-in-parts) and [The deploy's result](../exec/deploy-with-server-compute.md#the-deploys-result). - **Server Object crates pin ckx-sdk 0.12.1.** Generate writes `ckx-sdk = "0.12.1"` in a crate's `Cargo.toml` (it was `0.7.0`), and Deploy refuses a crate generated before. A **Generate** button on Server Compute, and `-op=generate`, generate every type that is out of date. **What you do:** press **Generate** once after updating; it keeps your `logic.rs`. See [Generate it](../exec/write-server-logic.md#generate-it). - **A starter's variables have their Get and On Changed nodes.** On a starter's definition the menu offered no **Get** or **On Changed** for its variables, and a pasted one failed to compile; they work now, and a flag such as `bHasWave` has a **Get** and no **On Changed**, since **On Wave Changed** reports it. **What you do:** nothing. See [Read a value that may be none](../exec/use-game-kit-starters.md#read-a-value-that-may-be-none). - **A type the active version lacks says it is not deployed**, as a type never deployed does, after you make an older version active; and a call while the connection redials into a paused or switched-off app reports **Paused** where it reported Timeout, as do the platform's new pause statuses. **What you do:** nothing. See [Outcomes](../exec/troubleshooting.md#outcomes). ### The command line and agents - **Drive the editor, Server Compute and Crowdy Studio from a shell.** The Server Compute commandlet gains `whoami`, `generate`, `exporttype`, `newtype`, `edittype`, `removetype`, `renametype`, `addstarter`, `removestarter`, `grant`, `deposit`, `restart` without a type, `enable` for the whole app, more of `logs` and `stats`, and `batch` (several operations in one editor start); the new `-run=CrowdyStudio` commandlet syncs, creates apps and manages access features, teams, channels and grids. Operations that write the project refuse while any Unreal Editor is running unless you pass `-editoropen`, and those that change the Game Kit settings refuse a read-only `DefaultGame.ini`. **What you do:** nothing; point a coding agent at [For AI agents](../for-ai-agents.md#drive-the-editor-without-a-window). See [From the command line](../exec/deploy-with-server-compute.md#from-the-command-line) and [Sync from the command line](../studio/config-sync.md#sync-from-the-command-line). ### Sign-in - **On Login fires for every failed restore.** **On Login** now fires with `bSuccess` false for every failed session restore, `No saved session found` on a first launch included. **What you do:** if your false branch shows an error, skip that message, or call **Restore Session** only when **Has Saved Session** is true. See [Restore a saved session](../services/authentication.md#restore-a-saved-session). - **A sign-in under way is cancelled by a logout or Clear Saved Session.** A sign-in, token mint, refresh or link still in flight then reports `Signed out before sign-in finished.` on the caller's **On Error** and signs nobody in. **What you do:** call **Clear Saved Session** only when no sign-in is under way. See [Session persistence and sign-out](../services/authentication.md#session-persistence-and-sign-out). - **Other sign-in changes.** A sign-in held for consent fails at once in a process with no window instead of waiting forever; password sign-in and the app token are retried when the platform is busy (`crowdy.net.retry.busy`); a token answer with no runtime gate clears a pause; Server Objects reconnect when another account signs in; Development builds have `crowdy.auth.signin.file`, `crowdy.auth.status` and `crowdy.auth.signout`. **What you do:** nothing. See [Authentication](../services/authentication.md) and [Sign in from the console](../guides/testing-locally.md#sign-in-from-the-console). ### Crowdy Studio - **Studio fixes.** A team role offers the team permissions (Manage Team, Manage Members, Manage Roles, Invite Members) and no longer Send Messages; a maximum set back to unlimited shows unlimited; an organization-token sign-in survives a network error; the module loads in the Default phase. **What you do:** nothing. See [Teams and channels](../studio/teams-and-channels.md). ### Runtime and Mass - **The actor tracker's Configure applies only before the tracker starts**, which is before any Blueprint can call it, so it now logs a Warning and changes nothing; the node is deprecated. **What you do:** set the limits in the map profile or the developer settings and remove the call, Blueprint or C++, or the actors past the profile's limit stop being tracked. See [The actor tracker](../runtime/entities-and-spawning.md#the-actor-tracker). - **An actor id that is not hexadecimal keys apart.** **To Guid** now hashes such an id instead of reading it as `FFFFFFFF` parts. **What you do:** derive again a key you saved from such an id. - **A promoted shell is hittable only on its character mesh** and on components tagged `CrowdyTraceCollision`, and a Mass trace the grid cannot handle tests every entity. **What you do:** tag a hand-made hit box `CrowdyTraceCollision`. See [The near tier component's settings](../mass/promote-close-entities-to-actors.md#the-near-tier-components-settings). - **Smaller changes.** A **Watch Record** added from a records handler runs once; the typed **Call** node labels its limit pin **Call Timeout** when the function has a **Timeout** input; an **Allowed Exec Gateway Domains** entry that is not a domain is named in a Warning; `crowdy.registry.dump` logs the baked registry; **Sync Actions Into Bank** can be called from a script. **What you do:** correct a gateway domain entry the Warning names; nothing else. ### CrowdyCPP 0.73.0 - **CrowdyCPP 0.73.0, and On Send Refused.** The realtime library moves from 0.67.0 to 0.73.0; the SDK sends and receives as before. The platform has two access tier permissions for realtime messages, `use_text_chat` and `use_direct_messages`, and the new **On Send Refused** event (`UCrowdyCppReplicationSubsystem::OnSendRefused`) reports each send the server refused and the permission it needs. **What you do:** nothing; bind On Send Refused if the player should hear that a chat or an event went nowhere. See [When the server refuses a send](../runtime/connection-and-reconnect.md#when-the-server-refuses-a-send). ## 2026-10-16 (SDK v2.20.0) :::caution[After updating, generate the server code of every type with a timer or a player event before you deploy.] The generated code changed for types with a timer, **On Player Joined**, **On Player Left** or **Remove Members Who Leave**. Until you run **Generate Server Code** on each of them, Server Compute marks it **Out of date** and refuses to deploy the project, and so does `-op=deploy`. Your `logic.rs` is kept. If CI deploys, commit the regenerated `Server/` folder. ::: - **A Server Object type can ask for more platform data.** A new **Platform Data** category in the definition holds **Reads World Data**, moved there from **Access**, and eight new settings: **Reads Players**, **Changes Players' App State**, **Changes the World**, **Reads Grids**, **Manages Grids**, **Grants Grid Permissions**, **Reads Players' Access** and **Manages Players' Access** (`bReadsPlayers`, `bWritesPlayerState`, `bWritesWorld`, `bReadsGrids`, `bManagesGrids`, `bGrantsGridPermissions`, `bReadsAccess`, `bManagesAccess`). Deploy asks the platform for a permission for each one ticked, so the server code can read players, change a player's app state, write voxels, read or manage grids, grant grid permissions, or read and manage players' access to the app. Before, Deploy could ask only for `players.read` and `world.read`, and the platform refused the others. **What you do:** tick what your server code uses, and deploy. See [Platform data](../exec/access-members-and-timers.md#platform-data). - **Deploy warns about platform data the code uses without asking for it.** Before it sends anything, the Deploy dialog lists each call into platform data whose setting is off, and `-op=changes` and `-op=deploy` add them as `warnings`. They do not stop a deploy. See [Deploy](../exec/deploy-with-server-compute.md#deploy). - **A changed Platform Data setting is a change to deploy.** Before, ticking **Reads World Data** alone left the type at **No changes** with **Deploy** unavailable. The **Changed** badge now also lists platform data and player access. - **A type players cannot reach is closed to them.** A type whose functions are all **Server Only**, with no variable **Visible to Players**, no records, no members and neither player event, is now deployed with players kept out of it, and Server Compute says "closed to players". Acquiring one in a game is refused at once with a reason saying so; before, it failed after its first read. **What you do:** nothing; a game had nothing to read or call on such a type. Its next deploy lists it as changed (player access). See [A type players cannot reach](../exec/access-members-and-timers.md#a-type-players-cannot-reach). - **A refusal by the platform says why.** A call the platform refused used to read "This player may not call Feed on this Server Object". It now reads "The server refused the call to Feed: " followed by the platform's own words. A call refused because its type or app is switched off or paused is a different outcome, **Paused**; see the entry below. A call the platform could not place, for example with the app at its instance limit, adds the platform's words after "The server could not be reached". A Server Object waiting to be read gives the same reasons, and one that could not connect at all now says why, such as a paused app, instead of a word like "Rejected". Refusals by your server code's own rules (**Callable By**, **Owner Only**, **Cooldown**) read as before, and every `Outcome` and `bRetryable` is unchanged except for Paused. **What you do:** if your game compares `Reason` with the old text, compare `Outcome` instead. See [Outcomes](../exec/troubleshooting.md#outcomes). - **A Server Object type can set its own server limits.** A collapsed **Server Limits** category in the definition holds **Memory per Instance**, **Time per Call**, **Fuel per Call (Millions)** and **Calls Waiting** (`MemoryMegabytes`, `CallTimeLimitMs`, `FuelPerCallMillions`, `MaxWaitingCalls`). Before, every type ran with the platform's defaults: 64 MB, 2 seconds and 50 million fuel a call, and 1,024 waiting calls. Deploy sends only the ones you change, so a type that keeps them deploys as before, and a changed one is a change to deploy (the **Changed** badge lists server limits). A player's connection now waits up to 20 seconds for a call's answer instead of 10: the longest **Time per Call** plus time spent waiting behind other calls. **What you do:** nothing, unless a type needs longer calls, more memory, or less memory so more of them fit. See [Every setting](../exec/create-a-type.md#every-setting). - **Server Compute can restart a type, and shows what runs.** A running type has a **Restart** button beside its switch: its running Server Objects are saved, stopped and started again on the live version at their next call, without refusing any calls, and players watching one read it again. After a deploy that changed a type with Server Objects still running the previous version, the Deploy card offers **Move now** or **Leave them**. Each type's line adds how many of its Server Objects are running, a red line names any the platform will not place and why, the header shows the app's Server Objects and reserved memory against its limits ("3 of 16 Server Objects · 192 of 1,024 MB"), and the **Budget paused** badge gives the platform's reason. The command line adds `-op=restart -type=T -yes` and `-op=instances`; `-op=status` gives the counts and limits, and a deploy's result its `sdkVersion`, the `ckx-sdk` the platform compiled against. Before, the only way to move running Server Objects to a new deploy was to switch the type off and on, which refused calls meanwhile. This needs a platform with ck-exec 0.15 or later. **What you do:** nothing. See [Restarting a type](../exec/deploy-with-server-compute.md#restarting-a-type). - **Call a Server Only function by hand, and Developers Only.** A Server Only function's Details now has **Call as Developer...**, which calls it on a live Server Object as you, the app's developer, with its inputs and the answer shown; the command line adds `-op=call -type=T -fn=F -key=K -inputs= -yes`. **Callable By** gains **Developers Only**, for a function only a person should run: players and other server code are refused with "only developer tools may call this function". Before, a Server Only function could only be reached from other server code or a tool of your own. **What you do:** nothing; regenerate a type's server code after choosing Developers Only. See [Call a function as the developer](../exec/deploy-with-server-compute.md#call-a-function-as-the-developer). - **Get Players Online.** A new node, **Get Players Online** (`UCrowdyServerObjectLibrary::GetPlayersOnline`), gives how many players are in the app's world: the number the platform's realtime servers report, in which a player who leaves is still counted for a few minutes. The count is kept by the app's root Server Object, which Deploy now generates from `CSO_PlayersOnline` in the SDK's content and opens to players; before, the root kept nothing and players could not reach it. `CSO_PlayersOnline` works with every node that takes a definition, so **On Changed** follows the count. **What you do:** deploy once after updating the SDK: Server Compute offers a deploy until the app runs the new root, and until then **Get Players Online** returns false and logs why once. See [How many players are online](../exec/from-blueprint/read-and-follow-variables.md#how-many-players-are-online). - **A project with no Server Object types can deploy, and the SDK's root is tracked.** Deploy used to need at least one type, and a removed last type kept running. It now sends the Crowdy SDK's own root alone, which keeps Players Online and removes every type; the confirm says "This project has no Server Object types, so only the Crowdy SDK's root (Players Online) is deployed." When a deploy removes types, the confirm adds "Their saved state is kept and is used again if you deploy them again." The root is also compared like a type: once a deploy from this SDK has recorded it, a later SDK whose root code differs makes Server Compute offer a deploy (the root is not a row), and an app whose root an earlier SDK deployed is compared only after your next deploy. **What you do:** deploy when Server Compute offers it. See [Deploy](../exec/deploy-with-server-compute.md#deploy). - **Generate is stricter about `root`, `lib.rs` and macros.** Generate refuses the SDK's own root definition ("This is the Crowdy SDK's own root; its server code is built into every deploy, so there is nothing to generate.") and a project type named `root`, and tells you to choose another Type Name. It asks before rewriting a `lib.rs` that loads a module file of your own (a top-level `mod name;` other than `logic` and `types`), with **No** as the default. When the `impl` block in `logic.rs` uses a macro such as `my_macro!(...)`, Generate and the Server Code tab no longer insert stubs, since the macro may define some functions; they copy them (**Copy Stubs**) instead. The question that offers stubs now says it writes into the file on disk and to save or close the file in another program first, and the tab says when another program saved the file without the functions Generate added. **Out of date** now names the first line that differs. **What you do:** declare your own modules at the top of `logic.rs` (`mod helpers;` there loads `src/logic/helpers.rs`), never in `lib.rs`. See [The Server Code tab](../exec/write-server-logic.md#the-server-code-tab). - **Platform data hints are quieter and clearer.** The warning for a call into platform data the type did not ask for now ignores code in Rust raw strings and in `#[cfg(test)]` items. **Reads Players** shows ticked and greyed, with a tooltip naming why, when **Members From** is **Crowdy Team**, the records belong to Crowdy Teams, or a function players may call has a **Required Feature**; a Required Feature on a Developers Only or Server Only function no longer asks for `players.read`. **What you do:** nothing. See [Platform data](../exec/access-members-and-timers.md#platform-data). - **A Cooldown or Required Feature on a function players cannot call is flagged.** A **Developers Only** or **Server Only** function with either shows a warning under it, and Deploy lists it ("... Cooldown has no effect, since only players' calls are limited." or "... Required Feature has no effect, since only players are checked."), because only players are limited and checked. **What you do:** clear the setting, or let players call the function. See [Required Feature](../exec/access-members-and-timers.md#required-feature). - **Call as Developer reaches built-in functions, and big ids need quotes.** A built-in member function such as **Add Member** (when **Members From** is **This Object**) has a play button on its row in the Members list, and `-op=call -fn=` finds functions by name, built-ins included. In the command line's JSON inputs, a 64-bit integer above 9007199254740991 must be a string, for example `"Player": "9007199254740993"`; a larger unquoted number is refused. **What you do:** quote such ids in `-inputs`. See [Call a function as the developer](../exec/deploy-with-server-compute.md#call-a-function-as-the-developer). - **Server Compute compares limits the way the platform applies them.** A limit that is out of range counts as the nearest value in range, and a limit written as a string is read as a number, so another tool's manifest no longer shows "server limits" as changed forever. After a deploy, the offer to move running Server Objects decides which ran the previous version from the Server Objects read as the deploy was sent (by their epoch), not from your computer's clock. **What you do:** nothing. See [Overview](../exec/deploy-with-server-compute.md#overview). - **A call past its Time per Call restarts the Server Object.** The platform answers the call `DeadlineExceeded` (the SDK reports **Timeout**, see the entry below for the reasons) and restarts that Server Object from its last save, so changes since the last save are lost. The limit covers the function itself, not the time a call waits behind others. **What you do:** keep functions short, and set **Save Every** low on a type whose recent changes matter. See [Server Limits](../exec/create-a-type.md#every-setting). - **Get Players Online waits once a minute when the app has no root.** If the app has no Crowdy SDK root (deployed with an older SDK, or never deployed from Server Compute), the root now asks again once a minute, with the reason in the log, instead of every few seconds; once the app is deployed from Server Compute the count arrives without restarting the game. **What you do:** nothing. See [How many players are online](../exec/from-blueprint/read-and-follow-variables.md#how-many-players-are-online). - **Retries are spread out.** Every Server Object retry and redial wait is now stretched by a random amount, up to half again: a failed read's first retry waits 1 to 1.5 seconds instead of exactly 1, a redial 1 to 1.5 seconds, then 2 to 3, up to 30 to 45, and a busy call's retries 0.25 to 0.375 seconds, then 0.5 to 0.75. Many players no longer retry in step after a server restart. **What you do:** nothing. See [Waiting is not failing](../exec/troubleshooting.md#waiting-is-not-failing). - **A timeout says which side gave up.** The outcome is still **Timeout**, and `Reason` now tells the two apart. "AttackBoss ran past its Time per Call on the server: handler exceeded its deadline" means the server's own limit stopped the call (for a read, "The server took longer than this type's Time per Call to read this Server Object", or "watch"). "The server sent no answer in time; the connection may be slow or lost" means no answer reached the game within 20 seconds. **What you do:** nothing; show `Reason` as before. See [Outcomes](../exec/troubleshooting.md#outcomes). - **A call to a switched-off or paused server comes back Paused.** `ECrowdyServerCallOutcome` has a new last value, **Paused**: the game's server, or the type, is switched off or paused (by its owner, or because the app ran out of funds). Calls refused this way used to come back **Denied**; they now come back **Paused** with `bRetryable` true and a reason such as "The server is switched off or paused, so it refused the call to Feed: `village_beacon` is switched off in app 123". A read or watch of such a server keeps trying, and its waiting reason reads "The server is switched off or paused, so this Server Object waits: " followed by the platform's words; a connection refused this way reads "The server is switched off or paused for ``: " followed by the error. **What you do:** a Blueprint Switch on the outcome gains a **Paused** pin to wire, and code that only checked for Denied should handle Paused too. See [Outcomes](../exec/troubleshooting.md#outcomes). - **The Game Session tells you the app is paused.** `UCrowdyGameSession` (category **Crowdy SDK, Game Session**) has **Is App Paused** and **Get App Paused Reason**, both pure, and the event **On App Paused Changed** (`Paused`, `Reason`). It turns on when signing in or refreshing the app token reports the app paused, or when the realtime server refuses the player's updates because the app is paused, and off when a later token answer says the app is active, when the realtime server relays to the player again (or their updates go through for 15 seconds), and on sign-out. The realtime log line for that refusal now names the code `AppPaused`. **What you do:** show a "the world is paused" message instead of retrying. See [When the app is paused](../services/authentication.md#when-the-app-is-paused). - **The Game Session tells you the player's access ended, and Server Objects stop.** `UCrowdyGameSession` has the event **On Access Ended** (`OnAccessEnded`: `Reason`, `BlockedUntil`) and the pure **Has Access Ended**, **Get Access Ended Reason** and **Get Access Blocked Until**. It fires when the player's access to the app ends: suspended, revoked, expired, signed out by the platform, or the account blocked. Every Server Object then fails with `Your access ended: ` followed by the reason, a call in flight or made later finishes with the new outcome **Access Ended** (`ECrowdyServerCallOutcome::AccessEnded`, not retryable), and nothing reconnects. A successful sign-in clears it; a **Crowdy Server Object** component and a typed node set to **Find By Asset** get their object again on their own, and code that calls `Acquire` itself calls it again. When the end time of a timed block is learned after access already ended, the event fires once more with it. **What you do:** show the reason and **Blocked Until**, return the player to the sign-in screen, and give a Blueprint Switch on the call outcome an **Access Ended** pin. See [When the player's access ends](../services/authentication.md#when-the-players-access-ends) and [Outcomes](../exec/troubleshooting.md#outcomes). - **A revoked, suspended or missing access is told so too.** When a sign-in or the background token renewal is refused with `ACCESS_REVOKED`, `ACCESS_SUSPENDED` or `ACCESS_NOT_GRANTED`, **On Access Ended** fires with the server's message and **Blocked Until** carries a suspension's end, then the SDK stops renewing the token until the next sign-in. Before, only the Server Object connection's close and a blocked account ended access, so a game with no Server Object saw nothing. The realtime connection now also asks for a new app token when its reconnect is refused for an invalid token, so a sanctioned player hears it within seconds and a player whose access was restored reconnects, where before the reconnect failed until the game restarted. **What you do:** nothing beyond handling **On Access Ended**. See [When the app refuses the player's access](../services/authentication.md#when-the-app-refuses-the-players-access). - **A blocked account is told so at sign-in.** Sign-in, register, the magic link, a social sign-in and a restored session fail with the server's message when the account is blocked, **On Access Ended** fires with **Blocked Until** (an ISO-8601 time, empty when the block lasts until it is lifted), and the app token is no longer rotated in the background until the player signs in again. **What you do:** show the message your sign-in screen already receives. See [When the account is blocked](../services/authentication.md#when-the-account-is-blocked). - **Ban and unban a channel or team member.** New nodes **Ban Channel Member**, **Unban Channel Member**, **Ban Team Member** and **Unban Team Member**, and the C++ calls `BanChannelMember`, `UnbanChannelMember` on `UCrowdyChannels` and `BanTeamMember`, `UnbanTeamMember` on `UCrowdyTeams`. A ban takes an optional `Reason` of at most 500 characters (a longer one fails at once with a `PolicyViolation` error) and answers the member record with status `banned`; an unban answers `Was Banned`. A banned user's join, request to join or add fails with the new error code `Banned` in `ECrowdyChannelErrorCode` and `ECrowdyTeamErrorCode`, taken from the server's own code. **What you do:** nothing for existing code; a `switch` on the error code that lists every value needs a `Banned` case. See [Ban a member](../runtime/channels.md#ban-a-member) for channels and [Ban a member](../services/teams.md#ban-a-member) for teams. - **On Error shows the error on Channels, Teams, Avatars and sign-in nodes.** The latent nodes' **On Success** and **On Error** now share one set of data pins: the result, then `Error` (with its `Code`) and `Message`; the sign-in nodes (**Login**, **Register**, **Restore Session**, **Begin Magic Link Sign In**, **Begin Social Sign In**) carry `Result` and `Message`, and **Record Player Consents** carries `Message`. Before, Blueprint could not read the error on **On Error**. On **On Success**, `Error` and `Message` are empty; on **On Error** the result is empty. A graph linked to the old pins keeps its links. In C++ the multicast pin types are renamed (`FChannelAsyncResult`, `FTeamAsyncResult`, `FAvatarAsyncResult` and the rest of that family, replacing the `...AsyncOnSuccess` ones, and `FAuthAsyncResult`, `FRecordPlayerConsentsAsyncResult` for sign-in); a C++ handler bound with `AddDynamic` to one of these pins takes the added parameters. The per-call `FOnChannel...`, `FOnTeam...` and `FOnAvatar...` delegates are unchanged. **What you do:** wire `Error` or `Message` where a graph needs the failure. See the nodes on [Channels](../runtime/channels.md#the-full-surface), [Teams](../services/teams.md#blueprint-nodes) and [Avatars](../services/avatars.md#async-action-nodes). - **A Server Object gateway must be on the Game API's own domains.** The SDK now checks that the gateway the Game API names is on the Game API's own domains, as the C++ library's own connect does. A new Project Settings entry, **Crowdy SDK, Server Objects, Allowed Exec Gateway Domains** (`AllowedExecGatewayDomains`), lists extra domains for a server you host yourself. A gateway outside both is refused for good: every Server Object fails with "Could not open the connection to the server: the Game API named a gateway outside its own domains (...); if you host the server yourself, add its domain to Allowed Exec Gateway Domains in Project Settings", and it is not retried until you acquire an object again. **What you do:** nothing on the platform's servers; add your domain if you host the server yourself. See [Server Object gateway domains](../reference/project-settings.md#server-object-gateway-domains). - **Players who left while a Server Object was stopped are cleaned up.** When a Server Object restarts, the platform does not tell it about players whose connection closed while it was stopped. The generated code now keeps the connection counts it restored as unconfirmed: right after the restart it tells watching players to read again, and any call from a player confirms them. A player not heard from within two minutes is treated as having left (**Remove Members Who Leave** removes them, `on_player_left` runs); a player who reconnects in that time just continues, and `on_player_joined` does not run again. The timer name `crowdy_sessions` is now reserved, and a definition with a timer of that name is refused. **What you do:** run **Generate Server Code** on each type with **On Player Joined**, **On Player Left** or **Remove Members Who Leave**, then deploy; until you do, Server Compute marks it **Out of date** and refuses to deploy the project. Rename a timer called `CrowdySessions`. See [Players who left while it was stopped](../exec/access-members-and-timers.md#players-who-left-while-it-was-stopped). - **A changed timer keeps its old schedule if arming fails.** When a deploy changes a timer's **Runs**, running Server Objects arm the new schedule without stopping the timer first, so if arming fails the timer keeps its old schedule and the server log says "timer EndMatch keeps any old schedule: the changed one could not be armed: ...". A timer you did not change keeps its clock. **What you do:** run **Generate Server Code** on each type with a timer, then deploy; until you do, Server Compute marks it **Out of date** and refuses to deploy the project. See [Timers and events](../exec/access-members-and-timers.md#timers-and-events). - **The limit on running Server Objects is documented.** An app with no funds can run 16 Server Objects at once, within 1 GB; a funded app 16,384, within 64 GB. Past it nothing new starts and calls come back **Unavailable**. For many players, prefer one shared object that holds a record per player (or per area, match or team) over one object per player. **What you do:** nothing. See [Large groups](../exec/access-members-and-timers.md#large-groups). - **Trace lines and warnings are tidier.** With `crowdy.exec.trace` on, the Players Online root prints its missing Instance Id as `-` (`exec: root/- read ...`). Get Players Online's "has no count yet" warning prints once for each reason in each game instance, so once per Play In Editor client. **What you do:** nothing. - **A voxel update with more than 1,024 bytes of state is refused when you send it.** The send returns false with "the voxel state is N octets, over the 1024 a voxel update carries", instead of reporting the update queued and then dropping it with a warning. **What you do:** keep a voxel's state at 1,024 bytes or less. - **Records in Blueprint.** A Server Object that keeps [a record per entity](../exec/keep-a-record-per-entity.md) gains **Holds Records**, **Num Records**, **Watch Record** and **Stop Watching Record**, and the event **On Records Updated**. **Watch Record** takes a **Record Id** and an event (`FCrowdyServerRecordEvent`: **Object**, **Record Id**, **Changed**) that runs after each change of that record and when it is removed (**Find Record** is then false), and once at once when the record is already current; records dropped because the object **Failed** are not reported, **On Status Changed** is. **On Records Updated** (`RecordsUpdated`, an `FCrowdyServerRecordsUpdated`) is **On Records Changed** with each changed record's **Changed Variables**, which is empty when only its owner or team changed. **On Records Changed** is unchanged, and C++ already had `WatchRecord` and `OnRecordsChanged` with the changed fields. **What you do:** nothing. See [From Blueprint](../exec/keep-a-record-per-entity.md#from-blueprint). - **Variables added in the asset are read and followed by the typed nodes.** For a definition whose variables are added in the asset (the List form), **Get**, **Get Server State** and **On Changed** read and follow them with the same pins as for a struct; this is now stated on the pages. **What you do:** nothing. See [Read and follow its variables](../exec/from-blueprint/read-and-follow-variables.md#get-a-variable). - **Get Definition, and a widget without a component.** **Get Definition** is a new pure node on a Server Object, giving the definition asset it was made from, to pass to nodes that take one. A widget needs no component for a Server Object: **Get Server Object** with the widget as **Owner**, or a typed node with **Find By Asset**, already works, and the page now says so. **What you do:** nothing. See [Without the component](../exec/from-blueprint/get-a-server-object.md#without-the-component). - **Ping Server Objects.** A new latent node (`UCrowdyServerPingAction`; in C++ `UCrowdyServerObjectSubsystem::Ping`) asks the platform for an answer over the open Server Object connection: **On Success** gives **Round Trip Ms**, **On Failed** gives **Reason**. It never opens a connection, so with none open it fails at once with "No Server Object connection is open". The platform answers at most 60 pings every 10 seconds on a connection and drops the rest without a word, so the SDK refuses more itself with "Too many pings: the platform answers at most 60 every 10 seconds". **What you do:** nothing; ping on a timer of a second or more. See [Ping the platform](../exec/from-blueprint/call-functions.md#ping-the-platform). - **A Call node can give up sooner.** The Call Server Function nodes, typed and generic, have an advanced **Timeout** input in seconds, and C++ `UCrowdyServerObject::Call` a trailing `float TimeoutSeconds = 0.f`. It can only shorten the wait: 0 leaves the project's **Server Object Call Timeout**. When it runs out the call fails with **Timeout**, not retryable, and `Reason` reads "No answer within 5 seconds; FeedBeacon may still have run on the server"; an answer that arrives later is not reported. **What you do:** nothing. See [Give up sooner](../exec/from-blueprint/call-functions.md#give-up-sooner). - **Server Object connection timeouts are settings.** The Connection tab of Crowdy Studio's Project page has a **Server Objects** heading with **Call Timeout** (`ServerObjectCallTimeoutSeconds`, 11 to 120 seconds, default 20, above the longest **Time per Call**) and **Open Timeout** (`ServerObjectOpenTimeoutSeconds`, 1 to 60 seconds, default 10). They are written to `DefaultGame.ini`, shown read-only under **Project Settings, Crowdy SDK, Developer, Server Objects**, and apply from the next Play and to the editor's developer connections. A dropped connection is always reopened. Before, the call wait was fixed at 20 seconds, as the entries above say. **What you do:** nothing unless a game needs a different wait. See [Server Object timeouts](../reference/project-settings.md#server-object-timeouts). - **Server Objects can send messages.** A definition's new **Server Messages** section lists messages (`Messages`, an array of `FCrowdyServerMessage`: **Name**, **Payload** as variables added in the asset or a struct, an advanced **Server Name** and **Players Can Listen**). The type's server code sends one with `messages::send_(&payload)`, and everyone listening hears it once: each player who listens to that Server Object, and each Server Object type that listens to it. Before, a Server Object could tell players only through its variables, which keep the last value but not that something happened. A message is an event: it is not kept and not replayed. It is published only when the function that sent it succeeds, after that function's changes to the variables and in the order it sent them, and a function that refuses sends nothing. **Players Can Listen** is on by default; off, only a type that Listens To this one hears the message, it does not open the type to players, and a player trying to watch it gets a warning and nothing. Two messages with one **Name**, or one topic, are refused when you save. When a type has a message players can listen to and **Readable By** is not **Every Player**, or any variable is Owner Only or Team Only, the editor warns "Every player who can connect to this type hears its messages, whoever may read its variables. Keep private data out of messages.", since a message reaches every player who can connect, whoever may read the variables. **What you do:** nothing for an existing type. To announce an event, add a message, send it from `logic.rs`, then generate and deploy. See [Server messages](../exec/create-a-type.md#server-messages) and [Send a message](../exec/write-server-logic.md#send-a-message). - **Hear a message from Blueprint or C++.** Blueprint gets a typed node for each message players can listen to, **On BossDefeated Received (CSO_Boss)**: **Bind**, **Then** and **Received** execution pins and an output pin for each value in the payload, with **Target** or **Find By Asset** as on **On Changed**. **Watch Message** and **Stop Watching Message** (`K2_WatchMessage`, `K2_UnwatchMessage`; the event is an `FCrowdyServerMessageEvent` with **Object**, **Message** and **Payload**, read with **Get Server Value**) work by name. C++ has `UCrowdyServerObject::WatchMessage` and `UnwatchMessage` with an `FOnCrowdyServerMessage` listener. Nothing runs for a message sent before the game listened. A message that arrives while the object is reading its values is held, up to 32, and announced once the values are current; a message sent while the connection is closed is lost, not replayed; and a listen the platform refuses is asked again after a wait that grows from 1 second to 30, with one warning giving the platform's reason. In a record type, a message can arrive before the record change it follows, since records go out in batches. A connection allows 256 subscriptions, and each subscribe counts against the 120 calls every 10 seconds. **What you do:** nothing. See [Hear its messages, from Blueprint](../exec/from-blueprint/hear-server-messages.md), [from C++](../exec/from-cpp/hear-server-messages.md) and [A message never arrives](../exec/troubleshooting.md#a-message-never-arrives). - **A Server Object type can listen to another type's messages.** A definition's new **Listen To** list, beside **Can Call**, names types whose messages the type's server code hears; each is also callable, so Listen To joins the type's `calls`. In `logic.rs`, `listen::::(ctx, key)` starts listening to one Server Object by its Instance Id and `listen::::stop_(ctx, key)` stops, and the generator adds a handler to `Functions` for each message of a listened type, `on__(&mut self, ctx, key, payload)`, with no `payload` for a message with no values. A listen lasts, is renewed when the Server Object starts again, and takes effect only when the function that made it succeeds; if the platform refuses it, the function fails with `could not listen to boss.boss_defeated:` and the reason. A handler works like any other: its changes reach the watchers and an error puts the state back. A handler whose name is a function's or a timer's is refused when you generate. **What you do:** nothing for an existing type; regenerate and deploy a type after adding Listen To, and write the handlers Generate offers. See [Listen to a message](../exec/write-server-logic.md#listen-to-a-message). - **A Server Object type can be a Server Function Library.** A definition's new **Kind** (`Kind`, an `ECrowdyServerObjectKind`: **Server Object** or **Server Function Library**) makes a type that has functions and nothing else, like a Blueprint Function Library. It saves nothing between calls, has no Instance Id, and is answered by several instances at once; the platform calls it a spoke. Choosing it hides, and ignores everywhere, the variables, **Visible to Players**, **Readable By**, **Only One Instance**, **Holds**, members, areas, timers, player events, messages, **Listen To**, **Save Every** and **Stop When Idle For**; they stay on the asset and come back when the Kind does. A new **Scaling** category sets **Min Instances**, **Max Instances** and **Parallel Calls per Instance** (`MinInstances`, `MaxInstances`, `ParallelCallsPerInstance`; the platform's `replicas`, `max_replicas` and `concurrency`, 1 to 64, 1 to 64 and 1 to 256, defaults 1, 1 and 16): the server adds an instance, up to Max Instances, once calls have waited 2 seconds, and removes one after about 30 seconds nearly idle. Each instance counts toward the app's limit on running Server Objects (16 without funds), and the first call after a library has been idle can take over a second. Saving refuses a function that is **Members** or **Leader** or has a **Cooldown**, a **Max Instances** below **Min Instances**, and a Server Object that lists a library under **Listen To**; a library needs no State struct. Its generated crate implements `Spoke`, with `impl Functions for Library` in `logic.rs`. Server Compute's row says "Server Function Library" and "1 to 4 instances" in place of the variable count, the **Changed** badge and `-op=changes` list `kind` and `scaling`, and a deploy that changes a type's kind says so and that its saved data is kept. Before, every type was a Server Object. **What you do:** nothing for an existing type. To share logic that keeps nothing, make a library, generate, write `logic.rs` and deploy. See [Make a Server Function Library](../exec/server-function-libraries.md), [A Server Function Library](../exec/write-server-logic.md#a-server-function-library) and [Loot for the Hollow Giant](../exec/examples/hollow-giant-loot.md). - **Call a library from Blueprint or C++.** A library offers a **Call** node for each function, under **Server Function Libraries** in the palette, that needs no **Target** and no Instance Id, and no **Get**, **On Changed** or **On Received**. C++ has `UCrowdyServerObjectLibrary::CallLibraryFunction(WorldContext, Definition, Function, Inputs, OnDone, TimeoutSeconds)`, with an `FInstancedStruct` or an `FInstancedPropertyBag` as Inputs. The game instance's `UCrowdyServerObjectSubsystem` holds the library until it shuts down, so there is nothing to release, and `OnDone` runs exactly once: a null definition, a definition that is not a library or an unknown function completes at once as `BadRequest`, and no world as `Unavailable`, each with a plain `Reason`. **What you do:** nothing. See [Call a Server Function Library, from Blueprint](../exec/from-blueprint/call-a-library.md) and [from C++](../exec/from-cpp/call-a-library.md). - **A type can call any type.** A definition's new **Can Call Any Type** check-box (`bCanCallAnyType`), in **Can Call**, lets its server code call and listen to every type in the app, including types added later; Deploy writes `"calls": ["*"]` in place of the names. **Can Call** and **Listen To** still decide which types get typed helpers, and with the box ticked `logic.rs` also gets `calls::any(ctx, node_type, key, method, payload)`, which calls a type by name with an encoded payload and returns the undecoded reply. A Server Object or library whose **Can Call** lists a library gets a typed helper with no key. A call to a type the app does not run fails with "app N has no node type". **What you do:** nothing; tick it only for a type that calls types it cannot name when you write the code. See [Call any type](../exec/access-members-and-timers.md#call-any-type). - **CrowdyCPP 0.67.0.** The SDK's `dev` branch now builds on CrowdyCPP 0.67.0 (0.57.0 before; 0.61.0 added the platform calls above, 0.62.0 and 0.63.0 the input log's missing records, request cancelling, channel and team bans and the ck-exec close for a player whose access ended, 0.64.0 to 0.66.0 the Game Kit's client code and a randomized Server Object reconnect wait, 0.67.0 the rename that sends both halves of a player's name). The Game Kit has no Unreal nodes yet. Nothing in the SDK's API changed. **What you do:** nothing. - **A failed connection made for the root or a library names no type.** Its `GetFailureReason()` reads "Could not open the connection to the server: ..." (or "The server is switched off or paused: ..."), since that try did not tell the platform a type; before, it named the root or the library as if the problem were theirs. **What you do:** nothing. See [Waiting is not failing](../exec/troubleshooting.md#waiting-is-not-failing). - **Account settings in the game.** New nodes under **Crowdy SDK, Account**, and the same calls on `UCrowdyAuthentication`: **Request Password Reset** and **Resend Confirmation Email** (both work signed out and answer the same whether or not the address has an account), **Change Password**, **Add Password** (for an account made by magic link or social sign-in), **Sign Out Everywhere** and **Delete Account** (both then sign out here). A failure carries an `FCrowdyAccountError` whose `Code` names **Wrong Password**, **No Password**, **Password Already Set**, **Not Signed In** and **Rate Limited**. **What you do:** add what your settings and sign-in screens need; a game in a store that requires it offers **Delete Account** behind its own confirm dialog. See [Account settings](../services/authentication.md#account-settings). - **Sign-out ends the session on the server.** `Logout` on the Crowdy SDK Subsystem now tells the server to end the session, and every gameplay token made from it, before it forgets the token; before, the server session stayed valid until it expired. It does not wait for the answer, so signing out offline still works. Crowdy Studio's **Sign Out** does the same for an account sign-in. **What you do:** nothing. See [Session persistence and sign-out](../services/authentication.md#session-persistence-and-sign-out). - **The player's own access, and free access.** **Get My Access** reads the player's access to this app (`FCrowdyAppAccess`: **None**, **Active**, **Suspended**, **Expired** or **Revoked**, the tier, when it expires or a suspension ends, and whether it came from the free tier), also while the app refuses them. **Claim Free Access** claims the app's free access for a player whose sign-in was refused with `ACCESS_NOT_GRANTED` and finishes that sign-in; it never lifts a revoke. **What you do:** show a Get Free Access button where your sign-in screen shows that refusal. See [When the player has no access yet](../services/authentication.md#when-the-player-has-no-access-yet). - **Player names.** A new **Crowdy Players** subsystem (`UCrowdyPlayers`) reads players' public names, `FCrowdyPlayerProfile` (`UserID`, `Gamertag`, `Disambiguation`), one with **Get Player Name** or up to 100 with **Get Player Names**, and renames the signed-in player with **Set Player Name**, refused with **Name Taken** when another player has the pair. **What you do:** use them for nametags, rosters and a Choose Your Name step. See [Player Names](../services/player-names.md). - **Crowdy Studio has an Account page.** Under **Home**, for the account Studio is signed in with: your name, changing or adding your password, and signing out everywhere. **What you do:** nothing. See [Your account](../studio/sign-in.md#your-account). - **Server Compute's Overview is a list and a detail.** The types are listed on the left, with **Search types** and the filters **All**, **To deploy** and **Problems**; the selected type's details and controls are on the right. **Deploy** moved to the page's title row, with the number of types it changes, so it no longer scrolls away under a long list; the status line and **Deploy starter pack** sit in a strip under the tabs. **What you do:** nothing. See [Overview](../exec/deploy-with-server-compute.md#overview). ## 2026-10-12 (SDK v2.19.0) - **Signing in now asks the player to agree to the terms and confirm their age.** Since the server's v2.35.0 release, no gameplay token is issued until the player has agreed to the current documents and attested they are at least 18, or the age of majority where they live if that is higher. The SDK handles it for you: a sign-in, sign-up, session restore or token refresh that is refused for this reason is held, not failed, and a prompt in the Crowded Kingdoms colors opens over the game viewport with the five documents as links, two check boxes, **Accept** and **Decline**. **Accept** records the agreement and the sign-in the player started completes, with its usual events (`OnLogin`, `OnRegister`, `OnSessionRestored`, `OnAppTokenRefreshed`). **Decline** fails it with `Sign-in needs the terms and the age confirmation.` The prompt also appears in the middle of a session when a document gets a new version. **What you do:** nothing if the built-in prompt suits your game; make sure a sign-in screen does not time out a sign-in that is waiting for the player. A dedicated server or any process with no window cannot show the prompt, so sign such an account in once with a window. See [Let the player agree to the terms](../services/authentication.md#let-the-player-agree-to-the-terms) and, for the server side, [Terms and age of majority](/management-api/portals-and-app-tokens#terms-age-and-country). - **You can turn the prompt off and draw your own.** The developer setting **Show Consent Prompt** (`bShowConsentPrompt`, on by default) switches the built-in prompt off. New on `UCrowdyAuthentication`: the events `OnLegalAcceptanceRequired` and `OnLegalAcceptanceEnded` (`FOnLegalAcceptanceRequired`, `FOnLegalAcceptanceEnded`, no parameters), the functions `RecordPlayerConsents`, `DeclineLegalAcceptance`, `IsLegalAcceptancePending` and the static `GetLegalDocuments` (an array of the new `FCrowdyLegalDocument`, with `Title` and `Url`), and the Blueprint node **Record Player Consents** (`UCrowdyAuth_RecordPlayerConsents`). Bind the required event, show the documents with two check boxes, and call `RecordPlayerConsents(true, true, ...)` only after the player ticked both; close your UI on `OnLegalAcceptanceEnded`. Never record consent without the player's own accept. See [Draw your own prompt](../services/authentication.md#draw-your-own-prompt). - **Crowdy Studio shows the same prompt.** When the account you signed in to Studio with has not yet agreed to the current terms, Studio opens it in a window titled **Crowdy Studio: terms and age** before it carries on; declining stops there with a message. See [Agreeing to the terms](../studio/sign-in.md#agreeing-to-the-terms). - **The SDK now vendors CrowdyCPP 0.57.0,** the native client its transport is built on (the `CrowdyCppBridge` module), which carries the new consent call. **What you do:** nothing; you never include it directly. See [Installation](../installation.md). - **CrowdyMass is part of the SDK.** There is no CrowdyMass plugin to enable: the Mass modules ship inside CrowdySDK, along with the engine's MassGameplay and MassCrowd plugins they need. A map opts in by setting its map profile's **Backend Class** to **Crowdy Mass Rendering Backend**; the actor pool stays the default. The module names are unchanged, so a `Build.cs` that lists `"CrowdyMass"` keeps working, and the shipped assets now live under `/CrowdySDK/Mass/`. The starting-point profile `/CrowdySDK/Mass/Data/DA_CrowdyMassDefaultProfile` has **Enable Networking** on, and its folder is always cooked. **What you do:** remove the CrowdyMass entry from your `.uproject`'s plugin list, and from the `Plugins` list of any `.uplugin` of your own that names it, and delete the separate plugin folder if you have one. A C++ project that still names it stops at build time with an error naming the file that references it. If the editor offers to disable one of your own plugins because CrowdyMass is missing, answer No and edit that file instead. See [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md). - **The Mass subsystems exist only on a Mass map.** `UCrowdyMassEntitySubsystem` and `UCrowdyMassServerRecordsSubsystem` are now created only in Play In Editor and game worlds whose map profile has **Backend Class** set to **Crowdy Mass Rendering Backend**. On an actor pool map the Crowdy Mass nodes and C++ calls that need them do nothing, quietly: a **Crowdy Mass Area Spawner** spawns nothing and warns once, **Set Crowdy Mass Hit Zone Table** warns once and hits use the default zone, and **Receive Crowdy Event on Entity** and **Receive Server Values Changed on Entity** log once and receive nothing. **Play Crowdy Mass Action** and the action lookup still work there, because they read the actor's class. **What you do:** in C++ that needs the subsystem, gate your own world subsystem in `ShouldCreateSubsystem` so it exists only on maps that draw crowd rows (see [Add your own crowd data](../mass/add-your-own-crowd-data.md#register-the-field)), and check the result for null where you get the subsystem. See [Fix a crowd that does not look right](../mass/troubleshooting.md#crowd-nodes-do-nothing-on-this-map). - **The anim set's Bank is used at run time.** **Authoring Bank** on the **Crowdy Mass Anim Set** is now **Bank**. A config the SDK builds from the class renders through it, so a class whose anim set names a **Bank** animates with nothing else authored. **What you do:** open and resave your anim sets so the rename persists. See [Set up the animation set](../mass/set-up-the-animation-set.md). - **A derived crowd row stands where the actor's mesh stands.** A config the SDK builds from the class copies the mesh component's location and rotation into the mesh entry's **Local Transform**. Before, a derived row of a character floated by its mesh's drop and faced a quarter turn away. **What you do:** nothing for a derived class; an authored config sets its own **Local Transform**. If you gave a near tier shell's `Mesh` or its **Entity Transform Offset** a drop or a turn to make up for the old placement, remove it, or the promoted actor now sinks and faces backwards. A mesh nested under other components is placed by the whole chain of components up to the actor's root, and a mesh that is the actor's root gets no placement of its own. `crowdy.mass.derive` now prints where the derived config places the mesh ("placed at ... turned ..."), so you can check it. See [Make a character draw as a crowd row](../mass/make-a-character-draw-as-a-crowd-row.md#what-derivation-builds). - **The SDK turns off the engine's animation pass for you.** The CrowdyMass module switches off `MassConsumeInstancedSkinnedMeshAnimationProcessor` when the game starts, because the SDK hands each row's animation to the renderer itself. Before, a project had to add `Config/DefaultMass.ini` to do it. **What you do:** nothing; you can delete that entry. If `LogCrowdyMass` warns that more than one processor is filling a component's animation batch, something turned the engine's pass back on, such as ticking it in Project Settings, Mass, or a `DefaultMass.ini` that sets it to True; remove that. This holds in every project with CrowdySDK, including one that never draws crowd rows: a Mass crowd of your own that uses skinned meshes then animates through the SDK's pass, which covers moving instances in visible chunks; a crowd of your own that stands still gets no animation updates. The MetaHuman crowd plugin's own pass, if you use that plugin, is left alone; turn it off under `[/Script/MetaHumanCrowd.MetaHumanMassSimpleAnimationProcessor]` in `DefaultMass.ini` if you want. See [The engine's animation pass](../mass/turn-mass-on-for-a-map.md#the-engines-animation-pass). - **A Blueprint receive node keeps the action.** Before, binding **Receive Crowdy Event on Entity** to an event marked an action replaced both the event's body and the automatic action write on a crowd row, so the row stopped animating, and Blueprint had no node to start it. Now a bound node still replaces the body on a row, but the declared action is still written, so the row plays it. A C++ handler registered with `RegisterEventHandler` still replaces both, and writes the action itself with `CrowdyMassWriteAction`. **What you do:** nothing; if you worked around the old limit, you can drop the workaround. See [Receive its events, from Blueprint](../mass/from-blueprint/receive-its-events.md#what-a-bound-graph-replaces). - **Typed C++ calls on a crowd row.** `CROWDY_MASS_SEND_EVENT(Target, Class, Event, Args...)`, in `Events/CrowdyMassEventLibrary.h`, calls a `CROWDY_EVENT` declared on `Class` for the row `Target` names. The arguments are checked against the event's parameters when your code compiles. `UCrowdyMassServerValuesLibrary::GetServerValueAs(Target, Name, T& Out)` reads a row's server variable into a C++ value of exactly that type, and returns false and resets `Out` when the value has not arrived, the variable is missing or the type differs. `FindServerValueProperty` and `PropertyHolds` come with it. **What you do:** nothing; `SendEventToMassTarget(Target, UFunction*, const void*)` and `GetServerValueInto` still work. See [Call an event on it](../mass/from-cpp/call-an-event-on-it.md#call-a-crowdy-event-on-an-entity) and [Read its server values](../mass/from-cpp/read-its-server-values.md#read-a-value). - **An actor pool map pays less for Crowdy Mass.** On a map whose profile does not select the Mass backend, the two Crowdy Mass processors that ran every frame now sit out that world, and a **Crowdy Mass Near Tier** component does not tick there until something binds it. Destroyed entities of your own Mass skinned crowd on such a map now have their animation tracks released; before, they stayed allocated for the session. **What you do:** nothing. For a world subsystem of your own that needs the Mass entity subsystem, return `Super::ShouldCreateSubsystem(Outer) && GetDefault()->ShouldCreateSubsystem(Outer)` from `ShouldCreateSubsystem`. It then exists only on maps that draw crowd rows, and an actor pool map's log stays free of the engine's "Failed to initialize subsystem dependency" line. See [Add your own crowd data](../mass/add-your-own-crowd-data.md#register-the-field). - **The area spawner's list of ids is named in the editor.** `ACrowdyMassAreaSpawner::GetSpawnedUUIDs` now shows as **Get Spawned UUIDs**. See [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md). - **The Game Model API is removed.** Gone: containers and attributes (the `CrowdyModel` and `CrowdyContainer` metadata), model functions, Crowdy Effects (the assets, the effect graph editor and the Apply Effect nodes), sessions on the Game Model subsystem, automations, signals, pre-seeding, the Game Kit, container manifests, and `UCrowdyGameModelSubsystem`; the `crowdy.gamemodel.*` console variables and the `LogCrowdyGameModel` log category; and the `DefaultModelNotificationCarrier` setting, which an old saved config now has ignored. A Blueprint variable's **Crowdy Replication** dropdown offers only **None** and **Replicated**; **Server Owned** is gone. Server Objects replace the API; see [Move from Game Models](../exec/move-from-game-models.md). - **Crowdy Studio has an Access page and no Game Model page.** **Access**, in the Configure group under **Project**, holds the features and tier-feature grants that were on the Game Model page: a searchable feature list, and for the selected feature every access tier with a **Grant** or **Revoke** button. It loads its lists the first time you open it for the selected app, and has a **Refresh** button. Reading and changing features now works: the old page sent them with the wrong credential, and the server refused them. See [Crowdy Studio Overview](../studio/overview.md#the-access-page). - **A Server Object function can require a feature.** **Required Feature**, beside **Callable By**, names a feature key the caller's access tier must grant; other players are refused with `Denied` and a reason such as "needs the vip feature". Define features and grant them on the Access page. A type that uses it asks for the `players.read` scope when deployed. See [Required Feature](../exec/access-members-and-timers.md#required-feature). - **Server Object timers can run on a schedule.** A timer's **Runs** has a third choice, **On Schedule**, with a **Cron** expression (five fields, six with seconds first, or `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`) and a **UTC Offset** in minutes. The generated server code arms the next run before your function is called, so an error in your function does not end the schedule. Generate again after you add one. See [Access, members and timers](../exec/access-members-and-timers.md#timers-and-events). - **A Server Object component in Player's Team mode asks for the player's teams again** every 5 seconds while it waits, instead of waiting for the next sign-in when one request failed. - **A Type Name change offers the right folder when two crates name one definition.** It offers the crate Generate wrote last, not the first by name. - **A changed timer reaches running Server Objects.** The generated server code saves each timer's **Runs**; after a deploy that changes one, a pending timer is armed again under the new Runs, a new **Start Automatically** timer starts, and a removed timer stops. A schedule changed to **Every** no longer stops in instances that were running. Generate again; the first deploy of the new code re-arms nothing. See [Access, members and timers](../exec/access-members-and-timers.md#timers-and-events). - **A Server Object waiting to connect says why.** When the connection to the server cannot be opened, `GetFailureReason()` on every Server Object still in Connecting reads "Could not open the connection to the server for ``: ..." until it opens. The next try uses another type's Server Object, so one type that is not deployed or is switched off no longer holds up the others. See [Troubleshooting](../exec/troubleshooting.md#waiting-is-not-failing). - **Seamless travel pauses the grace period.** A Server Object whose owners were destroyed by a seamless travel is kept until the travel ends, as it already was across a map load. - **Two types cannot share a Logic File.** Deploy refuses a project where two Server Object types use the same Logic File, naming both: each type builds the file against its own State. - **Generate and Remove from project leave server code the SDK did not generate.** A `Server//` folder holding a crate you wrote yourself (its `Cargo.toml` records no definition and its `lib.rs` and `types.rs` lack the generator's header) is no longer overwritten by Generate Server Code or deleted by **Remove from project...**; Generate refuses with "... holds server code the Crowdy SDK did not generate, so Generate would overwrite it. Choose another Type Name, or move that folder.", and the type shows **Problem** on the Server Compute page. See [Generate it](../exec/write-server-logic.md#generate-it). - **Deploy and Deploy starter pack say what they take offline.** Deploy's confirmation names the live types the project does not have right after its first sentence, worded "It removes tip_jar, which this project does not have; their Server Objects stop.", which includes server code deployed some other way. Deploy starter pack's confirmation now names every type the live version runs. See [Deploy](../exec/deploy-with-server-compute.md#deploy). - **CrowdyMass no longer carries Game Model values.** `GetModelValue`, `GetModelTargetState`, `GetEffectTarget` and the model fields are removed. Crowd rows read Server Object values from a record per entity instead; see the next entries. - **A Server Object can keep a record per entity.** **Holds**, a new setting on the definition asset, can be **A Record per Entity**: the variables are kept once for each entity of a crowd, keyed by the entity's id, in one Server Object for an area, a match or a team. Each variable is **Server Only**, **Everyone**, **Owner Only** or **Team Only**; Everyone values reach players in batches every **Send Changes Every** (250 ms unless you change it), and Owner Only and Team Only values are fetched by the owner or the team at most once a second. **Max Records** (2048, at most 4096) bounds an object. The server code gets `get`, `get_mut`, `insert`, `remove`, `set_owner`, `set_team`, `ids`, `len`, `owner`, `team`, `owned_by` and `area_of` on `Records`. **Team From** says where a team comes from: **This Object's Records**, the default, or **Crowdy Team**, whose ids the server checks through the platform (deploying asks for `players.read`). In Blueprint, **Find Record**, **Get Record Ids** and **On Records Changed**; in C++, `GetRecord`, `GetRecordIds`, `OnRecordsChanged` and `WatchRecord` on `UCrowdyServerObject`. A record type needs **Readable By** set to **Every Player** and **Members From** set to **None**, and reserves the function name `changes` and the timer name `crowdy_flush`. **What you do:** nothing unless you adopt it. Generate and deploy a type again after you change its **Holds**. See [Keep a record per entity](../exec/keep-a-record-per-entity.md) and [Raiders on the north road](../exec/examples/raiders-on-the-north-road.md). - **The Crowdy Server Object component has a This Actor's Area mode.** The component joins the Server Object of the area the actor stands in, by the definition's **Area Size** and `ECrowdyServerObjectInstanceMode::ThisActorsArea`, and moves to the next as the actor crosses into it. **Area Size** and **Areas Followed Around a Player** are new settings of the definition. See [Pick the object](../exec/keep-a-record-per-entity.md#pick-the-object) and [Get a Server Object](../exec/from-blueprint/get-a-server-object.md#add-the-component). - **A definition can read world data.** **Reads World Data**, in **Access**, lets the type's server code ask the world which actors are near or in a chunk. Deploy asks the platform for the `world.read` permission for such a type. It is off by default. See [Reads World Data](../exec/access-members-and-timers.md#platform-data). - **Crowd rows read Server Object values.** A row of a crowd drawn by CrowdyMass reads the record of its entity with **Get Server Value** on an entity target, hears changes with **Receive Server Values Changed on Entity**, and a promoted actor does the same through its **Crowdy Mass Near Tier** component. The entity's actor class carries a **Crowdy Server Object** component with a type that holds records. See [Crowd rows](../exec/keep-a-record-per-entity.md#crowd-rows). - **Channels no longer drop payloads that begin `cmc:`, `csg:` or `gms|`.** The SDK used to filter those out before your handler saw them, because Game Model notifications used them. They now arrive like any other payload. See [Channels](../runtime/channels.md). - **The CrowdyState layout's error for a container property names a Server Object.** The text now reads "use a CrowdyEvent RPC or a Server Object" where it ended "or a Game Model container". See [Crowdy State](../runtime/crowdy-state.md). - **Breaking for C++:** - `ICrowdyEntitySubscriber::ApplyModelChanges` is removed, so a class that implements the interface and marks it `override` stops compiling. - `UCrowdyEntitySubsystem::RegisterSubParticipant`, `RegisterSubParticipantAs` and `DeriveSubParticipantID` are removed. - `FCrowdyModelIdentity` is removed; `FCrowdyEntityIdentity` has `StableNetIDFromActorPath` and `NetIDFromBindingKey`. - `FCrowdyEntityRecord::AnchorNetID` is removed. - `ICrowdyBindingKeyProvider` stays, with its header now under `Replication/Components/`. - **Some leftovers give no warning.** A C++ property or Blueprint variable that still carries `CrowdyModel` or `CrowdyContainer` metadata compiles and does nothing. A Blueprint that still has **Apply Effect** nodes loses them when it loads, with no error. Crowdy Effect, container manifest and Game Kit assets no longer load. - **What you do:** - Before you upgrade, open and compile your project on the release that adds Server Objects. Its warnings list every use of Game Models, including each Apply Effect node, and you can move each system to a Server Object while the nodes are still there to read. - Search your C++ for `CrowdyModel` (and `CrowdyContainer`) and clean up what is left. - Fix the C++ that no longer compiles, from the list above. - Delete the effect, container manifest and Game Kit assets, once you have noted what they described. - Check any code that relied on channel payloads starting `cmc:`, `csg:` or `gms|` being dropped. - Do a clean cook, not an iterative one: the baked registry's layout changed. - See [Move from Game Models](../exec/move-from-game-models.md#upgrade-steps). ## 2026-10-07 (SDK v2.18.0) - **The Game Model API is deprecated and does nothing; a later release removes it.** It covers Game Model containers and attributes (the `CrowdyModel` and `CrowdyContainer` markers and container tags), model functions and Invoke Model Function, Crowdy Effects and the Apply Effect nodes, invoke policies, sessions and turns, collections, container manifests and pre-seeding, the Game Kits, automations and signals, **Listen for Model Changes**, `UCrowdyGameModelSubsystem` and the libraries `UCrowdyModel`, `UCrowdyGameModel`, `UCrowdyModelValue` and `UCrowdyEffects`. Every call now fails at once, once: an async node fires its **Failed** pin immediately with "Game Models are deprecated and no longer available; use Server Compute." (code `GAME_MODEL_DEPRECATED`), **Listen for Model Changes** never fires, nothing is sent to a server, waited for or retried, reads return their defaults (**Get Local User Id** still answers), and the SDK logs the deprecation once per run. In C++ the classes and functions are marked deprecated, so the compiler warns (C4996 on MSVC) wherever your code names one; they are warnings only, and nothing fails unless your project treats warnings as errors. In Blueprint the Game Model library functions and the subsystem's events show the engine's deprecation warning, and each Blueprint that uses Game Models also gets one compiler warning listing its uses (the first 12, then how many more), `[CrowdySDK] This Blueprint uses Game Models, which are deprecated and no longer available; move it to a Server Object (Server Compute). Uses: ...`, when it compiles, loads or cooks; assets still load, save and cook. The deprecated library functions no longer appear in the Blueprint context menu, and the editor silently drops a deprecated call node when you paste or duplicate it unless **Expose Deprecated Functions** is on in Editor Preferences; the async Game Model nodes and the Apply Effect nodes are still listed, and the per-Blueprint warning flags them. A **Failed** pin wired straight back into the same node with no **Delay** used to retry once per frame; it now calls itself straight away, because the failure is immediate, and can hang or crash the game. **Get Local User Id** is replaced by **Get User ID** on the Crowdy Game Session. In Crowdy Studio the Game Model page shows a deprecation banner and no longer loads its lists, schema sync, pre-seeding, purge and Game Kit deploy refuse with the same message, and the tier-features panel on that page keeps working; in a Crowdy Effect asset's editor the sync status in the toolbar reads "Deprecated". In CrowdyMass, Game Model values no longer land on crowd entities: **Get Crowdy Mass Model Value** answers "no fragment" and **Bind Entity Model Containers** binds nothing and returns 0; landing Server Object values on crowd entities is planned for a later release. A subsystem tagged as a Game Model container is no longer registered as an entity participant by the Game Model system. **What you do:** take this release and read the warnings, which name the uses; author each system as a Server Object type and switch the call sites; add a **Delay** to, or remove, any **Failed** loop; and seed any data that must persist with a one-off script, since nothing moves automatically. Server Objects need ck-exec on your app's environment, so a project can finish moving once its environment has it. See [Move from Game Models](../exec/move-from-game-models.md). - **Server Objects: server-owned state on ck-exec, from C++.** A new module, `CrowdyExec`, lets a game use server code on [ck-exec](/exec/intro) without meeting the platform's vocabulary. A `UCrowdyServerObjectDefinition` data asset describes one type: the struct the server keeps, the fields players may watch, and the Server Functions they may call. `UCrowdyServerObjectSubsystem::Acquire` hands out a shared `UCrowdyServerObject` per definition and Instance Id, held for its owners and given back 10 seconds after the last one is gone; `WatchValues` reports each server change once, naming every field it touched; `Call` sends a params struct and completes once with an `FCrowdyServerCallResult` whose `ECrowdyServerCallOutcome` says what happened, after the SDK has retried a busy server; a call the server may already have run is never repeated for you. Log category `LogCrowdyExec`. **What you do:** nothing unless you adopt it. To adopt it, add `CrowdyExec` to your module's `Build.cs`, and deploy the type's server code to your app; C++ only for now, with Blueprint support to come. See [Server Logic](../exec/overview.md). - **Generate Server Code: a Server Object type's server code, written from its definition asset.** Right-click a `UCrowdyServerObjectDefinition` asset in the Content Browser and choose **Generate Server Code**. The editor checks the definition, then writes a Rust crate for ck-exec under `Server//` in your project: `Cargo.toml`, `src/types.rs` (your structs and enums, with their Unreal defaults) and `src/lib.rs` (the glue that answers and publishes the watched values, applies Owner Only and Who Can Call, refuses with `unknown_method`, `denied` or `bad_params`, and saves and loads the state) are rewritten every time; `src/logic.rs`, where you implement the `Functions` trait with one method per Server Function, is written once and never overwritten. A Server Function that returns an error leaves the state as it was and publishes nothing. Before a regeneration drops a struct, field or enum value, or changes a field's type, the editor lists each one and asks whether to go on. The definition asset's editor also has a **Server Code** tab: the path of the Server Functions file, an external-editor button, and an editor for the file's code with **Save** and **Revert** (Revert asks before it discards edits; unsaved edits survive a refresh), while its toolbar has a **Generate** button (the same as the right-click command, with the same warning). A **Code Source** choice on the tab, **Generated** or **My own file**, says where the type's Server Functions come from: with My own file you choose your own `.rs` file, for example one shared in your repo, Generate Server Code writes only `Cargo.toml`, `lib.rs` and `types.rs` and never creates or overwrites your file, and Server Compute's Deploy sends your file as the type's `logic.rs`. Generated uses `Server//src/logic.rs`. **What you do:** nothing unless you use Server Objects. If you do, generate each type's crate, write its `logic.rs`, commit `Server//` (without `target/`), and build and deploy it from Crowdy Studio's Server Compute page (see the next entry). See [Write its server logic](../exec/write-server-logic.md). - **Server Compute: build and deploy a project's server code from Crowdy Studio, and watch it run.** Open Crowdy Studio and choose **Server Compute** in its navigation; the page uses Studio's sign-in and the app set in Studio. A header shows the app and one status badge (**Live: version N**, **Nothing deployed**, **Switched off** with a **Switch on** button, or **Budget paused**), over four tabs (a fifth, **Settings**, comes in the next entry). **Overview** lists every Server Object type in the project with the state of its generated code (Ready, Needs generating, Out of date, Problem), and **Deploy** sends all of them, plus a small root the SDK writes, to ck-exec's build and deploys the result as the app's active server code, replacing what was active; a type whose code is not Ready has to be regenerated first, and the confirmation warns about server code files with unsaved edits. The build output is shown when the build finishes, succeeded or failed. **Versions** lists the deployed versions with a **Make active** button per row (a rollback). **Logs** shows the app's logs by type and level, with **Show this call** on a line to see only that call and **Load older** to page back. **Activity** shows how the calls to each Server Function went over the last hour, day or week. Each type on Overview can be switched off and on (its calls are refused while it is off; its Server Objects are saved and stopped). **Deploy starter pack** deploys the platform's example types instead. The asset editor's Server Code tab also warns before Save overwrites a file that changed on disk, disables Save and Generate Server Code until the Type Name is valid, and closing the editor asks about unsaved server code edits. The command `UnrealEditor-Cmd -run=CrowdyServerCompute -op=deploy -yes` does the same for CI, with `-op=status`, `versions`, `starters`, `activate`, `disable`, `enable`, `logs` and `stats` alongside it and `-out=` for a JSON result. The operations that replace or switch the app's live server code (`deploy`, `starters`, `activate`, `disable`, `enable`) need `-yes`; without it they refuse and send nothing. **What you do:** nothing unless you use Server Objects. If you do, deploy from the page instead of the platform's own build and deploy, and mind that a deploy replaces the app's active server code. Add `-yes` to any CI job that runs one of those operations. See [Server Compute](../exec/deploy-with-server-compute.md). - **Server Objects: a definition asset editor, change tracking, and Remove.** Double-clicking a `UCrowdyServerObjectDefinition` now opens its own asset editor, like the Material or Blueprint editors, instead of a property list. Its toolbar, after Save and Browse, has **Generate**, **Deploy** (with a small dropdown: **Open Server Compute**, which opens Crowdy Studio on the Server Compute page itself, and **Show build output**) and a status readout, a coloured dot and one line such as "No changes" with the live version, or "Changed since version 7". It has two tabs you can rearrange, and the editor remembers the layout. **Server Code**, on the left, holds the file's path, the **Code Source** combo, the deployed revisions and the code, with line numbers and Rust colouring and **Save**, **Revert** and open-in-external-editor buttons. **Details**, on the right, has native property rows: a **Server Object** group (Type Name, Description, **Who Can Read**, State, **Players See** as tick-boxes drawn from State's fields, **Save Every**, **Stop When Idle For**), the **Server Functions** array (each entry shows **Name** and **Who Can Call** on its header, **Sends** and **Replies** underneath, and **Server Name** in a collapsed Advanced group), and a collapsed **Advanced** group with Server Names and Baked. Nothing about the definition's data changed, only its labels: Visibility reads **Who Can Read**, Watched Fields **Players See**, Save Interval Seconds **Save Every**, Idle Timeout Seconds **Stop When Idle For**, Params **Sends** and Reply **Replies**. Type Name is suggested from the asset name (`DA_VillageBeacon` becomes `village_beacon`) and checked as you type, and a Type Name another definition already uses is a **Problem** that disables Generate and Deploy. The field and enum-value overrides under Server Names now pick from lists instead of asking you to type a name. **Player Callable is replaced by Who Can Call, with Players and Server only**: a Server only function refuses a player's call with `denied`, and only other server code and developer tools can call it. Nothing is converted, so a function that had Player Callable off is Players again until you set it to Server only, and the type has to be generated and deployed again. New **Description** (shown under the header and on the Server Compute page) and **Code Source** (**Generated** or **My own file**): an empty Logic File no longer means the generated one, and **Logic File** is used only when Code Source is My own file, so set it once if you had your own file. With My own file and no file chosen the type cannot be generated or deployed, and the messages say "Code Source is My own file but no file is chosen". Each deploy now records what it sent in `Server//revisions.json`: per deployed revision, the files as sent, a fingerprint that ignores line endings and trailing whitespace, the versions and build digests. From it the Server Compute Overview gives each type one badge against the live version, **No changes**, **Changed** (with what differs: server code, save interval, idle timeout), **New**, or **Live code unknown** (the live version runs code this project has no record of), and shows a **Removed** row for a type that is live but no longer in the project. **Deploy is offered only when something changed**, the status line says what ("Since version 7: 1 changed, 1 new."), and the Deploy confirmation lists what it adds, changes, replaces unrecorded and removes. The Versions tab gains a line per version saying what changed from the one before. On the editor's Server Code tab, the deployed revisions combo lets you view an earlier deploy's code, **Compare with yours** (a line diff), or **Restore** it into the editor as unsaved changes, with a warning when the revision was built from a different definition. A type's **...** menu on the Overview tab has **Open definition** and **Remove from project...**, which deletes the definition and its `Server//` folder after a confirmation that says what stays and why; the type keeps running until the next deploy removes it. A new **Settings** tab holds **Revisions to keep** (3 to 50, default 5), saved in `Config/DefaultEditor.ini`. From the command line, `-op=changes` reports the same comparison as JSON (`liveVersion`, `types`, `problems`, `ready`, `hasChanges`), and `-op=deploy` records revisions and lists any it could not save under `recordErrors`. **What you do:** nothing unless you use Server Objects. If you do, commit `revisions.json` with each type's crate (and `Config/DefaultEditor.ini` if you change the setting) so your teammates see the same states, and in a CI job that deploys, commit the changed `revisions.json` files afterwards. See [Create a Server Object type](../exec/create-a-type.md), [Write its server logic](../exec/write-server-logic.md#what-a-deploy-records) and [Server Compute](../exec/deploy-with-server-compute.md). - **Server Objects: Lists instead of structs.** A definition's State, and each Server Function's Sends and Replies, can now each be a **Struct**, as before, or a **List**: named values added right in the asset with Unreal's property bag editor (the **+** button adds a value; pick its name and type and set its starting value), with no struct to write. New rows choose which: **State As** before **State**, and **Sends As** and **Replies As** before **Sends** and **Replies** in each Server Functions entry, all defaulting to Struct (`ECrowdyServerValuesForm`; C++ properties `StateForm`/`StateList`, `ParamsForm`/`ParamsList` and `ReplyForm`/`ReplyList`, the Lists being `FInstancedPropertyBag`s). A List travels exactly like a struct with the same fields, so the server code and saved data do not care which you chose; its starting values are the defaults a new Server Object starts from and the generated server code's defaults. The generated server code names a State List after the Type Name plus `State` (`village_beacon` gives `VillageBeaconState`) and a function's Lists `Params` and `Reply`, the names the structs usually have, so `logic.rs` does not change. **Players See** ticks a State List's values the same way. Saving checks the Lists like structs, and adds "Add at least one value to the State list" and "Two lists are named FeedBeaconParams; give their functions different names". A List value's name is its name on the server and Field Names cannot keep an old one, so renaming it loses its saved value, as renaming a struct field does without Field Names. From C++, `UCrowdyServerObject::MakeParams` hands out a function's params List with its starting values to set by name and pass to a new `Call` overload taking an `FInstancedPropertyBag`, `CrowdyExec::ToList(Result.Reply)` (in `CrowdyExecCodec.h`) reads a List reply by name, and `GetStateList()` returns a List State's watched values; passing a struct to a function that takes a List completes as `BadRequest`. Blueprint nodes are not here yet. **What you do:** nothing; existing definitions stay Struct. Use a List for values only one function or type needs, and keep a struct for values you share, may rename, or use elsewhere in C++. See [Create a Server Object type](../exec/create-a-type.md#variables-inputs-and-outputs-or-a-struct) and [Call its functions](../exec/from-cpp/call-functions.md#call-one-with-inputs-added-in-the-asset). - **Server Objects: the Server Functions header row is labelled.** Each entry's header row now names its two cells: **Function** before the name box ("The function's name, used to call it from Blueprint and C++ (the server's method name is made from it)") and **Who can call** before the Players / Server only choice ("Players: any player can call this function. Server only: only other server code and developer tools can; a player's call is refused."). Nothing else about the entry changed. **What you do:** nothing. See [Create a Server Object type](../exec/create-a-type.md#the-details-tab). - **Server Objects: the Server Object asset editor now works like the Blueprint editor.** It has three tabs that dock and rearrange, with the layout remembered: **Server Code** on the left, unchanged; **Server Object** at the top right, where My Blueprint is, listing the type's **Variables** (each with its type pill and an eye: open is **Visible to Players**, closed is **Server Only**) and **Functions** (each with its pins as the node reads, such as `(Oil) -> Oil`, and a **Server Only** tag), with **+** buttons, right-click menus, and double-click to jump to a function's `fn` in `logic.rs`; and **Details** at the bottom right, which follows the selection: the type's settings with nothing selected, a variable's name, type, visibility and default value, or a function's **Name**, **Callable By**, **Inputs** and **Outputs**. Labels now use Blueprint's words: **Who Can Read** is **Readable By** (**Every Player** or **Owner Only**), **Who Can Call** is **Callable By** (**Players** or **Server Only**), **Players See** is **Visible to Players**, **State As** is the Variables' **Use Struct** check-box and **State** is **State Struct**, **Sends** is **Inputs** (**Use Struct**, **Input Struct**), **Replies** is **Outputs** (**Use Struct**, **Output Struct**), and a List is now variables, inputs or outputs added in the asset. The save check "Add at least one value to the State list" now reads "Add at least one variable, or use a State struct". The wire, saved data, generated server code names and C++ calls are unchanged. **What you do:** nothing. See [Create a Server Object type](../exec/create-a-type.md#the-asset-editor). - **Server Objects: Blueprint nodes and the Crowdy Server Object component.** The `CrowdyExec` module now exposes Server Objects to Blueprint. The **Crowdy Server Object** component (`UCrowdyServerObjectComponent`, in the Crowdy class group) holds one Server Object for its actor: it joins at Begin Play and lets go at End Play, and the Server Object is given back a few seconds after its last holder lets go. Its properties are **Definition**, **Instance Mode** and **Instance Id**. **Instance Mode** (`ECrowdyServerObjectInstanceMode`) is **Instance Id** (the typed id; every actor with the same one shares a Server Object), **Signed-In Player** (the player's user id, for Owner Only types; it waits for sign-in and joins again after a new sign-in) or **This Actor** (the actor's placement in the level, the same on every machine; only for actors placed in the level). It raises **On Variables Changed** and **On Status Changed** (**Object** is None when it could not join; **Get Failure Reason** says why) and has **Get Server Object**, **Get Failure Reason** and **Rejoin**. The nodes, under **Crowdy SDK > Server Objects**: **Get Server Object** (Definition, Instance Id, Owner, which defaults to self; the object, or None with **Error**), **Release Server Object**, **Get Player Instance Id** (the signed-in player's user id), **Call Server Function** (latent, `UCrowdyServerCallAction`: Object, Function, Inputs; **On Success** or **On Failed**, exactly one, each with **Outputs**, **Outcome**, **Reason** and **Retryable**), **Make Inputs** (a function's Inputs with their defaults), **Set Server Value** and **Get Server Value** (by name, with a wildcard **Value** pin; false when there is no value by that name or the type differs), and on the object **Get Status**, **Get Failure Reason**, **Get Instance Id**, **Get Variables**, **Watch Variables** (runs an event after every change, and once at once when the Variables are already current, naming what changed), **Stop Watching Variables** and **On Status Changed**. Typed nodes, with typed pins, follow in a later entry below. **What you do:** nothing; the C++ API is unchanged. Blueprint needs no `Build.cs` entry. See [Get a Server Object, from Blueprint](../exec/from-blueprint/get-a-server-object.md). - **Server Objects: members, access rules, timers and Can Call.** The definition asset gains four Details categories. **Access**: `ECrowdyServerObjectVisibility` (**Readable By**) gains **Members**; `ECrowdyServerFunctionCaller` (**Callable By**) gains **Members** and **Leader**; each function gets **Cooldown** (`CooldownSeconds`, per player, only successful calls count); and a number input gets a **Value Range**, a **Min** and **Max** in the input's Details (a struct input's `ClampMin` and `ClampMax` read the same), refused as `bad_params: Amount must be 1 to 100`. **Members**: **Members From** (`ECrowdyServerMembersSource`: **None**, **This Object**, **Crowdy Team**), and for This Object **Max Members** (default 100; 0 is no limit and needs **Show Members to Players** off, which with it on must be 1 to 4,096), **Show Members to Players** and **Remove Members Who Leave**. This Object adds six built-in functions, greyed out in a **Built-in** section of the Server Object panel: **Join**, **Leave**, **Add Member** (server only), **Remove Member**, **Make Leader** and **Set Open for Joining** (the last three for the leader), and **Open For Joining**, which starts true and opens again when the last member leaves. The first member leads; when the leader goes the longest-standing member does. **Crowdy Team** takes the team id as the Instance Id, **Members** is the team's members and **Leader** a member who can manage the team. **Timers & Events**: **Timers** (`FCrowdyServerTimer`: **Name**, **Every** or **After**, **Seconds**, **Start Automatically**), each a function in `logic.rs`, and **On Player Joined** and **On Player Left**, which count connections. **Can Call**: other definitions this one's server code calls with typed functions. `logic.rs` gets `members::...`, `timers::start` and `timers::stop`, and `calls::::`. A refusal `denied:` reaches the caller as the **Denied** outcome (it was a `ServerError`) and `cooldown:` as **Denied** with `bRetryable`. The component (`UCrowdyServerObjectComponent`) gains two **Instance Mode** choices, **Player's Team** (**Team Id**; several teams fail and ask for one) and **From Server Value** (**Source Definition**, **Source Instance**, **Source Variable**, followed as it changes), and `UCrowdyServerObject` gains `GetMembers`, `GetMemberCount`, `GetLeader`, `IsOpenForJoining`, `IsMember` and `IsLeader`, with changes named `Members`, `MemberCount`, `Leader` and `OpenForJoining`. **What you do:** nothing for existing types; to use a setting, set it, then generate and deploy the type again. A Server Only function's refusal now reads as **Denied**, so code that checked for a `ServerError` starting with `denied` should check the outcome. See [Access, members and timers](../exec/access-members-and-timers.md) and [Guild halls and arena lobbies](../exec/examples/guild-halls-and-arenas.md). - **Server Objects: typed Blueprint nodes, find by asset, and Only One Instance.** In any Blueprint graph, typing a variable's name now offers `Get ()` and `On Changed ()`, a function's name offers `Call ()`, and each asset offers `Get Server State ()`, all under **Server Objects** and the asset's name, with a real pin for every value instead of a name and a wildcard. A generic **Call Server Function** with a **Function** dropdown is offered as well. Only variables ticked **Visible to Players** are offered. The pins: Boolean, Byte, Integer, Integer64, Float, String, Name, enums, Vector, Rotator, Vector2D, Quat, IntPoint, IntVector, LinearColor, Color, DateTime, Timespan, Guid, GameplayTag, soft object and soft class references, your own structs (with **Split Struct Pin**), and Array, Set and Map. Types only C++ has widen: `int8`, `int16` and `uint16` to Integer, `uint32` and `uint64` to Integer64, and a `TOptional` to a value pin with a **Has Value** pin beside it. A value that does not fit is refused: a **Call** fails through **On Failed** as a **Bad Request** naming the input, and a **Get** logs a warning and reads the default. An Array, Set, Map or optional inside another, and a C++ struct or enum that is not `BlueprintType`, have no pin, and the compile says which and why. **Get** reads the shared Server Object's current value, not a copy, and reads the variable's default until the server has sent the values; its advanced ` Has Value` pin (collapsed, expand the node's arrow) says whether they have arrived, an optional's visible **Has Value** is false when it is empty or they have not arrived, and **Get Server State** has one advanced **Has Values** pin. A **Call** whose input does not fit its type, such as -1 for a `uint32`, sends nothing and runs **On Failed** with a **Reason** naming the input. A variable, input or output named like one of a node's own pins (`Team Id`, `Instance Id`, `Value Changed`, `then`, `execute`, `self`, `Server Function`, `Call Outcome`, `Call Reason`, `Call Retryable`, `On Success`, `On Failed`, or another value's name plus ` Has Value`) gets no node, and the compile says to rename it. An `On Changed` whose **Bind** is not connected gives a compile warning. `On Changed` takes its **Bind** from Begin Play once, runs at once with the current value and after every change of that variable, stops when the Blueprint is destroyed, moves when it is bound again, and through a component follows whatever object the component joins. `Call ` has typed input and output pins and **On Success** and **On Failed** with **Outcome**, **Reason** and **Retryable**. The object comes from **Target** (the Server Object, a component, or an actor with one; **Self** by default), or from **Find By Asset** (right-click the node): **Instance Id**, **Signed-In Player** or **Player's Team**, for a widget with nothing to pass in. **Only One Instance**, a new setting in the asset's **Access**, gives a type one shared instance for every player, like a registry or a world event, and its nodes need neither a Target nor an Instance; saving refuses it with **Readable By** set to **Owner Only** or **Members From** set to **Crowdy Team**, and the generated server code refuses any other instance. Renaming or retyping a variable in the asset updates the nodes, and a wire that no longer fits is a compile error. A new page, [Tip jar: your first Server Object](../exec/examples/tip-jar.md), builds a tip jar with only the editor. **Behavior change:** the names **On Variables Changed** and **Watch Variables** give in their **Changed** list are now the names **Get Server Value** finds, the variable's own name in the asset, also for a variable renamed with **Server Names** (they used to give the server's name). **What you do:** nothing for existing Blueprints unless one compared a **Changed** name with a renamed variable's server name; use the variable's own name. The untyped nodes are unchanged. Generate and deploy a type again to use **Only One Instance**. See [Get a Server Object, from Blueprint](../exec/from-blueprint/get-a-server-object.md) and [Only One Instance](../exec/access-members-and-timers.md#only-one-instance). - **Server Objects: a shared boss example and `crowdy.exec.trace`.** A new page, [Shared boss fight](../exec/examples/shared-boss.md), builds a boss that every player hits from start to finish: the structs or the asset's own variables, **Readable By** set to **Every Player**, a `Hit` function that players may call with a 0.25 second **Cooldown** and a **Value Range** of 1 to 25 on its `Damage`, a `Respawn` timer that is **After** 10 seconds and not started automatically, the `logic.rs` that refuses a hit while the boss is defeated, clamps health at 0, starts the timer at 0 and restores full health on respawn, a Deploy from Server Compute, the **Crowdy Server Object** component on the actor (**This Actor**, **Instance Id** or **From Server Value**), the typed Blueprint nodes, two players in Play In Editor, and switching the type off and on from Server Compute to see both players recover. `CrowdyExec` gains the trace console variable `crowdy.exec.trace`, off by default, with the lines logged under `LogCrowdyExec` and starting `exec:`. When it is on it logs each Server Object's status changes, every read and push applied with its epoch, sequence number and the names of the variables that changed, each re-read and why (`gap`, `epoch`, `reconnect` or `ownerOnly`), subscribes, each call sent and answered with its outcome and time in milliseconds, and connections closing and redialing. It never prints a value or a token. **What you do:** nothing unless you want the lines; to see them, enter `crowdy.exec.trace 1` in the console or start with `-ini:Engine:[ConsoleVariables]:crowdy.exec.trace=1`. See [Shared boss fight](../exec/examples/shared-boss.md) and [Seeing what a Server Object is doing](../exec/troubleshooting.md#seeing-what-a-server-object-is-doing). - **Server Objects: a timer that cannot start refuses the call.** If the platform refuses a timer that a Server Function starts with `timers::start`, the whole call is refused: nothing it changed is kept or published, timers it already started in that call are cancelled, and the player sees `ServerError` with a reason such as `the change was not kept: timer Respawn could not start: `. **What you do:** nothing; generate and deploy the type again to get it. See [Timers and events](../exec/access-members-and-timers.md#timers-and-events). - **Server Objects: renames ask, every value is checked, and each function keeps its own defaults.** - **Renaming a variable, input or output added in the asset asks first.** Generate Server Code notices the rename and asks whether to keep the old name on the server, so saved values carry over; **Yes** adds an entry to the new **List Value Names** under Server Names (`FCrowdyServerListValueName`: List, value id, Server Name). Before, the rename dropped every saved value under the old name without a word. `types.rs` now records each such value's id in its comment. - **Changing a Type Name finds the old folder.** Each generated `Cargo.toml` now records its definition asset, and Generate Server Code offers to move the folder generated under the old name, keeping your `logic.rs`. The record stays on your machine: a deploy sends `Cargo.toml` without it. A Type Name another definition already uses is refused from the Content Browser too. - **The generated code checks every value an Unreal client refuses**: also GUID text, names and gameplay tags over 1,023 characters, soft object and class paths, dates and time spans, more than 65,536 list, set and map entries in one message, and keys Unreal counts as one. A new Server Object's starting values are checked (`bad_seed`), and a read of a state holding such a value is refused with `unreadable` and the field, which the Server Object shows as its reason while it waits in Connecting, instead of failing for every player. - **Each function keeps its own default values** in **Make Inputs**, the call nodes and replies, where two functions whose inputs or outputs had the same shape got the first one's. - **Fixed:** a Server Function's name could reach the server in another letter case in a packaged game, refused as `unknown_method`; it is now stored when the asset is saved. The palette's Get and On Changed entries now refresh after an edit to a Blueprint struct nested in a type's variables, or one that fixes a failed save. - **What you do:** generate and deploy your types again. Commit the regenerated `Cargo.toml` and `types.rs`; every type shows as changed once on Server Compute. See [After you change the definition](../exec/write-server-logic.md#after-you-change-the-definition) and [Troubleshooting](../exec/troubleshooting.md#refusals-from-your-server-code). - **Server Objects: Type Settings, CSO_ asset names, tidier Details and Call nodes.** - **Type Settings.** The Server Object panel starts with a pinned **Type Settings** row (gear icon), selected when the asset opens; it shows the type's own settings in Details. A **Type Settings** toggle before **Generate** in the toolbar does the same from anywhere. Clicking an empty part of the panel still shows them, now with the row highlighted, and the hint "Select a variable or function in the Server Object panel to edit it." is gone. - **CSO_ asset names.** Server Object assets take the prefix `CSO_` (Crowdy Server Object). **Add > Crowdy > Server Object** in the Content Browser makes one named `CSO_NewServerObject`; **Add > Miscellaneous > Data Asset** still works. The Type Name suggestion drops `CSO_`, or `DA_` for older assets, so `CSO_VillageBeacon` suggests `village_beacon`, and the palette category reads **CSO Village Beacon**. - **Details.** **Save Every** and **Stop When Idle For** always show seconds (300 s, not 5 min); **Value Range** is two boxes, **Min** and **Max**; a timer's **Repeat** is now **Runs** (**Once After** or **Every**) and **Seconds** is **Time**, and its row reads "Respawn: Once After 10 s"; the built-in functions show their inputs, such as **Add Member (Player)**; Code Source **My own file** reads **My Own File**; and the Variables, Inputs and Outputs source is the **Use Struct** check-box everywhere. Type Name uses the normal font. - **Call nodes.** `Call ` and **Call Server Function** show **Then**, **On Success** and **On Failed** on the right, then the function's outputs. **Outcome**, **Reason** and **Retryable** (`Call Outcome`, `Call Reason`, `Call Retryable`) move to the node's advanced area, under the small arrow at the bottom of the node. - **On Changed more than once.** Each `On Changed` node you place is its own binding, so one graph can bind the same variable from Begin Play and from a custom event; each runs once per change, and running the same node again replaces its own binding. - **A new page,** [Relate Server Objects to each other](../exec/relate-server-objects.md): one to one, one to many, many to one and many to many with the existing settings, and good practice for ids, ownership, calls between types and limits. - **Server Objects: Generate adds what logic.rs is missing, timers have constants, and the code tab follows the file.** - **Missing functions.** `logic.rs` is written once, so a Server Function, timer or event added later reached only the `Functions` trait and the crate stopped compiling. Generate Server Code now names what `logic.rs` lacks and asks "Add empty versions of the missing functions?"; **Yes** adds them before the closing brace of your `impl Functions` block (a function refuses with "not written yet", a timer or event does nothing). Without such a block they go to the clipboard. A method the type no longer has is named, never deleted. The Server Code tab shows the same as a line above the code, with **Add Stubs** (as unsaved changes) or **Copy Stubs**. - **The Server Code tab follows the file.** A save from VS Code or another editor now shows in the tab within a second; with unsaved changes in the tab, it keeps them and offers **Reload** or **Keep Mine**. Before, the tab kept the old text until **Revert**. - **Timer constants.** The generated `timers` module has one constant per timer, its function's name in capitals: `timers::END_MATCH` holds `"EndMatch"`. Write `timers::start(ctx, timers::END_MATCH)` so a misspelt name does not compile; a typed name is matched exactly, capitals included, and only fails when it runs. A timer whose constant would be `QUEUED` is refused at Generate. - **What you do:** generate again; `lib.rs` changes for types with timers. Switching your `timers::start` and `timers::stop` calls to the constants is optional. See [After you change the definition](../exec/write-server-logic.md#after-you-change-the-definition) and [Timers and events](../exec/access-members-and-timers.md#timers-and-events). - **What you do:** nothing required. Existing `DA_` assets keep working; rename them to `CSO_` if you like. A status pin you already wired stays visible on the Call node. See [Create a Server Object type](../exec/create-a-type.md#the-asset-editor) and [Call its functions](../exec/from-blueprint/call-functions.md#the-call-node). - **Server Objects and Crowdy Teams: a final pass.** - **The Crowdy Teams "my teams" cache keeps itself current.** Signing in (log in, register, magic link, social, or a restored session) clears it and fills it again with a Get My Teams; signing out clears it, so `HasCachedTeams()` is false and `OnMyTeamsCacheChanged` fires with an empty list if it held anything. A successful Create Team, Join Team, Leave Team, Delete Team, Update Team, Remove Team Member, Set Team Member Roles, Update Team Role or Delete Team Role refreshes it; Request To Join does not, since the membership is pending. An answer to a Get My Teams sent before a sign-out never fills the cache, and an older answer never replaces a newer one. C++ gains `ClearMyTeamsCache()` (not Blueprint). Before, only Get My Teams filled it and nothing cleared it. **What you do:** nothing; you can drop a Get My Teams call you made after a join or leave. See [Crowdy Teams](../services/teams.md#reading-the-cache-first). - **Too many new names no longer fail a whole message.** A client accepts at most 1 Mi (1,048,576) characters of new names (Name values and soft object path parts it has not seen before) per run. Before, going past it failed the whole message. Now a new Name reads as None and an object path made of new names reads as empty, the rest of the message still applies, a map or set entry whose key was skipped is dropped, and the client logs one warning. The text "None" never counts. See [Create a Server Object type](../exec/create-a-type.md#field-types). - **Lists that start with different values get their own struct in the generated server code.** Lists that share a shape (one copied from the other) shared one struct, and later ones became `pub type X = Y;` aliases that took the first List's starting values. Now a List that starts with different values gets its own struct and defaults, and one that starts with the same values stays an alias. Generate shows a notice when a List switches, such as "TipReply is now its own struct, since its starting values differ from those of TipJarState; code that uses one for the other no longer builds." **What you do:** generate again; fix any `logic.rs` that used one of those types for the other. See [Create a Server Object type](../exec/create-a-type.md#variables-inputs-and-outputs-or-a-struct). - **Moving or renaming a definition asset keeps its server code findable.** Saving a moved or renamed definition updates the record in its crate's `Cargo.toml`, so a later Type Name change still finds the folder. See [After you change the definition](../exec/write-server-logic.md#after-you-change-the-definition). - **Fixed:** a Ready Server Object no longer keeps an earlier read refusal's text as its failure reason once a read succeeds. - **Actor updates carry their payload type id.** `FCrowdyActorUpdate` gains `PayloadTypeID`, the payload type id the sender's state blob led with, kept even when no struct is registered for it on this client. The stranded and evicted pending-activation warnings, for an entity waiting on a spawn event that never resolves its class, now print that id when the state could not be decoded instead of saying the payload cannot be named. **What you do:** nothing; it is diagnostic. See [Entities and spawning](../runtime/entities-and-spawning.md). - **The CrowdySDK module no longer lists HTTP as a public dependency.** It never used it. A game module that only picked up Unreal's HTTP module because it came in through `CrowdySDK`'s public dependencies now needs `"HTTP"` in its own `Build.cs`. Separately, the `CrowdyCppBridge` module's build no longer prints a precompiled-header warning or the two `CS0618` obsolete-property warnings. **What you do:** add `"HTTP"` to your module's dependency list if your code calls it and relied on this. See [Installation](../installation.md#step-4-add-the-module-dependencies). - **Test-only accessors on `FCrowdyCppClient` are compiled only with automation tests.** `MakeForTest`, `GetLastTestRequest`, `GetLastTestRequestBody`, `SetTestStampsHandOff`, `SetTestResponseScript`, `SetTestOnRequest`, and the `Test*WebSocket*` helpers, and `FCrowdyCppLoopback::SetTimeForTests` / `ClearTimeForTests`, now exist only when `WITH_DEV_AUTOMATION_TESTS` is set, never in Shipping. **What you do:** guard any code that calls them behind the same `WITH_DEV_AUTOMATION_TESTS` check. - **Updates for actors not yet on screen wait in a bounded queue.** They used to queue without limit while the worker threads that place them were stalled. Past `crowdy.replication.tracker.maxqueuedupdates` (default 8192), a further update is dropped, counted, and reported in a warning at most once a second; the actor appears on a later update. A working client never reaches it. **What you do:** nothing; if you see the warning, the worker pool is not keeping up. See [Console variables](../reference/console-cvars.md#behavior-switches). :::caution[The Game Model entries below no longer reach a server.] The SDK's Game Model API (containers and attributes, Effects, sessions, pre-seeding) was built on the game API's legacy game-model engine, which ck-exec replaced; the game API no longer serves it. The entries that describe it are kept as history. Do not adopt it: since v2.18.0 the SDK marks it deprecated and makes every call fail at once, and a later release removes it. Its replacement is [Server Objects](../exec/overview.md), described under [SDK v2.18.0](#2026-10-07-sdk-v2180); [Move from Game Models](../exec/move-from-game-models.md) says what to change. ::: ## 2026-09-26 (SDK v2.17.0) - **The actor pool grows instead of dropping entities.** Up to and including 2.16.0 a class's pool held `DefaultPoolSizePerClass` actors (8) and never grew, so a client with more than eight live remote entities of one class drew only eight: the rest logged "Pool exhausted" and were never retried, and one whose spawn event had already made an actor lost that actor too, destroyed before the pool was asked. A Game Model container bound to such an entity never bound. Now a full pool spawns one more actor, up to the new `MaxPoolSizePerClass` on the Actor Pool Backend Config (default 256); the up-front sizes stay as they were. The spawn event's actor is only replaced once a pool actor is secured, and at the cap it is kept and drawn instead. An entity the pool cannot draw yet is retried when another of its class leaves or when its spawn event registers it, and the cap warns once per class instead of once per entity (each waiting entity is now a `crowdy.pool.trace` line). In the sandbox's two-client test, the observing client went from 0 of 20 respawned entities alive and bound to all 20. A custom rendering backend can take part in the retry by overriding the new optional `UCrowdyRenderingBackend::IsInstanceActive`. **What you do:** nothing; raise `MaxPoolSizePerClass` if a pool warns it is at its cap. See [Rendering backends](../runtime/rendering-backends.md#configure-it). - **A remote destroy no longer hands a still-live pool actor to another entity.** When a destroy event with a destroy delay arrived and the entity then left the tracker inside that delay, the pool actor went back to the pool, was given to the next entity, and was destroyed under it when the delay ran out, leaving that entity invisible until it timed out. The actor now stays with the destroy until it is gone, and an entity that keeps sending after its actor was destroyed is activated again. An actor's `EndPlay` also no longer removes the entity record of an actor that replaced it under the same id. **What you do:** nothing. - **More trace lines for entities that do not appear.** `crowdy.entity.trace` now logs the success path too: "Spawn dispatched", "Spawn received" and "Entity registered", each with the id, class and location. `crowdy.pool.trace` logs each activation, each pool growth step, and each entity waiting at the cap. `crowdy.net.trace` logs every spatial message received (opcode, actor, chunk) and the transport's totals, so "the server never sent it" can be told apart from "the client dropped it". **What you do:** nothing; see [Troubleshooting](./troubleshooting.md#only-some-remote-entities-of-a-class-appear). - **Known server issue: a player who just joined can stay invisible until they move.** The replication server refuses an actor's first update while it loads permissions, then accepts every later one but does not relay the actor to other players, or theirs to it, until it moves. The SDK sends what it should; this has been reported. See [Troubleshooting](./troubleshooting.md#a-player-who-just-joined-is-invisible-until-they-move). - **A string value that changes only in letter case is now a change.** Unreal compares strings without regard to case by default, and the Game Model used that comparison to decide whether a value changed, so `alice` becoming `Alice` was never applied: the member kept the old spelling and no `CrowdyOnRep` or change delegate fired. It now applies like any other change, on pulls, confirmed invokes, and free containers. The same fix reaches the editor: Crowdy Studio's sync preview now shows a value, policy, or effect expression that changed only in case (it used to report nothing and never send it), the **Apply Effect** node keeps a string input you set to a different case of its default instead of dropping it, including after the Blueprint is reloaded, and the effect script editor keeps an edit that only changes case. **What you do:** nothing; the first sync after updating may upload a function whose stored text differs from your project only in case, once. - **Your own invokes no longer pull back what they just wrote.** The notification the server sends for your own invoke used to be dropped once per container, so several quick invokes on one container pulled it again for all but the first. The SDK now expects one notification per invoke, counted when the request is sent, and drops that many, each expiring three seconds after its invoke; an invoke that fails or writes nothing gives its count back, and pulls instead if its notification was already dropped. Since a dropped notification could have been another client's write, a container that had one dropped is pulled once more within about three seconds. In the sandbox, a client invoking its own row ten times a second for a minute went from 118 pulls of that row to 29 (20 of them the three-second catch-up), with the watching client still pulling every change. `crowdy.gamemodel.stats` gains `SelfEchoMarksExpired` and `SelfEchoBackstopPulls`, and `SelfInvokesMarked` now counts invoke attempts sent (a retried invoke counts each attempt). **What you do:** nothing; another client's write to a container you are writing yourself can take up to about three seconds to show, where before it usually came with the next pull. See [Change pings and pull](../exec/move-from-game-models.md). - **Your copies of other players' entities bind from one paged list per type, and first pulls after a bind are merged.** A remote proxy used to read its owner's row by key, one call each and again on every retry while the owner had not created it yet; it now joins the same paged list shared entities use, under the session active when it is listed, and one the list does not find yet waits for the next list instead of calling on its own. A copy never creates its owner's row. A copy-only list reads at most one page per copy it looks for; a type with more rows than that (an app-wide per-player type) is marked, and its copies read one key at a time from then on. The first pull after any container binds now waits the usual tenth of a second, so entities binding together read their state in one bulk call, and every entity bound to that container receives it; an invoke you send while that pull waits keeps its value when the pull lands. In the sandbox's two-client test, the observing client bound the same 40 copies with 10 list calls instead of 140 keyed reads, and the spawning client read the state of its 200 new containers in 10 calls instead of 200. `crowdy.gamemodel.bulkresolve`'s help now names remote copies, and `crowdy.gamemodel.stats` gains `BulkResolveListPages`. **What you do:** nothing; a newly bound entity may show its defaults up to a tenth of a second longer than before. See [Pre-seeding](../exec/move-from-game-models.md) and [Change pings and pull](../exec/move-from-game-models.md). - **At most one state-applying pull per container is ever in flight, and a pull that would overlap it is held and merged instead of sent again.** A change notification, **Refresh Game Model** (`PullNow`), a first bind, or a bulk bind requested while a pull of that container is already out waits for it to land, then follows up: an explicit request (Refresh Game Model, a bulk bind) is sent right away, a notification or a first bind goes back through the usual tenth-of-a-second merge window. A bulk bind leaves out a container already being pulled. A read that never applies state (the combat kit's **Get Combatant State**, for example) is never held. `PullNow`'s tooltip now says it pulls now, or right after a read of that container already in flight lands, and `PullDataContainer`'s `OnDone` fires exactly once for the read that lands after the call, its own or the follow-up. Separately, a pull landing after your own invoke already wrote a newer value no longer rolls that value back: a key your invoke wrote after the pull was sent keeps the invoke's value, and if the pulled value actually differed, one follow-up pull is queued to re-converge the row; a key written before the pull is not protected, since the pull is newer. This does not cover another client's write landing in that same window, which still reaches you through the next notification as before. `crowdy.gamemodel.stats` gains `PullsHeldBehindInFlight`, `FollowUpPulls`, and `StaleKeysSkipped`, printed as `pulls held behind in flight N follow-ups N stale keys skipped N`; `OverlappingPulls` now counts only pulls that still applied state while another was out despite this, and should read near zero. **What you do:** nothing to keep working; a caller polling `PullNow` in a tight loop now gets fewer actual round trips for the same calls, not more state. See [Change pings and pull](../exec/move-from-game-models.md). - **The SDK now retries a platform-busy refusal on its own before the caller ever sees a failure.** A query (a container state read, a bulk state read, a `ListContainers` page, or any other SDK query) resends on any refusal the platform blames on itself and marks retryable, except `WRONG_DATACENTER` (the SDK already follows that redirect) and `APP_UNAVAILABLE` (nowhere else to serve it). A container ensure and a `gameModelInvoke` resend only on `PLATFORM_BUSY`, the one code that means the work never started, so nothing is ever applied twice. At most 3 retries, waiting the server's suggested time when it names one (at least 0.1 s, plus up to 20% jitter, and not retried past 5 s) or a random 0.1-0.2 s doubled each time otherwise; at most 256 requests may be waiting out a backoff at once, past which a refusal goes straight to the caller. An invoke's busy retries run beside its existing rate-limit retry (`RATE_LIMITED`, 2 retries): the two caps are separate, and every attempt still counts toward the SDK's local invoke allowance window. `crowdy.net.retry.busy` (default 1, every build) turns it off, handing every refusal straight to the caller as before. `crowdy.gamemodel.stats` gains `Retries` / `RecoveredByRetry` per operation and an `invoke busy retries N recovered M gave up K` line (`InvokeBusyRetries` / `InvokeBusyRecovered` / `InvokeBusyGaveUp` on `UCrowdyGameModelSubsystem::GetNetStats`); a failure reason now counts only the final outcome, not each retried attempt. `UCrowdyGameModelSubsystem::IsBusyRefusal` and `FCrowdyCppJsonResult::Blame` / `bRetryable` / `RetryAfterMs` are new. **What you do:** nothing to keep working; a caller that built its own retry loop around a busy refusal is now retrying on top of the SDK's own retries. See [Console variables](../reference/console-cvars.md#behavior-switches), [Coalescing](../exec/move-from-game-models.md), and [Change pings and pull](../exec/move-from-game-models.md). - **The SDK now asks for HTTP/2 on its own requests by default.** `crowdy.net.http2` (default 1, every build including Shipping, read per request) sets `IHttpRequest`'s HTTP version to `2TLS`, falling back to HTTP/1.1 when the server does not offer it; plain `http://` stays HTTP/1.1 regardless. Only the SDK's own requests are affected: the game's other HTTP traffic and the engine's `http.CurlAllowHTTP2` are untouched. On the test tier, multiplexing took a burst of 300 reads in 10 seconds from about 7.2 s p50 to 0.41 s, and stopped an unrelated call issued mid-burst from being delayed behind it. **What you do:** nothing to keep working, but read the caveat before you rely on the default: with HTTP/2 on, a request that fails at the network level (not a cancel or a timeout) can have its curl diagnostics logged at `Warning` with the request headers, bearer token included, in any build with logging enabled (a default Shipping build compiles logging out); set `crowdy.net.http2 0` if you ship or collect logs from a build with logging enabled. Faster delivery also means a burst reaches the server together, so expect more `PLATFORM_BUSY` refusals under heavy bursts; the SDK retries those on its own, see the busy-retry entry below. See [Console variables](../reference/console-cvars.md#behavior-switches) and [Change pings and pull](../exec/move-from-game-models.md). - **`ListContainers` now reads every matching row instead of only the server's first page.** It pages the server 1000 rows at a time until a short page, up to 50 pages (50,000 rows), logging a warning if it stops there with more left unread; a failed page fails the whole call (`bOk` false, no partial list), and a row the server returns on two pages comes back once. For an app-scoped type, `SessionId` is now ignored the same way create and ensure already ignore it. `FCrowdyCppClient::ListContainers` at the bridge layer is unchanged and still returns one page. **What you do:** nothing to keep working, but a caller that assumed at most 200 rows back may now see more, and a caller that actually wanted only the first page has no way to ask for that anymore. See [Collections and Free Containers](../exec/move-from-game-models.md). - **The invoke allowance governor now counts only invokes.** A pull, a list, an ensure, or any other Game Model read used to spend the same 120-per-10s allowance as a `gameModelInvoke`, which could stretch the coalesce merge window, the refresh-pull merge window, and container manifest pre-seeding under pure read traffic. Now an invoke is counted only when its request actually starts (a budget retry counts again); the coalesce window still widens as invokes approach the allowance, but the refresh-pull merge window no longer widens and manifest pre-seeding is no longer paced by the invoke count at all. **What you do:** nothing to keep working; a project whose pulls or a large manifest apply were being slowed down by the old shared counting should see that stop. See [Coalescing](../exec/move-from-game-models.md) and [Pre-seeding containers](../exec/move-from-game-models.md). - **`crowdy.gamemodel.stats` splits each operation's latency into SDK time and HTTP time, breaks its failures down by reason, and adds in-flight and dispatched counts.** Each op row gains `SdkP50Ms` / `SdkP95Ms` (request created to handed off to the engine's HTTP module) and `HttpP50Ms` / `HttpP95Ms` (handed off to completion delivered, including any wait inside the HTTP module, the network, the server, and up to one frame for delivery); a cancelled request records no SDK/HTTP split. `FCrowdyGameModelOpStatsRow::FailureReasons` (and the log line's `failures: CODE xN, CODE xM`) counts each failure under the server's error code, or `graphql`, `http `, `rejected`, `canceled`, or `transport` when there is none, up to eight reasons per op with the rest under `other`. Transport totals gain `InFlight` and `PeakInFlight`; net stats gain `InvokesDispatched`. No behavior changed. **What you do:** nothing; it is read-only, and a high HTTP time next to a low SDK time on the same op points at queueing inside the engine's HTTP module, not the SDK or the network. See [Change pings and pull](../exec/move-from-game-models.md). - **New network diagnostics for the Game Model path.** `crowdy.gamemodel.stats` logs op calls, failures, and latency per server operation; pull counts and how many were redundant or overlapping; self-echo counts; resolves and re-resolves; invokes in the rate window beside the SDK's own governor count; transport bytes; the top pulled containers; and invoke faults by code, since the last reset. `crowdy.gamemodel.stats.reset` zeroes it. Both are new console commands, stripped from Shipping builds like the existing watch commands. The same numbers are on `UCrowdyGameModelSubsystem` in C++: `GetNetStats`, `ResetNetStats`, `GetInvokesInWindow`, `GetOpStats`, `DescribeNetStats`. No behavior changed. **What you do:** nothing; it is read-only. See [Change pings and pull](../exec/move-from-game-models.md) and [Console variables](../reference/console-cvars.md#diagnostic-commands). ## 2026-09-23 (SDK v2.16.0) - **Backend gains a Test tier, and its default now follows the SDK build's own release tier.** `ECrowdyEnvironment` adds Test between Dev and Production, so the Backend selector on the Sign In page now offers Dev (shared), Test, Production, and Custom. The built-in hosts behind Dev and Production also moved, to `https://ck.dev.crowdedkingdoms.com` and `https://ck.prod.crowdedkingdoms.com`; the previous hosts, `api.dev.crowdedkingdoms.com` and `api.crowdedkingdoms.com`, no longer resolve, so a project left on Dev or Production with no Custom Discovery URL could not reach the backend. This release fixes that and adds `https://ck.test.crowdedkingdoms.com` for the new Test tier. Separately, a project's Backend used to default to Production until you ran Config Sync; it now defaults to the tier this SDK build was released for (Dev on a dev build, Test on a test build, Production on the public release), and the Discovery URL default follows the same tier instead of always pointing at Dev. Each tier also vendors its own CrowdyCPP build (0.42.1), so a connection opened with no origin configured at all dials that tier's host directly; a released build dials `ck.prod.crowdedkingdoms.com`. **What you do:** nothing if your `DefaultGame.ini` already names an `Environment`, which it does for any project that has used the Backend selector; that value is unaffected. A brand-new project now connects out of the box on whichever tier's build you are running, instead of defaulting to a Production host you may not have an app on. A new static C++ read, `UCrowdySDKDeveloperSettings::GetReleaseEnvironment()`, returns the tier a build defaults to; it is not exposed to Blueprint. See [Project settings](../reference/project-settings.md#the-default-backend) and [Installation](../installation.md#step-6-connect-your-app). ## 2026-09-22 (SDK v2.15.0) **Six changes.** One is a source break for C++ projects; two remove work you may have done; one moves the moment a player pawn gets its identity; one changes how inbound traffic is signed and needs nothing from you; one is a Crowdy Studio panel. The examples on this site are written for 2.15.0. - **Crowdy Studio shows every value a live model holds.** The Live tab's one-line summary, which named the first few attributes and then said "and N more", is now a panel under the instance list: one row per attribute with its value, what it holds, and its description, plus the attributes the model declares that the live model has no value for, a search box over names and values, **Copy value** and **Copy values**, and an **Internal** switch for the runtime's own keys. The page's **Pre-seed containers** strip hides itself while the Live tab is open, so the list and the panel get its height. **What you do:** nothing; it is an editor panel. - **Inbound bundles arrive with one signature per datagram.** The transport now tells the replication server what it can read (a `CLIENT_CAPABILITIES` message, sent once the connection is up and every 15 seconds after), and a server at v0.30.0 or later answers by packing its notifications into `MESSAGE_BUNDLE_SIGNED` datagrams: one HMAC over the whole bundle instead of one per member. The messages your handlers receive are the same; only the verification cost per datagram drops. Up to and including 2.14.0 every member carried and was checked against its own signature, and an older server still does that. **What you do:** nothing. `crowdy.net.recv.signedbundles 0` turns the advertisement off for a diagnostic comparison, read when a connection opens like `crowdy.net.send.bundle`. See [Connection and reconnect](../runtime/connection-and-reconnect.md#signed-inbound-bundles). - **Eighteen array delegates pass `const TArray&` instead of `TArray` by value.** Ten single-cast: `FOnTeamsSuccess`, `FOnTeamMembersSuccess`, `FOnTeamRolesSuccess`, `FOnMyTeamsSuccess` (teams), `FOnAvatarsSuccess`, `FOnAppStatesSuccess` (avatars), `FOnChannelsSuccess`, `FOnChannelMembersSuccess`, `FOnChannelRolesSuccess`, `FOnMyChannelsSuccess` (channels). Three multicast cache events a 2.1 project is likely to have bound with `AddDynamic`: `FOnMyTeamsCacheChanged`, `FOnMyAvatarsCacheChanged`, `FOnMyChannelsCacheChanged`. Five Game Model outcome events, for a project that adopted collections or sessions on 2.13 or 2.14: `FCrowdyCollectionItemsOutcome`, `FCrowdyTraverseOutcome` (both parameters), `FCrowdyContainerRefsOutcome`, `FCrowdySessionsOutcome`, `FCrowdySessionEventsOutcome`. **What you do:** on 2.15.0, any C++ `UFUNCTION` bound to one of them changes its parameter from `TArray Teams` to `const TArray& Teams`, and likewise for the rest. A Blueprint custom event bound to one of these pins is unaffected; Blueprint has no by-value versus by-reference distinction on the pin. :::warning[This is a source break, not a behavior change] A C++ project that takes 2.15.0 and rebuilds gets a compile error naming the handler's parameter. Nothing changes silently: if the project builds, it is right. On 2.14.0 and earlier the delegates pass by value, so a handler changed before you update fails to bind for the same reason. ::: - **The actor pool draws with no Backend Config at all.** Up to and including 2.14.0, a map profile whose Backend Config was empty, or whose config named no Replication Policy Class, left remote Dynamic entities tracked but never rendered, so choosing a Backend Config was mandatory in practice. In 2.15.0 a missing config falls back to a built-in transform-interpolation policy, `UCrowdyTransformRepPolicy`, and that step is optional. **What you do:** nothing. A project that authored its own Backend Config is unaffected; a project that left it empty and worked around the blank screen can delete the workaround once it is on 2.15.0. See [Map profile](../runtime/map-profile.md) and [Rendering backends](../runtime/rendering-backends.md). - **The shipped transform policy interpolates across a ring of samples.** Up to and including 2.14.0, `UCrowdyTransformRepPolicy` kept two samples per proxy: it snapped to a lone sample, blended between the two it held, and stopped at the newer one when the render time passed it, so a late update showed as a pause and the next as a jump. In 2.15.0 it keeps a ring of recent samples per proxy in `TInterpolatedField`, samples the pair that brackets the render time, and extrapolates from the last two for at most 0.2 seconds when the render time runs past the newest. **What you do:** nothing. Proxies drawn by the shipped policy move more smoothly through late and lost updates. A project that wrote its own policy to get that behavior can drop it, or rebuild it on `TInterpolatedField` and keep its extra fields; see [Rendering backends](../runtime/rendering-backends.md#the-sample-ring). - **A Player Derived pawn possessed after it begins play gets its account identity on possession, respawned pawns included.** Up to and including 2.14.0, `UCrowdyEntityComponent` read the Player Derived identity once, in `BeginPlay`, and the engine possesses a pawn spawned during play (`RestartPlayer`, a respawn, a spawn from a connected handler) only after that, so every runtime-spawned player pawn registered under a random id with one warning, and whatever is keyed on the local player id (the host lookup, ownership requests, the drop of your own echoed events) never worked for it; only a pawn possessed before `BeginPlay` got the account identity. In 2.15.0 a Player Derived pawn with no controller at `BeginPlay` waits, and registers under the account id inside its first possession, after the pawn's own Possessed and Controller Changed events; the first controller of any kind completes it (an AI controller gets the old fallback and warning), a later unpossess or repossess changes nothing, and a respawn that leaves the earlier pawn alive hands the id to the new pawn and clears the old one, which stops replicating. **What you do:** read the pawn's id and `IsLocallyOwned()` from `OnCrowdyOwnershipAssigned` (**On Crowdy Ownership Assigned**) or the entity subsystem's `OnEntityRegistered`, not from `BeginPlay` or Possessed: on 2.15.0 a pawn graph that gates on **Is Locally Owned** at `BeginPlay` reads false and runs nothing, where on 2.14.0 it ran with the random id. A pawn that begins play before the sign-in has answered is unchanged: it derives from user id 0 without a warning, so keep the spawn behind **On UDP Connection Success**. See [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity) and [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md#identity-policies). ## 2026-09-17 (SDK v2.14.0) **Everything in 2.14.0 is additive.** No game-facing function, pin, or type was renamed or removed, so a project on 2.0 / 2.1 has no required action for this release. Four things are new. - **Sessions can seed from app-scoped rows.** `FCrowdyGameModelCreateSessionOptions` gained `SeedFromAppTypeNames` (container types whose keyed app-scoped rows are copied into the new session as it is created) and `SeedInitialState` (`ECrowdySessionSeedState`: `Defaults` or `App`, sent only when the type list is non-empty). The create response carries `SeededContainerCount`, valid when `bHasSeededContainerCount` is set. **Create Game Session** exposes all of them. Each seeded type must be admin-instantiable or carry a bind policy; the whole create is refused otherwise. - **Container states pull in bulk.** The subsystem reads many containers in one call, up to 500 per request, chunked automatically past that, for a caller that already holds the id set. It is what **Get Collection With Items' State** uses instead of one pull per item. - **A container type can be app-scoped.** `meta=(CrowdyContainer="X", CrowdyScope="App")` declares one row per key for the whole app, bound with no session. The default is session-scoped and unchanged. An app-scoped type never takes a session id; naming one on it is refused with the code `CONTAINER_TYPE_APP_SCOPED`, which **Get Last Crowdy Model Error Code** returns. Worth doing on a project that has a shared, admin- or player-owned container every client used to race to create: declare it `CrowdyInstantiableBy="Admin"` and pre-seed it from Crowdy Studio. - **The active session survives opening a new map.** **Set Active Crowdy Session** (`SetActiveSession`) now persists on the Game Instance, and a new map reads it back when it loads. A **Leave** or **End** of the active session forgets it, as does **Clear Active Crowdy Session** (`ClearActiveSession`). Before 2.14.0 a fresh map never inherited a session. **What you do:** when a match ends, leave or end its session, or call **Clear Active Crowdy Session**, before you open the next map. :::warning[A map loaded after a finished match binds into that match unless you clear the session] A game that never clears the active session between matches loads its next map with placed shared containers still bound into the ended match's session instead of the app-global scope. Reads return the stale session's rows, and writes go there. Leave, end, or clear on the way out of every match. ::: ## 2026-09-15 (SDK v2.13.0) **Game Model sessions are new.** The July SDK had no `game-models` section and no session concept; 2.13.0 adds both. `UCrowdyGameModelSubsystem` creates, joins, leaves, ends, locks, and transfers host on a session (`CreateSession`, `JoinSession`, `LeaveSession`, `EndSession`, `SetSessionAdmission`, `TransferSessionHost`, `WatchSession`), with presence per participant and a typed change stream on **On Game Session Changed** (`OnSessionChanged`). The Blueprint category is **Crowdy SDK, Game Model, Sessions & Turns**. - **What changed:** authoritative, cheat-sensitive state (HP, inventory, scores, match rules) has a server-owned home, a Game Model container, and a match has a server-owned lifetime, a session. Neither existed in the SDK you integrated. High-frequency view state (movement, animation) stays where it was, in CrowdyState; the two do not merge. - **Session words are enums.** `FCrowdyGameModelSession::Status` is an `ECrowdySessionStatus`; admission is `ECrowdySessionAdmission`; presence is `ECrowdySessionPresence`; the reason a session ended is `ECrowdySessionEndReason`, and the reason a participant left is `ECrowdySessionLeftReason`. Each has a matching filter enum where a query takes one (`ECrowdySessionStatusFilter`, `ECrowdySessionAdmissionFilter`). In Blueprint, a **Switch on ECrowdySessionStatus** branches on a status; there is no string to compare. - **What you do otherwise:** nothing is required. A project that keeps its gameplay state in its own code keeps working. ## 2026-09-15 (SDK v2.12.0) **Outbound messages are bundled.** Several messages the SDK sends in the same pass now travel in one datagram instead of one datagram each. The switch is the CVar `crowdy.net.send.bundle`, default `1`, read when a connection opens. No API changed and nothing you send looks different to your handlers. - **What you do on a current replication server:** nothing. - **What you do on a replication server older than v0.27.0:** upgrade the server, or set `crowdy.net.send.bundle 0` in `DefaultEngine.ini` (`[ConsoleVariables]`) until you can. An older server does not unpack a bundle, so a bundled pair is dropped without an error. If reliable events start arriving in ones and not in pairs after the update, this is the first thing to check. See [Console CVars](../reference/console-cvars.md). ## 2026-08-30 (SDK, sign-in) **The dev-only sign-in bypass is gone.** `UCrowdyAuthentication::DevLogin`, email-only sign-in against a server flagged for it and never usable in production, is removed from the SDK; the server stopped accepting it on 2026-08-20. It does not exist as a deprecated stub; a call to it does not compile. - **What changed:** `Login(Email, Password, ...)` and `Register(Email, Password, ...)` work against every tier, including a local or development server, and are the replacement for any scripted sign-in. Magic link (`BeginMagicLinkSignIn`) and social sign-in (`BeginSocialSignIn`) are unchanged. - **What you do:** replace each `DevLogin` call with `Login` or `Register` and supply a real password. For a headless test client, register a dedicated account once and store its password the way you store any other secret, never in the project. - **Email + password is first-class and permanent.** If the July pages left you expecting passwordless sign-in to replace it, they should not have: password sign-in sits beside magic link and social as an equal method and has no removal planned. Nothing about it changed between 2.1 and 2.14. See [Authentication](../services/authentication.md). :::warning[A build script or CI job that signs in with DevLogin breaks at the next SDK update] Switch it to `Login` or `Register` with a real password before you update the plugin, not after. The failure is a compile error in your code, so a job that builds the project fails before it ever signs in. ::: ## Between 2.1.0 and 2.12.0 (SDK) Changes that shipped between the July plugin and v2.12.0. A project moving from 2.1 to any later release meets all of them at once. - **Teams types are named for teams, not groups.** `FCrowdyGroup`, `FCrowdyGroupMember`, `FCrowdyGroupMembership`, `FCrowdyGroupRole`, `FCrowdyRolePermissions` and `FCrowdyAppGroupPolicy` are now `FCrowdyTeam`, `FCrowdyTeamMember`, `FCrowdyTeamMembership`, `FCrowdyTeamRole`, `FCrowdyTeamPermissions` and `FCrowdyTeamPolicy`, and their members followed: `GroupId` to `TeamId`, `GroupMemberId` to `TeamMemberId`, `GroupRoleId` to `TeamRoleId`, `Membership.Group` to `Team`, `bManageGroup` to `bManageTeam`, `MaxGroupsPerUser` to `MaxTeamsPerUser`, `FCrowdyTeamsResult.Groups` to `Teams`, `FCrowdyChannelsResult.Groups` to `Channels`, and the enumerator `ECrowdyTeamPermission::ManageGroup` to `ManageTeam`. **What you do:** in C++, rename; a handler such as `OnCacheChanged(TArray)` or a read of `Team.GroupId` no longer compiles. Blueprint assets follow the plugin's own CoreRedirects and need only a resave, except a node whose function parameter was renamed (`GroupId` to `TeamId` on the `UCrowdyTeams` calls): open that graph once and reconnect the renamed pin. See [Teams](../services/teams.md) and [Packaging](./packaging.md#6-coreredirects-ship-with-the-plugin). - **`SetManagementApiUrl` is gone; `SetDiscoveryUrl` replaces it.** `UCrowdySDKSubsystem::SetManagementApiUrl` has no counterpart and no redirect. **What you do:** call `SetDiscoveryUrl` for the shared origin, or `SetGameApiUrl` for the gameplay endpoint; in Blueprint the old node is missing on load and is replaced by hand. See [Project settings](../reference/project-settings.md). - **The query and subscription subsystems are gone.** `UCrowdyQuerySubsystem` and `UCrowdySubscriptionSubsystem` (their `Connect`, `ExecuteQuery`, `GetSubscriptionState` calls and `OnConnected`, `OnDisconnected`, `OnGetVersionInfo`, `OnTeleport` events) do not exist. GraphQL now goes through the service subsystems. **What you do:** remove the calls; a Blueprint that used them has missing nodes on load. The services pages ([Teams](../services/teams.md), [Avatars](../services/avatars.md), [Authentication](../services/authentication.md)) are where the same data comes from now. - **The C++ reception-layer interface is gone.** `ICrowdyReceptionLayer` and `UCrowdySDKSubsystem::RegisterReceptionLayer` / `IsLayerRegistered` are removed. **What you do:** a C++ system that implemented the interface subscribes instead: `UCrowdySDKSubsystem::SubscribeToEventPayload`, `SubscribeToActorUpdatePayload`, `SubscribeToAllPayloads` and `SubscribeToOpcode` return an `FCrowdySubscription` handle that delivers for as long as it lives. The Blueprint **Crowdy Blueprint Reception Layer** is unchanged; see [Recipients and routing](../runtime/recipients-and-routing.md). - **The connection monitor is no longer part of the documented path.** `UCrowdySDKSubsystem` retries a dropped UDP connection on its own and broadcasts **On UDP Timed Out** when a re-assignment failed and **On UDP Connection Success** when the connection is back. A project that also calls `InitConnectionMonitor` runs a second, redundant retry loop beside the SDK's. Five functions on `UCrowdySDKSubsystem` are deprecated. Three do nothing: `StartUDPTimeoutMonitoring`, `TriggerUdpHeartbeat`, `DeregisterAllReceptionLayers`. `StopUDPTimeoutMonitoring` only stops the latency ping, and `SetQueryEndpoint` forwards to `SetGameApiUrl`. **What you do:** bind the two events in your Game Instance's `Init`, remove every call to `InitConnectionMonitor` and to the first four, and replace `SetQueryEndpoint` with `SetGameApiUrl` or `SetDiscoveryUrl`. The compiler warns on each deprecated call, so a build log lists them for you. See [Connection and reconnect](../runtime/connection-and-reconnect.md#deprecated-calls). - **A project that configures no map profile now runs networked.** The plugin registers a shipped default profile, so a map with no `MapProfiles` entry and no `DefaultProfile` resolves to it: networking on, the actor pool as backend. In 2.1 such a map went inactive. **What you do:** nothing if you have profiles; if you relied on an unlisted map being inactive, give it a profile with **Enable Networking** off. See [Map profile](../runtime/map-profile.md). ## Persistence **`UCrowdyPersistenceSubsystem` is deprecated in these docs.** The push / pull of tagged gameplay structs over UDP and GraphQL that the July `services/persistence` page described still exists in the SDK, with its three callables `PushState`, `PullState`, and `ClearAllState` (and the Blueprint **Push State** / **Pull State** wildcards), and it keeps working in 2.14. It receives no further development, and no removal date is set. The plugin does not mark it, so no compiler or Blueprint warning appears. The two struct tags it reads, `CrowdyPersistent` (push and pull this struct) and `CrowdySingleton` (one slot, not one per instance), are still honored and go with it. - **What you do:** nothing today. When you touch that code next, move the state into your app's server code on [ck-exec](/exec/intro), which is what persistence was standing in for. Authoritative and cheat-sensitive state belongs on the server, never in CrowdyState; see [The Two Planes](../concepts/two-planes.md). - **What not to do:** do not start a new feature on `PushState`. Server code gives you the same durable, per-subject state plus server-side validation, and it is where the effort goes. ## Gotchas - For a C++ project on 2.14.0 the source breaks are the teams rename, the removals listed under "Between 2.1.0 and 2.12.0", and `DevLogin`. If your project builds clean against 2.14.0 after those, nothing else here is blocking; the array delegates come with the next release. - A Blueprint project's exposure is the renamed `GroupId` pins on the teams calls, the missing `DevLogin`, `SetManagementApiUrl`, query and subscription nodes on load, the persistence nodes (still present, no warning) and the connection-monitor nodes (still present, marked deprecated). - `crowdy.net.send.bundle 0` is a compatibility switch for an old server, not a debugging aid. Leave it at `1` on a current server. - The [sample project](./sample-project.md) targets SDK 2.1.0 and predates every entry on this page; its tour is a 2.1 tour. ## Related - [Authentication](../services/authentication.md): every sign-in method that exists now. - [Connection and reconnect](../runtime/connection-and-reconnect.md): the two events that replace the connection monitor. - [Map profile](../runtime/map-profile.md) and [Rendering backends](../runtime/rendering-backends.md): the optional Backend Config. - [Teams](../services/teams.md), [Avatars](../services/avatars.md), [Channels](../runtime/channels.md): the services whose types were renamed and whose array delegates change next release. - [Recipients and routing](../runtime/recipients-and-routing.md): the Blueprint reception layer that survives the C++ interface's removal. - [Console CVars](../reference/console-cvars.md): `crowdy.net.send.bundle` and the rest. # Classes and Subsystems Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/subsystems # Classes and Subsystems The consumer-facing classes you fetch most, with the module they live in and where to read more. The manifest carries 31 classes tagged as subsystems; 8 are excluded here because they are internal plumbing or deprecated, listed at the bottom of this page for completeness. ## When you come here You know the shape of what you need (send an event, read a team, elect a host) but not which class owns it, or you want the one-line reminder of what a class you already use is for. :::note[Every subsystem here initializes automatically. You fetch it, you never create one.] ::: ## Fetching a subsystem How you fetch a subsystem depends on its scope. Game instance subsystems come from the game instance, world subsystems come from the world. Game instance subsystems: ```cpp UCrowdySDKSubsystem* SDK = GetGameInstance()->GetSubsystem(); UCrowdyTeams* Teams = GetGameInstance()->GetSubsystem(); ``` World subsystems: ```cpp UCrowdyEntitySubsystem* Entities = GetWorld()->GetSubsystem(); ``` Blueprint function libraries (`UCrowdyUtilities`, `UCrowdyStateBlueprintLibrary`, `UCrowdyOwnershipTransfer`, `UCrowdyReplicatedSubsystemLibrary`) are static and need no fetch; see [Entities and spawning](../runtime/entities-and-spawning.md), [Marking state from outside the actor](../runtime/crowdy-state.md#marking-state-from-outside-the-actor), [Ownership transfer](../runtime/ownership-transfer.md) and [Replicated subsystems](../runtime/replicated-subsystems.md). ## Excluded from this table Eight of the manifest's 31 subsystem classes do not appear in the table above: - `UCrowdyCppClientSubsystem`, `UCrowdyCppReplicationSubsystem`, `UCrowdySDKBridgeSubsystem`, `UCrowdyWorkerThreadsSubsystem`: the vendored CrowdyCPP bridge and its worker threads. The game-facing surface is `UCrowdySDKSubsystem`; see [Connection and reconnect](../runtime/connection-and-reconnect.md). - `UCrowdyConnectionMonitor`: the SDK reconnects on its own after a timeout, so starting the monitor adds a second retry loop. What it reports is covered on [Connection and reconnect](../runtime/connection-and-reconnect.md). - Two test harnesses in the Mass module that the SDK uses to verify itself. They are not part of the surface a game uses. - `UCrowdyPersistenceSubsystem`: superseded by server code on ck-exec; still present and working, and the plugin does not mark it deprecated. It reads the `CrowdyPersistent` and `CrowdySingleton` struct tags. See [What's Changed](../guides/whats-changed.md#persistence) for what still works and what does not get further development. ## Gotchas - A world subsystem needs a valid world; fetch it after `BeginPlay`, not in a constructor. - Read the Outer column for scope; the class name does not always say it (`UCrowdyGameSession` is a game instance subsystem, `UCrowdyHostSubsystem` a world one). - `UCrowdySDKDeveloperSettings` looks like it belongs here but is not a subsystem at all; it lives on [Project Settings](./project-settings.md). ## Related - [Host authority](../runtime/host-authority.md) - [Connection and reconnect](../runtime/connection-and-reconnect.md) - [What's Changed](../guides/whats-changed.md#persistence) # Blueprint Async Actions Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/async-actions # Blueprint Async Actions A Blueprint async action is a latent node: it keeps running after the graph moves on and fires one of its own exec pins when it finishes. The SDK ships 60 of them, one C++ class each. One, the class behind the typed Call node of a Server Object, has no palette entry of its own, so the table lists the other 59. Every one listed is covered by the pages below, so this table is a lookup, not a gap list. ## When you come here You have a node open in the graph and want the page that explains its inputs, or you remember a page and want the exact node name to search for in the palette. The **Blueprint node** column is the title you see in the Blueprint editor, taken from the factory function's display name. The **C++ factory** column is the static function that creates the action, and the **Class** column is the C++ class behind it; you need those only for a `Cast` or a manual `Activate` call from C++, since the whole point of an async action is that Blueprint drives it. **Completion pins** lists the node's output exec pins in order, so you know which ones to wire before you drop the node. :::note[Every row above links forward. There is nothing left uncovered in this table.] Coverage for the 59 listed is complete: every one is mentioned on the page its row links to. A class that later stops matching any page (a rename, a page deleted) reopens as a coverage gap, not a silent removal from this table. ::: ## Gotchas - The node's display name, its factory function and its C++ class name can all differ (the **Call Server Function** node is `UCrowdyServerCallAction`); search the palette by the **Blueprint node** column, not the **Class** column. - A latent node only fires its completion pins while the graph that called it is still alive. A widget removed from its parent before the call completes never sees the result. - An async action with no ID pin operates on whatever the node's own inputs named at spawn time; it does not automatically track a Target that changes after the call starts. ## Related - [Subsystems](./subsystems.md) - [Delegates](./delegates.md) - [RPC Events in Blueprint](../runtime/rpc-events-blueprint.md) # Blueprint Delegates Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/delegates # Blueprint Delegates A dynamic multicast delegate is what you bind with **Bind Event to** or **Assign** in Blueprint, or `AddDynamic` in C++. The table below lists the ones a game can bind, grouped by the owner class. It leaves out `UCrowdyConnectionMonitor::OnConnectionStateChanged`, because the SDK reconnects on its own. The voice ones are `OnVoiceMicStateChanged` and `OnVoiceSpeakerTalking` on `UCrowdySDKSubsystem`. ## When you come here You have a delegate bound and want to know what its parameters mean, or you know roughly what you want to react to and want the class that fires it. Every delegate below fires on the game thread. This is a constant fact about every dynamic multicast delegate the SDK broadcasts, never a per-delegate choice, so it is stated once here instead of as a column that would read the same value on every row: your handler runs like any other Blueprint event or `UFUNCTION`, with no thread-safety code of your own to write. :::note[OnConnectionStateChanged is not in the table.] `UCrowdyConnectionMonitor::OnConnectionStateChanged` is not part of the documented path: the SDK reconnects on its own, and starting the monitor adds a second retry loop. Binding it to observe the connection state is safe; do not build retry logic on it. See [Connection and reconnect](../runtime/connection-and-reconnect.md). ::: ## Gotchas - A delegate's owner class must be reachable before you can bind it: a game instance subsystem's delegate is safe to bind in a C++ Game Instance's `Init` after `Super::Init()`, and in Blueprint from the Game Mode's Event BeginPlay (the Game Instance's Event Init runs before the subsystems exist); a world subsystem's needs a valid world first. - Binding twice fires your handler twice. Unbind (`Remove Dynamic` or `RemoveDynamic`) before a re-bind on a path that can run more than once, such as a widget that reconstructs. - The voice delegates live on `UCrowdySDKSubsystem`, which stays alive across maps, so you bind them once and they keep firing after a map change. The earlier voice delegates (`OnAudioNotify`, `OnAudioDataGenerated`, `OnAudioDataReceived`, `OnStreamTimeout`) and the audio device monitor's two are gone. See [Voice chat](../services/voice-chat.md). ## Related - [Subsystems](./subsystems.md) - [Async actions](./async-actions.md) - [Connection and reconnect](../runtime/connection-and-reconnect.md) # Console Variables Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/console-cvars # Console Variables This page lists 51 `crowdy.*` console entries. They come in three kinds: trace gates that turn on informational logging for one area, behavior switches that change what the SDK does, and diagnostic commands that run once and hold no stored value. A row badged **Editor only** exists only in the editor process. Five of the diagnostic commands (`crowdy.rpc.dumpfn`, `crowdy.state.heartbeat.advisories`, `crowdy.registry.dump`, `crowdy.mass.derive`, `crowdy.mass.state.verdict`) and every other `crowdy.mass.*` entry are compiled out of a Shipping build and are absent there rather than silent; every other row ships in Development and Shipping. ## When you land here You want to know what a `crowdy.*` name does before you type it, or you are looking for the switch that turns on a specific area's logging. For the workflow around them (loopback testing, two-PIE setups), see [Testing locally](../guides/testing-locally.md); this page is the lookup table. ## Trace gates All 19 are off by default. Seventeen turn on that area's informational lines when set to `1`; the two `.scopes` entries add Unreal Insights CPU scopes instead and log nothing. Warnings and errors print regardless of the setting. :::caution[`crowdy.serialize.trace` and `crowdy.serialize.scopes` are high frequency.] They log on every message encode and decode. Leave them off unless you are actively debugging serialization. ::: :::caution[`crowdy.net.trace` logs every routed send and every received spatial message.] Since 2.17.0 it also prints one line per spatial message received ("Received opcode ... for actor ... in chunk ...") and the transport's totals (datagrams received, verification failures, drops) about once a second. That is what tells "the server never sent it" apart from "the client dropped it"; it is too chatty to leave on. ::: :::note[`crowdy.serialize.scopes` and `crowdy.state.scopes` are nested CPU trace scopes.] Each nests inside a wider enclosing scope, so turning one on shifts the timing you read for that enclosing scope too. If you are taking a performance reading, say whether the flag was on. ::: `crowdy.studio.trace` and `crowdy.mass.nodes.trace` are editor only. `crowdy.studio.trace` never logs the bearer token, a pattern worth copying in your own logging around Studio calls. ## Behavior switches These change what the SDK does rather than what it logs. None of the 24 exist only in the editor. The notes map above stands in for these six rows because the surface exporter drops a CVar help built from adjacent `TEXT()` literals. :::warning[Turn `crowdy.net.http2` off if you ship, or collect logs from, a build with logging enabled.] On by default, it asks for HTTP/2 on the SDK's own requests (falling back to HTTP/1.1 when the server does not offer it); the game's other HTTP traffic and the engine's `http.CurlAllowHTTP2` are untouched. With HTTP/2, when a request fails at the network level (not a cancel or a timeout), the curl diagnostics the engine logs at `Warning` can include the request headers, bearer token included. A default Shipping build compiles logging out, so most projects never see this; if yours ships or collects logs from a build with logging enabled, set `crowdy.net.http2 0`. ::: :::note[`crowdy.net.retry.busy` resends a refusal the platform blames on itself before the caller ever sees `Failed`.] On by default. An SDK query resends on any platform-blamed retryable refusal except `WRONG_DATACENTER` and `APP_UNAVAILABLE`. At most 3 retries, with the server's suggested wait when it names one or a doubling local wait otherwise. Password sign-in, the app token mint and the app token refresh are retried the same way when the platform answers that it is busy, and a retried request is dropped if the account's token changed meanwhile. Registration, link and social completion, password changes, email sends, logout and account deletion are never retried. Turn it off (`0`) only to compare against the un-retried behavior; leave it on for a shipping build. ::: :::note[`crowdy.kit.emotes.*` cap the emotes a client will read, per sender and across all senders.] Each sender has its own small budget, and every emote also has to fit one budget shared across all senders. `crowdy.kit.emotes.persender.maxpersecond` (default 4) is how fast one sender's budget refills; `crowdy.kit.emotes.persender.burst` (default 4) is the most it holds. The per-sender budget is checked first, so one player flooding emotes runs out of their own budget and no longer uses up the shared one. An emote the shared budget drops costs its sender nothing. A client tracks at most 256 senders, and when that is full the sender that has been silent longest is forgotten. The shared budget exists because a sender id is only what the sender claims. `crowdy.kit.emotes.maxpersecond` (default 64) is how fast it refills; `crowdy.kit.emotes.burst` (default 16) is the most it holds. An emote over either budget is dropped before it is read, with one Warning in the log. A peer that forges a player's id can use up that player's per-sender budget, which costs that player nothing but their own emotes. Raise the numbers only if a busy scene legitimately sends more emotes than that. See [Send an emote](../services/play-together.md#send-an-emote). ::: :::warning[Leave `crowdy.rpc.allowObjectLoad` off in production.] While it is off, an untrusted peer cannot trigger an arbitrary asset load; an unresolved object or class reference in a received RPC is delivered as null instead. ::: `crowdy.rpc.loopback` and `crowdy.state.loopback` are not the same switch wearing two names. `crowdy.rpc.loopback` delivers a sent event back to your own receive path. `crowdy.state.loopback` cannot replay onto the same actor that sent the delta: an owner's own entity drops a non-host-sourced delta, and changed-detection on decode sees no change against identical values. Instead it spawns a distinct, lazily-created mirror entity and replays deltas onto that, so `CrowdyOnRep` still fires with one PIE client. Expect two entities, not one, when you turn it on. `crowdy.net.receive.poolactorupdates` defaults to `1` (pooling on). Turning it off is for an A/B measurement against the same build, not a fix for anything. ## Diagnostic commands Eight commands with no stored value. Only `crowdy.rpc.dumpfn` and `crowdy.mass.derive` take arguments and print usage without them; every other command acts as soon as you run it. Five are compiled out of a Shipping build: `crowdy.rpc.dumpfn`, `crowdy.state.heartbeat.advisories`, `crowdy.registry.dump`, `crowdy.mass.derive` and `crowdy.mass.state.verdict`. `crowdy.registry.dump` logs every entry the cook baked into the registry, one line each: the structs and their layout hashes, the CrowdyState properties, and the CrowdyEvent functions, with their ids. It is the console version of the Registry page (see [Inspector and Registry](../studio/inspector-and-registry.md)). :::tip[`crowdy.rpc.dumpfn` and `crowdy.state.heartbeat.advisories` print structured text you can paste directly into a bug report.] ::: The development-build sign-in commands `crowdy.auth.signin.file`, `crowdy.auth.status` and `crowdy.auth.signout` are described under [Sign in from the console](../guides/testing-locally.md#sign-in-from-the-console). `crowdy.cpp.selftest` and `crowdy.net.routes` carry no build guard, so they exist in every build including Shipping; the four diagnostics above do not, and a Shipping console answers them with an unknown command. ## Gotchas - Warnings and errors are never gated behind a trace flag. They print under the module's log category no matter what. - `crowdy.net.receive.maxdrainms` (4 ms) and `crowdy.net.receive.maxmessages` (3072) bound one frame's receive drain. If drains keep ending on the time budget, delivery is costing more per message than the frame can afford, and raising the count is not the lever. ## Related - [Log categories](./log-categories.md): the category each trace gate's lines print under. - [Project settings](./project-settings.md): the settings-class properties that sit beside these CVars. - [Testing locally](../guides/testing-locally.md): loopback and two-PIE workflows built on these switches. # Project Settings Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/project-settings # Project Settings The SDK's settings live on `UCrowdySDKDeveloperSettings`, under **Project Settings, Plugins, Crowdy SDK**. It is written to `Config/DefaultGame.ini` under `[/Script/CrowdyReplication.CrowdySDKDeveloperSettings]`. None of it is secret; commit the file as usual. ## When you land here You are looking for a property's name, category, or default, or you want to know whether a value is safe to type by hand or gets overwritten the next time you sync. ## UCrowdySDKDeveloperSettings The **Written by** column tells you which: **Config Sync** properties are read-only in Project Settings and come from [Config Sync](../studio/config-sync.md); **you** properties are yours to edit directly. `Environment` is an `ECrowdyEnvironment`: **Dev (shared)**, **Test**, **Production**, or **Custom**, which is when the Discovery URL is read. Dev, Test, and Production each resolve to a built-in host (`ck.dev.crowdedkingdoms.com`, `ck.test.crowdedkingdoms.com`, `ck.prod.crowdedkingdoms.com`); Custom is the only value that reads `DiscoveryUrl` from the project. **Choose Production.** Dev and Test are Crowded Kingdoms' internal environments: only staff and approved partners can sign in there, and any other account is refused with `TIER_ACCESS_REQUIRED`. :::danger[Never hand-type a Config Sync property.] The Network category is read-only in the editor on purpose, so Crowdy Studio stays the single source of truth. A value pasted into the ini by hand is overwritten by the next sync, and a project whose settings drift from its app fails to connect with no obvious error. ::: :::warning[`UDPTimeoutSeconds` must stay above the SDK's ping interval.] It is set on Crowdy Studio's Project page (Connection) and clamped to 6 to 120 seconds; the ping interval is 5 seconds, so the floor already keeps it above. Too low and the SDK would re-assign a server during normal traffic instead of only after a real drop. ::: :::warning[`bAutoClearPersistentStateOnShutdown` fires only while UDP is still connected.] It is not guaranteed at shutdown. For cleanup you can rely on, call `ClearAllState` before logout instead. ::: :::info[`bShowConsentPrompt` is on by default.] It opens the built-in terms and age prompt when a sign-in needs the player's agreement. Turn it off only if your game draws its own; see [Draw your own prompt](../services/authentication.md#draw-your-own-prompt). ::: :::caution[Map profiles decide what the SDK does on each level.] These settings point the SDK at an app; `MapProfiles` and `DefaultProfile` decide what it does per map. A map with neither runs on the SDK's shipped default profile; a row whose asset does not load leaves the map inactive. See [Map profile](../runtime/map-profile.md). ::: `IDOverrides` and `ClassIDOverrides` exist only to resolve a startup type-id hash collision. Leave both empty until the log reports one; in practice they stay empty for almost every project. A row is the type and the id to force: an `FCrowdyIDOverride` holds `Struct` and `Override ID` (0 to 65535) for `IDOverrides`; an `FCrowdyClassIDOverride` holds `Entity Class` and `Override ID` (1 or more) for `ClassIDOverrides`. ### The default Backend A project that has never run Config Sync's Backend selector has no `Environment` written to its `DefaultGame.ini` yet, so it runs on whatever this SDK build defaults to: the tier the build was released for. A build off the `dev` branch defaults to Dev, a `test` build to Test (both internal), and the public release defaults to Production; the Discovery URL default follows the same tier. Read the active default from code with the static `UCrowdySDKDeveloperSettings::GetReleaseEnvironment()` (C++ only, not exposed to Blueprint). Once you pick a Backend on the Sign In page and sync, `DefaultGame.ini` holds that choice from then on and this default no longer applies. ### Setting endpoints at runtime Two runtime setters on `UCrowdySDKSubsystem` look like a way to configure these values in code, but they are session-only overrides, not writes to the settings class: `SetDiscoveryUrl` stores a shared origin for the current session and is only read when `Environment` is Custom, and `SetGameApiUrl` stores one Game API HTTP endpoint for the session. `ReloadEndpointsFromSettings` is deprecated: it is kept so old call sites still compile and does nothing, so remove it from your graphs and code; the API client already re-reads both endpoints from the settings on every call. ### Server Object gateway domains A Server Object connects to the gateway the Game API names. The SDK connects only when that gateway is on the Game API's own domains, so a Game API that points somewhere else cannot receive the player's connect token. If you host the server yourself on a domain of your own, list it under **Project Settings, Crowdy SDK, Server Objects, Allowed Exec Gateway Domains** (`AllowedExecGatewayDomains` in `Config/DefaultGame.ini`, with the other Crowdy SDK settings). An entry such as `example.com` allows that domain and its subdomains. A scheme, a path, a port, a leading dot or `*.` in an entry is ignored, so `https://*.example.com:8443/` counts as `example.com`. An entry that does not read as a domain at all, such as `https://` or `*.`, allows nothing, and the SDK logs a Warning naming it when it checks a gateway outside the Game API's own domains. A gateway outside both the Game API's own domains and this list is refused for good: every Server Object fails with a reason that starts "Could not open the connection to the server: the Game API named a gateway outside its own domains", and none is retried until you acquire it again (or call Restart). See [Troubleshooting](../exec/troubleshooting.md#what-makes-it-fail). ### Server Object timeouts Two properties under **Crowdy SDK, Developer, Server Objects** set how long a Server Object connection waits. Like the Network category they are read-only in Project Settings and are edited on Crowdy Studio's Project page, in the **Connection** tab under **Server Objects**; see [Config Sync](../studio/config-sync.md). - `ServerObjectCallTimeoutSeconds` (**Server Object Call Timeout**) is how long a call waits for the platform's answer before it fails with a `Timeout`: 11 to 120 seconds, 20 by default. It stays above the longest **Time per Call** a type may have, 10 seconds. A **Call** node's **Timeout** can shorten it for one call; see [Give up sooner](../exec/from-blueprint/call-functions.md#give-up-sooner). - `ServerObjectOpenTimeoutSeconds` (**Server Object Open Timeout**) is how long opening the connection waits before the attempt fails: 1 to 60 seconds, 10 by default. Both apply from the next Play, and to the editor's developer connections. A dropped connection is always reopened; there is no setting for that. ## Gotchas - The class is `Config=Game`. A property you change in code at runtime through the settings object itself does not persist past the session; use the two runtime setters above for a session-only override instead. - `MapProfiles` and `DefaultProfile` decide what runs per level; a property here being set correctly does not mean a given map resolved the profile you meant. - The baked registry asset is required in a packaged build, but the `BakedRegistry` setting may stay empty: the SDK falls back to the fixed path the cook writes. See [Packaging](../guides/packaging.md). ## Related - [Config Sync](../studio/config-sync.md): the authoring source for the Network category split. - [Map profile](../runtime/map-profile.md): the other half of "make replication work". - [Console variables](./console-cvars.md): the CVar surface alongside these settings. # RPC Metadata Keys Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/rpc-meta-keys # RPC Metadata Keys This page lists the metadata you place on a CrowdyEvent receiver to control routing and, for a Blueprint custom event, to mark it as RPC-style in the first place. Come here when you are writing or reviewing a `CrowdyEvent` receiver and need the exact key names, their accepted values and defaults, and where each one is legal to place. In C++ the keys go on the `UFUNCTION(meta=(...))` line of your `_Implementation` function. ```cpp UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "SpatialMulticast", CrowdyDecay = "Exponential_Decay", CrowdyDistance = "Four_Chunks")) void FireWeapon_Implementation(int32 Ammo, FVector Direction); CROWDY_EVENT(FireWeapon) ``` ## Keys The marker that turns a function into a CrowdyEvent receiver: The routing keys that go alongside it, and the Blueprint-only markers that identify an RPC-style event to the compiler: The manifest carries the symbol and its header; the accepted values, defaults, and behavior below come from reading the header directly, since the manifest has no default or notes field for a meta key. `CrowdyEvent` takes no value and is required on every receiver. Its legal place is any `UFUNCTION`, whether you wrote the C++ by hand or the compiler generated it from a Blueprint custom event marked `CrowdyReplicates`. Both a C++ receiver and a generated Blueprint one carry `CrowdyEvent`; only the Blueprint one also needs `CrowdyReplicates` sitting next to it, because a Blueprint graph has no other way to tell the router this custom event is RPC-style rather than a struct handler. `CrowdyRecipient` takes an `ECrowdyEventRecipient` name and defaults to `SpatialMulticast`. The enumerators, in declared order, are `SpatialMulticast`, `Multicast`, `OwningClient`, and `Host`. See [enums](/unreal-sdk/reference/enums) for the full type. `CrowdyDecay` takes an `ECrowdyDecayRate` name and defaults to `No_Decay`. The enumerators are `No_Decay`, `Exponential_Decay`, `Linear_50`, `Linear_25`, `Linear_10`, and `Linear_5`. It applies only to `SpatialMulticast`; the SDK reads it unconditionally, but it only changes anything on the spatial send path. `CrowdyDistance` takes an `ECrowdyReplicationDistance` name and defaults to `Eight_Chunks`, the maximum range. It applies only to `SpatialMulticast`, same as `CrowdyDecay`. `CrowdyChannel` takes a channel name string and defaults to empty, which means the app-wide default session channel. It names the channel a `Multicast` event routes over by name rather than id, so the same event resolves to the same channel in every environment your app is deployed to. `CrowdyAction` takes no value. It declares that the receiver's parameters describe a one-shot action, an attack swing, a flinch, an emote, rather than a body to run. The Crowdy Mass Rendering Backend plays it as an animation action on a receiver that holds the entity as a crowd row rather than a live actor. On the actor pool the declaration changes nothing. See [Play poses and one-shot actions](../mass/play-poses-and-one-shot-actions.md). `CrowdyReplicates` takes no value. It marks a Blueprint custom event as RPC-style ("Crowdy Replicates" in the Blueprint dropdown), which is what tells the router to bind it through the RPC path rather than as a struct handler, and lets the compiler inject the dispatch gate. A C++ receiver never sets this key; `CROWDY_EVENT` is the C++ equivalent. `CrowdyReplicate` is a legacy alias of `CrowdyReplicates`, kept so an older Blueprint asset still resolves. It is deliberately not reused by Crowdy State: the same string already means "this is an RPC event," so a state property never carries it. See [state metadata keys](./state-meta-keys.md). :::caution[The receiver must be a real `UFUNCTION`. The `CROWDY_EVENT(Name)` macro only generates the call site. It cannot emit the `UFUNCTION`, because UnrealHeaderTool does not expand macros when it scans for reflected functions.] ::: :::warning[`CrowdyRecipient = "Host"` addresses the elected host. Election is a convention, not an enforcement boundary, so do not treat Host-only delivery as a security control.] Anyone can hold the host role at some point in a session. If a call must only take effect for a server-validated reason, check that reason on the receiving end; do not rely on the recipient filter alone to keep an untrusted client from reaching the logic. ::: ## Gotchas - `CrowdyDecay` and `CrowdyDistance` are read unconditionally, but neither changes anything outside the `SpatialMulticast` recipient. - `CrowdyReplicates` and its legacy alias `CrowdyReplicate` only ever appear on a Blueprint custom event; a C++ receiver never sets them. - `CrowdyAction` is a declaration only. The actor pool ignores it; the Crowdy Mass Rendering Backend reads it to play the call as an action on a crowd row. See [Play poses and one-shot actions](../mass/play-poses-and-one-shot-actions.md). ## Related - [State metadata keys](./state-meta-keys.md) - [RPC parameter types](./rpc-types.md) - [RPC Events in C++](/unreal-sdk/runtime/rpc-events-cpp) - [Enums](./enums.md) # Crowdy State Metadata Keys Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/state-meta-keys # Crowdy State Metadata Keys This page lists the metadata you place on a property to control Crowdy State property replication. For the full guide, see [Crowdy State](/unreal-sdk/runtime/crowdy-state). Come here for the exact key names, their accepted values and defaults, and what a Crowdy State property may hold. In C++ the keys go on the `UPROPERTY(meta=(...))` line. In Blueprint they are set from the variable's Details panel through the **Crowdy Replication** dropdown, not typed by hand. ```cpp UPROPERTY(meta = (CrowdyState, CrowdyOnRep = "OnRep_Stance", CrowdyOwnerOnly, CrowdyManualDirty, CrowdyHeartbeat)) uint8 Stance = 0; UFUNCTION() void OnRep_Stance(); ``` ## Keys The manifest carries the symbol and its header; the accepted values, defaults, and behavior below come from the header directly. `CrowdyState` takes no value and is required. It marks the property for Crowdy State; the other four keys do nothing without it. Its legal place is any `UPROPERTY`, never a `UFUNCTION` or `UCLASS`. `CrowdyOnRep` takes the name of a parameterless notify function and has no default. It runs on the receiver right after the value is written, and only when the value actually changed: GAS-style, no arguments, no previous value. Read the new value off the property itself. `CrowdyOwnerOnly` takes no value and defaults to off. It is a delivery scope, not secrecy: the property ships only to the entity's owning client, over the targeted single-actor path, never on the spatial broadcast. This changes who receives the value, not who could intercept it. `CrowdyManualDirty` takes no value and defaults to off. It skips the automatic per-tick diff; the value ships only when you call `MarkStateDirty`. One mark sends exactly the next update, then clears. It is meant for a big or expensive property you do not want diffed every tick. `CrowdyHeartbeat` takes no value and defaults to off in C++. It opts a property into the periodic keyframe baseline, so a late-relevant peer gets its current value. On-change replication is unaffected either way: a changed property always ships, marked or not, marked properties are simply re-sent while unchanged too. The Blueprint "Crowdy Replication" dropdown pre-checks Heartbeat the first time a variable enters Replicated mode, as a convenience; the underlying mechanism is the same opt-in metadata either way. :::note[`CrowdyOwnerOnly`, `CrowdyManualDirty`, and `CrowdyHeartbeat` are presence markers. You add the bare key to turn each on; they take no value. The code tests only whether the key is present, so do not write `= true` -- and `= false` would still count as on. `CrowdyOnRep` is the one key that takes a value, the notify function's name.] ::: :::note[`CrowdyState` is deliberately not spelled `CrowdyReplicate`. That string already belongs to the RPC side, as an alias of the Blueprint marker `CrowdyReplicates`. Reusing it here would make one key mean two different systems and break RPC-versus-state discovery. If you came from the RPC metadata keys page expecting the two families to mirror each other by name, they do not, on purpose. See [RPC metadata keys](./rpc-meta-keys.md).] ::: ## What can be marked Crowdy State covers plain values and plain structs: numbers, bools, enums, `FName`, `FString`, and USTRUCTs built from those. Object references, containers (`TArray`, `TSet`, `TMap`, including buried inside a struct), and static arrays are rejected at discovery and simply do not replicate. See [Unsupported types](/unreal-sdk/runtime/crowdy-state#rejected) for the full list and the reasons. A struct type that carries a native net serializer (`FVector_NetQuantize`, `FRotator`, and the rest of the `_NetQuantize` family) quantizes on the wire automatically, with no key to set. See [type-driven quantization](/unreal-sdk/runtime/crowdy-state#accepted). The Blueprint "Crowdy Replication" dropdown offers **None** and **Replicated**; **Replicated** writes these keys for you. A `CrowdyModel` or `CrowdyContainer` key left on a property from the removed Game Model API does nothing; see [Move from Game Models](../exec/move-from-game-models.md#upgrade-steps). :::warning[`HasMetaData`-backed discovery helpers, including the one behind this header, answer false in a cooked build, because cooked builds strip metadata. A runtime check routes through the baked registry instead, never through live metadata.] ::: ## Gotchas - All five keys go on a `UPROPERTY`; none apply to a `UFUNCTION` or `UCLASS`. - `CrowdyHeartbeat` only changes how often an unchanged value is re-sent. A changed value always ships, heartbeat or not. - A property is discovered live only in the editor. A packaged build reads the baked rep table, not the metadata on the header. ## Related - [Crowdy State](/unreal-sdk/runtime/crowdy-state): owner diffing, host precedence, world entities, and the keyframe baseline. - [RPC metadata keys](./rpc-meta-keys.md) # Enums Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/enums # Enums The enums you reach for most often when working with entities, RPC events, and Crowdy State properties, described in full below. Every other BlueprintType enum the SDK exposes is in the generated table at the end of this page, so a name you cannot find in the ten sections here is still worth a search there before you open a header. ## ECrowdyEventRecipient Who runs a CrowdyEvent, and over which transport. Header: `Core/UDP/Enums/ECrowdyMessageType.h`. | Value | Meaning | | --- | --- | | `SpatialMulticast` | Everyone in range runs it, over the spatial path, thinned by decay and distance. The default. | | `Multicast` | Every member of the session channel runs it, over the channel transport, at any distance with no decay. | | `OwningClient` | Only the client that owns the target entity runs it. | | `Host` | Only the elected host runs it. The host may act on any entity it has locally. | :::note[`SpatialMulticast` is the default recipient. For a full walkthrough of how each value selects its transport, see [Recipients and Routing](/unreal-sdk/runtime/recipients-and-routing).] ::: ## ECrowdyDecayRate How delivery probability falls off with distance. :::tip[Decay applies only to `SpatialMulticast`. It has no effect on the other recipients.] ::: | Value | | --- | | `No_Decay` | | `Exponential_Decay` | | `Linear_50` | | `Linear_25` | | `Linear_10` | | `Linear_5` | ## ECrowdyReplicationDistance The maximum distance, in chunks, that a `SpatialMulticast` event travels from the sender. Values: `None`, `One_Chunk`, `Two_Chunks`, `Three_Chunks`, `Four_Chunks`, `Five_Chunks`, `Six_Chunks`, `Seven_Chunks`, `Eight_Chunks`. `Eight_Chunks` is the default. ## ECrowdyEntityMode How an entity replicates. Set it on `UCrowdyEntityComponent`. Header: `Replication/Components/CrowdyEntityComponent.h`. | Value | Meaning | | --- | --- | | `Dynamic` | The continuous state channel streams the executor's snapshot every interval. Use it for movement and anything high frequency. | | `Static` | Event only. State travels through `SendEvent` and CrowdyEvent handlers. | ## ECrowdyIdentityPolicy How an entity gets its `NetID`. | Value | Meaning | | --- | --- | | `PlayerDerived` | From the signed in user id. Only valid on the locally controlled player pawn. | | `Stable` | Hashed from the actor's level path. Every client computes the same id for a level placed actor. | | `Random` | A fresh id each session. Use for runtime spawns. | ## ECrowdyRole This client's relationship to an entity. Header: `Data/CrowdyEntityTypes.h`. | Value | Meaning | | --- | --- | | `None` | Not registered. | | `Owner` | This client simulates the entity and sends its state and events. | | `RemoteProxy` | This client receives state and events from the network. | | `HostOwned` | Owned by whoever is host. Reassigned on host migration. | ## ECrowdyOwnership The authority axis for an entity: who owns and simulates it. Set it on `UCrowdyEntityComponent`. It is orthogonal to `ECrowdyEntityMode` and `ECrowdyIdentityPolicy`, and mostly matters for level placed world entities (a runtime host spawn is already owner on the host). Header: `Replication/Components/CrowdyEntityComponent.h`. | Value | Meaning | | --- | --- | | `LocalClient` | This client owns and simulates the entity (`Role=Owner`). The default. | | `Host` | Owned by whichever client is currently host (`Role=HostOwned`, no per client owner). Use it for world or AI entities that must share one authority across every client. | ## ECrowdyHostOverride Whether the elected host may super-user-write a client owned entity's Crowdy State. Set it on `UCrowdyEntityComponent`. Only meaningful when `Ownership` is `LocalClient`. Header: `Replication/Components/CrowdyEntityComponent.h`. | Value | Meaning | | --- | --- | | `Allow` | The host may override this entity's Crowdy State as a super-user, and the owner adopts the correction. The default. | | `OwnerOnly` | Only the owning client may change this entity. Even a host-sourced correction is dropped. | :::note[Host authority on the Crowdy State plane is a precedence-by-convention, not a server-enforced boundary. Keep authoritative or cheat-sensitive state off this plane. See [host authority](/unreal-sdk/runtime/host-authority).] ::: ## ECrowdyStateHeartbeat Per entity control over the Crowdy State keyframe heartbeat, the periodic re-send of `CrowdyHeartbeat`-marked properties. Set it on `UCrowdyEntityComponent`. On-change replication is unaffected either way. Header: `Replication/Components/CrowdyEntityComponent.h`. | Value | Meaning | | --- | --- | | `Inherit` | Follow the map profile. Marked properties re-send every `StateKeyframeIntervalSeconds` when the map's keyframe interval is greater than 0. The default. | | `Off` | Never emit a keyframe heartbeat for this entity, regardless of its property marks or the map setting. A kill switch for always-active entities. | ## ECrowdyReplicationMode The mode chosen in the "Crowdy Replication" dropdown on a Blueprint variable's Details panel. Editor only. Header: `Customizations/CrowdyReplicationMode.h`. | Value | Meaning | | --- | --- | | `None` | Not networked by Crowdy. Selecting it scrubs every Crowdy key off the variable. | | `Replicated` | Crowdy State: the fast, client-authoritative view plane. Stamps the Crowdy State metadata so the variable flows through the same discovery and bake as a C++ `meta=(CrowdyState)` property. | :::note[For how to author a replicated variable, see [Crowdy State](/unreal-sdk/runtime/crowdy-state).] ::: ## Every BlueprintType enum The ten above are the ones this page explains in prose. The table below is generated from the same manifest that backs the rest of this reference section, grouped by module, and it lists every BlueprintType enum the manifest reports, including ones no page describes in words. Two wire-level enums no Blueprint-callable takes, `ECrowdyMessageType` and `ECrowdyErrorCode`, are BlueprintType in the header but missing from the manifest's enum group in this export, so they are absent below. The manifest carries each enum's name, its enumerator list, and where it lives, but not a meaning for each value, so for the "why" behind a value you still want the sections above or the concept page a row links to. Thirteen enums the manifest reports are excluded from the table above: two are deprecated (the persistence pull result and the connection monitor's reconnect state), one is a transport protocol enumeration nothing game-facing selects, and ten belong to another first-party module that rides in the same plugin build and are outside this SDK's own contract. None of them is part of the surface a game calls. ## Gotchas - The generated table's page column is blank where no other page mentions the enum by name; that is a gap in the docs, not a sign the enum does not exist. - A value's meaning lives in the ten prose sections above or on the concept page it links to, never in the generated table, which only has names. ## Related - [Recipients and routing](../runtime/recipients-and-routing.md) - [Entities, identity, and ownership](../concepts/entities-identity-ownership.md) - [Crowdy State](../runtime/crowdy-state.md) - [Host authority](../runtime/host-authority.md) - [SDK surface manifest](./sdk-surface.md): what the generated table above is built from. # RPC Parameter Types Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/rpc-types # RPC Parameter Types Which types a CrowdyEvent function's parameters can use directly, and the hard limits the wire format enforces on top of that. Come here when you are shaping a new event's signature, or a registration failed and you need to know which rule it hit. :::note[There is no payload struct to declare and nothing to serialize by hand. You pass the values, and the SDK handles the wire form.] ::: ## Allowed These types can be used directly as CrowdyEvent parameters. | Category | Types | | --- | --- | | Booleans and numbers | `bool`, `uint8`, `int32`, `int64`, `float`, `double` | | Text | `FName`, `FString`, `FText` | | Enums | any `UENUM` | | Structs | any `USTRUCT` built from the allowed types above (a struct that hides a container is rejected; see below) | | Object and class references | `UObject*`, `UClass*`, `TSubclassOf<...>`, `TSoftObjectPtr<...>`, `TSoftClassPtr<...>` | | Arrays | `TArray<>` of any of the above, including arrays of object references | | Sets and maps | `TSet<>` and `TMap<>` of non object element, key, and value types | A struct's own members are not checked one by one, only for a buried container, so a struct built entirely from allowed leaf types always passes. Unreal's reflection already forbids a container nested inside another container at the parameter declaration, so the container rules below only ever bite one level down: inside a struct, or as a struct-typed array, set, or map element. ## Not allowed These do not work as CrowdyEvent parameters, each for a specific reason. | Not allowed | Why | | --- | --- | | A return value | A CrowdyEvent is one way; it cannot send data back to the caller. | | A non const output reference | Same reason. A `const&` input is fine. | | `TSet` or `TMap` of object references | Object identity hashing is out of scope. Use a `TArray` of object references instead. | | A `USTRUCT` that hides a container (`TArray`, `TSet`, or `TMap`) anywhere inside it, and any `TArray`, `TSet`, or `TMap` whose element, key, or value is such a struct | A buried container's element count has no bound on the wire, so it is rejected at registration to keep a malformed packet from forcing a huge allocation. Pass the container as a top-level parameter instead. This includes `FGameplayTagContainer`, which holds an inner array; pass the tags as a `TArray` parameter. | | Delegates and interfaces (`TScriptInterface<>`) | No stable wire form. | :::caution[A CrowdyEvent is one way. It cannot return a value or write back through a non const output reference. A `const&` input is fine.] ::: ## How references travel References are not sent as raw pointers. Each kind resolves to a portable identity on the wire, one of three tags. | Reference | On the wire | | --- | --- | | A tracked entity actor | Sent as its entity `NetID`, so it resolves to the matching instance on each client. | | An asset or a class | Sent as its object path. | | A runtime object that is neither an entity nor an asset | Sent as null, with a warning, because it has no portable identity. | :::tip[A received object or class reference resolves by finding an already loaded asset. To allow loading missing assets from disk by path, set `crowdy.rpc.allowObjectLoad 1`. It is off by default so an untrusted peer cannot trigger arbitrary loads. See [Console variables](./console-cvars.md).] ::: ## Reading a value on receipt A handler reads each parameter by name off the incoming call, never by its position on the wire, through a typed getter: a bool, a name, or a string comes back as is. `GetInt32` never narrows an incoming 64 bit value for you, because the value came from someone else's process; `GetInt64` accepts a 32 bit value widened, which loses nothing. `GetFloat` accepts a double narrowed, because a Blueprint float pin is a double under the hood while a C++ `float` property is not, so this one direction has to happen somewhere. Every getter answers a safe default rather than reinterpreting the bytes when the stored type does not match what you asked for. ## Limits The manifest that drives the other reference tables on this site does not carry these numbers; they come from the wire format itself. - A `Multicast` event's payload is capped at 1024 bytes on its reliable channel. An encoded call that would exceed this is dropped loudly at send, never truncated. `SpatialMulticast` has no channel cap of its own, but it still travels in a UDP datagram of at most 1232 bytes including the envelope, so it is a little more room, not an unbounded route. - An array, set, or map parameter is capped at 65,536 elements. A forged, out of range count fails before any allocation happens, and the whole call is dropped cleanly rather than driving an unbounded allocation from an untrusted packet. - The buried-container check above stops recursing at 8 levels of struct nesting. A struct cannot contain itself by value, so this is a defensive bound, not a limit you can reach by ordinary authoring. ## Gotchas - A rejected registration for a buried container is a compile-time (editor load time) failure, not a runtime one; move the container to the top level of the parameter list. - `TArray` of an object reference is fine; `TSet` and `TMap` of one are not. - The 1024 byte cap belongs to `Multicast` alone. Do not size a `SpatialMulticast` payload against it. - A getter mismatch fails quietly (a default value, not a crash), so a handler that reads the wrong type from a payload looks like it worked until you check the value. ## Related - [RPC metadata keys](./rpc-meta-keys.md): the keys that route the event this page's parameters ride on. - [RPC events in C++](../runtime/rpc-events-cpp.md): worked examples of declaring and calling an event. - [Console variables](./console-cvars.md): `crowdy.rpc.allowObjectLoad` and the rest. - [What a state property may be](../runtime/crowdy-state.md#what-a-state-property-may-be): the view plane's own, stricter type rules. Crowdy State rejects containers and object references outright; RPC parameters accept both. # Log Categories Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/log-categories # Log Categories The SDK logs under 12 `LogCrowdy` categories, one or more per module. Two knobs control what you see: the category's own log verbosity, which is native Unreal and free, and the paired `crowdy..trace` CVar, which is a one-line switch for that area's extra informational stream. The two are independent: a trace CVar gates SDK trace lines that print at `Log` verbosity, and `Log Verbose` reveals the engine-style lower-level lines the module also emits. Raising verbosity never turns a trace gate on; turn on both when you need the full picture for one area. ## When you land here You want to raise verbosity on a category by name, or you are trying to work out why a category's lines never show up in a packaged build. ## Reading the table A row badged **Editor only** belongs to a module that is not in a cooked build. `LogCrowdyEditor` and `LogCrowdyStudio` are editor-module categories, present only in the editor process. `LogCrowdyNodes` belongs to an `UncookedOnly` module: present in the editor and in an uncooked `-game` launch, absent from every cooked build, Development or Shipping alike. ```text Log LogCrowdyRPC Verbose ``` For a problem that happens before the console is available, pin the verbosity in `DefaultEngine.ini`: ```ini [Core.Log] LogCrowdyReplication=Verbose ``` :::note[Warnings and errors are never gated behind a trace CVar.] They always print under their module's category regardless of the trace setting. Turning a trace CVar off only silences the extra informational lines. ::: :::warning[Never log a bearer token or other secret material.] `crowdy.studio.trace`'s own logging never includes the bearer token; follow the same rule for any logging you add around SDK calls. ::: `CKSharedTypes` has no active logging of its own; do not go looking for a `LogCrowdyShared` category, it does not exist. ## Gotchas - `LogCrowdyNodes`, `LogCrowdyEditor`, and `LogCrowdyStudio` are the three categories badged **Editor only** above. None is in a cooked build; `LogCrowdyNodes` is additionally present in an uncooked `-game` launch, where the other two are not. - `LogCrowdyCpp` has no paired trace CVar. Its only diagnostic is running `crowdy.cpp.selftest`. - A category with two or more paired trace CVars (`LogCrowdyNet`, `LogCrowdyReplication`, `LogCrowdyRPC`, `LogCrowdyServices`) mixes those areas' lines together; narrow with the trace CVar first, then read the category. The two `.scopes` CVars are Insights CPU scopes and write no log line, so they pair with no category. - Set a category back to its default verbosity when you are done; leaving several at `Verbose` adds volume that makes the next real problem harder to spot. ## Related - [Console variables](./console-cvars.md): the `crowdy..trace` CVars this page's pairing points at. - [Testing locally](../guides/testing-locally.md): the workflow that replaces the old debugging and logging guide. - [Project settings](./project-settings.md): the settings classes alongside this CVar and log surface. # SDK Surface Manifest Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/reference/sdk-surface # SDK Surface Manifest `sdk-surface.json` is a committed, machine-readable inventory of every reflected symbol the plugin ships: every Blueprint-callable function, delegate, subsystem, struct, enum, setting, console variable, and metadata key, plus a few groups of plain C++ types with no Blueprint exposure at all. Every table elsewhere in this reference section is generated from it. Come here to script against the surface directly, an audit of your own project's usage, a code generation tool, or an agent checking whether a symbol exists before it goes looking through headers, rather than to read a rendered table. ## Where to get it [`/helpers/unreal-sdk/sdk-surface.json`](pathname:///helpers/unreal-sdk/sdk-surface.json). It ships alongside this site's other machine-readable files and is regenerated as the SDK changes, so a fresh copy always describes a specific SDK release rather than "whatever is newest." Three fields sit above the groups: `generated`, the export timestamp; `sdk_version`, the SDK release this snapshot describes; and `source_root`, the header directory it was built from. Check `sdk_version` first if something here looks wrong against a copy you pulled yourself. :::note[If the sdk_version in your own copy does not match this page's, the tables on this site and this page's counts are both describing a different release than yours.] Regenerate your understanding from the file you have rather than patching around a mismatch; the groups below can gain or lose rows between releases. ::: ## The groups | Group | What it holds | | --- | --- | | `functions` | Blueprint-callable and pure functions, with their owning class, module, category, and display name. | | `delegates` | Blueprint-assignable dynamic multicast delegates, with their owning class and signature type. | | `async_actions` | Latent Blueprint nodes: the class, its factory function names (the node's display name is on the matching `functions` entry), and the completion pins it exposes. | | `subsystems` | Game Instance and World subsystem classes. | | `structs` | BlueprintType `USTRUCT`s. | | `enums` | BlueprintType `UENUM`s. | | `settings` | Properties on the SDK's developer-settings classes, each marked as one you set or one Config Sync writes for you. | | `cvars` | Console variables, split into trace gates, behavior switches, and one-shot commands. | | `meta_keys` | The `UFUNCTION`/`UPROPERTY` metadata marker keys: `CrowdyEvent`, `CrowdyState`, and the rest. | | `log_categories` | Log categories with their module and header. The trace-variable pairing on the log categories page is derived from `cvars` by the site's table generator, not stored here. | | `cpp_types`, `cpp_functions`, `cpp_delegates`, `cpp_macros` | Plain C++ types, methods, delegates, and macros with no Blueprint exposure at all: internal to the plugin, not something a game calls directly. Most of what these four groups report belongs to the vendored networking bridge, the plugin's own wire and JSON helpers, or editor-only tooling. | The first ten groups are Blueprint's own reflected surface, the one a game actually reaches. The last four exist in the manifest because the export walks the plugin's headers exhaustively, not because a game is meant to build against them. ## How a symbol counts as documented This documentation set's own completeness check reads every page under the Unreal SDK section and asks, for each symbol in the manifest, whether that symbol's name appears anywhere as a whole word. A row in one of this reference section's generated tables satisfies the check exactly like a paragraph of prose does; the check has no notion of "the one canonical page" for a symbol, only whether it is mentioned somewhere at all. That also means a name typed once, anywhere, technically counts, so treat the check as a floor, not a guarantee that every symbol got a real explanation. Some symbols the manifest reports are deliberately left off every table and every page: internal plumbing behind a documented subsystem, a class the SDK itself has deprecated, editor-only tooling, a symbol that belongs to a different first-party module riding in the same plugin build rather than to this SDK's own contract. Those are tracked in an exceptions list (`docs-unreal-sdk/reference/surface-allowlist.json` in the documentation repository, not published beside the manifest) alongside the reason, so an absence from this site is either "here, just not in prose" or "excluded, and here is why," never a silent gap. ## Gotchas - The manifest is exhaustive over the plugin's own headers, with one known exporter gap in this release: two BlueprintType enums with braces inside their bodies (`ECrowdyMessageType`, `ECrowdyErrorCode`) appear only under `cpp_types`, not `enums`. Otherwise a symbol you cannot find in it does not exist in this SDK version; that is an answer, not a reason to grep the source. - The four raw C++ groups (`cpp_types`, `cpp_functions`, `cpp_delegates`, `cpp_macros`) are not a promise of a stable public API. They are what the export sees, not what the SDK commits to keeping. - A symbol's presence in the manifest says nothing about whether it is a good idea to use. The exceptions list marks deprecated symbols for exactly this reason. ## Related - [Subsystems](./subsystems.md), [Async actions](./async-actions.md), [Delegates](./delegates.md), [Console variables](./console-cvars.md), [Project settings](./project-settings.md): the generated tables built from this manifest. - [RPC metadata keys](./rpc-meta-keys.md), [Crowdy State metadata keys](./state-meta-keys.md): the `meta_keys` group, split by where each key is legal. - [Log categories](./log-categories.md): the `log_categories` group. - [Enums](./enums.md): the `enums` group.