Content Data #

Studios can use the Content Data system to create and interact with game content managed by the platform. This includes inventory, player loadouts, and player progression artifacts like missions, quests, and battle passes.

Understanding Content Data #

Pragma Engine’s Content Data system is designed for structured, versioned content management. It includes versioned content packages that can be quickly promoted to multiple environments, ensuring production is set up identically to staging. Content Data works together with the Player Data and Inventory services to support live service game content with a comprehensive set of tools and workflows.

Live service games need to patch content constantly, requiring tools and patterns around data versioning and data migration. These must be coordinated across platforms and across multiple game client versions.

The Content Data system enables live update scenarios such as rebalancing weapons without game client or server patches. It also supports more complex schema migration scenarios in which new fields are added, removed, and renamed.

Content Data system overview.

In the sections below, we’ll take a look at how the pieces of the Content Data system fit together, and how to create, access, and leverage new content.

Provided vs Custom Content #

The Content Data system supports two categories of content definitions: provided and custom.

Provided content: This is content used by the Pragma Engine Inventory service, such as instanced and stackable item specs. These can be customized by editing ext fields in the 5-ext folder. We recommend nesting your proto messages as oneOfs under the provided ext fields.

Custom content: This content is completely built by the user, who can define custom services, custom JSON, and custom proto definitions.

Commands #

typeexplanationcommand
init- initializes a new type of content by creating empty JSON and metadata files

- related proto and Content Handler must be defined
java -jar 5-ext/target/$(COMPANY).jar contentdata init -d <CONTENT PATH> -c <CONTENT HANDLER CLASS NAME> <CONTENT TYPE>

- CONTENT PATH: path to the content directory, with a default of 5-ext/content
- CONTENT HANDLER CLASS NAME: class name of the content handler
- CONTENT TYPE: name of content file without “.json” extension
plan- prints a summary of changes that can be appliedjava -jar 5-ext/target/$(COMPANY).jar contentdata plan -d <CONTENT PATH>

- CONTENT PATH: path to the content directory, with a default of 5-ext/content
apply- applies and packages content changes made to the src files

- prints a plan summary of changes.

Response options:
-y for auto-approve
-v for verbose (prints out entire JSON objects)
java -jar 5-ext/target/$(COMPANY).jar contentdata apply -d <CONTENT PATH>

- CONTENT PATH: path to the content directory, with a default of 5-ext/content

Adding Content #

File Architecture #

Two types of files make up the basics of adding and updating content in the Content Data system: protobuf files that define blueprints for content, and JSON files that populate these blueprints with specific content.

Content Protos #

Content protos are the source of truth for content definitions. They provide a blueprint for possible fields and attributes for each type of content.

JSON Files #

These files populate the actual instances of protos with user-defined data. They must perfectly match proto definitions.

Adding content with protos and JSON files.


Steps for Making New Content #

The content addition process follows several steps, which we’ll cover in this section:

  1. Creating the protos, which serve as blueprints for the JSON files
  2. Adding content with the JSON files
  3. Defining Content Handlers
  4. Running the apply command to verify and generate a JSON package that services can read

Creating the protos #

Edit or create any relevant protos in the 5-ext/ext-protos folder. If you’re editing provided content files, this is where you put the customizable ext fields.

Be sure to only edit files in 5-ext! If you edit files in the 1-protos folder, it won’t work.

Creating the JSON files #

Create and populate the two required JSON src/ files with the desired content. You can use the init command (in bash) to generate these files, or you can create them manually.

The two required files:

  • a <content-type>.json file that contains the list of content objects using the blueprints from the protos and defining specific fields.
  • a <content-type>.metadata file that contains metadata information, such as a link to the Content Handler and a version incrementer.
Remember to make sure that JSON objects can represent nested messages, so they need to have all fields filled out, not just ext fields.

Defining Content Handlers #

Content Handlers are a type of plugin assigned within the metadata file. ContentHandler is the class responsible for validating, containing, and providing access to content protos.

  • Provided Inventory service content comes with default Content Handlers.
  • Custom content must have an associated custom-defined Content Handler.

Custom-defined Content Handlers should be created and populated in the relevant service directory within 5-ext/ext/src/main/kotlin.

Defining a custom Content Handler for provided content is possible to enable certain features, such as cross-content validation across ext fields, which is covered below in Cross-Content Validation.

-> Creating a custom Content Handler
At minimum, Content Handlers must contain the class along with the following information:

  • the handler’s name and the fact that it’s a ContentHandler, along with the custom proto’s location, name, and its class:

    class [handler name]: ContentHandler<[package].[proto name]>([package].[proto name]::class) 
    
  • the idFromProto override that returns the proto ID

    override fun idFromProto(proto: [package].[proto name]): String {
        return proto.[id field]
        }
    
Example Content Handler

Below is an example of a very basic custom Content Handler based on an imaginary custom proto called MyCustomContent.

package demo.content

import pragma.content.ContentHandler

class MyCustomHandler: ContentHandler<CustomContent.MyCustomProto>(CustomContent.MyCustomProto::class) {
    override fun idFromProto(proto: CustomContent.MyCustomProto): String {
        return proto.id
    }
} 

Once the Content Handler has been created, you must assign it in the metadata file nested under applyTriggers:

“contentHandler”: “[Content Handler class name]

Applying changes and validating #

Run the apply command (in bash or RPC for dev-only) to verify data and generate a JSON package/ file that services can now read.

Do not make the package/ file yourself, as this would skip the validation step. We also recommend never editing package files manually.

-> Basic Validation
It’s easy to have typos or missing data, especially when working with nested protos. If you update your source of truth directly, you can potentially crash your game.

The apply command makes sure that when new content is applied, it’s been validated. It checks that the JSON matches the proto schema directly. Invalid content will prevent changes from being applied, and will offer specific error details to give users the chance to fix content immediately.

In addition to the validation of proto schemas, you can write custom validation for protos. For example, if you’d like to verify that a specific field is never empty.

-> Cross-Content Validation
Cross-content validation is available beginning in release 0.0.69.

For provided content, Pragma Engine validates content across nested IDs (outside of ext) during the contentdata apply command. Validation of custom content (across nested exts) can be added to a Content Handler under the validateExtWithAllContent function.

To validate across ext fields in provided content, a new Content Handler must be written. The new Content Handler needs to inherit from the provided Content Handler to maintain non-ext content validation.

The validateExtWithAllContent function must contain these steps:

  1. Pull out relevant contentData from the contentByType map. The keys are strings of content types, so use the content type’s file name without the JSON extension.
    • InventoryCrossContentValidator can be used to make this step easier when validating across provided Inventory content.
  2. Pull out nested content IDs from the exts.
    • These must be stored in a mutableSetOf<CorrespondingIds>(). CorrespondingIds is a class that holds the idToValidate and the parentId. This ensures error messages are clear to assist with debugging.
  3. Check that nested IDs are valid.
    • Confirm that IDs are all part of the relevant content data type using ContentData.validateIds(idsToValidate: Set<CorrespondingIds>, contentType: String).
Example class

Because cross-content validation is highly dependent on custom proto structure, the dropdown below contains a specific example based on our DemoCraftingEntriesHandler in 4-demo. As you can see, only the pet evolution spec has cross-content validation, as each data case will need its own implementation.

class DemoCraftingEntriesHandler: CraftingEntriesHandler() {
   override fun validateExtWithAllContent(contentByType: Map<String, ContentData<*>>) {
       val inventoryValidator = InventoryCrossContentValidator(contentByType)
       val catIdsToValidate = mutableSetOf<CorrespondingIds>()

       values.forEach { craftingEntry ->
           val ext: ExtCraftingEntry = craftingEntry.ext
           when (ext.dataCase) {
               ExtCraftingEntry.DataCase.PET_EVOLUTION_SPEC -> {
                   val catalogId = ext.petEvolutionSpec.targetPetCatalogId
                   catIdsToValidate.add(CorrespondingIds(catalogId, craftingEntry.id))
               }
               ExtCraftingEntry.DataCase.COOKING_SPEC -> {}
               ExtCraftingEntry.DataCase.COOKING_COMPLETE_SPEC -> {}
               ExtCraftingEntry.DataCase.QUEST_COMPLETE_SPEC -> {}
               else -> {}
           }
       }

       inventoryValidator.validateCatalogIds(catIdsToValidate, CRAFTING_ENTRIES)
   }
}

Accessing and Leveraging Content #

To use the content created in the previous section, it must be accessed by the ContentDataNodeService. Once this is set up, plugins can use the content.

Giving plugins access to your new content.

Enable Content Access #

Content access is built on two main pieces:

  • the ContentDataNodeService, which calls
  • the ContentData interface

The ContentDataNodeService is a service used to fetch specific content. This uses the following call, and requires the content proto and the .json file resource path:

fun <ContentProto: GeneratedMessageV3> getHandler(
	resourcePath: String //<content-type>.json
): ContentData<ContentProto> {

The getHandler returns a ContentData interface, which provides read-only access to content through a map where keys are the content ID. The getHandler only returns the requested content type.

This interface has two primary functions:

  • get(id: String) takes in the ID and string, and provides the content proto.
  • contains(id: String) is a helper function that provides a boolean confirming if content is available.

Leverage Content with Plugins #

Plugins can access all content visible to the ContentDataNodeService.

Provided plugins are already set up to pull in the ContentDataNodeService. Custom plugins that want to use content will need the following line added to their run() function:

content.init(nodeServicesContainer[ContentDataNodeService::class])