Decomposed ingredients list (total raw)

Place to get help with not working mods / modding interface.
Post Reply
Pi-C
Smart Inserter
Smart Inserter
Posts: 1645
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Decomposed ingredients list (total raw)

Post by Pi-C »

If recipe.allow_decomposition is true, recipe ingredients are broken down as far as possible to "raw" ingredients during the game. I wonder if it's possible to get access to that list during the data stage.

The idea is to check all recipes if they make an item that contains wood, if the list of ingredients is completely broken down. It's relatively easy to check the list of ingredients of one recipe, but consider this example from Bio Industries:
  • The vanilla wooden chest is made from wood. Wood is the only ingredient, so that's easy to catch.
  • Our medium wooden chest requires no wood, but several vanilla wooden chests and additional ingredients.
  • Our large wooden chest requires no wood and no vanilla wooden chests, but several medium wooden chest + other ingredients.
  • We also have a giant wooden chest that, again, takes no wood but several large wooden chests + other ingredients.
I thought about assigning a fuel value to items that contain wood, or items that are made of wood, or items that are made from items that are made of wood ... However, it seems to be quite complicated to get a list of all items that may contain wood (and the total amount of wood) -- especially as the game builds such a list by itself anyway. Is there any shortcut I could use?

I'm not sure if my idea really makes sense because one item can be the result of several recipes, and it may well be possible that one recipe would require wood while another doesn't. Also, recipes may have difficulty, and it may be possible that recipe.normal.ingredients contains wood while recipe.expensive.ingredients doesn't, or that different amounts of wood are used in the different difficulties. I therefore allowed for the following simplification:
  • If at least one recipe producing the item requires wood, this item will get a fuel value.
  • Use the amount of wood used in recipe.normal.ingredients, or recipe.expensive.ingredients, or recipe.ingredients (in this order).
  • If several recipes containing wood as ingredient produce an item, use the one with the highest amount of wood as reference for calculating the fuel value.
Any opinions on this?
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!

User avatar
DaveMcW
Smart Inserter
Smart Inserter
Posts: 3700
Joined: Tue May 13, 2014 11:06 am
Contact:

Re: Decomposed ingredients list (total raw)

Post by DaveMcW »

Pi-C wrote:
Fri Apr 02, 2021 1:20 pm
However, it seems to be quite complicated to get a list of all items that may contain wood (and the total amount of wood) -- especially as the game builds such a list by itself anyway. Is there any shortcut I could use?
No, the only way is to build the list yourself. I am not impressed with the game engine's calculation anyway, since it uses allow_decomposition=false as a crutch.

Pi-C
Smart Inserter
Smart Inserter
Posts: 1645
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Decomposed ingredients list (total raw)

Post by Pi-C »

DaveMcW wrote:
Fri Apr 02, 2021 2:14 pm
Pi-C wrote:
Fri Apr 02, 2021 1:20 pm
However, it seems to be quite complicated to get a list of all items that may contain wood (and the total amount of wood) -- especially as the game builds such a list by itself anyway. Is there any shortcut I could use?
No, the only way is to build the list yourself.
I'm trying to do that now! Not really easy because debugging gets quite hard with the amount of data involved (checking for just the items that contain wood is easy, but these items become potential fuel items themselves, so there's more data with every pass I need to make). Yet, I still hope to get it working in a somewhat elegant way.

Anyway, have a nice Easter weekend, and thanks for your help!
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!

pleegwat
Filter Inserter
Filter Inserter
Posts: 258
Joined: Fri May 19, 2017 7:31 pm
Contact:

Re: Decomposed ingredients list (total raw)

Post by pleegwat »

Pi-C wrote:
Sun Apr 04, 2021 10:08 am
DaveMcW wrote:
Fri Apr 02, 2021 2:14 pm
Pi-C wrote:
Fri Apr 02, 2021 1:20 pm
However, it seems to be quite complicated to get a list of all items that may contain wood (and the total amount of wood) -- especially as the game builds such a list by itself anyway. Is there any shortcut I could use?
No, the only way is to build the list yourself.
I'm trying to do that now! Not really easy because debugging gets quite hard with the amount of data involved (checking for just the items that contain wood is easy, but these items become potential fuel items themselves, so there's more data with every pass I need to make). Yet, I still hope to get it working in a somewhat elegant way.

Anyway, have a nice Easter weekend, and thanks for your help!
You shouldn't need to test any item more than once if you do a depth first search. Simply go over the list once, for each ingredient first check if you calculated it yet, if you have not calculate the value of the ingredient first, repeating. Keep track of which items are being actively evaluated to prevent loops.

Pi-C
Smart Inserter
Smart Inserter
Posts: 1645
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Decomposed ingredients list (total raw)

Post by Pi-C »

pleegwat wrote:
Sun Apr 04, 2021 12:42 pm
You shouldn't need to test any item more than once if you do a depth first search. Simply go over the list once, for each ingredient first check if you calculated it yet, if you have not calculate the value of the ingredient first, repeating.
I won't know in the beginning what ingredients will have a fuel value. There will be cases like this: I check a recipe that requires the "wooden-chest" item before I know that "wooden-chest" is made of wood and thus, data.raw.item["wooden-chest"].fuel_value = data.raw.item["wood"].fuel_value * amount of wood. The wooden chest is easy because it only has one ingredient. But mods could change it to require something else which also will eventually have a fuel value. So several passes are needed at some point.
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!

pleegwat
Filter Inserter
Filter Inserter
Posts: 258
Joined: Fri May 19, 2017 7:31 pm
Contact:

Re: Decomposed ingredients list (total raw)

Post by pleegwat »

The idea is you store that you haven't processed the wooden chest yet, so you recursively determine the fuel value of a wooden chest before returning to whatever you were doing.

Pi-C
Smart Inserter
Smart Inserter
Posts: 1645
Joined: Sun Oct 14, 2018 8:13 am
Contact:

Re: Decomposed ingredients list (total raw)

Post by Pi-C »

OK, took me more time than expected, but I've got a working solution now:
  • Define the reference fuel: base_fuel = {name = "wood", type = "item", fuel_value = data.raw.item.wood.fuel_value or "2MJ"}.
  • Initialize dictionaries fuel_items and fuel_values with data of base_fuel:

    Code: Select all

    local fuel_items = { [base_fuel.name] = {type = base_fuel.type} }
    local fuel_values = { [base_fuel.name] = {fuel_value = base_fuel.fuel_value, type = base_fuel.type} }
    
  • Create a set of filters: blacklist_items, whitelist_items (dictionaries of known items for quick look-up) and blacklist_patterns, whitelist_patterns (arrays of strings that I can use with string.match). There's a default set with names/patterns for items from BI and vanilla. If mods/mod suites like Angel's, Bob's, IR2, Krastorio, Py etc. are active, customized filters for these mods will be loaded. If the same pattern is used by more than one mod, only one instance of it will make it into the final list.
  • Make a list of potential_fuel_items. Anything from data.raw[x] (where x is any of "ammo", "armor", "capsule", "gun", "item", "item-with-entity-data", "mining-tool", "rail-planner", "tool") that is either whitelisted or not blacklisted will be added:

    Code: Select all

    potential_fuel_items[item_name] = { type = item_type,  made_by = {} }
  • For each recipe, get results (recipe.normal.results, or recipe.expensive.results, or recipe.results -- in that order; if a recipe doesn't have "results", make it from "result"). There may be recipes that have neither result nor results (e.g. the incineration recipes from IR2 that allow you to get rid of excess items); such recipes are skipped. "results" has this format:

    Code: Select all

    {
              name = result.name,
              type = result.type or "item",
              amount = result.amount,
              amount_per_sec = result.amount / energy_required
    }
    
  • If a recipe has results, get the ingredients in this format:

    Code: Select all

    { ingredient.name = {name = ingredient.name, type = ingredient.type or "item",  amount = ingredient.amount}, … }
    If any ingredients are in potential_fuel_items or fuel_items, they will be added to a temporary list:

    Code: Select all

    { ingredient.name = ingredients[ingredient.name], }
    If this list isn't empty after all ingredients have been checked, a new entry will be initialized in fuel_items (if the ingredient already has fuel_value set), and the recipe will be added to the items data in potential_fuel_items:

    Code: Select all

    made_by[recipe.name] = {
                result = results,
                ingredients = temporary list
              }
    
  • After all recipes have been checked, remove all potential_fuel_items where made_by is empty. Also remove such items from the ingredients list for all recipe data stored in "made_by" of all other potential_fuel_items and start over again until table_size(potential_fuel_items) doesn't change anymore. At that point, potential_fuel_items will only contain items that have data of recipes to make them.
  • So far, fuel_items only contains the data of base_fuel and placeholders for all other items. Now, the lists are merged by moving all data from potential_fuel_items to fuel_items because it's less hassle to work with just one table.
  • A fuel_item may be made by several recipes, but only one recipe can be considered when determining the fuel_value of an item. So, for each item only the recipe with the lowest result.amount_per_second will be kept, all other recipes will be discarded.
  • Determine fuel values: Check all ingredients of the recipe for all fuel_items. If all ingredients have a known fuel_value, remove the item from fuel_items and add it to fuel_values in this format:

    Code: Select all

    fuel_values[item.name] = {fuel_value = item.fuel_value, type = item.type}
    Otherwise, set a flag requesting a re-run and proceed. (There could be fuel_value missing for one of the ingredients of the first item which is available after the second item has been processed.) Repeat until there are no fuel_items left or table_size(fuel_items) doesn't change anymore.
  • If there still are fuel_items, there was no way to calculate their fuel_values (fuel_value[ingredient_1] * ingredient_1.amount + … ). This means that at least one of the fuel_items is used recursively (e.g. electronic-circuit made by disassembling a longhanded-inserter requiring an inserter made with electronic-circuit). Such recursively used items are removed from fuel_items and the ingredients of all other fuel_items. If this happened to be the last ingredient for an item, that item will also be removed from fuel_items and the other fuel_items' ingredients, … After that, start over calculating fuel_values for the remaining items. (Actually, this code is run immediately before the exit condition for "Determine fuel values" is checked. Thus, if an item was used recursively, it would be removed and table_size(fuel_items) would have changed, so it would be just a normal re-run requested by the flag.)
  • Finally, for each item in fuel_values set

    Code: Select all

    data.raw[item.type][item.name] = fuel_values[item.name].fuel_value
    unless it was already set.
Sorry for the wall of text, but it was actually useful! While writing this, I noticed something that I could improve:
A fuel_item may be made by several recipes, but only one recipe can be considered when determining the fuel_value of an item. So, for each item only the recipe with the lowest result.amount_per_second will be kept, all other recipes will be discarded.
Discarding the recipes isn't necessary. It's better to keep them all, but only use the recipe with the lowest result.amount_per_second. If it is removed because it contained a recursively used item, discard that recipe and try the next one; if no recipes are left, discard the item from fuel_items.
A good mod deserves a good changelog. Here's a tutorial (WIP) about Factorio's way too strict changelog syntax!

Post Reply

Return to “Modding help”