Upgrading Instanced Items

Upgrading Instanced Items

Patrick

Top level header for Upgrading Instanced Items.

This article was written on Pragma Engine version 0.0.94.

Upgrading Instanced Items #

For the last article in this series, we’re going to continue building out our InstancedItemSeriesPlugin by learning how to create an upgrade system for already built instanced items.

If you haven’t already, make sure you’ve read the other two articles in the Instanced Items series for the custom content and context required in this guide.

For this tutorial, we’ll create an upgrade system that upgrades gear-type items with elemental runestones. To do this, we’re going to define stackable runestone items in JSON catalogs, catalog UpdateEntries in Protos and JSON, and customize our InstancedItemSeriesPlugin’s update() function.

Understanding Updates with Instanced Items #

Customizing an InstancedItemPlugin allows you to create logic for building entirely new instanced items as well as modifying already built items. Updating and modifying already built items requires a fleshed out update() function–an incredibly versatile tool for any type of item modification scenario that either a game client or server needs to run. In other words, the update() allows you to interact with and modify an already existing instanced item’s ext data via a client or server-based RPC.


A infograph on an InstancedItemPlugin’s functions.

The InstancedItemPlugin being used to build new and modify existing instanced items.

For example, in-game, the update() function can allow players to personalize or modify items in their inventory (like upgrading a sword with an elemental runestone). It can also enable certain structural services integral to your game, like a battlepass, quest log, and progress trackers that require continual, consistent updates to your backend.

To demonstrate how this works, we’re going to create an update() function that allows players to socket runestones (a stackable item) into any gear-type item’s ext fields in the player inventory.


Gear and runestones grouped together.

A grouping of gear-type items and socketable runestones.

However, before we start defining our update() logic, we need to first create the runestone stackable items players will upgrade their items with. We’ll also need to define the UpdateEntires catalog, which books the different update requests players can call via a client RPC to upgrade their instanced items.

Before we commence the proto and item updating… #

You’ll want to get your engine built and your 5-ext directory created.

Build the engine by running the following command in the terminal from the platform directory:

make skip-tests protos engine

Now run the next command to create a 5-ext folder (if you don’t have one already):

make ext

Now that our platform is all set to go, make sure you have all the custom content from the previous Instanced Items series articles before we define our item-upgrading system.

Creating the Stackable Runestones #

Our upgrading system requires that players socket their gear-type items with runestones. So, since each runestone-type item is going to be identical to one another, let’s create these runestones as stackable items!


A fire, ice, and earth runestone.

The three stackable, socketable runestones: Fire Runestone, Ice Runestone, and Earth Runestone.

Go to 5-ext/content/src/StackableSpecs.json and add the following code block below to create three different types of runestones. For more information on stackable items, read our Creating Stackable Items article in the Stackable Items Series.

[
  {
    "catalogId": "runestone_fire",
    "name": "Fire Runestone",
    "tags": ["fire", "socketable", "runestones"],
    "limit": 10,
    "removeIfNone": true
  },
  {
    "catalogId": "runestone_water",
    "name": "Ice Runestone",
    "tags": ["water", "socketable", "runestones"],
    "limit": 10,
    "removeIfNone": true
  },
  {
    "catalogId": "runestone_earth",
    "name": "Earth Runestone",
    "tags": ["earth", "socketable", "runestones"],
    "limit": 10,
    "removeIfNone": true
  }
]

For every runestone, a player has a limit of 10 per stack due to the limit field, and each stack is removed after depletion due to the removeIfNone value of true. We’re also giving each runestone a tag for being a runestone, a tag for being socketable with other items, and an elemental tag for each individual runestone (for example, the earth tag is given for runestone_earth).

Now that our runestones are made, we need to create our upgrade system’s UpdateEntries to keep track of the different runestone upgrades player’s can request for their gear-type instanced items.

Defining Update Entries in Protos and JSON #

Update entries are used to catalog the different types of item-updates for client or server based RPCs. Using update entries is great if you need to catalog many different types of item updates that require a cost, or something that must be exchanged for the update to proceed. Not every update() function scenario requires using update entries, but for our purposes, we’re using update entries to catalog the different types of runestone upgrades a player can make.


A diagram demonstrating how UpdateEntires are used with an InstancedItemPlugin

UpdateEntries being utilized to hold ext data for the update() function in the InstancedItemPlugin.

In its simplest form, you only need to define your UpdateEntries in the UpdateEntries.json catalog. However, because our upgrade system requires modifying an item’s ext fields (aka custom content), we need to add an additional ext field to each of our update entries. To do so, we need to create a protobuf definition for our update entries.

Creating the custom content for an Update Entry. #

Go to 5-ext/ext-protos/src/main/proto/shared/inventoryContentExt.proto (where we previously defined our GearSpec), and add the following code to the message for ExtUpdateEntry. This protobuf message is used to store all ext field data for any UpdateEntry.

message ExtUpdateEntry {
    oneof data {
        string socketed_runestone_catalog_id = 1;
    }
}

We’re nesting our protobuf message as a oneof data field for the socketed_runestone_catalog_id string. These strings will be defined in our UpdateEntries catalog and then added to a Gear’s repeated string field for socketed_runestones (you can find where we defined Gear in inventoryExt.proto).

The last thing we need to do before defining any UpdateEntries in JSON is getting our custom content in Protos built into Pragma Engine.

Making our proto definitions in 5-ext #

Do the following make command using platform as the working directory to get our newly defined custom content built into Pragma Engine.

make ext-protos

Now that our protobufs for UpdateEntries are ready to go, let’s create each update entry in the UpdateEntries.json` catalog.

Creating the UpdateEntries catalog #

Go to 5-ext/content/src/UpdateEntries.json and add the following code block to define the three different UpdateEntries for each runestone:

[
  {
    "id": "socket_runestone_fire",
    "costByCatalogId": {
      "runestone_fire": {
        "cost": 1
      }
    },
    "ext": {
      "socketedRunestoneCatalogId": "runestone_fire"
    }
  },
  {
    "id": "socket_runestone_water",
    "costByCatalogId": {
      "runestone_water": {
        "cost": 1
      }
    },
    "ext": {
      "socketedRunestoneCatalogId": "runestone_water"
    }
  },
  {
    "id": "socket_runestone_earth",
    "costByCatalogId": {
      "runestone_earth": {
        "cost": 1
      }
    },
    "ext": {
      "socketedRunestoneCatalogId": "runestone_earth"
    }
  }
]

UpdateEntries are very similar to storeEntries in stores; you have an id for the name of the entry, the costByCatalogId which defines the item cost for the entry to occur, and the amount of the costed item required. Additionally, for our specific UpdateEntries, we have an ext field that contains the string data we just defined in the ExtUpdateEntry protobuf message.

So, if a player still wants to upgrade their metal_sword_1 with a runestone_fire, they would call the UpdateEntry for socket_runestone_fire. This would remove 1 runestone_fire from their inventory and add the socketedRunestoneCatalogId string value of runestone_fire to their copper_sword’s ext field in socketed_runestones.


An upgrade copper sword enchanted with a fire runestone.

An upgraded Copper Sword with a Fire Runestone.

Now, in order for that scenario to take place, we still need to define the update() logic for our gear items to add socketed runestones to an already existing gear’s ext data. But before we move on, let’s apply our new content data changes from the StackableSpecs.json and UpdateEntries.json catalogs.

Register content changes #

To register the content changes we just made, run the following line of code in a terminal using platform as the working directory:

make ext-contentdata-apply

With our protos and JSON catalog for stackable runestones and UpdateEntries defined, we can create the coding logic to upgrade gear-type items with runestones! To start, we’ll continue working in our custom InstancedItemSeriesPlugin–made previously in the last article–by editing the update() function.

Modifying the InstancedItemPlugin’s update() function #

Creating any type of coding logic for update() operates very similarly to writing logic for newInstanced() –the function responsible for building new instanced item ext data. So with that in mind, you might notice some very similar structures and formulas we’ll be implementing with update() in order to create our gear upgrading structure.


A flowchart demonstrating how to make an update() function.

A flowchart describing the workflow of making a custom update() function in an InstancedItemPlugin.

The update() function’s required parameters #

To get started on implementing our gear upgrading system, go to the update() function’s code that we created previously in 5-ext/ext/src/main/kotlin/InstancedItemSeriesPlugin.kt. Both update() and newInstanced() share a lot of the same parameters, with a few key differences that we’ll explain in the table below.

    override fun update(
        initialInstancedItem: InstancedItem,
        instancedSpec: InventoryContent.InstancedSpec,
        updateEntry: InventoryContent.UpdateEntry,
        inventoryContent: InventoryServiceContent,
        startingInventory: InventoryData,
        pendingInventory: InventoryData,
        clientRequestExt: ExtInstancedItemUpdate?,
        serverRequestExt: ExtInstancedItemServerUpdate?
    ): InstancedItemPlugin.InstancedItemPluginResult {
        TODO("Not yet implemented")
    }
ParameterDescription
initialInstancedItemThe object for the already built instanced item ext that the function will modify. For our plugin, this initialInstancedItem is Gear.
updateEntryThe object for UpdateEntries defined in the UpdateEntry catalog (JSON files). Our plugin will be using the three UpdateEntries we previously defined for socketing runestones into Gear.
clientRequestExtThe object used for passing ext data from a client request, like a store purchase or an item update RPC. Since our item upgrading scenario requires player input from the client, we’ll be using this specific RequestExt.
serverRequestExtThe object used for passing ext data from a server request. Use this RequestExt for passing data for update scenarios that require server input.

Remember that not all of these parameters will be used by our update() function, but they are required in order for the pre-existing update() function to run. Make sure to take note of both the initialInstancedItem and updateEntry parameter, which are unique to the update() function and will be used by our own custom update() logic.

Now, let’s implement our custom logic to upgrade gear-type items with runestones!

Implementing the function’s result #

In the InstancedItemPluginResult’s curly brackets–replacing TODO("Not yet implemented") –add the following code block to carry out our custom gear-upgrading logic. Some of the values and logic are almost identical to our newInstanced() function, with some new additions that we’ll explain below:

        val extInstancedItemBuilder = ExtInstancedItem.newBuilder()
        val extItem: ExtInstancedItem = initialInstancedItem.ext
        return when (extItem.dataCase) {
            ExtInstancedItem.DataCase.GEAR -> {
                val updateExt: ExtUpdateEntry = updateEntry.ext
                val gearBuilder = extInstancedItemBuilder.gearBuilder
                gearBuilder.attributeValue = extItem.gear.attributeValue
                gearBuilder.addSocketedRunestones(updateExt.socketedRunestoneCatalogId)

                InstancedItemPlugin.InstancedItemPluginResult(
                    extInstancedItemBuilder.setGear(gearBuilder).build(),
                    InventoryModifications()
                )
            }

            else -> error("Unknown item!")
        }

We’re using the value extItem to represent the already built gear instanced item exts that players have in their inventory. We’ve also created an updateExt value that contains each individual UpdateEntry’s ext data defined in the JSON UpdateEntry catalog. Our gearBuilder then builds updated instanced items by using extItem to grab the original attributevalue from a gear’s ext, and adds runestones to a gear’s socketedRunestone ext field by using updateExts.

Notice how our gearBuilder line for socketedRunestones uses the .add interface instead of .set. This is because we need to add new strings to a gear’s empty SocketedRunestones array, unlike with AttributeValue, where we can set expected int values.

The structure of our code logic is very similar to newInstanced(), so with that in mind, we’ll want to end ExtInstancedItem.DataCase.GEAR with an InstancedItemPluginResult to build our updated Gear, and finish our return when statement with an else for "Unknown item!".

Making ext with the custom plugin #

To fully finish our InstancedItemSeriesPlugin and newly added runestone upgrade system, you’ll want to get your 5-ext directory created again to get our custom plugin built.

make ext

Testing our item upgrade system with Postman #

With our InstancedItemSeriesPlugin complete and fully fleshed out, we can test our runestones, gear-type instanced items, UpdateEntries, and custom InstancedItemSeriesPlugin by running service calls with Postman!

Simulating service calls with Postman #

To get Pragma Engine ready for testing, make sure you’re running a MySQL database and do a make run command in a terminal with platform as our working directory.

make run

With our engine up and running, let’s get testing!

To start, we’ll need to run the required authentication calls and grant our stackable runestones to a player.

Authenticating Operator and Player logins #

In the PragmaDev folder, navigate to the two service calls: Public ➨ Operator - AuthenticateOrCreateV2 and Public ➨ Player - AuthenticateOrCreateV2. Then click Send for both service calls and check that the response body for each call has given us pragmaTokens with a filled pragmaGameToken and pragmaSocialToken.

Next, let’s grant a player one Fire, Ice, and Earth Runestone. We need to make sure the player has these three runestones before upgrading any gear-type item, or else the upgrades won’t work.

Granting stackable items to a player #

Go to Game ➨ RPC - Operator ➨ Inventory ➨ GrantItemsOperatorV1 and open the service call’s body. In the payload’s itemGrants’s square brackets, add three separate stackable grants for runestone_fire, runestone_water, and runestone_earth. Then, click Send.

{
  "requestId": 1,
  "type": "InventoryRpc.GrantItemsOperatorV1Request",
  "payload": {
    "playerId": "{{test01PlayerId}}",
    "itemGrants": [
      {
        "stackable": {
          "catalogId": "runestone_fire",
          "amount": 1,
          "tags": []
        }
      },
      {
        "stackable": {
          "catalogId": "runestone_water",
          "amount": 1,
          "tags": []
        }
      },
      {
        "stackable": {
          "catalogId": "runestone_earth",
          "amount": 1,
          "tags": []
        }
      }
    ]
  }
}

Once you’ve called the GrantItemsOperatorV1 with our three stackable runestones, let’s make sure the player has all the stackables and instanced items for our runestone upgrades.

Checking an inventory for instanced and stackable items #

Go to Game ➨ RPC - Operator ➨ Inventory ➨ GetInventoryOperatorV1 and click Send to check the player’s inventory.

The player should have the three different stackable runestones alongside three gear-type instanced items from the last article in their inventory (one Copper Sword, Bronze Breastplate, and Iron Helmet). If they don’t, make sure you grant each gear item to a player via the GrantItemsOperatorV1. Just make sure to change the item grant from stackable to instanced.

Now that all of our prep work is complete, we can test our InstancedItemSeriesPlugin by upgrading each gear ext with one of our socketed runestones!

Updating instanced items with new ext data #

Let’s assume that our player, with their newly acquired Copper Sword, Bronze Breastplate, and Iron Helmet, wants to upgrade all their gear with one runestone each. This player wants to socket a Fire Runestone into their Copper Sword, an Ice Runestone into their Bronze Breastplate, and an Earth Runestone into their Iron Helmet.

Since our InstancedItemSeriesPlugin now has the logic to fulfill this player’s request, we can accomplish these three item upgrades by running the correct service call.

Go to Game ➨ RPC - Player ➨ Inventory ➨ UpdateItemV4 and open the service call’s body. In the payload’s itemUpdate’s curly brackets, make three separate instanced update calls by filling the instanceId with each gear’s instanceId (which can be found by running GetInventoryOperatorV1), and adding the correct updateEntryIds for each itemUpdate (check each updateEntryId with your UpdateEntries.json catalog). Below is an example of what socketing a runestone_fire to a metal_sword_1 looks like:

 {
  "requestId": 1,
  "type": "InventoryRpc.UpdateItemV4Request",
  "payload": {
    "itemUpdate": {
      "instanced":
      {
        "instanceId": "2e9404ab-c970-4e16-9542-8358819846a4",
        "updateEntryId": "socket_runestone_fire",
        "tags": [""]
      }
    }
  }
}

When you click Send for each call, you can check the response body to see each instanced item gain a runestone in the gear’s socketedRunestones array, and also see the instanceId of the stackable runestone that was removed for the upgrade.

Once you’ve run the service calls, let’s make sure all three of our gear has been upgraded by checking the player’s inventory with GetInventoryOperatorV1. Below, you can see an example of what a metal_sword_1 will look like once you’ve upgraded it with a runestone_fire.

"instanced": [
  {
    "catalogId": "metal_sword_1",
    "instanceId": "2e9404ab-c970-4e16-9542-8358819846a4",
    "ext": {
      "gear": {
        "attributeValue": 9.0,
        "socketedRunestones": [
          "runestone_fire"
        ]
      }
    }
  }
]

If your metal_sword_1 has a runestone_fire, your metal_chest_2 a runestone_water, and your metal_hat_3 a runestone_earth… congrats! You’ve just created your own custom content, three unique instanced items, and an InstancedItemPlugin that can create and upgrade gear-type instanced items!


A collage of all the gear and runestones alongside books and scrolls.

All of our work in the Instanced Items series: writing custom content in Protos, item catalogs in JSON, and an InstancedItemPlugin in Kotlin.

There’s so much you can do with defined custom content and custom Kotlin plugins; we’ll continue touching the surface of these systems in future tech blog articles.

Appendix #

Click here to view the fully complete InstancedItemSeriesPlugin.

import kotlin.random.Random
import kotlin.math.min
import pragma.content.ContentDataNodeService
import pragma.inventory.InventoryContent
import pragma.inventory.InventoryData
import pragma.inventory.InstancedItem
import pragma.inventory.InstancedItemPlugin
import pragma.inventory.InventoryModifications
import pragma.inventory.InventoryServiceContent
import pragma.inventory.ext.*
import pragma.services.Service

class InstancedItemSeriesPlugin(
    val service: Service,
    val contentDataNodeService: ContentDataNodeService
) : InstancedItemPlugin {     
  override fun newInstanced(
      instancedSpec: InventoryContent.InstancedSpec,
      inventoryContent: InventoryServiceContent,
      startingInventory: InventoryData,
      pendingInventory: InventoryData,
      clientRequestExt: ExtPurchaseRequest?,
      serverRequestExt: ExtInstancedItemServerGrant?
  ): InstancedItemPlugin.InstancedItemPluginResult {
      val extInstancedItemBuilder = ExtInstancedItem.newBuilder()
      val specExt: ExtInstancedSpec = instancedSpec.ext
      return when (specExt.dataCase) {
          ExtInstancedSpec.DataCase.GEAR_SPEC -> {
              val gearSpec: GearSpec = specExt.gearSpec
              val gearBuilder = Gear.newBuilder()
                  .setAttributeValue(Random.nextInt(gearSpec.primaryAttributeValueMin, gearSpec.primaryAttributeValueMax))
                  .build()
              val gearInstance = extInstancedItemBuilder.setGear(gearBuilder).build()
              InstancedItemPlugin.InstancedItemPluginResult(gearInstance)

          }
          else -> {
              error("Unknown item!")
          }
      }
  }



    override fun update(
        initialInstancedItem: InstancedItem,
        instancedSpec: InventoryContent.InstancedSpec,
        updateEntry: InventoryContent.UpdateEntry,
        inventoryContent: InventoryServiceContent,
        startingInventory: InventoryData,
        pendingInventory: InventoryData,
        clientRequestExt: ExtInstancedItemUpdate?,
        serverRequestExt: ExtInstancedItemServerUpdate?
    ): InstancedItemPlugin.InstancedItemPluginResult {         val extInstancedItemBuilder = ExtInstancedItem.newBuilder()
        val extItem: ExtInstancedItem = initialInstancedItem.ext
        return when (extItem.dataCase) {
            ExtInstancedItem.DataCase.GEAR -> {
                val updateExt: ExtUpdateEntry = updateEntry.ext
                val gearBuilder = extInstancedItemBuilder.gearBuilder
                gearBuilder.attributeValue = extItem.gear.attributeValue
                gearBuilder.addSocketedRunestones(updateExt.socketedRunestoneCatalogId)

                InstancedItemPlugin.InstancedItemPluginResult(
                    extInstancedItemBuilder.setGear(gearBuilder).build(),
                    InventoryModifications()
                )
            }

            else -> error("Unknown item!")
        }
    }
}


For more information, check out the rest of the articles in this series:

Part I: Creating Custom Content for Instanced Items
Part II: Building Unique Instanced Items with Plugins
Part III: Upgrading Instanced Items (this article)



Posted by Patrick Olszewski on December 16th, 2022
Other posts

Contact Us

Pragma is currently working with a select group of studios in development.

A great underlying platform is essential in bringing players together. Pragma's technology lets us focus on the creative side of game development while offloading much of the complexity of shipping at scale.

Nate Mitchell

Founder of Mountaintop Studios and Oculus