Write its server logic
Server Objects run on ck-exec.
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. 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 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:
Server/
village_beacon/
Cargo.toml
src/
lib.rs
types.rs
logic.rs
The folder is Server/<Type Name>/ 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 is Generated |
A deploy adds one more file to the folder, revisions.json, which records what each deploy sent; see 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). 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/<Type Name>/ 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 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, 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.

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). 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 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.rsfor Generated. Until the Type Name is valid it reads "Server/<Type Name>/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. 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.
- 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
.rsfiles; 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 <State> 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.rschanged 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 longerlogic.rs. Save your edits tologic.rsanyway?" 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 savelogic.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.rschanged 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.rsfirst? Generating never replaces an existing logic file." - Deploy, in the editor's toolbar, with unsaved edits asks "Save your changes to
logic.rsfirst? 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 showslogic.rsagain."
Unsaved edits are kept if the panel closes or refreshes. They are not on disk, so Server Compute's 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/<Type Name>/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
.rsfile with it, relative to the project folder. Choose Generated again to go back tologic.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.rsandsrc/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.rsof another. Deploy refuses it with "<Type A>and<Type B>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.
View, compare and restore an earlier deploy
Every 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.
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.rswith version 6's code?" first. Restore replaces only thelogic.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 nologic.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 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:FVillageBeaconStatebecomesVillageBeaconState. 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:EBeaconColorbecomesBeaconColor. Each value keeps its name on the server. - A type name that Rust or the generated code already uses (
Vec,Error,Functionsand the like), or that another of your types already has, getsTypeadded, then a number if that is taken too. - Fields keep their names on the server exactly, so the beacon's code reads
self.Oilandself.bLit. A field named like a Rust keyword, such astype, is writtenr#type. - Server Functions become methods named by their Server Name:
FeedBeaconisfeed_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++:
#[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:
pub trait Functions {
fn feed_beacon(&mut self, ctx: &Ctx, call: &Call<'_>, params: FeedBeaconParams) -> Result<FeedBeaconReply>;
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:
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<FeedBeaconReply> {
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.keyis its Instance Id, andctx.logwrites to the 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 noparamsargument.
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). 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: 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 type reach their own on_<type>_<message> function instead; see 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.
Send a message
A type that defines 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, and a BossRespawned message with no values for the second:
fn hit(&mut self, ctx: &Ctx, call: &Call<'_>, params: BossHitInputs) -> Result<BossHitOutputs> {
// ... 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:
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. 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
Errputs 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 withbad_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 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:
impl Functions for Library {
fn roll_loot(&mut self, _ctx: &Ctx, _call: &Call<'_>, params: LootRollInputs) -> Result<LootRollOutputs> {
// ... 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.
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.
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.keyis the name of the instance that answered, such asr0orr1, 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_afterandctx.timer_everyare refused when the code runs, and so is subscribing to a topic, so a library cannot usetimers::,listen::ormessages::; it has noon_player_joinedoron_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:
fn donate(&mut self, ctx: &Ctx, _call: &Call<'_>, params: DonateParams) -> Result<DonateReply> {
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.
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:
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. To call a type that is not in the list, tick Can Call Any Type and use calls::any; see 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) andbad_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
FGuidfield 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 startingthe 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 withunreadableand 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 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.rsas themembers,timersandcallsnames listed under For logic.rs. -
Refusals. An unknown function is refused with
unknown_method, a call the checks above do not allow withdeniedorcooldown, and params that cannot be read as the function's inputs, or are outside a Value Range, withbad_paramsand the field at fault. These are the reasons listed on Troubleshooting. -
Messages and listening. It queues each
messages::send_...and eachlisten::...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 itson_<type>_<message>handler. See Send a message and 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. 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
Functionstrait, and the crate does not compile untillogic.rshas its method. After writing the other files, Generate Server Code compareslogic.rswith 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 yourimpl Functions for <State>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 asmy_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 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.rskeeps usingGold(it adds a List Value Names 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.rsand, if a struct, field or enum value is gone or a field changed type, lists each one (for exampleVillageBeaconState.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 for what each change does to existing data. - Code in
logic.rsthat used a removed field stops compiling; fix it there. - A new type name (the definition's Type Name) goes into a new folder,
Server/<new name>/. Each generatedCargo.tomlrecords the definition asset it was generated for, so Generate Server Code finds the folder it wrote under the old name and asks: Yes movesServer/<old name>/toServer/<new name>/, keeping yourlogic.rsandrevisions.json, then generates; No generates into a fresh folder with a newlogic.rsand 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'sCargo.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). A Type Name another definition already uses is refused.
Keep it in version control
The crate folder is source: commit Server/<Type Name>/ 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/<Type Name>/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 thesrcfiles, with your own file's code aslogic.rsif 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 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 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. 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.
A Server Object acquired before its type is deployed waits in Connecting and comes up once it is; see 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:
-
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 undersrc/to the platform's build; see Builds and starter packs. CrowdyJS and CrowdyCPP wrap it: see From an SDK. You can also build locally withcargo build --release --target wasm32-unknown-unknownand deploy the module yourself; see Deploying. -
Deploy. Give the type an entry in your app's manifest, named by its Type Name, and deploy the build with it; see 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
roottype:"village_beacon": { "kind": "hub", "parent": "root", "crate": "village_beacon", "client": true, "persist_every_ms": 30000, "evict_after_ms": 300000 }clientmust betruefor players to reach it.persist_every_msandevict_after_msare the definition's Save Interval Seconds and Idle Timeout Seconds in milliseconds. A Server Function Library's entry has"kind": "spoke"andreplicas,max_replicasandconcurrencyfrom its Scaling instead, and neither of those two.
Building and deploying need your organization's manage_compute permission. Operations 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
- Create a Server Object type
- Call its functions, from Blueprint and from C++
- Hear its messages, from Blueprint and from C++: what a player's game does with the messages sent here
- Make a Server Function Library: the asset behind A Server Function Library
- Troubleshooting: the refusals this code produces
- Unreal and ck-exec names
- ck-exec overview: hubs, the manifest, and the
ckx-sdkyour code runs on