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 section for more information.

Below is an example of a generated Unreal Delegate:

DECLARE_DELEGATE_OneParam(FOnExampleOperationsEchoDelegate, TPragmaResult<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 Content Schema defined Request endpoints.

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

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 #

The PragmaSDK contains generated code from Pragma Engine’s Player Data service. This includes an Operation’s PlayerDataRequests and PlayerDataResponses through the game client and the game server.

All Operations are accessed through the Player Data Service API and the Operation’s respective Sub Service in the PragmaSDK.

To access an Operation in the Pragma SDK, first you need to know what kind of session the Operation falls under.

Unreal #

Operations are accessed through the UPragmaPlayerDataService on the game client and UPragmaPlayerDataPartnerService for the game server. In other words, UPragmaPlayerDataService provides access to Operations allowed on a Player session and UPragmaPlayerDataPartnerService provides access to Operations allowed on a Partner session.

The Operation’s parameters correspond with the properties of the Kotlin PlayerDataRequest class, and the last parameter of the Operation is a delegate providing access to the PlayerDataResponse.

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. */
                }
         }
    )
);

Unity #

Operations are accessed through either Pragma.Player.PlayerData for PLAYER facing Operations and Pragma.Server.PlayerData for PARTNER facing Operations.

The Operation’s parameters correspond with the properties of the Kotlin PlayerDataRequest and PlayerDataResponse classes.

Below are examples of accessing an Operation called Echo() belonging to the ExampleOperationsSubService:

var echoRequest = new EchoRequestProto
{
    Message = "hello from the client"
};
            
var future = player.PlayerData.DemoPlayerDataOperations.Echo(echoRequest);
yield return Utils.WithTimeout("PlayerData Echo Operation", future);
if (future.Result.IsSuccessful)
{
    EchoResponseProto response = future.Result.Payload;
}

Utilize PlayerDataClient to call Operations within Pragma Engine Services and Plugins #

The PlayerDataClient class helps facilitate calling PlayerDataOperations from custom services and Plugins.

The PlayerDataClient has two functions: doOperation() for calling a single Operation, and doBatchOperation() for calling multiple Operations. Learn more in the PlayerDataClient reference section.

Since we’re making a service to service call within Pragma, only Player Data Operations that allow the SERVICE session type through the @PlayerDataOperation annotation can be called. If you try to call an Operation that does not allow the SERVICE session type, an error will occur.

To use PlayerDataClient, construct it with an instance of a Pragma Engine service.

val service: Service
val playerDataClient = PlayerDataClient(service)

Below is an example of using the PlayerDataClient in the Party Plugin’s onAddPlayer.

    override suspend fun onAddPlayer(
        requestExt: ExtPlayerJoinRequest, playerToAdd: Party.PartyPlayer, party: Party.Party, partyConfig: PartyConfig,
    ) {
        val playerDataClient = PlayerDataClient(service)
        val getPartyData = GetPartyData() // PlayerDataRequest 
        val result = playerDataClient.doOperation(getPartyData, playerToAdd.playerId)
            .onSuccess { response ->
                 (response as PartyData) // Cast to expected PlayerDataResponse
                // Can now hand off data from this response onto the ExtPartyPlayer
                playerToAdd.ext = publicPlayerData
                    .setMmr(response.mmr)
                    .setDesiredCharacter(response.character)
                    .setSelectedCostumeCatalogId(response.selectedCostume)
                    .build()

            }
            .onFailure { /* handle failure - log, throw, etc ...  */ }
    }