# 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.