Creating the Battlepass Plugin #

In this section, we’ll be creating the plugin that powers the battlepass logic, and then registering it in the config.

Create an instanced item plugin #

First we’ll create a Pragma Engine plugin that initializes the battlepass and defines the leveling logic.

  1. Create new directories at 5-ext/ext/src/main/kotlin/myDemo/inventory.
  2. Create a Kotlin file at 5-ext/ext/src/main/kotlin/myDemo/inventory/MyInstancedItemPlugin.kt.
  3. Define a class that implements the InstancedItemPlugin interface:

package myDemo.inventory

import pragma.ExtError
import pragma.ExtException
import pragma.content.ContentDataNodeService
import pragma.inventory.InstancedItem
import pragma.inventory.InstancedItemPlugin
import pragma.inventory.InstancedItemServerGrant
import pragma.inventory.InventoryContent
import pragma.inventory.InventoryData
import pragma.inventory.InventoryServiceContent
import pragma.inventory.StackableItemGrant
import pragma.inventory.ext.Battlepass
import pragma.inventory.ext.BattlepassSpec
import pragma.inventory.ext.ExtInstancedItem
import pragma.inventory.ext.ExtInstancedItemServerGrant
import pragma.inventory.ext.ExtInstancedItemServerUpdate
import pragma.inventory.ext.ExtInstancedItemUpdate
import pragma.inventory.ext.ExtInstancedSpec
import pragma.inventory.ext.ExtPurchaseRequest
import pragma.inventory.ext.Reward
import pragma.utils.TimeProxy

class MyInstancedItemPlugin(private val timeProxy: TimeProxy = TimeProxy()) : InstancedItemPlugin {

    constructor(service: Service, contentDataNodeService: ContentDataNodeService) : this()

    override fun newInstanced(
        instancedSpec: InventoryContent.InstancedSpec,
        inventoryContent: InventoryServiceContent,
        startingInventory: InventoryData,
        pendingInventory: InventoryData,
        clientRequestExt: ExtPurchaseRequest?,
        serverRequestExt: ExtInstancedItemServerGrant?
    ): InstancedItemPlugin.InstancedItemPluginResult {
        val extSpec: ExtInstancedSpec = instancedSpec.ext
        val newitem = ExtInstancedItem.newBuilder()
        if (extSpec.dataCase == ExtInstancedSpec.DataCase.BATTLEPASS_SPEC) {
            val battlepass = Battlepass.newBuilder().setLevel(0).setXp(0)

        return InstancedItemPlugin.InstancedItemPluginResult(

    override fun update(
        initialInstancedItem: InstancedItem,
        instancedSpec: InventoryContent.InstancedSpec,
        updateEntry: InventoryContent.UpdateEntry,
        inventoryContent: InventoryServiceContent,
        startingInventory: InventoryData,
        pendingInventory: InventoryData,
        clientRequestExt: ExtInstancedItemUpdate?,
        serverRequestExt: ExtInstancedItemServerUpdate?
    ): InstancedItemPlugin.InstancedItemPluginResult {
        val extSpec: ExtInstancedSpec = instancedSpec.ext
        val extItem: ExtInstancedItem = initialInstancedItem.ext
        val battlepassSpec: BattlepassSpec = extSpec.battlepassSpec
        val battlepass = extItem.battlepass

        // example for server battlepass update, because we trust incoming requests
        // players should not be able to say "hey I'm going to give myself battlepass updates!"
        // (clientRequestExt)

        if (serverRequestExt != null) {
            when (serverRequestExt.dataCase) {
                ExtInstancedItemServerUpdate.DataCase.XP_UPDATE -> {
                    if (isBattlepassMaxedOut(battlepassSpec, battlepass)) {
                        return InstancedItemPlugin.InstancedItemPluginResult(extItem)

                    val instancedItemServerGrants = mutableListOf<InstancedItemServerGrant>()
                    val stackableItemGrants = mutableListOf<StackableItemGrant>()

                    var level = battlepass.level.toInt()
                    var xp = (battlepass.xp + serverRequestExt.xpUpdate.amount).toInt()

                    while (isLevelComplete(battlepassSpec, level, xp)) {
                        toPragmaReward(battlepassSpec.levelsList[level].rewardsList).apply {
                            instancedItemServerGrants += this.first
                            stackableItemGrants += this.second

                        if (!isMaxLevel(battlepassSpec, level)) {
                            xp -= getLevelXpToComplete(battlepassSpec, level)
                            level += 1
                        } else {
                            xp = getLevelXpToComplete(battlepassSpec, level)

                    val updatedBattlepass =
                    val updatedExt = extItem.toBuilder().setBattlepass(updatedBattlepass).build()
                    return InstancedItemPlugin.InstancedItemPluginResult(
                        instancedItemServerGrants = instancedItemServerGrants,
                        stackableItemGrants = stackableItemGrants
                ExtInstancedItemServerUpdate.DataCase.DATA_NOT_SET -> {
                    throw ExtException(
                        "serverRequestExt does not contain battlepass update information"
                else -> throw Exception("shouldn't get here")
        } else {
            throw ExtException(
                "Cannot process this request"
            ) // client request, or at least server not provided

    private fun isBattlepassMaxedOut(
        battlepassSpec: BattlepassSpec,
        battlepass: Battlepass
    ): Boolean {
        return isMaxLevel(battlepassSpec, battlepass.level.toInt()) &&
            isLevelComplete(battlepassSpec, battlepass.level.toInt(), battlepass.xp.toInt())

    private fun isMaxLevel(battlepassSpec: BattlepassSpec, level: Int): Boolean {
        return level == battlepassSpec.levelsList.size - 1

    private fun isLevelComplete(battlepassSpec: BattlepassSpec, level: Int, xp: Int): Boolean {
        return xp >= getLevelXpToComplete(battlepassSpec, level)

    private fun getLevelXpToComplete(battlepassSpec: BattlepassSpec, updatedLevel: Int) =

    fun findActive(
        battlepassSpecs: List<InventoryContent.InstancedSpec>
    ): InventoryContent.InstancedSpec? {
        return battlepassSpecs.firstOrNull() {
            val ext: ExtInstancedSpec = it.ext

    private fun isActive(battlepassSpec: BattlepassSpec): Boolean {
        val currentTime = timeProxy.currentEpochMillis()
        return currentTime >= battlepassSpec.startUnixTimestamp &&
            currentTime <= battlepassSpec.endUnixTimestamp

    private fun toPragmaReward(
        rewards: List<Reward>
    ): Pair<List<InstancedItemServerGrant>, List<StackableItemGrant>> {
        return rewards.fold(
            mutableListOf<InstancedItemServerGrant>() to mutableListOf<StackableItemGrant>()
        ) { acc, it ->
            when (it.itemCase) {
                Reward.ItemCase.INSTANCED -> {
                Reward.ItemCase.STACKABLE -> {
                        StackableItemGrant(it.stackable.catalogId, it.stackable.amount, listOf())
                else -> {}
Method overview

The newInstanced method makes a battlepass builder that sets XP and level to 0. It also creates a new instanced item representing the battlepass that can be added to a player’s inventory. Using proto builders, this method first makes the ext spec which contains XP and level (both set to 0), then adds that to the instanced item which represents the battlepass.

The update method defines the logic that powers a battlepass update when XP is earned by the player. We first ensure the request is coming from the server and that they are battlepass XP updates, preventing players from exploiting client-side information to directly trigger leveling up their battlepasses. Next, we utilize several helper functions while building out the math behind receiving XP and leveling. There are two possible scenarios that are dependent on whether the player has reached the max battlepass level or XP:

a. If the battlepass is entirely capped and cannot receive more levels or XP, we leave the battlepass as-is. We finish by returning the battlepass to the player with no earned Rewards.

b. If the battlepass is able to receive additional XP, we apply XP to the current level.

  • This continues until we can no longer level up, and the remaining XP is applied to the current level to mark its progress.
  • As the battlepass completes levels, we also build a list of Rewards of InstancedItems and StackableItems the player has earned for completing each level.
  • Using proto builders, we update the battlepass with its level and XP progress as computed before.
  • To finish, we return the plugin result containing the updated battlepass and the lists of InstancedItems and StackableItems the player may have earned.
  1. Register your plugins by editing 5-ext/config/local-dev.yml. Ensure the game section of the config file matches the following:
   class: "myDemo.inventory.MyInstancedItemPlugin"
  1. Run make ext using a terminal from the platform directory.