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:
- Defining a Sub Service
- Creating the Request and Response classes
- Authoring an Operation function
- Modifying players’ data
- Accessing Content Schema data in an Operation
- Performing a Content Request
- Utilizing an Operation in the PragmaSDK
- Utilizing PlayerDataClient to call Operations within Pragma Engine Services and Plugins
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 ... */ }
}