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 a new directory at 5-ext/ext/src/main/kotlin/myDemo/inventory.
  2. Create a new Kotlin file in that directory called 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.*
import pragma.inventory.ext.*
import pragma.services.Service

@Suppress("unused", "UNUSED_PARAMETER")
class MyInstancedItemPlugin(service: Service, contentDataNodeService: ContentDataNodeService) : InstancedItemPlugin {

    override suspend 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)
            newItem.setBattlepass(battlepass)
        }

        return InstancedItemPlugin.InstancedItemPluginResult(newItem.build())
    }

    override suspend 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)
                            break
                        }
                    }

                    val updatedBattlepass = battlepass.toBuilder().setXp(xp.toLong()).setLevel(level.toLong())
                    val updatedExt = extItem.toBuilder().setBattlepass(updatedBattlepass).build()
                    val inventoryModifications = InventoryModifications(
                        instancedItemServerGrants = instancedItemServerGrants,
                        stackableItemGrants = stackableItemGrants
                    )
                    return InstancedItemPlugin.InstancedItemPluginResult(updatedExt, inventoryModifications)
                }
                ExtInstancedItemServerUpdate.DataCase.DATA_NOT_SET -> {
                    throw ExtException(
                        ExtError.UNKNOWN_EXT_ERROR,
                        "serverRequestExt does not contain battlepass update information"
                    )
                }
                else -> throw Exception("shouldn't get here")
            }
        } else {
            throw ExtException(
                ExtError.UNKNOWN_EXT_ERROR,
                "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) =
        battlepassSpec.levelsList[updatedLevel].xpToComplete

    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 -> {
                    acc.first.add(
                        InstancedItemServerGrant(
                            ExtInstancedItemServerGrant.getDefaultInstance(),
                            it.instanced.catalogId,
                            listOf()
                        )
                    )
                }
                Reward.ItemCase.STACKABLE -> {
                    acc.second.add(
                        StackableItemGrant(it.stackable.catalogId, it.stackable.amount, listOf())
                    )
                }
                else -> {}
            }
            acc
        }
    }
}
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.

Register the plugin #

  1. Open the 5-ext/config/local-dev.yml config file.
  2. Add the following to the game section of the config file:
pluginConfigs:
  InventoryService.instancedItemPlugin:
    class: "myDemo.inventory.MyInstancedItemPlugin"

Build #

Run make ext using a terminal from the platform directory.