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.
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.
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!
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.
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
.
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.
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")
}
Parameter | Description |
---|---|
initialInstancedItem | The object for the already built instanced item ext that the function will modify. For our plugin, this initialInstancedItem is Gear . |
updateEntry | The 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 . |
clientRequestExt | The 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 . |
serverRequestExt | The 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 ext
s 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 updateExt
s.
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 updateEntryId
s 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!
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 #
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)