Simple recipe analyzer for Factorio mods.
Find a file
2023-08-23 11:14:24 +02:00
mod-recipes Expanded scope, added second set of functionality and outlined process for useful output generation. 2023-08-23 11:14:24 +02:00
README.org Expanded scope, added second set of functionality and outlined process for useful output generation. 2023-08-23 11:14:24 +02:00

Factorio Recipe Analyzer

Intro

This is a simple Raku script which generates an analysis of each recipe into its component parts, and delivers insight into the balance and progression of a mod.

The .csv files used as input are generated in Factorio, by loading a new game with only base and your chosen mod enabled, and running the following Lua code:

/c
local whitelist = {}
for _, k in pairs({
    "iron-plate",
    "copper-plate",
    "steel-plate",
    "copper-wire",
    "iron-gear-wheel",
    "iron-stick",
    "pipe",
}) do whitelist[k] = true end

local parts = {}
for name, recipe in pairs(game.recipe_prototypes) do
  local history = script.get_prototype_history("recipe", name)
  if history.created == "base" then
    local add = false
    local ingredients = {}
    for _, ingredient in pairs(recipe.ingredients) do
      if whitelist[ingredient.name] then add = true end
      ingredients[#ingredients+1] = ingredient.amount .. "," .. ingredient.name
    end
    if add then
      local item = game.item_prototypes[name] or game.fluid_prototypes[name]
      parts[#parts+1] = "," .. name .. "," .. item.subgroup.name .. "," ..  table.concat(ingredients, ",")
    end
  end
end
game.write_file("recipes.csv", table.concat(parts, "\n"), false)

On the other hand, this script outputs ALL available recipes in an org-readable format, for easy overview, sorting, and insight.

/c
local recipes = {}
for name, recipe in pairs(game.recipe_prototypes) do
    local ingredients = {}
    for _, ingredient in pairs(recipe.ingredients) do
        ingredients[#ingredients+1] = ingredient.name .. "," .. ingredient.amount
    end
    local products = {}
    for _, product in pairs(recipe.products) do
        local amount = product.amount or product.amount_min .. "-" .. product.amount_max
        products[#products+1] = product.name .. "," .. amount
    end
    recipes[#recipes+1] = "\n* " .. recipe.subgroup.name .. "\n** " .. name .. "\n*** products" .. "\n- " .. table.concat(products, "\n- ") .. "\n*** ingredients" .. "\n- " ..  table.concat(ingredients, "\n- ")
end
game.write_file("recipes.org", table.concat(recipes, "\n"), false)

The output of this is an org-mode file in the following pattern:

* recipe.subgroup
** recipe.name
*** products
- product1
- product2
- ...
*** ingredients
- ingredient1
- ingredient2
- ...

This script

This is a literate script. The source code is embedded in these code blocks, and tangled into the Raku script using org-babel. This allows me to write a description of what I want to do, and comment on it without resorting to // /* */ ugly comments.

Better yet, I can include links directly to places within the file and easily cross-reference what I'm doing with the documentation/ design.

If you open this org file raw (e.g. by clicking here), you will see that there are a number of different blocks.

There is a rough overview of the structure:

#+name: Name of the following source block.
#+begin_src raku :tangle file.name
[code goes here]
#+end_src

:tangle defines the file into which the code block will be passed to org-babel-tangle. To disable, pass no.

In the begging of the file, we can set global (in the scope of the file) properties, such as the default target for :tangle:

#+PROPERTY: header-args :tangle fra.raku

The easiest way to generate outputs from here is to open this file in Emacs, and run M-x org-babel-tangle. If you're using Doom Emacs, the default key binding is C-c C-v C-t.

Important note: the files in ./mod-recipes are working files. They're not intrinsic parts of the software - they're there primarily for testing and development.

License

I don't own the source csv files generated by Factorio, nor the mods the script is pulling from. The Raku script itself is GPLv3.

TODO: include GPLv3 in the repo

Credits:

The following are mods from which .csv files have been generated. The recipe .csv files are included in the mod-recipes directory.

The Code1

Idea:

Create an org-mode file with all the calculations included, and complete, for a clear overview of recipe progression, subgroups, item inputs and outputs, and total cost.

Setup

Open csv file

Open (create if needed) output csv

pseudocode is in Lisp./

For this, we'll likely want to include some metadata, like creation date, mod name, number of ingredients, maybe the total amount of raw mats needed to make one of everything?

Make a list of products

That's the first column in the .csv file. Read first column of the csv file and insert it into the .org (output) file.

For each product, create a templated section

Ideally we'd end up with Something like:

* product
** direct inputs
- input and amount
- input and amount
** raw ingredients
+ raw ingredient and amount
+ raw ingredient and amount
Parse the csv file:

For each line:

  1. First column becomes the top header
  2. Insert second header
  3. Insert each ingredient and its amount as a separate item
Parse the output.org file, filling it out recursively
  1. Open .org (output file)
  2. Loop over output.org:

    1. Find product section.
    2. Find (next) ingredient lines in this product section.
    3. Pass the product and each direct input item and its number to raw-ingredients. (We can distinguish direct inputs from raw ingredients easily because org-mode supports multiple characters for defining lists. So we can just look for lines beginning with - and not really think about anything else.) This would look something like:
(raw-ingredients iron-gear-wheel iron-plate 2)
(raw-ingredients yellow-belt iron-plate 1)
  1. (raw-ingredients (product item number)):

    1. Store in variables for clarity:

      • product
      • item
      • number
    2. Read .csv file:

      1. If a recipe for this item exists in the csv file:

        1. Go to the line with the recipe (first column.
        2. For each (item number) pair, call (raw-ingredients (product new-item (* new-number old-number))).
    3. If a recipe does not exist:

      1. Find * product section in the .org file.
      2. If the ingredient item already exists:

        1. Add new number we just got to the existing number.
      3. Else: write new raw ingredient line and number in this section.

The Code 2

Idea/ outline

This is not for analysis as much as helping Galdoc out with creating compat layers for Galdoc's Manufacturing.

Take above csv data dump from Factorio, then:

  1. Figure out appropriate categories. This is the starting point we have:

    • Telescoping, (inserters, belts, things that reach)
    • Metalworking,
    • Plastic,
    • Wood,
    • Stone,
    • Glass,
    • Electronics.
    • And possibly in the future:

      • Motors,
      • Agriculture,
      • Chemicals,
      • Small Arms / Equipment.

This has to be done manually. The csv file already includes an empty first column, which lets us manually go over it and add the tags to each item.

  1. Then pull the output template CSV file, which should have the following structure:

    item-category, item-name, amt1, catitem1, amt2, catitem2, amt3, catitem3, amt4, catitem4, ...

In this example, we create an arbitrary number of columns, based on the largest number of ingredients a recipe requires from each category.

For example, stack inserters would look like so:

item-category, item-name, amt1, telescoping1, amt2, metal2, amt3, electronics3, amt4, electronics4
telescoping, stack-inserter, 1, fast-inserter, 15, iron-gear-wheel, 15, electronic-circuit, 1, advanced-circuit

This structure allows for unambiguous selection of the relevant data from the csv file, for the following reasons:

  1. Amounts and categories are paired by the matching number at the end of the column name.
  2. These cannot be confused with the amounts themselves, because the column names include [a-zA-Z] characters.
  3. The first column makes it easy to find whether an item belongs to a particular column or not.
  4. Additionally, this way we can automate creating the output .csv template, since we can check what number of columns we need for each ingredient category.