Building the Cooking Plugins #

In this section, we’ll build the plugins for cooking.


  1. Navigate to platform/5-ext/ext/src/main/kotlin/ in IntelliJ’s Project view.
  2. (if necessary) If the icon for the kotlin folder isn’t blue, right click it and select Mark directory as ➨ Sources root.
  3. Right click the kotlin folder and chose New ➨ Package. Name it cooking.
  4. In the cooking folder, choose New ➨ Kotlin class/file and name it CookingCrafter.kt. Replace the contents 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 incompleteFoodExt below.
package cooking

import pragma.inventory.*
import pragma.inventory.ext.ExtInstancedItem
import pragma.inventory.ext.ExtInstancedItemServerGrant
import pragma.inventory.ext.IncompleteFood
import pragma.inventory.ext.CookingCompleteRequirements
import pragma.inventory.ext.CookingCompleteSpec
import pragma.inventory.ext.CookingSpec
import pragma.utils.TimeProxy

class CookingCrafter(val timeProxy: TimeProxy) {

    fun meetsRequirements(
        cookingCompleteRequirements: CookingCompleteRequirements,
        destroyedInstancedItems: List<InstancedItem>,
    ): List<String> {
        val errors = mutableListOf<String>()
        try {
            val incompleteFood = destroyedInstancedItems.single()
            val incompleteFoodExt: ExtInstancedItem = incompleteFood.ext
            val completeTimeMillis = incompleteFoodExt.incompleteFood.timestampMillis

            if (timeProxy.currentEpochMillis() < completeTimeMillis) {
                errors.add("Food not cooked yet.")
            } else if (destroyedInstancedItems.single().catalogId != cookingCompleteRequirements.consumedCatalogId) {
                errors.add("Cooking Complete Requirement not met.")
            }
        } catch (e: RuntimeException) {
            errors.add("Not the correct number of IncompleteFoods were submitted for cooking.")
        }

        return errors
    }

    fun startCooking(cookingSpec: CookingSpec): CraftingPlugin.CraftResult {
        val catalogId = cookingSpec.catalogIdToCreate
        val readyTime = cookingSpec.cookTimeInSeconds * 1000 +
                timeProxy.currentEpochMillis()
        val incompleteFood = IncompleteFood.newBuilder().setTimestampMillis(readyTime)

        return CraftingPlugin.CraftResult(
            listOf(
                InstancedItemServerGrant(
                    ExtInstancedItemServerGrant.newBuilder()
                        .setIncompleteFood(incompleteFood)
                        .build(),
                    catalogId,
                    listOf()
                )
            ), listOf()
        )
    }

    fun completeCooking(
        cookingCompleteSpec: CookingCompleteSpec,
        inventoryContent: InventoryServiceContent
    ): CraftingPlugin.CraftResult {
        val instancedList = mutableListOf<InstancedItemServerGrant>()
        val stackableList = mutableListOf<StackableItemGrant>()
        if (inventoryContent.instancedSpecs.contains(cookingCompleteSpec.catalogIdToCreate)) {
            instancedList.add(
                InstancedItemServerGrant(ExtInstancedItemServerGrant.getDefaultInstance(),
                    cookingCompleteSpec.catalogIdToCreate,
                    listOf())
            )
        } else {
            stackableList.add(
                StackableItemGrant(
                    cookingCompleteSpec.catalogIdToCreate,
                    1,
                    listOf()
                )
            )
        }
        return CraftingPlugin.CraftResult(
            instancedList,
            stackableList
        )
    }
}
CookingCrafter method overview
  • meetsRequirements — called by Pragma Engine to check that the craft request is valid:
    • checks that the player has the required items in their inventory
    • checks that the incomplete food items can be redeemed
  • startCooking — called for the first step of cooking to cook ingredients into incomplete food
  • completeCooking — redeems incomplete food for the completed version
  1. Create another kotlin file in this directory named DemoCraftingPlugin.kt. Replace the contents 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.
package cooking

import 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.ExtCraftRequest
import pragma.inventory.ext.ExtCraftingEntry
import pragma.inventory.ext.ExtPurchaseRequirements
import pragma.playerprogression.ProgressionData
import pragma.services.Service
import pragma.utils.TimeProxy

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

    private val cookingCrafter = CookingCrafter(TimeProxy.defaultInstance)

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

        when (extCraftingRequirements.dataCase) {
            ExtPurchaseRequirements.DataCase.COOKING_COMPLETE_REQUIREMENTS -> {
                errors.addAll(
                    cookingCrafter.meetsRequirements(
                        extCraftingRequirements.cookingCompleteRequirements,
                        destroyedInstancedItems
                    )
                )
            }
            else -> {
                // this is an intentional no-op
            }
        }
        if (errors.isNotEmpty())
            return PragmaResultError(errors)
        return PragmaResultResponse(Unit)
    }

    override fun craft(
        craftingEntry: CraftingEntryWrapper,
        destroyedInstancedItems: List<InstancedItem>,
        inventoryContent: InventoryServiceContent,
        requestExt: ExtCraftRequest
    ): CraftingPlugin.CraftResult {
        val extCraftingEntry: ExtCraftingEntry = craftingEntry.ext

        return when (extCraftingEntry.dataCase) {
            ExtCraftingEntry.DataCase.COOKING_SPEC -> {
                cookingCrafter.startCooking(extCraftingEntry.cookingSpec)
            }
            ExtCraftingEntry.DataCase.COOKING_COMPLETE_SPEC -> {
                cookingCrafter.completeCooking(extCraftingEntry.cookingCompleteSpec, inventoryContent)
            }
            else -> {
                throw PragmaException(PragmaError.UNKNOWN_ERROR, "An unknown error occurred.")
            }
        }
    }
}

The DemoCraftingPluginclass sits between Pragma Engine and the CookingCrafter class.

Pragma Engine calls DemoCraftingPlugin’s methods, then DemoCraftingPlugin calls the correct CookingCrafter methods.

Note: While not demonstrated in this example, the DemoCraftingPlugin class is also necessary for games that contain multiple types of crafting which are implemented in different classes.

  1. Create another kotlin file in this directory named DemoInstancedItemPlugin.kt. Replace the contents with the following code:
package cooking

import pragma.inventory.*
import pragma.content.ContentDataNodeService
import pragma.inventory.ext.ExtInstancedItem
import pragma.inventory.ext.ExtInstancedItemUpdate
import pragma.inventory.ext.ExtInstancedItemServerGrant
import pragma.inventory.ext.ExtInstancedItemServerUpdate
import pragma.inventory.ext.ExtPurchaseRequest
import pragma.services.Service
import pragma.utils.RandomProxy

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

    private lateinit var randomProxy: RandomProxy

    override fun init(randomProxy: RandomProxy) {
        this.randomProxy = randomProxy
    }

    override fun newInstanced(
        instancedSpec: InventoryContent.InstancedSpec,
        clientRequestExt: ExtPurchaseRequest?,
        serverRequestExt: ExtInstancedItemServerGrant?
    ): ExtInstancedItem {
        if (clientRequestExt != null) {
            return newClientItem()
        }
        return newServerItem(serverRequestExt!!)
    }

    override fun update(
        initialInstancedItem: InstancedItem,
        instancedSpec: InventoryContent.InstancedSpec,
        updateEntry: InventoryContent.UpdateEntry,
        inventoryContent: InventoryServiceContent,
        clientRequestExt: ExtInstancedItemUpdate?,
        serverRequestExt: ExtInstancedItemServerUpdate?
    ): InstancedItemPlugin.UpdateResult {
        return InstancedItemPlugin.UpdateResult(
            ExtInstancedItem.getDefaultInstance(),
            emptyList()
        )
    }

    private fun newClientItem(): ExtInstancedItem {
        val builder = ExtInstancedItem.newBuilder()
        return builder.build()
    }

    private fun newServerItem(
        requestExt: ExtInstancedItemServerGrant
    ): ExtInstancedItem {
        val builder = ExtInstancedItem.newBuilder()
        if (requestExt.hasIncompleteFood()) {
            builder.incompleteFoodBuilder.timestampMillis = requestExt.incompleteFood.timestampMillis
        }
        return builder.build()
    }
}

In this example, we use DemoInstancedItemPlugin to create our incomplete muffin. Incomplete muffins are instanced items which use the ext field to store completion time requirements.

  1. Register your plugins by editing 5-ext/config/LocalConfig.yml. Ensure the game section of the config file matches the following:
game:
  core:
    clusterName: "local-game"
  pluginConfigs:
    InventoryService.craftingPlugin:
      class: "cooking.DemoCraftingPlugin"
    InventoryService.instancedItemPlugin:
      class: "cooking.DemoInstancedItemPlugin"
  1. Run make ext using a terminal from the platform directory.