Skip to main content

Call a Server Function Library

note

Server Objects run on ck-exec.

A Server Function Library has no instance to pick and no values to read, so a game does not acquire it and hold it as it does a Server Object. It calls a function by naming the definition: CallLibraryFunction, a static function of UCrowdyServerObjectLibrary in CrowdyServerObjectLibrary.h, in the CrowdyExec module.

Call one​

The chest on the north road hands out a trinket when the local player walks into it. LootRollerDefinition is the CSO_LootRoller asset from Make a Server Function Library, picked in the chest's Details panel, and RollLoot takes the structs below:

// Chest.h
USTRUCT(BlueprintType)
struct FLootRollInputs
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite) int64 Seed = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = 0, ClampMax = 4)) int32 Tier = 1;
};

USTRUCT(BlueprintType)
struct FLootRollOutputs
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite) FString Item;
};

virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;

UPROPERTY(EditAnywhere, Category = "Crowdy")
TObjectPtr<UCrowdyServerObjectDefinition> LootRollerDefinition;

// Chest.cpp
#include "LanternPlayer.h"

void AChest::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
const ALanternPlayer* Player = Cast<ALanternPlayer>(OtherActor);
if (!Player || !Player->CrowdyEntity->IsLocallyOwned())
{
return;
}
FLootRollInputs Inputs;
Inputs.Seed = 7;
Inputs.Tier = 1;
UCrowdyServerObjectLibrary::CallLibraryFunction(this, LootRollerDefinition, TEXT("RollLoot"), FInstancedStruct::Make(Inputs), [](const FCrowdyServerCallResult& Result)
{
if (!Result.IsSuccess())
{
UE_LOG(LogTemp, Warning, TEXT("The chest rolled nothing: %s"), *Result.Reason);
return;
}
const FLootRollOutputs* Reply = Result.Reply.GetPtr<FLootRollOutputs>();
if (Reply)
{
UE_LOG(LogTemp, Log, TEXT("The chest holds %s"), *Reply->Item);
}
});
}

Walk into the chest and the log reads The chest holds SilverRing:t1. The library keeps nothing, so the same Seed and Tier give the same item every time.

CallLibraryFunction(WorldContext, Definition, Function, Inputs, OnDone, TimeoutSeconds) takes:

  • WorldContext: any object in the game's world, such as the actor.
  • Definition: the library's definition asset.
  • Function: the function's Name on the definition, RollLoot here.
  • Inputs: an FInstancedStruct holding the function's Input Struct, made with FInstancedStruct::Make. For a function with no inputs, pass an empty FInstancedStruct().
  • OnDone: runs once with an FCrowdyServerCallResult, on the game thread. It holds the Outcome, the Reply on success, a Reason for anything but success, and bRetryable, as for a call to a Server Object.
  • TimeoutSeconds: optional. A limit of its own for this call that can only shorten the wait, as in Give up sooner.

A function whose inputs are added in the asset takes an FInstancedPropertyBag, the second overload. MakeParams of the library's Server Object hands you a copy with the default values; set the ones you want by name. The subsystem gives you that object for an empty Instance Id, and CallLibraryFunction shares it:

// Chest.cpp
UCrowdyServerObjectSubsystem* Objects = GetGameInstance()->GetSubsystem<UCrowdyServerObjectSubsystem>();
FString Error;
UCrowdyServerObject* Library = Objects->Acquire(LootRollerDefinition, FString(), this, Error);
if (Library)
{
FInstancedPropertyBag Inputs = Library->MakeParams(TEXT("RollLoot"));
Inputs.SetValueInt64(TEXT("Seed"), 7);
Inputs.SetValueInt32(TEXT("Tier"), 1);
UCrowdyServerObjectLibrary::CallLibraryFunction(this, LootRollerDefinition, TEXT("RollLoot"), Inputs, [](const FCrowdyServerCallResult& Result)
{
const TValueOrError<FString, EPropertyBagResult> Item = CrowdyExec::ToList(Result.Reply).GetValueString(TEXT("Item"));
UE_CLOG(Result.IsSuccess() && Item.HasValue(), LogTemp, Log, TEXT("The chest holds %s"), *Item.GetValue());
});
}

The reply reads by name through CrowdyExec::ToList from CrowdyExecCodec.h, as on Call one with inputs added in the asset.

How it is held and answered​

  • The world holds the library. The first call to a library connects it, and the game instance's UCrowdyServerObjectSubsystem keeps it, as its own owner, until the subsystem shuts down. There is nothing to release, and calls to the same library share one connection. Calling Acquire yourself works too and is ready as soon as the connection opens, but CallLibraryFunction needs none of it.

  • OnDone runs exactly once. A call that cannot be sent completes at once, inside CallLibraryFunction, so OnDone can run before it returns. Only a game instance shutdown skips it, for calls still waiting; sign-out completes them as Canceled.

  • A refusal is an outcome, not a crash:

    CauseOutcomeReason names
    A null definition, or a definition that is not a Server Function LibraryBadRequestthe asset, "... is not a Server Function Library"
    A function the definition does not haveBadRequestthe type and the function
    No world, or a world with no game instanceUnavailablethat it needs a world with a game instance

    Definition->IsFunctionLibrary() tells you whether an asset picked at run time is a library before you call it, and Definition->Kind is the ECrowdyServerObjectKind behind it.

    Every other outcome is a call's, listed on Troubleshooting: Denied for a function that is Server Only or Developers Only, ServerError for an input outside its Value Range, Timeout, Paused and the rest.

  • Calls run side by side. Two calls that are sent together are two independent calls, and the server may answer them on different instances, in either order. A library has no state to link them.

  • The first call after the library has been idle can take over a second. Your OnDone simply runs later. A call that goes on past the project's Server Object Call Timeout, 20 seconds unless you changed it, ends as Timeout.

  • Capture your actor weakly, as on Call one: OnDone can run after the actor that made the call is gone. The chest's lambda touches nothing of the actor, so it captures nothing.