Setting up a Materials Module #
This tutorial will walk you through implementing a player data feature for managing collectable resource items.
This is our most comprehensive guide that shows how to incorporate content and custom error handling with player data. If you are just getting started with player data, check out our key-value store guide.
Get started #
Start with a fresh build of your project.
./pragma build -s
Define the data model #
Define a component class that will be used to store material data.
Define a Component #
Add a file:
[project]/[project]-player-data/player-data/src/main/kotlin/materials/Materials.kt
package documentation
import pragma.playerdata.Component
import pragma.playerdata.FieldNumber
import pragma.types.SerializedName
@SerializedName("<TIMESTAMP-IN-EPOCH>")
data class Material(
@FieldNumber(1) val materialId: String,
@FieldNumber(2) var balance: Int
): Component
Define APIs #
For our Materials Module, we’re going to define two PlayerDataOperations: one that lets you grant and add a material, and one that lets you deduct from the balance.
Define the request and response payloads #
You define the payload of the request and response for each operation.
Add the following to the Materials.kt file.
import pragma.playerdata.PlayerDataRequest
import pragma.playerdata.PlayerDataResponse
data class DeductMaterial(
val materialId: String,
val amount: Int
): PlayerDataRequest
class DeductedMaterial: PlayerDataResponse
data class AddMaterial(
val materialId: String,
val amount: Int
): PlayerDataRequest
class MaterialAdded: PlayerDataResponse
Define the operations #
Operations must be defined on a class that inherits from PlayerDataSubService and has a constructor that takes a PlayerDataContentLibrary instance.
addMaterial()will allow calls from trusted backends like game servers and other pragma game services and plugins.deductMaterial()can be called from trusted backends and clients.
package documentation
import pragma.playerdata.Context
import pragma.playerdata.PlayerDataContentLibrary
import pragma.playerdata.PlayerDataOperation
import pragma.playerdata.PlayerDataSubService
import pragma.rpcs.SessionType
class Materials(
contentLibrary: PlayerDataContentLibrary
): PlayerDataSubService(contentLibrary) {
@PlayerDataOperation(sessionTypes = [SessionType.PARTNER, SessionType.SERVICE])
fun addMaterial(request: AddMaterial, context: Context): MaterialAdded {
updateMaterial(request.materialId, request.amount, context)
return MaterialAdded()
}
@PlayerDataOperation(
sessionTypes = [SessionType.PLAYER, SessionType.PARTNER, SessionType.SERVICE]
)
fun deductMaterial(request: DeductMaterial, context: Context): DeductedMaterial {
updateMaterial(request.materialId, (-1 * request.amount), context)
return DeductedMaterial()
}
companion object {
const val MATERIALS_ENTITY_ID = "materials"
}
private fun updateMaterial(materialId: String, amount: Int, context: Context) {
// have one Entity to hold all Material Components
val materialsEntity = context.snapshot
.getOrCreateUniqueEntity(MATERIALS_ENTITY_ID, ::emptyList)
// check if there is a Material component for this materialId - if not, add one
materialsEntity.getComponentsByType<Material>()
.singleOrNull { it.materialId == materialId }
?: materialsEntity.addComponent(Material(materialId = materialId, balance = 0))
// filter out the component to update
val materialsToUpdate = materialsEntity
.getComponentsByType<Material>().single { it.materialId == materialId }
// updates the balance
materialsToUpdate.balance += amount
}
}
- In this example, we’ve decided to put all material
Componentsunder oneEntity. - We are using
context.snapshot.getOrCreateUniqueEntity()to ensure each player has oneEntitynamed “materials”. - You can add any number of
Componentsto anEntitywith theEntity.addComponentfunction. - To see all of the helper functions available for managing Entities and
Components, see the Reference documentation.
Add validation and error handling #
We have the basics of a materials module! However, the module currently does not have any validation There is no way to prevent unwanted scenarios such as a balance going negative or ability to cap at a limit.
Let’s add logic to handle expected problems. This is where we will integrate with the content data system and use custom errors.
Define a materials content catalog #
1. Define the content protobuf #
Add a new file at:
[project]/[project]-protos/src/main/proto/[project]/contentdata/materials.proto.
syntax = "proto3";
package documentation.customcontent;
import "pragmaOptions.proto";
option csharp_namespace = "CustomContent";
option (pragma.unreal_namespace) = "CustomContent";
message MaterialTemplate {
string material_id = 1;
int32 max_balance = 2;
}
Run the Pragma CLI command to generate protos
./pragma build project-protos
2. Define the content handler #
Content Handlers are classes that are responsible for loading, validating, and providing access to content data. When implementing, you must override the function idFromProto and have it return the String property that represents the id.
Add the content handler to:
[project]/[project]-player-data/player-data/src/main/kotlin/materials/Materials.kt
package documentation
import documentation.customcontent.Materials.MaterialTemplate
import pragma.content.ContentHandler
class MaterialsContentHandler
: ContentHandler<MaterialTemplate>(MaterialTemplate::class) {
override fun idFromProto(proto: MaterialTemplate): String {
return proto.materialId
}
}
3. Add the catalog JSON files #
- Under
[project]/content/src/. Add a fileMaterials.jsonandMaterials.metadata - Copy the following code in the
Materials.json.[ { "materialId" : "iron_ore", "maxBalance" : 1000 }, { "materialId" : "iron_ingot", "maxBalance" : 750 }, { "materialId" : "copper_ore", "maxBalance" : 500 } ]- Copy the following into
Materials.metadata
{ "applyTriggers": { "contentHandler": "MaterialsContentHandler" }, "versionsToRetain": 1 } - Copy the following into
Apply the catalog #
Run the content apply Pragma CLI command.
./pragma build -s # ensure jar has new types
./pragma content apply
Define a custom error #
We will use Pragma Application Errors to control expected error flows.
First we need to define a custom proto to use when throwing an ApplicationErrorException.
Add a new file: [project]/[project]-protos/src/main/proto/[project]/playerdata/applicationErrors.proto
syntax = "proto3";
package documentation.playerdata;
import "pragmaOptions.proto";
option csharp_namespace = "PlayerData";
option (pragma.unreal_namespace) = "PlayerData";
enum MaterialErrorTypes {
UNKNOWN = 0;
LIMIT_EXCEEDED = 1;
NEGATIVE_BALANCE = 2;
UNKNOWN_TYPE = 3;
INVALID_REQUEST_AMOUNT = 4;
}
message MaterialApplicationError {
// required option - force includes this proto in the sdk type generation
option (pragma.force_include_type) = true;
MaterialErrorTypes error_type = 2;
}
Run the generate protos command:
./pragma build project-protos
Update Materials sub-service #
We can now update the Materials sub-service to use our content catalog to verify material types and use application errors when an update would create an unwanted result.
[project]/[project]-player-data/player-data/src/main/kotlin/materials/Materials.kt
package documentation
// new imports
import documentation.customcontent.Materials.MaterialTemplate
import documentation.playerdata.ApplicatonErrors.MaterialApplicationError
import documentation.playerdata.ApplicatonErrors.MaterialErrorTypes
import documentation.playerdata.ApplicatonErrors.MaterialErrorTypes.*
import pragma.applicationRequire
import pragma.content.ContentData
// updated logic with validation and errors
class Materials(
contentLibrary: PlayerDataContentLibrary
): PlayerDataSubService(contentLibrary) {
companion object {
const val MATERIALS_ENTITY_ID = "materials"
}
// use contentLibrary to get the materials content catalog
private fun materialsTemplates(): ContentData<MaterialTemplate>
= contentLibrary.getContentData("MaterialTemplate.json")
// helper to build application error proto
private fun materialApplicationError(
errorType: MaterialErrorTypes
): MaterialApplicationError = MaterialApplicationError.newBuilder()
.setErrorType(errorType)
.build()
private fun validateRequestAmount(amount: Int) {
applicationRequire(amount >= 0 ) {
materialApplicationError( INVALID_REQUEST_AMOUNT)
}
}
@PlayerDataOperation(
sessionTypes = [SessionType.PLAYER, SessionType.PARTNER, SessionType.SERVICE]
)
fun deductMaterial(request: DeductMaterial, context: Context): DeductedMaterial {
validateRequestAmount(request.amount)
updateMaterial(request.materialId, (-1 * request.amount), context)
return DeductedMaterial()
}
@PlayerDataOperation(sessionTypes = [SessionType.PARTNER, SessionType.SERVICE])
fun addMaterial(request: AddMaterial, context: Context): MaterialAdded {
validateRequestAmount(request.amount)
updateMaterial(request.materialId, request.amount, context)
return MaterialAdded()
}
private fun updateMaterial(materialId: String, amount: Int, context: Context) {
// use the content data catalog to validate the materialId sent on the request
val materialTemplates = materialsTemplates()
applicationRequire(materialsTemplates().contains(materialId)) {
materialApplicationError(UNKNOWN_TYPE)
}
val materialTemplate = materialTemplates[materialId]!!
val materialsEntity = context.snapshot
.getOrCreateUniqueEntity(MATERIALS_ENTITY_ID, ::emptyList)
materialsEntity.getComponentsByType<Material>()
.singleOrNull { it.materialId == materialId }
?: materialsEntity.addComponent(Material(materialId, balance = 0))
val materialsToUpdate = materialsEntity
.getComponentsByType<Material>()
.single { it.materialId == materialId }
val currentBalance = materialsToUpdate.balance
applicationRequire(currentBalance + amount >= 0) {
materialApplicationError(NEGATIVE_BALANCE)
}
applicationRequire(amount + currentBalance <= materialTemplate.maxBalance) {
materialApplicationError(LIMIT_EXCEEDED)
}
materialsToUpdate.balance += amount
}
}
Call operations from game server and client #
Once operations are defined generate the sdk.
From your game project run the provided SDK update script.
update-pragma-sdk.sh
Call addMaterial from Partner SDK #
Unreal SDK: #
void AUnicornGameMode::AddBalance(const FString &PlayerId) {
// providing type - assign as private property on the GameMode etc ...
const Pragma::FServerPtr PartnerSdk;
PartnerSdk->PlayerDataService().Materials().AddMaterial(
PlayerId, "health-potions", 10,
FOnMaterialsAddMaterialDelegate::CreateLambda(
[](const TPragmaResult<FPragma_PlayerData_MaterialAddedProto>
&Result) {
if (Result.IsSuccessful()) {
const auto PlayersLoadout =
Result.Payload<FPragma_PlayerData_GetLoadoutResponseProto>();
UE_LOG(LogTemp, Display, TEXT("Current character from load: %s"),
*PlayersLoadout.SelectedCharacter);
} else {
// failure occurred check types and handle as wanted
}
}));
}
Call deductMaterial from Player SDK #
Unreal SDK #
void AUnicornPlayerController::DeductMaterial(
const FString MaterialId,
const int32 Amount
) {
Pragma::FPlayerPtr Player; // type provided - save as property on controller
Player->PlayerDataService().Materials().DeductMaterial(
MaterialId, Amount,
FOnMaterialsDeductMaterialDelegate::CreateLambda(
[](const TPragmaResult<FPragma_PlayerData_DeductedMaterialProto>
&Result) {
if (Result.IsFailure()) {
return; // check failure type and handle.
}
// cache has been updated - access current balance through it
const auto Payload =
Result.Payload<FPragma_PlayerData_DeductedMaterialProto>();
}));
}