# What a Server Object is Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/overview # What a Server Object is :::note Server Objects run on [ck-exec](/exec/intro). You can use them from Blueprint or from C++; see [From Blueprint](./from-blueprint/get-a-server-object.md) and [From C++](./from-cpp/get-a-server-object.md). ::: A Server Object is a piece of game state that lives on the server and that every player sees the same way. The server keeps its values, saves them, and is the only thing that changes them. Your game reads the values it is allowed to see, is told each time they change, and asks for a change by calling one of its **Server Functions**. The server decides, and every player watching sees the result. That makes a Server Object the place for state a player could gain by lying about: a boss's health, a shared stockpile, a match's score, a player's inventory. It is the SDK's door into the [truth plane](../concepts/two-planes.md). Fast view state (movement, animation, a light flickering) stays on the view plane, in [Crowdy State](../runtime/crowdy-state.md). ## In the example world The village square has a beacon every villager feeds with oil. How much oil it holds is the kind of value a client must not decide for itself, so it is a Server Object: one per village. When a player walks into the beacon, the game calls the beacon's `FeedBeacon` Server Function; the server adds the oil (or refuses), and every player in the village sees the beacon's light change with the new amount. The pages in this section build that beacon, `AVillageBeacon`, one piece at a time. ## How the parts fit - **A definition asset** (`UCrowdyServerObjectDefinition`) describes one type of Server Object: the variables the server keeps, which of them players may see, and the functions they may call. You make one asset per type, in an editor laid out like the Blueprint editor. See [Create a Server Object type](./create-a-type.md). - **Access, members and timers** are settings on the asset that the server enforces before your code runs: who may read and call, how often, members and a leader, timers, and calling other types. See [Access, members and timers](./access-members-and-timers.md), and [Relate Server Objects to each other](./relate-server-objects.md) for which to use when one object belongs to a player, lists others or is shared by many. - **Messages** are what the server code sends to everyone listening when something happens, such as a boss falling. Players hear them in a game, and other Server Object types hear them in their own server code. A message is an event: nothing keeps it. See [Server messages](./create-a-type.md#server-messages). - **A Server Function Library** is a type with functions and nothing to remember, like a Blueprint Function Library: logic that many callers share, with no instance of its own, answered by several instances at once. You set the asset's **Kind** to make one. See [Make a Server Function Library](./server-function-libraries.md). - **A record per entity** is a setting on the asset, **Holds**, for a crowd: the same variables kept once for each entity, with some visible to everyone, some to the entity's owner and some to its team. See [Keep a record per entity](./keep-a-record-per-entity.md). - **The server code** is a Rust crate the editor generates from the definition asset; you fill in what each Server Function does. See [Write its server logic](./write-server-logic.md), then [Deploy it with Server Compute](./deploy-with-server-compute.md). - **An Instance Id** picks one Server Object of that type. The beacon uses the village's name, so each village has its own. - **From Blueprint**, the **Crowdy Server Object** component and the typed nodes get the object, read and follow its variables, hear its messages and call its functions. See [Get a Server Object](./from-blueprint/get-a-server-object.md), [Read and follow its variables](./from-blueprint/read-and-follow-variables.md), [Hear its messages](./from-blueprint/hear-server-messages.md), [Call its functions](./from-blueprint/call-functions.md), [Call a Server Function Library](./from-blueprint/call-a-library.md) and [Pins for every type](./from-blueprint/pins-for-every-type.md). - **From C++**, `UCrowdyServerObjectSubsystem`, a game instance subsystem, hands out Server Objects. Every actor or object that acquires the same definition and Instance Id shares one `UCrowdyServerObject`, and the subsystem gives it back a short while after the last of them is gone. The variables players can see arrive in a copy of the state struct and raise one change event per server change, and Server Functions are called with a params struct and answer with a reply struct or an outcome that says what went wrong. See [Get a Server Object](./from-cpp/get-a-server-object.md), [Read and follow its variables](./from-cpp/read-and-follow-variables.md), [Hear its messages](./from-cpp/hear-server-messages.md), [Call its functions](./from-cpp/call-functions.md) and [Call a Server Function Library](./from-cpp/call-a-library.md). The platform has its own words for all of this (hubs, keys, methods, topics). You do not need them to use the SDK, but the platform's pages and logs use them; [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) maps one to the other. ## What you need 1. **Your app on the Dev backend.** Server Objects connect with the signed-in player's app token, like the rest of the runtime. See [Player sign-in](../runtime/player-sign-in.md). 2. **The module dependency, for C++.** Blueprint needs none. Add `CrowdyExec` to your game module's `Build.cs`, next to the modules from [Installation](../installation.md#step-4-add-the-module-dependencies): ```csharp PrivateDependencyModuleNames.Add("CrowdyExec"); ``` 3. **A definition asset per type**, described on [Create a Server Object type](./create-a-type.md). 4. **The type's server code, deployed to your app on ck-exec.** The editor generates it from the definition asset, so the server and your game agree on every name and field, and you write only what each Server Function does. The Server Compute page in Crowdy Studio sends it to your app. See [Deploy it with Server Compute](./deploy-with-server-compute.md). ## What is next Describe the beacon's state and functions in [Create a Server Object type](./create-a-type.md), set who may do what in [Access, members and timers](./access-members-and-timers.md), see how types relate in [Relate Server Objects to each other](./relate-server-objects.md), write what its functions do in [Write its server logic](./write-server-logic.md), and [deploy it](./deploy-with-server-compute.md). Then use it [from Blueprint](./from-blueprint/get-a-server-object.md) or [from C++](./from-cpp/get-a-server-object.md). When something does not behave, [Troubleshooting](./troubleshooting.md) has the checks, and [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) maps the SDK's words to the platform's. If your project used the SDK's Game Models, which were removed in v2.19.0, [Move from Game Models](./move-from-game-models.md) says what to do before you upgrade and what to use instead. A crowd of entities, each with values of its own, is [Keep a record per entity](./keep-a-record-per-entity.md). The worked examples, a tip jar, guild halls and an arena, a boss every player hits, a warband of raiders, and a loot roller for the boss and a chest, are together under [Examples](./examples/tip-jar.md). # Create a Server Object type Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/create-a-type # Create a Server Object type :::note Server Objects run on [ck-exec](/exec/intro). ::: Each type of [Server Object](./overview.md) is described by one data asset of the class `UCrowdyServerObjectDefinition`. The asset names the variables the server keeps, which of them players may see, the functions players may call, and the messages the server sends to everyone listening. The SDK reads it at runtime to encode and decode every value, and the type's server code is generated from it, so both sides agree on every name. A type that needs functions only, with nothing to remember between calls, sets its **Kind** to **Server Function Library**; see [Make a Server Function Library](./server-function-libraries.md). The rest of this page is about the default, **Server Object**. ## Make the asset In the Content Browser, choose **Add**, then **Crowdy**, then **Server Object**. The new asset is named `CSO_NewServerObject`; rename it after the type, for example `CSO_VillageBeacon`, and double-click it. Server Object assets take the prefix `CSO_` (Crowdy Server Object), the way Blueprints take `BP_`; an older asset named `DA_` works the same. **Add**, **Miscellaneous**, **Data Asset**, then **Crowdy Server Object Definition** makes the same asset. It opens in an editor laid out like the Blueprint editor (see [The asset editor](#the-asset-editor)), with the type's variables, functions and messages in the **Server Object** panel and whatever you select there in **Details**. The village beacon keeps its oil, whether it is lit, and who fed it last; feeding it sends an amount of oil and gets back how much the beacon holds afterwards. 1. **Add the variables.** In the **Variables** section of the Server Object panel, clear **Use Struct** in its header if it is ticked, then click **+**. A new variable is an Integer named `NewVar`, and it is selected, so Details shows it. Set its **Variable Name** to `Oil` and its starting value, under **Default Value**, to 50. Click **+** twice more for `bLit`, a Boolean that starts ticked, and `LastFedBy`, an Integer64 that starts at 0; change each one's type with **Variable Type**. 2. **Choose what players see.** Each variable has an eye at the end of its row. Leave it open on `Oil` and `bLit`: **Visible to Players**, so players can read and watch them. Click it on `LastFedBy` to close it: **Server Only**, so it never leaves the server. 3. **Add the function.** Click **+** in the **Functions** section. The new function, `NewFunction`, is selected; set its **Name** to `FeedBeacon` and leave **Callable By** on **Players**. Click **+** in the **Inputs** header, rename the new `NewParam` to `Oil`, leave it an Integer and give it the default value 10. Click **+** in the **Outputs** header and name that one `Oil` too. 4. **Name the type.** Select **Type Settings**, the first row of the Server Object panel, and Details shows the type's own settings: set **Type Name** to `village_beacon`, the **Description**, and **Readable By**. The panel now reads: | Server Object panel | The beacon | |---|---| | Variables | `Oil` Integer (open eye), `bLit` Boolean (open eye), `LastFedBy` Integer64 (closed eye) | | Functions | `FeedBeacon` with `(Oil) returns Oil` beside it | and, with **Type Settings** selected, Details: | Setting | The beacon's value | |---|---| | Type Name | `village_beacon` | | Description | Keeps the village beacon's oil and whether it is lit. | | Readable By | Every Player | ![With Type Settings selected, Details shows Type Name, Kind, Description, Save Every and Stop When Idle For, then the Access, Platform Data (folded), Members, Areas, Timers & Events, Can Call and Listen To categories](/img/unreal-sdk/server-object-type.png) Then pick the asset in the beacon's **Beacon Definition** slot in the level's Details panel; see [Get a Server Object](./from-cpp/get-a-server-object.md). ## Variables, inputs and outputs, or a struct The variables, and each function's inputs and outputs, can each be added right in the asset, as above, or taken from a `USTRUCT` of your own. Tick **Use Struct** in the section's header to take a struct: for the Variables, pick it as **State Struct** in Details with **Type Settings** selected; for a function's Inputs or Outputs, the list gives way to an **Input Struct** or **Output Struct** picker, where None sends or replies nothing. The beacon's structs: ```cpp // VillageBeacon.h // Everything the server keeps for one village's beacon; only the watched fields ever reach a player. USTRUCT(BlueprintType) struct FVillageBeaconState { GENERATED_BODY() UPROPERTY() int32 Oil = 50; UPROPERTY() bool bLit = true; UPROPERTY() int64 LastFedBy = 0; }; USTRUCT(BlueprintType) struct FFeedBeaconParams { GENERATED_BODY() UPROPERTY() int32 Oil = 10; }; USTRUCT(BlueprintType) struct FFeedBeaconReply { GENERATED_BODY() UPROPERTY() int32 Oil = 0; }; ``` With **Use Struct** ticked, the Server Object panel lists the struct's fields as the variables, each with its type and eye, but they are renamed and retyped in the struct, not in the asset. Both travel exactly alike, so the server code and the saved data do not care which you chose. The generated server code gives the ones added in the asset the names the structs would have, `VillageBeaconState`, `FeedBeaconParams` and `FeedBeaconReply`, so the beacon's `logic.rs` is the same either way. The C++ on the following pages uses these structs. With variables, inputs and outputs added in the asset there is no struct, so C++ reads and sends them by name instead: see [Read and follow its variables](./from-cpp/read-and-follow-variables.md#read-the-values) and [Call its functions](./from-cpp/call-functions.md#call-one-with-inputs-added-in-the-asset). Keep a struct when the same values are shared by several types or functions, when you may rename a field and want to keep its saved value with [Field Names](#every-setting), or when your C++ uses the struct elsewhere. ## The asset editor Double-click a definition in the Content Browser and it opens in its own editor, laid out like the Blueprint editor and using its words, not as a plain property list. The editor has a toolbar and three tabs. ![The CSO_VillageBeacon editor: Server Code on the left, the Server Object panel (Type Settings, Variables, Functions and Server Messages) and the Details of FeedBeacon on the right, Generate and Deploy in the toolbar](/img/unreal-sdk/server-object-editor.png) ### The toolbar After the usual **Save** and **Browse** buttons the toolbar holds: - **Type Settings**, a toggle that selects the **Type Settings** row of the Server Object panel from anywhere, so Details shows the type's own settings. - **Generate** writes the type's server code, the same as the Content Browser command. Its tooltip reads "Writes this type's server code under `Server/`; your logic file is never replaced". - **Deploy** builds and deploys every Server Object type in the project together, since a deploy replaces the app's whole version, and asks first. See [Server Compute](./deploy-with-server-compute.md#deploy). - A small arrow beside Deploy opens a menu with two entries. **Open Server Compute** opens [Crowdy Studio](../studio/overview.md) on the Server Compute page itself (it opens on Sign In first when nobody is signed in, and shows the page after). **Show build output** opens a **Build Output** window with what the last build of this session printed; it is disabled until a build has printed something. - A **status readout** at the right: a coloured dot and one line saying where the type stands. Hover it for the reason. Generate is disabled until the Type Name is valid. Deploy is disabled until the Type Name is valid, while another definition has the same Type Name, while another operation runs, and while nothing in the project has changed since the live version. While a deploy runs, its progress line takes the place of the status readout. The readout says: | Readout | Meaning | |---|---| | **Ready** | The server code on disk matches this definition. It is compared with the live version once [Server Compute](./deploy-with-server-compute.md) has read it, which needs you signed in to Crowdy Studio. | | **No changes**, then "live in version 7" (green dot) | The live version runs this code with these settings. | | **Changed since version 7 (server code, save interval)** (yellow dot) | Version 7, the live one, runs something else. The brackets say what: server code, save interval or idle timeout. | | **Not deployed yet** (blue dot) | The live version does not run this type. Deploy adds it. | | **Live code unknown**, then "app version 7" (yellow dot) | The live version runs code this project has no record of, for example deployed from another copy of the project. | | **Needs generating**, **Out of date** (yellow dot) | The type's server code has to be generated, or generated again; see [Write its server logic](./write-server-logic.md#the-server-code-tab). | | **Problem: ...** (red dot) | The definition fails its checks, or another definition has the same Type Name (see [Two definitions with one Type Name](#two-definitions-with-one-type-name)). The reason follows the colon. | ### The three tabs - **Server Code**, the large tab on the left: the path of the Server Functions file, the Code Source choice (**Generated** or **My Own File**), the deployed revisions, and the code itself with line numbers, Rust colouring, **Save**, **Revert** and an external-editor button. It is described on [Write its server logic](./write-server-logic.md#the-server-code-tab). - **Server Object**, top right, where My Blueprint is in the Blueprint editor: the type's variables, functions and messages. - **Details**, bottom right: the settings of whatever is selected in the Server Object panel. The tabs dock and rearrange like any editor tab, for example to put the code above the rest; the editor remembers the layout. ### The Server Object panel The panel starts with a pinned row, **Type Settings**, with a gear icon. It is selected when the asset opens, and selecting it shows the type's own settings in Details. Below it are three sections. - **Variables**, with a **Use Struct** check-box and a **+** button in its header. Each row shows the variable's type as a coloured pill, the way a Blueprint variable does, its name, its type name (Integer, Boolean, String) and an eye. An open eye is **Visible to Players**: players can read and watch it. A closed eye is **Server Only**: it never leaves the server. Click the eye to switch it. With Use Struct ticked the + button is hidden and the rows are the struct's fields. - **Functions**, with a **+** button in its header. Each row shows the function's name and its pins the way its node reads, inputs in brackets, then "returns" and the outputs, so `FeedBeacon` reads `(Oil) returns Oil`; a struct shows as its name. A **Server Only** tag follows when only server code may call it (see [Callable By](#callable-by)). Double-click a function to jump the Server Code tab to its `fn` in `logic.rs`. When **Members From** is **This Object**, a **Built-in** section lists **Join**, **Leave**, **Add Member**, **Remove Member**, **Make Leader** and **Set Open for Joining**, greyed out: they cannot be edited or deleted. Each shows its inputs the way a function row does, such as **Add Member (Player)**. See [Members](./access-members-and-timers.md#members). - **Server Messages**, with a **+** button in its header, below the functions. Each row shows the message's name and its payload the way its Received node reads, so `BossDefeated` reads `(Winner, Hits)` beside it; a message that players cannot listen to carries "(server only)". See [Server messages](#server-messages). Right-click a section's header for **Add Variable** (while Use Struct is clear), **Add Function** or **Add Message**, a function or a message for **Duplicate** and **Delete**, and a variable added in the asset for **Delete**. The Delete key deletes the one selected. New variables are Integers named `NewVar`, then `NewVar_1` and on; new functions are named `NewFunction`, new messages `NewMessage`, and new inputs and outputs `NewParam`. A warning icon after a name means the variable, function or message fails a [check made when you save](#checks-when-you-save); hover it to read the problem. ### The Details tab Details follows the selection in the Server Object panel. **With Type Settings selected** it shows the type's own settings: **Type Name**, **Kind** (**Server Object** or **Server Function Library**; a library shows only **Type Name**, **Kind**, **Description**, **Platform Data**, **Can Call**, **Scaling** and **Server Limits**, and Details rebuilds when you change it), **Description**, **State Struct** (only when the Variables use a struct), **Save Every** and **Stop When Idle For**, both always in seconds (such as 300 s), and a collapsed **Advanced** group with **Server Names** (**Field Names** and **Enum Value Names**) and **Baked**. Six categories hold the settings for who may do what: **Access** (**Readable By**: **Every Player**, **Owner Only** or **Members**; **Only One Instance**), **Platform Data** (**Reads Players**, **Changes Players' App State**, **Reads World Data**, **Changes the World**, **Reads Grids**, **Manages Grids**, **Grants Grid Permissions**, **Reads Players' Access**, **Manages Players' Access**), **Members** (**Members From**, **Max Members**, **Show Members to Players**, **Remove Members Who Leave**), **Timers & Events**, **Can Call** and **Listen To**. They are described on [Access, members and timers](./access-members-and-timers.md). **Holds** sits with the type's other settings, and two more categories belong to [a record per entity](./keep-a-record-per-entity.md): **Records** (**Max Records**, **Send Changes Every**, **Team From**), shown only when **Holds** is **A Record per Entity**, and **Areas** (**Area Size**, **Areas Followed Around a Player**). A collapsed **Server Limits** category holds what the server allows each Server Object of the type: **Memory per Instance**, **Time per Call**, **Fuel per Call (Millions)** and **Calls Waiting**. When the type has [messages](#server-messages) that players can listen to and some players may read less than every player can, a **Server Messages** category with a warning follows. See [Every setting](#every-setting). **A variable** shows **Variable Name**, where you rename it, **Variable Type**, and **Visible to Players**, the same switch as the eye. In a type whose **Holds** is **A Record per Entity**, **Visible to Players** is a drop-down of who receives the variable: **Server Only**, **Everyone**, **Owner Only** or **Team Only**; the eye then switches between **Server Only** and **Everyone**. See [Who receives a value](./keep-a-record-per-entity.md#who-receives-a-value). Variable Type is Unreal's Blueprint type picker, offering only the types a Server Object can carry (see [Field types](#field-types)). Below them a **Default Value** section holds the variable's starting value, where a new Server Object starts. A variable from a struct shows its name and type read only; change them in the struct. ![Oil selected: Variable Name, Variable Type, Visible to Players and its Default Value](/img/unreal-sdk/server-object-variable.png) **A function** shows: - a **Function** section whose header reads the node line, for example `FeedBeacon (Oil) returns Oil`, with **Name**, the name your C++ passes to `Call`, **Callable By** (**Players**, **Members**, **Leader**, **Server Only** or **Developers Only**) and **Cooldown**, in seconds, and for a Server Only or Developers Only function a **Call as Developer...** button (see [Call a function as the developer](./deploy-with-server-compute.md#call-a-function-as-the-developer)); - **Inputs**, what the caller sends, and **Outputs**, what the server answers with. Each has a **Use Struct** check-box and a **+** button in its header, and lists its pins the Blueprint way: type pill, name and default value on each row, with a drop-down to change the type, and rename, delete and drag to reorder. An empty one says "No inputs: the caller sends nothing. Add one with the + button." or "No outputs: the function replies nothing. Add one with the + button." Ticking Use Struct swaps the list for an **Input Struct** or **Output Struct** picker. A number input also gets a **Value Range** under Inputs, two boxes labelled **Min** and **Max** that the server holds callers to; see [Value Range](./access-members-and-timers.md#value-range); - a collapsed **Advanced** section with the **Server Name**, the [name the function has on the server](#every-setting). **A message** shows a **Message** section whose header reads the node line, for example `BossDefeated (Winner, Hits)`, with the message's **Name** and **Players Can Listen**; a **Payload** section with a **Use Struct** check-box and a **+** button, listing the values the message carries the way a function's Inputs are listed (an empty one says "No payload: listeners are told it happened and receive nothing. Add a value with the + button."); and a collapsed **Advanced** section with the **Server Name**, the [topic the message has on the server](#every-setting). **Code Source** and the logic file are not in Details: they are on the Server Code tab, beside the code they choose. ### Type Name The Type Name box checks what you type as you type it and says what is wrong: "Needed: the type's name on the server.", "At most 48 characters.", "Start with a lowercase letter." or "Use only lowercase letters, digits and underscores." The problem appears under the box with a warning icon. The value is stored when you press Enter or leave the box. When there is a problem and the asset's name gives a better idea, a **Use village_beacon** link beside it sets the Type Name from the asset name ("Sets the Type Name from the asset's name"): `CSO_VillageBeacon` becomes `village_beacon`, dropping the `CSO_` (or the `DA_` of an older asset) and putting an underscore where each word starts. ### Callable By **Callable By**, in a function's Details, decides whether a player may call it. | Choice | What happens | |---|---| | **Players** | Players can call it, and so can other server code and developer tools. | | **Members** | Members of the Server Object can call it. Needs **Members From**; see [Members](./access-members-and-timers.md#members). | | **Leader** | Only its leader can. Needs **Members From**. | | **Server Only** | Only other server code and developer tools can. **A player's call is refused**, and the caller's `OnDone` sees the `Denied` outcome, with a `Reason` such as "players may not call this function"; see [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code). The function's row in the Server Object panel carries a **Server Only** tag. | | **Developers Only** | Only developer tools can, such as [Call as Developer](./deploy-with-server-compute.md#call-a-function-as-the-developer): **players and other server code are refused**, with the `Reason` "only developer tools may call this function". The row carries a **Developers Only** tag. | Other server code and developer tools may call every function whatever it says, except that a Developers Only function refuses other server code too. Server Only is for functions that should never be reachable from a game client, such as a payout your own server code triggers or an administrative reset. Developers Only is for the ones only a person should run, such as a repair or a data fix you call by hand. The check is made on the server, so a modified game client cannot get around it. ### Two definitions with one Type Name Two definitions cannot use the same Type Name, since both would write the same `Server//` folder and become the same type on the server. When another definition has the Type Name, the Type Name row says "Another definition already uses this Type Name; give this one its own.", the status readout reads **Problem** followed by the same text, **Generate** and **Deploy** are disabled, and the list of deployed revisions is hidden, until one of them is renamed. [Server Compute](./deploy-with-server-compute.md#deploy) refuses to deploy too, naming both assets: "Two Server Object types are named village_beacon (...) and (...); each needs its own Type Name". ## Server messages A Server Function answers the player who called it, and the variables tell everyone what a value is now. A **message** is for the third thing: telling everyone listening that something happened. Your server code sends it, and each player listening to that Server Object, and each Server Object type that listens to this one, hears it once. A message is not a value and nothing keeps it: a player who was not listening, or whose connection was closed at that moment, never hears it, so a fact players must be able to look up later belongs in a variable. The Hollow Giant, the [shared boss](./examples/shared-boss.md), announces its fall with a `BossDefeated` message that names the player who landed the last hit and the hits it took. On `CSO_Boss`, in the **Server Messages** section of the Server Object panel: 1. Click **+**. The new message, `NewMessage`, is selected. Set its **Message** to `BossDefeated`. 2. Leave **Players Can Listen** ticked. 3. Under **Payload**, click **+** twice. Name the first value `Winner`, an Integer64, and the second `Hits`, an Integer. To carry a struct of your own instead, tick **Use Struct** and pick it as the **Payload Struct**. The row reads `BossDefeated (Winner, Hits)`. A message with no values is allowed: listeners are told it happened and receive nothing, such as a `BossRespawned` message. | Setting | The boss's value | |---|---| | Message | `BossDefeated` | | Players Can Listen | ticked | | Payload | `Winner` Integer64, `Hits` Integer | The message is sent from the type's server code; see [Send a message](./write-server-logic.md#send-a-message). It is heard from Blueprint with **On BossDefeated Received** and **Watch Message**, and from C++ with `WatchMessage`; see [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md). The message is the struct `FCrowdyServerMessage` in the definition's `Messages`. ### Who hears a message - **Players Can Listen**, ticked, the default: any player who can connect to the type may listen to the message, whatever the type's **Readable By** says. - **Players Can Listen** cleared: only the server code of a type that [listens to this one](./access-members-and-timers.md#listen-to-other-server-objects) hears it. Watching it from Blueprint or C++ logs a warning and does nothing, and a **Received** node for it does not compile. A message players cannot listen to does not open the type to players: a type whose only messages are these is still [closed to players](./access-members-and-timers.md#a-type-players-cannot-reach). **Readable By** and the Owner Only and Team Only variables decide who may read the variables. They do not decide who hears a message. So when the type has a message players can listen to and **Readable By** is not **Every Player**, or any variable is Owner Only or Team Only, the Server Messages section and the type's settings show a warning: "Every player who can connect to this type hears its messages, whoever may read its variables. Keep private data out of messages." Put what only some players may know in a variable, and send a message only for what anyone may hear. ### What a message carries The payload takes the same types as a variable (see [Field types](#field-types)) and is held to the same limits as a reply: nothing a client could not read, at most 1 MiB and 65,536 list, set and map entries. A message that breaks them is not sent: the send returns an error, which fails the function that made it when its code passes the error on; see [What the generated code does for you](./write-server-logic.md#what-the-generated-code-does-for-you). A message's **Name** is what Blueprint and C++ use and must be unique in the type. Its **Server Name** in the **Advanced** section is its topic on the server: empty uses the name in snake case, so `BossDefeated` becomes `boss_defeated`. A topic is lowercase letters, digits and underscores, starts with a letter, is at most 64 characters, is not `state` (the topic the variables use), and is not used by another message. After you add, rename or change a message, [generate the server code](./write-server-logic.md#after-you-change-the-definition) and deploy the type again, since the server code carries it. ## Every setting The platform term is in brackets, for when you read the platform's own [ck-exec pages](/exec/intro) or its logs. Where the editor labels a setting differently from its C++ name, the label is given too. **Server Object** - **Type Name** [node type]: the type's name on the server. Lowercase letters, digits and underscores, starting with a letter, at most 48 characters, and unique in your project. - **Kind** (`ECrowdyServerObjectKind`): **Server Object**, the default, keeps variables and has one instance for each Instance Id. **Server Function Library** (`FunctionLibrary`) has functions only, saves nothing between calls and is answered by several instances at once. The settings a library cannot have stay on the asset, hidden and ignored. See [Make a Server Function Library](./server-function-libraries.md). - **Description**: what this type is for, in a sentence. It is shown beside the type on the [Server Compute](./deploy-with-server-compute.md#overview) page. It is for you and your team: it is not sent to the server, and it is not part of a packaged game. - **Visibility** (`ECrowdyServerObjectVisibility`, labelled **Readable By**): who may see the values. - **Public** (**Every Player**): every player can read the variables visible to players and is told of each change. - **Owner Only**: each Server Object belongs to one player. Its Instance Id is that player's user id, and only that player can read its values or call its functions. See [Owner Only types](./from-cpp/get-a-server-object.md#owner-only-types). - **Members**: only the Server Object's members read its values; anyone can still see who the members are and join. Needs **Members From**. See [Readable By](./access-members-and-timers.md#readable-by). - **Only One Instance** (`bOnlyOneInstance`): one shared instance for every player, like a registry or a world event; nothing picks an instance. See [Only One Instance](./access-members-and-timers.md#only-one-instance). - **Platform Data**: **Reads Players** (`bReadsPlayers`), **Changes Players' App State** (`bWritesPlayerState`), **Reads World Data** (`bReadsWorld`), **Changes the World** (`bWritesWorld`), **Reads Grids** (`bReadsGrids`), **Manages Grids** (`bManagesGrids`), **Grants Grid Permissions** (`bGrantsGridPermissions`), **Reads Players' Access** (`bReadsAccess`) and **Manages Players' Access** (`bManagesAccess`): what the type's server code may read and change of players, the world, grids and players' access to the app. All off by default. Deploy asks the platform for a permission [scope] for each one ticked, the way **Required Feature** asks for `players.read`; without it the platform refuses those calls. They work for any type. See [Platform data](./access-members-and-timers.md#platform-data) and, for reading who stands in an area, [Count who stands in the area](./keep-a-record-per-entity.md#count-who-stands-in-the-area). - **Holds** (`ECrowdyServerObjectHolds`): **One Set of Variables**, the default, keeps the variables once for the Server Object. **A Record per Entity** (`RecordPerEntity`) keeps them once for each entity, keyed by the entity's id, such as the health of every member of a crowd. See [Keep a record per entity](./keep-a-record-per-entity.md). - **Visible to Players** (`OwnerFields` and `TeamFields` hold the Owner Only and Team Only variables): who receives each variable of a record, **Server Only**, **Everyone**, **Owner Only** or **Team Only**. A variable is in at most one of the two lists, and must be Visible to Players. - **Max Records** (`MaxRecords`): the most records one Server Object keeps. 2048 unless you change it, 1 to 4096. - **Send Changes Every** (`PushIntervalMs`): how often the server sends what changed, in milliseconds. 250 unless you change it, 50 to 5000. Changes in between are sent together, and the last value wins. - **Team From**: where a record's team comes from. **This Object's Records**, the default: a player's team is the team of a record they own in the same Server Object. **Crowdy Team**: a record's team is a Crowdy Team id in digits, and the server checks the player's membership through the platform, so deploying asks for `players.read`. - A record type needs **Readable By** set to **Every Player** and **Members From** set to **None**. - **Area Size** (`AreaSizeChunks`) and **Areas Followed Around a Player** (`InterestRadiusAreas`): for a component or a crowd row in **This Actor's Area** mode, in any type (see [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md)). Area Size is how many chunks one area measures on each side of the ground, 4 unless you change it, 1 to 64. Areas Followed Around a Player is how many areas out from a player's own a crowd follows, 1 unless you change it, 0 to 3. See [Pick the object](./keep-a-record-per-entity.md#pick-the-object). - **Members From**, **Max Members**, **Show Members to Players**, **Remove Members Who Leave**, **Timers**, **On Player Joined**, **On Player Left**, **Can Call**, **Can Call Any Type** (`bCanCallAnyType`) and **Listen To**: the settings on [Access, members and timers](./access-members-and-timers.md). Members From is **None**, **This Object** or **Crowdy Team**. **Can Call Any Type** lets the server code call every type in the app and deploys `calls: ["*"]`; see [Call any type](./access-members-and-timers.md#call-any-type). - **State Form** (`StateForm`, an `ECrowdyServerValuesForm`, shown as the Variables' **Use Struct** check-box): **Struct** (Use Struct ticked) or **Variables** (`List` in C++), added in the asset. The one not chosen is kept but not used. - **State** (labelled **State Struct**) [the hub's state, kept in its snapshots]: the struct holding everything the server keeps for one Server Object, when the Variables use a struct. - **State List** (`StateList`, an `FInstancedPropertyBag`, labelled **Variables**): the variables added in the asset, each with its default value, where a new Server Object starts. In the generated server code they are a struct named after the Type Name in PascalCase plus `State`, so `village_beacon` gives `VillageBeaconState`. - **Watched Fields** [the `state` topic] (labelled **Visible to Players**; the eyes): the variables players can read and watch, by the name the editor gives them. The others never leave the server. In a record type, the Owner Only and Team Only variables are listed here too. - **Functions** [methods]: what players, other server code and developer tools can ask the Server Object to do. Each, an `FCrowdyServerFunction`, has: - **Name**: the name your C++ passes to `Call`. - **Server Name** (in the function's **Advanced** section) [method name]: the function's name on the server. Empty uses Name in snake case, so `FeedBeacon` becomes `feed_beacon`. Lowercase letters, digits and underscores, starting with a letter, at most 64 characters; `read` is reserved. - **Params Form** (`ParamsForm`, shown as the Inputs' **Use Struct** check-box): **Struct** or **Variables**, added in the asset. - **Params** (labelled **Input Struct**): the struct the caller sends, when the Inputs use a struct. None sends nothing. - **Params List** (`ParamsList`, labelled **Inputs**): the inputs the caller sends, with their default values. No inputs sends nothing. In the generated server code they are a struct named after the function plus `Params`, such as `FeedBeaconParams`, with the default values as its defaults. - **Reply Form** (`ReplyForm`, shown as the Outputs' **Use Struct** check-box): **Struct** or **Variables**. - **Reply** (labelled **Output Struct**): the struct the server answers with, when the Outputs use a struct. None answers nothing. - **Reply List** (`ReplyList`, labelled **Outputs**): the outputs the server answers with. No outputs answers nothing. In the generated server code they are a struct named after the function plus `Reply`, such as `FeedBeaconReply`. - **Who Can Call** (`ECrowdyServerFunctionCaller`, labelled **Callable By**): **Players** lets players call it; **Members** and **Leader** limit it to the members or the leader; **Server Only** refuses a player's call, and only other server code and developer tools can call it; **Developers Only** lets only developer tools call it. See [Callable By](#callable-by). - **Cooldown** (`CooldownSeconds`): how long one player waits between two calls that succeeded; 0 for none. See [Cooldown](./access-members-and-timers.md#cooldown). - **Messages** [topics other than `state`]: what the server code sends to everyone listening. Each, an `FCrowdyServerMessage`, has: - **Name**: the name Blueprint and C++ use, such as the `Message` that `WatchMessage` takes. Unique in the type. - **Server Name** (in the message's **Advanced** section) [topic]: the message's name on the server. Empty uses Name in snake case, so `BossDefeated` becomes `boss_defeated`. Lowercase letters, digits and underscores, starting with a letter, at most 64 characters; `state` is reserved. - **Players Can Listen** (`bPlayersCanListen`): on by default. Off, only a type that Listens To this one hears the message. See [Who hears a message](#who-hears-a-message). - **Payload Form** (`PayloadForm`, shown as the Payload's **Use Struct** check-box): **Struct** or **Variables**, added in the asset. - **Payload** (labelled **Payload Struct**): the struct the message carries, when the payload uses a struct. None carries nothing. - **Payload List** (`PayloadList`, labelled **Payload**): the values the message carries, with their default values. No values carries nothing. In the generated server code they are a struct named after the message plus `Message`, such as `BossDefeatedMessage`. - **Listen To** (`ListenTo`) [`calls`]: other Server Object types whose messages this type's server code listens to. Each is also in Can Call. See [Listen to other Server Objects](./access-members-and-timers.md#listen-to-other-server-objects). A function added with **+** starts with its inputs and outputs added in the asset, and none of either. A message added with **+** starts with its payload added in the asset, and no values. Variables, inputs and outputs added in the asset can be any type the Blueprint type picker offers there: booleans, integers, floats and doubles, names, strings, enums, structs, soft object and soft class paths, and arrays, sets and maps of those. Text, object, class and interface references, `FInstancedStruct` and `FGameplayTagContainer` are not offered, since a Server Object cannot carry them. Variables, inputs or outputs share one struct in the generated server code when they have the same names, types and order and one was copied from the other, which keeps each value's hidden id. Lists you built separately in the editor never share, even if they look alike. The first of the sharing Lists keeps the struct, counting the variables first, then the functions in order with inputs before outputs. Each of the others that starts with the same values gets its own name as a type alias, so `logic.rs` can still use it: in the [tip jar](./examples/tip-jar.md), the Tip output (`Total`, starting at 0) is `pub type TipReply = TipJarState;`, because the jar's `Total` also starts at 0. A List that has the same shape but starts with different values gets its own struct with its own defaults. In the village beacon, the beacon's `Oil` starts at 50, so the FeedBeacon input (`Oil`, starting at 10) and output (`Oil`, starting at 0) are each their own struct, and code that used one for the other no longer builds. Generate shows a notice when a List switches between the two, such as "TipReply is now its own struct, since its starting values differ from those of TipJarState; code that uses one for the other no longer builds." In Unreal each function keeps its own default values: **Make Inputs**, the call nodes and a reply that leaves a value out use that function's. In the generated server code the defaults matter only when an older client leaves out a value it does not know yet. **Save and stop** (with the type's own settings) - **Save Interval Seconds** [`persist_every_ms`] (labelled **Save Every**): how often the server saves a running Server Object, 5 to 60 seconds, default 30. After a crash it starts again from its last save, so at most this much is lost. - **Idle Timeout Seconds** [`evict_after_ms`] (labelled **Stop When Idle For**): how long a Server Object nobody uses keeps running before the server stops it, 1 to 1800 seconds, default 300. The next use starts it again from its last save. **Scaling** (a Server Function Library only) - **Min Instances** (`MinInstances`) [`replicas`]: how many instances run all the time, 1 to 64, default 1. - **Max Instances** (`MaxInstances`) [`max_replicas`]: the most instances the server adds when calls have waited 2 seconds, 1 to 64, default 1. It removes an added one after about 30 seconds nearly idle. It must be at least Min Instances. - **Parallel Calls per Instance** (`ParallelCallsPerInstance`) [`concurrency`]: how many calls one instance answers at the same time, 1 to 256, default 16. Each running instance counts toward the app's limit on running Server Objects. See [Scaling](./server-function-libraries.md#scaling). **Server Limits** (collapsed; the defaults suit most types) - **Memory Megabytes** [`memory_mb`] (labelled **Memory per Instance**): how much memory each running Server Object may use, 1 to 512 MB, default 64. Each one reserves it from the app's memory limit, so a type that needs less fits more Server Objects. An app with no funds has 1 GB in all, so at 512 MB only two run at once and the next is answered **Unavailable**; a funded app has 64 GB. An app with no funds can also run only 16 Server Objects at once, a funded app 16,384. - **Call Time Limit Ms** [`deadline_ms`] (labelled **Time per Call**): how long one Server Function call may run before the server stops it and the caller gets **Timeout**, 10 to 10,000 ms, default 2,000. A player's connection waits up to 20 seconds for an answer: the longest **Time per Call** (10 seconds) plus time the call waits behind others at the Server Object. The limit covers only the function itself, not that waiting. A call that runs past it is answered `DeadlineExceeded` (the SDK reports **Timeout**, with a reason such as "AttackBoss ran past its Time per Call on the server: handler exceeded its deadline"; if no answer reaches the game at all, the reason is "The server sent no answer in time; the connection may be slow or lost"), and the platform also restarts that Server Object from its last save, so changes since the last save are lost. Keep functions short, and set **Save Every** low on a type whose recent changes matter. - **Fuel Per Call Millions** [`fuel_per_call`] (labelled **Fuel per Call (Millions)**): how much work one call may do before the server stops it, 1 to 1,000 million, default 50 million. - **Max Waiting Calls** [`mailbox`] (labelled **Calls Waiting**): how many calls may wait at one Server Object, 1 to 4,096, default 1,024. Past that a call is answered **Busy**. Deploy sends a limit only once you change it, so a type that keeps the defaults deploys exactly as before. A value outside its range is brought into it. **Server Names** (in the Advanced group) - **Field Names**: keeps a field's name on the server when you rename the field in Unreal. Each entry, an `FCrowdyServerFieldName`, names the struct, the field as the struct now names it, and the Server Name to keep. You pick the struct, then the field from a list of that struct's fields, so there is nothing to spell. For variables, inputs and outputs added in the asset, use **List Value Names**. - **Enum Value Names**: the same for an enum value, each entry an `FCrowdyServerEnumValueName`: pick the enum, then the value from a list of its values. - **List Value Names**: the same for a variable, input or output added in the asset, each entry an `FCrowdyServerListValueName`: pick the List (the variables, or a function's inputs or outputs), then the value, and the Server Name to keep. An entry follows its value through later renames, since it records the value's id, not its name. You rarely add one yourself: Generate Server Code adds it when you choose to keep a renamed value's old name (see [After you change the definition](./write-server-logic.md#after-you-change-the-definition)). An entry stops applying when its function is renamed; Generate then asks again. In both, the second list stays empty until the first is chosen ("Choose the struct first", "Choose the enum first"), a list with nothing to offer says "Nothing to pick", an entry still to be filled in reads "None", and one whose struct, field, enum, value or List is gone reads its name followed by "(missing)" ("Unknown value (missing)" for a List value, which is kept by its id). A Blueprint enum's values are listed by the names the editor shows. **Server Code** (set on the editor's Server Code tab, not in Details) - **Code Source**: where this type's Server Functions come from. **Generated** (the default) uses `Server//src/logic.rs`, the file Generate Server Code writes once and never replaces. **My Own File** uses a `.rs` file of your own instead, for example one shared in your repo. It is the combo beside the file's path on the tab. In C++ the choice is the enum `ECrowdyServerCodeSource`, with the values `Generated` and `OwnFile`, and a third, `GameKitStarter` (**Game Kit Starter**), which only **Add starter** sets; see [Use the Game Kit starters](./use-game-kit-starters.md). - **Logic File**: your `.rs` file, relative to the project folder, chosen with the folder button on the Server Code tab. The button is shown, and the file used, only when Code Source is My Own File; with My Own File and no file chosen, the type cannot be generated or deployed, and the messages say "Code Source is My Own File but no file is chosen". The tab shows the file's state and code; see [Write its server logic](./write-server-logic.md#the-server-code-tab) and [Use your own logic file](./write-server-logic.md#use-your-own-logic-file). The Logic File is editor-only, like the Description; Code Source is kept in a packaged game, which reaches a Game Kit starter differently from other types. **Baked** (in the Advanced group, read only) - **Baked Structs** and **Baked Enums**: the field and value tables the SDK uses at runtime, rebuilt every time you save the asset. You never edit them. ## Checks when you save Saving the asset, and the editor's **Validate Data**, check the whole definition and list every problem at once, naming the struct and field (or the function or message) each is about: a Type Name or Server Name that breaks the rules, two functions or two fields with the same name on the server, a message with no Name, two messages with one Name or one topic, a topic that is `state`, a variable visible to players that is not in the State struct, and any field of a type a Server Object cannot carry. For a record type it also names an Owner Only or Team Only variable that is not a field of the State or not Visible to Players, a variable that is both Owner Only and Team Only, **Readable By** other than **Every Player**, **Members From** other than **None**, a function named `changes`, **Max Records** or **Send Changes Every** out of range, and **Area Size** or **Areas Followed Around a Player** out of range. The Server Object panel puts a warning icon on the variable, function or message a problem is about. A definition with a problem cannot be used until it is fixed. A Server Function Library is checked on what it shows only, and needs no State struct or variable. It is also refused when a function is **Members** or **Leader**, when a function has a **Cooldown**, and when **Max Instances** is below **Min Instances**; see [Checks when you save](./server-function-libraries.md#checks-when-you-save). Variables, inputs and outputs added in the asset are checked the same way, named by the name the server code gives them, for example "FeedBeaconParams.Label: text is not supported; use FString". Two more checks are about them only: - "Add at least one variable, or use a State struct": the Variables are added in the asset and there are none. - "Two lists are named FeedBeaconParams; give their functions different names": two functions with the same Name both have inputs (or outputs) added in the asset, which would give the server two structs with one name. Field names on the server are your C++ member names (`Oil`, `bLit`), or a Blueprint struct's field names with every character other than letters, digits and underscores removed (`Max Health` becomes `MaxHealth`). Two fields whose names differ only in case are refused. :::warning[After you change a struct, save the definition again.] The tables in the asset describe the structs as they were when it was saved. If a struct gains or loses a field and the asset is not saved again, acquiring a Server Object of that type fails with a message saying the struct has changed since the definition was saved. Every cook saves it again, so this bites in the editor, not in a packaged game. ::: ## Field types | Type | Notes | |---|---| | `bool`, `uint8`, `int32`, `int64`, `float`, `double` | C++ also allows `int8`, `int16`, `uint16`, `uint32`, `uint64`. | | `FString`, `FName` | Use `FString` for text the server makes up or players choose (names, messages). | | Enums | Travel by the value's name, so reordering values is safe. | | Nested `USTRUCT`s | | | `TArray`, `TSet`, `TMap` | Set elements and map keys must be strings, names, integers or enums. | | `TOptional` | C++ only. | | `FVector`, `FVector2D`, `FIntPoint`, `FIntVector`, `FRotator`, `FQuat`, `FLinearColor`, `FColor` | | | `FDateTime`, `FTimespan` | To the millisecond. | | `FGuid`, `FGameplayTag` | A tag this build does not know fails to decode. | | `TSoftObjectPtr`, `TSoftClassPtr`, `FSoftObjectPath` | The path only; nothing is loaded. | :::warning[Keep server-made text in FString, not FName or a soft path.] Every `FName` and soft object path the server sends becomes a permanent name in the engine, never freed, so a client accepts at most 1 Mi (1,048,576) characters of new names (Name values and soft object path parts it has not seen before) from the server per run. Past that, a new Name reads as None and an object path made of new names reads as empty; the rest of the message still applies, a map or set entry whose key was skipped is dropped, and the client logs one warning. The text "None" never counts. A variable holding player-chosen names or free text should be a String (`FString`). Keep `FName`, enums, gameplay tags and soft paths for values drawn from a fixed set your game already knows. ::: Refused: object, class, weak and interface references, `FText`, `FInstancedStruct`, `FGameplayTagContainer` (use a `TArray` of `FGameplayTag`), fixed-size arrays, delegates, and field paths. Editor-only, deprecated and `Transient` properties never travel. ## Changing a type later A Server Object's state outlives the code that wrote it, so some changes are safe and some are not. | Change | What happens | |---|---| | Add a field | Safe. Older saves and older clients leave it at its default. | | Remove a field | Safe. The server drops it from its saves. | | Rename a field | The server sees one field removed and another added, and the old value is lost. Keep the old name under **Field Names** instead. | | Rename a variable, input or output added in the asset | Generate Server Code notices the rename and asks whether to keep the old name on the server. Keep it and saved values carry over (a **List Value Names** entry holds the old name); rename it on the server and the old value is lost, as with a field. | | Change a field's type | Refused when the value is read, and a save holding the old type cannot be loaded. Add a new field instead. | | Reorder enum values | Safe: values travel by name. | | Change **Kind** | Safe both ways. To a Server Function Library the saved data is kept, unused, and comes back if the type becomes a Server Object again; the deploy says so. See [Switch Kind](./server-function-libraries.md#switch-kind). | | Rename or remove a message | A **Received** node for the old name no longer compiles ("has no message ... any more"), and `WatchMessage` of it logs a warning and does nothing. Other types that listen to it need their server code generated again. | | Add an enum value | An older client reads the new value as the field's default and logs a warning; older server code refuses it. | | Remove or rename an enum value | A save holding it cannot be loaded. Keep the old name under **Enum Value Names**, or stop using the value before you remove it. | After any change, the server code has to be generated and deployed again for the server to know it; see [After you change the definition](./write-server-logic.md#after-you-change-the-definition). ## Related - [Make a Server Function Library](./server-function-libraries.md): a type with functions only, answered by several instances at once - [Access, members and timers](./access-members-and-timers.md): who may read and call, members, timers, and calling other types - [Keep a record per entity](./keep-a-record-per-entity.md): a Server Object that holds values for every entity of a crowd - [Write its server logic](./write-server-logic.md): generating the type's server code from this asset, and the Server Code tab - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md) and [from C++](./from-cpp/get-a-server-object.md): using the type in a game - [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md): listening to the messages defined above - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) - [ck-exec overview](/exec/intro): hubs, snapshots and the manifest these settings become # Access, members and timers Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/access-members-and-timers # Access, members and timers :::note Server Objects run on [ck-exec](/exec/intro). ::: Some rules are the same in every game: only the players of a team may spend the team's gold, a match starts once and only its leader starts it, a purchase can be made once a minute. You can write each of them in `logic.rs`, or you can set them in the [definition asset](./create-a-type.md) and let the server code the editor generates enforce them before your code runs. Then `logic.rs` holds only the rules that are truly yours. The settings fall into five groups in the asset's Details tab, each in its own category: - **Access**: who can read a Server Object, who can call each Server Function, how often, and with what values. - **Members**: who belongs to a Server Object. - **Timers & Events**: things the server does on a clock, and when a player arrives or leaves. - **Can Call** and **Listen To**: the other Server Object types this one's server code may call, or any type with **Can Call Any Type**, and the ones whose messages it listens to; see [Call other Server Objects](#call-other-server-objects) and [Listen to other Server Objects](#listen-to-other-server-objects). A [Server Function Library](./server-function-libraries.md) has only **Can Call**, **Platform Data** and the function rules **Callable By** (without Members and Leader), **Required Feature** and **Value Range**; the rest of this page is about Server Objects. After you change any of these, [generate the server code](./write-server-logic.md#after-you-change-the-definition) and deploy it again, since the server enforces them. Two examples in Oakford's world use every group; see [Guild halls and arena lobbies](./examples/guild-halls-and-arenas.md). ## Access ### Readable By **Readable By** is on the type: select **Type Settings** in the Server Object panel. It says who can read the variables that are [Visible to Players](./create-a-type.md#the-details-tab) and is told when they change. | Choice | Who reads the variables | |---|---| | **Every Player** | Any player. | | **Owner Only** | The one player who owns it. Its Instance Id is that player's user id. See [Owner Only types](./from-cpp/get-a-server-object.md#owner-only-types). | | **Members** | The members of the Server Object. Anyone can still see who the members are and whether joining is open, so a player who is not a member can find it and join. **Members** needs **Members From**. | With **Members** or **Owner Only** the server does not send a change's values to everyone. It tells the watchers that something changed, and each reads the values again. That is fine for a team of five; for a big group, use **Every Player**. See [Large groups](#large-groups). ### Callable By **Callable By** is on each function, in its Details. | Choice | Who may call it | |---|---| | **Players** | Any player. | | **Members** | A player who is a member of this Server Object. | | **Leader** | The leader of this Server Object. | | **Server Only** | No player. Only other server code and developer tools. | | **Developers Only** | Only developer tools, such as [Call as Developer](./deploy-with-server-compute.md#call-a-function-as-the-developer); no player and no other server code. | Other server code and developer tools may call every function whatever it says, except a Developers Only one, which only developer tools reach. **Members** and **Leader** need **Members From**. The check is made on the server, so a modified game client cannot get around it. ### Required Feature **Required Feature**, beside **Callable By**, names a feature key the caller's access tier must grant, such as `vip`. Define features and grant them to tiers on the [Access page](../studio/overview.md#the-access-page) in Crowdy Studio. A player whose tier does not grant it is refused before your code runs, with the outcome `Denied`, not retryable, and the reason "needs the vip feature". A refused call does not start the **Cooldown**. Leave it empty for no check. It checks players only, like **Callable By**: other server code and developer tools are let through. A key is 1 to 64 letters, digits, underscores, hyphens and dots, matched exactly, capitals included. A **Server Only** or **Developers Only** function has no player to check, so a Required Feature there does nothing: the function shows a warning under it ("Cooldown and Required Feature have no effect here: they limit only players' calls, and only developer tools or server code may call this function.") and Deploy lists it ("village_beacon Refill: Required Feature has no effect, since only players are checked."). A type with a Required Feature on a function that players may call asks the platform for the `players.read` scope when it is deployed; nothing else is needed. ### Cooldown **Cooldown**, in seconds, sits beside **Callable By**. It is how long one player waits between two calls of that function that succeeded; 0 means no wait. A call inside the wait is refused, and the refusal is retryable: "Feed can be called again in 4 s". A call that failed does not start the wait. Each player has their own wait, and the server remembers the last calls of up to 10,000 players per function. Only players wait: on a **Server Only** or **Developers Only** function a Cooldown does nothing, and the function shows the same warning as for a Required Feature, which Deploy lists as "village_beacon Refill: Cooldown has no effect, since only players' calls are limited." ### Value Range **Value Range** limits a number a caller sends. Select an **Input** of a number type in the function's Details and fill in the two boxes under **Value Range**, **Min** and **Max**, both optional. A call with a number outside them is refused before your code runs: `bad_params: Amount must be 1 to 100`. ![guild_hall's Deposit: Callable By Members, Cooldown 2 s, and the Amount input's Value Range of 1 to 100](/img/unreal-sdk/server-object-function-rules.png) For an input taken from a struct, set the range on the struct's field with `meta = (ClampMin = 1, ClampMax = 100)`; the two ways read the same. The range is read when the server code is generated, so generate and deploy again after changing it. Only number inputs can have one. ### Only One Instance **Only One Instance** is on the type, in **Access**, beside **Readable By**. Tick it for one shared instance for every player, like a registry or a world event: a list of the world's open arenas, the day's festival. Nothing picks an instance, so no Instance Id is needed anywhere. - In Blueprint, the [typed nodes](./from-blueprint/get-a-server-object.md#where-the-object-comes-from) for the asset need neither a **Target** nor a **Find By Asset** choice. Place the node and use it. A **Crowdy Server Object** component with the asset joins the one object whatever its **Instance Mode** says. - From C++, acquire it with any Instance Id, or an empty one; the subsystem uses the one object. - The generated server code refuses a call for any other instance with `denied`, so a stray tool cannot make a second one. - It cannot be combined with **Readable By** set to **Owner Only**, whose Instance Id is each player's user id, or with **Members From** set to **Crowdy Team**, whose Instance Id is the team id. The asset says so when you save it. Ticking or clearing it changes which object players reach, so generate and deploy the type again. Data saved under the old Instance Id is not moved to the new one. A [Server Function Library](./server-function-libraries.md) is reached the same way, with no Instance Id, and never needs this setting. ### Who receives a variable of a record In a type whose **Holds** is **A Record per Entity**, each variable has its own audience instead of the single **Visible to Players** switch. Select the variable and set **Visible to Players** in its Details: | Choice | Who receives it | |---|---| | **Server Only** | Nobody. It never leaves the server. | | **Everyone** | Every player who watches the object. | | **Owner Only** | The player who owns the record's entity. | | **Team Only** | Players whose own record in the same object has the same team, or who are on the record's Crowdy Team, as **Team From** says. | The record's owner and team are set by server code, not by a variable. A player who owns no record, or no matching team, receives the Everyone variables only; the others read as their defaults and never raise a change. Everyone values arrive in the server's batches. Owner Only and Team Only values are fetched when the server says they changed, at most once a second for each object, so they arrive a moment later. See [Who receives a value](./keep-a-record-per-entity.md#who-receives-a-value). **Readable By** must be **Every Player** for such a type, and **Members From** must be **None**: who sees what is decided per variable. ### Platform data The **Platform Data** category says what the type's server code may read and change outside its own Server Objects: players, the world and grids. Deploy asks the platform for a permission (a scope) for each setting you tick, and the platform refuses the server code's calls into anything else with `Denied`. All are off by default; tick only what the code uses. | Setting | C++ | Scope | Lets the server code | |---|---|---|---| | **Reads Players** | `bReadsPlayers` | `players.read` | Read a player's name, the features their access tier grants, their group membership and permissions, and their app state. | | **Changes Players' App State** | `bWritesPlayerState` | `players.write` | Replace a player's app state. | | **Reads World Data** | `bReadsWorld` | `world.read` | Read a chunk, the voxels written into it, and which actors are in or near it. | | **Changes the World** | `bWritesWorld` | `world.write` | Write voxels and a chunk's state. | | **Reads Grids** | `bReadsGrids` | `grids.read` | Find the grid over a chunk, read a grid and its owner, and check a player's permission on it. | | **Manages Grids** | `bManagesGrids` | `grids.write` | Create and delete grids, give a group keys on one, and open one to every player. | | **Grants Grid Permissions** | `bGrantsGridPermissions` | `permissions.write` | Grant and revoke a player's permissions on the app's grids. | | **Reads Players' Access** | `bReadsAccess` | `access.read` | Read a player's access to the app: status, tier, expiry and suspension. | | **Manages Players' Access** | `bManagesAccess` | `access.write` | Grant, revoke and suspend a player's access to the app. | **Manages Grids** and **Manages Players' Access** give the server code what an app admin can do in Crowdy Studio over grids and players' access, for your app only; tick them only on the types that need them. Crowdy Team members, records whose **Team From** is **Crowdy Team** and a **Required Feature** on a function players may call ask for `players.read` on their own, so **Reads Players** changes nothing for such a type. The box then shows ticked and greyed, and its tooltip names why, for example "Needed because Members From is Crowdy Team; Required Feature is set on Feed, so deploy asks for players.read whatever this says." (a Required Feature on a Developers Only or Server Only function does not count). The calls themselves are on the platform's [World and platform data](/exec/world-and-platform-data) page. Before it sends anything, Deploy looks through the type's server code for calls into these, and lists each one whose setting is off, such as "village_beacon: its server code calls world().set_voxels(), which needs world.write; turn on Changes the World or the platform refuses that call". It is a hint, not a check: it sees a call written as `ctx.world().set_voxels(...)`, but not one made through a variable that holds `ctx.world()` or through `ctx.node`. It ignores code inside Rust raw strings and inside `#[cfg(test)]` items (tests), so a call there raises no hint. A changed setting takes a deploy, like changed code. ### A type players cannot reach A type whose functions are all **Server Only**, with no variable **Visible to Players**, **Holds** set to **One Set of Variables**, **Members From** set to **None**, no message with **Players Can Listen** ticked, and neither **On Player Joined** nor **On Player Left**, gives players nothing to do. (A type that holds a record per entity always reaches players: each record carries its entity, owner and team.) Deploy tells the platform to keep players out of it altogether, so only other server code and developer tools reach its Server Objects, and Server Compute describes it as "closed to players". Acquiring one in a game is refused at once, with "`` is closed to players: only server code and developer tools reach it". Give the type any of those and the next deploy opens it to players again. ## Members **Members From** is on the type. It decides who belongs to the Server Object. | Choice | Members are | |---|---| | **None** | Nobody. There are no members, and saving refuses **Members** in **Readable By** and **Members** or **Leader** in **Callable By**. | | **This Object** | Players the Server Object keeps itself. Players **Join** and **Leave**. | | **Crowdy Team** | The members of a [Crowdy Team](../services/teams.md) whose id is the Server Object's Instance Id. | ### This Object The Server Object keeps its members, their order of joining, its leader and whether joining is open, and saves them with its variables. Set: - **Max Members**: the most members it takes. It is 100 unless you change it. 0 means no limit, and needs **Show Members to Players** off. - **Show Members to Players**: on by default. Players can read who the members are. Turn it off for a large group: players then get only the number of members, and **Max Members** may be 0 or any size. With it on, **Max Members** must be 1 to 4,096. - **Remove Members Who Leave**: on by default. A member who leaves the game, once their last connection to the Server Object closes, stops being a member. Turn it off to keep a member listed while they are away. When a Server Object restarts (switched off and on, or moved), the platform does not tell it about players whose connection closed while it was stopped, so see [Players who left while it was stopped](#players-who-left-while-it-was-stopped) below. The panel's **Built-in** section then lists six functions, greyed out, each with its inputs as a function row shows them, such as **Add Member (Player)**: you cannot edit or delete them, and you cannot make a function of your own with the same name. They work like any Server Function: call them from Blueprint with **Make Inputs** and **Call Server Function**, or from C++ with `Call`. ![arena_match: the greyed Built-in functions, Readable By Members, Members From This Object, Max Members 8 and one Timer](/img/unreal-sdk/server-object-members.png) | Function | Inputs | Who may call it | What it does | |---|---|---|---| | **Join** | none | Any player | Adds the caller. Refused with "It is full" at **Max Members** and "It is not open for joining" while joining is closed. A player who is already a member succeeds. | | **Leave** | none | Any player | Removes the caller. A player who is not a member succeeds. | | **Add Member** | `Player` (Integer64) | Server code only | Adds that player, with the same limits as Join. | | **Remove Member** | `Player` | The leader, or server code | Removes that player. | | **Make Leader** | `Player` | The leader, or server code | Makes that member the leader. Refused for a player who is not a member. | | **Set Open for Joining** | `bOpen` (Boolean) | The leader, or server code | Opens or closes joining. | The inputs of **Add Member**, **Remove Member** and **Make Leader** are the struct `FCrowdyServerMemberInputs`, whose one field is `Player`. The input of **Set Open for Joining** is `FCrowdyServerOpenInputs`, whose one field is `bOpen`. **Add Member** exists so that one type can seat players in another, as a registry does; see [Guild Halls](./examples/guild-halls-and-arenas.md#guild-halls). You can also call it by hand as the developer, with the play button on its row in the Built-in list; see [Call a function as the developer](./deploy-with-server-compute.md#call-a-function-as-the-developer). The inputs of **Add Member**, **Remove Member** and **Make Leader** are the struct `FCrowdyServerMemberInputs`, whose one field is `Player`. The input of **Set Open for Joining** is `FCrowdyServerOpenInputs`, whose one field is `bOpen`. ### The leader - The first member to join is the leader. - When the leader leaves or is removed, the member who has been a member longest becomes the leader. - An object with no members has no leader. - **Make Leader** hands the lead to another member. ### Open for joining A Server Object with members kept by itself also has **Open For Joining**, which starts true. While it is false, **Join** and **Add Member** are refused. The leader closes it with **Set Open for Joining**, and your code closes it with `members::set_open(false)`, for example when a match starts. It opens again by itself when the last member leaves, so nobody can close an empty object for good. Everyone can read it, and a registry reads it to skip a match that has already started. ### What a player who is not a member sees With **Readable By** on **Members**, a player who is not a member can still acquire the Server Object. They read the member count, the leader, **Open For Joining** and, with **Show Members to Players** on, the list of members, so they can decide to join. The variables stay at their defaults, **Is Member** is false, and any function whose **Callable By** is **Members** or **Leader** is refused. With **Show Members to Players** off they see the count, not the list. After a successful **Join** the game reads again and they see everything. ### Crowdy Team With **Crowdy Team**, the platform's teams own joining, roles and the size of the team. The Server Object keeps no members. Players join and leave through the **Teams** nodes ([Create Team, Join Team, Request to Join Team, Leave Team](../services/teams.md)); the Server Object asks the platform who belongs. - The Instance Id is the team's id, written in digits. Anything else is refused, as is **Crowdy Team** with **Readable By** on **Owner Only**. - **Members** means the team's members. - **Leader** means a member who can manage the team, which the team's leader role can. - The six built-in functions do not exist, and there are no `Members`, `MemberCount`, `Leader` or `OpenForJoining` variables. **Is Member** and **Is Leader** work as they do for This Object. - If the platform cannot answer who a player is, the call is refused: "membership could not be checked", with the reason after it. Give an actor the Server Object of the player's team with the component's **Player's Team** mode; see [Add the component](./from-blueprint/get-a-server-object.md#add-the-component). ## Timers and events Under **Timers & Events**, a **Timer** is something your Server Object does on a clock, each with: - **Name**: what it is called. Not empty, unique, and not the name of a function. - **Runs**: **Every**, which runs again and again, **Time** apart; **Once After**, which runs once, **Time** after it starts; or **On Schedule**, which runs at the times a **Cron** expression names. In C++ this is the enum `ECrowdyServerTimerRepeat`, with the values `Every`, `After` and `Schedule`. - **Time**: in seconds, at least 0.01. Not shown for **On Schedule**. - **Cron** and **UTC Offset**: for **On Schedule** only. **Cron** has five fields (minute, hour, day of month, month, day of week), or six with seconds first, or is one of `@hourly`, `@daily`, `@weekly`, `@monthly` and `@yearly`; `0 */6 * * *` is every six hours, on the hour. Times are UTC; **UTC Offset** reads them that many minutes ahead of UTC, from -840 to 840, with no daylight saving: 60 runs `0 9 * * *` at 09:00 in UTC+1. Saving checks each field against its range (an hour of 25 is refused, naming the field), with the syntax in [Timers and presence](/exec/timers-and-presence#cron-schedules). - **Start Automatically**: on by default. It starts when the Server Object first starts. With it off, your code starts it. Each timer's row is titled with its name and when it runs, such as "Respawn: Once After 10 s". A schedule runs on the platform's own clock. Each time it fires, the generated code arms its next run before your function is called, so an error in your function does not end the schedule. A run that came due while the Server Object was stopped runs once when it starts again; the other missed runs are skipped. Changing a timer's **Runs** reaches instances that are already running too, once they start on the new code: a deploy does not restart them, so that is when they next start, or at once if you press **Restart** on the type in Server Compute (see [Restarting a type](./deploy-with-server-compute.md#restarting-a-type)). Each Server Object saves its timers' Runs with its values, and when it starts after a deploy that changed one, a timer that was still pending is armed again under the new Runs. The Server Object arms the changed timer without stopping it first, so if arming fails the timer keeps its old schedule, and the server log says "timer EndMatch keeps any old schedule: the changed one could not be armed: " followed by the error. A timer you did not change keeps its clock. A timer that had stopped, or a Once After that already ran, stays so. A timer with **Start Automatically** that you add later starts in running instances as well, and a timer you remove or rename stops under its old name. A changed timer starts its new Runs from that moment, so a pending Once After waits its full new time. Server code generated by an earlier SDK did not save the Runs, so the first deploy of code generated by this one re-arms nothing; every deploy after it does. Each timer is a function in `logic.rs`, named after it in snake case, so a timer named `EndMatch` is `end_match`. It changes the variables the same way a Server Function does, and its changes reach the watchers. ```rust fn end_match(&mut self, _ctx: &Ctx) -> Result<()> { self.Phase = "Ended".into(); Ok(()) } ``` Each timer's name is also a constant in the `timers` module, its function's name in capitals: `EndMatch` is `timers::END_MATCH`, which holds `"EndMatch"`. Start and stop one from your code with `timers::start(ctx, timers::END_MATCH)` and `timers::stop(ctx, timers::END_MATCH)`. Use the constant rather than typing the name: the name is matched exactly, capitals included, so `"end_match"` or a misspelling is only refused when the code runs, while a misspelt constant does not compile. A timer whose constant would be `QUEUED` is refused when you generate, since the timers code uses that name itself. Start uses the period set in the asset. Inside a Server Function they take effect only when that function succeeds. If the platform refuses a timer the function starts, the whole call is refused: nothing it changed is kept or published, any timer it already started in that call is cancelled, and the player sees `ServerError` with a reason such as `the change was not kept: timer EndMatch could not start: `. The two **Events** are tick-boxes, **On Player Joined** and **On Player Left**. Each adds a function to `logic.rs`, `on_player_joined` and `on_player_left`, which get the player's user id. They count a player's connections to the Server Object, not its members. A player who opens the Server Object twice counts once: the first open runs `on_player_joined` and the last close runs `on_player_left`. When **Remove Members Who Leave** is on, the member is removed first and then `on_player_left` runs; it runs even when the player was never a member. ### Players who left while it was stopped When a Server Object restarts, whether it was switched off and on or moved, the platform does not tell it about players whose connection closed while it was stopped. The generated code keeps the connection counts it restored as unconfirmed. Right after the restart it tells the players watching to read again, and any call from a player confirms them. A player who is not heard from within two minutes is treated as having left: **Remove Members Who Leave** removes them, and `on_player_left` runs. A player who reconnects within that time just continues, and `on_player_joined` does not run again. ## Call other Server Objects **Can Call**, in the **Can Call** category, is a list of other Server Object definitions. Each is a type this one's server code may call, from `logic.rs`. Add the definition assets of the types, never the type's own. The types deploy together, so the callee is there when the caller runs. Each function of a listed type becomes a typed function in `logic.rs`, under `calls::::`. It takes the `Ctx`, the callee's Instance Id and its inputs, and returns its outputs: ```rust let hall = format!("hall-{}", self.Halls); calls::guild_hall::add_member(ctx, &hall, &calls::guild_hall::MemberInputs { Player: player })?; ``` A call from server code is not stopped by **Callable By**, so this is also how a **Server Only** function gets called. When the call fails, its error comes back to your code with the type and function in front, "guild_hall.add_member failed: It is full". Return it with `?` and the caller's player sees it as the reason their call failed. What the callee already changed stays changed even if your function then fails. The first call to an instance that is just starting, such as a new match, can take longer than a call is allowed, and fails with "call deadline passed" although the callee may still finish it. A function that is safe to repeat, like **Add Member**, can simply be called again once: ```rust let inputs = calls::guild_hall::MemberInputs { Player: player }; calls::guild_hall::add_member(ctx, &hall, &inputs).or_else(|_| calls::guild_hall::add_member(ctx, &hall, &inputs))?; ``` ### Call a library A [Server Function Library](./server-function-libraries.md) can be listed under **Can Call** like any other type. It has no Instance Id, so its typed helpers take no key: ```rust let loot = calls::loot_roller::roll_loot(ctx, &calls::loot_roller::LootRollInputs { Seed: 7, Tier: 1 })?; ``` Everything else is as above, and a call to a library that has been idle can take over a second; see [A call to a library from server code fails](./troubleshooting.md#a-call-to-a-library-from-server-code-fails). A library can list types under **Can Call** too, and calls them with their keys; see [A library that changes a Server Object](./write-server-logic.md#a-library-that-changes-a-server-object). ### Call any type **Can Call Any Type**, a check-box in the **Can Call** category of any type, lets the type's server code call and listen to every type in the app, including types added later. Deploy then writes `"calls": ["*"]` for the type in place of the names. It keeps the list: **Can Call** and **Listen To** still decide which types get typed helpers in `logic.rs`, so the types you name keep their `calls::::` functions, and **Listen To** keeps its handlers. For a type that is not in the list, there is one more function, `calls::any`. It is made only when **Can Call Any Type** is on. It calls a type by its Type Name, with a payload you encode yourself, and returns the reply bytes undecoded: ```rust #[derive(Serialize)] struct Roll { Seed: i64, Tier: i32, } #[derive(Deserialize, Default)] #[serde(default)] struct Rolled { Item: String, } let payload = encode(&Roll { Seed: 7, Tier: 1 })?; let reply = calls::any(ctx, "loot_roller", "", "roll_loot", &payload).map_err(|error| Error::new(&format!("loot_roller.roll_loot failed: {error}")))?; let loot: Rolled = decode(&reply)?; ``` The arguments are the type's Type Name, the Instance Id (empty for a [Server Function Library](./server-function-libraries.md)), the function's **Server Name** and the encoded inputs. A function with no inputs takes `&[0x80]`. You write the structs for the inputs and the reply yourself, with the field names the type has on the server, since only the listed types get them generated. Use it when the type to call is not known when you write the code, for example a plug-in type another team deploys. Otherwise list the types: a typed helper checks the name and the inputs when the crate builds, while `calls::any` finds a wrong name only when it runs, as a call that fails with "app N has no node type". Deploy does not check that the type exists. ## Listen to other Server Objects **Listen To**, in the **Listen To** category, is a list of other Server Object definitions, the way **Can Call** is. Each is a type whose [messages](./create-a-type.md#server-messages) this one's server code hears. Add the definition assets of the types, never the type's own. Checks and naming are those of Can Call. - **Each listened type is also callable.** **Listen To** adds its types to the ones the server code may call, so the `calls::::` functions above exist for them too; there is no need to list a type in both. - **Every message of the listened type is heard**, including those with **Players Can Listen** off: that setting is what tells a type's messages apart from the ones players hear, and a Server Object type is not a player. - **Hearing is chosen in server code.** The setting only makes the messages available. The code starts listening to one Server Object of the listened type, by its Instance Id, with a `listen::::` function, and handles what it hears in a function the generator adds, `on__`; see [Listen to a message](./write-server-logic.md#listen-to-a-message). A Server Object can have up to 256 listens at once, and each one takes effect only when the function that made it succeeds. Messages are not replayed: one sent while the listener is not running is missed. ## What a refused player sees A refused call reaches the caller's `OnDone`, or **On Failed** in Blueprint, with its **Outcome** and **Reason**. See [Troubleshooting](./troubleshooting.md#outcomes). | Why | Outcome | Reason | |---|---|---| | **Server Only** function | Denied | players may not call this function | | **Developers Only** function | Denied | only developer tools may call this function | | **Members** function, caller not a member | Denied | only members may call Deposit | | **Leader** function, caller not the leader | Denied | only the leader may call StartMatch | | Owner Only, not the owner | Denied | only the owner may use this Server Object | | Team membership could not be checked | Denied | membership could not be checked, and the reason | | **Cooldown** not over | Denied, **Retryable** | Feed can be called again in 4 s | | The game's server, or this type, is switched off or paused | Paused, **Retryable** | The server is switched off or paused, so it refused the call to Feed: `village_beacon` is switched off in app 123 | | **Value Range** | ServerError | `bad_params: Amount must be 1 to 100` | | Join refused | ServerError | It is full, or It is not open for joining | Denied is not retryable except for a cooldown: calling again will be refused the same way until something changes. A cooldown is retryable, and the number of seconds is rounded up. ## Large groups A guild of hundreds or a match of thousands works, with care: - Set **Readable By** to **Every Player** and **Show Members to Players** off, with **Max Members** as high as you need or 0. Players are then sent the member count, the leader and whether joining is open along with each change, and nobody re-reads. With **Members** or **Owner Only**, every change to a large group makes every member read again. - With the list hidden, **Is Member** and **Is Leader** follow the player's own **Join** and **Leave** and the next read, not a list. - Keep busy per-player actions, such as every player's score or purse, off the one shared object. Every call on one Server Object goes through that one object, and every player watching it is told of each change. For a handful of players, give each an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) object for them, and let the shared one hold what the group shares. For many players, prefer one shared object that holds a record per player (or per area, match or team) over one object per player; see [Keep a record per entity](./keep-a-record-per-entity.md). - Know the platform's limit on running Server Objects per app. An app with no funds can run 16 at once, within 1 GB of memory; a funded app can run 16,384, within 64 GB. Past the limit nothing new starts, and calls to an object that is not running come back **Unavailable**. One object per player runs into it quickly. ## Checks when you save Saving refuses the asset, and lists every problem at once, when: - **Members** or **Leader** is used in **Callable By**, or **Readable By** is **Members**, and **Members From** is **None**. - A function of your own has the name of a built-in one while **Members From** is **This Object**. - **Max Members** is below 0, or is 0 or above 4,096 while **Show Members to Players** is on. - A **Value Range** has a minimum above its maximum, or is on an input that is not a number. - A timer's name is empty, is used twice, is not a usable name once written in snake case, or is a function's name, or is `crowdy_flush` (the name a record type uses to send its changes) or `crowdy_sessions` (the name the generated code uses to keep track of connected players) in snake case, or its **Time** is under 0.01 seconds. - **Can Call** or **Listen To** has an empty entry, the type itself, or a type without a Type Name. - **Listen To** names a Server Function Library ("loot_roller is a Server Function Library and sends no messages."). Put it under **Can Call** instead. - **Members From** is **Crowdy Team** and **Readable By** is **Owner Only**. - **Only One Instance** is ticked with **Readable By** on **Owner Only**, or with **Members From** on **Crowdy Team**. - **Holds** is **A Record per Entity** and **Readable By** is not **Every Player** ("needs Readable By Every Player; make the variables others must not see Owner Only or Team Only"), or **Members From** is not **None** ("cannot use Members From; each record carries its own owner and team"), or a function is named `changes`. - A variable is Owner Only or Team Only but is not a field of the State or not Visible to Players, or is both ("'Gold' is both Owner Only and Team Only; choose one"). - **Max Records**, **Send Changes Every**, **Area Size** or **Areas Followed Around a Player** is outside its range ("Max Records is 5000; it must be from 1 to 4096"). ## For logic.rs The generated server code gives `logic.rs` these, when the definition uses the setting. | Name | What it does | |---|---| | `members::list()` | The members' user ids, in the order they joined. This Object only. | | `members::leader()` | The leader's user id, if any. | | `members::is_member(player)` | Whether a player is a member. | | `members::is_open()` | Whether joining is open. | | `members::set_open(open)` | Opens or closes joining. | | `members::add(player)` | Adds a member, and errors when full or closed. The first member leads. | | `members::remove(player)` | Removes a member. | | `members::make_leader(player)` | Makes a member the leader, and errors for a player who is not one. | | `members::is_member_of_team(ctx, player)` | Whether a player is in the team. Crowdy Team only. | | `timers::start(ctx, timers::END_MATCH)`, `timers::stop(ctx, timers::END_MATCH)` | Starts or stops a timer with the asset's period. | | `timers::END_MATCH` | A timer's name, one constant per timer. | | `calls::::(ctx, key, &inputs)` | Calls a function of a Can Call or Listen To type and returns its outputs. For a Server Function Library there is no `key`. | | `calls::any(ctx, node_type, key, method, &payload)` | With Can Call Any Type: calls any type by its Type Name with an encoded payload, and returns the reply bytes. | | `messages::send_boss_defeated(&payload)`, `messages::BOSS_DEFEATED` | Sends one of the type's messages, and the message's topic as a constant; one of each per message. | | `listen::::(ctx, key)`, `listen::::stop_(ctx, key)` | Starts or stops listening to a message of one Server Object of a Listen To type. | The `members` functions change the Server Object's own members, so a function that fails leaves them as they were, like its variables. ## Related - [Create a Server Object type](./create-a-type.md): the type's other settings and the asset editor - [Relate Server Objects to each other](./relate-server-objects.md): which of these settings to use for one-to-one, one-to-many and many-to-many relations - [Guild halls and arena lobbies](./examples/guild-halls-and-arenas.md): a registry of teams of five, and a lobby with a leader and a timer - [Write its server logic](./write-server-logic.md): what the generated code does for you, and where `logic.rs` goes - [Server messages](./create-a-type.md#server-messages): the messages a Listen To type hears - [Make a Server Function Library](./server-function-libraries.md): a type with functions and no instance, which any Can Call list can name - [Troubleshooting](./troubleshooting.md#outcomes): outcomes and refusals - [Read and follow its variables, from Blueprint](./from-blueprint/read-and-follow-variables.md#by-name): **Get Members**, **Is Member** and **Is Leader** - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md#add-the-component): the component's **Player's Team** and **From Server Value** modes - [Teams](../services/teams.md): the platform's teams, for **Crowdy Team** # Relate Server Objects to each other Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/relate-server-objects # Relate Server Objects to each other :::note Server Objects run on [ck-exec](/exec/intro). ::: A game rarely has one Server Object on its own. A player has a stash, a guild has a hall, a hall sits in a registry, a match reports to a leaderboard. This page shows how to model those relations with the settings you already have: the Instance Id, **Readable By**, **Members From**, **Only One Instance** and **Can Call**. Nothing here is a separate feature. The rule under all of it: **an object refers to another by its Instance Id.** A Server Object's type and Instance Id are all it takes to reach it, from Blueprint, from C++ or from another type's server code. ## One to one One thing in your game has exactly one Server Object of its own. | Relation | Settings | Instance Id | |---|---|---| | A player and their own object, such as a locker or a stash | **Readable By**: **Owner Only** | The player's user id | | An actor placed in the level and its object | The component's **Instance Mode**: **This Actor** | None to type: the actor's placement | | A team and its object | **Members From**: **Crowdy Team** | The team's id | An **Owner Only** object belongs to one player: only that player reads it or calls it. In Blueprint, the component's **Signed-In Player** mode, or **Find By Asset** with **Signed-In Player**, finds it with no id to type. See [Owner Only types](./from-cpp/get-a-server-object.md#owner-only-types). **This Actor** gives each placed actor its own object, the same on every player's machine; the [shared boss](./examples/shared-boss.md#the-actor) uses it. An actor spawned at run time needs an id of its own instead, set with **Instance Id**. A **Crowdy Team** object is the team's: the platform's teams decide who belongs, and the component's **Player's Team** mode finds it. See [Crowdy Team](./access-members-and-timers.md#crowdy-team). ## One to many One object keeps a list of others, such as a registry of the village's guild halls. The registry holds each hall's Instance Id, never the hall's values. | Part | Settings | |---|---| | The registry | **Only One Instance** when there is truly one per app, or one Instance Id per region; a String, Array or Map variable of Instance Ids; **Can Call** with the listed type, if it seats players or changes them | | Each listed object | Its own type and Instance Id, such as `hall-1`, with its own **Readable By** and members | To reach a listed object, take its Instance Id from the registry: - give the component **From Server Value**, pointing **Source Variable** at a variable that holds one Instance Id, and it follows the id as it changes; - or pass the id to **Find By Asset**'s **Instance Id** pin, or to **Get Server Object**. In [Guild halls](./examples/guild-halls-and-arenas.md#guild-halls), `hall_registry` seats each player in a hall with room, writes the hall's id into the player's own `player_team` object, and the hall's actor follows that id with **From Server Value**. ## Many to one Many players, or many objects, share one Server Object. | Relation | Settings | |---|---| | Many players on one shared object, such as a boss | **Readable By**: **Every Player**; a **Cooldown** and a **Value Range** on the functions players call | | Many objects reporting to one, such as matches to a leaderboard | **Can Call** on the reporting type, listing the shared one; the shared function's **Callable By** on **Server Only** | | Many entities, such as a crowd, each with values of its own | **Holds**: **A Record per Entity**, in one object for an area, a match or a team; see [Keep a record per entity](./keep-a-record-per-entity.md) | The [shared boss](./examples/shared-boss.md) is the first: every player hits one boss, and the **Cooldown** and **Value Range** keep each player to a fair rate and a fair hit, enforced on the server before your code runs. **Every Player** sends each change to every watcher; with **Members** or **Owner Only**, every watcher reads again after every change. See [Large groups](./access-members-and-timers.md#large-groups). For the second, an arena match calls the leaderboard's function from its own server code, as `calls::::`. A call from server code is not stopped by **Callable By**, so a **Server Only** function on the leaderboard takes reports from matches and refuses players. See [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). ## Many to many Players belong to several groups, and each group has several players. | Relation | Settings | |---|---| | Players and lobbies | **Members From**: **This Object** on the lobby type, with **Max Members**; one Instance Id per lobby | | Teams and their objects | **Members From**: **Crowdy Team** on each type a team has, all keyed by the team's id | Each lobby keeps its own members, so a player can be a member of several lobbies at once, and each lobby takes up to its **Max Members**. Players **Join** and **Leave**, and **Is Member** and **Get Members** answer per lobby. See [This Object](./access-members-and-timers.md#this-object) and [The arena lobby](./examples/guild-halls-and-arenas.md#the-arena-lobby). With **Crowdy Team**, the platform keeps the relation: a team can have a hall, a banner and a stash, each a type with **Members From** set to **Crowdy Team** and the team's id as its Instance Id. A player in several teams picks one with **Team Id**. See [Crowdy Team](./access-members-and-timers.md#crowdy-team). ## Good practice **Refer by Instance Id; never copy another object's values.** A copy goes stale the moment its owner changes. The registry keeps `hall-1`, not the hall's gold; the game reads the gold from the hall. **Give each fact one owner.** One type changes a value, and the others read it or call that type's function. Only `guild_hall` changes its `Gold` and its members; the registry seats a player by calling the hall's **Add Member**, and the hall does the seating. **Keep calls between types few, and plan for a timeout.** Each call from server code is one more thing that can fail. The first call to an instance that is just starting can run out of time and fail with "call deadline passed", although the callee may still finish it. So call again only a function that is safe to run twice, such as **Add Member**, or make yours safe, as `Assign` is when it gives a seated player back the same hall. What the callee already changed stays changed even if your function then fails. From the game, a **Timeout** outcome means the call may or may not have taken effect; watch the values before you call again. See [Outcomes](./troubleshooting.md#outcomes). **Keep a registry within the limits.** An Unreal client refuses a list, set or map of more than 4,096 entries, more than 65,536 entries in all the lists of one message, and a message over 1 MiB, and a change that would put one into a variable players see is not kept. See [What the generated code does for you](./write-server-logic.md#what-the-generated-code-does-for-you). A list that can grow without bound needs splitting: one registry per region or per day, each its own Instance Id, or the detail kept in the members' own objects, as each player's `player_team` holds their hall. **Choose Readable By per object, not per relation.** Related objects need not share it. A registry readable by every player can list lobbies whose values only their members read. **Use Only One Instance only for what is truly one per app.** It cannot be combined with **Owner Only** or **Crowdy Team**, and ticking or clearing it later changes which object players reach without moving the saved data. A registry that may one day be per region is better off with an Instance Id from the start. See [Only One Instance](./access-members-and-timers.md#only-one-instance). ## Related - [Access, members and timers](./access-members-and-timers.md): every setting used here - [Guild halls and arena lobbies](./examples/guild-halls-and-arenas.md): a registry, team objects and a lobby, built step by step - [Shared boss fight](./examples/shared-boss.md): many players on one object - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md#add-the-component): the component's instance modes - [Get a Server Object, from C++](./from-cpp/get-a-server-object.md): acquiring an object by its Instance Id # Write its server logic Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/write-server-logic # Write its server logic :::note Server Objects run on [ck-exec](/exec/intro). ::: A Server Object's values change only when its server code says so. That code is a small Rust crate, one per type, which the editor writes from the type's [definition asset](./create-a-type.md). Most of it is generated and kept in step with the definition; the part that decides what a Server Function does is yours, and the generator never touches it again once it exists. ## Generate it There are two places to do it, and they do the same thing: in the Content Browser, right-click the definition asset, for example `CSO_VillageBeacon`, and choose **Generate Server Code**; or open the asset and press **Generate** in its editor's toolbar (see [The Server Code tab](#the-server-code-tab)). The editor checks the definition the same way saving it does and lists any problem instead of generating. When it succeeds, a notification names the folder it wrote: ```text Server/ village_beacon/ Cargo.toml src/ lib.rs types.rs logic.rs ``` The folder is `Server//` in your project, next to the `.uproject` file and outside `Content`, so nothing in it is cooked into the game. The crate is named after the Type Name. | File | What it holds | Written | |---|---|---| | `Cargo.toml` | The crate's name, its two dependencies, `ckx-sdk` (pinned to `0.12.1`; it was `0.7.0`, so a crate generated earlier must be generated again before Deploy takes it) and `serde`, and the definition asset it was generated for. | Every time | | `src/types.rs` | A Rust struct for every struct the definition uses and a Rust enum for every enum, with the same fields and the same defaults as in Unreal. | Every time | | `src/lib.rs` | The glue between the platform and your code, and the `Functions` trait listing your Server Functions. | Every time | | `src/logic.rs` | Your code: what each Server Function does. | Once, only when it does not exist, and only when [Code Source](#use-your-own-logic-file) is Generated | A deploy adds one more file to the folder, `revisions.json`, which records what each deploy sent; see [What a deploy records](#what-a-deploy-records). Do not edit `Cargo.toml`, `types.rs` or `lib.rs`: the next generation replaces them. That includes loading a module file of your own from `lib.rs` (a top-level `mod name;` other than `logic` and `types`, with or without `pub` or `#[path]`). Declare your own modules at the top of `logic.rs` instead: `mod helpers;` there loads `src/logic/helpers.rs`. When `lib.rs` has such a line, Generate asks first: "... has lines Generate does not write and would remove: mod helpers;. Generate rewrites lib.rs; declare your own modules at the top of logic.rs instead, where `mod name;` loads src/logic/name.rs. Regenerate anyway?" The default is **No**. `logic.rs` is yours from the moment it is written. When you later add a Server Function, timer or event, Generate offers to add an empty version of it to `logic.rs` (see [After you change the definition](#after-you-change-the-definition)). To get a fresh starting point instead, delete `logic.rs` and generate again. Generate writes only into a folder that is missing or that the SDK generated: its `Cargo.toml` records a definition asset, or the first line of its `src/lib.rs` or `src/types.rs` is the generator's header ("... generated by the Crowdy SDK from its definition. Regenerate it; do not edit."). A `Server//` folder that holds a crate you wrote yourself, for example one you deploy with the platform's API, is refused instead of overwritten: "Server/village_beacon holds server code the Crowdy SDK did not generate, so Generate would overwrite it. Choose another Type Name, or move that folder." The type shows **Problem** on the [Server Compute](./deploy-with-server-compute.md#overview) page with the same remedy. A crate copied from a generated one keeps its record or header, so it counts as generated and Generate rewrites its `Cargo.toml`, `types.rs` and `lib.rs`. Generate also refuses two definitions that have no server code of their own to write. The Crowdy SDK's own root definition (`CSO_PlayersOnline`) says "This is the Crowdy SDK's own root; its server code is built into every deploy, so there is nothing to generate." A project type whose Type Name is `root` says "root is the name of the platform's root Server Object type; choose another Type Name." ### The Server Code tab The definition asset opens in its own [editor](./create-a-type.md#the-asset-editor), and the large tab on its left is **Server Code**, so you can generate, edit and deploy the type's server code without leaving the asset. ![The Server Code tab: the logic.rs path, the Generated choice, revisions, Save and Revert, and the beacon's logic.rs](/img/unreal-sdk/server-code-tab.png) Generate and Deploy are in the editor's toolbar, after Save and Browse, not on the tab. **Generate** is the same as the Content Browser command, including the warning before a change would drop saved data (see [After you change the definition](#after-you-change-the-definition)). **Deploy** builds and deploys every Server Object type in the project, and the small arrow beside it opens **Open Server Compute** and **Show build output**. At the right of the toolbar a coloured dot and one line say where the type stands: **Ready**, **Needs generating**, **Out of date** or **Problem**, or how it compares with the live version (**No changes**, **Changed since version 7**, **Not deployed yet**, **Live code unknown**), the same states as on the [Server Compute](./deploy-with-server-compute.md#overview) page. Hover the line to read the reason. Along the top of the tab, from left to right: - **The file.** The path of the Server Functions file this type uses, relative to the project folder, `Server/village_beacon/src/logic.rs` for Generated. Until the Type Name is valid it reads "`Server//src/logic.rs`, once the Type Name is valid", and for My Own File with no file chosen "No file chosen: choose your .rs file with the folder button". "Unsaved changes" appears beside it while you have edits. - **The source.** A **Code Source** combo, **Generated** or **My Own File**; see [Use your own logic file](#use-your-own-logic-file). With My Own File, a folder button beside it ("Chooses your .rs file") picks the file. - **Deployed revisions.** A combo of the code this type had at earlier deploys, starting with **Your code**; see [View, compare and restore an earlier deploy](#view-compare-and-restore-an-earlier-deploy). - **Three icon buttons.** **Save** ("Writes the code below to the logic file") is enabled only while you have edits, and keeps the file's line endings. **Revert** ("Shows the logic file as it is on disk again"). **Open in external editor** opens the file in the program your computer uses for `.rs` files; it is disabled until the file exists. Below is **the code**: an editor with line numbers down its left edge, and Rust colouring for keywords, types, strings, comments and numbers. Long lines scroll sideways instead of wrapping. When the file does not exist yet, a line above the editor says so ("logic.rs does not exist yet. Generate writes it.") with a **Generate** button for a generated `logic.rs`. When the code lacks a function the definition now has, or defines one it no longer has, a line above the editor says so as you type, for example "logic.rs is missing test_function, on_player_joined, which this type now has." Its **Add Stubs** button adds empty versions of the missing ones just before the closing brace of your `impl Functions` block, as unsaved changes: look them over, then **Save**. When the code has no `impl Functions for ` block, the button reads **Copy Stubs** and copies them for you to paste where they belong. It reads **Copy Stubs** too when that block uses a macro directly inside it, such as `my_macro!(...)`, since the macro may already define some of the functions: the line adds "logic.rs also uses my_macro, which may define some of these; check before adding them.", and nothing is inserted for you. If another program saves the file without the functions Add Stubs or Generate added, the tab says so: "logic.rs was saved by another program without the functions Generate added: test_function, on_player_joined." A function the type no longer has is only named ("logic.rs defines join, which this type no longer has: rename or delete it."); the editor never deletes your code. The paths and the Type Name decide what the card can do: - **Generate and Save are disabled until the Type Name is valid**, and their tooltips say why: "Set a valid Type Name first: lowercase letters, digits and underscores, starting with a letter, at most 48 characters". They are also disabled when the logic file would be outside the project folder ("The logic file must be inside the project folder"), and when another definition has the same Type Name. **Save** is also disabled when the file exists but could not be read ("The logic file could not be read, so saving could replace what it holds"). With no valid path the code editor is read-only, and says "Set a valid Type Name to edit this type's server code here." - **Save asks before it could lose anything.** If the file changed on disk since the editor opened it: "`logic.rs` changed on disk since it was opened here. Overwrite it with your edits?" Yes overwrites the file; No discards your edits and shows the file from disk, after a second question, since they cannot be recovered; Cancel keeps your edits unsaved. If the definition now points at another logic file: "This definition's logic file is no longer `logic.rs`. Save your edits to `logic.rs` anyway?" If the file is not UTF-8 text, it warns that saving writes it as UTF-8, which can change characters that are not plain ASCII. If the file cannot be written: "Could not save `logic.rs`. Close any program using it and try again." - **Revert** shows the file as it is on disk again, and asks first ("Discard your unsaved changes to `logic.rs`?") when you have edits. - **Edits made in another editor show up by themselves.** The tab checks the file every second while it is visible. Saved in VS Code or another editor, the new code appears in the tab within a second when you have no unsaved changes here. If you do, the tab keeps them and says "`logic.rs` changed on disk, for example in another editor, while you have unsaved changes here." with **Reload**, which drops your changes after asking, and **Keep Mine**, after which Save still asks before it overwrites the file. - **Generate** with unsaved edits asks "Save your changes to `logic.rs` first? Generating never replaces an existing logic file." - **Deploy**, in the editor's toolbar, with unsaved edits asks "Save your changes to `logic.rs` first? A deploy sends the files as they are saved." - **Changing the Type Name, Code Source or Logic File** while you have edits asks "Save your changes to `logic.rs`? If not, they stay unsaved and come back when this editor shows `logic.rs` again." Unsaved edits are kept if the panel closes or refreshes. They are not on disk, so [Server Compute's Deploy](./deploy-with-server-compute.md#deploy) warns you about them before it sends the saved files, and closing the editor asks: > These server code files have edits that are not saved: ... Quit anyway and lose the edits? ## Use your own logic file The **Code Source** combo at the top of the Server Code tab, **Generated** or **My Own File** (the definition's Code Source), says where the type's Server Functions come from. - **Generated**, the default, uses `Server//src/logic.rs`, the file Generate writes once. - **My Own File** keeps them somewhere else, for example in a file shared in your repo. A folder button appears beside the combo; choose your `.rs` file with it, relative to the project folder. Choose **Generated** again to go back to `logic.rs`; the file you chose is remembered but not used. The choice is explicit: an empty **Logic File** no longer means "use the generated file". With My Own File and no file chosen, nothing can be generated or deployed for the type, and the messages say so: - The toolbar's status line and the type's Server Compute row read **Needs generating** with "Code Source is My Own File but no file is chosen; choose one, or switch Code Source to Generated". - **Generate** still writes the generated files, then shows "Code Source is My Own File but no file is chosen. Choose one, or switch Code Source to Generated." - The tab says "Choose your .rs file above, or switch to Generated to use the generated logic.rs.", and Save says "Choose your .rs file first". With your own file chosen: - **Generate** writes only the generated files, `Cargo.toml`, `src/lib.rs` and `src/types.rs`. It never creates or overwrites your file. - **Deploy** sends your file as the type's `logic.rs`, whatever it is called on disk. - **One file per type.** Each type builds its logic file against its own State, so two types cannot use the same file, nor one type the generated `logic.rs` of another. Deploy refuses it with "`` and `` use the same Logic File ...; each type needs its own". - **If the file is missing**, the tab says "This file does not exist. Choose an existing file, switch to Generated, or type code below and Save to create it.", the type reads **Needs generating** with the reason "its Logic File ... does not exist; choose an existing file, or switch Code Source to Generated", and Generate says "The Logic File ... does not exist. Choose an existing file, or switch Code Source to Generated." Your file implements the same `Functions` trait as the default one; see [Write logic.rs](#write-logicrs). ## View, compare and restore an earlier deploy Every deploy [records](#what-a-deploy-records) the code it sent for each type, so the type's Server Code tab can show you what was live before. The revisions combo in the tab's header lists them, newest first. Its first entry is **Your code**, the editor as you have been using it. The others give the version, how long ago it was deployed ("just now", "5 min ago", "3 h ago", "2 d ago"), and the word "live" for the revision the live version runs, so the beacon might list Version 7, live, 3 h ago, then Version 6, 2 d ago. When the type has never been deployed, the list says "No deployed revisions yet" and is disabled. How many revisions are kept is set on the Server Compute page's [Settings tab](./deploy-with-server-compute.md#settings). Pick a revision and the editor shows that revision's `logic.rs`, read only, under a line such as "Viewing version 6's code (deployed 2 d ago)". Three buttons act on it: - **Compare with yours** shows a line-by-line difference against the code in your editor, unsaved edits included: lines the revision has that yours does not in red with a minus, lines yours adds in green with a plus. The button then reads **Show its code**, to switch back. - **Restore** puts the revision's code in your editor **as unsaved changes**. Nothing is written until you press **Save**, and nothing is live until you **Deploy** afterwards, so you can look at it, change it, or press **Revert** to throw it away. If you already have unsaved edits it asks "Replace your unsaved changes to `logic.rs` with version 6's code?" first. Restore replaces only the `logic.rs` (or your own file); the generated files stay as **Generate** writes them from the definition today. It is disabled, with the reason on hover, when there is no file to put it in (the Type Name is not valid, or My Own File has none chosen), when the file cannot be read, or when the revision has no `logic.rs`. - **Back to your code** returns to the editor. A revision may have been built from an older definition: if you added a field or a function since, the old `logic.rs` no longer fits the types Generate writes now. The banner then warns "This revision was built from a different definition, so its code may not compile with the types Generate writes now." You can still restore it; expect to fix compile errors before it deploys. To go back to an earlier version as it ran, without editing anything, use **Make active** on the [Versions tab](./deploy-with-server-compute.md#versions) instead: it switches the app to that whole version at once. Restore is for when you want that code back in your project. ## The Rust names - **Structs** are named after the C++ struct without its `F`: `FVillageBeaconState` becomes `VillageBeaconState`. A Blueprint struct uses its name as the editor shows it, with every character other than letters, digits and underscores removed. - **Enums** drop a leading `E`: `EBeaconColor` becomes `BeaconColor`. Each value keeps its name on the server. - A type name that Rust or the generated code already uses (`Vec`, `Error`, `Functions` and the like), or that another of your types already has, gets `Type` added, then a number if that is taken too. - **Fields** keep their names on the server exactly, so the beacon's code reads `self.Oil` and `self.bLit`. A field named like a Rust keyword, such as `type`, is written `r#type`. - **Server Functions** become methods named by their Server Name: `FeedBeacon` is `feed_beacon`. A few names cannot be written in Rust at all: a field or enum value named `self`, `Self`, `super`, `crate` or `_`, and a Server Function whose Server Name is `on_timer`, `on_topic`, `on_presence`, `on_session`, `on_player_joined` or `on_player_left`. Generation stops and names the one it hit; give it a different name on the server under the definition's **Server Names**, or the function a different **Server Name**. Field types map like this: | In Unreal | In Rust | |---|---| | `bool` | `bool` | | `int8`, `int16`, `int32`, `int64` | `i8`, `i16`, `i32`, `i64` | | `uint8`, `uint16`, `uint32`, `uint64` | `u8`, `u16`, `u32`, `u64` | | `float`, `double` | `f32`, `f64` | | `FString`, `FName`, `FGuid`, `FGameplayTag`, soft object and class paths | `String` | | `FDateTime`, `FTimespan` | `i64` | | `TArray` | `Vec` | | `TSet` | `BTreeSet` | | `TMap` | `BTreeMap` | | `TOptional` | `Option` | | `FVector`, `FVector2D`, `FRotator`, `FQuat` | generated structs `Vector`, `Vector2D`, `Rotator`, `Quat` with `f64` fields | | `FIntPoint`, `FIntVector` | generated structs `IntPoint`, `IntVector` with `i32` fields | | `FLinearColor`, `FColor` | generated structs `LinearColor` (`f32` fields) and `Color` (`u8` fields) | The generated structs keep the Unreal field names too: `X`, `Y`, `Z`, `Pitch`, `Yaw`, `Roll`, `R`, `G`, `B`, `A`. Each field in `types.rs` ends with a comment naming its Unreal type, since several Unreal types share one Rust type. For the beacon, `types.rs` holds the state struct with the defaults `FVillageBeaconState` gives it in C++: ```rust #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[serde(default)] pub struct VillageBeaconState { pub Oil: i32, // Int32 pub bLit: bool, // Bool pub LastFedBy: i64, // Int64 } impl Default for VillageBeaconState { fn default() -> Self { Self { Oil: 50, bLit: true, LastFedBy: 0, } } } ``` A new beacon on the server starts from these values, the same ones a client shows before the first values arrive. A default must be the same every time the struct is made: a field initialized with `FGuid::NewGuid()` or the current time cannot be written as server code, and Generate Server Code names it. ## Write logic.rs `lib.rs` declares a trait, `Functions`, with one method per Server Function. For the beacon it reads: ```rust pub trait Functions { fn feed_beacon(&mut self, ctx: &Ctx, call: &Call<'_>, params: FeedBeaconParams) -> Result; fn on_timer(&mut self, ctx: &Ctx, name: &str) -> Result<()> { Ok(()) } fn on_topic(&mut self, ctx: &Ctx, msg: TopicMsg<'_>) -> Result<()> { Ok(()) } fn on_presence(&mut self, ctx: &Ctx, presence: &Presence) -> Result<()> { Ok(()) } } ``` You implement it on the State struct in `logic.rs`. The first generation writes a version whose functions all refuse with "not written yet"; replace them with your own. Here is the beacon's, which takes the oil it is offered up to a limit, relights itself, and remembers who fed it: ```rust use crate::*; use ckx_sdk::prelude::*; const MAX_OIL: i32 = 100; impl Functions for VillageBeaconState { fn feed_beacon(&mut self, _ctx: &Ctx, call: &Call<'_>, params: FeedBeaconParams) -> Result { if params.Oil < 1 { return Err(Error::new("offer at least one unit of oil")); } if self.Oil >= MAX_OIL { return Err(Error::new("the beacon is full")); } self.Oil = self.Oil.saturating_add(params.Oil).min(MAX_OIL); self.bLit = true; if let Ok(player) = call.player() { self.LastFedBy = player as i64; } Ok(FeedBeaconReply { Oil: self.Oil }) } } ``` Each method gets: - **`self`**: the Server Object's state, to read and change. - **`ctx`**: the platform's context for this Server Object. `ctx.key` is its Instance Id, and `ctx.log` writes to the [logs](/exec/operations#logs). - **`call`**: who is calling. `call.player()` is the calling player's user id, or an error when the caller is not a player (a developer tool or other server code). - **`params`**: the function's inputs, already decoded into their struct (`FeedBeaconParams`). A function with no inputs has no `params` argument. It returns its outputs' struct (`FeedBeaconReply`), or `Ok(())` for a function with no outputs. To refuse, return `Err(Error::new("..."))`. The state is then put back exactly as it was before the call, nothing is published, and the caller's `OnDone` sees `ServerError` with your message as its `Reason` (see [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code)). So a function can change fields as it goes and still refuse halfway. Do not start your own messages with `unknown_method`, `denied` or `bad_params`: those words mean the generated code refused the call. The beacon never trusts the amount it is offered: the params are a request, and the reply says what the server did with it. ### The optional hooks `on_timer`, `on_topic` and `on_presence` do nothing unless you implement them in the same `impl`. They are the platform's hooks, described on [Timers, subscriptions and presence](/exec/timers-and-presence): `on_timer` runs when a timer your code set with `ctx.timer_after` or `ctx.timer_every` comes due, `on_topic` when another instance publishes on a topic your code subscribed to (the messages of a [Listen To](./access-members-and-timers.md#listen-to-other-server-objects) type reach their own `on__` function instead; see [Listen to a message](#listen-to-a-message)), and `on_presence` only on the app's root hub. A hook changes the state the same way a Server Function does: its changes reach watchers, and an `Err` puts the state back. ### A record per entity For a type whose **Holds** is **A Record per Entity**, `Functions` is implemented on `Records`, not on the state struct, and the state struct (`RaiderState`) is one record's values. `Records` has `get`, `get_mut`, `insert`, `remove`, `set_owner`, `set_team`, `ids`, `len`, `owner`, `team`, `owned_by` and `Records::area_of(chunk_x, chunk_y)`. A function that returns an error puts back only the records it touched. The names `changes` (for a function) and `crowdy_flush` (for a timer) are reserved. See [Write the server code](./keep-a-record-per-entity.md#write-the-server-code). ## Send a message A type that defines [messages](./create-a-type.md#server-messages) gets a `messages` module in `lib.rs`: one function for each message, `send_` followed by its server name, that takes the payload (no argument for a message with no values), and one constant for each, its topic in capitals, such as `messages::BOSS_DEFEATED`. Call it from any Server Function, timer or hook of the type. The shared boss announces its fall from `hit`, and its return from `respawn`. Add `messages::send_boss_defeated` to the `Hit` code from [Shared boss fight](./examples/shared-boss.md#the-server-code), and a `BossRespawned` message with no values for the second: ```rust fn hit(&mut self, ctx: &Ctx, call: &Call<'_>, params: BossHitInputs) -> Result { // ... clamp, count, set the phase, as before if self.Health == 0 { timers::start(ctx, timers::RESPAWN)?; messages::send_boss_defeated(&BossDefeatedMessage { Winner: self.LastHitBy, Hits: self.Hits })?; } Ok(BossHitOutputs { Health: self.Health, Hits: self.Hits }) } fn respawn(&mut self, _ctx: &Ctx) -> Result<()> { if self.Phase != BossPhase::Defeated { return Ok(()); } // ... restore the health, as before messages::send_boss_respawned()?; Ok(()) } ``` `BossDefeatedMessage` is the name the generated code gives a payload added in the asset: the message's name plus `Message`. A payload taken from a struct uses the struct's name without its `F`, so `FBossDefeated` is `BossDefeated`. What a send does: - **It waits for the function to succeed.** The message is queued and published only when the function returns `Ok`, after the change to the watched variables has been published, and in the order your code sent them. A function that refuses, or whose change is not kept, sends nothing, the way it publishes nothing and starts no timer. - **It reaches everyone listening.** Every player who listens to this Server Object hears it, and so does every Server Object that listens to it. Nobody is named: the message cannot be aimed at one player. - **It is not kept.** Nothing is saved or replayed. A listener that is not listening, or whose connection is closed, at that moment misses it. - **A message that cannot be sent returns an error.** The payload may hold nothing a client cannot read and at most 1 MiB and 65,536 list, set and map entries, and the error names the message, such as `the change was not kept: message boss_defeated: it is larger than 1 MiB`. With `?` the function fails and sends nothing; without it the message is simply not sent. ## Listen to a message A type listens to another type's messages when the other type is in its **Listen To** list, and its code then chooses which Server Object to listen to. The village beacon lights up when the Hollow Giant falls. On `CSO_VillageBeacon`, add `CSO_Boss` under **Listen To**, add a function `FollowBoss` with **Callable By** on **Developers Only** and one String input, `BossId`, and generate the server code. The generated code adds, for each message `CSO_Boss` defines, a handler to the `Functions` trait and two functions to a `listen::boss` module: | Generated | For `BossDefeated` | |---|---| | The handler in `Functions` | `on_boss_boss_defeated(&mut self, ctx, key, payload)`: the type's name and the message's server name, joined | | Start listening | `listen::boss::boss_defeated(ctx, key)` | | Stop listening | `listen::boss::stop_boss_defeated(ctx, key)` | The handler gets the Instance Id of the Server Object that sent the message as `key`, and the payload decoded into the sender's payload type, which `calls::boss` holds. A message with no values has no `payload` argument. The beacon's code: ```rust fn follow_boss(&mut self, ctx: &Ctx, _call: &Call<'_>, params: FollowBossParams) -> Result<()> { listen::boss::boss_defeated(ctx, ¶ms.BossId) } fn on_boss_boss_defeated(&mut self, _ctx: &Ctx, _key: &str, payload: calls::boss::BossDefeatedMessage) -> Result<()> { self.Oil = MAX_OIL; self.bLit = true; self.LastFedBy = payload.Winner; Ok(()) } ``` Deploy, then select `FollowBoss` in the Server Object panel and press **Call as Developer...** to call it on the beacon `oakford` with `boss-1`, the boss's Instance Id; see [Call as Developer](./deploy-with-server-compute.md#call-a-function-as-the-developer). From then on, each time boss `boss-1` falls the beacon refills, lights and credits the player who landed the last hit, and every player watching the beacon sees the change. What listening does: - **It lasts.** A listen is saved with the Server Object and renewed whenever it starts again, so call `listen::...` once, not every time it starts. `stop_boss_defeated(ctx, key)` ends it. A Server Object can listen to at most 256 messages at once. - **It takes effect when the function succeeds.** Like a message sent, a listen or stop is applied after the function's change is kept. If the platform refuses one, the function fails with `could not listen to boss.boss_defeated:` followed by the reason, its state is put back, and any listen the same call already made is undone. - **The handler is a handler like any other.** Its changes reach every watcher, an `Err` puts the state back, and it can send messages of its own, start timers, listen or stop. A payload that cannot be decoded fails the handler with `bad_message: boss.boss_defeated:` and the reason. - **One handler for each message.** The handler's name must not be the name of a function or timer of the type, or of another handler; Generate refuses it, naming the clash, and you rename one of them. A message whose server name cannot be a Rust name is refused too; set another Server Name for it on the type that sends it. Any other topic the Server Object receives still reaches `on_topic`. - **Only from a function, timer or hook.** Starting or stopping a listen anywhere else fails with "listening can only be started or stopped from a handler". The type that sends does not know who listens. It sends the same message whether nobody, a hundred players or three Server Objects hear it. ## A Server Function Library The server code of a [Server Function Library](./server-function-libraries.md) is generated the same way, with the same four files, but it has no state to hold. `lib.rs` implements the platform's `Spoke` instead of `Hub` and has no snapshot, no watched values, no saving, no timers, no sessions, no messages, no members, no cooldowns and no instance check. Your functions go on a struct with no fields that the generator names `Library` (`LibraryType` when one of your structs is already called `Library`), and the loot roller's `RollLoot` is: ```rust impl Functions for Library { fn roll_loot(&mut self, _ctx: &Ctx, _call: &Call<'_>, params: LootRollInputs) -> Result { // ... turn the seed and the tier into an item, as in Loot for the Hollow Giant } } ``` It is the signature of a Server Object's function without the state: `self` is the empty `Library`, `ctx` is the platform's context, `call` says who is calling, `params` holds the decoded inputs, and an `Err` is the caller's `Reason`. The full function is in [Loot for the Hollow Giant](./examples/hollow-giant-loot.md#the-server-code). What the generated code still does for you, before your function runs: **Callable By** (**Players**, **Server Only** and **Developers Only**; **Members** and **Leader** are refused when you save), **Required Feature**, decoding the inputs, the **Value Range**, and the limits on what a reply may hold; see [What the generated code does for you](#what-the-generated-code-does-for-you). A library keeps nothing, and the platform runs each instance in several copies with their own memory. So: - **Do not keep a value in a variable and expect it on the next call.** It may be there or not, and another instance never has it. If the answer has to be remembered, give it to a Server Object. - **`ctx.key` is the name of the instance that answered,** such as `r0` or `r1`, and not an Instance Id. Do not compare it with anything; it is useful in a log line. - **No timers, no listening and no platform events.** `ctx.timer_after` and `ctx.timer_every` are refused when the code runs, and so is subscribing to a topic, so a library cannot use `timers::`, `listen::` or `messages::`; it has no `on_player_joined` or `on_player_left`. ### A library that changes a Server Object A library changes game truth by calling a Server Object. List the object's definition under **Can Call** on the library and call it with the typed helper, which takes the object's Instance Id like any call between types. This function turns coins into oil and feeds the village's beacon: ```rust fn donate(&mut self, ctx: &Ctx, _call: &Call<'_>, params: DonateParams) -> Result { let oil = (params.Coins / 10).max(1); let fed = calls::village_beacon::feed_beacon(ctx, ¶ms.Village, &calls::village_beacon::FeedBeaconParams { Oil: oil })?; Ok(DonateReply { Oil: fed.Oil }) } ``` The library keeps no oil: the beacon does, and everyone watching the beacon sees it change. As for any call between types, an error comes back with the type and function in front, and what the beacon already changed stays changed; see [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). ### A Server Object that calls a library Put the library's definition under **Can Call** on the Server Object, or on another library. A library has one instance, so its typed helper takes no key: ```rust let roll = calls::loot_roller::LootRollInputs { Seed: self.LastHitBy, Tier: 3 }; let loot = calls::loot_roller::roll_loot(ctx, &roll)?; ``` The first call to a library that has been idle can take over a second and fail with "call deadline passed" although the library may still finish; see [A call to a library from server code fails](./troubleshooting.md#a-call-to-a-library-from-server-code-fails). To call a type that is not in the list, tick **Can Call Any Type** and use `calls::any`; see [Call any type](./access-members-and-timers.md#call-any-type). ## What the generated code does for you `lib.rs` handles everything the definition asset already says, so `logic.rs` holds only your rules: - **Watched values.** It answers the SDK's request for the watched values, and after every Server Function or hook that succeeds it compares the watched fields with what they were before and tells every watcher about the ones that changed, all in one notice. A change to a field that is not watched, like `LastFedBy`, sends nothing. - **Records.** On a type that keeps a record per entity, it collects the changes of the records, sends them every **Send Changes Every** and answers each player's request for the records they may see, with the Owner Only and Team Only variables left out for everyone else; the refusals it adds are `records_full` (past **Max Records**) and `bad_record` (an id or team that is empty, too long or not allowed, or a record that does not exist). - **Owner Only.** On an Owner Only type, a player may read or call a Server Object only when its Instance Id is that player's user id. An Instance Id that is not a user id written plainly in digits (`007`, `abc`) starts no Server Object at all. - **Values every client can read.** An Unreal client refuses some values: - a number that is not finite; - a list, set or map of more than 4,096 entries, or more than 65,536 entries in all the lists, sets and maps of one message; - a text over 65,535 bytes, or a message over 1 MiB; - two map keys or set values Unreal counts as one (it ignores the case of the letters A to Z, and reads an empty name as None); - a text in an `FGuid` field that is not a GUID, a name or gameplay tag over 1,023 characters, a soft object or class path that is not an object path; - a date outside the years 1 to 9999, or a time span longer than Unreal holds. Params holding one are refused with `bad_params`. A change that would put one into a watched field, or a reply holding one, is not kept: the state goes back to what it was and the call fails with a message starting `the change was not kept`. This keeps one bad call from breaking the Server Object for everyone watching it. The starting values of a new Server Object are checked too, and a bad one refuses to start it (`bad_seed`). A value that is already in the state, for example one loaded from a save made by older server code, makes reads fail with `unreadable` and the field at fault: players' Server Objects wait in Connecting, with that reason, until your server code replaces the value. Only the client knows which gameplay tags exist and which content roots are mounted, so keep tags and paths valid in your code. - **Callable By.** A player may call only functions their **Callable By** allows: **Players**, **Members** for a member, **Leader** for the leader; a **Server Only** function refuses a player's call with `denied`. Developer tools and other server code may call every function. - **Members, Cooldown, Value Range, timers and Can Call.** The settings on [Access, members and timers](./access-members-and-timers.md) are enforced here, in order: who may call, the params, the **Value Range**, the **Cooldown**, then your function. Members, timers and calls to other types reach `logic.rs` as the `members`, `timers` and `calls` names listed under [For logic.rs](./access-members-and-timers.md#for-logicrs). - **Refusals.** An unknown function is refused with `unknown_method`, a call the checks above do not allow with `denied` or `cooldown`, and params that cannot be read as the function's inputs, or are outside a **Value Range**, with `bad_params` and the field at fault. These are the reasons listed on [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code). - **Messages and listening.** It queues each `messages::send_...` and each `listen::...` call your code makes and applies them only when the function succeeds, after the change to the watched values is published; it sends the messages in order, and it routes each message the type listens to into its `on__` handler. See [Send a message](#send-a-message) and [Listen to a message](#listen-to-a-message). - **Saving and loading.** It saves the whole state on the definition's Save Interval and loads it when the Server Object starts again. For a change that should not wait for the next save, call `ctx.persist_now()` from your function. ## After you change the definition Generate the server code again after any change to the definition asset or to a struct it uses, then [build and deploy it](./deploy-with-server-compute.md). `Cargo.toml`, `types.rs` and `lib.rs` are rewritten to match; `logic.rs` is not. - **A new Server Function, timer, event or listened message** (a message added to a type in your Listen To list) appears in the `Functions` trait, and the crate does not compile until `logic.rs` has its method. After writing the other files, Generate Server Code compares `logic.rs` with the trait and, when a method is missing, says which and asks: "Add empty versions of the missing functions?" **Yes** adds each one just before the closing brace of your `impl Functions for ` block, the way the first generation writes it: a Server Function refuses with "not written yet" until you write it, and a timer or event does nothing. The question ends "This writes them into logic.rs on disk. If another program has it open with unsaved changes, save or close it there first, or its next save removes them." **No**, the default, leaves the file as it is. If the file has no such block, is not UTF-8 text, or the block uses a macro such as `my_macro!(...)` that may define some of the functions, nothing is added and the empty versions are copied to the clipboard for you to paste. If the file changes while the question is open, for example saved from another editor, Yes adds the stubs to the file as it is then, or, when they no longer fit, adds nothing and asks you to generate again. If an open Server Code tab has unsaved edits to the file, Generate leaves it alone and the tab's [Add Stubs](#the-server-code-tab) line shows what is missing. - **A renamed or removed Server Function, timer or event** leaves its old method in `logic.rs`, which no longer compiles against the trait. Generate Server Code names it ("logic.rs defines join, which this type no longer has: rename or delete it.") and never deletes it: rename it to the new name or delete it yourself. A rename is offered as a missing function plus a left-over one, so move your code from the old method into the new one. - **A renamed variable, input or output added in the asset** is noticed, since each one keeps an id of its own. Before it writes anything, Generate Server Code lists each one, such as "Gold was renamed to Coins (VillageBeaconState)", and asks: **Yes** keeps the old names on the server, so saved values carry over and `logic.rs` keeps using `Gold` (it adds a [List Value Names](./create-a-type.md#every-setting) entry for each); **No** renames them on the server, and saved values under the old names are dropped; **Cancel** stops. Server code generated by an older SDK has no ids, so the first generation after updating reports a rename as the value being gone. - **A removed or renamed field or enum value** is something your saved Server Objects may still hold. Before it writes anything, Generate Server Code compares the new types with the ones already in `types.rs` and, if a struct, field or enum value is gone or a field changed type, lists each one (for example `VillageBeaconState.Oil is gone`) and asks whether to regenerate anyway. Answer **No** and keep the old name on the server with a Server Name under **Field Names** or **Enum Value Names** on the definition, or keep the value instead of deleting it. See [Changing a type later](./create-a-type.md#changing-a-type-later) for what each change does to existing data. - **Code in `logic.rs` that used a removed field** stops compiling; fix it there. - **A new type name** (the definition's Type Name) goes into a new folder, `Server//`. Each generated `Cargo.toml` records the definition asset it was generated for, so Generate Server Code finds the folder it wrote under the old name and asks: **Yes** moves `Server//` to `Server//`, keeping your `logic.rs` and `revisions.json`, then generates; **No** generates into a fresh folder with a new `logic.rs` and leaves the old one as it is; **Cancel** stops. A folder generated for another asset, such as the one you duplicated this definition from, is never offered. If you move or rename the definition asset in the Content Browser, saving it updates the record in its crate's `Cargo.toml`, so a later Type Name change still finds the folder. Either way, to the server it is a new type, so Server Objects saved under the old name are not carried over (see [What a deploy records](#what-a-deploy-records)). A Type Name another definition already uses is refused. ## Keep it in version control The crate folder is source: commit `Server//` with the rest of your project, `logic.rs` above all, and `revisions.json` with it (see the next section). Building it locally creates a `target/` folder inside it; leave that out, for example with a `target/` line in your ignore file. ## What a deploy records The platform keeps only a fingerprint of what it built for each type (its build digest), never the code. So every deploy from the editor or the command line writes a record beside each type's crate, `Server//revisions.json`, and that record is what lets the editor tell you what changed. Per deployed revision of the type it holds: - the files as they were sent: `Cargo.toml` (without the definition asset it records, which stays on your machine) and the `src` files, with your own file's code as `logic.rs` if you use one, - a fingerprint of those files that ignores line endings and spaces at the end of a line, so opening a file in an editor that changes them does not make a new revision, - the versions that ran it and the platform's build digests for it, and - when it was deployed. Deploying the same code again does not add a revision: it moves the existing one to the top and adds the version. The newest revisions are kept, as many as **Revisions to keep** on the [Settings tab](./deploy-with-server-compute.md#settings) says, 5 by default. The file is written the same way on every platform, so it only changes in version control when a revision does. A type's state on the [Server Compute](./deploy-with-server-compute.md#overview) page is worked out from it. The platform tells the editor which build digest the live version runs for the type. If `revisions.json` has a revision with that digest, the type is compared with it: the fingerprint of the crate on disk now against the recorded one, and Save every and Stop when idle for against the live version's. Same means **No changes**; different means **Changed**, naming what differs (server code, save interval, idle timeout). If no revision has that digest, the type reads **Live code unknown**: the live version runs something this project has no record of. If the live version's manifest cannot be read at all, every type reads **Live code unknown**, never **No changes**. **Commit `revisions.json` with the crate**, so your teammates see the same states you do. If a teammate deploys and you do not have their `revisions.json` yet, the type reads **Live code unknown** on your machine until you do, and Deploy's confirmation says it replaces live code you have no record of. A `revisions.json` that cannot be read (someone edited it, or a merge left markers in it) is reported in the editor's log as "... is not a deploy record; fix or delete it", and its type reads **Live code unknown**. Deleting it forgets that type's history. The next deploy cannot add to a file it cannot read: it still deploys, then says "Deployed as version 8. Its record could not be saved, so its changes cannot be compared: ..." in the warning color, and the command line lists the same problems as `recordErrors`. Fix the file and deploy again. **Deploy starter pack** records nothing, since it does not send your code. ## Build and deploy it Open Crowdy Studio, choose **Server Compute** in its navigation, and press **Deploy** in its title row, or press **Deploy** in a definition asset editor's toolbar. Deploy is offered only when something changed since the live version; see [Server Compute](./deploy-with-server-compute.md#deploy). The editor sends every Server Object type in the project to the platform, builds it there (no Rust toolchain needed on your machine), and deploys the result as your app's new active server code. The same page shows the build output, the versions you can make active again, and the logs and call activity of what is running, and `-run=CrowdyServerCompute` does it from the command line for CI. See [Server Compute](./deploy-with-server-compute.md). A Server Object acquired before its type is deployed waits in Connecting and comes up once it is; see [Waiting is not failing](./troubleshooting.md#waiting-is-not-failing). ### By hand The platform's own build and deploy do the same work without the editor, for a setup the Server Compute page does not cover: 1. **Build.** Send the crate's `Cargo.toml`, without its `[package.metadata.crowdy]` section (the platform's build accepts only `[package]`, `[lib]` and `[dependencies]`), and its three files under `src/` to the platform's build; see [Builds and starter packs](/exec/builds#building). CrowdyJS and CrowdyCPP wrap it: see [From an SDK](/exec/builds#from-an-sdk). You can also build locally with `cargo build --release --target wasm32-unknown-unknown` and deploy the module yourself; see [Deploying](/exec/intro#deploying). 2. **Deploy.** Give the type an entry in your app's [manifest](/exec/intro#the-manifest), named by its Type Name, and deploy the build with it; see [Deploying a build](/exec/builds#deploying-a-build). A deploy replaces the app's whole manifest, so it lists every type your app runs. The beacon's entry, as the editor writes it under its `root` type: ```json "village_beacon": { "kind": "hub", "parent": "root", "crate": "village_beacon", "client": true, "persist_every_ms": 30000, "evict_after_ms": 300000 } ``` `client` must be `true` for players to reach it. `persist_every_ms` and `evict_after_ms` are the definition's **Save Interval Seconds** and **Idle Timeout Seconds** in milliseconds. A [Server Function Library](./server-function-libraries.md)'s entry has `"kind": "spoke"` and `replicas`, `max_replicas` and `concurrency` from its **Scaling** instead, and neither of those two. Building and deploying need your organization's `manage_compute` permission. [Operations](/exec/operations#instances-and-versions) explains how to move a running Server Object to new code at once, and where a crashed call's log is. ## Related - [Deploy it with Server Compute](./deploy-with-server-compute.md) - [Create a Server Object type](./create-a-type.md) - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md) - [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md): what a player's game does with the messages sent here - [Make a Server Function Library](./server-function-libraries.md): the asset behind [A Server Function Library](#a-server-function-library) - [Troubleshooting](./troubleshooting.md#refusals-from-your-server-code): the refusals this code produces - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) - [ck-exec overview](/exec/intro): hubs, the manifest, and the `ckx-sdk` your code runs on # Deploy it with Server Compute Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/deploy-with-server-compute # Deploy it with Server Compute :::note Server Objects run on [ck-exec](/exec/intro). ::: Once a type's [server code](./write-server-logic.md) is generated and its `logic.rs` written, the server still has to run it. **Server Compute** is the page in [Crowdy Studio](../studio/overview.md) that builds your project's server code on the platform, deploys it to your app, and then lets you watch it work: which version is live, what it logged, how players' calls to each Server Function went, and a switch to stop one type if it misbehaves. There is nothing to install: the build happens on the platform, so no Rust toolchain is needed on your machine. ## Open the page Open Crowdy Studio (**Tools, Crowdy SDK, Crowdy Studio**) and choose **Server Compute** under **COMPUTE** in its navigation. It uses the sign-in and the app you already set in Studio: [sign in](../studio/sign-in.md), then pick your app on the [Project page](../studio/projects-and-apps.md). If you are not signed in, or no app is set, the page says so ("Sign in to Crowdy Studio first" or "Set the app in Crowdy Studio first", then "Sign in and choose an app on the Project page.") and shows a **Try Again** button; fix it in Studio and press that. The header holds the title, the line "Deploy your Server Objects' server code, then watch how players' calls to it go.", a line naming the app and backend ("App 12345 on Dev"), one status badge, and **Refresh**. The badge shows the first of these that applies: | Badge | Meaning | |---|---| | **Budget paused** | The app's Server Compute is paused by billing, so players' calls are refused until it is resumed. Hover it for the reason the platform gives, such as `insufficient_funds` or `spend_cap`. | | **Switched off** | The app's Server Compute is switched off, so players' calls are refused. A **Switch on** button sits beside the badge. | | **Nothing deployed** | The app has no active server code yet. | | **Live: version 7** | Version 7 is the app's active server code. | Beside the badge a small line shows how much of the app's room the Server Objects use in this datacenter, for example "3 of 16 Server Objects · 192 of 1,024 MB": how many are placed against the app's limit, and the memory they reserve (each reserves its type's **Memory per Instance**, see [Server Limits](./create-a-type.md#every-setting)) against the app's memory limit. Both limits come from your organization's funding: an app with no funds can run 16 Server Objects at once within 1 GB, a funded app 16,384 within 64 GB. Past either, no new Server Object starts until some stop, and a call to one that is not running is answered **Unavailable** with the platform's reason. Each running instance of a [Server Function Library](./server-function-libraries.md#scaling) counts as one. The line is missing when the platform does not report it. Hover a badge for a one-line explanation. **Refresh** reads the app's status, versions, logs and activity again and checks the project's Server Object types. If you change the app in Studio, the header line adds "The project's app has changed since; press Refresh to show it." The page has five tabs: **Overview**, **Versions**, **Logs**, **Activity** and **Settings**. Building, deploying, making a version active and switching on or off need your organization's `manage_compute` permission; reading needs `view_compute_diagnostics`. A problem from the platform, such as a missing permission, is written in red under the tab it came from, for example "Could not read the app's status: ..."; a busy server is retried for you first. ## Overview The left side lists every definition asset in the project, sorted by Type Name, then any type that is on the server but no longer in the project, each with its Type Name and **one badge**. **Search types** narrows the list by name, and the tabs above it filter it: **All**, **To deploy** (types that are new, changed, running code not recorded here, or removed) and **Problems** (types whose code cannot be built, or with a Server Object the platform holds back). Click a type to show it on the right; the first type is shown when the page opens. The right side shows the selected type: its Type Name (hover it to see its asset), its badge, the running switch and a **...** menu, then the type's description, if its [definition](./create-a-type.md#every-setting) has one, and a small line with how many functions it has, how many variables are visible to players ("2 variables visible to players"), who can read it ("readable by every player", "readable by its members", "only its owner can read it", or "closed to players: only server code and developer tools reach it" for [a type players cannot reach](./access-members-and-timers.md#a-type-players-cannot-reach)) and, once the Activity numbers are in, how many calls it had over the Activity tab's period, for example "1.2k calls in the last hour", and how many of its Server Objects are running or starting now ("2 running"). The parts are separated by dots. A [Server Function Library](./server-function-libraries.md) says "Server Function Library" first, and in place of the variables and who can read it says how many instances it runs, "1 to 4 instances" for a Min Instances of 1 and a Max Instances of 4, or "4 instances" when the two are equal. When the platform will not place one of the type's Server Objects, for example after it crashed again and again, a red line under it says which and why: "village is held back: ...". Last, a box says what the badge means for this type, such as "Not live yet. Deploy to add it." or "Changed since version 7: server code. Deploy to make it live." ![The Server Compute page: the deploy strip under the tabs with Add starter and Deploy starter pack, five of the project's Server Object types on the left, and the selected type's details on the right](/img/unreal-sdk/server-compute-overview.png) The badge is the first of these that applies. What stops the type's server code from being built comes first: | Badge | Meaning | |---|---| | Needs generating | There is no folder for the type yet, `logic.rs` is missing, or the type uses [My Own File](./write-server-logic.md#use-your-own-logic-file) and no file is chosen or the file is missing. The reason is written under the Type Name. | | Out of date | The definition or a struct it uses changed since the code was generated. Regenerate it with **Generate**. The reason names the file and the first line that differs, for example `... no longer matches the definition (line 7: the file has "...", Generate writes "..."); regenerate it with Generate Server Code`; a long line is cut after 80 characters. | | Problem | The definition fails its checks, or its Type Name is `root`, the name of the platform's top-level type ("root is the name of the platform's root Server Object type; choose another Type Name"), or starts with `ck_staged_`, which is kept for the stand-ins of [a deploy in parts](#a-deploy-in-parts) ("Type Names starting ck_staged_ are kept for the stand-ins a deploy in parts stores its builds with; choose another Type Name"). The reason is written under the Type Name. | Once the type's code is fine, the badge says how the type compares with the version that is live now: | Badge | Meaning | |---|---| | No changes | The live version runs this code with these settings. Hover: "Live in version 7 exactly as it is here". | | Changed | The live version runs another revision of the code, or other settings. Hover: "Changed since version 7: server code, save interval. Deploy to make it live." It can list server code, save interval, idle timeout, platform data (the [Platform Data](./access-members-and-timers.md#platform-data) settings), player access (whether [players can reach the type](./access-members-and-timers.md#a-type-players-cannot-reach)), server limits (the [Server Limits](./create-a-type.md#every-setting) settings), kind (the type is a Server Object live and a [Server Function Library](./server-function-libraries.md#switch-kind) here, or the reverse), scaling (a library's [Scaling](./server-function-libraries.md#scaling) settings), and, for a [Game Kit starter](./use-game-kit-starters.md#change-a-starter), config and parent. Limits are compared the way the platform applies them: a value out of range counts as the nearest value in range, and a limit written as a string in a manifest from another tool is read as a number, so such a manifest no longer shows "server limits" as changed on every check. | | New | The live version does not run this type: "Not live yet. Deploy to add it." | | Live code unknown | The live version runs code this project has no record of, for example deployed from another copy of the project. "Deploying replaces it." See [What a deploy records](./write-server-logic.md#what-a-deploy-records). | | Ready | The type's server code is fine, but it has not been compared yet, because the app's versions have not been read (you are not signed in, or the read failed). Press **Refresh**. | A type that the live version runs and the project no longer has is listed with a **Removed** badge ("On the server, not in this project"); selected, it shows its name, the running switch, and the line "On the server, not in this project. Your next deploy removes it." A project with no types left can still deploy: the deploy sends the Crowdy SDK's own root alone (see [Deploy](#deploy)), which removes every type from the server. The definition asset's editor shows the same state for its type in the status readout at the right of its toolbar, with **Changed since version 7** and **Not deployed yet** in place of Changed and New; see [The asset editor](./create-a-type.md#the-asset-editor). Its **Deploy** dropdown has an **Open Server Compute** entry that brings you to this page. The page does not regenerate code for you. Regenerate from the Content Browser or the asset, then open the tab again or press **Refresh**. With no definition assets it says "No Server Object types yet." and "Create a Server Object Definition asset to add one." Once the app's status is known, a type the live version runs also says **Running** or **Switched off**, with a **Switch off** or **Switch on** button (see [Switching a type or the app off and on](#switching-a-type-or-the-app-off-and-on)), and a running one has a **Restart** button before it (see [Restarting a type](#restarting-a-type)). A type that is not live yet has nothing to switch, so it shows neither. ### Remove a type The **...** menu on a type has two entries. **Open definition** opens the type's definition asset. **Remove from project...** takes the type out of the project: it deletes the definition asset and, when the folder is the type's own, its `Server//` folder. It is disabled while another operation runs, and it asks first, in a dialog titled "Remove village_beacon": > Remove village_beacon from this project? > > This deletes its definition /Game/Beacon/CSO_VillageBeacon and its Server Code folder Server/village_beacon. > > village_beacon keeps running on the server (App 12345 on Dev) until your next deploy, which removes it. The confirm changes with the situation: - **What it deletes.** Everything in the type's `Server//` folder goes with it: `logic.rs`, the generated files and `revisions.json`, so the type's revision history goes too. Get them back from version control if you need them. A logic file of your own that lives outside that folder is never touched. - **What it leaves, and why.** The folder is left alone, and the dialog says "This deletes its definition ... only." followed by the reason, when deleting it could hurt something else: "Another definition has the same Type Name and so the same Server Code folder, which is left as it is."; "Its Type Name is not a valid one, so its Server Code folder may be another type's; it is left as it is."; "Its Server Code folder is not directly inside the project's Server folder, so it is left as it is."; "It has no Server Code folder."; "Its Server Code folder holds server code the Crowdy SDK did not generate, so it is left as it is."; or "Its Server Code folder is, or holds, a link to somewhere else, and deleting it could delete what the link points to; it is left as it is, so delete it by hand." A folder that could not be deleted is reported in red under the tab: "Removed village_beacon's definition, but could not delete its Server Code folder Server/village_beacon. Delete it by hand." - **Unsaved code edits.** If the type's code has edits you have not saved, the dialog lists the files: "Its unsaved code edits are lost:". - **What it does on the server: nothing yet.** A type that is live keeps running, with players calling it, until your next deploy, which no longer includes it and removes it (the deploy's confirm says "It removes village_beacon, which this project does not have; their Server Objects stop."). Until then it shows as a **Removed** row, where you can still switch it off. The deploy stops its Server Objects and keeps their saved state, and the type's switch goes with it: a later deploy that adds a type of the same name starts those Server Objects from what they saved, switched on. A type that was never deployed ("New") says "village_beacon is not live on the server (App 12345 on Dev), so nothing changes there." - **The last type.** Removing the project's last type is no different: the next deploy sends the Crowdy SDK's root alone and removes the type from the server. To stop players' calls to it before then, use **Switch off** on this page. After you confirm, the editor's own delete runs, with its usual check of what still refers to the definition, for example a Blueprint slot holding it; it may ask again, and if you cancel there nothing else happens. If the row went stale (the asset moved, or its Type Name changed since the list was read) nothing is removed and the page says "/Game/Beacon/CSO_VillageBeacon is no longer the definition of village_beacon, so nothing was removed. The list is read again; try once more from it." For a build server or a script, `-op=removetype` does the same from the command line; see [From the command line](#from-the-command-line). ### Deploy **Deploy** sits in the page's title row, beside **Refresh**, so it stays in reach however long the list of types grows. It shows how many types the deploy adds, changes or removes ("Deploy 3"), reads **Up to date** when there is nothing to deploy, and **Working...** while an operation runs. Under the tabs, a strip holds the status line, **Add starter** (see [Use the Game Kit starters](./use-game-kit-starters.md#add-a-starter)) and **Deploy starter pack**; before anything is known it says "Deploy builds every type's server code on the server and makes it the app's live version. Players use it at once." **Generate** sits between **Refresh** and **Deploy**. It reads "Generate 3" while 3 types are out of date or have no server code yet, and it is enabled only then, and only while no other operation runs. Game Kit starters are not counted, and neither are types that need you, such as a [My Own File](./write-server-logic.md#use-your-own-logic-file) type with no Logic File. Its tooltip reads "Generates the server code of every type that is out of date or has none yet, asking what a single Generate asks, then saves the definitions it changed." It generates those types one after another through the same Generate as a definition's own button, so it asks each question that Generate asks (see [Generate it](./write-server-logic.md#generate-it)), and a cancel stops the run. It then saves the definitions it changed, refreshes, and shows a notice of what was written and what was not, and why. **Deploy is offered only when something changed.** Once every type's code is fine and the live version is known, the status line says what a deploy would do, and **Deploy** is enabled only if there is something to do: - "Since version 7: 1 changed, 1 new. Deploying sends the project's 2 types." Any of "changed", "new", "running code not recorded here" (the live version runs something this project has no record of) and "removed" can appear, each with its count. - "Nothing changed since version 7." **Deploy** reads **Up to date** and cannot be pressed. - "Nothing is deployed yet. Deploying sends the project's 2 types." for an app with no active version. - "Ready to deploy 2 Server Object types." when the live version could not be compared, for example because it has not been read yet; press **Refresh**. - "This project has no Server Object types, so deploying sends only the Crowdy SDK's root (Players Online)." when the project has no types. A project like that can still deploy: the root goes alone, and any type the app ran before is removed. A change you have just deployed keeps its "Deployed as version 8." line until something changes again, then the summary takes over. A failed or half-recorded deploy keeps its message until the next deploy. **Deploy** is disabled while any type is not fine, and the line says what to do: "Fix the 1 type marked Problem first." or "Generate Server Code for 1 type first (right-click the definition in the Content Browser)."; two definitions with one Type Name show "Two Server Object types are named village_beacon (...) and (...); each needs its own Type Name". A type whose `Server//` folder holds a crate the SDK did not generate is marked **Problem** with "... holds server code the Crowdy SDK did not generate; choose another Type Name, or move that folder" (see [Generate it](./write-server-logic.md#generate-it)). It is also disabled while another operation runs. The same rules apply to the **Deploy** button in a definition asset editor's toolbar. **Deploy** asks first, in a dialog titled Deploy. The first sentence is always there, then one sentence for each kind of change that applies, naming the types, with what it removes first: > Deploying replaces the active server code of App 12345 on Dev with this project's 2 Server Object types. > > It removes tip_jar, which this project does not have; their Server Objects stop. Their saved state is kept and is used again if you deploy them again. > > It adds lantern. > > It changes village_beacon (server code, save interval). A type whose kind changes also gets a sentence of its own, "loot_roller changes from Server Object to Server Function Library: its saved data is kept, unused, and comes back if it becomes a Server Object again." or, the other way, "loot_roller changes from Server Function Library to Server Object: it starts from the saved data it had as a Server Object, if any." A project with no Server Object types gets "Deploying replaces the active server code of App 12345 on Dev. This project has no Server Object types, so only the Crowdy SDK's root (Players Online) is deployed." as its first sentence. The other one is "It replaces live code this project has no record of in village_beacon." When what the app runs now could not be read, the dialog says "What the app runs now could not be read, so what this changes is not known." instead. Read the list before you answer Yes: it is the difference between what you have here and what players are running. "It removes" names every type the live version runs that this project does not have, however it got there: a type you removed from the project, one deployed from another copy of the project, or server code deployed another way, such as a crate written by hand and deployed with the platform's API. Deploy cannot send such code, so answering Yes takes it offline. If your app runs code like that, deploy it the same way you deployed it before, not from this page. If any server code file has edits you have not saved in the asset editor's Server Code tab, the dialog adds: > 1 Server Code file has unsaved edits; Deploy sends the saved files. and lists the file names. Answer No, save them, and deploy again if you meant to send them. When the server code of some types is exactly what an earlier deploy to this app sent, the dialog adds "The server code of lantern, village_beacon is as it was deployed before, so it is not built again." When the deploy is built in parts, it adds "The server code is built in 2 parts. Each part but the last is first stored by a version that runs what runs now, so the version history gains 1 more version."; see [A deploy in parts](#a-deploy-in-parts). If the server code calls into platform data its definition does not ask for, the dialog adds "Check before deploying:" and one line for each, such as: > village_beacon: its server code calls world().set_voxels(), which needs world.write; turn on Changes the World or the platform refuses that call Answer No, tick the setting in the definition, and deploy again; see [Platform data](./access-members-and-timers.md#platform-data). The check ignores code inside Rust raw strings and inside `#[cfg(test)]` items, so a call that only appears there is not listed. The same list holds a function that cannot use what it sets: a **Developers Only** or **Server Only** function with a **Cooldown** or a **Required Feature** gets "village_beacon Refill: Cooldown has no effect, since only players' calls are limited." or "village_beacon Refill: Required Feature has no effect, since only players are checked." Only players are limited and checked, so clear the setting, or let players call the function. See [Cooldown](./access-members-and-timers.md#cooldown). Confirm, and Studio sends the platform: - the server code of **every** Server Object type in the project, exactly as it is on disk under `Server//` (a type whose Code Source is My Own File sends that file as its `logic.rs`), and - a crate named `root` that the SDK generates for you from `CSO_PlayersOnline` in its own content. It is the top-level type every Server Object type sits under on the platform. It keeps one variable, the number of players in the app's world, which **Get Players Online** reads (see [How many players are online](./from-blueprint/read-and-follow-variables.md#how-many-players-are-online)), and you never edit it. An app whose root an older Crowdy SDK deployed keeps no count, and Server Compute offers a deploy until you make one. The root is not shown as a row, but it is tracked like a type: once a deploy from this SDK has recorded it, a later Crowdy SDK whose root code differs makes Server Compute offer a deploy again. An app whose root was deployed by an earlier SDK is compared only after your next deploy. A project with no Server Object types deploys the root alone. A project with the Game Kit's `world` starter does not send this crate: `world` is the root instead, and every other type runs under it; see [The world becomes the app's root](./use-game-kit-starters.md#the-world-becomes-the-apps-root). together with an entry for each type saying whether players may reach it and what platform data its server code may use, and giving its **Save Interval Seconds** and **Idle Timeout Seconds** from the definition (the platform's [manifest](/exec/intro#the-manifest)). A Server Function Library's entry is `"kind": "spoke"` with its **Min Instances**, **Max Instances** (only when above Min Instances) and **Parallel Calls per Instance** as `replicas`, `max_replicas` and `concurrency`, and no save interval or idle timeout; a type with **Can Call Any Type** lists `"calls": ["*"]`. A deploy replaces the app's active server code as a whole; it does not add to it. That is why every type goes together: a type left out of a deploy is a type the app no longer has in its active code, until a deploy or a version that includes it is active. That is also how [removing a type](#remove-a-type) takes effect. For the example world, a project with `CSO_VillageBeacon` and a `CSO_Lantern` sends `village_beacon`, `lantern` and `root` in one deploy, even when only the beacon's `logic.rs` changed. Only the beacon is built, though: see [Server code that is not built again](#server-code-that-is-not-built-again). The status line follows the build: "Sending the server code...", "Waiting for a builder... 4 s", "Building on the server... 12 s", "Deploying...", and finally the line below. A deploy built in parts says which part it is on, "Part 1 of 2: building on the server... 12 s", and one with nothing to build goes straight to "Deploying...". > Deployed as version 7. Once the deploy succeeds, Studio also records what it sent in each type's `Server//revisions.json`, and the Crowdy SDK's root in `Server/root/revisions.json`, which is how the page tells you next time what changed; see [What a deploy records](./write-server-logic.md#what-a-deploy-records). If a record cannot be written the deploy still stands, and the line says so in the warning color: "Deployed as version 7. Its record could not be saved, so its changes cannot be compared: ..." Commit each `revisions.json` with the crate, and `Server/root/revisions.json` too. A **Build output** button appears once a build has output, and opens it in a read-only box. A failed build deploys nothing: the line says "The build failed. See the build output below." and the box opens by itself, usually with a compiler error in a `logic.rs`. Only one operation runs at a time; the buttons are disabled until it ends. Each type's server code, its crate, may have at most 64 files and 2 MB of source; the [builds page](/exec/builds#limits) has the platform's full list. A project with a crate over either shows the reason on the status line, such as "village_beacon's server code has 70 files; each type's server code can have at most 64" or "village_beacon's server code is 2.10 MB; one build takes at most 2 MB of source", and **Deploy** stays disabled. A deploy with more crates or more source than one build takes is not refused for it: it is [built in parts](#a-deploy-in-parts). A running Server Object keeps its old code until it next starts; see [Operations](/exec/operations#instances-and-versions). So after a deploy that changed a type, the card reads the app's Server Objects, and when some of a changed type are still running it adds a line and two buttons: > 3 running Server Objects of village_beacon still run the previous version until they stop when idle. Move them to the new version now? **Move now** restarts those types (see [Restarting a type](#restarting-a-type)) without asking again; **Leave them** dismisses the line, and they move when they next start. Which Server Objects ran the previous version is decided from the Server Objects read as the deploy was sent, by their epoch, not from your computer's clock, so a clock that is off does not hide or add any. ### Server code that is not built again A type's server code is not built again when its files are exactly those of a revision in its `revisions.json` that this app already holds, because one of the app's versions ran it. The deploy names that module by its digest instead, and the confirm says "The server code of lantern is as it was deployed before, so it is not built again." So a deploy that changed one type's code builds that one crate, a change of settings alone, such as a save interval, builds none, and a deploy with nothing to build sends no build at all. This needs the app's versions, so when they could not be read every crate is built. **Deploy** on the page always reuses what it can. To build every type's server code again, including types whose code did not change, so they pick up the platform's current SDK and toolchain, open the drop-down arrow beside **Deploy** and pick **Rebuild All and Deploy**. It is available even when there is nothing to deploy, and it asks to confirm first. The command line's `-op=deploy -rebuild` does the same, and both follow one rule. ### A deploy in parts The platform builds at most 16 crates and 2 MB of source at a time. A deploy with more, such as a project with many types or with the [Game Kit starters](./use-game-kit-starters.md), is built in several parts, in order, each at most 16 crates and 2 MB. It still becomes one version, and what players run changes once: 1. **Every part is built first.** A part that fails deploys nothing, and nothing on the app changes. 2. **Each part but the last is stored.** The platform keeps a build's modules only once a version has used them, so each of those parts is deployed in a staging version: the types that run now, unchanged, plus a stand-in type for each of its crates, named `ck_staged_` and the crate's name, which nothing calls and players cannot reach. The version that was active is then made active again at once. Before the first of these, the versions are read again, and if the app's active version changed since the deploy started, it stops: "The app's active version is version 9, not version 8 as when the deploy started, so nothing was deployed; refresh, then deploy again". 3. **The deploy itself** names the stored and reused modules by their digest, with the last part's build, and its version runs no stand-in. So the [Versions](#versions) tab gains one version per extra part, before the deploy's own. The confirm says how many parts there are and how many more versions they add. On an app with nothing deployed there is nothing to make active again, so a staging version stays active until the deploy. If storing a part fails, the line says "Built, but storing the builds failed: " followed by the platform's words and what is active now, such as "Version 8 is active again, as before the deploy." A deploy in parts needs to know what runs now, so when the app's versions could not be read it refuses before building anything: "This deploy is built in parts, each first stored by a version that runs what runs now, and what runs now could not be read; refresh, then deploy again". ### Deploy starter pack **Deploy starter pack** builds and deploys the platform's example types instead of your project, after asking: > Deploying the starter pack replaces the active server code of App 12345 on Dev with the platform's example Server Object types. > > It removes village_beacon, lantern, which the active version runs, unless the starter pack has a type of the same name; that type then takes over its saved Server Objects. Their saved state is kept and is used again if you deploy them again. The second sentence names what the live version runs, the SDK's `root` left out, and is missing when nothing is live. When what the app runs could not be read, it says "What the app runs now could not be read, so what this removes is not known." instead. It is meant for trying the platform out on an empty app. It replaces your project's server code just as **Deploy** replaces the previous code, so do not press it on an app your game uses; if you do, make the earlier version active again on the Versions tab. See [Starter packs](/exec/builds#starter-packs). ## Versions Every deploy makes a new version. The **Versions** tab lists them newest first: "Version 7", an **Active** badge on the live one, and a line such as "3 h ago by Ada, 2 types" (hover it for the exact time). Under it, a second line says what that deploy changed from the version before it, for example "Since version 6: village_beacon changed", or "Since version 6: lantern added" and "lantern removed" when a type came or went, several joined by dots. A deploy that changed nothing says "Same server code as version 6". Hover it: "The types whose server code or settings differ from the version before it". The platform reports each version's types, so this works for versions anyone deployed, from any copy of the project; the oldest version has no line, and neither does one whose code cannot be compared with the one before because the platform has no build digest for it. With nothing deployed the tab says "Nothing deployed yet." and "Deploy from the Overview tab." **Make active** on any other version asks first, in a dialog titled "Make version 6 active": > Makes version 6 the active server code of App 12345 on Dev. That is a rollback: if version 7 has a bug, make 6 active and players get the earlier code, while you fix `logic.rs` and deploy again. It changes no Server Object's saved values. As with a deploy, a running Server Object moves to the code when it next starts, and the Server Objects of a type the older version lacks are stopped with their saved state kept. That type's switch goes too, so a type you switched off comes back switched on when a later version runs it again. Rolling back to a version older than a definition change can leave Server Objects saved in a newer shape; read [Changing a type later](./create-a-type.md#changing-a-type-later) before you go back across one. ## Switching a type or the app off and on Each type the live version runs has a **Switch off** button on the Overview tab, beside its name when selected, or **Switch on** once the type is off. Use it when one type is causing trouble and you want it quiet while the rest of the app carries on. Switching off asks first, in a dialog titled "Switch off village_beacon": > Players' calls to village_beacon in App 12345 on Dev are refused until it is switched on again. Its Server Objects are saved and stopped. Switching on asks nothing. While a type is off, calls to it are refused and reach the client as the `Paused` outcome, which is retryable (see [Troubleshooting](./troubleshooting.md#outcomes)), and nothing of that type runs. Switch it on and its Server Objects start again, from what was saved, the next time a call reaches them. It is not the same as removing the type in a deploy: the code stays and the type stays live. To move running Server Objects onto newly deployed code, use **Restart** instead, which refuses no calls. The buttons are disabled until the app's status has loaded and while another operation runs. When the whole app is switched off, the header badge says so and its **Switch on** button, after asking "Players can call the Server Functions of App 12345 on Dev again.", lets players call the app again. The page cannot switch the whole app off; see [the kill switch](/exec/operations#the-kill-switch) for that. ## Restarting a type **Restart** on a running type moves its running Server Objects to the live version now. It asks first, in a dialog titled Restart: > Move village_beacon's running Server Objects on App 12345 on Dev to version 8 now? > > Each is saved and stopped, and starts again on version 8 at its next call. Players watching one see it read again; a call that arrives while it stops may be answered Unavailable. Nothing is refused while it happens and no state is lost: each Server Object is saved first and starts again from what was saved. Players watching one see its values read again on the new start, the way they do after a reconnect. The deploy line then says how many it stopped, for example "3 running Server Objects stopped; each starts again on the active version at its next call." Use it after a deploy, from the **Move now** button the deploy strip offers, or whenever a fix should reach Server Objects that are running now rather than when they next stop. From the command line, `-op=restart` with no `-type` restarts every type that still has an instance on older code; see [From the command line](#from-the-command-line). ## Call a function as the developer A [Server Only or Developers Only](./create-a-type.md#callable-by) function has no player to call it, so you run it by hand from the definition asset: select the function in the Server Object panel, and in its Details press **Call as Developer...**. A window titled "Call as Developer" opens with: - **Instance Id**, the Server Object to call, such as `boss-1`. A type that is **Owner Only** takes its owner's user id, and a type with **Only One Instance** always calls its one Server Object, whatever is typed. It is needed otherwise. - the function's inputs, starting from their default values, to fill in; - **Call**, and under it the answer: "Ok" and the outputs as JSON, such as `{"total":7}`, or the outcome and the reason, such as "Denied: only members may call Deposit". If the connection itself is refused it says why, for example "Unavailable: could not connect as the developer: start failed: denied: an owner-only Server Object's Instance Id is its owner's user id". The built-in member functions that only server code may call, such as **Add Member** when **Members From** is **This Object**, can be called this way too. They sit greyed out in the **Built-in** list of the Members panel, and a play button beside the row ("Call as Developer: runs this function on a live Server Object as you, the app's developer") opens the same window. The call goes to the live app with your Crowdy Studio sign-in, as you: the server code sees `Caller::Developer` with your user id, not a player, and whatever it changes stays changed. It starts the Server Object if it is not running. If the function's inputs change in the definition while the window is open, **Call** says so; close the window and open it again. ## Logs The **Logs** tab shows the lines your server code writes with `ctx.log`. For the beacon: ```rust ctx.log(Level::Info, &format!("fed {} oil, now {}", params.Oil, self.Oil)); ``` Filter by type (type a name in the "All types" box and press Enter) and by level: **All**, **Info**, **Warnings** or **Errors**. Lines come newest first, 100 at a time; **Load older** at the bottom pages back. Each line has a colored dot (hover for Error, Warning, Info or Debug), its text, and a small line such as "3 min ago, village_beacon, 42": when, the type, and the Instance Id. Hover it for the exact time. The platform keeps lines for 24 hours; with none to show the tab says "No log lines match." and "Lines are kept for 24 hours." A **flow** is the platform's name for one call and everything it causes. **Show this call** on a line shows only the lines of that call, and a **One call** chip appears beside the level choices; its x shows every call again. That is how you follow one `FeedBeacon` from a player's request through the beacon's own lines. Lines written outside a call, such as when a Server Object starts or stops, have no flow and no button. The limits on how many lines a call and an app may log are on [Operations](/exec/operations#following-one-call). ## Activity The **Activity** tab shows "How players' calls to each Server Function went, busiest first." for the **Last hour**, **Last day** or **Last week**. Each Server Function gets a card, for example `village_beacon.FeedBeacon`: - how many calls: "1.2k calls in the last hour" (or "1 call in the last hour"), - "no failures", or "94% succeeded", - "usually answers in 12 ms (slowest 1.4 s)", - and a badge for each kind of problem there was: **N server errors** (your code answered with an error, or the call failed on the server), **N timed out** (a call past its **Time per Call** also restarts that Server Object from its last save, so see [Server Limits](./create-a-type.md#every-setting)), **N refused** (the caller was not allowed) and **N busy** (the server was busy; the caller may try again). Hover a badge for what it counts. If `FeedBeacon` is slow or refusing, this is where it shows. With no calls it says "No calls yet in this period." The numbers come from [Calls per endpoint](/exec/operations#calls-per-endpoint) on the platform. ## Settings The **Settings** tab holds Server Compute's settings for this project: "Server Compute's settings for this project. They are saved with the project, so everyone working on it shares them." It has one card so far, **Revisions**, with one setting. **Revisions to keep** is how many deployed revisions of each type's server code the project keeps, to view, compare or restore in the definition asset editor's [Server Code tab](./write-server-logic.md#view-compare-and-restore-an-earlier-deploy). It is a number from 3 to 50 and starts at 5. The hint under it reads "Per type, how many deployed revisions of its server code the project keeps to view, compare or restore (3 to 50). Saved in Config/DefaultEditor.ini; keeping fewer takes effect at the next deploy." The number is changed when you press Enter or leave the box. The setting is saved to `Config/DefaultEditor.ini` in your project, not to your own editor preferences, so commit that file and your team shares one value. If the file cannot be written the page says "Could not save to Config/DefaultEditor.ini, so the change lasts only until the editor closes. Check that the file can be written, for example that it is not read-only." Check the file out first if your version control keeps it read only. Each type's history is `Server//revisions.json`, which holds every kept revision's files in full, so a higher number makes it bigger; lowering the number trims each type the next time it is deployed. ## From the command line For a build server or a script, the same operations run without the editor window: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=deploy -yes ``` It uses Crowdy Studio's saved sign-in and app, so sign in and pick the app in the editor once on that machine first. There is no token to pass on the command line. | Operation | What it does | |---|---| | `-op=status` | The app's active version, whether the app is switched off, which types are switched off, whether its budget is paused and why, and its Server Objects and reserved memory against its limits (`instances`, `instanceLimit`, `reservedMb`, `maxReservedMb`). | | `-op=whoami` | Whether the machine is ready: `signedIn`, `scope` (`session` for a Studio sign-in, `organizationToken` for an organization token), `userId`, `appId`, `orgId` and `environment`. No token is read into the output. Without a sign-in it fails with "Sign in to Crowdy Studio on this machine", and without an app with "Choose the app in Crowdy Studio and press Sync"; both can be in one message. | | `-op=versions` | The versions, newest first. | | `-op=changes` | Which types differ from the live version, like the badges on the Overview tab, and whether a deploy would change anything. Read only. | | `-op=deploy -yes` | Assembles the project, builds what changed, in [parts](#a-deploy-in-parts) when it needs more than one build, waits, deploys it, and records what it sent in each type's `revisions.json`, like **Deploy**. `-batch=N` puts at most N crates in a build, 1 to 16 (16 unless given). `-rebuild` builds every crate instead of [reusing](#server-code-that-is-not-built-again) what the app already holds, as **Rebuild All and Deploy** does. See [The deploy's result](#the-deploys-result). | | `-op=starters -yes` | Builds and deploys the starter pack, like **Deploy starter pack**. It takes `-batch=N` too. | | `-op=addstarter -starter=session` | Adds a Game Kit starter, and the starters it needs, to the project, like **Add starter**: their crates under `Server/` and their definitions under `/Game/ServerObjects/GameKit/`. `-starter=` is one of the seventeen: `world`, `matchmaker`, `session`, `leaderboard`, `player`, `market`, `npcs`, `minigame`, `tactics`, `director`, `instance`, `territory`, `zone`, `deck`, `arena`, `race` or `ball`. `-config=` sets the named starter's **Config**, as JSON or `@` and a file, like `-inputs=`. The result's `added` lists them. See [Use the Game Kit starters](./use-game-kit-starters.md#add-a-starter). | | `-op=removestarter -starter=world -yes` | Removes a Game Kit starter from the project with every starter that runs under it or calls it: their definitions, their `Server/` folders with any edits, and the **Game Kit World** setting when it named one. The result's `removed` lists them. See [Remove starters](./use-game-kit-starters.md#remove-starters). | | `-op=generate [-type=village_beacon] [-yes]` | Generates server code, like **Generate** (see [Generate it](./write-server-logic.md#generate-it)). Without `-type` it generates every type that is out of date or has no server code. A type that needs you (a My Own File type with no Logic File, or a missing one) and a type with a problem are listed under `skipped` and do not fail the run. With `-type`, it generates that one type even when it is up to date; an unknown name fails and lists the types. A question a single Generate would ask refuses that type, and the question is the reason, unless you pass `-yes`. `-yes` answers only the safe questions: keep the old List value names on the server, move the old Type Name folder to the new one (not when the type's own Logic File is inside it), and add empty versions of the missing functions. It never accepts dropping values that saved instances hold, rewriting `lib.rs` lines it would remove, or a move when both folders exist. See [The generate result](#the-generate-result). | | `-op=exporttype`, `-op=newtype`, `-op=edittype` | Read a type's settings as a file, create a type from one, and change a type from one. See [Define a type from a file](#define-a-type-from-a-file). | | `-op=removetype -type=village_beacon [-yes]` | Removes an ordinary type the way **Remove from project...** does (see [Remove a type](#remove-a-type)). It refuses a Game Kit starter (use `-op=removestarter`), a definition outside `/Game/`, and a definition that other assets use, naming them. A redirector that is still in use is named too: "fix up redirectors in the Content Browser first". It deletes the definition asset and then, only if the asset is really gone, the type's Server Code folder when the SDK generated it, any unused redirectors to the asset, and the type's Game Kit World and Actor Starters settings. Without `-yes` it says what it would remove, changes nothing and exits 1. | | `-op=renametype -type=village_beacon -to=/Game/Beacon/CSO_Beacon [-yes]` | Moves or renames the definition asset the way the Content Browser does, which leaves a redirector. The type's Server Code folder follows it, and its Game Kit settings move with it. The Type Name does not change: a changed Type Name is a new type, which `-op=edittype` refuses. It refuses a definition or a target outside `/Game/`, and a target that is taken. Without `-yes` it says what it would do and changes nothing. | | `-op=activate -version=6 -yes` | Makes version 6 the active one, like **Make active**. | | `-op=disable -type=village_beacon -yes` | Switches one type off. | | `-op=enable -type=village_beacon -yes` | Switches it on. Without `-type`, `-op=enable -yes` switches the whole app back on, as the app's **Switch on** does. `-op=disable` still needs `-type`: switching the whole app off stays an action of the page. | | `-op=restart -type=village_beacon -yes` | Moves its running Server Objects to the active version, like **Restart**; the result's `stopped` says how many. | | `-op=restart [-yes]` | With no `-type`, restarts every type that has an instance running code older than the active version. Each instance is compared with the version that was active when it started, and an instance that started within a minute of the deploy counts as old too. It stops every running or starting instance of those types, the ones already on the active version included. Without `-yes` it lists each such type's instances that it would stop (Instance Id, phase and age), changes nothing and exits 1. After a rollback (`-op=activate` to an older version) it refuses and asks for `-type=`. | | `-op=call -type=village_beacon -fn=Refill -key=village -yes` | Calls a Server Only or Developers Only function as the developer, like **Call as Developer**. `-inputs=` gives its inputs as JSON, or `-inputs=@inputs.json` from a file, since a shell mangles quotes (a relative path is read from the folder the command runs in; a path with a space goes in quotes with its `@`, as in `-inputs="@C:\My Game\inputs.json"`, or with the quotes after the `@`, as in `-inputs=@"C:\My Game\inputs.json"`, where the whole path is read; `-config=` and `-bundle=` take the same forms); missing inputs keep their defaults, and a name the function has no input for is refused rather than ignored. `-key=` is needed unless the type has Only One Instance. `-fn=` finds a function by name, the built-in ones such as `AddMember` included, and the Developers Only functions of a [Game Kit starter](./use-game-kit-starters.md#functions-only-a-developer-calls), such as the matchmaker's `Configure`. It also calls a starter's read-only player functions as the developer, today the npcs starter's `state`. A 64-bit integer above 9007199254740991 must be written as a string, for example `{"Player": "9007199254740993"}`; a larger number written without quotes is refused with "... is above 9007199254740991; write it as a string, for example "9007199254740993"." The result gives `status`, `message` and `reply`, the outputs as JSON. A Game Kit starter's function that takes a `Ref`, such as the player's **Grant**, gets a fresh one when `-inputs=` leaves it out, and the result gives it as `ref`. | | `-op=grant -player=101910912345678 -bundle=@reward.json -yes` | Gives one player in-game soft currency and items through the [player starter](./use-game-kit-starters.md#functions-only-a-developer-calls)'s **Grant**, never real money. `-bundle=` is an `FCrowdyKitBundle` as JSON or `@` and a file, such as `{"Currency": {"gold": 100}, "Items": {"wood": 5}}`; `-xp=N` adds experience; `-ref=R` sends a `Ref` of your own, else a fresh one is made; `-type=` names the player starter when its Type Name is not `player`. The result gives `ref`, `status`, `message` and `reply`, what the grant earned the player. | | `-op=deposit -player=101910912345678 -bundle=@restore.json -yes` | Puts soft currency and items into one player's hub through **Deposit**, counting nothing toward quests or achievements, such as to give back what a bug took. It takes `-bundle=`, `-ref=` and `-type=` as `grant` does. | | `-op=instances` | Every Server Object the platform has placed: its type, Instance Id, phase (`idle`, `starting`, `running` or `stopping`), epoch, `heldBack` when the platform will not place it, with the reason, and `orphaned`, true for one whose type the active version no longer deploys (a deploy or a rollback dropped the type and stopped it; it keeps its saved state). | | `-op=logs` | Recent log lines. Optional `-type=`, `-flow=` and `-limit=`. `-level=all`, `info`, `warnings` or `errors` filters by level, like the Logs tab's choices. `-before=` pages back: pass the previous page's `nextBefore`. | | `-op=stats` | Call counts per Server Function. Optional `-type=`. `-window=hour`, `day` or `week` picks the period, like the Activity tab's. | | `-op=batch -ops=` | Runs the operations in a file, one per line, in this one editor start. See [Several operations in one start](#several-operations-in-one-start). | `-yes` stands in for the confirmation the page asks. `deploy`, `starters`, `activate`, `disable`, `enable`, `restart`, `call`, `grant` and `deposit` replace, switch, restart or run the app's live server code, so without `-yes` they refuse with "This changes the app's live server code or what it holds; pass -yes to confirm." and send nothing. The read-only operations (`status`, `versions`, `changes`, `instances`, `logs`, `stats`) do not need it. `addstarter` changes only the project and needs neither `-yes` nor a sign-in. `removestarter` changes only the project too, but deletes the starters' code with any edits, so without `-yes` it names what it would remove and refuses, for example "This deletes session, matchmaker, world: their definitions and server code, edits included; pass -yes to confirm." An unknown `-op` is refused with the list of operations. `generate` and `newtype` change only the project and need no `-yes`; `edittype`, `removetype` and `renametype` change it only with `-yes`. **Close the Unreal Editor first.** An operation that writes the project (`generate`, `newtype`, `edittype` with `-yes`, `addstarter`, `removestarter`, `removetype` and `renametype` with `-yes`) refuses while an Unreal Editor is running: "An Unreal Editor is running. Close it first: it holds this project's Server Object types in memory and would save its own copy over what this op writes. Pass -editoropen to write anyway." `-editoropen` writes anyway. **A read-only `Config/DefaultGame.ini`.** The Game Kit World and Game Kit Actor Starters settings live in that file. When `removetype`, `renametype`, `addstarter` or `removestarter`, or **Remove from project...** on the page, would change them and the file is read-only, as a file checked in to source control can be, it refuses before changing anything: "...DefaultGame.ini is read-only (checked in?), and this changes the Game Kit settings it holds; make it writable, then run again; nothing was changed". The engine would otherwise keep the change in memory only and lose it when the editor closes. The command's exit code is its own result: 0 when the operation succeeded and 1 when it did not, whatever else the editor logs. It writes one line per step to the log under `LogCrowdyExec`. Add `-out=` to also get the result as one JSON object: `op`, `ok`, `error` and `errorCode` (both empty when it succeeded), and the fields of that operation. A CI job can read it: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=deploy -yes -out=Saved/deploy.json ``` A job that runs `-op=deploy` without `-yes` fails with exit code 1 and deploys nothing, so add it once you are sure the job should replace the app's server code. `-op=deploy` refuses, as the page does, when a type's server code is not ready (it needs generating, is out of date, or has a problem), and says why. Regenerate and commit the server code before the job runs; a `Server/` folder that no longer matches its definition asset fails the job instead of deploying old code. Unlike the page, the command knows nothing about unsaved edits in an open editor; it sends what is on disk. It also does not check whether anything changed: it deploys what is on disk every time, as a new version, even when every crate is reused and nothing is built, so ask `-op=changes` first if the job should skip a deploy that would change nothing. ### Several operations in one start Starting the editor takes most of the time of a short operation. `-op=batch` runs several in one start: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=batch -ops=Saved/ops.txt ``` Each line of the file is the complete argument list one run of that operation would take. Blank lines and lines that start with `#` are skipped: ```text # read first -op=status -out=Saved/status.json -op=changes -out=Saved/changes.json -op=call -type=leaderboard -fn=Submit -key=weekly -inputs="@Saved/score.json" -yes -out=Saved/submit.json ``` The operations run in order. Each one connects on its own, has its own time limit, and writes its own `-out` file exactly as it would alone. The batch stops at the first operation that fails, and the operations after it write nothing; put `-batchcontinue` on a line to go on after that line fails. The exit code is 1 if any operation failed and 0 otherwise. The log has one line per operation, such as "Server Compute batch: 2/3 -op=changes ok". A line cannot be `-op=batch`, and a file with such a line, or with no operations, runs nothing. ### The deploy's result `-op=deploy` logs what it builds and what it reuses, such as "building 3 pieces of server code in 1 build; 15 unchanged, named by their digest", then each build as it goes ("build 1 of 2: ... queued", "build ... succeeded against ckx-sdk 0.12.1 after 41 s"), "every build succeeded; deploying", and "deployed as version 9, now the app's active server code", which names the staging versions after it when there were any. With `-out=` the JSON result adds: | Field | What it holds | |---|---| | `version` | The version the deploy made. | | `builds` | Every build sent, in order, each with `buildId`, `buildStatus`, `buildLog`, `sdkVersion` and its `artifacts`. Empty when nothing was built. | | `buildId`, `buildStatus`, `buildLog`, `sdkVersion` | The last build's, as before builds were split. `sdkVersion` is the `ckx-sdk` version the platform compiled the server code against, whatever version the crates name. | | `reused` | Each crate that was not built again: `crate` and the `digest` it was named by. | | `artifacts` | Every crate's module in this deploy, built or reused: `crate`, `digest` and `sizeBytes`. | | `stagedVersions` | The staging versions a [deploy in parts](#a-deploy-in-parts) made, oldest first. Empty for a deploy in one part. | | `restoredVersion` | The version made active again after the last staging version, or 0 when none was. | A failed deploy fills the same fields as far as it got, and its `error` says what is active now, for example "... Version 8 is active again, as before the deploy." A build that failed gives "The build ... failed, so nothing was deployed; its log is above and in buildLog". ### Changes and records `-op=changes` compares the project with the live version the way the page does, without changing anything or needing `-yes`. It logs one line per type, for example `village_beacon: changed (server code, save interval), live revision 2`, and a last line saying `nothing changed`, `a deploy would change what runs` or `not ready to deploy; see problems`. With `-out=` the JSON result adds: | Field | What it holds | |---|---| | `liveVersion` | The live version's number, or 0 when nothing is live. | | `types` | One row per type in the project, then one for each type on the server that the project no longer has: `type` (the Type Name), `change`, `what` and `liveRevision`. | | `change` | `unchanged`, `changed`, `new`, `unknown` (the live version runs code this project has no record of), `removed` (on the server, not in the project) or `not_compared` (the type's server code is not ready). | | `what` | For a changed type, what differs: any of `server code`, `save interval`, `idle timeout`, `platform data`, `player access`, `server limits`, `kind`, `scaling`, and for a Game Kit starter `config` and `parent`. Otherwise empty. | | `liveRevision` | Where the revision the live version runs sits in the type's `revisions.json`, counting from 0 for the newest, or null when there is none. | | `problems` | Why the project cannot be deployed yet, one sentence each, as the page shows them. Empty when it can. | | `warnings` | What may go wrong after a deploy without stopping it, one sentence each, such as platform data the server code calls into without asking for it. `-op=deploy` writes them too, and both log each as a `warning:` line. | | `ready` | True when `problems` is empty. | | `hasChanges` | True when a deploy would change what runs (a type is `new`, `changed`, `unknown` or `removed`), and also when `ready` is false, since a type whose code is not ready differs from what a deploy would send once it is. | A job that should deploy only when something changed can run `-op=changes`, stop when `hasChanges` is false, and otherwise run `-op=deploy -yes`. `-op=deploy` writes each type's `revisions.json` after the deploy succeeds and logs "recorded the deployed revision of 2 types". A record that cannot be written does not fail the job: the deploy stands, the log warns "the deploy record was not saved for ...", and the JSON result lists each one under `recordErrors` (an empty list when all were saved). A job that builds the project's server code in CI should commit the changed `revisions.json` files afterwards, so the next `-op=changes` and everyone's Overview tab agree with what is live. `-op=starters` records nothing. ### The generate result `-op=generate` logs one line per type. With `-out=` the JSON result adds: | Field | What it holds | |---|---| | `generated` | The types it generated. | | `refused` | The types it did not generate because a question a single Generate would ask was not answered by `-yes`: `type` and `reason`, the question itself. | | `skipped` | The types it left alone because they need you (a My Own File type with no Logic File, or a missing one) or have a problem. These do not fail the run. | | `notes` | Other lines about what the run did or did not do. | | `saved` | The definitions it saved. Generating can change a definition, for example by adding List Value Names entries. | | `logicGaps` | Per type, what `logic.rs` still needs: `logicFile` (the file), `missing` (functions to write), `leftover` (functions the file defines that the type no longer has) and `stubsAdded` (empty versions it added). | The exit code is 1 if anything was refused or the save failed. A job can generate, then read `logicGaps` to see which functions to write next. ### Define a type from a file A Server Object type is a definition asset you normally fill in with the Details panel. From the command line you write the same settings as a JSON file, a **spec**, and create or change the type from it. `-op=exporttype` shows what a spec looks like. A spec holds the settings the Details panel shows, under their C++ property names: `TypeName`, `Functions`, `bOnlyOneInstance`, `StateForm`, `StateList` and so on, exactly as `-op=exporttype` writes them. What the bake rebuilds on its own, such as the baked field tables, is not in a spec. - **Keys are matched with their capitals.** `typename` is not `TypeName`. - **An enum is its value name** as the editor writes it, such as `"List"` for a StateForm or `"Every"` for a timer. A struct or enum asset is its path, such as `/Script/MyGame.MyState`, and `null` is none. - **`CanCall` and `ListenTo` are Type Names,** not asset paths. - **A 64-bit integer is a string,** such as `"9007199254740993"`. - **A List is an array of variables.** `StateList`, `ParamsList`, `ReplyList` and `PayloadList` each hold objects with `Name`, `Type`, `TypeObject` (for Enum, Struct and object kinds), `Containers` (at most 2), `KeyType` and `KeyTypeObject` (for a map), `Id`, `MetaData` and `Default`. `ClampMin` and `ClampMax` in `MetaData` are a variable's Value Range. A spec is refused whole, and nothing changes, when it has any problem: an unknown or wrongly capitalised key, a value of the wrong kind or outside its range, a path that does not load, control characters in a name or text, a Logic File outside the project or not a `.rs` file, or a problem the bake finds. Every problem is listed in `errors` of the result, not only the first. For example, a tip jar with a Total and the last tipper, both visible to players, an `AddTip` function and a timer, saved as `tip_jar.json`: ```json { "TypeName": "tip_jar", "bOnlyOneInstance": true, "StateForm": "List", "StateList": [ { "Name": "Total", "Type": "Int64" }, { "Name": "LastTipper", "Type": "String" } ], "WatchedFields": ["Total", "LastTipper"], "Functions": [ { "Name": "AddTip", "ParamsForm": "List", "ParamsList": [ { "Name": "Amount", "Type": "Int32", "MetaData": { "ClampMin": "1", "ClampMax": "100" } } ], "ReplyForm": "List", "ReplyList": [ { "Name": "Total", "Type": "Int64" } ] } ], "Timers": [ { "Name": "Hourly", "Repeat": "Every", "Seconds": 3600 } ] } ``` Create the type, then generate its server code: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=newtype -spec=@tip_jar.json UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=generate -type=tip_jar ``` The operations: | Operation | What it does | |---|---| | `-op=exporttype [-type=T] [-spec=]` | Reads the type's settings. The spec is under `definition` in the `-out` result; with `-spec=` it also writes the bare spec to that file. Without `-type` it exports a new type's defaults, as a template to fill in. | | `-op=newtype -spec= [-path=/Game/...]` | Creates a definition asset `CSO_`, so `tip_jar` becomes `CSO_TipJar`, in `-path` (`/Game/ServerObjects` unless given; it must start with `/Game/`). A setting the spec leaves out keeps its default. It refuses a Type Name that any type in the project already has, in any capitals, and a Code Source of Game Kit Starter: add a starter with `-op=addstarter`. When another type already holds the asset name (`tip__jar` and `tip_jar` both make `CSO_TipJar`), the refusal names that type and suggests `-path=` to put the new asset somewhere else. | | `-op=edittype -type=T -spec= [-yes]` | Replaces the type's settings with the spec's. The spec must hold every top-level setting, so start from `-op=exporttype`; a partial spec cannot empty `Functions` by leaving it out. Settings inside a nested object may be left out. Without `-yes` it changes nothing: it reports `changed` (what would differ) and `renamedListValues`, and exits 1, except that a spec identical to the type succeeds with `changed` empty. With `-yes` it saves the definition, and if the save fails it puts the type back as it was. | `edittype` refuses a changed Type Name, because to the server a new name is a new type (see [After you change the definition](./write-server-logic.md#after-you-change-the-definition)), and a switch to or from a Code Source of Game Kit Starter. `newtype`, and `edittype` with `-yes`, write the project, so they refuse while an editor runs, as described above. The next step after either is `-op=generate -type=T`. ## Be careful on a shared app A deploy replaces the app's active server code for every player and every teammate on that app at once, and there is no separate step to try it first: the staging versions of [a deploy in parts](#a-deploy-in-parts) run what was already live and are not a preview. Use an app of your own for experiments, keep `Server/` in version control so a deploy can be repeated, and know how to make the previous version active before you deploy. ## Related - [Write its server logic](./write-server-logic.md) - [Create a Server Object type](./create-a-type.md) - [Use the Game Kit starters](./use-game-kit-starters.md): Add starter, world as the root, and removing starters - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md) - [Troubleshooting](./troubleshooting.md): outcomes, refusals and `crowdy.exec.trace` - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md) - [Crowdy Studio](../studio/overview.md) - [Builds and starter packs](/exec/builds) and [Operations](/exec/operations) on the platform # Use the Game Kit starters Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/use-game-kit-starters # Use the Game Kit starters :::note Server Objects run on [ck-exec](/exec/intro). The starters themselves are the platform's; see [Game Kit](/game-kit/intro). ::: The platform's [Game Kit](/game-kit/intro) ships **starters**: ready-made server code for systems most games need, such as a world clock, matchmaking and a game session. In Unreal a starter is not a new kind of thing. It is an ordinary [Server Object type](./create-a-type.md) whose **Code Source** is **Game Kit Starter**: a definition asset with variables and Server Functions, and a Rust crate under `Server//` that is the starter's own code instead of code Generate writes. You add it to the project once, deploy it with your own types, and use it with the same nodes and the same C++ calls as any other Server Object. ## The starters **Add starter** offers seventeen of the platform's starters. Each becomes a type of the project with the Type Name of the starter: | Type Name | Definition | What it keeps | Instance Id | |---|---|---|---| | `world` | `CSO_Kit_World` | The game day's clock, the weather, resource nodes that grow back, liveops events with their modifiers, and the players online. While the project has it, it is the app's root. | None: it has **Only One Instance**. | | `matchmaker` | `CSO_Kit_Matchmaker` | One matchmaking queue per Instance Id, with a rating window that widens while players wait, and a match every player must accept, which reserves a session. | The queue's name, such as `ranked` or `casual`. | | `session` | `CSO_Kit_Session` | One game session per Instance Id, from lobby to result: a host, ready checks, turns with a time limit, scores and a winner. With a board in its Config, a finished session posts each player's score to that leaderboard. | The session's key, such as the one a match reserved. | | `leaderboard` | `CSO_Kit_Leaderboard` | One leaderboard per Instance Id: it ranks the scores your server code submits, with ties, seasons that end on a schedule, and the last season's standings. Players read it; they never submit to it. | The board's name, such as `weekly-score` or `fastest-lap`. | | `player` | `CSO_Kit_Player` | One hub per player: their items, in-game soft currency, crafting, loot, idle production, levels, achievements and quests. Only that player reads their inventory, wallet and progress; anyone reads their profile. | The player's user id; empty for the signed-in player. | | `market` | `CSO_Kit_Market` | One market per Instance Id: vendors with fixed prices, an order book for each item it lists, and trades between two players. It holds nothing itself: what a player puts up waits in their own `player` hub until it settles. | The market's name, such as `town` or `harbor`, at most 48 bytes. | | `npcs` | `CSO_Kit_Npcs` | One area per Instance Id: six mobs that wander, chase the nearest player and take players' hits, two NPCs that walk their routes and talk, and the pets players adopt there. They are shown to the players near them as [actors](#show-a-starters-actors). | The area's place on a grid of 4 by 4 chunks, `x,y`, such as `10,-4`: the key **This Actor's Area** gives for a player standing in it. | | `minigame` | `CSO_Kit_Minigame` | One duel per Instance Id: two players each throw rock, paper or scissors, the hub keeps both throws hidden until both are in or the round's time runs out, and the first to win a majority of the rounds wins (best of 3 by default). | Any name you choose, such as `tavern-1`. | | `tactics` | `CSO_Kit_Tactics` | One turn-based grid battle per Instance Id, up to 4 players: each brings a unit, the host starts it, every unit moves and attacks once in the players' phase, then the hub's enemies move and strike, until one side has no unit standing. | Any name you choose, such as `skirmish-1`. | | `director` | `CSO_Kit_Director` | One tower defence per Instance Id: waves of enemies walk a lane to the players' base, players build towers with the gold kills pay and strike enemies themselves, and the last wave brings a boss that changes phase as it weakens. It sends its events as one message, **EncounterEvent**. | Any name you choose, such as `gate-defense`. | | `instance` | `CSO_Kit_Instance` | One dungeon run per Instance Id for a party: floors the hub generates from the run's seed, room by room (fight, treasure, rest, boss), until the party clears the last floor, is wiped, abandons the run or leaves it idle. With a board in its Config, a cleared or wiped run posts each member's depth to that leaderboard. | Any name you choose, such as `cellar-run-7`. | | `territory` | `CSO_Kit_Territory` | One war over control points per Instance Id: players join a faction, a faction captures a point by having more members standing on it than any other, points pay income, and the first faction to the winning score wins. It reads where players are in the world, and sends its events as one message, **TerritoryEvent**. | The map's name, such as `valley`. | | `zone` | `CSO_Kit_Zone` | One shrinking circle per Instance Id, as in a battle royale: it holds, then shrinks phase by phase, and wears down every player outside it until one is left. It reads where players are in the world, and sends its events as one message, **ZoneEvent**. | The match's name, such as `match-1`. | | `deck` | `CSO_Kit_Deck` | One card table per Instance Id, two to six players, playing crazy eights: the hub keeps the deck's order and every hand, the table shows only how many cards each player holds, and each player reads their own hand with a call of their own. | Any name you choose, such as `tavern-2`. | | `arena` | `CSO_Kit_Arena` | One real-time fight per Instance Id, up to 10 players: each picks a class, warrior or mage, with abilities the hub referees, a strike on the fighter nearest the aim, a projectile or an area that goes off after a delay. Each player's client reports where they are a few times a second and the hub keeps what their class's speed allows. The fallen come back after a few seconds, and the first to ten kills, or the most kills when the time runs out, wins. It sends its events as one message, **ArenaEvent**. | Any name you choose, such as `yard-1`. | | `race` | `CSO_Kit_Race` | One race per Instance Id, up to 8 racers: a countdown on the start line, then a course of gates in order for a number of laps, with checkpoint and lap times, places and the winner's run as a ghost. Each racer's client reports where they are, and the hub times the positions it kept. With a board in its Config, a finished race posts each finisher's time to that leaderboard. It sends its events as one message, **RaceEvent**. | Any name you choose, such as `mill-loop-1`. | | `ball` | `CSO_Kit_Ball` | One ball game per Instance Id, red against blue, up to 5 a side: a ball the hub moves, claimed near it, passed and shot, a goal in each end, the first to the winning score or the better score when time runs out. Each player's client reports where they are, and the ball is shown to the players near it as an [actor](#show-a-starters-actors). It sends its events as one message, **BallEvent**. | The match's name, such as `green-1`, of at most 29 characters, with no `@` or spaces, when its ball is shown. | The `player` and `market` starters keep in-game soft currency, such as gold and gems, which is game state like a score. It is never real money, and nothing in them buys, sells or pays out real money. What each one does, function by function, is on the platform's pages: [World and liveops](/game-kit/modules/world), [Matchmaking](/game-kit/modules/matchmaking), [Sessions](/game-kit/modules/sessions), [Leaderboards](/game-kit/modules/leaderboards), [Player](/game-kit/modules/player), [Market](/game-kit/modules/market), [NPCs](/game-kit/modules/npcs), [Duels](/game-kit/modules/minigames), [Grid battles](/game-kit/modules/tactics), [Waves and bosses](/game-kit/modules/encounters), [Private runs](/game-kit/modules/instances) and [Control points and circles](/game-kit/modules/territory), which also covers the zone, [Card tables](/game-kit/modules/tables), [Arenas](/game-kit/modules/arenas) and [Races and ball games](/game-kit/modules/racing). The SDK names the functions in Unreal style, so the platform's `harvest` is **Harvest**, `state` is **GetState**, `status` is **GetStatus** and `me` is **GetMyPlace**. A starter brings the starters it needs. The matchmaker runs under the world and calls the session, and the session runs under the matchmaker, so adding `session` or `matchmaker` adds those three, and adding `world` adds only `world`. The leaderboard runs under the world, so adding it adds `world` and `leaderboard`. A session calls a leaderboard only when [its Config names a board](#post-a-sessions-scores-to-a-leaderboard), so adding the session does not add the leaderboard: add it yourself when you want one. The player runs under the world; the market runs under the world and calls the player, so adding `market` adds `world`, `market` and `player`. The npcs starter runs under the world and reads its day and night, so adding `npcs` adds `world` and `npcs`. The six game-mode starters, `minigame`, `tactics`, `director`, `instance`, `territory` and `zone`, each run under the world and call nothing that they need, so adding one adds `world` and itself. An `instance` calls a leaderboard only when [its Config names a board](#send-a-party-down-a-dungeon), as the session does. The `deck` runs under the world and calls nothing either, so adding it adds `world` and `deck`. The `arena` runs under the world and calls nothing too, so adding it adds `world` and `arena`, and the `ball` the same. The `race` runs under the world and calls a leaderboard only when [its Config names a board](#run-a-race), as the session does. **Add starter** is not **Deploy starter pack**. The starter pack deploys every starter of the platform as it is, in place of your project, to try the platform out; see [Deploy starter pack](./deploy-with-server-compute.md#deploy-starter-pack). **Add starter** makes a starter part of your project, deployed with your own types. ## Add a starter On the [Server Compute](./deploy-with-server-compute.md) page, the strip under the tabs has an **Add starter** menu beside **Deploy starter pack**. Pick any of the seventeen, from `world` to `ball`. It asks first, naming every starter it adds: > Adds world, matchmaker, session to the project as Server Object types: each one's Rust under `Server/`, which you can change, and its definition under /Game/ServerObjects/GameKit. With world, world is the app's root from the next deploy in place of the Crowdy SDK's: your own types run under it, and Get Players Online reads it. Confirm, and for each starter it: - copies the starter's crate, its `Cargo.toml` and `src/lib.rs`, from the Crowdy SDK into `Server//`, and - saves its definition under `/Game/ServerObjects/GameKit/`: `CSO_Kit_World`, `CSO_Kit_Matchmaker`, `CSO_Kit_Session`, `CSO_Kit_Leaderboard`, `CSO_Kit_Player`, `CSO_Kit_Market`, `CSO_Kit_Npcs`, `CSO_Kit_Minigame`, `CSO_Kit_Tactics`, `CSO_Kit_Director`, `CSO_Kit_Instance`, `CSO_Kit_Territory`, `CSO_Kit_Zone`, `CSO_Kit_Deck`, `CSO_Kit_Arena`, `CSO_Kit_Race` or `CSO_Kit_Ball`. A notice says "Added world, matchmaker, session; deploy to run them". The types appear on the Overview tab as **New**, and the next **Deploy** sends them with your own types. A starter the project already has is skipped, and picking one the project has whole says "The project already has the world starter." Nothing is written when something is in the way, and the message says what to do: - "The project already has a Server Object type named session (...), which the session starter needs; rename that type first" - "... already exists, so the world starter's server code would replace what it holds; move that folder first" - "/Game/ServerObjects/GameKit/CSO_Kit_World already exists; move or delete it to add the world starter" Commit the new `Server//` folders and the definitions with the rest of your project. From the command line, `-op=addstarter` does the same without asking: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=addstarter -starter=session ``` It changes only the project, so it needs no `-yes` and no sign-in. It logs "added world, matchmaker, session", and its `-out=` result has `starter` and `added`, the Type Names it added. `-config=` sets the named starter's **Config** as it is added, as JSON or as `@` and a file holding it, so `-starter=session -config=@session.json` with `{"board": "weekly-score"}` in `session.json` adds a session that posts its scores to that board; the starters it brings keep an empty Config. A Config that is not a JSON object is refused before anything is written: "the Config for session is not a JSON object: ...". So is a `world` Config whose `day_ms`, `nodes`, `node_amount` or `regrow_ms` is not a whole number without a decimal point or exponent, from 0 (`nodes` at most 65535), in the editor's **Add starter** too; [Change a starter](#change-a-starter) says more. So is a Config for a starter the project already has, which it would not use: "The project already has the session starter, so this Config would not be used; set Config in its definition's Game Kit category instead". See [From the command line](./deploy-with-server-compute.md#from-the-command-line). ### The world becomes the app's root Every app has one root type, which every other type runs under. Without starters it is the Crowdy SDK's own root, which keeps the players online for **Get Players Online**. Once the project has `world`, `world` is the root instead: Deploy does not send the SDK's root, your own types run under `world`, and the first deploy lists `root` among what it removes. Adding `world` also sets **Project Settings, Crowdy SDK, Server Objects, Game Kit World** (`GameKitWorld` in `Config/DefaultGame.ini`) to `CSO_Kit_World`. **Get Players Online** reads the count from the definition that setting names, the `Online` variable of the world, and from the SDK's root when it is empty. Commit `Config/DefaultGame.ini` too, so a packaged game reads the same root the app runs. When the setting and the project disagree, Deploy warns, for example "Get Players Online reads ..., but this deploy's root is world; set Game Kit World to ... in Project Settings, Crowdy SDK". Until the app runs a deploy with `world`, **Get Players Online** returns false and logs "This app has no Game Kit world, so Players Online has no count: deploy the app from Server Compute, or clear the Game Kit World setting if the project no longer has the world starter. It is asked again every minute." See [How many players are online](./from-blueprint/read-and-follow-variables.md#how-many-players-are-online). **Get Players Online** also returns false, with 0, for good when the world's snapshot carries no player count: a world deployed from the platform's own starter pack sends none, where the SDK's `world` starter does. The log says why once. `FCrowdyKitWorldConditions` has a `bHasOnline` flag for this, so read it before `Online` in C++, or call `UCrowdyServerObjectLibrary::ReadKitWorldOnline(Target, World, Online, bUnsent)`, which returns false with `bUnsent` set when the world sent no count. A `world` definition saved with an earlier SDK reports "has changed since ... was saved" until you save it again. ## Use it from Blueprint and C++ A starter is used like any other Server Object type, so the pages on [getting one](./from-blueprint/get-a-server-object.md), [reading its variables](./from-blueprint/read-and-follow-variables.md), [hearing its messages](./from-blueprint/hear-server-messages.md) and [calling its functions](./from-blueprint/call-functions.md) apply as they are, from Blueprint and [from C++](./from-cpp/get-a-server-object.md): - **Get the object** with **Get Server Object**, a typed node with **Find By Asset**, or the **Crowdy Server Object** component, given the starter's definition and its Instance Id. The world has **Only One Instance**, so its nodes need neither a Target nor an Instance. In C++, `Acquire` the definition with the Instance Id. - **Its variables** are what the starter shows every player: the world's conditions, a queue's counts, a session's whole state. **Get** and **On Changed** read and follow them, such as **Get Online (CSO_Kit_World)** or **On Phase Changed (CSO_Kit_Session)**. In C++, `GetState()` holds the State struct and `WatchValues` reports each change. - **Values only this player reads.** A player's inventory, wallet and progress, and a market's orders and trades for this player, are variables too: **Get Wallet (CSO_Kit_Player)**, **On Wallet Changed (CSO_Kit_Player)**, **On Orders Changed (CSO_Kit_Market)** and `GetState()` hold them. See [Values only the player reads](#values-only-the-player-reads). - **Its functions** have a **Call** node each, such as **Call Harvest (CSO_Kit_World)**, and `Call` in C++. The answer comes back as for any Server Function, with the [outcome and code](#refusals-and-their-codes) below. - **Its messages.** A hub's `event` messages reach the game as the type's [server messages](./create-a-type.md#server-messages), matched by their topic, which is the message's server name. The world, matchmaker and session starters put everything in their state and send no events, so their definitions declare no messages. The `director`, `territory` and `zone` starters send their events as [one message each](#react-to-a-directors-waves), and the `arena` sends **ArenaEvent**, described in [Fight in an arena](#fight-in-an-arena). If you change a starter to send one, add the message to its definition. What is particular to a starter: - **Its records.** A starter whose hub keeps a collection, a set of members it changes one at a time, holds them as [records](./from-blueprint/read-and-follow-variables.md), one per member, keyed by the member's id as text. The world's resource nodes are its records: **Find Record** with `3` gives node 3's values (`FCrowdyKitWorldNode`: `Id`, `Amount`, `RegrowAt`), **Get Record Ids** lists every node, **On Records Changed** and **On Records Updated** report each harvest and regrowth, and **Watch Record** follows one node. A record has no owner or team. The object is **Ready** once both its variables and its records are read, so a record is there when **On Status Changed** says Ready. The events come back from **Call GetState** (`Events`), and a player's place in a queue from **Call GetStatus**. - **Pacing and resending.** Calls to starters go through the Game Kit's own client, which keeps them under the platform's limit and sends one again when that is safe. The SDK's own retry of a busy call is not added on top. - **A starter that will not let this player read it** fails with "The server code for session refused to let this player read it (forbidden): ..." and stays **Failed**, as an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) object of someone else's does. A refusal coded `cooldown` or `unavailable` is waited out like any other. ### The values in C++ The State, inputs and outputs of the starters are C++ structs and enums the SDK declares in `CrowdyGameKitTypes.h`, in the `CrowdyExec` module, and Blueprint sees them too: | Starter | Structs and enums | |---|---| | world | `FCrowdyKitWorldConditions` (the State: `TimeOfDay`, `bNight`, `Weather`, `Modifiers`, `Online` with `bHasOnline`), `FCrowdyKitWorldView` (what **GetState** answers: `Conditions`, `ClockMs`, `Nodes`, `Events`, `Online`), `FCrowdyKitWorldNode`, `FCrowdyKitWorldEvent`, `FCrowdyKitHarvestInputs`, `FCrowdyKitHarvestReply`, `FCrowdyKitCancelEventInputs`, `FCrowdyKitCancelEventReply`, `ECrowdyKitWeather` | | matchmaker | `FCrowdyKitQueueCounts` (the State: `Waiting`, `Proposals`, `Forming`), `FCrowdyKitMatchJoinInputs`, `FCrowdyKitMatchPlace` (what **Join**, **Accept** and **GetStatus** answer), `ECrowdyKitMatchState`, `FCrowdyKitMatchmakerConfig` | | session | `FCrowdyKitSessionState` (the State, which **GetState** answers too), `FCrowdyKitSessionConfig` (with `Board`), `FCrowdyKitSessionMove`, `FCrowdyKitSessionReadyInputs`, `FCrowdyKitSessionReadyReply`, `FCrowdyKitSessionActInputs`, `FCrowdyKitSessionActReply`, `ECrowdyKitSessionPhase` | | leaderboard | `FCrowdyKitBoardView` (the State: `Season`, `SeasonStarted`, `Entries`, `Top`), `FCrowdyKitBoardPlace` (`Rank`, `Player`, `Score`; what **GetMyPlace** answers), `FCrowdyKitBoardPlaces` (what **GetTop** and **GetAround** answer), `FCrowdyKitBoardPageInputs`, `FCrowdyKitBoardAroundInputs`, `FCrowdyKitBoardSeason`, `FCrowdyKitBoardSubmitInputs`, `FCrowdyKitBoardSubmitReply`, `FCrowdyKitBoardConfig`, `FCrowdyKitBoardRollReply`, `ECrowdyKitBoardKeep`, `ECrowdyKitBoardOrder` | | player | `FCrowdyKitPlayerState` (the State: the public profile, `Player`, `Level`, `Achievements`, `QuestsDone`, and the player's own `Inventory`, `Wallet` and `Progress`), `FCrowdyKitBundle` (`Items` and `Currency`, each a `TMap`: a price, a reward, an offer), `FCrowdyKitPlayerMe` (what **GetMe** answers), `FCrowdyKitInventory` (**GetInventory**: `Slots`, `Stacks` of `FCrowdyKitStack`, `Mail`, `Held`), `FCrowdyKitWallet` (**GetWallet**: `Balances`, `Held`), `FCrowdyKitProgress` (**GetProgress**: `Level`, `Xp`, `Next`, `TotalXp`, `Achievements` of `FCrowdyKitAchievement`, `Quests` of `FCrowdyKitQuest`, each with `FCrowdyKitObjective`s), `FCrowdyKitProduction` and `FCrowdyKitProducer` (in **GetMe**), `FCrowdyKitPlayerProfile` (**GetProfile**), `FCrowdyKitPlayerRules` (**GetRules**: the `FCrowdyKitItemDef` items, soft currencies, `FCrowdyKitRecipe`s, `FCrowdyKitLootTable`s with their `FCrowdyKitLootEntry`s, `FCrowdyKitQuestDef`s and `FCrowdyKitAchievementDef`s its Config sets), `FCrowdyKitEarned`, `FCrowdyKitCraftInputs`, `FCrowdyKitCraftReply`, `FCrowdyKitOpenLootInputs`, `FCrowdyKitOpenLootReply` (with an `FCrowdyKitLootRoll`), `FCrowdyKitQuestInputs`, `FCrowdyKitStepInputs`, `FCrowdyKitDiscardInputs`, `FCrowdyKitGrantInputs`, `FCrowdyKitRecordInputs`, `FCrowdyKitDepositInputs`, `ECrowdyKitQuestState` | | market | `FCrowdyKitMarketState` (the State: `Vendors`, `Currency`, `Quotes`, `OpenTrades`, `PendingSteps`, and this player's `Orders` and `Trades`; **GetState** answers it too, without those two), `FCrowdyKitVendors` (**GetVendors**), `FCrowdyKitVendor`, `FCrowdyKitVendorOffer`, `FCrowdyKitQuote`, `FCrowdyKitBookInputs`, `FCrowdyKitBook` (**GetBook**: `Bids` and `Asks`, each a list of `FCrowdyKitPriceLevel`), `FCrowdyKitOrders` (**GetOrders**), `FCrowdyKitOrder`, `FCrowdyKitTrades` (**GetTrades**), `FCrowdyKitTrade`, `FCrowdyKitTradeSide`, `FCrowdyKitShopInputs` (**Buy** and **Sell**), `FCrowdyKitOrderInputs`, `FCrowdyKitPlacedOrder`, `FCrowdyKitCancelOrderInputs`, `FCrowdyKitOpenTradeInputs`, `FCrowdyKitTradeReply`, `FCrowdyKitTradeOfferInputs`, `FCrowdyKitTradeInputs`, `FCrowdyKitAcceptTradeReply`, `ECrowdyKitOrderSide`, `ECrowdyKitTradeState` | | npcs | `FCrowdyKitAreaState` (the State, which is empty), `FCrowdyKitAreaMember` (a record: `Id`, `Type` of `ECrowdyKitAreaMemberType`, `X`, `Y`, and the fields of its type), `FCrowdyKitAreaMembers` (what **GetState** and **GetPets** answer: `Members`), `FCrowdyKitAttackInputs` (`Mob`), `FCrowdyKitAttackReply` (`Hp`), `FCrowdyKitTalkInputs` (`Npc`; **Talk** answers the NPC's `FCrowdyKitAreaMember`), `FCrowdyKitAdoptInputs` (`Species`, `Name`, each 1 to 24 characters), `FCrowdyKitAdoptReply` (`Pet`, the new pet's id), `FCrowdyKitPetInputs` (`Pet`: **SummonPet**, **DismissPet** and **ReleasePet**), `FCrowdyKitRenamePetInputs` (`Pet`, `Name`) | | minigame | `FCrowdyKitMinigameState` (the State: `Phase`, `Seats` of `FCrowdyKitMinigameSeat`, `BestOf`, `Round`, `Deadline`, `Last` an `FCrowdyKitMinigameReveal`, `Winner`), `FCrowdyKitMinigameJoinReply`, `FCrowdyKitMinigameThrowInputs`, `FCrowdyKitMinigameThrowReply`, `ECrowdyKitMinigamePhase` | | tactics | `FCrowdyKitTacticsState` (the State: `Phase`, `Round`, `Width`, `Height`, `Obstacles` of `FCrowdyKitGridCell`, `Units` of `FCrowdyKitTacticsUnit` with their `FCrowdyKitTacticsStats`, `Players`, `Host`, `EndedTurn`, `PhaseEnds`, `Log`), `FCrowdyKitTacticsMoveInputs`, `FCrowdyKitTacticsAttackInputs`, `FCrowdyKitTacticsAttackReply`, `ECrowdyKitTacticsPhase`, `ECrowdyKitTacticsTeam` | | director | `FCrowdyKitDirectorState` (the State: `Phase`, `Players`, `Width`, `Height`, `Spawn`, `Base`, `BaseHp`, `Gold`, `Wave` with `bHasWave`, `Waves`, `NextWaveAt`, `Enemies` of `FCrowdyKitDirectorEnemy`, `Towers` of `FCrowdyKitDirectorTower`, `BossPhase`, `Kills`), `FCrowdyKitDirectorEvent` (the **EncounterEvent** message: `Type` with `bHasType`, `Wave`, `Phase`), `FCrowdyKitDirectorBuildInputs`, `FCrowdyKitDirectorBuildReply`, `FCrowdyKitDirectorHitInputs`, `FCrowdyKitDirectorHitReply`, `ECrowdyKitDirectorPhase`, `ECrowdyKitDirectorEventType` | | instance | `FCrowdyKitInstanceState` (the State: `Phase`, `Owner`, `Seed`, `Floor`, `Floors`, `Room`, `Rooms` of `FCrowdyKitInstanceRoom`, `Members` of `FCrowdyKitInstanceMember`, `Invited`, `Gold`, `Relics`, `Grid`, `Log`), `FCrowdyKitInstanceOpenInputs` (`Seed`, `bHasSeed`, `Party`), `FCrowdyKitInstanceOpenReply`, `FCrowdyKitInstanceInviteInputs`, `FCrowdyKitInstanceEnterReply`, `FCrowdyKitInstanceDescendReply`, `ECrowdyKitInstancePhase`, `ECrowdyKitInstanceRoomKind` | | territory | `FCrowdyKitTerritoryState` (the State: `Phase`, `Points` of `FCrowdyKitTerritoryPoint`, `Scores`, `Members`, `Winner`), `FCrowdyKitTerritoryEvent` (the **TerritoryEvent** message: `Type` with `bHasType`, `Point`, `Owner`, `Previous`, `Paid`, `Faction`), `FCrowdyKitTerritoryJoinInputs`, `ECrowdyKitTerritoryPhase`, `ECrowdyKitTerritoryEventType` | | zone | `FCrowdyKitZoneState` (the State: `Phase`, `Center` an `FCrowdyKitZoneCenter`, `Radius`, `Stage` with `bHasStage`, `NextRadius`, `Players` of `FCrowdyKitZonePlayer`, `Alive`, `Winner`), `FCrowdyKitZoneEvent` (the **ZoneEvent** message: `Type` with `bHasType`, `Phase`, `ToRadius`, `Radius`, `ShrinksAt`, `Player`, `Place`), `ECrowdyKitZonePhase`, `ECrowdyKitZoneEventType` | | deck | `FCrowdyKitDeckState` (the State, which **GetState** answers too, plus this player's own `Hand`: `Phase`, `Players`, `Host`, `HandCounts`, `Top`, `Suit` with `bHasSuit`, `DrawCount`, `Turn`, `TurnEnds`, `Last`, `Winner`, `Scores`), `FCrowdyKitCard` (`Id`, `Rank`, `Suit`), `FCrowdyKitDeckHand` (`Cards`, `Playable`, `bDrawn`; what **GetHand** and **Draw** answer), `FCrowdyKitDeckPlayed`, `FCrowdyKitDeckPlayInputs` (`Card`, `Suit`, `bHasSuit`), `FCrowdyKitDeckPlayReply` (`bPlaying`), `ECrowdyKitDeckPhase`, `ECrowdyKitDeckAction`, `ECrowdyKitCardSuit` | | arena | `FCrowdyKitArenaState` (the State, which **GetState** answers too: `Phase`, `Origin` an `FCrowdyKitGroundPosition`, `Width`, `Depth`, `Fighters` of `FCrowdyKitArenaFighter`, `Projectiles` of `FCrowdyKitArenaShot`, `Areas` of `FCrowdyKitArenaBlast`, `KillTarget`, `EndsAt`, `Winner`), `FCrowdyKitArenaEvent` (the **ArenaEvent** message: `Type` with `bHasType`, `Ability`, `Caster`, `X`, `Y`, `Victim`, `Damage`, `Killer`, `Player`, `What`, `Detail`, `Why`, `Winner`), `FCrowdyKitArenaJoinInputs` (`HeroClass`), `FCrowdyKitArenaMoveInputs` (`X`, `Y`), `FCrowdyKitGroundPosition` (what **Move** answers: `X`, `Y`), `FCrowdyKitArenaCastInputs` (`Ability`, `X`, `Y`), `FCrowdyKitArenaCastReply` (`bHit`), `ECrowdyKitArenaPhase`, `ECrowdyKitArenaEventType` | `FCrowdyKitMatchPlace` is the player's place in a queue, and only the fields of its `State` are filled: `Rating`, `WaitedMs` and `Window` while **Queued**; `Players`, `Accepted` and `ExpiresInMs` while **Proposed**; `Session`, the Instance Id of the session reserved for the match, once **Matched**. A session's `Scores` is a `TMap` from user id to score, and the world's `Modifiers` a `TMap`, which is why these are structs of the SDK's rather than variables added in the asset. On a leaderboard, players with the same score share a `Rank`, and **GetMyPlace** answers a `Rank` of 0 when the player is not on the board. Blueprint cannot leave an input out, so a `Limit` of 0 on **GetTop** means the board's own Top Size, and a `Player` of 0 on **GetAround** means the player calling. Blueprint has no empty value either, so where the player and market starters answer "none", the field reads a value they never use: a vendor offer's `Stock` and `PerPlayer` read -1 for no limit, a `FCrowdyKitQuote`'s `Bid`, `Ask` and `Last` read 0 for no price yet, `FCrowdyKitProgress`'s `Next` reads 0 at the top level, and an achievement's `UnlockedAt` reads 0 while it is locked. A player's **GetMe**, **GetInventory**, **GetWallet** and **GetProgress** answer only that player; anyone else is refused **Denied** with **Code** `forbidden`. When both players **AcceptTrade**, the answer's `State` is **Settling**: the market then moves each side's offer to the other player, and their wallets and inventories show it a moment later, when the trade reads **Done**. The functions a market calls on a player's hub to hold and move what is traded are not in `CSO_Kit_Player`, since only the market's server code may call them. ### Values only the player reads A starter's hub keeps some values for one player alone, which its snapshots never carry: the player starter's inventory, wallet and progress, a market's orders and trades of the player calling, and a card table's hand. The SDK reads each with its own call, so they are ordinary variables of the State, with **Get**, **On Changed** and `WatchValues` as for any other: - The object reads them after every read of its State: when it first connects, after a reconnect, after the server restarted, and after a missed snapshot. It is **Ready** only once they are read, so **On Status Changed** saying Ready brings them. - When the hub says one of them changed for the signed-in player, such as the wallet after a **Craft** or a market trade settling, the SDK reads that one again, and **On Wallet Changed** follows a moment after the change. A change for another player reads nothing. - When the object fails, as after a sign-out, they go back to their defaults, so one player's wallet is never left for the next. - After a hub restart that arrives as a pushed snapshot, they are read again. - If a customized starter's snapshot carries a field that is also an Owner Value, the snapshot's value wins. For the player starter they are read only on the signed-in player's own player. Get `CSO_Kit_Player` with an empty Instance Id for that: the SDK uses the signed-in player's user id, and **Get Server Object** refuses with "Nobody is signed in yet, so player has no player to read: sign in first, or give a player's user id as the Instance Id" before anyone signs in. A **Crowdy Server Object** component with `CSO_Kit_Player` and its **Instance Id** left empty does the same as **Signed-In Player**: it waits for the sign-in instead of failing, and joins the new player's own hub whenever another player signs in. Get it with another player's user id and you read their public profile, `Player`, `Level`, `Achievements` and `QuestsDone`, while `Inventory`, `Wallet` and `Progress` are not read: their **Has Value** pins are false and **Get** gives the default, as it does for every player's while nobody is signed in. In C++, the object's `IsUnreadOwnerValue(TEXT("Wallet"))` is true for such a value. A market reads the orders and trades of whoever is signed in, whatever its Instance Id, and so does a deck: its `Hand` is the signed-in player's own at the table the Instance Id names. See [Play a card game with a hidden hand](#play-a-card-game-with-a-hidden-hand). In `CSO_Kit_Player`, `CSO_Kit_Market` and `CSO_Kit_Deck` these are listed under **Game Kit**, **Owner Values** (`FCrowdyKitOwnerValue`: `Method`, `What`, `Variable`): each names the call that answers it (`wallet`), what the hub's changed messages call it (`wallet`) and the State variable it fills (`Wallet`). **Keyed By Player** is on for the player, which a definition's `IsKeyedByPlayer()` reads in C++. A definition whose Owner Value names no variable of its State reports the problem, such as "Owner Value Wallet is not a variable of the State", and so does one with Owner Values and no State Topic. ### Field names are snake_case The starters' Rust names its fields in snake_case, so a starter's definition does too, by default: `TimeOfDay` is `time_of_day` on the server, and a bool's `b` is dropped, so `bNight` is `night`. Enum values follow the same rule. An entry under the definition's **Server Names** still wins over it. It matters when you point a starter at a struct of your own, and in C++, where the field names `WatchValues` reports are the server names: `Fields.Contains(TEXT("night"))`, not `bNight`. ### Send Once A function of a starter has a **Send Once** setting in its Details, which no other type shows. With it on, each call carries an id of its own, and the starter runs the call once however often it is sent again, so a call resent after a lost answer does not harvest, join or score twice. Add starter turns it on for every function that changes something and off for those that only read, such as **GetState**, **GetStatus** and **GetTop**. Leave it as it is unless you change what a function does. ## Refusals and their codes A starter refuses a call with a code the game can branch on and words to show, such as `full`, `not_your_turn` or `empty`. The codes are listed on the platform's [Refusals carry a code](/game-kit/protocol#refusals-carry-a-code). In Unreal a refusal arrives on **On Failed**, or in `OnDone`, with: - **Outcome** **Refused** (`ECrowdyServerCallOutcome::Refused`) for a refusal of the game's own, such as a full match or another player's turn. A Blueprint Switch on the outcome has a **Refused** pin. - **Code**, the code itself as a name, such as `full`. On a **Call** node it is in the advanced area with **Outcome**, **Reason** and **Retryable**: click the small arrow at the bottom of the node to show it. In C++ it is `FCrowdyServerCallResult::Code`, an `FName`, and `None` for anything that is not a starter's refusal. - **Reason**, the starter's own words, such as "only the host starts the session". When it gives none, "The server refused the call to Start: " followed by the code. Three codes read as an outcome a call already has, and fill **Code** too: | Code | Outcome | Retryable | |---|---|---| | `forbidden` | Denied | No | | `cooldown` | Denied | Yes | | `unavailable` | Unavailable | Yes | So a game that already handles **Denied** and **Unavailable** handles these without change, and can still read **Code** to tell them apart. Every other code is **Refused** and not retryable. Your own types never answer **Refused**. See [Outcomes](./troubleshooting.md#outcomes). In an actor whose **Crowdy Server Object** component holds `CSO_Kit_Session` with the session's Instance Id, so the **Call** node needs no **Target**: [Blueprint snippet: kit-refused-code, see /bp/unreal-sdk/kit-refused-code.txt] ## Functions only a developer calls Some of a starter's functions are for you, not for players. Their definition makes them [Developers Only](./create-a-type.md#callable-by), and the starter's Rust refuses anyone else with `forbidden`, which a game reads as **Denied**: | Starter | Function | What it does | |---|---|---| | world | **ScheduleEvent** | Schedules or replaces a liveops event (`FCrowdyKitWorldEvent`: `Name`, `StartMs`, `EndMs` and `Modifiers`); while it runs, its modifiers apply. | | world | **CancelEvent** | Cancels one by `Name`, and answers whether there was one. | | matchmaker | **Configure** | A queue's settings (`FCrowdyKitMatchmakerConfig`): `PlayersPerMatch`, the rating window, how long players have to accept. | | leaderboard | **Submit** | Puts a player's score on the board (`FCrowdyKitBoardSubmitInputs`: `Player`, `Score`), and answers their score now. Other server code, such as a session, submits too; a player never does. | | leaderboard | **Configure** | A board's settings (`FCrowdyKitBoardConfig`): whether it keeps a player's best, sum or latest score, whether high or low ranks first (points or times), its Top Size, a cron schedule that ends each season, and how many players it holds. | | leaderboard | **Roll** | Ends the season now: the board keeps its final standings, which **GetSeason** answers as `Last`, and starts the next one empty. | | player | **Grant** | Gives a player a reward of items and in-game soft currency (`FCrowdyKitGrantInputs`: `Ref`, `Bundle`, `Xp`, `Counters`), and answers what it earned them (`FCrowdyKitEarned`). Items that do not fit wait in their mail. | | player | **Record** | Counts something the player did toward quests and achievements (`FCrowdyKitRecordInputs`: `Ref`, `Counter`, `N`). | | player | **Deposit** | Puts items and soft currency into a player's hub without counting anything (`FCrowdyKitDepositInputs`: `Ref`, `Bundle`). | | territory | **Reset** | Starts a new war: every point and score starts again and the phase reads **Open**. Players keep their factions. | Run them with **Call as Developer...** in the function's Details in the definition asset, or with `-op=call` from the command line; see [Call a function as the developer](./deploy-with-server-compute.md#call-a-function-as-the-developer). For a one-player test of the `ranked` queue: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=call -type=matchmaker -fn=Configure -key=ranked -inputs=@ranked.json -yes ``` with `{"PlayersPerMatch": 1}` in `ranked.json`; the inputs left out keep their defaults. The world has Only One Instance, so a call to it needs no `-key=`. A session's **Configure** is not Developers Only: the session's host calls it in the lobby. ![The Call as Developer window for the matchmaker's Configure: the Instance Id box, the Players Per Match, Window, Widen Per Sec, Max Window, Accept Ms and Party Ms inputs, and the Call button](/img/unreal-sdk/kit-call-as-developer.png) A player's **Grant**, **Record** and **Deposit** each take a `Ref`, up to 128 characters, that names that one change. The player's hub runs a change once per `Ref` and, for up to seven days (its last 4096 changes), answers a second call with the same `Ref` as it answered the first, giving nothing more and saying nothing about it. So give every grant its own `Ref`, such as `quest-reward:4211`, and reuse one only to send the same grant again after a lost answer. The `-key=` of a call to the player is the player's user id. ### Grant and deposit soft currency from Studio On the [Server Compute](./deploy-with-server-compute.md) page, the **...** menu of the `player` row has **Grant soft currency...** and **Deposit soft currency...**. Each opens **Call as Developer** on that function, which runs it on the live app now. Fill **Player** with the player's user id, and the `Bundle`'s `Currency` and `Items` with the amounts, such as `gold` 100. The window says what it gives is in-game soft currency, never real money. Leave `Ref` empty: each press of **Call** makes a fresh one, such as `studio-grant-` and 32 hex digits, so each press gives again, and the answer shows the `Ref` it sent. To send a grant again after its answer was lost, copy that `Ref` into the field and press once more: the player's hub answers as it did the first time and gives nothing more. From the command line, `-op=grant` and `-op=deposit` do the same: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=grant -player=101910912345678 -bundle=@reward.json -xp=50 -yes ``` with `{"Currency": {"gold": 100}, "Items": {"wood": 5}}` in `reward.json`. Without `-ref=` a fresh `Ref` is made, which the result gives as `ref`. Both refuse, before sending anything, a call that would give a player nothing they would see: a player id other than their user id as a game reads it ("007 is not a player's user id as a game reads it, ..."; a number above 0, no sign or leading zero), a grant or deposit with an empty bundle and no experience ("this gives the player nothing: fill the Bundle's Currency or Items, such as gold 100"), a `-bundle=` key other than `Items` and `Currency`, and an `-xp=` that is not a whole number. See [From the command line](./deploy-with-server-compute.md#from-the-command-line). ## Post a session's scores to a leaderboard A leaderboard ranks scores that your server code decided, never scores a player sends, so a player's **Submit** comes back **Denied** with **Code** `forbidden`. The session starter posts for you: give it a board and, when a session finishes, it submits each player's score to that leaderboard. 1. Add the `leaderboard` starter, if the project does not have it. 2. In `CSO_Kit_Session`, under **Game Kit**, set **Config** to `{"board": "weekly-score"}`, with the board's name. The session's **Calls** already lists `leaderboard`, and Deploy sends that link while the project has the leaderboard. 3. Deploy. From then on, every session that finishes, by reaching the target score, running out of rounds or losing too many players, submits each player's score once to the `weekly-score` board. A session sees its board as `Config.Board` in its variables. **Configure** leaves the board as the Config set it, so a host cannot send a session's scores elsewhere. To read the board, get `CSO_Kit_Leaderboard` with `weekly-score` as its Instance Id: its variables are the top of the board and the season, which **On Top Changed** follows, and **Call GetMyPlace**, **GetTop**, **GetAround** and **GetSeason** read more. A board keeps each player's best score by default, higher ranking first. For other rules, call its **Configure** as the developer, for example `{"Keep": "Sum"}` for a total over the season, or `{"Order": "Low"}` for times. A project with a board in a Config and no leaderboard deploys with a warning: "session: its Config posts scores to the board weekly-score, but the project has no leaderboard, so they go nowhere; add the leaderboard starter". If the leaderboard is not deployed, a finished session's scores go nowhere; when the platform refuses the post, the session's server log says "a score did not reach board weekly-score: ...". The session checks its Config as **Configure** checks its inputs: `min_players` at least 1 and at most `max_players`, `max_players` at most 64, `turn_ms` at least 1000, and every key of the right kind. A Config it refuses stops each of its sessions from starting, so test a new Config on one session before you rely on it. ## Find a match and take turns The matchmaker tells a player nothing on its own: a player in a queue asks its **GetStatus** to learn they were proposed a match, accepts it, and asks again until they are matched. **Find Match** does all of that. **In Blueprint**, add **Find Match** (Crowdy SDK, Game Kit). Its **Matchmaker** is the queue's Server Object: **Get Server Object** with `CSO_Kit_Matchmaker` and the queue's name, such as `ranked`, held by your Owner while the search runs. Set **Rating**, **Auto Accept** and **Timeout** (seconds; 0 waits for as long as it takes). Its pins: - **Queued** runs when the player waits in the queue. - **Proposed** runs when a match is proposed, and again when who has accepted it changes, with **Players** and **Accepted** (user ids). - **Matched** runs once, with **Session Id**: get `CSO_Kit_Session` with it as the Instance Id, then **Call Join**, **Call Ready** and, as the host, **Call Start**. - **Failed** runs once, with **Code** and **Reason**: the matchmaker's refusal (such as `full`), `cancelled`, `timeout`, `expired` (a proposed match this player did not accept in time) or `left` (the player left the queue another way). With **Auto Accept** the node accepts each proposed match itself. Without it, call **Accept** on the node's **Search** pin from **Proposed**, for example after the player presses a button; **Cancel** on the same pin stops the search. A match that falls through after this player accepted, because another player declined or let it expire, is searched for again. Every ending but **Matched** leaves the queue, so nobody is proposed a match with a player who stopped looking. **Cancel** and the **Timeout** wait for a call already sent, and end with **Matched** if it made the match. A matchmaker that cannot reserve a session puts its players back in the queue, so a search with no Timeout then waits on. [Blueprint snippet: kit-find-match, see /bp/unreal-sdk/kit-find-match.txt] **Is My Turn** and **Get Turn Player** read the session's turn: give them the session's Server Object. **Is My Turn** is true while the session plays and it is the signed-in player's turn; **Get Turn Player** gives the user id whose turn it is, and is false, with **Player** 0, in the lobby, once the session finished, and before it is read. Call them from **On Turn Changed (CSO_Kit_Session)** and **On Phase Changed (CSO_Kit_Session)** to enable the player's controls: **Turn** reads 0 in the lobby and on the first turn alike, so it is the phase changing to **Playing** that starts the first turn. In a widget whose `SessionId` variable holds the **Session Id** from **Matched**, and whose Play button is named `PlayButton` with **Is Variable** ticked: [Blueprint snippet: kit-session-turn, see /bp/unreal-sdk/kit-session-turn.txt] From queue to result, every player of a match runs the same steps. The matchmaker matches two players by default (`players_per_match`), and each player's **Find Match** ends **Matched** with the same **Session Id**. Each player gets that session, calls **Join** and then **Ready**. The session's **Host** variable is the first player to join: compare it with **Get User ID** on the Crowdy Game Session, and only that player calls **Start**. **Start** is refused with `not_ready` until every player is ready, so the host calls it again when **Ready** changes. The other players wait for **Phase** to read **Playing**. Then each player calls **Act** only while **Is My Turn** is true; a move out of turn is refused with `not_your_turn`. A player who runs out of the session's `turn_ms` is skipped. When a player reaches `target_score`, or the rounds run out, **Phase** reads **Finished** with the **Winner**. If the session's Config names a `board`, every player's score is posted to that leaderboard exactly once, so a board that keeps a **Sum** grows by each session's score. **In C++**, `CrowdyKit::FindMatch` in `CrowdyGameKitMatch.h` is the same search, with a function for each update: ```cpp #include "CrowdyGameKitMatch.h" UCrowdyServerObject* Queue = GetGameInstance()->GetSubsystem()->Acquire(KitMatchmaker, TEXT("ranked"), this, Error); Search = CrowdyKit::FindMatch(Queue, 1000, /*bAutoAccept*/ true, /*TimeoutSeconds*/ 60.f, [this](const FCrowdyKitMatchProgress& Progress) { if (Progress.Stage == ECrowdyKitMatchStage::Matched) { JoinSession(Progress.Session); } UE_CLOG(Progress.Stage == ECrowdyKitMatchStage::Failed, LogTemp, Log, TEXT("No match: %s (%s)"), *Progress.Code.ToString(), *Progress.Reason); }); ``` `Search` is a `TSharedPtr`, whose `Accept()` and `Cancel()` match the node's and whose `IsOver()` says whether it ended; the search keeps itself going whether or not you hold it. `CrowdyKit::IsMyTurn(Session)` and `CrowdyKit::GetTurnPlayer(Session, Player)` read the turn. The Blueprint node is `UCrowdyFindMatchAction`, whose pins are `FCrowdyFindMatchUpdate` delegates, and the turn nodes are in `UCrowdyGameKitLibrary`. ## Trade between two players Two players swap in-game soft currency and items through a market; it is game state, never real money. The market moves nothing until both players accepted both offers, and until then what each player offers waits, held, in their own `player` hub. 1. Both players get `CSO_Kit_Market` with the same market name, such as `town`, and their own `CSO_Kit_Player` with an empty Instance Id, whose **Wallet** and **Inventory** they watch. 2. One player calls **OpenTrade** with the other player's user id as **With**. Its answer's **Trade** is the trade's id. The other player's **Trades** shows the trade a moment later, so **On Trades Changed (CSO_Kit_Market)** is where that player learns of it and reads its **Id**. 3. Each player calls **Offer** with the **Trade** and a **Bundle**, such as **Currency** `gold` 5. What they offer moves to their wallet's **Held** at once. A new offer from either player takes back both acceptances, so a player accepts only once the other's offer, in **Trades**, is what they agreed to. 4. Each player calls **AcceptTrade**. The first answer's **State** is **Open**; the second's is **Settling**, and the market moves each offer to the other player. Once it is **Done** the trade leaves both players' **Trades**, and each **On Wallet Changed (CSO_Kit_Player)** brings the new balances. **DeclineTrade** calls a trade off, and so does the market when the trade is still open after its `trade_ms` (five minutes by default); either way each offer goes back to the player who made it. A player can offer only what their wallet and inventory hold; **Offer** is refused otherwise. A player is in at most 10 open or settling trades, the market's `trades_max`, so nobody can fill another player's **Trades**: past that, **OpenTrade** is refused with `limit`, whichever of the two players is full. Set another number in `CSO_Kit_Market`'s **Config**, such as `{"trades_max": 3}`. Two players have at most one open trade with each other: **OpenTrade** while one is open answers that trade's id. A trade that is done or called off is gone, so a later **Offer**, **AcceptTrade** or **DeclineTrade** on it is refused `not_found`. ## Run a game mode on a starter `minigame`, `tactics`, `director`, `instance`, `territory` and `zone` each run one kind of match. Each keeps one hub per Instance Id, which is any name you choose: a table, a battle, a map, a run. Get it as any Server Object, with **Get Server Object** and the starter's definition, and the Instance Id of the match; the players of one match all get the same one. Every player id in their variables is a user id. A match is read from its variables, which everyone in it sees, and changed with the functions below; the hub decides what a move does, and a refusal comes back with the **Code** named under each. The `deck` is a card table and has [its own section](#play-a-card-game-with-a-hidden-hand) after these, since it also keeps a value only its player reads. The `arena` is a real-time fight and has [its own section](#fight-in-an-arena) at the end, since a player's client also [reports where the player is](#report-where-the-player-is). Two of them keep a board, `tactics` and `director`, and a board keeps its server's x and z axes: a cell is **X** and **Z** (`FCrowdyKitGridCell`), and z is not height. `territory` and `zone` are the world: z is up, as in Unreal, the ground is x and y, and positions are in chunks of 1600 units, as for the [npcs](#the-npcs-starters-area). Their definitions have [Reads World Data](./access-members-and-timers.md) on, since their hubs read where players are in the world: a client never says it stands on a point or inside the circle. A player's position is the chunk the platform tracks, to one chunk, and never the actor state the player's own client writes, so a client cannot place itself inside the circle or on a point. ### Duel two players In Oakford's tavern, two villagers play rock, paper, scissors at the table `tavern-1`. - **Join** seats a player. The second to sit starts the match, and its answer's `bStarted` says so. A third player is refused `full`. **Leave** stands up before the match starts or once it is over, and is refused `started` while it is under way. A caller who is not seated is refused `not_member` in every phase, and nothing changes. - **Throw** takes a **Hand**: `rock`, `paper` or `scissors`. Nobody sees a throw before both are in, or the round's time runs out: `Seats` says only whether each player has thrown (`bThrown`). Then `Last` holds the round's `Hands` in seat order, with empty text for a throw that was not in time, and its `Winner`, a user id, or 0 for a draw. A player who does not throw in time loses the round. Another hand is refused `bad_request`. - The first to win a majority of `BestOf` rounds (3 by default) wins: `Phase` reads **Finished** and `Winner` is the user id. **Rematch** plays again at the same table once it is over, and is refused `conflict` before. - `Deadline` is when the round closes, in Unix milliseconds, and 0 while no round waits on throws. A rematch times out its round too, also when the match is a best of 1, which the platform's own `minigame` does not. ### Fight a grid battle - **Join** brings a player's unit, up to 4 players. The first to join is the `Host`, and only the host calls **Start**; anyone else is refused `forbidden`. The enemies take the far side. - In the players' phase every unit may **Move** once, to an **X** and **Z** it can reach within its `Moves` and around obstacles and other units, and **Attack** once, with the **Target** unit's `Id` within its `Range`. **EndTurn** hands over early. A call out of turn is refused `not_your_turn`, a second move or attack `conflict`, and a cell or target too far `out_of_reach`. - When every player has ended their turn, or `PhaseEnds` passes, the hub's enemies move and strike, and `Round` goes up. `Phase` reads **Won** or **Lost** when a side has no unit standing. `Log` keeps the last 24 lines of what moved and hit. ### Defend against waves - **Join** takes up to 8 players. The first to join, or a developer, calls **Start**; anyone else is refused `forbidden`. - Enemies walk one lane from `Spawn` to `Base`, and an enemy that arrives takes from `BaseHp`. **Build** puts a tower on a free cell with **X** and **Z**, for gold; its answer is the `Gold` left. A cell that is taken is refused `conflict`, a cell off the field `bad_request`, too little gold `insufficient`, and a tower that would close the lane `blocks_lane`. **Hit** strikes the enemy with the **Enemy** id for its `Hp` left, and a player may strike again only after the Config's cooldown, or is refused `cooldown`. A hit still answers with the enemy's health at once. - While an encounter runs, the hub publishes only events on a hit or a build; the next tick, at most 500 ms later, carries the snapshot. - `Wave` is the wave under way, `Waves` how many there are, and `NextWaveAt` when the next one starts. The last wave brings a boss, and `BossPhase` reads 0 to 3 as it weakens. `Enemies` and `Towers` are what is on the field, and `Kills` counts each player's kills. - Its events arrive as one message, **EncounterEvent**, described in [React to a director's waves](#react-to-a-directors-waves). ### Send a party down a dungeon - **Open** starts the run for the player calling, who becomes its `Owner`; opening it again is refused `started`. **Invite** adds a player, which only the owner does, and the invited player calls **Join** (a player who was not invited is refused `forbidden`). A developer's **Open** may name the whole party in **Party**. - **Enter** resolves the room the party is at: a fight, a treasure, a rest or the boss. The hub rolls a fight from the party's strength, so `Rooms` shows each room's `Kind` and `bDone`, `Members` each member's `Hp`, and `Log` what happened. A member who is down is refused `forbidden`, and **Enter** on a cleared floor `conflict`. The owner calls **Descend** when the floor's rooms are done, refused `not_ready` before. **Abandon** ends the run, by the owner or a developer. - The run ends **Cleared** when the party clears the last floor, **Wiped** when nobody is standing, **Abandoned**, or **Expired** when nobody touched it for the Config's `idle_ms`. A call to a run that is over is refused `finished`, and to one nobody opened `not_ready`. - **Seed** decides the floors: the same seed makes the same dungeon. It is 0 to 2^63-1, so it fits an `int64` exactly, and a larger one is refused `bad_request`. In **Open**, **Has Seed** decides whether the **Seed** is sent: with it set the run uses your seed, and with it unset the hub draws one. Either way the answer's `Seed` is the one the run uses. The seed is in `Seed` for everyone in the run, so it gives away the whole dungeon. - With a board in its Config, such as `{"board": "oakford-deepest"}`, a run that is cleared or wiped posts each member's depth, the floors reached and rooms done as one score, to that leaderboard, once. Add the `leaderboard` starter for it, as for [a session's scores](#post-a-sessions-scores-to-a-leaderboard); when the post fails, the run's server log says "the run's depth did not reach board ...". With a `volume` in its Config, `Grid` is the id of the grid of the world reserved for the run, and 0 without one. ### Fight over control points - **Join** takes a **Faction**, one of the Config's `factions` such as `red`, once: a player keeps theirs, and a different one is refused `conflict`. A faction that is not in the Config is refused `bad_request`, and a war with all the members it takes `full`. Faction names must differ by more than case. - A faction captures a point by having more members standing on it than any other faction has. Each `Points` entry has its `Owner`, the faction `Capturing` and its `Progress` from 0 to 1, and `Present`, how many of each faction stood on it at the last scan. A point sits at ground x and y with a height z in the Config, in chunks; players within one chunk of its height count. - Points pay income to the faction that holds them; `Scores` and `Members` count by faction. The first faction to the Config's `win_score` wins: `Phase` reads **Won** and `Winner` is the faction. - **Reset**, for a developer, starts a new war. ### Shrink a circle - **Join** takes players, and the first to join, or a developer, calls **Start**; anyone else is refused `forbidden`. A match takes two (`not_ready`), and a full or running match refuses a join (`full`, `started`). - The circle holds, then shrinks phase by phase to the radii of the Config's `phases`. `Center` is its ground x and y, `Radius` what it is now, and `NextRadius` what it shrinks to in the phase `Stage`. Every player outside it loses health, and `Players` shows each one's `Hp`, whether they are `bOutside` at the last scan, and their `Place` once they fall, with the winner at 1. `Alive` counts those standing, and `Winner` is the last one. - Players more than one chunk above or below the circle's height, or more than 10 chunks from its centre on either ground axis, count as outside, so the first circle's radius is at most 10 chunks. Each scan makes more world reads than the platform's `zone` does: 21 at the default radius of 10, fewer as the circle shrinks, and 1 at the end. A scan that cannot read the world carries its damage over to the next good scan, up to 5 scan periods' worth. - The circle is scanned every 2 seconds by default (it was every second), which halves its world reads. Damage scales with the time a player spends outside, so a second outside costs the same as before. Set `scan_ms` in the Config, at least 250, to scan faster. - Its events arrive as one message, **ZoneEvent**, in the same way. The realtime ZoneChange event carries `centerZ` beside `centerY`, both the ground value. The `territory` and `zone` of the platform keep y up. The SDK's keep z up, so a war or a match the platform's starter saved starts again. ### Play a card game with a hidden hand In Oakford's tavern, two to six villagers play crazy eights at the table `tavern-2`: match the top card's suit or rank, an eight is wild and names the next suit, draw when you cannot play, and the first to empty their hand wins the points left in the others'. Add `deck`, deploy, and have the players at the table get `CSO_Kit_Deck` with the Instance Id `tavern-2`. A table is one game: when it is over, open a new Instance Id for the next one. - **Join** seats a player, up to six; a seventh is refused `full`, and a table that has started refuses a new player `started`. **Leave** stands up. During a game the leaver's cards go to the bottom of the deck, and when only one player is left that player wins. Someone else leaving keeps the current player's clock and their one draw; the last seat leaving on its own turn hands the turn to the first seat. The platform's own `deck` resets the clock and the draw instead, and hands the turn back a seat. **Start** deals five cards to each player and turns the first card up. Only the host, the first to join, calls it, and anyone else is refused `forbidden`; with fewer than two players it is refused `too_few_players`. - **Play** takes the **Card**, which is a card's `Id`, and, for an eight, the suit it names: set **Suit** and **Has Suit**. An eight with **Has Suit** unset is refused `bad_request`. A card that is not in the hand is refused `not_found`, one that does not go on the top card `not_playable`, and any move out of turn `not_your_turn`. Its answer is **Playing**, false when that card emptied the hand and ended the game. - **Draw** takes one card, once a turn, and its answer is the new hand (`FCrowdyKitDeckHand`). A second draw is refused `conflict`, and `empty` when no cards are left. **Pass** ends a turn after drawing, and is refused `conflict` before. - A turn lasts 30 seconds. A player who runs out of time draws, if they had not, and passes; `Last.Action` reads **Timeout**. - **GetState** answers the table, and **GetHand** the caller's own hand, which the object reads for you. The table's variables are public, and never show a card in anyone's hand: - `Phase` reads **Lobby**, **Playing** or **Finished**. `Players` are the user ids in seat order, `Host` the first to join, `Turn` the player whose turn it is and `TurnEnds` when it runs out, in Unix milliseconds; `Turn` and `TurnEnds` read 0 outside **Playing**. - `HandCounts` says how many cards each player holds, by user id, `DrawCount` how many are left to draw, and `Top` the card on the table. - `Suit` is the suit in play, which an eight changes. It has **Has Suit** beside it, false in the lobby, as for [a value that may be none](#read-a-value-that-may-be-none). - `Last` is the last move (`FCrowdyKitDeckPlayed`): its `Player`, its `Card` (empty for a draw, a pass or a timeout) and its `Action`, **Play**, **Draw**, **Pass** or **Timeout**. `Player` reads 0 before the first move. - `Winner` is the user id of the first to empty their hand, and `Scores` holds each player's points: the winner is given what the others' hands are worth, eights 50, tens and faces 10, aces 1 and any other card its number. The `Hand` is the [value only this player reads](#values-only-the-player-reads). It holds `Cards`, every card in the signed-in player's hand, `Playable`, and `bDrawn`, whether they have drawn this turn. Nobody else's cards reach the game, and a player who is not seated reads an empty hand. The object reads it after every read of the table, and again when the table says it changed for the signed-in player: after they play or draw, and also when their turn begins and when it ends, so `Playable` is always the server's own list of the cards that may go now, and it is empty when it is not their turn. Use it for the controls and do not work the rule out in the game. Right after a turn moves, **On Turn Changed** can run before the new `Hand` arrives, and when no card in the hand can go, the `Hand` does not change at all as the turn begins, so **On Hand Changed** does not run then: refresh the controls from both. A card (`FCrowdyKitCard`) is its `Id`, the text **Play** takes, a `Rank`, 1 for an ace up to 13 for a king, with the jack 11 and the queen 12, and a `Suit`, **Spades**, **Hearts**, **Diamonds** or **Clubs**. A Blueprint can **Switch on ECrowdyKitCardSuit** and pick a card's art from its rank and suit, without reading the text. A card that is none, such as `Top` in the lobby, has an empty `Id`. **In Blueprint**, in the tavern table's widget: 1. Add **On Hand Changed (CSO_Kit_Deck)** and wire **Construct** into its **Bind**, with the Server Object from **Get Server Object** as its **Target**. From **Changed**, add **Get Hand** and show a card for each of its **Cards** with **For Each Loop**, using **Break Crowdy Kit Card** and a **Switch on ECrowdyKitCardSuit** to choose its art. 2. Add **On Turn Changed (CSO_Kit_Deck)** bound the same way, and from both events call one function that enables the buttons: **Play** only for a card that is in the hand's **Playable**, and **Draw** while it is this player's turn and **bDrawn** is false. **Pass** is for **bDrawn** true. The table's **Turn** is a user id, so compare it with **Get User ID** on the Crowdy Game Session. 3. On the button, add **Call Play (CSO_Kit_Deck)** with **Card** set to the card's **Id**. For an eight, set **Suit** to the suit the player picked and tick **Has Suit**. **Call Draw**, **Call Pass**, **Call Join**, **Call Start** and **Call Leave** are the other buttons. Under **On Failed**, **Code** says why, such as `not_playable`. [Blueprint snippet: kit-deck-play, see /bp/unreal-sdk/kit-deck-play.txt] **In C++**, with `KitDeck` a `UPROPERTY` holding `CSO_Kit_Deck`, the villager's table plays the first card the server says may go: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" FString Error; Table = GetGameInstance()->GetSubsystem()->Acquire(KitDeck, TEXT("tavern-2"), this, Error); Table->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, TConstArrayView Fields) { if (Fields.Contains(TEXT("hand")) || Fields.Contains(TEXT("turn"))) { PlayFirstPlayable(); } })); ``` ```cpp void ATavernTable::PlayFirstPlayable() { const FCrowdyKitDeckState* State = Table->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitDeckPhase::Playing || State->Turn != Table->GetSignedInUserId() || State->Hand.Playable.IsEmpty()) { return; } const FCrowdyKitCard& Card = State->Hand.Playable[0]; FCrowdyKitDeckPlayInputs Inputs; Inputs.Card = Card.Id; Inputs.Suit = ECrowdyKitCardSuit::Hearts; Inputs.bHasSuit = Card.Rank == 8; Table->Call(TEXT("Play"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("Not played: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); } ``` **Draw** and **Pass** are `Table->Call(TEXT("Draw"), FInstancedStruct())` and `Table->Call(TEXT("Pass"), FInstancedStruct())`. A draw's `Result.Reply` holds the new `FCrowdyKitDeckHand`, and the table's `Hand` changes with it. To play another game, replace `playable` and `card_points` in the crate's `Server/deck/src/lib.rs`: `playable` says which cards may go on the top card, and `card_points` what a card left in a hand is worth. Keep the function names and the `table` topic as they are, as for [any starter](#change-a-starter). The platform's [Card tables](/game-kit/modules/tables) page has the rest of the starter. **Add starter** gives the deck's **Config** a random `secret`, such as `{"secret": "6f1c..."}`, and the hub mixes it into every shuffle. Everything else a deal is seeded from, the table's name, the time and the epoch, is something a player can see, so without the secret a modified game could work out every hand. Keep it when you edit the Config, and give the deck one of your own if you add it with a Config of yours. The **Config** stays in the editor and is never cooked into the game. The player starter's loot rolls use a `secret` the same way, and **Add starter** writes one for it too. If a Config of yours leaves the `secret` out, Deploy warns about it for the `deck` and the `player`; a `secret` that is not a string is a problem that blocks Deploy. ### Read a value that may be none Blueprint cannot leave a value out, so where a variable may be none, the struct has a **Has** flag beside it, and the value means nothing while the flag is false. - An index that may be none has a flag. In the director's state, **Wave** is valid only while **Has Wave** is true, which is false before the first wave starts and after the last. In the zone's, **Stage** is valid only while **Has Stage** is true, which is false until the first warning. In C++ these are `bHasWave` and `bHasStage`. A flag changes with its value, so it has a **Get** node and no **On Changed** of its own: **On Wave Changed (CSO_Kit_Director)** runs when either changes, and **Get bHasWave (CSO_Kit_Director)** reads the flag there. - For an input, the flag decides whether the value is sent. **Open** on the instance sends **Seed** only when **Has Seed** is set; with it unset the hub draws a seed and the **Seed** you set is ignored. - Anything else that is never 0 reads 0 for none: a user id such as `Winner` or `Host`. A time reads 0 when nothing is waiting, such as `Deadline` and `NextWaveAt`, and text reads empty, such as a point's `Owner`. In C++, with `Gate` acquired as [below](#react-to-a-directors-waves) does: ```cpp const FCrowdyKitDirectorState* State = Gate->GetState().GetPtr(); if (State && State->bHasWave) { WaveText->SetText(FText::Format(INVTEXT("Wave {0} of {1}"), State->Wave + 1, State->Waves)); } ``` ### React to a director's waves `director`, `territory` and `zone` send their events as a single message each, **EncounterEvent**, **TerritoryEvent** and **ZoneEvent**, rather than a message for each kind of event. Its `Type` says which event it is, and its other fields carry what that event has: only the fields of its `Type` are filled, and the rest read 0 or empty. `Type` has a flag beside it, **Has Type** (`bHasType`): it is false when this client does not know the event's type, as when the server is newer than the game, and then you ignore the event. An event arrives just before the snapshot that shows its effect, so read the event's own fields; the State catches up on the next snapshot. The territory's `Type` is **Captured**, **Income** or **Won**; the zone's is **Warning**, **Shrinking**, **Settled**, **Final**, **Eliminated** or **Won**; the director's is **Wave Started**, **Wave Cleared**, **Boss Phase**, **Won** or **Lost**. A message is an event and not a value, so read the variables for what is true now; see [Hear its messages](./from-blueprint/hear-server-messages.md). Oakford's village gate is attacked in waves. Add `director`, deploy, and have the gate's players get `CSO_Kit_Director` with the Instance Id `gate-defense`. The gate rings its bell for each wave and shows the boss's phase. **In Blueprint**, in the gate's event graph: 1. Add **On EncounterEvent Received (CSO_Kit_Director)** and wire **Event BeginPlay** into its **Bind**, with the Server Object from **Get Server Object** as its **Target**, or the actor's **Crowdy Server Object** component. 2. From **Received**, add a **Branch** on **Has Type**, and from its true pin a **Switch on ECrowdyKitDirectorEventType** wired to the **Type** pin. 3. On **Wave Started**, ring the bell and show **Wave** plus 1, since it counts from 0. On **Boss Phase**, show **Phase**, 1 to 3. On **Won** and **Lost**, show the result. **Wave Cleared** needs nothing here: the last wave's end is **Won**, not **Wave Cleared**. Here the gate's **Crowdy Server Object** component holds `CSO_Kit_Director`, so the node's **Target** is left empty: [Blueprint snippet: kit-director-event, see /bp/unreal-sdk/kit-director-event.txt] **In C++**, with `KitDirector` a `UPROPERTY` holding `CSO_Kit_Director`: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" FString Error; UCrowdyServerObject* Gate = GetGameInstance()->GetSubsystem()->Acquire(KitDirector, TEXT("gate-defense"), this, Error); if (!Gate) { return; } Gate->WatchMessage(TEXT("EncounterEvent"), FOnCrowdyServerMessage::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, FName Message, const FInstancedStruct& Payload) { const FCrowdyKitDirectorEvent* Event = Payload.GetPtr(); if (!Event || !Event->bHasType) { return; } switch (Event->Type) { case ECrowdyKitDirectorEventType::WaveStarted: Bell->Play(); UE_LOG(LogTemp, Log, TEXT("Wave %d is coming"), Event->Wave + 1); break; case ECrowdyKitDirectorEventType::BossPhase: UE_LOG(LogTemp, Log, TEXT("The boss weakens: phase %d"), Event->Phase); break; case ECrowdyKitDirectorEventType::Won: case ECrowdyKitDirectorEventType::Lost: ShowResult(Event->Type == ECrowdyKitDirectorEventType::Won); break; default: break; } })); ``` The territory and zone work the same way with `FCrowdyKitTerritoryEvent` and `FCrowdyKitZoneEvent`: on a zone's **Warning**, `Phase`, `ToRadius` and `ShrinksAt` say how and when the circle will shrink, and on **Eliminated**, `Player` and `Place` say who fell and where they placed. ### Report where the player is Some starters decide what a player may do from where the player is: the `arena`, the `race` and the `ball`. Their definitions say so in the **Game Kit** category: **Position Function** names the Server Function that takes the position (`Move` on each of the three), and **Position Reports Per Second**, from 1 to 10 and 5 by default, says how often to send it. A definition is marked as a problem when the function is not there, is **Send Once**, takes inputs without `X` and `Y` doubles, or answers something other than nothing or a struct with `X` and `Y`. A starter with no Position Function reports nothing. You do not call that function. The **Crowdy Server Object** component on the player's pawn holding such a starter sends it for you, from the moment the Server Object is **Ready**: - **Position Report** (`PositionReport`, an `ECrowdyPositionReport`) says who is reported. **Player Pawn**, the default, reports only when the owner is a pawn this game's player controls, so an AI-controlled pawn, or an actor such as a scoreboard that holds the arena, never reports. **This Actor** reports any owner, for an actor that stands in for the player, such as a vehicle or a camera rig. **Off** turns the reports off. - **Snap To Kept Position** (`bSnapToKeptPosition`, on by default) corrects the owner when the server kept another place than the one reported. It moves the owner by the server's correction, the place the server kept minus the place that was sent, from where the owner is now, so movement made while the answer was on its way is kept. - **On Position Kept** (`OnPositionKept`, an `FOnCrowdyPositionKept`) runs when the server kept another place, with the **Location** the owner was moved to, in Unreal units, at the owner's own height. It runs after the owner was moved when Snap To Kept Position is on, so a stumble effect or a camera nudge belongs there. - In C++, `ReportsPosition()` on a definition (`UCrowdyServerObjectDefinition`) says whether it has a Position Function, so whether its components report at all. ![The Crowdy Server Object component's Position rows: Position Report set to Player Pawn and Snap to Kept Position ticked](/img/unreal-sdk/kit-position-report-component.png) When several components hold the same Server Object, one of them reports; another takes over when it leaves. How it reports: - **Positions are world metres.** The report's **X** and **Y** are the owner's location in Unreal units over 100: a pawn at 20000, 12000 reports 200, 120. Only the ground is sent. Height stays the owner's own, also after a snap. - **One report is in flight at a time, and the newest wins.** When the last report has not been answered by the time the next is due, that one is skipped, so a slow moment never leaves a queue of old places in front of the new one or in front of the starter's other calls. A player who stands still, moved by less than a centimetre, is reported once a second. - **A refusal holds the reports.** When the hub refuses a report, because the player is not a fighter yet, the fight is not running, the fighter is down or was removed, the component sends no more for at least 1 second, and after that until the object's State changes, and at most 5 seconds. When the report did not reach the server, it waits 1 second. Reports stop when the object is released or the player leaves, and start again when it is **Ready** again. - **With Player Pawn, only a pawn this game controls is reported.** A pawn another player drives never is: that player's own game reports it. - **A paused game keeps reporting.** The reports run on real time, not game time, so a player who opens a pause menu in the middle of an arena fight is not benched as idle. - **The call budget.** The Game Kit's client keeps its calls under 100 every 10 seconds. At 5 reports a second the component uses half of that, which leaves the rest for **Cast**, **Join** and the other calls; lower the rate in the definition if your game casts a lot. Snapping is what keeps a player honest and in the fight. The hub keeps what the player's class allows: it cuts a move that is too fast to the farthest place the class's speed reaches, keeps a fighter where they were after a jump of the Config's `teleport` metres or more, and answers with the place it kept. When that place is more than 5 centimetres from the one reported, the component teleports the owner by the difference. Without that the pawn would stay ahead of its fighter and every report would be cut again. A jump of `teleport` metres or more, and a cut of more than 1 metre, each count as a strike against the player. Strikes clear only after 10 seconds without one, and `remove_after_strikes` of them (8 by default) remove the fighter for the rest of the fight. Turn **Snap To Kept Position** off only if you move the owner yourself from **On Position Kept**. The `territory` and `zone` starters do not use reports: they read where players are from the world, as [above](#run-a-game-mode-on-a-starter) says. **In Blueprint**, there is nothing to wire: add a **Crowdy Server Object** component to the pawn, give it `CSO_Kit_Arena` and the arena's Instance Id, and leave **Position Report** on **Player Pawn** and **Snap To Kept Position** on. Until the player has **Join**ed, the hub refuses the reports, and the component holds them for at least a second and then until the State changes, or 5 seconds at most. To react to a snap, select the component and add **On Position Kept** from its Events. **In C++**, the component's event is a dynamic delegate: ```cpp #include "CrowdyServerObjectComponent.h" UCrowdyServerObjectComponent* Reporter = FindComponentByClass(); if (Reporter) { Reporter->OnPositionKept.AddDynamic(this, &AYardPawn::HandlePositionKept); } ``` ```cpp void AYardPawn::HandlePositionKept(FVector Location) { PlayStumbleEffect(Location); } ``` ### Fight in an arena In Oakford's practice yard, villagers fight in the arena `yard-1`. Add `arena`, deploy, and have the fighters get `CSO_Kit_Arena` with the Instance Id `yard-1`. There is one fight for each Instance Id, so a second yard is a second name. The arena is a rectangle of the world. In `CSO_Kit_Arena`'s **Game Kit** category, **Config** places it: `origin` is its corner in world metres, `width` runs along X from there and `depth` along Y, 40 each by default. The yard is a 40 by 40 metre square at 200, 120, so its Config is `{"origin": [200, 120]}`. The hub keeps fighters inside the rectangle, so build the yard's floor to match, and change the level and the Config together. A fight that is already under way keeps the Config it started with. The other keys, each with the default it keeps when the Config leaves it out: - `spawns`, offsets from the origin in metres where the fighters stand when the fight starts (the four corners, 5 metres in), and `classes`, the classes and their abilities (`warrior` with `strike` and `slam`, `mage` with `bolt` and `ignite`). - `players_max` (10), `kill_target` (10), `duration_ms` (300000, five minutes) and `respawn_ms` (3000). - `teleport` (12), the metres a reported move may jump at once, and `remove_after_strikes` (8), the strikes that remove a fighter for the rest of the fight. A strike is a jump of `teleport` metres or more, or a speed cut of more than 1 metre, and strikes clear only after 10 seconds without one. - `idle_ms` (15000), how long a running, standing fighter may go without a report before they are benched; 0 never does. A paused game keeps reporting, so a pause menu does not bench a fighter. - `realtime` (false) and `height` (0): with `realtime` on, casts, impacts and the hub's movement flags are also sent on the realtime channel to the players around the arena's centre, at `height` metres. The functions: - **Join** takes a **Hero Class**, `warrior` or `mage`. Another class is refused `bad_request`, a full arena `full`, and a different class once the fight is running `started`. A player may join a fight that is already running. - **Start** is called by the first to join, or a developer; anyone else is refused `forbidden`, and one fighter is refused `not_ready`. It puts every fighter at a spawn point. A player's next report, if it comes from farther than the player could have moved, is answered with the spawn point, a success and not a refusal, and the component moves the player there; a player already nearer stays where they are. - **Cast** takes an **Ability**, such as `strike`, and an aim **X** and **Y** in world metres, not Unreal units. A `strike` or `ignite` lands at once on the fighter nearest the aim, within 1.5 metres of it, and the answer's `bHit` says whether it hit someone; `bolt` flies on from the caster, and `slam` goes off at the aim after a short delay, so their answer is false and the hits arrive in the State and the events. A cast is refused `cooldown`, `insufficient` (the class's resource), `out_of_reach` (the aim is farther than the ability reaches), `not_found` (the class has no such ability), `conflict` while the fighter is down, `not_ready` before the fight runs and `not_member` before **Join**. - **Leave** stands the player down. **Move** is the position function the component calls for you. **GetState** answers the State. The State, which everyone in the fight sees: - `Phase` reads **Waiting**, **Running** or **Finished**. `Origin`, `Width` and `Depth` are the Config's, so a client can draw the yard. - `Fighters` has an `FCrowdyKitArenaFighter` each: `Player`, `HeroClass`, `X` and `Y` in world metres, `Hp` and `MaxHp`, `Resource`, `Statuses` (such as `burning`), `Kills`, `Deaths` and `bDown`. To place an actor at a fighter, multiply `X` and `Y` by 100. - `Projectiles` are the shots in the air (`Ability`, `Caster`, `X`, `Y`), and `Areas` the blasts waiting to go off (`Ability`, `Caster`, `X`, `Y`, `Radius` and `At`, in Unix milliseconds). - `KillTarget` is the kills that win. `EndsAt` is when the time runs out, in Unix milliseconds, and 0 outside **Running**. `Winner` is a user id, and 0 until the fight is won, and when the time ran out with the most kills tied. A fighter whose health runs out is down (`bDown`) for `respawn_ms`, and cannot cast or move meanwhile. Then they come back at their spawn point with full health and no statuses, and a burn that kills credits whoever set it. The first to `KillTarget` wins, or the most kills when `EndsAt` passes. The events arrive as one message, **ArenaEvent**, heard as the director's is: its `Type` says which event it is, with **Has Type** false for a type this client does not know, and only the fields of that `Type` are filled. **Cast** has `Ability`, `Caster`, `X` and `Y`; **Impact** `Ability`, `Caster`, `Victim` and `Damage`; **Kill** `Killer` and `Victim`; **Respawn** `Player`; **Violation** `Player`, `What` and `Detail`, for a move the hub cut or held back; **Removed** `Player` and `Why`, `movement` or `idle`; and **Finished** `Winner`. An event arrives just before the snapshot that shows its effect. Where the SDK's copy differs from the platform's [Arenas](/game-kit/modules/arenas): - **Positions are world metres, z up.** The platform's keep x and z on the ground inside `(0, 0)` to `(width, depth)`, and place an arena in the world with an `anchor` chunk. The SDK's `Move`, `Cast`, State and events are in the world's own metres, ground X and Y, and the arena sits where `origin` says. - **A NaN or infinite aim or move is refused `bad_request`,** and spends nothing, not even the cooldown. - **Leaving and joining again keeps your record.** A fighter who leaves while the fight runs is set aside with their class, health, kills, deaths, cooldowns, resource and respawn, and **Join** brings them back as they were. A fighter removed for movement is set aside as removed: their **Join** is refused `forbidden` until the next **Start**. - **The movement check is a travel budget, and its slack does not grow with the report rate.** The platform's allows half a metre more on every report, so a client that reports often could move about twice as fast as its class. The SDK's holds a sustained speed to 1.25 times the class's speed whatever the rate. A late report's unused allowance covers the next one, and that banking stops at about a second of travel, so a player on a slow connection, whose reports arrive further apart, is not cut short. Only a cut of more than 1 metre counts against a player toward being removed. - **The hub's own placements never count against a player.** Join, start, a respawn and a rejoin put the fighter somewhere. A report after one from farther than the player could have moved is answered with the placed point, a success and not a refusal, and the component moves the player there; a nearer player stays where they are. A player pressing against the arena's edge is clamped to it without a strike. - **A fighter who leaves stays a target for `respawn_ms`** for projectiles, areas and burns, at the spot they left, and then is not; a fighter benched as idle is never a target. An instant ability, such as `strike`, also needs its target within the ability's range of the caster, not only of the aim. At most four times `players_max` set-aside records are kept. - **An idle fighter is benched.** A running, standing fighter with no report for `idle_ms` is set aside with a **Removed** event whose `Why` is `idle`, and rejoins as they were. The platform's keeps them. - **`Resource` is current.** The State shows a fighter's resource with its regeneration up to now. The platform's shows the value from their last cast. **In Blueprint**, in the yard pawn's event graph, with the **Crowdy Server Object** component holding `CSO_Kit_Arena` and `yard-1`: 1. On the join button, add **Call Join (CSO_Kit_Arena)** with **Hero Class** set to `warrior`. The first player to join then presses another button wired to **Call Start**. 2. On a click in the yard, add **Call Cast (CSO_Kit_Arena)** with **Ability** `strike` and the clicked point's X and Y divided by 100 into **X** and **Y**. Under **On Failed**, **Code** says why, such as `cooldown`. 3. Add **On ArenaEvent Received (CSO_Kit_Arena)** and wire **Event BeginPlay** into its **Bind**, with the component as its **Target**. From **Received**, add a **Branch** on **Has Type**, and a **Switch on ECrowdyKitArenaEventType** on its true pin. On **Kill**, show **Killer** and **Victim**; on **Finished**, show **Winner**. 4. To draw the fighters, add **On Fighters Changed (CSO_Kit_Arena)** on the same object and place an actor at each fighter's **X** and **Y** times 100. **In C++**, with `Arena` the acquired `CSO_Kit_Arena` (`Acquire(KitArena, TEXT("yard-1"), this, Error)`), the pawn strikes the nearest fighter that is standing. Acquiring the arena this way does not report the pawn's place: the pawn still needs the **Crowdy Server Object** component holding `CSO_Kit_Arena` to report, or the game must call **Move** itself. Otherwise the fighter never moves on the hub and is benched as idle after `idle_ms`. ```cpp void AYardPawn::StrikeNearest() { const FCrowdyKitArenaState* State = Arena->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitArenaPhase::Running) { return; } const FVector2D Me(GetActorLocation().X / 100.0, GetActorLocation().Y / 100.0); const FCrowdyKitArenaFighter* Nearest = nullptr; double NearestDistance = TNumericLimits::Max(); for (const FCrowdyKitArenaFighter& Fighter : State->Fighters) { const double Distance = FVector2D::Distance(Me, FVector2D(Fighter.X, Fighter.Y)); if (Fighter.Player == Arena->GetSignedInUserId() || Fighter.bDown || Distance >= NearestDistance) { continue; } Nearest = &Fighter; NearestDistance = Distance; } if (!Nearest) { return; } FCrowdyKitArenaCastInputs Inputs; Inputs.Ability = TEXT("strike"); Inputs.X = Nearest->X; Inputs.Y = Nearest->Y; Arena->Call(TEXT("Cast"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { const FCrowdyKitArenaCastReply* Reply = Result.Reply.GetPtr(); UE_CLOG(Reply && Reply->bHit, LogTemp, Log, TEXT("Hit")); UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("Not cast: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); } ``` The ArenaEvent is heard with `WatchMessage` and an `FCrowdyKitArenaEvent`, as the director's is in [React to a director's waves](#react-to-a-directors-waves). The platform's [Arenas](/game-kit/modules/arenas) page has the rest of the starter. ### Run a race On Oakford's fair day, villagers race their carts around the mill loop, `mill-loop-1`. Add `race`, deploy, and have each racer's pawn hold `CSO_Kit_Race` with that Instance Id in a **Crowdy Server Object** component, which [reports where the racer is](#report-where-the-player-is). A race runs once for each Instance Id: the next heat is a new name, such as `mill-loop-2`. The course is a list of gates in the world. In `CSO_Kit_Race`'s **Game Kit** category, **Config** sets it: `gates` is a list of `{"x", "y", "radius"}`, each a ring on the ground in world metres, and `laps` how many times round. The first gate is the start and finish line. A racer passes the gates in order, and a gate counts when the path between two of their reports comes within its radius. The default is a triangle of three gates at 0, 0 and 30, 0 and 15, 25, 3 metres across, twice round. The other keys, each with the default it keeps when the Config leaves it out: - `course_id` (`loop`), the course's name in the realtime timing events; `countdown_ms` (3000) and `racers_max` (8). - `rules`, the movement check: `max_speed` (12 metres a second), `teleport` (20 metres at once), `bounds` (none: a `[x0, y0, x1, y1]` rectangle in world metres keeps racers inside) and `grace` (1.25). The hub allows up to `grace` times `max_speed`, so set `max_speed` to your cart's top speed over 1.25 for a strict limit. - `finish_grace_ms` (30000), how long the others have once the winner is in, and `time_limit_ms` (600000, ten minutes), how long after the start the race closes whoever is still out; 0 is no limit. - `idle_ms` (15000), how long a racer who has not finished may go without a report before they retire; 0 never does. - `board`, a leaderboard's Instance Id: with one, a closed race posts each finisher's time to it in milliseconds. Configure that board with `order: "low"`, so the fastest time ranks first, and add the `leaderboard` starter yourself. - `realtime` (false) and `height` (0): with `realtime` on, timing is also sent on the realtime channel to the players around the start line, at `height` metres. The functions: - **Join** puts the player on the start line; it is refused `started` once the countdown begins, unless the player is already racing, and `full` past `racers_max`. A player's next report from farther than they could have moved is answered with the line, a success and not a refusal, and the component moves them there. - **Start** is called by the first to join, or a developer; anyone else is refused `forbidden`, and a race with no racers `not_ready`. It begins the countdown, which holds every racer on the line: their reports are answered with it. - **Leave** takes a player off the grid in the lobby. Once the countdown begins, it retires them: they did not finish and cannot join again. - **GetGhost** answers the winner's run, `FCrowdyKitRaceGhost`: `Player`, `TotalMs`, and `Frames`, each an `FCrowdyKitRaceGhostFrame` with an `AtMs` from their start and an `X` and `Y` in world metres, to replay as a ghost cart. Before anyone has won, `Player` is 0 and `Frames` is empty. - **Move** is the position function the component calls for you. **GetState** answers the State. The State, which everyone sees: - `Phase` reads **Lobby**, **Countdown**, **Racing** or **Finished**. `CourseId`, `Gates` (`X`, `Y` and `Radius` each) and `Laps` are the Config's, so a client can draw the course. - `GoAt` is when the countdown ends, `ClosesAt` when the finish grace runs out, and `EndsAt` when the time limit does, in Unix milliseconds, each 0 when there is none. - `Racers` has an `FCrowdyKitRacer` each: `Player`, `X` and `Y` in world metres, `Lap`, `NextGate` (the index in `Gates` of the gate to pass next), `BestLapMs`, `Place` and `TotalMs` once they finish, and `bDidNotFinish`. A value that is not there yet reads 0. The events arrive as one message, **RaceEvent** (`FCrowdyKitRaceEvent`), heard as the arena's is: **Go** has `At`; **Timing** has a `Stage` (an `ECrowdyKitRaceStage`: **Started**, **Checkpoint**, **Lap** or **Finished**, with **Has Stage** false for one this client does not know), `Player`, `Checkpoint`, `Lap` and `ElapsedMs`; **Violation** has `Player`, `What` and `Detail`, for a report the hub cut or held back; **Retired** has `Player` and `Why`, `left` or `idle`; and **Finished** has `Results`, an `FCrowdyKitRaceResult` for every racer with their `Player`, `Place` and `TotalMs`, the finishers first in place order and then, with 0 for both, those who did not finish. Where the SDK's copy differs from the platform's [Races and ball games](/game-kit/modules/racing): - **Positions are world metres, z up,** for the gates, `Move`, the State, the ghost and the events. The platform's keep x and z in its own units from the course's origin. - **A NaN or infinite position is refused `bad_request`,** and changes nothing. - **The movement check is the arena's travel budget,** so a client that reports more often does not move faster, a slow connection is not cut short, and the hub's own placements on the line never count against a racer. - **No flying start.** The countdown holds racers on the line and the start sets them there with no travel saved, so a racer cannot be past the line at the go. - **A gate between two reports counts,** and its time is when the path crossed it, not when the report arrived. The platform's checks only the reported points, so a racer who reports less often could miss a gate, and is timed later. - **A race always ends.** It closes when every racer has finished or retired, `finish_grace_ms` after the winner, or at `time_limit_ms`. A racer who leaves or stops reporting retires. The platform's waits for every racer to finish. - **Snapshots are not sent for every report:** at most five a second, as the arena's, and one at once when the race starts, goes, or closes. - **Results list every racer, the finishers first in place order,** and a time reaches the board whatever the length of the race's Instance Id. The ghost covers the whole run, however long. **In Blueprint**, with the **Crowdy Server Object** component holding `CSO_Kit_Race` and `mill-loop-1`: 1. On the join button, add **Call Join (CSO_Kit_Race)**. The first to join then presses a button wired to **Call Start**. 2. Add **On RaceEvent Received (CSO_Kit_Race)**, bound at **Event BeginPlay** with the component as its **Target**. From **Received**, **Switch on ECrowdyKitRaceEventType**: on **Go**, hide the countdown; on **Timing**, show **Lap** and **Elapsed Ms** when **Player** is the signed-in player; on **Finished**, show the podium from **Results**. 3. Add **On Racers Changed (CSO_Kit_Race)**, and for the signed-in player's racer, light the gate at **Next Gate** in the State's **Gates**. **In C++**, with `Race` the acquired `CSO_Kit_Race`, the pawn finds the gate to drive to next: ```cpp bool AMillCart::NextGate(FVector& OutWhere) const { const FCrowdyKitRaceState* State = Race->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitRacePhase::Racing) { return false; } const int64 Me = Race->GetSignedInUserId(); const FCrowdyKitRacer* Mine = State->Racers.FindByPredicate([Me](const FCrowdyKitRacer& Racer) { return Racer.Player == Me; }); if (!Mine || Mine->bDidNotFinish || Mine->Place > 0 || !State->Gates.IsValidIndex(Mine->NextGate)) { return false; } const FCrowdyKitRaceGate& Gate = State->Gates[Mine->NextGate]; OutWhere = FVector(Gate.X * 100.0, Gate.Y * 100.0, GetActorLocation().Z); return true; } ``` ### Play a ball game On the village green, two teams play ball in `green-1`. Add `ball`, deploy, and have each player's pawn hold `CSO_Kit_Ball` with that Instance Id in a **Crowdy Server Object** component. The pitch is a rectangle of the world. In `CSO_Kit_Ball`'s **Game Kit** category, **Config** places it: `origin` is its corner in world metres, `width` runs along X from there (60 by default) and `depth` along Y (40), and each end wall, at `origin` and at `origin` plus `width`, has a goal `goal_width` wide (12) at its middle. Red attacks the far end, at `origin` plus `width`, and blue the near end. To draw the ball, set `realtime` to `true` and give **Kit Actors**' kind `ball` your ball's class, as [Show a starter's actors](#show-a-starters-actors) says: the ball is then shown to every player within reach of the pitch, at `height` metres (0 by default). With `realtime` on, the match's Instance Id may be at most 29 characters, with no `@` or spaces, since the ball's actor carries it. The other keys, each with its default: - `team_max` (5), `score_to_win` (3) and `duration_ms` (300000, five minutes). - `claim_radius` (1.5 metres), how near the ball a player must be to claim it, and `protect_ms` (1500), how long a new holder cannot be robbed. - `pass_speed` (18) and `shoot_speed` (28) in metres a second, and `friction` (0.8), the share of its speed a loose ball loses each second. - `max_speed` (9), `teleport` (15) and `grace` (1.25), the movement check as the race's. - `idle_ms` (15000), how long a player may go without a report while the game is played before they are taken off the pitch; 0 never does. The functions: - **Join** takes a **Team** (`FCrowdyKitBallJoinInputs`), **Red** or **Blue**, and puts the player at the middle of their half; a full team is refused `full`. Before the game and after it, Join again changes team. While it is played, a player keeps their team (another is refused `started`), and one who left comes back where they left, so leaving and joining is not a shortcut across the pitch. - **Start** is called by the first to join, or a developer, and needs a player on each team (`not_ready`). It puts every player at the middle of their half and the ball at the centre. - **Claim** takes a loose ball within `claim_radius`, or the ball from an opponent once their `protect_ms` is over. It is refused `out_of_reach`, `conflict` when a teammate has it, and `cooldown` while the holder is protected or when claiming twice within half a second. - **Pass** takes **To** (`FCrowdyKitBallPassInputs`), a teammate's user id (`bad_request` for anyone else), and **Shoot** an aim **X** and **Y** in world metres; both are refused `conflict` when the player does not have the ball. The ball runs on, slows, and turns off the side and end walls; it scores when it crosses an end wall inside the goal. - **Leave** takes the player off the pitch; a ball they held drops loose where they were. **Move** is the position function. **GetState** answers the State. The State, which everyone sees: - `Phase` reads **Lobby**, **Playing** or **Finished**; `Origin`, `Width`, `Depth` and `GoalWidth` are the Config's. - `Ball` is an `FCrowdyKitBall`: `X` and `Y` in world metres, `VX` and `VY` in metres a second, `Holder` (0 when loose), `HeldSince` and `LastTouch`. A held ball is where its holder is. - `Players` has an `FCrowdyKitBallPlayer` each: `Player`, `Team`, `X` and `Y`. `Score` is red's then blue's. `EndsAt` is when time runs out, 0 outside a game being played, and `Winner` the winning team when **Has Winner** is true (false before the end, and for a draw). The events arrive as one message, **BallEvent** (`FCrowdyKitBallEvent`, whose `Type` is an `ECrowdyKitBallEventType`): **Claimed** (`Player`, `Team`, and `From`, the user id they took it from, 0 for a loose ball), **Passed** (`Player`, `To`, `Team`), **Shot** (`Player`, `Team`), **Goal** (`Team`, `Player`, the last to touch it, and `Score`), **Violation** (`Player`, `What`, `Detail`), **Left** (`Player`, `Team`), **Removed** (`Player`, `Team`, and `Why`, `idle`) and **Finished** (`Winner` with **Has Winner**, and `Score`). Where the SDK's copy differs from the platform's [Races and ball games](/game-kit/modules/racing): - **Positions are world metres, z up,** and `origin` places the pitch. The platform's keep the pitch at its own origin, and show the ball at an `anchor` chunk. - **The ball's actor names its match.** Its **Instance Id** on the Crowdy Kit Actor component is the match's, so a client with several games near each other knows which ball is whose; it reaches every player within the pitch's size of it. - **A NaN or infinite position or aim is refused `bad_request`,** and changes nothing. - **There is a Leave, and a player may join a game being played,** at their place when they come back. The platform's has no way to leave and refuses a join once the game starts, so a player who dropped could never return. - **A player who stops reporting is taken off the pitch** after `idle_ms`, with a **Removed** event, and the ball they held drops loose. The platform's keeps them, and the ball, until the game ends. - **The movement check is the arena's travel budget,** the pitch's edge is no strike, and **Start** puts the teams at their halves. **In Blueprint**, with the component holding `CSO_Kit_Ball` and `green-1`: 1. On the team buttons, add **Call Join (CSO_Kit_Ball)** with **Team** set to **Red** or **Blue**. 2. On the kick button, add **Call Claim** when **Holder** in the State's **Ball** is not the signed-in player, and **Call Shoot** with the goal's X and Y otherwise: red's goal is at **Origin X** plus **Width**, and both goals at **Origin Y** plus **Depth** over 2. 3. Add **On BallEvent Received (CSO_Kit_Ball)**, bound at **Event BeginPlay**, and on **Goal** show **Score**. **In C++**, with `Ball` the acquired `CSO_Kit_Ball`, the pawn shoots at the goal it attacks: ```cpp void AGreenPlayer::ShootAtGoal(ECrowdyKitBallTeam MyTeam) { const FCrowdyKitBallState* State = Ball->GetState().GetPtr(); if (!State || State->Phase != ECrowdyKitBallPhase::Playing || State->Ball.Holder != Ball->GetSignedInUserId()) { return; } FCrowdyKitGroundPosition Aim; Aim.X = MyTeam == ECrowdyKitBallTeam::Red ? State->Origin.X + State->Width : State->Origin.X; Aim.Y = State->Origin.Y + State->Depth / 2.0; Ball->Call(TEXT("Shoot"), FInstancedStruct::Make(Aim), [](const FCrowdyServerCallResult& Result) { UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("No shot: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); } ``` The ball's actor is drawn and moved by the SDK; its **Crowdy Kit Actor** component's **Owner** is the holder, so a glow on the held ball listens to **On Kit Actor Changed**. The platform's [Races and ball games](/game-kit/modules/racing) page has the rest of both starters. ## Show a starter's actors Some of the platform's starters show actors their server code moves: of the ones Add starter offers, `npcs`, whose mobs, NPCs and pets are its actors, and `ball`, whose ball is one when its Config's `realtime` is on. Such an actor reaches the players near it on the realtime channel, and the SDK draws it like another player's character. 1. In the starter's definition, in the **Game Kit** category, give each kind in **Kit Actors** the actor class it is drawn as, such as `mob` and `BP_Wolf`. `CSO_Kit_Npcs` comes with the kinds `mob`, `npc` and `pet` and no classes. While Kit Actors has a kind, the definition is listed in the **Game Kit Actor Starters** project setting (**Crowdy SDK**, **Server Objects**; `GameKitActorStarters` on `UCrowdySDKDeveloperSettings`), which a packaged game reads, and which cooks the definition and its classes. A kind with no class that loads is not drawn, and the log says so once for that kind. 2. Every actor drawn for a kit actor has a **Crowdy Kit Actor** component, which the SDK adds when the class has none. Its **Kit Actor** is the actor as its hub last showed it: **Kind**, **Id** (its id in its hub, which that hub's calls take), **Instance Id** (the Instance Id of the hub that shows it, such as the area `2,-1` of an `npcs` mob: the Server Object to call for that actor; empty when the hub's actor id does not name one), **Name**, **Hp** and **Max Hp** when **Has Hp** is true, **Mode**, **Owner**, **Location** and **Rotation**. **On Kit Actor Changed** (`OnKitActorChanged`, an `FOnCrowdyKitActorChanged` with the component and the Kit Actor) runs when the actor starts drawing a kit actor and whenever anything but where it is changes, so a health bar or a nameplate listens there; the actor itself moves smoothly to where it is. To bind the event at **Begin Play**, add the component to the class yourself. 3. On a map drawn with Mass, which has no actor per entity, the Crowdy Kit Actor subsystem lists the kit actors this player sees now. **Get Kit Actor Count** (**Kind**) says how many, and **Get Kit Actor** (**Kind**, **Index**, and a **Kit Actor** output) returns true and fills the one at that index, or false when there is none. **Kind** None means every kind. **Get Kit Actors** returns them all as a copy, so a Blueprint that reads every tick, such as a Mass map's, uses the count and **Get Kit Actor**. An actor's index changes as others come and go, so follow an actor by its **Id**, not its index. A kit actor leaves when its hub says so, or after **Kit Actor Timeout** without an update: 1 second by default, a setting of the map profile's actor management beside the timeout for other actors. A kind that no starter's Kit Actors names reads as None. Positions on the wire are in chunks with z up, which the SDK turns into units by the session's chunk size (`UCrowdyGameSession::SetChunkSize`, 1600 units unless your project changes it). **Chunk Position** (`ChunkPosition`) on a **Kit Actor** holds the raw chunks the hub sent, and **Location** is in world units. In C++ the subsystem's `GetChunkSize()` gives the size it uses, and `PlaceInWorld(KitActor)` turns a Kit Actor's chunks into a place in this world. An update whose **Name** is over 128 bytes or whose **Mode** is over 32 bytes is dropped, and so is one whose **Id**, **Hp**, **Max Hp** or **Owner** is not a whole 64-bit integer. What the component holds is view state: an actor's health and mode are its hub's to decide, as with any [Server Object](/exec/intro) value. Nothing on the realtime channel proves a hub sent an update, so another player's game can send one under a kit actor's id: show what **Kit Actor** says, and never decide gameplay from it. Call the hub, which checks every call, as **Attack** does. ### The npcs starter's area A player is in an area while they stand in its 4 by 4 chunks, within one chunk (1600 units) of the area's ground height, so a **Crowdy Server Object** component on the player's pawn holding `CSO_Kit_Npcs`, with its Instance Id set to **This Actor's Area**, follows the area they are in. The ground height is 0 chunks unless the starter's **Config** says otherwise: `{"z": 3}` puts it at 3 chunks, so areas work on a map whose ground is far from Unreal Z 0. `z` is a whole number of chunks within plus or minus 4294967296. The scan counts players within 1 chunk of that height and the area's actors are drawn at it, so give their classes a way to stand on your ground if it is not flat at that height. An area keeps the height it was created with, so changing `z` affects only areas made afterwards. - Its records are the area's members, one **FCrowdyKitAreaMember** each: its **Type** (**Mob**, **Npc** or **Pet**), **Id**, **X** and **Y** in chunks, and the fields of its type (a mob's **Kind**, **Hp**, **Max Hp** and **Mode**; an NPC's **Name** and **Line**; a pet's **Species**, **Name** and **Mode**). Its State is empty. - Mobs live 1.5 chunks from the middle of the area (it was 3). So a player standing beside a mob is always in the area, and **Attack** never refuses `not_in_area` there, and one mob is always in view and within day aggro range from anywhere in the area: a player at the centre is always chased. An area saved earlier puts its mobs at the new distance when it loads and keeps its pets. - **Attack** with a mob's **Id** hits it for 10, once every half second, while the player is within 1.5 chunks of it; its answer is the mob's **Hp**, and at 0 the mob is down (refused with `down`) for 10 seconds. Mobs never hurt players. **Talk** with an NPC's **Id** answers that NPC. - **Adopt** gives the player a pet where they stand (at most 3 a player and 64 an area), and **RenamePet**, **SummonPet** (it follows them), **DismissPet** and **ReleasePet** take one of their own pets; **GetPets** answers their pets with each one's **Bond**, which only its owner reads. - A refusal's **Code** is `not_in_area`, `out_of_reach`, `cooldown`, `down`, `limit`, `full` or `forbidden`. The SDK's `npcs` keeps x and y on the ground and z up, as Unreal does, so its areas and actors are not where the platform's own `npcs` starter, which is y up, puts them; an area the platform's starter saved starts again. ```cpp #include "Replication/Components/CrowdyKitActorComponent.h" #include "Replication/Subsystems/CrowdyKitActorSubsystem.h" // In the wolf's BeginPlay, with the component on the class: its health bar follows its hub. if (UCrowdyKitActorComponent* Kit = FindComponentByClass()) { Kit->OnKitActorChanged.AddDynamic(this, &AWolf::HandleKitActorChanged); } // Anywhere in the game world: every mob this player sees now. const TArray Mobs = GetWorld()->GetSubsystem()->GetKitActors(TEXT("mob")); // Every tick, without a copy: a reference to the subsystem's own list. A Kind of None is the actors of no named kind. const TArray& Wolves = GetWorld()->GetSubsystem()->ViewKitActors(TEXT("mob")); ``` ### Hit a drawn mob Two neighbouring areas each number their mobs from 1, so a wolf's **Id** alone does not say which area it belongs to. Its **Instance Id** does: the SDK's `npcs` names each actor `M`, `N` or `P` (mob, NPC or pet), the area's key, `@` and the actor's id, such as `M2,-1@3`, and the SDK reads the area out of it. In Oakford, with `BP_Wolf` given to the `mob` kind and `CSO_Kit_Npcs` added and deployed, a villager hits the wolf they click. **In Blueprint**, in `BP_Wolf`'s event graph, on a click: 1. Add a **Crowdy Kit Actor** component to `BP_Wolf` in its Components panel, named `CrowdyKitActor`, so the graph can read it. Get its **Kit Actor**, and break it with **Break Crowdy Kit Actor State**. Its **Instance Id** is the wolf's area, such as `2,-1`. 2. Put that **Instance Id** into **Get Server Object** for `CSO_Kit_Npcs`, then **Call Attack (CSO_Kit_Npcs)** on it with **Mob** set to the Kit Actor's **Id**. (Or use a **Crowdy Server Object** component on the wolf holding `CSO_Kit_Npcs`, with its Instance Id set from the same pin.) 3. Under **On Success**, **Hp** is the wolf's health after the hit. At 0 the wolf is down: it stops being drawn and comes back after 10 seconds. Under **On Failed**, **Code** is `out_of_reach` when the villager is too far away, `cooldown` when they hit again within half a second, and `down` when the wolf is already down. [Blueprint snippet: kit-npc-attack, see /bp/unreal-sdk/kit-npc-attack.txt] **Talk** works the same with an NPC: its **Instance Id** and **Call Talk (CSO_Kit_Npcs)** with **Npc** set to its **Id**. **In C++**, with `KitNpcs` a `UPROPERTY` holding `CSO_Kit_Npcs`, in the wolf's click handler: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" #include "Replication/Components/CrowdyKitActorComponent.h" const UCrowdyKitActorComponent* Kit = FindComponentByClass(); if (!Kit || Kit->KitActor.InstanceId.IsEmpty()) { return; } FString Error; UCrowdyServerObject* Area = GetGameInstance()->GetSubsystem()->Acquire(KitNpcs, Kit->KitActor.InstanceId, this, Error); if (!Area) { return; } FCrowdyKitAttackInputs Inputs; Inputs.Mob = Kit->KitActor.Id; Area->Call(TEXT("Attack"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { if (const FCrowdyKitAttackReply* Reply = Result.Reply.GetPtr()) { UE_LOG(LogTemp, Log, TEXT("The wolf has %d left"), Reply->Hp); return; } UE_CLOG(Result.Code == FName(TEXT("out_of_reach")), LogTemp, Log, TEXT("Too far: %s"), *Result.Reason); }); ``` An empty **Instance Id** means the hub's actor id does not name one, which a kit actor of the SDK's `npcs` never does. An `npcs` added before this keeps the Rust it was added with, and sends the old ids, so **Instance Id** reads empty until you remove it and add it again, or bring the SDK's `Resources/GameKit/npcs/` changes into your `Server/npcs/`, and deploy. ## Change a starter A starter is a beginning, not a black box, and changing one is changing two things, as for a type of your own: - **Its Rust.** A starter's server code is `Server//src/lib.rs`, the starter's own, and the definition asset's [Server Code tab](./write-server-logic.md#the-server-code-tab) shows and edits that file. Its Code Source reads **Game Kit Starter**, which you cannot choose there: only Add starter makes one. **Generate** never writes a starter's code, and refuses with "world is a Game Kit starter: its server code is the starter's own Rust in .../src/lib.rs, which Generate never writes; edit that file". Keep the functions' names and the topics as they are, and the SDK keeps reaching them; the platform's [Your own version of a starter](/game-kit/deploy#your-own-version-of-a-starter) says what to keep. - **Its definition.** The functions, their inputs and outputs, and the State struct, which you can point at a struct of your own. A **Game Kit** category in Details holds what only starters have: **State Topic**, the topic whose snapshots are the variables (`world`, `queue`, `session`, `board`, `player`, `market`, `duel`, `battle`, `encounter`, `run`, `territory`, `zone`, `table`, `arena`, `race` or `ball`); **Collection Topic** and **Record Struct**, the topic whose members are the records and the struct each one fills (`nodes` and `FCrowdyKitWorldNode` on the world, `area` and `FCrowdyKitAreaMember` on the npcs, empty on the others; a field whose server name is `id` takes the member's id); **Owner Values** and **Keyed By Player**, the [values only the player reads](#values-only-the-player-reads); **Position Function** and **Position Reports Per Second**, the function the component [reports where the player is](#report-where-the-player-is) with, and how often (the arena's are `Move` and 5); **Parent**, the type it runs under (empty makes it the app's root); **Calls**, the types its Rust calls; and **Config**, a JSON object the starter reads when one of its Server Objects first starts, such as `{"day_ms": 600000}` for a ten-minute game day on the world, whose keys are `day_ms`, `nodes`, `node_amount` and `regrow_ms`. Each must be a whole number written without a decimal point or an exponent, from 0, and `nodes` at most 65535. A key in another case, such as `Nodes`, is ignored by the starter and by the check. **Add starter** and **Deploy** refuse a starter Config that holds two ids of one kind (item, currency, faction, counter or pity group) differing only by case, such as `Gold` and `gold`, since the SDK reads them as one. The session's keys are those of `FCrowdyKitSessionConfig` in snake_case (`min_players`, `max_players`, `turn_ms`, `target_score`, `max_rounds`, `lobby_idle_ms`) and `board`. The player's Config, one for every player, sets the inventory's `slots`, the `items` catalog, the soft `currencies`, the `start` bundle a new player gets, `levels`, `achievements`, `quests`, the `client_counters` a client may report with **Step**, `recipes`, `loot` tables, idle `production` and a `secret` mixed into loot rolls; the market's sets its `vendors`, the `book_items` with an order book, the `book_currency` they trade in, `orders_max` per player, `trade_ms` before an open trade is called off, and `player_type`, the Type Name of the players' hubs. The platform's [Player](/game-kit/modules/player) and [Market](/game-kit/modules/market) pages give each key's shape. The deck's Config holds one key, the `secret` mixed into every shuffle, which **Add starter** writes for you, as it writes the player's. The matchmaker and leaderboard read no Config. The other game-mode starters do: the minigame's `best_of` and `round_ms`; the tactics battle's board size, obstacles, phase time and units; the director's field, lane, towers, `kinds`, `waves` and `boss`; the instance's `floors`, `rooms_per_floor`, `party_max`, `idle_ms` and `board`; the territory's `points`, `factions`, `win_score` and income; the zone's `center`, `radius`, `phases` and damage; and the arena's `origin`, `width`, `depth`, `spawns`, `classes`, `players_max`, `kill_target`, `duration_ms`, `respawn_ms`, `teleport`, `remove_after_strikes`, `idle_ms`, `realtime` and `height`, as [Fight in an arena](#fight-in-an-arena) lists them; the race's and the ball's are in [Run a race](#run-a-race) and [Play a ball game](#play-a-ball-game). The platform's [Duels](/game-kit/modules/minigames), [Grid battles](/game-kit/modules/tactics), [Waves and bosses](/game-kit/modules/encounters), [Private runs](/game-kit/modules/instances) and [Control points and circles](/game-kit/modules/territory) and [Arenas](/game-kit/modules/arenas) pages give each one's shape. The settings a starter's Rust decides for itself are hidden: Readable By, the visibility of each variable, Members, Records, Timers and Events, Callable By, Required Feature and Cooldown. ![The Game Kit category of CSO_Kit_Npcs: its Collection Topic area and Record Struct, Kit Actors with the mob, npc and pet kinds, Parent world, Calls world and an empty Config](/img/unreal-sdk/kit-starter-definition.png) On the Overview tab a changed starter reads **Changed** like any type, and lists `config` or `parent` when those changed. A starter is marked **Problem** when its Config is not a JSON object ("its Game Kit Config is not a JSON object; correct it, or empty it to keep the starter's defaults"), when two starters have no Parent ("... an app has one root; give one of them a Parent"), when Parents run in a circle, when a Parent is a Server Function Library, when the `world` starter's Config has a `day_ms`, `nodes`, `node_amount` or `regrow_ms` that is not a whole number as described above, or when a `player` or `deck` starter's `secret` is not a string, since the starter would refuse its Config. A problem blocks Deploy. Deploy also warns, without stopping, when a `player` or `deck` starter's Config has no `secret` (exactly that key, a non-empty string), since players could then work out loot rolls and deals; see [Play a card game with a hidden hand](#play-a-card-game-with-a-hidden-hand). It is marked **Needs generating** when its crate lost a file: "its server code is missing ...; restore it, or remove the type in Server Compute and add the starter again". ## Remove starters From the command line, `-op=removestarter` takes a starter out of the project together with every starter that runs under it or calls it: ```text UnrealEditor-Cmd.exe Oakford.uproject -run=CrowdyServerCompute -op=removestarter -starter=world -yes ``` Removing `world` removes every starter, since they all run under it; removing `matchmaker` removes the matchmaker and the session; removing `session` removes the session and the matchmaker, which calls it, and leaves the leaderboard; removing `leaderboard` removes only the leaderboard, since a session or an instance calls it only when its Config names a board; removing `player` removes the player and the market, which calls it, and removing `market` removes only the market. Removing `npcs`, `minigame`, `tactics`, `director`, `instance`, `territory`, `zone`, `deck` or `arena` removes only that starter, since nothing else calls it. For each it deletes the definition and the whole `Server//` folder, your edits to its Rust included, and it clears **Game Kit World** when that named one of them. Without `-yes` it names what it would remove and changes nothing: "This deletes session, matchmaker, world: their definitions and server code, edits included; pass -yes to confirm." It logs "removing session, matchmaker, world", then "removed session, matchmaker, world", and its result has `starter` and `removed`. Before it changes anything it checks, and removes nothing when: - a starter's folder is, or holds, a link to somewhere else, since deleting it could delete what the link points to; - a definition's file is read-only, for example checked in to source control without a checkout: make it writable and run it again; - something else uses a definition, such as a Blueprint whose Server Object component holds `CSO_Kit_World`: the message names each one, so remove those uses first. If a file is held open while it deletes, for example by an IDE or `cargo`, the message ends "run -op=removestarter again to finish", which picks up where it stopped. On the Server Compute page, **Remove from project...** in a starter's **...** menu does the same, after a dialog naming every starter it removes and what the next deploy takes offline. Either way the server is not touched until your next deploy, which no longer sends the starters and removes them. Without `world`, that deploy sends the Crowdy SDK's root again, and **Get Players Online** reads it. ## In the example world Oakford's lantern posts light up at night, and villagers gather oil from the world's resource nodes. Add the `world` starter on Server Compute and deploy. **In Blueprint**, in the lantern post's event graph: 1. Add **On bNight Changed (CSO_Kit_World)** and wire **Event BeginPlay** into its **Bind**. It needs no Target, since the world has Only One Instance. From **Changed**, wire its **bNight** to **Set Visibility** on the lantern's Point Light component, named `Light`. 2. To gather oil, add **Call Harvest (CSO_Kit_World)** and set its **Node** input to a node's id. The world's resource nodes are its records: from the world's **Get Server Object**, **Get Record Ids** lists them, and **Watch Record** on the oil node's id runs with what it holds now and after each harvest, read with **Find Record** and **Get Server Value** `Amount`. 3. Under **On Success**, its **Left** output is what the node holds now. Under **On Failed**, show the arrow at the bottom of the node: **Outcome** is **Denied** with **Retryable** true and **Code** `cooldown` when the villager harvests too soon, and **Refused** with **Code** `empty` when the node is bare until it grows back. [Blueprint snippet: kit-world-night, see /bp/unreal-sdk/kit-world-night.txt] [Blueprint snippet: kit-world-harvest, see /bp/unreal-sdk/kit-world-harvest.txt] **In C++**, with `KitWorld` a `UPROPERTY` holding `CSO_Kit_World`, the lantern post follows the night and a villager harvests: ```cpp #include "CrowdyGameKitTypes.h" #include "CrowdyServerObjectSubsystem.h" FString Error; UCrowdyServerObject* World = GetGameInstance()->GetSubsystem()->Acquire(KitWorld, FString(), this, Error); World->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, TConstArrayView Fields) { const FCrowdyKitWorldConditions* Conditions = Object->GetState().GetPtr(); if (Conditions && Fields.Contains(TEXT("night"))) { Light->SetVisibility(Conditions->bNight); } })); FCrowdyKitHarvestInputs Inputs; Inputs.Node = NodeId; World->Call(TEXT("Harvest"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result) { if (const FCrowdyKitHarvestReply* Reply = Result.Reply.GetPtr()) { UE_LOG(LogTemp, Log, TEXT("The node holds %d"), Reply->Left); return; } UE_CLOG(Result.Code == FName(TEXT("empty")), LogTemp, Log, TEXT("Bare until it grows back: %s"), *Result.Reason); }); const FString OilNode = LexToString(NodeId); World->WatchRecord(OilNode, FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [OilNode](UCrowdyServerObject* Object, TConstArrayView Fields) { const FCrowdyServerRecord* Record = Object->GetRecord(OilNode); const FCrowdyKitWorldNode* Node = Record ? Record->Values.GetPtr() : nullptr; UE_CLOG(Node != nullptr, LogTemp, Log, TEXT("Oil left: %d"), Node ? Node->Amount : 0); })); ``` The world's Instance Id is ignored, since it has Only One Instance. For a match, [Find Match](#find-a-match-and-take-turns) on `CSO_Kit_Matchmaker` with the queue's name, then acquire `CSO_Kit_Session` with its **Session Id** and call **Join**, **Ready** and, as its host, **Start**; **Is My Turn** says when to let the player move. Oakford also keeps a weekly board of its lantern-lighting matches. Add the `leaderboard` starter, set `CSO_Kit_Session`'s **Config** to `{"board": "oakford-weekly"}` and deploy. Each finished match then posts its players' scores, and the village notice board, with `KitLeaderboard` a `UPROPERTY` holding `CSO_Kit_Leaderboard`, reads them: ```cpp UCrowdyServerObject* Board = GetGameInstance()->GetSubsystem()->Acquire(KitLeaderboard, TEXT("oakford-weekly"), this, Error); Board->Call(TEXT("GetMyPlace"), FInstancedStruct(), [](const FCrowdyServerCallResult& Result) { const FCrowdyKitBoardPlace* Place = Result.Reply.GetPtr(); UE_CLOG(Place && Place->Rank > 0, LogTemp, Log, TEXT("This week you are number %d with %lld"), Place ? Place->Rank : 0, Place ? Place->Score : 0LL); }); ``` In Blueprint the same is **Call GetMyPlace (CSO_Kit_Leaderboard)** on the object **Get Server Object** gives for `CSO_Kit_Leaderboard` and `oakford-weekly`, with its **Rank** and **Score** outputs; **On Top Changed (CSO_Kit_Leaderboard)** keeps the notice board's top ten current. At Oakford's market, villagers swap the planks they craft for gold. Add the `market` starter, which brings `player`, and deploy. [Trade between two players](#trade-between-two-players) is the whole exchange; in C++, with `KitMarket` and `KitPlayer` `UPROPERTY`s holding `CSO_Kit_Market` and `CSO_Kit_Player`, the villager selling planks offers them on the trade the buyer opened, and the purse follows the wallet: ```cpp UCrowdyServerObjectSubsystem* Objects = GetGameInstance()->GetSubsystem(); UCrowdyServerObject* Market = Objects->Acquire(KitMarket, TEXT("oakford"), this, Error); UCrowdyServerObject* Me = Objects->Acquire(KitPlayer, FString(), this, Error); FCrowdyKitTradeOfferInputs Offer; Offer.Trade = TradeId; Offer.Bundle.Items.Add(TEXT("plank"), 3); Market->Call(TEXT("Offer"), FInstancedStruct::Make(Offer), [](const FCrowdyServerCallResult& Result) { UE_CLOG(!Result.IsSuccess(), LogTemp, Log, TEXT("Not offered: %s (%s)"), *Result.Code.ToString(), *Result.Reason); }); Me->WatchValues(FOnCrowdyServerValuesChanged::FDelegate::CreateWeakLambda(this, [this](UCrowdyServerObject* Object, TConstArrayView Fields) { const FCrowdyKitPlayerState* State = Object->GetState().GetPtr(); if (State && Fields.Contains(TEXT("wallet"))) { Purse->SetText(FText::AsNumber(State->Wallet.Balances.FindRef(TEXT("gold")))); } })); ``` In Blueprint the offer is **Call Offer (CSO_Kit_Market)** with the trade's id and a **Bundle** made with **Make Crowdy Kit Bundle**, and the purse is **On Wallet Changed (CSO_Kit_Player)** on the player's own `CSO_Kit_Player`. Gold here is in-game soft currency, never real money. ## Related - [Game Kit](/game-kit/intro), [The starter hubs](/game-kit/starters) and [Deploying the starters](/game-kit/deploy) on the platform - [Deploy it with Server Compute](./deploy-with-server-compute.md): deploying the starters with your types, and the command line - [Create a Server Object type](./create-a-type.md) and [Write its server logic](./write-server-logic.md) - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md): the **Code** pin and `FCrowdyServerCallResult::Code` - [Troubleshooting](./troubleshooting.md#outcomes): every outcome, **Refused** included # Keep a record per entity Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/keep-a-record-per-entity # Keep a record per entity :::note Server Objects run on [ck-exec](/exec/intro). Records per entity are new in v2.19.0; see [What's Changed](../guides/whats-changed.md#2026-10-12-sdk-v2190). ::: A crowd of raiders on Oakford's north road is hundreds of entities, and each one has values the server must own: how hurt it is, what it carries, who hired it. A Server Object with **Holds** set to **A Record per Entity** keeps those values once for each entity, in one object, and tells each player only what that player may see. ## When to use it Use a record per entity when: - you have many entities, such as a crowd, a warband or a field of crops, each with the same few values; - the values are the server's to decide, so they belong in a [Server Object](./overview.md) and not in [Crowdy State](../runtime/crowdy-state.md); - players should see some values of an entity and others only for their own entities or their team's. The object that holds the records is a shared one: one for an area of the world, for a match or for a team. It is not one object per entity. An app can run at most 1,024 instances at once (see [Operations](/exec/operations#instances-and-versions)), so a Server Object for each of two thousand raiders cannot work, while one object per area holding their records can. A thing that has one set of values, such as a boss, a beacon or a player's stash, stays a normal Server Object. See [Relate Server Objects to each other](./relate-server-objects.md). The worked version of this page is [Raiders on the north road](./examples/raiders-on-the-north-road.md). ## Make the type Make the asset as on [Create a Server Object type](./create-a-type.md), then select **Type Settings** and set: | Setting | Where | Default | What it does | |---|---|---|---| | **Holds** | Server Object | **One Set of Variables** | Choose **A Record per Entity**. The variables become the fields of each record. | | **Visible to Players** | Each variable's Details | | **Server Only**, **Everyone**, **Owner Only** or **Team Only**. See [Who receives a value](#who-receives-a-value). | | **Max Records** | Records | 2048 | The most records one object keeps, 1 to 4096. | | **Send Changes Every** | Records | 250 ms | How often the server sends what changed, 50 to 5000 ms. | | **Team From** | Records | **This Object's Records** | Where a team comes from: **This Object's Records** or **Crowdy Team**. See [Who receives a value](#who-receives-a-value). | | **Area Size** | Areas | 4 | The side of one area, in chunks, 1 to 64. See [Pick the object](#pick-the-object). | | **Areas Followed Around a Player** | Areas | 1 | How many areas out from a player's own are followed, 0 to 3. 1 follows the 3 by 3 around it. | | **Reads World Data** | Platform Data | Off | Lets the server code ask which actors are near a chunk. Deploying asks for the `world.read` permission. | ![The raider type with Type Settings selected: Holds set to A Record per Entity, then the Records category (Max Records, Send Changes Every, Team From) and the Areas category (Area Size, Areas Followed Around a Player)](/img/unreal-sdk/server-object-records.png) **Readable By** must be **Every Player**: who sees which variable is decided per variable, not for the whole object. **Members From** must be **None**, since each record carries its own owner and team. Saving the asset says so when either is wrong. **Area Size** and **Areas Followed Around a Player** work for every type, whatever it holds. **Reads World Data** does too. A type of this kind starts with no records. The server code adds them. ## Who receives a value Every variable of a record has one audience: | Audience | Who receives it | |---|---| | **Server Only** | Nobody. It stays on the server. | | **Everyone** | Every player who watches the object. | | **Owner Only** | The player who owns the record's entity. | | **Team Only** | A player whose own record in this object has the same team as this record. | ![Loot selected on the raider type: Variable Name, Variable Type, Visible to Players set to Owner Only and the Default Value](/img/unreal-sdk/server-object-variable-audience.png) Each record has an **owner**, a player's user id or 0 for none, and a **team**, a string. Your server code sets both. They are not variables of your struct, and no player can set them. Which players count as the record's team depends on **Team From**: - **This Object's Records**, the default. A player's team in an object is the team of a record that player owns in that same object. A player who owns several records in one object uses the first by id. An empty team matches nothing. The team is a string you choose, such as `ravens`. - **Crowdy Team**. A record's team is the id of a [Crowdy Team](../services/teams.md), written in digits, and a player is on it when the platform says they are a member of that team. `insert` and `set_team` refuse a team that is not digits; an empty team means no team. Deploying asks for the `players.read` permission, since the server checks membership through the platform. Choose by how far a team reaches. With **This Object's Records** a player's team is known only inside one object. When records are kept per area, that means Team Only values reach a teammate only while both players are in the same area. If teams must see each other's values across areas, use **Crowdy Team**. A player who owns no record in the object and is on no matching team sees the Everyone values and nothing else. How the values travel depends on the audience: - **Everyone values** come in batches. The server collects the changes and, every **Send Changes Every**, sends the records that changed, each with only the Everyone variables that changed. A record that changes twice in between is sent once with its last value. A record you remove is removed on every player's side with the same batch. - **Owner Only and Team Only values** are fetched. The batch carries a notice that some changed, and the owner's or team's game asks the server for them, at most once a second for each object. They arrive a moment after the Everyone values. - **A player who joins, or whose game missed a batch,** reads every record again, a page at a time. A value a player does not receive reads as the variable's default, and raises no change. When a record's owner or team changes, the Owner Only and Team Only values of that record go back to their defaults for everyone who just lost access. When a player's own team changes, their game reads the object again. ## Pick the object The component on an actor, and a crowd row, choose which object holds the records. For a crowd the usual choice is the area the entity stands in. - **Only One Instance** (a setting of the type): one object for the whole world. Only for a small number of entities. - **Instance Id**: a name you choose, such as `match-7`, for a match or a team. - **This Actor's Area**: the object for the area the actor stands in, which changes as the actor moves. **This Actor's Area** cuts the ground into squares of **Area Size** by **Area Size** chunks. The object's Instance Id is the square's `x,y`: the chunk's x and y, which are Unreal's X and Y over the session's chunk size, each divided by Area Size and rounded down, so the area at chunk (-1, 5) with an Area Size of 4 is `-1,1`. The height is not part of the key. The component checks every half second and, when the actor crosses into another area, joins that area's object and lets go of the old one after a short grace period, so an actor on a border does not flap. Moving a record from one area's object to the next is your server code's job. One approach: the census in each area adds the entities it finds standing there and removes the ones that left, as the code below does. ## Write the server code Generate the type as on [Write its server logic](./write-server-logic.md). In `logic.rs` the functions are implemented on `Records`, the object's records, and not on the state struct: ```rust impl Functions for Records { fn skirmish(&mut self, _ctx: &Ctx) -> Result<()> { let ids: Vec = self.ids().map(str::to_string).collect(); for id in ids { let Some(raider) = self.get_mut(&id) else { continue }; raider.Health -= 5; if raider.Health <= 0 { self.remove(&id); } } Ok(()) } } ``` `Records` gives your code: | Name | What it does | |---|---| | `get(id)` | The record's values, or `None`. | | `get_mut(id)` | The record's values to change, or `None`. | | `insert(id, owner, team, state)` | Adds the record, or replaces the one with that id. Returns an error past **Max Records** (`records_full`), for an id that is empty or over 64 bytes, for a team over 64 bytes, and for either holding a control character (`bad_record`). | | `remove(id)` | Removes the record and returns its values. | | `set_owner(id, owner)`, `set_team(id, team)` | Change who owns the record and which team it is on. An error (`bad_record`) when there is no such record. | | `owner(id)`, `team(id)` | The record's owner (0 for none) and team. | | `ids()`, `len()` | Every id in order, and how many records there are. | | `owned_by(player)` | The ids of the records a player owns. | | `Records::area_of(chunk_x, chunk_y)` | The Instance Id of the area holding a chunk, worked out with this type's **Area Size**: the same key the component uses. | Changes made in one call are kept or put back together, like the variables of any Server Object: a function that returns an error leaves the records as they were. Ids are text, 1 to 64 bytes, and compared exactly, capitals included. ### Count who stands in the area To add the entities standing in an area, turn on **Reads World Data** and ask the world from a timer. `actors(at)` returns the actors in one chunk, and each one's `uuid` is the id its record uses. Ask for each chunk of the area in turn: ```rust fn census(&mut self, ctx: &Ctx) -> Result<()> { let Some((x, y)) = area_origin(&ctx.key) else { return Ok(()) }; for cx in x..x + AREA_SIZE { for cy in y..y + AREA_SIZE { let Ok(actors) = ctx.world().actors(ChunkPos::new(cx, cy, 0)) else { return Ok(()) }; for actor in actors { if self.get(&actor.uuid).is_none() { let _ = self.insert(&actor.uuid, 0, "", RaiderState::default()); } } } } Ok(()) } ``` Three things to know about the census: - **It reads the heights it asks for.** The call above reads ground level, height 0, and sees nothing standing higher. Ask for more heights if your world has them. - **One call answers up to 200 actors.** A chunk with 200 in the answer may hold more. Do not remove a record because its actor is missing from an answer that is full: skip the removals for that tick. - **Do not use `actors_near` for a dense crowd.** It answers at most 500 actors across several chunks, so a crowded area is cut short, and records of live entities would look as if they had left. `area_origin` turns the `x,y` in `ctx.key` back into the area's first chunk. [Raiders on the north road](./examples/raiders-on-the-north-road.md) shows the whole census, with the removal of the entities that left. The platform's pages on these calls are [World and platform data](/exec/world-and-platform-data). ## From Blueprint Put a **Crowdy Server Object** component on the actor, with **Definition** set to the type and **Instance Mode** set to **This Actor's Area**, **Instance Id** or the type's single instance. The actor needs a **Crowdy Entity** component as well: its id names the actor's record. Without one the component fails with "needs a Crowdy Entity component on this actor". ![The Crowdy Server Object component: Definition CSO_Raider and Instance Mode This Actor's Area](/img/unreal-sdk/server-object-component-area.png) The component reads **its actor's own record**, so the nodes work as they do for any Server Object. The typed **Get** and **On Changed** nodes, **On Variables Changed**, **Get Variables** and **Get Server Value** all give that record's values, and **Changed** runs when one of them changes. See [Read and follow its variables](./from-blueprint/read-and-follow-variables.md). To reach other entities' records, take the Server Object from the component with **Get Server Object**, and use these nodes on it: | Node | What it does | |---|---| | **Find Record** | Takes an **Id** and gives that record's **Values**, its **Owner** and its **Team**. Returns false when there is no such record. Read a variable out of **Values** with **Get Server Value**. | | **Get Record Ids** | The id of every record the player can see, in no particular order. | | **Holds Records** | True when the type keeps a record per entity rather than one set of variables. | | **Num Records** | How many records the object holds now. | | **Watch Record** | Takes a **Record Id** and an event (the delegate `FCrowdyServerRecordEvent`). The event runs with the **Object**, the **Record Id** and the **Changed** variables after each change of that record, and when it is removed (**Find Record** is then false). When the record is already current it runs once at once, naming all its variables. A watch added from an **On Records Changed** or **On Records Updated** handler gets that catch-up and does not run a second time for the same change. Records dropped because the object **Failed** are not reported; **On Status Changed** is. | | **Stop Watching Record** | Takes the same **Record Id** and event, and stops that watch. | | **On Records Changed** | An event that runs after every read, batch or fetch that changed records, with the **Object**, the ids that are new or changed, and the ids that were removed. | | **On Records Updated** | Like **On Records Changed**, but each changed record comes with its changed variables: a list of **Record Id** and **Changed Variables**, then the removed ids. **Changed Variables** is empty when only the record's owner or team changed. | **Watch Record** works like **Watch Message**: get the Server Object from the component, call **Watch Record** with a **Record Id**, and wire a custom event with the delegate's pins (the **Object**, the **Record Id** and the **Changed** variables) to its **Event**. The figure follows one record, here an id `raider-1` that your server code inserted, and prints its id each time it changes. [Blueprint snippet: so-record-watch, see /bp/unreal-sdk/so-record-watch.txt] Pan a Blueprint figure with the right mouse button. Press **Copy nodes**, then Ctrl+V in your own event graph. **Stop Watching Record** takes the same **Record Id** and the same event: [Blueprint snippet: so-record-unwatch, see /bp/unreal-sdk/so-record-unwatch.txt] **On Records Updated** is bound from **Begin Play** like any event of the object, with **Bind Event to On Records Updated** on the Server Object. Its event gets the changed records, each with its **Record Id** and **Changed Variables**, and the removed ids. The figure prints how many records changed: [Blueprint snippet: so-records-updated, see /bp/unreal-sdk/so-records-updated.txt] **Find By Asset** and the by-name **Watch Variables** read a type of this kind as if it had no records, so they give its defaults. Use the component, or **Find Record**. ## From C++ The component works as in Blueprint, and its `GetVariables()` and `OnVariablesChanged` give the actor's own record. To read others, acquire the area's object yourself. `FindGroundChunk` gives the chunk under a location as actor updates place it, and `GetAreaKey` on the definition turns it into the Instance Id: ```cpp UCrowdyServerObjectSubsystem* Objects = GetGameInstance()->GetSubsystem(); int64 ChunkX = 0; int64 ChunkY = 0; if (!Objects || !Objects->FindGroundChunk(GetActorLocation(), ChunkX, ChunkY)) { return; } FString Error; UCrowdyServerObject* Area = Objects->Acquire(RaiderDefinition, RaiderDefinition->GetAreaKey(ChunkX, ChunkY), this, Error); if (Area) { Area->OnRecordsChanged.AddUObject(this, &ARoadWatch::HandleRecords); } ``` `OnRecordsChanged`, an `FOnCrowdyServerRecordsChanged`, runs after every read, batch or fetch that changed records. It gives the object, the ids that are new or changed, the ids removed, and for each changed id the State fields that changed, one bit per field. Read a record with `GetRecord`, which returns an `FCrowdyServerRecord` holding the `Values`, the `Owner` and the `Team`: ```cpp void ARoadWatch::HandleRecords(UCrowdyServerObject* Object, TConstArrayView Changed, TConstArrayView Removed, TConstArrayView> ChangedFields) { for (const FString& Id : Changed) { const FCrowdyServerRecord* Record = Object->GetRecord(Id); const FRaiderState* Raider = Record ? Record->Values.GetPtr() : nullptr; if (Raider) { UpdateBar(Id, Raider->Health); } } } ``` `HoldsRecords()` says whether the definition keeps records, `NumRecords()` counts them, `GetRecordIds()` lists them, and `WatchRecord(Id, Listener)` follows one record, naming its variables on each change. Ids are compared with capitals significant. `WatchValues` and `GetState()` on a records object give the type's defaults, not a record. The rest of the C++ surface for records: - `UCrowdyServerObject::FindValues` gives a record's values by id, or the object's own when the id is null. The Blueprint **On Records Changed** is the `RecordsChanged` property, an `FCrowdyServerRecordsChanged`, and **On Records Updated** is the `RecordsUpdated` property, an `FCrowdyServerRecordsUpdated` whose `FCrowdyServerRecordChange` entries hold a `RecordId` and its `ChangedVariables`. - `UCrowdyServerObjectComponent::ReadsRecord` says whether the component reads its actor's record, and `UCrowdyServerObjectComponent::GetRecordId` gives that record's id. - `UCrowdyServerObjectDefinition::GetAreaKey` and the static `UCrowdyServerObjectDefinition::MakeAreaKey` turn a chunk into an area's Instance Id, and `UCrowdyServerObjectDefinition::HasCrowdyTeamRecords` says whether the type keeps records on Crowdy Teams. `UCrowdyServerObjectSubsystem::GetChunkSize` gives the chunk size the game session uses. - The settings are typed by `ECrowdyServerObjectHolds`, `ECrowdyServerFieldAudience` (**Everyone**, **Owner Only**, **Team Only**) and `ECrowdyServerTeamSource` (**This Object's Records**, **Crowdy Team**). ## Crowd rows A crowd drawn with the Crowdy Mass Rendering Backend has no actor for most entities, only rows, but a row reads the same records. See [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md). The record's id is the entity's id, the 32 characters the census calls report as `uuid`, so a census that keys records by the actor's `uuid` lands each record on its row. - Set the **Crowdy Server Object** component's **Instance Mode** on the entity's actor class to **Only One Instance**, **Instance Id** with an id typed, or **This Actor's Area**. With any other mode the rows get no server values, and the log says why once for the class. - Read the values with [Read its server values, from Blueprint](../mass/from-blueprint/read-its-server-values.md) or [Read its server values, from C++](../mass/from-cpp/read-its-server-values.md). ## Size it What a player receives is the changed records, times the bytes of each record, times the sends a second: > 500 records changed in a send, 20 bytes each, 4 sends a second: about 40 KB a second to each player. - **Raise Send Changes Every** when records change more than once in between: the last value is the only one sent. Lower it for faster feel, at the cost of more sends. - **Keep records small.** Put a value in a record only if a player or the server logic needs it per entity. - **Max Records** is 2048 unless you change it, and 4096 at most. A call that inserts past it is refused with `records_full`. - A client refuses a message over 1 MiB. The server splits a batch at 4,096 records or 512 KiB into several sends, in order, and a player's first read comes in pages of up to 512 KiB. - A player on a slow link falls behind and catches up. Batches queue instead of dropping. - Size the area so one object holds the records you want its watchers to receive. A player follows `(2r+1)` squared objects, with `r` the **Areas Followed Around a Player**: 9 by default. ## Limits and rules - **Readable By** must be **Every Player**, and **Members From** must be **None**. - A variable's audience is set per variable, and only a variable that is **Visible to Players** can be Owner Only or Team Only. A variable cannot be both. Saving checks both. - The function name `changes` and a timer whose name in snake case is `crowdy_flush` or `crowdy_sessions` are reserved. - A record's id is 1 to 64 bytes of text with no control characters. Ids that differ only in capitals are different records. - A team is at most 64 bytes. With **Team From** on **Crowdy Team** it is a team id in digits or empty, and anything else is refused. - A type starts with no records, so a seed must be empty. - The owner and team are set by server code only. ## If a value is not what you expect - **Always the default.** A player gets Owner Only values only for records they own, and Team Only values only when their own record in the object has the same team. A player who owns no record sees neither. - **A Team Only value arrives late.** Owner Only and Team Only values are fetched, at most once a second for each object. - **The component stays Connecting or fails.** See [Troubleshooting](./troubleshooting.md#a-server-object-does-not-become-ready). A record type needs its census or other server code to add records first. - **To see the pushes and fetches,** turn on [`crowdy.exec.trace`](./troubleshooting.md#seeing-what-a-server-object-is-doing). ## Related - [Raiders on the north road](./examples/raiders-on-the-north-road.md): a warband with a record per raider - [Create a Server Object type](./create-a-type.md#every-setting): every setting - [Access, members and timers](./access-members-and-timers.md#who-receives-a-variable-of-a-record): the audience of a variable, and the checks when you save - [Write its server logic](./write-server-logic.md#a-record-per-entity): the functions in `logic.rs` - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md#add-the-component) and [from C++](./from-cpp/get-a-server-object.md#one-object-per-area): This Actor's Area - [Read and follow its variables, from Blueprint](./from-blueprint/read-and-follow-variables.md#records) and [from C++](./from-cpp/read-and-follow-variables.md#records) - [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md): the entities drawn as crowd rows that read these records # Make a Server Function Library Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/server-function-libraries # Make a Server Function Library :::note Server Objects run on [ck-exec](/exec/intro). ::: A **Server Function Library** is a Server Object type that has functions and nothing else. It is the server's counterpart of a Blueprint Function Library: a place for logic that many callers share, with no instance of its own to hold. You describe it in the same [definition asset](./create-a-type.md) as any Server Object type, choose **Kind: Server Function Library**, and the editor hides what it cannot have. | | Server Object | Server Function Library | |---|---|---| | Keeps variables, saved between calls | Yes | No | | Instances | One for each Instance Id | One name for everyone; the server runs several instances of it and picks one for each call | | Calls answered at the same time | One for each Server Object | Several for each instance, on several instances | | Players watch its values, hear its messages | Yes | No: there is nothing to watch | | Members, timers, records, messages, cooldowns | Yes | No | | Players call its functions | Yes | Yes | | Other server code calls its functions | Yes | Yes | ## When to use one Use a library for a rule or a calculation that has no memory of its own and that several callers need: turning a seed into a loot drop, pricing an item, checking whether a move is legal, scoring a result. Because several instances answer at once, a busy game keeps getting answers where one Server Object would make its callers queue. A library can also change game truth, but only by calling a Server Object: a library function that rewards a player calls the player's own Server Object and that object changes its values. Do not use one for anything that has to remember. A counter, a stockpile, a match or a record per player belongs in a [Server Object](./create-a-type.md), because a library saves nothing: each instance runs your code in several copies, each with its own memory, so a value a function leaves in a variable can be there for one call and gone for the next. A library has no [timers](./access-members-and-timers.md#timers-and-events) and cannot [listen to messages](./access-members-and-timers.md#listen-to-other-server-objects), and a player cannot watch it; whatever players should see goes in a Server Object that the library's caller changes. Two things to plan for: - **The first call after the library has been idle can take over a second.** Players wait a little; server code calling it may be told the call timed out. See [A call to a library from server code fails](./troubleshooting.md#a-call-to-a-library-from-server-code-fails). - **Each instance counts toward the app's limit on running Server Objects**, 16 for an app with no funds and 16,384 for a funded one. A library that keeps 4 instances running leaves an unfunded app room for 12 other Server Objects. See [Scaling](#scaling). ## In the example world The Hollow Giant should drop something when it falls, and a chest on the north road should hand out a trinket. Both want the same rule: turn a seed and a tier into an item name. That rule keeps nothing, so it is a library, `CSO_LootRoller`, with one function, `RollLoot`. The chest calls it from a Blueprint or from C++, and the boss's server code calls it too. [Loot for the Hollow Giant](./examples/hollow-giant-loot.md) builds all of it. This page makes the asset. ## Make the asset Make a **Crowdy Server Object Definition** as on [Create a Server Object type](./create-a-type.md#make-the-asset) and name it `CSO_LootRoller`. Select **Type Settings**, the first row of the Server Object panel. In Details, under **Server Object**: | Setting | Value | |---|---| | Type Name | `loot_roller` | | Kind | **Server Function Library** | | Description | Turns a seed and a tier into an item name. | **Kind** is the second row, after **Type Name**. It is **Server Object** until you change it, and changing it rebuilds Details at once. The Server Object panel then shows **Type Settings** and **Functions**, and no Variables or Server Messages. Add the function `RollLoot` with **+** in **Functions**, as on [Create a Server Object type](./create-a-type.md#make-the-asset): **Callable By** on **Players**, and two inputs, `Seed` (Integer64) and `Tier` (Integer, starting at 1, with a **Value Range** of 0 to 4), and one output, `Item` (String). Or use structs, as the [boss](./examples/shared-boss.md#what-the-server-keeps) does. ![The CSO_LootRoller editor: the Server Object panel lists only Type Settings and Functions, and RollLoot reads (Seed, Tier) returns Item](/img/unreal-sdk/server-function-library-editor.png) A library has these settings, and no others: - **Type Name**, **Kind** and **Description**. - **Functions**, with their **Name**, **Callable By**, **Required Feature**, inputs, outputs, **Value Range** and **Server Name**. **Callable By** can be **Players**, **Server Only** or **Developers Only**; see [Callable By](./create-a-type.md#callable-by). - **Platform Data**, as on [Access, members and timers](./access-members-and-timers.md#platform-data), for what the library's server code reads and changes of players, the world and grids. - **Can Call** and **Can Call Any Type**, the types its server code may call; see [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). - **Scaling**, below. - **Server Limits**, collapsed, and **Server Names**, in the Advanced group, as on [Every setting](./create-a-type.md#every-setting). Everything else is hidden in Details and ignored everywhere, even if the asset still holds values from when it was a Server Object: it is not checked when you save, not in the generated server code, not deployed and not used by the game. That is the variables or State struct, **Visible to Players**, **Readable By**, **Only One Instance**, **Holds** and its records, **Members From** and the other member settings, **Areas**, **Timers**, **On Player Joined** and **On Player Left**, **Server Messages**, **Listen To**, **Save Every** and **Stop When Idle For**. Nothing picks an instance of a library, so a caller gives no Instance Id. A library whose functions are all **Server Only** is [closed to players](./access-members-and-timers.md#a-type-players-cannot-reach): only other server code and developer tools reach it. That is what a library that only your Server Objects use should be. ## Scaling The **Scaling** category, shown only for a library, says how many instances the server runs and how many calls each answers at once. The platform term is in brackets. ![With Type Settings selected, Details of CSO_LootRoller shows Type Name, Kind set to Server Function Library and Description, then Platform Data, Can Call and the Scaling category: Min Instances 1, Max Instances 1 and Parallel Calls per Instance 16](/img/unreal-sdk/server-function-library-type.png) | Setting | C++ | Range | Default | What it does | |---|---|---|---|---| | **Min Instances** [`replicas`] | `MinInstances` | 1 to 64 | 1 | How many instances run all the time. | | **Max Instances** [`max_replicas`] | `MaxInstances` | 1 to 64 | 1 | The most the server adds when calls wait. Equal to Min Instances, it never adds any. | | **Parallel Calls per Instance** [`concurrency`] | `ParallelCallsPerInstance` | 1 to 256 | 16 | How many calls one instance answers at the same time. | The server adds an instance, up to **Max Instances**, once calls have waited 2 seconds for an answer, and removes an added instance after about 30 seconds in which it is nearly idle. It never goes below **Min Instances**. So the library answers at least Min Instances times Parallel Calls per Instance calls at once, and more when it has grown. Start with the defaults. Raise **Max Instances** when bursts of callers wait, **Min Instances** to have those instances ready before the burst, and **Parallel Calls per Instance** to let each instance answer more at once. The longest one call may run is still the type's **Time per Call**, 2 seconds unless you change it, and a call that runs past it is answered **Timeout** like any other; see [Server Limits](./create-a-type.md#every-setting). Calls one instance runs at the same time share its processor and Time per Call counts the time that passes, so a function that keeps the processor busy needs a Time per Call that covers Parallel Calls per Instance of them; see [Library calls time out during a burst](./troubleshooting.md#library-calls-time-out-during-a-burst). Every instance that runs counts toward the app's limit on running Server Objects, and the Server Compute header shows it ("3 of 16 Server Objects"); past the limit no new instance starts. See [Large groups](./access-members-and-timers.md#large-groups) for the limits, and [Deploy it with Server Compute](./deploy-with-server-compute.md#overview) for how a library's row reads. ![The Server Compute row of loot_roller: its description, the line Server Function Library, 1 function and 1 instance, and the New badge](/img/unreal-sdk/server-compute-library.png) With a Min Instances of 1 and a Max Instances of 4 the same line says "1 to 4 instances". ## Checks when you save Saving a library lists these problems at once, with the usual checks of the visible settings (the Type Name, the function and server names, inputs and outputs). A library needs no State struct and no variable, so "Choose a State struct" does not apply to it. - A function's **Callable By** is **Members** or **Leader**: "RollLoot: Members and Leader need members, and a Server Function Library has none." - A function has a **Cooldown**: "RollLoot: a cooldown is saved data, and a Server Function Library saves none." Clear it. A library cannot remember when a player last called. - **Max Instances** is below **Min Instances**: "Max Instances must be at least Min Instances." - Another Server Object type lists the library under **Listen To**: "loot_roller is a Server Function Library and sends no messages." Remove it from **Listen To**; a type can still list it under **Can Call** and call it. The editor shows the first two as a warning under the function's row too. ## Switch Kind You can change **Kind** on a type that is already deployed, in either direction, and nothing is lost: - **To Server Function Library.** The type's variables, members, timers, messages and the rest stay in the asset, hidden and unused. The server keeps the saved data of its Server Objects, unused, and it comes back if the type becomes a Server Object again. Server Objects of the type that were running stop answering. The deploy asks first and names the change: "loot_roller changes from Server Object to Server Function Library: its saved data is kept, unused, and comes back if it becomes a Server Object again." - **To Server Object.** The hidden settings apply again, and the server starts each Server Object from the saved data it had, if any: "loot_roller changes from Server Function Library to Server Object: it starts from the saved data it had as a Server Object, if any." Games that used the type change with it. A library has only **Call** nodes, and they take no **Target** and no Instance Id; **Get**, **On Changed** and **On Received** nodes of the type no longer apply, so remove them. See [Call a Server Function Library](./from-blueprint/call-a-library.md). The Server Compute row reads **Changed** with "kind" in its list, and **Changed** with "scaling" when only Min Instances, Max Instances or Parallel Calls per Instance differ from the live version. After you change **Kind**, **Scaling** or a function, [generate the server code](./write-server-logic.md#after-you-change-the-definition) and deploy again. ## Write and call it - **Its server code** is a `logic.rs` like a Server Object's, without a state: [A Server Function Library](./write-server-logic.md#a-server-function-library) shows the function, a library calling a Server Object, a Server Object calling a library, and what a library cannot do. - **A game calls it** with the **Call RollLoot** node, or `UCrowdyServerObjectLibrary::CallLibraryFunction` in C++: [Call a Server Function Library, from Blueprint](./from-blueprint/call-a-library.md) and [from C++](./from-cpp/call-a-library.md). - **Server code calls it** with a typed helper that takes no key, once the library is in the caller's **Can Call** list: [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). ## Related - [Create a Server Object type](./create-a-type.md): the asset editor, **Callable By** and every setting - [Loot for the Hollow Giant](./examples/hollow-giant-loot.md): the loot roller, a chest and the boss, step by step - [Access, members and timers](./access-members-and-timers.md#call-other-server-objects): Can Call and Can Call Any Type - [Write its server logic](./write-server-logic.md#a-server-function-library): the server code of a library - [Deploy it with Server Compute](./deploy-with-server-compute.md): a library's row and the deploy that changes a Kind - [Troubleshooting](./troubleshooting.md#a-call-to-a-library-from-server-code-fails): a cold first call - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md): Kind, Scaling and Can Call Any Type on the platform # Troubleshooting Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/troubleshooting # Troubleshooting :::note Server Objects run on [ck-exec](/exec/intro). ::: This page is for Blueprint and C++ alike. In Blueprint, **Get Status** and **Get Failure Reason** are the nodes for what `GetStatus()` and `GetFailureReason()` answer in C++, and **On Failed** carries the **Outcome**, **Reason**, **Retryable** and **Code** that `OnDone` receives. On a **Call** node those pins are in the advanced area: click the small arrow at the bottom of the node to show them. **Code** is filled only by a [Game Kit starter](./use-game-kit-starters.md#refusals-and-their-codes). ## A Server Object does not become Ready Read its status first; the four statuses are on [Get a Server Object](./from-cpp/get-a-server-object.md#status). Then the reason it gives. ### Waiting is not failing Most problems on the server clear up by themselves, so reading and watching a Server Object recover from every temporary one without your help. Before its first values arrive, a Server Object waits them out in **Connecting** instead of failing. It keeps trying, a little less often each time and, unless the server asks for longer, never more than 15 seconds apart, and becomes Ready on its own once the cause is gone. Every wait before a retry or a redial is stretched by a random amount, up to half again: a failed read's first retry waits 1 to 1.5 seconds, and a lost connection is redialled after 1 to 1.5 seconds, then 2 to 3, and so on up to 30 to 45. So many players do not retry in step after a server restart. While it waits, `GetFailureReason()` says what it is waiting for. It waits when: - the type is not deployed on the server yet, or the app now runs an older version without it: the reason reads "`village_beacon` is not deployed on the server"; - the type is switched off on the server, or the whole app is switched off or paused (by its owner, or because it ran out of funds): the reason reads "The server is switched off or paused, so this Server Object waits: " followed by the platform's own words, such as "`village_beacon` is switched off in app 456" or "app 456 is switched off". It reads the same whether the server says so with its pause and switch-off statuses (`AppPaused`, `AppDisabled`, `TypeDisabled`) or, on an older server, with a refusal in words, and a read made while the connection is redialling into a paused or switched-off app shows this reason too. To show the player a message instead of retrying, see [When the app is paused](../services/authentication.md#when-the-app-is-paused); - the server does not let players use the type; - the server code crashed and its Server Object is restarting; - the server did not answer in time, or failed internally; - the server code refused the read, such as `unreadable` when the state holds a value players cannot read (see [refusals from your server code](#refusals-from-your-server-code)); - the server is busy or cannot be reached; when the platform could not start the Server Object to read it, for example because the app is at its instance or memory limit, the reason reads "The server could not be reached: " followed by the platform's words; - the connection to the server could not be opened: the reason reads "Could not open the connection to the server for ``: " followed by why, and names the type the SDK tried it with. When the refusal is that the server is switched off or paused, its error ending in `(APP_PAUSED)`, `(APP_DISABLED)` or `(TYPE_DISABLED)`, or saying so in words on an older server, it reads "The server is switched off or paused for ``: " followed by the error instead. A try made with the root or a Server Function Library names no type, since the platform is not told one: "Could not open the connection to the server: " or "The server is switched off or paused: ". The next try uses another type's Server Object, so one type that is not deployed or is switched off does not hold up the others. So a game can acquire its Server Objects before their server code is deployed, and they come up as soon as it is. ### What makes it fail A Server Object moves to **Failed**, and stays there, only when waiting cannot help: - this player may not read it: an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) Server Object that belongs to someone else, or a [Game Kit starter](./use-game-kit-starters.md) that refused the read with a code, such as "The server code for session refused to let this player read it (forbidden): ..." (a starter's `cooldown` or `unavailable` is waited out instead); - the deployed server code does not serve the variables players can see, or was built for another version of the SDK: generate and deploy it again; - the values the server sends do not match the definition's State struct: the definition and the deployed code are out of step, so generate and deploy again; - the server refused the SDK's request as malformed; - the Game API named a Server Object gateway outside its own domains, so the SDK will not connect to it: the reason reads "Could not open the connection to the server: the Game API named a gateway outside its own domains (" followed by why and "); if you host the server yourself, add its domain to Allowed Exec Gateway Domains in Project Settings". It is not retried until you acquire an object again (or call Restart). See [Server Object gateway domains](../reference/project-settings.md#server-object-gateway-domains) for the setting; - the player signed out (`The player signed out`); - the player's access to the game ended (`Your access ended: ` followed by the reason); see [When access ends](#when-access-ends). To try again after a failure, call `Acquire` again with the same definition and Instance Id, from the same owner or another; in Blueprint, call **Rejoin** on the component. The same object starts over from Connecting, and its values start from the definition's defaults. ### When access ends A player's access to the game can end while they play: it is suspended, revoked or expired, the platform signs the player out, or the account is blocked. The Game Session fires **On Access Ended** (see [When the player's access ends](../services/authentication.md#when-the-players-access-ends)) and Server Objects stop: - Every Server Object fails with `Your access ended: ` followed by the reason, whatever its status was. - A call in flight, and every call made afterwards, finishes as the outcome **Access Ended** (`ECrowdyServerCallOutcome::AccessEnded`), with the same reason. It is not retryable. - Nothing reconnects or retries by itself, since the server would refuse it. Show the reason and send the player back to sign in. A successful sign-in clears the ended state: a **Crowdy Server Object** component and a typed node set to **Find By Asset** get their Server Object again on their own. Code that called `Acquire` itself calls it again. ## A value is not what you expect - **A newer server sends an enum value this build does not have.** The field keeps its default and the SDK logs a warning once, under `LogCrowdyExec`. Update the game, or keep the value's old name under **Enum Value Names** if it was a rename. - **The connection dropped.** Changes made while it was gone are not replayed. The SDK reads the values again when it reconnects and reports the fields that differ as one change. See [Get a Server Object](./from-cpp/get-a-server-object.md#sign-out-shutdown-and-dropped-connections). - **The values stopped changing, or never arrived.** Check `GetStatus()` and `GetFailureReason()`. A Server Object waiting in Connecting (its type not deployed yet, or switched off) or one that has Failed keeps the last values it had, and the reason says what it is waiting for or why it stopped. See [Status](./from-cpp/get-a-server-object.md#status). ## A message never arrives A [message](./create-a-type.md#server-messages) is heard once, when the server sends it, and nothing keeps it. When a game or a Server Object expects one and gets nothing, check these in order. **In a game, from Blueprint or C++:** - **Players Can Listen is off.** Only the server code of a type that Listens To this one hears the message. A Blueprint **Received** node for it does not compile ("heard only by other Server Object types; turn on Players Can Listen"), and **Watch Message** or `WatchMessage` logs "... is heard only by other Server Object types; turn on Players Can Listen to hear it in a game" once and does nothing. Tick it in the asset, then generate and deploy the type. Likewise a name the definition does not have logs "... has no message named ..., so nothing can listen to it". - **The deployed server does not send it yet.** A message you added, renamed or changed reaches the server only with the generated code and a deploy; see [After you change the definition](./write-server-logic.md#after-you-change-the-definition). - **The function that sends it did not succeed.** A message is published only when the function that sent it succeeds, after its changes. A refusal sends nothing; look at the caller's `Reason` and the server code's [logs](./deploy-with-server-compute.md). - **The game was not listening at that moment.** A message sent before the game started listening, or while its connection was closed, is lost and never replayed. Check `GetStatus()`: a Server Object that is not Ready announces nothing, and a message that arrives while it is still reading its values is held and announced once they are current, at most 32 of them. Past 32 the oldest is dropped and the log warns "held back 32 messages while reading its values, so the oldest was dropped". - **The platform refused to let the player listen.** The log warns once, "`` could not listen to its `` message, so it is asked again after a wait: " followed by the platform's reason. The SDK asks again after a wait that grows from 1 second to 30, and nothing else about the Server Object fails. A message sent while the listen is refused is lost. - **A limit was reached.** A connection allows 256 subscriptions, and each time the game listens it counts against the 120 calls a connection may make every 10 seconds. Each message a game listens to on each Server Object is one subscription, so many objects with several messages each can reach it: give back the objects the game no longer needs, and listen only to the messages it uses. - **The message comes before the change it follows.** A type that keeps [a record per entity](./keep-a-record-per-entity.md) sends its records in batches, so a message sent right after a record changed can arrive first. Put what the listener needs into the payload. - **The payload does not fit its struct.** The message is dropped with a warning that names it. The definition and the deployed code are out of step: generate and deploy again. **In server code, listening to another type:** - **The listen was never applied.** A `listen::...` call takes effect only when the function that made it succeeds, and the function fails, with `could not listen to boss.boss_defeated:` and the reason, when the platform refuses it. A type can listen to 256 messages at once. Check the function's result. - **The key is not the sender's.** The key is the sender's Instance Id, with capitals significant: `boss-1` and `Boss-1` are two Server Objects. - **The sender is not in Listen To.** Without it the `listen` functions and the `on__` handler do not exist; see [Listen to a message](./write-server-logic.md#listen-to-a-message). Generate and deploy after adding it. - **The message was sent too early.** One sent before the listen was applied, or while the listener was stopped, is missed. - **The handler failed.** A handler that returns an error puts its state back, and a payload it cannot read fails it with `bad_message: .: ...`; read the logs. ## Outcomes `Outcome` is an `ECrowdyServerCallOutcome`. The platform's own status is in brackets. | Outcome | What happened | What to do | |---|---|---| | Success [`Ok`] | The server code ran and answered. `Reply` holds the outputs. | Nothing. The watched values change for everyone, you included. | | ServerError [`AppError`, `Internal`] | The server code refused the call or failed, or the platform failed handling it. `Reason` carries the message. Also returned when the reply does not fit the function's outputs. | Show or log `Reason`. See [refusals from your server code](#refusals-from-your-server-code). | | Busy [`Busy`, `RateLimited`] | The server stayed busy through every retry. | Try again later, and call less often. If one Server Object takes many calls at once, raise **Calls Waiting** in the type's [Server Limits](./create-a-type.md#every-setting). | | Unavailable [`Unavailable`, `Moved`] | The server could not be reached, so the call may or may not have run; or the Server Object has Failed (`Reason` is its failure reason). When the platform says why, such as the app being at its instance limit, `Reason` adds its words after "The server could not be reached:". Not retried by the SDK. A Game Kit starter's `unavailable` refusal (another hub the call needed did not answer) arrives as Unavailable too, with **Code** `unavailable` and `bRetryable` set. | Check `GetStatus()`. Call again only if running the function twice is harmless, or after the watched values show it did not happen. | | NotDeployed [`NotFound`] | This type is not deployed on the server. The Server Object itself waits in Connecting until it is. | Deploy the type's server code to the app. | | Timeout [`DeadlineExceeded`] | Nobody answered in time, and `Reason` says which side gave up. "AttackBoss ran past its Time per Call on the server: handler exceeded its deadline" means the server's own limit stopped the call (the type's **Time per Call**, 2 seconds unless changed), followed by the server's message; for a read it reads "The server took longer than this type's Time per Call to read this Server Object" (or "watch"). "The server sent no answer in time; the connection may be slow or lost" means no answer reached the game within the project's **Server Object Call Timeout** (20 seconds unless changed; see [Project settings](../reference/project-settings.md#server-object-timeouts)): the longest **Time per Call** (10 seconds) plus time spent waiting behind other calls on the server. "No answer within 5 seconds; FeedBeacon may still have run on the server" means the call's own **Timeout** ran out first (5 seconds here); it is not retryable, and an answer that arrives later is not reported. The call may or may not have taken effect. A call past **Time per Call** also restarts that Server Object from its last save, so changes since the last save are lost. | Watch the values before you call again. If the function does real work that needs longer, raise **Time per Call** in the type's [Server Limits](./create-a-type.md#every-setting); otherwise keep functions short, and set **Save Every** low on a type whose recent changes matter. **Time per Call** counts the function only, not the time a call waits behind others. A call that gives up earlier than the project's timeout has a **Timeout** set on its Call node (`TimeoutSeconds` in C++); clear it, or raise it, if the server needs longer. | | Denied [`Denied`] | The server refused the call. Either the platform refused it, because players may not call this type at all, and `Reason` reads "The server refused the call to Feed: " followed by the platform's words; or the type's own server code refused the player by a rule in the asset, such as **Callable By**, **Owner Only** or a **Cooldown**, and `Reason` says why. A Game Kit starter's `forbidden` and `cooldown` refusals arrive as Denied too, with **Code** set and the starter's words in `Reason`; `cooldown` is `bRetryable`. A call refused because the server is switched off or paused is [Paused](#outcomes), not Denied. | Show `Reason`. A refusal by a rule stays a refusal until something changes, except a cooldown, which is `bRetryable`: call again after the seconds it names. See [refusals from your server code](#refusals-from-your-server-code). | | Paused [`AppPaused`, `AppDisabled`, `TypeDisabled`; `Denied` in words from an older server] | The game's server, or this type, is switched off or paused, by its owner or because the app ran out of funds. The platform says which with its own status: `AppPaused` for an app paused by billing, `AppDisabled` for the whole app switched off, `TypeDisabled` for one type switched off. A server from before those statuses refuses with `Denied` and says so in words, and the SDK reads that as Paused too. It is retryable, and `Reason` reads "The server is switched off or paused, so it refused the call to Feed: " followed by the platform's words, such as "`village_beacon` is switched off in app 456", "app 456 is switched off" or "app 456 is paused: ...". A call that got no answer because the connection was redialling into a paused or switched-off app reports Paused with the platform's words too, not Timeout. | Try again later, or show the player a paused message; see [When the app is paused](../services/authentication.md#when-the-app-is-paused). Code that only checked for Denied should handle Paused too, and a Blueprint Switch on the outcome has a **Paused** pin. | | AccessEnded | The player's access to the game ended: suspended, revoked, expired, signed out by the platform, or the account blocked. `Reason` reads `Your access ended: ` followed by why. Not retryable, and it applies to every Server Object until the player signs in again. | Show `Reason` and return the player to sign in; see [When access ends](#when-access-ends). A Blueprint Switch on the outcome has an **Access Ended** pin. | | Refused [`AppError` with a Game Kit code] | A [Game Kit starter](./use-game-kit-starters.md#refusals-and-their-codes) refused the call for a reason of the game's, such as a full match (`full`) or another player's turn (`not_your_turn`). **Code** (`Code` in C++) holds the code and `Reason` the starter's words. Not retryable. Your own types never answer Refused. | Branch on **Code**, and show `Reason`. The codes are on the platform's [Refusals carry a code](/game-kit/protocol#refusals-carry-a-code). A Blueprint Switch on the outcome has a **Refused** pin. | | ServerCrashed [`Trapped`] | The server code crashed handling the call. The Server Object restarts on the server from its last save, so its most recent changes may be lost; the SDK reads its values again and reports what differs. | Read the server code's log. | | BadRequest [`BadRequest`] | The call could not be sent as asked: an unknown function name, params of the wrong struct, or params that could not be encoded. `Reason` says which. | Fix the call. | | Canceled | The Server Object was given back, or the player signed out, before the call finished. | Nothing, usually; the owner is gone or the session ended. | ## Refusals from your server code A refusal whose message starts with one of these words came from the [server code generated for the type](./write-server-logic.md#what-the-generated-code-does-for-you), not from your own logic: - `unknown_method`: the deployed server code has no such function. The definition is newer than what is deployed; generate and deploy the server code again. It reaches `OnDone` as a `ServerError`. - `denied`: a player called a function they may not: **Server Only**, **Members** or **Leader** without being one, or, on an Owner Only type, a Server Object that is not theirs. It reaches `OnDone` as `Denied`, and `Reason` is the text after the word, such as "only members may call Deposit". A Server Only function is one no player may call, so a call from a game client is refused this way every time; only other server code and developer tools can call it. Set it back to **Players** in the [definition asset](./create-a-type.md#callable-by) if players should be able to. Deploy the type again afterwards, since the server code carries the choice. - `cooldown`: the player called a function inside its **Cooldown**. It reaches `OnDone` as `Denied` with `bRetryable` set, and `Reason` says how many seconds to wait, such as "Feed can be called again in 4 s". - `bad_params`: the server could not read the params, or a number is outside its **Value Range** (`bad_params: Amount must be 1 to 100`), and the rest of the reason names the field. Usually the deployed code is older than the definition. It reaches `OnDone` as a `ServerError`. - `unreadable`: the Server Object's state holds a value no Unreal client could read, such as text in an `FGuid` field that is not a GUID, often one loaded from a save that older server code made. The rest of the reason names the field, such as `unreadable: Id: the server state holds a value players cannot read: a GUID is not in a form Unreal reads`. Reads are refused until your server code replaces the value, so players' Server Objects wait in Connecting with this as their reason and become Ready by themselves once it is fixed. See [the values every client can read](./write-server-logic.md#what-the-generated-code-does-for-you). - `bad_seed`: the starting values given to a new Server Object hold a value no Unreal client could read, and the rest of the reason names the field. The Server Object does not start. - `the change was not kept`: the call or hook would have put such a value into a watched field, or the reply held one, or a message it sent held one or was larger than 1 MiB, so the state went back to what it was. It reaches `OnDone` as a `ServerError`. - `bad_message`: a message this type listens to could not be read as its payload, as in `bad_message: boss.boss_defeated: ...`. The handler fails and the state goes back; the sender's definition and this type's deployed code are out of step, so generate and deploy both again. The [full list of what a refused player sees](./access-members-and-timers.md#what-a-refused-player-sees) is on the members page. Any other `ServerError` reason is the message your server code refused with, such as the beacon saying it is full. ## A call to a library from server code fails The first call to a [Server Function Library](./server-function-libraries.md) after it has been idle can take over a second. A player's game just waits for it. A Server Object's function that calls the library is held to the platform's call deadline, and a call that outlasts it fails with "call deadline passed" although the library may still finish. A boss that rolls loot when it falls sees it as `loot_roller.roll_loot failed: call deadline passed`. - **Decide what a failed call means.** If the answer is optional, as a drop is, take it only when the call came back: `if let Ok(loot) = calls::loot_roller::roll_loot(ctx, &roll) { ... }`, as in [Loot for the Hollow Giant](./examples/hollow-giant-loot.md#the-boss-rolls-on-its-fall). - **Call again once when the function is safe to repeat.** A pure calculation is. The second call finds the library warm: ```rust let loot = calls::loot_roller::roll_loot(ctx, &roll).or_else(|_| calls::loot_roller::roll_loot(ctx, &roll))?; ``` Do not repeat a call that changes a Server Object unless repeating it is harmless; see [Call other Server Objects](./access-members-and-timers.md#call-other-server-objects). - **The helper does not exist.** `calls::loot_roller` is made only when the caller lists the library under **Can Call**, or the caller uses `calls::any` with **Can Call Any Type**. Add it, generate the caller's server code and deploy again. - **`app N has no node type`.** A call by `calls::any` named a type the app does not run, or misspelled it. Deploy does not check names given to `calls::any`. - **`denied`.** A player called a function that is **Server Only** ("players may not call this function") or **Developers Only** ("only developer tools may call this function"). Other server code can call a Server Only one, and developer tools both; see [Callable By](./create-a-type.md#callable-by). - **Unavailable, with the platform's words.** The app is at its limit on running Server Objects, and every instance of the library counts toward it; see [Scaling](./server-function-libraries.md#scaling). A function that sees a value reset between two calls is working as designed: a library keeps nothing, and each instance runs several copies of your code with their own memory. Keep the value in a Server Object. ## Library calls time out during a burst A burst of calls that each keep the processor busy, such as many loot rolls at once, can come back as **Timeout** with "ran past its Time per Call on the server", even though one call alone is quick. The calls an instance runs at the same time share its processor, and **Time per Call** counts the time that passes, not the work done. The platform adds an instance only after calls have waited 2 seconds, so the first moments of a burst all land on the instances already running. In a test with one instance running four calls at once, 300 ms of work per call, and two players each sending 24 at once, a Time per Call of 2000 ms failed some calls and 5000 ms answered all of them. - **Give the function room.** Raise **Time per Call** to cover **Parallel Calls per Instance** times the work one call does. - **Start with more instances.** Raise **Min Instances** when bursts are expected, such as when a round ends and every player opens a chest. - **Run fewer at once per instance.** A lower **Parallel Calls per Instance** gives each call more of the processor; raise **Max Instances** to keep the total. ## A Server Function Library will not save Saving shows these for a library, and they are the whole list besides the usual checks of its Type Name, functions and server names: | Message | What to do | |---|---| | "RollLoot: Members and Leader need members, and a Server Function Library has none." | Set the function's **Callable By** to **Players**, **Server Only** or **Developers Only**. | | "RollLoot: a cooldown is saved data, and a Server Function Library saves none." | Set its **Cooldown** to 0. To limit how often a player may call, put the function on a Server Object. | | "Max Instances must be at least Min Instances." | Raise **Max Instances**, or lower **Min Instances**. They are under **Scaling**. | | "loot_roller is a Server Function Library and sends no messages." | A Server Object lists the library under **Listen To**. Move it to **Can Call**. | See [Checks when you save](./server-function-libraries.md#checks-when-you-save). ## A ping fails **Ping Server Objects** (`UCrowdyServerObjectSubsystem::Ping` in C++) fails with a `Reason` instead of a round trip when: - "No Server Object connection is open": a ping never opens a connection. Acquire a Server Object first and ping once it is Ready, or ping again after the connection comes back. - "Too many pings: the platform answers at most 60 every 10 seconds": the platform drops the rest of a connection's pings without a word, so the SDK refuses them before sending. Ping on a timer of a second or more, not every frame. See [Ping the platform](./from-blueprint/call-functions.md#ping-the-platform). ## Seeing what a Server Object is doing When a Server Object never becomes Ready, stops changing, or two players disagree about it, turn on `crowdy.exec.trace`. It is off by default, and the warnings and errors print whether it is on or not. Turn it on in the editor's console (press the backtick key, in Play In Editor too): ```text crowdy.exec.trace 1 ``` or, for a packaged game or an editor started from the command line, add this to the command line: ```text -ini:Engine:[ConsoleVariables]:crowdy.exec.trace=1 ``` The lines go to the Output Log under `LogCrowdyExec`, at Log verbosity, each starting with `exec:`, then the type and Instance Id (`boss/boss-1`). An [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) Server Object prints `owner` in place of the Instance Id, so the player's user id never reaches the log. The Players Online root has no Instance Id, so it prints `-` in its place, such as `exec: root/- read applied ...`. For a shared boss being hit by two players, one player's log reads: ```text exec: boss/boss-1 status Connecting -> Ready exec: boss/boss-1 subscribe ok exec: boss/boss-1 read applied epoch=3 seq=41 changed=Health,MaxHealth,Phase,Respawns,Hits,LastHitBy exec: boss/boss-1 call Hit sent exec: boss/boss-1 push applied epoch=3 seq=42 changed=Health,Hits,LastHitBy exec: boss/boss-1 call Hit answered Success ms=63 exec: boss/boss-1 push applied epoch=3 seq=43 changed=Health,Phase,Hits,LastHitBy exec: boss/boss-1 push stale epoch=3 seq=43 exec: boss/boss-1 call Hit sent exec: boss/boss-1 call Hit answered Denied ms=41 exec: connection closed exec: redial ok exec: boss/boss-1 reread reason=reconnect exec: boss/boss-1 read applied epoch=3 seq=47 changed=Health,Hits,LastHitBy ``` What it prints: - **Status changes**, such as `status Connecting -> Ready`. - **Every read and push that was applied**, with its `epoch`, its `seq` and the names of the variables that changed (`changed=-` when none did). The `epoch` changes when the server restarts the Server Object, and `seq` orders its changes. The `seq` printed is the one the SDK applied, so if two players' logs show the same `epoch` and `seq` lines, they saw the same changes. The names come in the order Watch Variables reports them. A push that is older than what the object already has prints `push stale`, with the object's current `epoch` and `seq`, and is not applied. - **Why it read again**, as `reread reason=` followed by `gap` (a change was missed), `overflow` (so many changes arrived while a read was in flight that they could not all be held), `epoch` (the server restarted the object), `reconnect` (the connection came back) or `ownerOnly` (an [Owner Only](./from-cpp/get-a-server-object.md#owner-only-types) or Members type, which tells watchers to read instead of sending the values). - **Subscribes**, as `subscribe ok` or `subscribe failed` followed by the reason. - **Calls**: `call Hit sent`, and `call Hit answered` with the [outcome](#outcomes) and how many milliseconds the answer took. A call the SDK retries for you because the server was busy prints `sent` once. - **Connection**: `exec: dial failed` with the reason when the first connection could not be made, `exec: connection closed` when an open one drops, then `exec: redial ok` or `exec: redial failed` with the reason. Every `redial ok` is preceded by a `connection closed`. A close on purpose names why, and no redial follows it: `exec: connection closed (no Server Object in use)`, `(signed out)` or `(shutting down)`. The next Server Object you get opens a fresh connection. It prints the names of the variables that changed, never their values, and never a token. Turn it off again with `crowdy.exec.trace 0` once you have your answer, since a busy fight writes a line for every hit. ## Related - [Get a Server Object, from Blueprint](./from-blueprint/get-a-server-object.md) and [from C++](./from-cpp/get-a-server-object.md): the statuses, and what sign-out and a dropped connection do - [Read and follow its variables, from Blueprint](./from-blueprint/read-and-follow-variables.md) and [from C++](./from-cpp/read-and-follow-variables.md) - [Hear its messages, from Blueprint](./from-blueprint/hear-server-messages.md) and [from C++](./from-cpp/hear-server-messages.md): when a message is heard, held or lost - [Call its functions, from Blueprint](./from-blueprint/call-functions.md) and [from C++](./from-cpp/call-functions.md): what the SDK retries - [Make a Server Function Library](./server-function-libraries.md), and calling one [from Blueprint](./from-blueprint/call-a-library.md) or [from C++](./from-cpp/call-a-library.md) - [Access, members and timers](./access-members-and-timers.md#what-a-refused-player-sees): every refusal a rule in the asset can cause - [Deploy it with Server Compute](./deploy-with-server-compute.md): the Logs tab and the Activity tab - [ck-exec operations](/exec/operations): logs, including a crashed call's # Unreal and ck-exec names Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/unreal-and-ck-exec-names # Unreal and ck-exec names :::note Server Objects run on [ck-exec](/exec/intro). ::: The SDK names things the way an Unreal project would. The platform's [ck-exec pages](/exec/intro), its operations tools and its logs use their own words. This table maps one to the other. | Unreal SDK | ck-exec | |---|---| | Server Object type: one definition asset, `UCrowdyServerObjectDefinition` | hub node type | | Type Name | node type name | | Server Object: `UCrowdyServerObject`, from `UCrowdyServerObjectSubsystem` | a hub instance | | Instance Id | instance key | | Variables: the State Struct, or the variables added in the asset | the hub's state, kept in its snapshots | | Server Function | method | | Server Name of a function | method name | | Calling a Server Function: `UCrowdyServerObject::Call` | a call | | Call outcome: `ECrowdyServerCallOutcome` | reply status (mapped on [Troubleshooting](./troubleshooting.md#outcomes)) | | Variables Visible to Players, and following them: `WatchValues` | the hub's `state` topic, subscribed to, and its `read` method | | Server message, sent by `messages::send_...` | a topic the hub publishes on (any topic but `state`) | | Server Name of a message | topic | | Listening to a message: `WatchMessage` in a game, `listen::::` in server code | subscribing to a hub instance's topic | | Listen To | the types in the manifest's `calls`, which may subscribe | | Kind: Server Function Library | a spoke node type, `"kind": "spoke"` in the manifest | | Min Instances | `replicas` | | Max Instances | `max_replicas` | | Parallel Calls per Instance | `concurrency` | | Calling a library: `UCrowdyServerObjectLibrary::CallLibraryFunction`, the **Call** node with no Target | a call to the spoke with the empty key; the platform picks an instance | | Can Call Any Type | `"calls": ["*"]`; in server code, `calls::any` over `ctx.call` | | Save Interval Seconds | `persist_every_ms` | | Idle Timeout Seconds | `evict_after_ms` | | The type's server code, generated into `Server//` by **Generate Server Code** | the type's crate, built into the module its manifest entry names | | Your code in `logic.rs`, implementing `Functions` | the hub's handlers: the generated `lib.rs` implements `Hub` and calls yours. For a library, it implements `Spoke` | | Deploy on the Server Compute page, and `-op=deploy` | a build (`execBuild`) then a deploy (`execDeploy`) of a manifest that lists every type under a `root` hub type | | Version: one deploy, listed on the Versions tab of the Server Compute page, with **Make active** to go back | a version (`execVersions`); activating an earlier one is `execActivateVersion`, a rollback | | Switch off and Switch on for a type | the kill switch, `execSetEnabled` with a node type; calls are refused with `Paused` meanwhile | | A call and its log lines: click a line's flow to follow it | a flow, the 32 hex digits every line of one call carries (`execLogs`) | | The Activity tab of the Server Compute page | `execEndpointStats`, calls per endpoint | Mods and CLIENT halves have no Unreal SDK counterpart yet. A spoke is a [Server Function Library](./server-function-libraries.md). Two platform names show up where you might not expect them. `read` is the method the SDK calls to fetch the watched values, which is why no Server Function may use that name. And `state` is the topic every Server Object's server code publishes its variable changes on; a message cannot use it. ## Related - [What a Server Object is](./overview.md) - [Make a Server Function Library](./server-function-libraries.md) - [ck-exec overview](/exec/intro) # Move from Game Models Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/exec/move-from-game-models # Move from Game Models :::note Server Objects run on [ck-exec](/exec/intro). v2.18.0 added them and deprecated the Game Model API; [v2.19.0 removes it](../guides/whats-changed.md#2026-10-12-sdk-v2190). ::: The SDK's Game Model API is removed in v2.19.0, the release after the one that adds Server Objects. It was built on a game API engine that ck-exec replaced, so it had no server to talk to. This page says what went, what to do in your project before you upgrade, what takes the place of each part, and how to move a system across. The replacement is [Server Objects](./overview.md), from Blueprint or C++. ## What was removed - **The Game Model API.** Containers and attributes (the `CrowdyModel` and `CrowdyContainer` metadata), model functions, Crowdy Effects (the assets, the effect graph editor and the Apply Effect nodes), sessions on the Game Model subsystem, automations, signals, pre-seeding, the Game Kit, container manifests, and `UCrowdyGameModelSubsystem`. **Get Local User Id** goes with it: use **Get User ID** on the Crowdy Game Session. - **Its settings and logging.** The `crowdy.gamemodel.*` console variables, the `LogCrowdyGameModel` log category and the `DefaultModelNotificationCarrier` setting. - **Server Owned in the Replication dropdown.** A Blueprint variable's **Crowdy Replication** dropdown now offers only **None** and **Replicated**. - **CrowdyMass's Game Model values.** `GetModelValue`, `GetModelTargetState`, `GetEffectTarget` and the model fields are gone. Crowd rows read Server Object values instead, from a record per entity; see [Crowd rows](./keep-a-record-per-entity.md#crowd-rows). - **Crowdy Studio's Game Model page.** The **Features** and **Tier-feature grants** cards that were on it are on the new **Access** page, under **Project** in the Configure group. ## Upgrade steps Do these in order. 1. **Compile and open the project on the release that adds Server Objects first.** That release still has the Game Model API, and every use of it is listed: each C++ use gets a compiler warning (C4996 on MSVC), and each Blueprint that uses Game Models gets one compiler warning listing its uses when it compiles, loads or cooks. Write the list down, and [move each system](#move-a-system) off Game Models while the nodes are still there to read. 2. **Search your C++ for `CrowdyModel`, and for `CrowdyContainer`.** A property or Blueprint variable that still carries either marker compiles and does nothing, and nothing warns you. Remove the marker, or the property if it was only a Game Model attribute.3. **Know what the upgrade drops without a message.** A Blueprint that still has **Apply Effect** nodes loses those nodes when it loads, with no error, so step 1 is the only place they are listed. Crowdy Effect, container manifest and Game Kit assets no longer load; note what they described and delete them. An old saved `DefaultModelNotificationCarrier` is ignored and can be deleted from your config. 4. **Fix the C++ that no longer compiles** (see [the removed C++ API](#the-removed-c-api)). 5. **Do a clean cook.** Do not cook iteratively over the upgrade: the baked registry's layout changed, so rebuild it and cook from scratch. See [Packaging](../guides/packaging.md). 6. **Check your channel code.** The SDK no longer drops channel payloads that begin `cmc:`, `csg:` or `gms|`, so a payload that starts that way now reaches your handler. See [Channels](../runtime/channels.md). ## The removed C++ API - `ICrowdyEntitySubscriber::ApplyModelChanges` is gone. A class that implements the interface and marks it `override` stops compiling; delete the function. - `UCrowdyEntitySubsystem::RegisterSubParticipant`, `RegisterSubParticipantAs` and `DeriveSubParticipantID` are gone. - `FCrowdyModelIdentity` is gone. `FCrowdyEntityIdentity` has `StableNetIDFromActorPath` and `NetIDFromBindingKey` for the same jobs. - `FCrowdyEntityRecord::AnchorNetID` is gone. - `ICrowdyBindingKeyProvider` stays. Its header now lives under `Replication/Components/`, so fix an include that names the old path. The CrowdyState layout's message for a container property now reads "use a CrowdyEvent RPC or a Server Object" where it used to name a Game Model container; see [Crowdy State](../runtime/crowdy-state.md). ## What takes the place of each part | Game Models | Server Objects | |---|---| | A container type | A Server Object definition asset: [Create a Server Object type](./create-a-type.md) | | Container attributes (`CrowdyModel`) | The state struct's variables. What players may read is the values that are **Visible to Players**, the ones they watch: [Create a Server Object type](./create-a-type.md#the-details-tab) | | Model functions and Effects | Server Functions, written in the type's `logic.rs`: [Write its server logic](./write-server-logic.md) | | Invoke policies | **Callable By** and **Readable By** on the asset, and checks in the logic: [Access, members and timers](./access-members-and-timers.md#access) | | A match session | One Server Object per match, with the match id as its Instance Id: [Relate Server Objects to each other](./relate-server-objects.md) | | Change pings, **Listen for Model Changes**, OnRep on model attributes | Following the watched variables, from [Blueprint](./from-blueprint/read-and-follow-variables.md) or [C++](./from-cpp/read-and-follow-variables.md) | | Automations: interval schedules | The type's timers: [Timers and events](./access-members-and-timers.md#timers-and-events) | | Automations: cron schedules | No setting on the asset for now; the platform's [Cron schedules](/exec/timers-and-presence#cron-schedules) | | `player_joined`, `player_left`, `player_count_changed` triggers | **On Player Joined** and **On Player Left** on the type, and presence on the server: [Presence](/exec/timers-and-presence#presence) | | Authority rules (tier features, group and grid permissions) | A tier feature: [Required Feature](./access-members-and-timers.md#required-feature) on the function. Group and grid permissions: checks in the logic against the player and the world, [World and platform data](/exec/world-and-platform-data) | In Oakford, had the beacon that a villager feeds with oil been a Game Model container, it would have had an `Oil` attribute and a `FeedBeacon` model function, guarded by an invoke policy. As a Server Object it is the `AVillageBeacon` type from [What a Server Object is](./overview.md): `Oil` is a variable players watch, and `FeedBeacon` is a Server Function that adds the oil or refuses. ## Move a system Do this for each system on your list, while the project is still on the release that has both Game Models and Server Objects. 1. **Author each system as a Server Object type**: make the [definition asset](./create-a-type.md), set [who may read and call it](./access-members-and-timers.md), [write its logic](./write-server-logic.md) and [deploy it with Server Compute](./deploy-with-server-compute.md). 2. **Switch the call sites.** Read and follow values and call functions [from Blueprint](./from-blueprint/get-a-server-object.md) or [from C++](./from-cpp/get-a-server-object.md), and remove the Game Model nodes and calls as you go. 3. **Seed any data that must persist.** Nothing moves automatically. A one-off script writes the starting values into the new type, for example by calling a [Server Only](./create-a-type.md#callable-by) Server Function, which developer tools can call and players cannot. Server Objects need ck-exec on your app's environment, so a project can finish moving once its environment has it. ## Why Server Objects are not marked on the class Game Models were declared by markers on your own classes. Server Objects are declared in one asset, on purpose: - **The truth is the server code.** A property on an actor that looks writable is only a request to the server. Keeping the state in the server code makes that plain. - **A Server Object is not an actor.** It is picked by an Instance Id, exists before any actor does and outlives them, and many actors can share one. - **One versioned contract.** The asset is the single place the type is described, so compiling a Blueprint can never silently change what the server expects. - **Marker metadata is stripped when a game is cooked,** so a property marker is not something a packaged game can read back. - **A call answers or refuses plainly,** with an outcome and a reason, which an OnRep on an attribute cannot. ## Related - [What a Server Object is](./overview.md) - [From the legacy engines](/exec/from-the-legacy-engines): the platform's full mapping from game models and automations to ck-exec - [Port a compute module](/exec/port-a-compute-module): a worked port to a hub - [Unreal and ck-exec names](./unreal-and-ck-exec-names.md): the SDK's words next to the platform's - [Troubleshooting](./troubleshooting.md): outcomes and refusals