I play Factorio.

If you don’t know what Factorio is, it’s a factory simulation game with lots of depth and really tickles the engineering side of my brain - but with visuals. You start with raw materials which you smelt and make basic components, and then you move them around and make more advanced components, and eventually you can launch a rocket, research more technologies, cleanse the planet, etc.

The power fantasy is a tale of automation: As you progress, you get faster, more capable machines and robots to place things for you, and that’s where blueprints come in.

A blueprint is essentially a way to save a section of your factory and allow you to to paste it elsewhere as many times as you want. This, as you can imagine, allows you to scale up much more than before. You can put down a blueprint, grab some water, and come back to see it completed. The scale of the game has been fundamentally changed.

I was playing the other day when I realized I wanted to re-organize my base to make it more modular and to scale up production. I wanted to achieve this with a train base, basically designing it around a standardized train stop design with inputs and outputs. For example, instead of directly linking the mines to the smelters (which would become useless once the mines run out), I could have a smelting stop and if I wanted to connect more resources to the base I could just set up another train stop at a different mine. Just a little bit of Plug n’ Play.

Such a design has great theoretical upsides in-game but a great downside in real-life: it would be time-consuming to design all the stops for all of the recipes. For each “Block”, I would have to manually configure (# of inputs + # of outputs) train stations. The configuration includes setting the name of each stop (which is used as an identifier), the resource to request/provide, and the to request/provide. Furthermore, I could only specify the amount as a raw amount, but storage in this game only cares about the stack size; i.e. how much you can fit in one inventory slot. For example, if iron ore has a stack size of 100, and my storage has 32 slots, I could hold 3200 iron. While not difficult to compute, I had to make sure that my storage would not become full - otherwise the train would be unable toBut surely I could automate this, I thought to myself.

I would simply have to make a few standardized blueprints and scrape the recipe information out of the game. Then I could make stations with the right requests (e.g. this station wants 10000 units of iron plates), slap it on the standardized train stop design, import it in-game and bam done.

My first plan of attack was using Draftsman, a python library designed for interacting with blueprints. I was especially hooked by its idea of groups; essentially allowing you to place blueprints within other blueprints, something not supported in-game. However, I couldn’t get the library to work because I have an extensive mod list that seem to have a few key mods that the library couldn’t port. I did try and ended up committing some time to a shallow dive of lua and how the library emulates Factorio’s mod-loading process, but I thought it would be simpler to write a quick script for my use case - how bad could it be?

Well not that bad, but it was a little annoying. The general procedure is:

  1. Design blueprints
    1. Standard Modular Block blueprint (photo)
    2. Train Rail Stop that could be placed within the Standard Modular Block. This is because the number of inputs and outputs will vary depending on the recipe (photo)
    3. Provider/Requester Stations. These are the actual Train Stations that will be placed on the Train Rail Stops (photo)
  2. Get the ingredients and results for a given recipe.
  3. Create a structure that represents a virtual station which has the information for the train stations.
  4. Take the Provider/Requester Stations blueprints and convert those virtual stations into actual stations.
  5. Take those actual stations and place them on Train Rail Stops.
  6. Take the rails with the stations and stick ‘em on the Standard Modular Block blueprint.
  7. Convert that JSON back into a Factorio string and import it.
  8. (Optional) Bask in your greatness
  9. (Optional) Wonder if this was worth the time

For the recipe data, I found an in-game script that dumped the state and I ran the game so that it would be piped to a log. After a little bit of regex action, the extraction was done.

One core operation we must perform is combining blueprints. This is the heart of steps 4 and 5. As it turns out, blueprints are represented as JSON. In python, it’s as simple as loading the JSON and just combining the dictionaries. “Simple”… There are two pitfalls: 1. Entity Number. It’s essentially an ID for an entity in the blueprint that starts at 1 for each blueprint. This is a problem because, for example, if we had entities that were connected (like power poles), that connection is based off entity number. If you were to simply copy the entities, then there would be multiple of the same entity number. (Which surprisingly does not make the game complain! I didn’t even notice until some wires weren’t properly connected.) 2. Positions. Each entity has a pair of (x, y) coordinates that are relative for each blueprint. If you were to simply combine the blueprints, they would not automatically have any logical position relative to each other, usually they would overlap.

Entity numbers were simple to fix: just take the highest entity number from one of the blueprints, add 1 to it, and add that number to all of the entity numbers of the other blueprint. Below is the snippet (The quick and dirty 6-indent deep loop is for entity numbers in the properties of other entities)

def find_largest_entity_number(entities):
    largest = 0
    for entity in entities:
        number = entity['entity_number']
        if number > largest:
            largest = number
    
    return largest
 
def replace_entity_numbers(entities, start):
    current_number = start
    replacement_map = {}
    
    for i, entity in enumerate(entities):
        replacement_map[entities[i]['entity_number']] = current_number
        current_number += 1
        
    for i, entity in enumerate(entities):
        entities[i]['entity_number'] = replacement_map[entities[i]['entity_number']]
        if 'connections' in entity:
            for connection_point, connection_point_dict in entity['connections'].items():
                for wire_color, connection_data in connection_point_dict.items():
                    for connection_data_dict in connection_data:
                        connection_data_dict['entity_id'] = replacement_map[connection_data_dict['entity_id']]
                
    return entities

Positions were a little thorny. I ended up with a function that takes a blueprint, a target set of coordinates, the alignment (top-left, bottom-right, etc.) and arbitrary x, y shifts in case i needed to empirically adjust things.

def position_adjust(bp_to_be_adjusted, target_location, align, x_shift=0, y_shift=0):
    assert align in ['top-left', 'top-right', 'bottom-left', 'bottom-right']
    
    new_bp = copy.deepcopy(bp_to_be_adjusted)
    
    top_left, top_right, bottom_left, bottom_right = get_rectangular_bounds(new_bp)
    if align == 'top-left':
        x_move, y_move = target_location[0] - top_left[0], target_location[1] - top_left[1]
    elif align == 'top-right':
        x_move, y_move = target_location[0] - top_right[0], target_location[1] - top_right[1]
    elif align == 'bottom-left':
        x_move, y_move = target_location[0] - bottom_left[0], target_location[1] - bottom_left[1]
    elif align == 'bottom-right':
        x_move, y_move = target_location[0] - bottom_right[0], target_location[1] - bottom_right[1]
        
    for i, entity in enumerate(new_bp['blueprint']['entities']):
        if entity['name'] == 'se-space-curved-rail':
            addt_x_shift = 2
            addt_y_shift = 2
        else:
            addt_x_shift = 0
            addt_y_shift = 0
        new_bp['blueprint']['entities'][i]['position']['x'] += x_move + x_shift + addt_x_shift
        new_bp['blueprint']['entities'][i]['position']['y'] += y_move + y_shift + addt_y_shift
        
    return new_bp

(The ‘se-space-curved-rail’ was a special case - I’m not sure why it didn’t respond the same to x, y shifts but this was the only entity I observed that behaved this way)