Modding:Meal Container

From Vintage Story Wiki
Revision as of 20:22, 2 August 2024 by Bluelightning32 (talk | contribs) (Add source code links for some of the block classes)

Semantically, a meal is an array of food ingredient item stacks it was cooked with. Each item stack in the meal must have the same size, and this becomes the initial serving size of the meal. For example, cooking 1 raw meat in slot 0 and slot 1 of a claypot-burned creates a meal. That meal has a 1x stack of raw meat in slot 0 and a 1x stack of raw meat in slot 1. Slots 2 and 3 are empty.

However, in the VS implemenation, meals are tightly coupled with their containers. Meals never exist independently of their container, neither as collectibles (items/blocks), nor as C# objects. In the implementation, transferring a meal between containers actually involves creating a new container block and then transferring over the ingredients from the source container. Furthermore, meals and meal containers barely use behaviors, with almost all of the logic implemented in Block classes. This limits how much one can customize new meal containers in a mod, without having to resort to coding and subclassing the existing block classes.

This tight coupling of meals and containers contrasts with liquids. Liquids have liquid portion items that are independent of their containers, although the liquid portions are rarely shown in the game. Having a separate liquid portion items creates a clean division between the liquid logic and the liquid container logic, and makes it easier to add new liquid containers.

Viewing cooked meal ingredients

It is possible to demonstrate that the meal data is simply the ingredients that it was made with. All of the cooked meal containers inherit from BlockContainer in some way. While a BlockContainer is in an inventory (as oppsed to existing as a block on the ground outside of a ground storage), its inventory is stored in the contents attribute in the item stack. In creative mode, these attributes can be viewed with .gencraftjson command. Note that the item stack attributes are distinct from the attributes defined in the block's json file. Every block of a given type shares the block attributes, but the item stack attributes can be different for every stack of that block.

For example, this is the output from an apple pie. Notice how it contains the 4 applies it was made with as well as the spelt dough.

{
  type: "Block",
  code: "game:pie-perfect",
  attributes: {
    "pieSize": 4,
    "topCrustType": 1,
    "bakeLevel": 2,
    "contents": {
      "0": { "type": "item", "code": "dough-spelt", "attributes": { "transitionstate": {  } }},
      "1": { "type": "item", "code": "fruit-redapple", "attributes": { "transitionstate": {  } }},
      "2": { "type": "item", "code": "fruit-redapple", "attributes": { "transitionstate": {  } }},
      "3": { "type": "item", "code": "fruit-redapple", "attributes": { "transitionstate": {  } }},
      "4": { "type": "item", "code": "fruit-redapple", "attributes": { "transitionstate": {  } }},
      "5": { "type": "item", "code": "dough-spelt", "attributes": { "transitionstate": {  } }}
    }
  }
}

As another example, here is a bowl-meal holding a "Red meat stew with boiled cranberries."

{
  type: "Block",
  code: "game:bowl-meal",
  attributes: {
    "contents": {
      "0": {
        "type": "item",
        "code": "redmeat-raw",
        "attributes": { "transitionstate": { "createdTotalHours": 883.6644722075278, "lastUpdatedTotalHours": 904.0225999776667, "freshHours": [144], "transitionHours": [36], "transitionedHours": [13.331751] }, "temperature": { "temperatureLastUpdate": 898.3115222173333, "temperature": 0 } }
      },
      "1": {
        "type": "item",
        "code": "redmeat-raw",
        "attributes": { "transitionstate": { "createdTotalHours": 883.6644722075278, "lastUpdatedTotalHours": 904.0225999776667, "freshHours": [144], "transitionHours": [36], "transitionedHours": [13.331751] }, "temperature": { "temperatureLastUpdate": 898.3115222173333, "temperature": 0 } }
      },
      "2": {
        "type": "item",
        "code": "fruit-cranberry",
        "attributes": { "transitionstate": { "createdTotalHours": 883.6644722075278, "lastUpdatedTotalHours": 904.0225999776667, "freshHours": [144], "transitionHours": [36], "transitionedHours": [13.331751] }, "temperature": { "temperatureLastUpdate": 898.3115222173333, "temperature": 0 } }
      }
    },
    "recipeCode": "meatystew",
    "quantityServings": 1
  }
}

Meal containers blocks

Most of the cooking containers have a raw form, empty container form (sometimes called a cooking block in the game), and container with meal form (sometimes called a cooked block in the game).

Raw Empty container Container with meal
bowl-raw bowl-fired bowl-meal
crock-raw-* crock-burned-* crock-burned-*
claypot-raw claypot-burned claypot-cooked
- dirtyclaypot-empty dirtyclaypot-cooked
pie-raw - pie-partbaked, pie-perfect, pie-charred

All of the meal containers are implemented as blocks. Many of the contains have comments like this in their json file, stating that they are only blocks for legacy reasons:

{ name: "Unplaceable", "__comment": "The ground storable obsoletes this being a block. Should be an item, but is kept a block for backwards compatibility" }

The defining characteristic of a block, relative to an item, is that blocks can be placed in the world. However, around version 1.16, the Unplaceable behavior and GroundStorable behavior were added to all of the meal containers (except for pie-* which is still directly placed on the ground). Now when the block is right clicked onto the ground, instead of directly setting the block at that location to the meal container's id, a groundstorage block is created that contains the block in inventory form. The meal containers may still exist as placed blocks in the world, if they were placed before version 1.16, and the world was upgraded.

Despite the containers being blocks for legacy reasons, any new meal containers introduced by mods must be blocks, because BlockCookedContainerBase requires the serving target to be a block. Specifically some methods in BlockCookedContainerBase directly look at the Itemstack.Block field on the serving target instead of also checking Itemstack.Item or using Itemstack.Collectible.

Regardless, the block entities of the meal containers can largely be ignored, because the meal containers typically only exist in item stacks (which do not have block entities). A block entity is only created if the block is directly placed in the world (storing it in a container like ground storage doesn't count). So the only meal container block entities are pies, and legacy block entities from pre 1.16 worlds.

Meal container block classses

This is the class hierachy for the meal container block classes.

Block
BlockContainer
BlockMeal
Features
  • Right click to eat while holding in the hotbar.
  • After eating, it is replaced with the block described in the eatenBlock attribute, or destroyed if the attribute is unset.
Used by
  • bowl-meal
BlockPie
Features
  • Uses the pieSize item stack attribute to set the number of slices remaining
  • Allows baking raw pies in an oven
  • Reduces the spoilage rate of the raw ingredients
  • Handles rendering full pies and sliced pies by calling MealMeshCache.GetPieMesh
  • Prevents eating raw pies and pies with more than a single slice
  • Reduces the nutrition of partbaked and charred pies based on the nutritionMul attribute.
Used by
  • pie-raw
  • pie-partbaked
  • pie-perfect
  • pie-charred
BlockCookedContainerBase
Features
  • Supports serving from this block (serving source) on the ground into empty meal containers. Specifically, the cooked container intercepts the right click event while it is inside of a groundstorage block. If the current item in the hotbar is an empty meal container, then the contents are served from the container on the ground into the container in the hotbar.
BlockCookedContainer
Features
  • Handles serving from this block in the hotbar into empty meal containers.
Used by
  • dirtyclaypot-cooked
  • claypot-cooked
BlockCrock
Features
  • Reduces the spoilage rate by overriding GetContainingTransitionModifierContained
Used by
  • crock-burned-*
BlockCookingContainer
Features
  • Allows cooking meals in a firepit, if the ingredients match a cooking recipe
Used by
  • claypot-burned

Serving between containers

Serving means transferring a meal from a source container to an empty target meal container. In order for the target to be recognized as an empty meal container, it must have the mealContainer=true block attribute. Also the mealBlockCode attribute must be set to a valid block code. The non-empty meal container must implement IBlockMealContainer, because that interface is used to fill the meal container.

BlockCookedContainerBase has the logic to allow it to be the serving source, when it is inside of a groundstorage block, and the serving target is the active block in the hotbar. BlockCookedContainer has the logic to allow it to be the serving source, when it is active in the hotbar, and the serving target is on the ground.

So for example, when holding a bowl-fired (serving target), it can serve from a claypot-cooked (serving source) on the ground, because the claypot-cooked's class derives from BlockCookedContainerBase, and the bowl-fired has mealContainer=true and mealBlockCode set.

Similarly, when holding a claypot-cooked (serving source), it can serve into a bowl-fired (serving target) on the ground, because the claypot-cooked's class is BlockCookedContainer.

Nothing can serve from a bowl-meal, because the bowl-meal's class, BlockMeal, does not inherit from BlockCookedContainerBase. Likewise, nothing can serve from a pie, because its class also does not inherit from BlockCookedContainerBase.

Meal nutrition gain

Meals have a nutrition boost comapred to eating their raw ingredients. This nutrition boost comes from the difference between the nutritionProps and nutritionPropsWhenInMeal fields for all of the ingredients. So the exact boost depends on each ingredient. Although all of the ingredients in the base game get a boost from putting them in the meal, it would be possible to make an ingredient that gets a debuff from putting it in a meal.

For example, the nutrition of red meat is boosted by 140, because redmeat-cooked sets nutritionProps to { satiety: 280, health: 0, foodcategory: "Protein" } and the nutritionPropsWhenInMeal to { satiety: 420, foodcategory: "Protein" }.

Cooking recipes

BlockCookingContainer determines what meals can be cooked by searching for matching cooking recipes in the recipes/cooking asset folder. Each of recipes set a minimum number of some ingredients necessary to engage the recipe. For example, meatystew requires 2 slots with redmeat. The recipes then list other ingredients that may be optionally added to the other cooking slots. Here is part of meatystew.json:

{
  code: "meatystew",
...
  ingredients: [
      { 
        code: "protein-base", 
        validStacks: [ 
          { type: "item", code: "redmeat-raw",  shapeElement: "bowl/meat stew/*" },
          { type: "item", code: "redmeat-cured",  shapeElement: "bowl/meat stew/*" },
          { type: "item", code: "fish-raw",  shapeElement: "bowl/meat stew/*" },
          { type: "item", code: "fish-cured",  shapeElement: "bowl/meat stew/*" },
          { type: "item", code: "poultry-raw",  shapeElement: "bowl/meat stew/*" },
          { type: "item", code: "poultry-cured",  shapeElement: "bowl/meat stew/*" }
        ],
        minQuantity: 2,
        maxQuantity: 2
      },
      { 
        code: "egg-extra", 
        validStacks: [ 
          { type: "item", code: "egg-chicken-raw",  shapeElement: "bowl/egg/*" },
        ],
        minQuantity: 0,
        maxQuantity: 1
      },
...