Customizing the Crafting Plugin #

In this section, we’ll go over writing a Crafting Plugin for evolving pets and an Instanced Item Plugin that builds pets with custom ext data.

Prerequisites:


Write the Crafting Plugin #

  1. Go to 5-ext/ext/src/main/kotlin.
  2. Create a Kotlin file called PetTutorialCraftingPlugin.kt.
  3. Copy the following code into the PetTutorialCraftingPlugin.kt file to create a class that implements the CraftingPlugin interface
Even if you only use craft() and not meetsRequirements(), both functions must still be implemented in order for the plugin to run.
package pragma.inventory

import pragma.PragmaError
import pragma.PragmaException
import pragma.PragmaResult
import pragma.PragmaResultError
import pragma.PragmaResultResponse
import pragma.content.ContentDataNodeService
import pragma.inventory.ext.*
import pragma.services.Service

class PetTutorialCraftingPlugin(
    val service: Service,
    val contentDataNodeService: ContentDataNodeService,
) : CraftingPlugin {

    override fun meetsRequirements(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        requestExt: ExtCraftRequest
    ): PragmaResult<Unit, List<String>> {
        TODO("Not yet implemented")
    }

    override fun craft(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        inventoryContent: InventoryServiceContent,
        requestExt: ExtCraftRequest
    ): InventoryModifications {
        TODO("Not yet implemented")
    }
}
  1. Replace the empty meetsRequirements() function with the following code:
    override fun meetsRequirements(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        requestExt: ExtCraftRequest
    ): PragmaResult<Unit, List<String>> {
        val extCraftingRequirements: ExtPurchaseRequirements = craftingEntry.requirements.ext
        val errors = mutableListOf<String>()

        try {
            val itemToDestroy = destroyedInstancedItems.single()
            if (
                itemToDestroy.catalogId !=
                extCraftingRequirements.petEvolutionRequirements.requiredPetCatalogId
            ) {
                errors.add("Pet Evolution Requirement not met.")
            }
        } catch (e: RuntimeException) {
            errors.add("Incorrect number of Pets were offered for evolution.")
        }
        if (
            requestExt.petEvolutionRequest.craftingLocation !=
            extCraftingRequirements.petEvolutionRequirements.requiredPlayerLocation
        ) {
            errors.add("Pet Evolution Location not met.")
        }

        if (errors.isNotEmpty()) {
            return PragmaResultError(errors)
        }
        return PragmaResultResponse(Unit)
    }
Method Overview

The meetsRequirements() function creates a PragmaResultResponse object when requirements are met for the craft() call. On failure, it creates a PragmaResultError object instead.

  • In an actual game meetsRequirements() might need to check the available properties of the craftingEntry.requirements.ext to determine specific crafting behavior when the plugin supports multiple forms of crafting. In this example we are only evolving pets, so this check is not necessary.

  • The meetsRequirements() function then adds any relevant error messages to a list of strings. This example checks that only one pet is being evolved (destroyedInstancedItems) and that the given pet has the correct catalogId.

  • If any errors are encountered, the list of errors are returned inside a PragmaResultError object. The platform identifies that the craft() action cannot be completed and will report the given errors back to the client requester.

  • If all requirements are met, the meetsRequirements() function returns a PragmaResultResponse(Unit) to allow the platform to continue with its item grants or deletions.

  1. Replace the craft() stub with the following code and implement the petEvolution() method:
    override fun craft(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        inventoryContent: InventoryServiceContent,
        requestExt: ExtCraftRequest
    ): InventoryModifications {
        val extCraftingEntry: ExtCraftingEntry = craftingEntry.ext
        if (extCraftingEntry.hasPetEvolutionSpec()) {
            return petEvolution(
                extCraftingEntry.petEvolutionSpec,
                destroyedInstancedItems,
                requestExt.petEvolutionRequest
            )
        }
        throw PragmaException(PragmaError.UNKNOWN_ERROR, "An unknown error occurred.")
    }

    private fun petEvolution(
        petEvolutionSpec: PetEvolutionSpec,
        destroyedInstancedItems: List<InstancedItem>,
        request: PetEvolutionRequest
    ): InventoryModifications {
        val catalogId = petEvolutionSpec.targetPetCatalogId
        val instancedItemGrants = mutableListOf<InstancedItemServerGrant>()
        val ext: ExtInstancedItem = destroyedInstancedItems.single().ext
        val pet =
            ext.pet
                .toBuilder()
                .setBonus(ext.pet.bonus)
                .setBonusAmount(ext.pet.bonusAmount + petEvolutionSpec.bonusIncrement)
                .build()

        instancedItemGrants.add(
            InstancedItemServerGrant(
                ExtInstancedItemServerGrant.newBuilder()
                    .setPetEvolution(PetEvolution.newBuilder().setPet(pet))
                    .build(),
                catalogId,
                mutableListOf(request.craftingLocation)
            )
        )
        val stackableItemGrants = mutableListOf<StackableItemGrant>()

        return InventoryModifications(
            instancedItemServerGrants = instancedItemGrants,
            stackableItemGrants = stackableItemGrants
        )
    }
Method Overview

The craft() function is responsible for returning the InventoryModifications object that contains two lists. The first is a list of instanced items to grant (via an InstancedItemServerGrant) and the second is a list of stackable items to grant (via a StackableItemGrant.)

  • craft() checks the available properties of extCraftingEntry to determine the proper crafting behavior. In this example, the function checks to make sure the entry has a petEvolutionSpec before entering the petEvolution() flow. Other flows will make use of the oneof data property on the ExtCraftingEntry so that widely-varying types can be handled in the same plugin.

  • The petEvolution helper method uses the information from craft() to perform the requested changes. petEvolution uses the data in the craftingEntry, the destroyedInstancedItems, and the extCraftRequest, and generates an appropriate number of InstancedItemServerGrant or StackableItemGrant objects to add to the InventoryModifications object. In this example, only InstancedItemServerGrant objects will be returned.

  • InventoryModifications() returns a generated list of the instancedItemGrants and the stackableItemGrants.

Write the Instanced Item Plugin #

  1. Go to 5-ext/ext/src/main/kotlin.

  2. Create a Kotlin file called PetTutorialInstancedItemPlugin.kt.

  3. Copy the following code into the PetTutorialInstancedItemPlugin.kt file to create a class that implements the InstancedItemPlugin’s interface:

Even though newInstanced() is the only function utilized, update() must still be implemented in order for the plugin to run.
package pragma.inventory

import pragma.content.ContentDataNodeService
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.ExtInstancedItem
import pragma.inventory.ext.Pet
import pragma.services.Service
import pragma.utils.Bound
import pragma.utils.RandomProxy

class PetTutorialInstancedItemPlugin(
    val service: Service,
    val contentDataNodeService: ContentDataNodeService
) : InstancedItemPlugin {

    private val randomProxy = RandomProxy()
    private val petFactory = PetFactory(randomProxy)

    override fun newInstanced(
        instancedSpec: InventoryContent.InstancedSpec,
        inventoryContent: InventoryServiceContent,
        startingInventory: InventoryData,
        pendingInventory: InventoryData,
        clientRequestExt: ExtPurchaseRequest?,
        serverRequestExt: ExtInstancedItemServerGrant?
    ): InstancedItemPlugin.InstancedItemPluginResult {
        val instancedSpecExt: ExtInstancedSpec = instancedSpec.ext
        val builder = ExtInstancedItem.newBuilder()

        if (instancedSpecExt.dataCase == ExtInstancedSpec.DataCase.PET_SPEC) {
            if (serverRequestExt != null && serverRequestExt.hasPetEvolution()) {
                builder.pet = serverRequestExt.petEvolution.pet
            } else {
                builder.pet = petFactory.create(instancedSpecExt)
            }
        }

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

    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 used in this example")
    }
}

class PetFactory(private val randomProxy: RandomProxy) {
    fun create(instancedSpecExt: ExtInstancedSpec): Pet {
        val bonusType = instancedSpecExt.petSpec.bonusesList.random()
        val minVal = instancedSpecExt.petSpec.bonusMin
        val maxVal = instancedSpecExt.petSpec.bonusMax

        return Pet.newBuilder()
            .setBonus(bonusType)
            .setBonusAmount(randomProxy.random(minVal, maxVal, Bound.INCLUSIVE))
            .build()
    }
}
Method Overview

newInstanced() interacts with and builds an instanced item’s ext data with ExtInstancedItem molds, and all custom logic must be defined in the InstancedItemPluginResult. For this example, newInstanced() checks if it is building a pet, and if so, it uses the PetFactory() function to build a pet’s ext data.

For a more detailed overview on how the Instanced Item Plugin works, check out the Instanced Item tutorial on Customizing the Instanced Item Plugin.

Configure the plugins #

Plugins must be configured in YAML configuration files before they can be utilized by Pragma Engine.

  1. Open 5-ext/config/local-dev.yml.
  2. Register the PetTutorialCraftingPlugin and PetTutorialInstancedItemPlugin by ensuring the game section of the config file matches the following:
game:
  pluginConfigs:
    InventoryService.craftingPlugin:
      class: "pragma.inventory.PetTutorialCraftingPlugin"
    InventoryService.instancedItemPlugin:
      class: "pragma.inventory.PetTutorialInstancedItemPlugin"

Build plugin changes #

Run the following make command using platform as the working directory to register plugin changes:

make ext

Test the Plugins #

In this section, we’ll test if the plugin logic builds and evolves pets with custom ext data by running API calls in Postman.

Start Pragma Engine #

Run Pragma Engine via one of the following methods.

Running via Make
Run make run to start the platform. Run this in a terminal with platform as the working directory.
Running in IntelliJ

From the IntelliJ toolbar in the upper right, ensure MainKt - LocalConfigured is selected, then click the play button.

If MainKt - LocalConfigured isn’t available, you will need to configure it. In the IntelliJ toolbar, click the dropdown next to the run button, then click Edit Configurations…. In the Run/Debug Configurations window that appears, expand Kotlin in the left hand side, then select MainKt - LocalConfigured. Click OK. Click the play button in the IntelliJ toolbar to start Pragma Engine.

Once the engine has started successfully, it prints the message [main] INFO main - Pragma server startup complete.

Simulate service calls #

Test the PetTutorialCraftingPlugin and PetTutorialInstancedItemPlugin by running the required authentication calls, granting the required stackable materials and a pet fire_dragon, and running a craft request to evolve a fire_dragon into a fire_dragon_2.

  1. Open Postman.
  2. Navigate to the two service calls PragmaDev ➨ Public ➨ Operator - AuthenticateOrCreateV2 and PragmaDev ➨ Public ➨ Player - AuthenticateOrCreateV2.
  3. Click Send for both service calls and check that the response body for each call has pragmaTokens with a filled pragmaGameToken and pragmaSocialToken.
  4. Go to PragmaDev ➨ Game ➨ RPC - Operator ➨ Inventory ➨ ApplyInventoryOperationsOperatorV1 and open the service call’s body.
  5. Insert the following code block to add 2000 gold_coins and fire_shards, 2 fire_gems, and 1 fire_dragon.
{
    "requestId": 1,
    "type": "InventoryRpc.ApplyInventoryOperationsOperatorV1Request",
    "payload": {
        "playerId": "{{test01PlayerId}}",
        "dataOperations": {
            "stackableItemGrants": [ 
                { 
                    "catalogId": "gold_coins", 
                    "amount": 2000
                },
                { 
                    "catalogId": "fire_shard", 
                    "amount": 2000
                },
                { 
                    "catalogId": "fire_gem", 
                    "amount": 2
                }  
            ],
            "instancedItemServerGrants": [
                {
                    "ext": {},
                    "catalogId": "fire_dragon"
                }
            ]
        }
    }
}
  1. Click Send to grant the player their items. You can also check the payload to make sure the fire_dragon has an attribute of XP, Gold, or Luck with a value between 5 to 10 as specified in InstancedSpecs.json.
  2. Go to PragmaDev ➨ Game ➨ RPC - Player ➨ Inventory ➨ Crafting Scenarios ➨ craftV1 and open the service call’s body.
  3. Insert the following code block to make a craftRequest for evolving a pet. Note that the ext field contains the petEvolutionRequest as defined in inventoryContentExt.proto and has the craftingLocation set to fire_lake.
{
  "requestId": 1,
  "type": "InventoryRpc.CraftV1Request",
  "payload": {
    "craftRequest": {
      "ext": {
        "petEvolutionRequest": {
          "craftingLocation": "fire_lake"
        }
      },
      "craftingEntryId": "fire_dragon_2_evolution",
      "itemsToDestroy": [
        "<craftableItemId>"
      ]
    }
  }
}
  1. Insert the instanceId of the fire_dragon into <craftableItemId> in itemsToDestroy. You can find the instancedId of the fire_dragon by going to the payload from the previous ApplyInventoryOperationsOperatorV1 service call.
  2. Click Send to evolve the fire_dragon into a fire_dragon_2. Check the service call’s payload to confirm the removal of previously granted gold_coins, fire_shards, fire_gems, and that fire_dragon has been exchanged for a fire_dragon_2 with the same bonus type as the fire_dragon increased by 2.

Appendix #

The completed PetTutorialCraftingPlugin.kt and DemoInstancedItemPlugin.kt files are below.

Click here to view the completed PetTutorialCraftingPlugin.kt file
package pragma.inventory

import pragma.PragmaError
import pragma.PragmaException
import pragma.PragmaResult
import pragma.PragmaResultError
import pragma.PragmaResultResponse
import pragma.content.ContentDataNodeService
import pragma.inventory.ext.*
import pragma.services.Service

class PetTutorialCraftingPlugin(
    val service: Service,
    val contentDataNodeService: ContentDataNodeService,
) : CraftingPlugin {

    override fun meetsRequirements(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        requestExt: ExtCraftRequest
    ): PragmaResult<Unit, List<String>> {
        val extCraftingRequirements: ExtPurchaseRequirements = craftingEntry.requirements.ext
        val errors = mutableListOf<String>()

        try {
            val itemToDestroy = destroyedInstancedItems.single()
            if (
                itemToDestroy.catalogId !=
                extCraftingRequirements.petEvolutionRequirements.requiredPetCatalogId
            ) {
                errors.add("Pet Evolution Requirement not met.")
            }
        } catch (e: RuntimeException) {
            errors.add("Incorrect number of Pets were offered for evolution.")
        }
        if (
            requestExt.petEvolutionRequest.craftingLocation !=
            extCraftingRequirements.petEvolutionRequirements.requiredPlayerLocation
        ) {
            errors.add("Pet Evolution Location not met.")
        }

        if (errors.isNotEmpty()) {
            return PragmaResultError(errors)
        }
        return PragmaResultResponse(Unit)
    }

    override fun craft(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        inventoryContent: InventoryServiceContent,
        requestExt: ExtCraftRequest
    ): InventoryModifications {
        val extCraftingEntry: ExtCraftingEntry = craftingEntry.ext
        if (extCraftingEntry.hasPetEvolutionSpec()) {
            return petEvolution(
                extCraftingEntry.petEvolutionSpec,
                destroyedInstancedItems,
                requestExt.petEvolutionRequest
            )
        }
        throw PragmaException(PragmaError.UNKNOWN_ERROR, "An unknown error occurred.")
    }

    private fun petEvolution(
        petEvolutionSpec: PetEvolutionSpec,
        destroyedInstancedItems: List<InstancedItem>,
        request: PetEvolutionRequest
    ): InventoryModifications {
        val catalogId = petEvolutionSpec.targetPetCatalogId
        val instancedItemGrants = mutableListOf<InstancedItemServerGrant>()
        val ext: ExtInstancedItem = destroyedInstancedItems.single().ext
        val pet =
            ext.pet
                .toBuilder()
                .setBonus(ext.pet.bonus)
                .setBonusAmount(ext.pet.bonusAmount + petEvolutionSpec.bonusIncrement)
                .build()

        instancedItemGrants.add(
            InstancedItemServerGrant(
                ExtInstancedItemServerGrant.newBuilder()
                    .setPetEvolution(PetEvolution.newBuilder().setPet(pet))
                    .build(),
                catalogId,
                mutableListOf(request.craftingLocation)
            )
        )
        val stackableItemGrants = mutableListOf<StackableItemGrant>()

        return InventoryModifications(
            instancedItemServerGrants = instancedItemGrants,
            stackableItemGrants = stackableItemGrants
        )
    }

}
Click here to view the completed PetTutorialInstancedItemPlugin file
package pragma.inventory

import pragma.content.ContentDataNodeService
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.ExtInstancedItem
import pragma.inventory.ext.Pet
import pragma.services.Service
import pragma.utils.Bound
import pragma.utils.RandomProxy

class PetTutorialInstancedItemPlugin(
    val service: Service,
    val contentDataNodeService: ContentDataNodeService
) : InstancedItemPlugin {

    private val randomProxy = RandomProxy()
    private val petFactory = PetFactory(randomProxy)

    override fun newInstanced(
        instancedSpec: InventoryContent.InstancedSpec,
        inventoryContent: InventoryServiceContent,
        startingInventory: InventoryData,
        pendingInventory: InventoryData,
        clientRequestExt: ExtPurchaseRequest?,
        serverRequestExt: ExtInstancedItemServerGrant?
    ): InstancedItemPlugin.InstancedItemPluginResult {
        val instancedSpecExt: ExtInstancedSpec = instancedSpec.ext
        val builder = ExtInstancedItem.newBuilder()

        if (instancedSpecExt.dataCase == ExtInstancedSpec.DataCase.PET_SPEC) {
            if (serverRequestExt != null && serverRequestExt.hasPetEvolution()) {
                builder.pet = serverRequestExt.petEvolution.pet
            } else {
                builder.pet = petFactory.create(instancedSpecExt)
            }
        }

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

    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 used in this example")
    }
}

class PetFactory(private val randomProxy: RandomProxy) {
    fun create(instancedSpecExt: ExtInstancedSpec): Pet {
        val bonusType = instancedSpecExt.petSpec.bonusesList.random()
        val minVal = instancedSpecExt.petSpec.bonusMin
        val maxVal = instancedSpecExt.petSpec.bonusMax

        return Pet.newBuilder()
            .setBonus(bonusType)
            .setBonusAmount(randomProxy.random(minVal, maxVal, Bound.INCLUSIVE))
            .build()
    }
}