Crafting Items #

In the previous section, we defined the store, crafting, and item catalogs. We also created protos to define the structure of our pets and pet evolutions. In this section, we will author plugins to evolve our pets via Pragma Engine’s crafting system.


Create a Crafting Plugin #

  1. Create a Kotlin file at 5-ext/ext/src/main/kotlin/PetCraftingPlugin.kt.
  2. In PetCraftingPlugin.kt, define a class that implements the CraftingPlugin craft() and meetsRequirements() interfaces.
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 PetCraftingPlugin(
    val service: Service,
    val contentDataNodeService: ContentDataNodeService,
) : CraftingPlugin {

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

    override fun craft(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        inventoryContent: InventoryServiceContent,
        requestExt: ExtCraftRequest
    ): CraftingPlugin.CraftResult {
        TODO("Not yet implemented")
    }
}
  1. Replace the meetsRequirements() stub with the following code:
Due to known issues with IntelliJ performing type inferences on protos, using a value nested in an ext field can cause false syntax error. Create a local variable with a manually assigned type to hold proto ext fields, as we’ve done with extCraftingRequirements below.
override fun meetsRequirements(
    craftingEntry: CraftingEntryWrapper,
    destroyedInstancedItems: List<InstancedItem>,
    playerInventory: InventoryData,
    ext: 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 (
        ext.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.

a. In an actual game meetsRequirements() might need to check the available properties of the craftingEntry.requirements.ext to determine proper behavior as there are potentially different forms of crafting. In this example we are only evolving pets, so this check is not necessary.

b. 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 is of the correct catalogId.

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

d. 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:
override fun craft(
    craftingEntry: CraftingEntryWrapper,
    destroyedInstancedItems: List<InstancedItem>,
    playerInventory: InventoryData,
    inventoryContent: InventoryServiceContent,
    requestExt: ExtCraftRequest
): CraftingPlugin.CraftResult {
    val extCraftingEntry: ExtCraftingEntry = craftingEntry.ext
    if (extCraftingEntry.hasPetEvolutionSpec()) {
        return petEvolution(
            extCraftingEntry.petEvolutionSpec,
            destroyedInstancedItems,
            requestExt.petEvolutionRequest
        )
    }
    throw PragmaException(PragmaError.UNKNOWN_ERROR, "An unknown error occurred.")
}
  1. Implement the petEvolution() method:
private fun petEvolution(
    petEvolutionSpec: PetEvolutionSpec,
    destroyedInstancedItems: List<InstancedItem>,
    request: PetEvolutionRequest
): CraftingPlugin.CraftResult {
    val catalogId = petEvolutionSpec.targetPetCatalogId
    val instancedItemGrants = mutableListOf<InstancedItemServerGrant>()
    val ext: ExtInstancedItem = destroyedInstancedItems.single().ext
    val pet =
        ext.pet
            .toBuilder()
            .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 CraftingPlugin.CraftResult(instancedItemGrants, stackableItemGrants)
}
Method overview

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

a. craft() checks the available properties on the extCraftingEntry to determine the proper 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.

b. The craft() function then performs the requested changes via the petEvolution helper method. 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 CraftResult. In this example, only InstancedItemServerGrant objects will be returned.

c. It then returns the CraftingPlugin.CraftResult with a generated list of the instancedItemGrants and the stackableItemGrants.

Create an Instanced Item Plugin #

  1. Create a Kotlin file at 5-ext/ext/src/main/kotlin/DemoInstancedItemPlugin.kt.

  2. Define a class that implements the InstancedItemPlugin newInstanced interface.

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.Pet
import pragma.services.Service
import pragma.utils.Bound
import pragma.utils.RandomProxy

class DemoInstancedItemPlugin(
    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 {
        TODO("Not yet implemented")
    }

    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()
    }
}

Note the presence of a second class PetFactory in this file. Also note that we will not be using the update() method in this example.

  1. Implement the newInstanced() method:
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.hasPetSpec()) {
        builder.pet = petFactory.create(instancedSpecExt)
    }

    if (serverRequestExt != null) {
        if (serverRequestExt.hasPetEvolution()) {
            builder.pet = serverRequestExt.petEvolution.pet
        }
    }
    return InstancedItemPlugin.InstancedItemPluginResult(builder.build())
}

newInstanced() takes the ExtInstancedItemServerGrant is used when the instanced items granted from the crafting request provide the data that populates their ExtInstancedItem and persists with them in the database.

As this plugin has both the Service and the ContentDataNodeService, it should be able to make internal RPC requests and reference other content known to Pragma Engine.

Craft your items #

  1. Register your plugins by editing local-dev.yml.
game:
  pluginConfigs:
    InventoryService.craftingPlugin:
      class: "pragma.inventory.PetCraftingPlugin"
    InventoryService.instancedItemPlugin:
      class: "pragma.inventory.DemoInstancedItemPlugin"
  1. Run make ext using a terminal from the platform directory.
  2. 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.

  1. Open Postman, then send PragmaDev ➨ Public ➨ GetInQueuev1 to enter the login queue.
  2. Send PragmaDev ➨ Public ➨ Player - AuthenticateOrCreateV2 to log into Pragma Engine as a player.
  3. Grant a pet fireDragon and the necessary crafting materials by running PragmaDev ➨ Game ➨ RPC - Player ➨ Inventory ➨ storePurchaseV4 with the following body:
{
    "requestId": 1,
    "type": "InventoryRpc.StorePurchaseV4Request",
    "payload": {
        "data" : {
            "ext": {},
            "storeId": "materialShop",
            "storeEntryId": "craftingKit",
            "amount": 1
        }
    }
}

The player should receive a fireDragon, 2000 gold, 2000 fireShards, and 2 fireGems. This was specified in Stores.json. The fireDragon should have one of the following attributes with a value between 5 to 10: XP, Gold, or Luck. This was specified in InstancedSpecs.json.

  1. Evolve your fireDragon by running PragmaDev ➨ Game ➨ RPC - Player ➨ Inventory ➨ Crafting Scenarios ➨ craftV1 with the following body:
{
  "requestId": 1,
  "type": "InventoryRpc.CraftV1Request",
  "payload": {
    "craftRequest": {
      "ext": {
        "petEvolutionRequest": {
          "craftingLocation": "fireLake"
        }
      },
      "craftingEntryId": "fireDragon2Evolution",
      "itemsToDestroy": [
        "{{craftableItemId}}"
      ]
    }
  }
}
  1. Confirm that the previously granted gold, fireShards, fireGems, and fireDragon were removed from the player inventory. They should have been exchanged for a fireDragon2 with the same bonus type as the fireDragon increased by 2.

Appendix #

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

Click here to view the completed PetCraftingPlugin.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 PetCraftingPlugin(
    val service: Service,
    val contentDataNodeService: ContentDataNodeService,
) : CraftingPlugin {

    override fun meetsRequirements(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        playerInventory: InventoryData,
        ext: 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 (
            ext.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
    ): CraftingPlugin.CraftResult {
        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
    ): CraftingPlugin.CraftResult {
        val catalogId = petEvolutionSpec.targetPetCatalogId
        val instancedItemGrants =
            mutableListOf<InstancedItemServerGrant>()
        val ext: ExtInstancedItem =
            destroyedInstancedItems.single().ext
        val pet =
            ext.pet
                .toBuilder()
                .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 CraftingPlugin.CraftResult(
            instancedItemGrants,
            stackableItemGrants
        )
    }
}
Click here to view the completed DemoInstancedItemPlugin.kt file
package pragma.inventory

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

class DemoInstancedItemPlugin(
    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.hasPetSpec()) {
            builder.pet = petFactory.create(instancedSpecExt)
        }

        if (serverRequestExt != null) {
            if (serverRequestExt.hasPetEvolution()) {
                builder.pet = serverRequestExt.petEvolution.pet
            }
        }
        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()
    }
}