rdck.dev

Factorio Factory Generator

2021-01-01

Automating the game about automation! How cool is that? :D

Making factories is hard. Especially calculating all those ratios and stuff. But I’m sure I can make a program that will make that instead of me!

Parsing Recipes

I started with stealing a recipes Lua file from the game and parsing it into a Json that I then parsed into a python dictionary that I then parsed into a recipes tree. :D

The Lua file with recipes can be conveniently found in the Wube Github repo with Lua prototypes here.

You can export Lua table into a json like this:

luna = require 'lunajson'

local data =
-- copy from factorio...

local json = luna.encode(data)

print(json)

Note: you need to install luarocks and install the json module yay -S luarocks && sudo luarocks install json.

Parsing the json into a recipe tree can be by something like this:

recipe = {}
for d in json.loads(open("recipe.json", "r").read()):
    if "ingredients" not in d.keys():
        d["ingredients"] = d["normal"]["ingredients"]
    recipe[d["name"]] = []
    for i in d["ingredients"]:
        if isinstance(i, dict):
            recipe[d["name"]] += [[i["name"], i["amount"]]]
        else:
            recipe[d["name"]] += [i]

You need to handle that sometimes recipe has 2 variants for normal and expensive mode and that liquids contain some extra informations that needs to be filtered out.

The resulting tree recipe is filled like this:

[
  [result, amount],
  [
    [ingredient1, ammount],
    [ingredient2, ammount],
    [...]
  ],
  ...
]

Then I of course needed to use my favorite graph rendering python library and look at rendered recipe trees for a while.

Locomotive:

locomotive recipe tree

Power armor MK2:

power-armor-mk2 recipe tree

Rocket silo:

rocket silo recipe tree

Now with parsing out of the way, let’s do the fun part :D

Blueprint planning

I came up with a smart way to recursively build a factory for any item.

Start like this:

step 1

Let’s call the upper horizontal belts the local bus. Note that the Assemblers can be unlimitedly expanded (If you don’t consider belt throughput).

Now, just recursively add Assemblers that output to the local bus:

step 2

step 3

Gears need only iron plates so just leave the input belts unconnected and let the player connect those to his main bus.

step 4

Same thing with pipes.

Last ingredient for engines is steel which I will also leave for the player and skip to the second ingredient of the locomotive

step 5

The final step are cable assemblers. If I then connect all the inputs to my main bus, everything starts working.

step 6

The ratios are of course totally wrong but I will keep this problem for later.

Now let’s somehow generate this.

I can print that recipe tree from the program. It looks like this:

- locomotive
  - engine-unit
    - iron-gear-wheel
    - pipe
  - electronic-circuit
    - copper-cable

This can be a little hard to see how to transform this into the factory that I build before. But watch this.

factory on a side

Now the recursion is quite clear. Let’s start with placing the assemblers. Each assembler is always 11 to the left and 3 down if it’s on lower level of the recursion.

Implementing this is easy.

x = 0
def buildBP(r, y=0):
    global x  # how left we are is global, no matter how deep down
    if r[0] in ignore: return
    if r[0] in recipes.keys():  # only if there is a recipe for this item
        x += 11  # move to the left
        placeAssemblerUnit(-x, y + 0, r[0])
        placeAssemblerUnit(-x, y + 3, r[0])
        placeAssemblerUnit(-x, y + 6, r[0])
        for i in recipes[r[0]]:  # loop over all ingredients for this recipe
            buildBP([i[0], 1], y + 3)

The result looks like this:

The stuff around each assebmler is static so adding those is done simply with some static offsets from the assembler

The next thing will be connecting these assemblers to the local bus.

By that I mean these belt up at the top.

This is also quite easy. The input line is static so nothing hard there but the output line on the right is actually not static. It’s length depends on which belt it outputs to the upper bus.

Simple variable n that tells the lower levels of recursion that they should connect to the n-th bus line does the trick.

Last thing is the bus itself. It is created as a extension from the lower level to the upper level. The upper level of recursion gives lower level its X coordinate and then when the lower lever is eventually evaluated, it compares its X coordinate with the parent X. This difference is how long the bus needs to go to connect there.

And thats it!

The bus lines that aren’t connected to any assemblers are not aligned like in the prototype that I build manually but I consider this a feature, not a bug. :D

The final recursion function looks like this:

def buildBP(r, y=0, n=0, px=0):
    global x  # how left we are is global, no matter how deep down
    myx = x
    if r[0] in recipes.keys():  # only if there is a recipe for this item
        x += 11  # move to the left
        placeBusLink(-x, y, n)  # place entities into the blueprint
        placeBusLine(-x, y, n, myx - px)
        placeAssemblerUnit(-x, y + 0, r[0])
        placeAssemblerUnit(-x, y + 3, r[0])
        placeAssemblerUnit(-x, y + 6, r[0])
        nn = 0
        for i in recipes[r[0]]:  # loop over all ingredients for this recipe
            if i[0] in ignore: continue
            buildBP([i[0], 1], y + 3, n=nn, px=myx)
            nn += 1

Ratios

How many assemblers are needed is calculated like this: craftTime * howManyPerSecINeed / assebmlerSpeed

How long it takes is again stored in the same Lua table. I can again just steal those values and save them in another Python dict rTimes.

The new recursive function now looks like this:

def buildBP(r, y=0, n=0, px=0):
    global x  # how left we are is global, no matter how deep down
    myx = x
    if r[0] in recipes.keys():  # only if there is a recipe for this item
        x += 11  # move to the left
        placeBusLink(-x, y, n)  # place entities into the blueprint
        placeBusLine(-x, y, n, myx - px)
        for i in range(ratioCalc(r[1], r[0])): # create assemblers
            placeAssemblerUnit(-x, y + i * 3, r[0])
        nn = 0
        for i in recipes[r[0]]:  # loop over all ingredients for this recipe
            if i[0] in ignore: continue
            buildBP([i[0], r[1] * i[1]], y + 3, n=nn, px=myx)
            nn += 1

The only difference now is that the correct amount of items per second required is now being send down. (that’s the [i[0], r[1] * i[1]] part. r[1] is how many items per second I need the current item, i[1] is how many of those ingredients I need for one current item.)

And thats it! Prototype is working :D

There are of course some limitations such as no liquids, max 3 ingredients, no mod support, no belt max throughput check, no modules support, no beacon support, doesn’t place substations, using only 1/2 of the bus belts and no custom assemblers…

But I do plan to fix those! :D

Or if I’m too slow for you, feel free to PR me new features on on GitHub.

Checkout the web tool here.

Articles from blogs I follow