Operations Tasks #

Operations can serve multiple different purposes when designing the endpoints and data modeling for a player data feature. This page describes how to create an Operation, as well as common Operation structures that are frequently implemented in a Player Data Sub Service.

Common tasks when authoring an Operation include:

Define a Sub Service #

Sub Services are defined as Kotlin classes and house Operation functions.

All Player Data Sub Services are authored in the 5-ext/ext-player-data directory and are responsible for housing Operation functions.

To create a Sub Service, go to the 5-ext/ext-player-data directory and create a new Kotlin file for housing your Sub Service. Then, create a Kotlin class that inherits the PlayerDataSubService interface with a primary constructor that takes a parameter for PlayerDataContentLibrary. This parameter accesses the Player Data service’s content ecosystem for storing and retrieving content data.

Learn more about PlayerDataContentLibrary by checking out the Content Library section in the Content overview page.

Below is an example of a Sub Service and its generated SDK API:

package exampleGamesStudio.playerdata

class ExampleOperations(contentLibrary: PlayerDataContentLibrary): PlayerDataSubService(contentLibrary)
namespace PragmaPlayerData {

class PRAGMASDK_API ExampleOperationsSubService {
public:
    void SetDependencies(FPlayerDataRequestDelegate Delegate);            
    
private:
    FPlayerDataRequestDelegate RequestDelegate;
};
}

After the SubService class is defined, you must run a make ext command to generate code for the engine layer and build 5-ext with your new Player Data classes.

Create Request and Response classes #

An Operation’s Request and Response payloads are defined as Kotlin classes.

An Operation has a Request and Response class that must be defined before you can write any business logic for a player data endpoint. These classes also allow you to define specific fields for the Operation’s Request or Response type depending on what is necessary for your Operation.

First, make sure that you’ve already created your PlayerDataSubService class that will use your Operation.

Author your Request and Response classes so that they inherit the PlayerDataRequest and PlayerDataResponse interfaces. Without these structures, the Player Data service won’t be able to identify the Request and Response classes used for a specific Operation and won’t be able to generate into protobufs or SDK APIs.

Below is an example of what the classes for an Operation’s Request and Response type look like in Kotlin and their generated protobuf and SDK code:

data class ExampleEchoRequest(
  val message: String
): PlayerDataRequest

data class ExampleEchoResponse(
  val message: String
): PlayerDataResponse
message ExampleEchoRequestProto {
    string message = 1;
}

message ExampleEchoResponseProto {
    string message = 1;
}
USTRUCT(BlueprintType, Category=Pragma)
struct FPragma_PlayerData_ExampleEchoRequestProto
{
	GENERATED_BODY()

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Pragma)
	FString Message;
};

USTRUCT(BlueprintType, Category=Pragma)
struct FPragma_PlayerData_ExampleEchoResponseProto
{
	GENERATED_BODY()

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Pragma)
	FString Message;
};

After the Request and Response classes are defined, you must run a make ext command to generate code for the engine layer and build 5-ext with your new Player Data classes.

Author an Operation function #

An Operation’s business logic is written as Kotlin functions inside a Sub Service’s class.

Once you have the PlayerDataRequest and PlayerDataResponse classes defined, you can use those classes to author a specific Kotlin function for implementing business logic into an Operation’s workflow. These functions are always written inside a PlayerDataSubService class.

In order for an Operation to work properly, the function must start with the @PlayerDataOperation() annotation. This annotation contains a parameter for sessionTypes, which allows you to limit and assign Pragma Engine gateway session types that can call this Operation. These gateways can be either PLAYER, OPERATOR, PARTNER, or SERVICE.

An Operation must have a unique name and a parameter for the request property and the context property.

After the Operation class is defined, you run a make ext command to generate code for the engine layer and build 5-ext with your new Player Data classes.

An example of an Operation function’s structure in a Sub Service and its generated SDK code can be seen below:

package exampleGamesStudio.playerdata

data class ExampleEchoRequest(
  val message: String
): PlayerDataRequest

data class ExampleEchoResponse(
  val message: String
): PlayerDataResponse

class ExampleOperations(contentLibrary: PlayerDataContentLibrary): PlayerDataSubService(contentLibrary) {
  
  @PlayerDataOperation(sessionTypes = [SessionType.PLAYER]) // Required Annotation
  fun echo(
      // Function must be defined with these parameters
      request: ExampleEchoRequest, 
      context: Context
  ): ExampleEchoResponse { // Function must return a PlayerDataResponse class 
      TODO("implement")
  }
}
namespace PragmaPlayerData {

class PRAGMASDK_API ExampleOperationsSubService {
public:
    void SetDependencies(FPlayerDataRequestDelegate Delegate);            
    
    /* The parameters correspond to the fields of its request. */
    void Echo(const FString& Message, FOnExampleOperationsEchoDelegate Delegate) const;
    
private:
    FPlayerDataRequestDelegate RequestDelegate;
};
}

Every Operation includes a generated Delegate for the SDK, which provides access to the Operation’s Response object. The SDK only calls the generated Delegate after the client side cache has been updated. This means that anytime you access cached data in the Delegate, the Delegate provides the most up to date version of the player’s data.

See the Player Data Service SDK reference topic for more information.

Below is an example of a generated Unreal Delegate:

DECLARE_DELEGATE_OneParam(FOnExampleOperationsEchoDelegate, TOptional<FPragma_PlayerData_EchoResponseProto>);

Modify player data #

Write business logic for creating and modifying live data using the Snapshot and Context objects.

After your Operation has Request and Response classes along with an Operation function, you can write the business logic involving live data by using context and PlayerDataSnapshot.

The context parameter provides access to PlayerDataSnapshot and playerId, and includes helper functions for making content data Requests and retrieving other Sub Service Operations. PlayerDataSnapshot is a class containing multiple helper functions specifically for modifying live data (Entities).

These two objects are often used in tandem when authoring an Operation’s business logic. Check out the Reference documentation on Context and Snapshot to learn more about the ways these two objects can be used in an Operation,

To create or modify data in an Operation, integrate an instance of the context or context.snapshot object into your Operation function’s business logic. Usually you’ll want to integrate this instance as a value or variable.

Below is an example of a PlayerDataSnapshot helper function utilized in an Operation function:

    @PlayerDataOperation(sessionTypes = [SessionType.PLAYER])
    fun echo(
        request: ExampleEchoRequest, 
        context: Context
    ): ExampleEchoResponse {
       ...
       val entity = context.snapshot.getOrCreateUniqueEntity(name)
       ...
       return ExampleEchoResponse()
    }
}

Access Content Schema data in an Operation #

Data using a Content Schema structure is accessed using the Content Library object.

Every Sub Service class is constructed with a parameter including the PlayerDataContentLibrary object. This object contains a function called getCatalog() that allows the developer to acquire content data so an Operation can modify it. If you want to get a specific value from a JSON catalog entry use the getValue() function.

To learn more about content data and how you can use it in an Operation, see the Content Overview page.

Below is an example of getCatalog() acquiring data from a Content Schema’s associated JSON file:

val ExampleContentData = contentLibrary.getCatalog("custom-content-item-1.json", ExampleContentSchema::class).getValue(exampleData)

Perform a Content Request #

The Context object contains a function for performing Request endpoints defined using a Content Schema.

Content-oriented endpoints have Request entries that can be accessed in an Operation function by utilizing the context object. This object contains the doContentRequest() which allows you to perform ContentRequest endpoint as defined by an endpoint using a Content Schema.

To learn more about content data endpoints, see the Author Content Endpoints Task.

Below is an example of a ContentRequest accessing a part of its data called exampleEndpoint:

val response: Response =
    context.doContentRequest(exampleContentRequest1.exampleRequest)

Utilize an Operation in the PragmaSDK #

Operations are accessed through the UPragmaPlayerDataService on the game client and UPragmaPlayerDataPartnerService for the game server.

To access an Operation in the Pragma SDK, first you need to know what kind of session the Operation falls under. For example, the UPragmaPlayerDataService provides access to Operations allowed on a player session and the UPragmaPlayerDataPartnerService provides access to Operations allowed on a partner session.

Operations are also accessed through the Player Data Service API and the Operation’s respective Sub Service. The Operation’s parameters correspond with the properties of the Kotlin Request class. The last parameter of the Operation is a delegate that provides access to the Response.

Below is an example of accessing an Operation called Echo() that belongs to the ExampleOperationsSubService:

Player->PlayerDataService()->ExampleOperationsSubService().Echo(
    "Hello from the client!", // request property
    FOnExampleOperationsExampleDelegate::CreateLambda(
         [](TOptional<FPragma_PlayerData_EchoResponseProto> Response){
                if (Response)
                {
                    auto DataFromPragmaOperation =         Response.GetValue().DataForClient;
                }
                else
                {
                    /* If there was an error with the operation, check the logs. */
                }
         }
    )
);