Skip to content

Snagnar/Factompiler

Repository files navigation

Facto compiled blueprint in Factorio

Facto - Write code. Get Factorio circuits.

CI Pipeline codecov PyPI version Python versions License: MIT

Try it out with the online compiler!

Facto is a programming language that compiles to Factorio circuit network blueprints. You write readable code describing the logic you want, and the compiler generates optimized combinators that you paste directly into your game. No manual wiring, no layout headaches, no remembering signal types, no memory frustration, no debugging visual spaghetti.

Memory counter: "signal-A";
counter.write((counter.read() + 1) % 20);

Entity lamp = place("small-lamp", 0, 0);
lamp.enable = counter.read() < 30;
RGB color cycling lamp grid in Factorio

Save this to a blink.facto, run factompile blink.facto, copy the output, import it into Factorio, and watch your lamp blink. You can also visit https://snagnar.github.io/factompile/ to run factompile without having to install anything!


What Facto Does

Facto handles the tedious parts of circuit building so you can focus only on logic. The compiler takes care of placing combinators in a sensible layout, routing wires between them (choosing red vs green to avoid signal conflicts), and inserting relay poles when distances exceed 9 tiles. It catches type mismatches at compile time rather than leaving you to debug mysterious in-game behavior.

What Facto doesn't do: it might not produce the most minimal, most compact circuit layouts. The goal is to make circuits that are efficient enough to run well in-game while being easy to read, write, and maintain. If you need absolute minimalism, hand-optimizing the generated blueprint is still an option.


Installation

pip install factompile

Verify it works with factompile --help.

Try It Instantly

Don't want to create a file? Try compiling directly from the command line:

factompile -i 'Signal x = 42; Entity lamp = place("small-lamp", 0, 0); lamp.enable = x > 20;'

Copy the output blueprint into Factorio and see a lit lamp. Now you're circuit building.


The Language

Facto programs describe circuit networks using four main concepts: signals, memory, entities, and bundles. Here's what each one does and how to use it.

Signals

Signals are values that flow through circuit networks. Every signal has a type (like "iron-plate" or "signal-A") and a value (an integer). You can do arithmetic on signals, and the compiler generates the necessary combinators:

Signal iron = ("iron-plate", 100);    # Explicit type and value
Signal count = 42;                     # Compiler picks a type
Signal doubled = count * 2;            # Arithmetic combinator
Signal isHigh = count > 50;            # Comparison combinator
Signal copper = iron | "copper-plate";  # Type cast

Conditional Values

Facto has a concise syntax for conditional values: condition : value. This outputs value when the condition is true, and 0 when false. It compiles to a single decider combinator, making it both readable and efficient:

Signal capped = (count > 100) : 100;   # 100 if over limit, else 0
Signal passed = (count <= 100) : count; # count if within limit, else 0
Signal clamped = capped + passed;       # Combined: clamped to max 100

This compiles more efficiently than the condition * value pattern you might use in raw Factorio circuits, where multiplication adds an extra combinator.

Memory

Memory stores values that persist across game ticks. Without memory, signals exist only for a single tick. Memory cells are implemented as decider combinators feeding back into themselves:

Memory counter: "signal-A";
counter.write(counter.read() + 1);     # Increment every tick

You can make writes conditional, which is how you build latches and state machines:

Memory buffer: "signal-B";
buffer.write(new_value, when=trigger > 0);   # Only write when triggered

Memory latch: "signal-L";
latch.write(1, set=turn_on > 0, reset=turn_off > 0);  # SR latch

Entities

Entities are Factorio objects you place in the world. You control them by assigning to their properties:

Entity lamp = place("small-lamp", 0, 0);
lamp.enable = count > 50;              # Turn on when count exceeds 50

Entity inserter = place("fast-inserter", 2, 0);
inserter.enable = chest_full == 0;     # Run when chest isn't full

Entity station = place("train-stop", 5, 0, {station: "Iron Pickup"});
station.send_to_train = departure_signal;

The compiler automatically wires entities to the combinators that compute their control signals.

Bundles

Bundles let you operate on multiple signals at once, using Factorio's "each" wildcard. This is how you build circuits that handle arbitrary item types without hardcoding each one:

Bundle resources = { ("iron-plate", 100), ("copper-plate", 80), ("coal", 50) };
Bundle doubled = resources * 2;           # Double all values
Signal iron = resources["iron-plate"];    # Access a single signal
Signal anyLow = any(resources) < 20;      # True if any resource is below 20
Bundle positive = (resources > 0) : resources;  # Filter to only positive values

Bundles are powerful for building generic circuits like balanced train loaders that work regardless of what items you're loading.

Functions

Functions let you reuse logic without copy-pasting combinator setups:

func clamp(Signal value, int min_val, int max_val) {
    Signal use_min = (value < min_val) : min_val;
    Signal use_max = (value > max_val) : max_val;
    Signal use_val = (value >= min_val && value <= max_val) : value;
    return use_min + use_max + use_val;
}

Signal safe_speed = clamp(raw_speed, 0, 100);

Functions are inlined at compile time—there's no runtime call overhead, just shared combinator logic.

Loops

For loops generate multiple entities at compile time:

for i in 0..8 {
    Entity lamp = place("small-lamp", i * 2, 0);
    lamp.enable = counter.read() == i;  # Chaser effect
}

This places 8 lamps, each enabled when the counter matches its index.


Automatic optimizations

The compiler applies several optimizations automatically. Common subexpressions are shared—if you compute counter.read() + 1 in three places, it only generates one arithmetic combinator. Compound conditions like (a > 5) && (b < 10) fold into a single multi-condition decider combinator (a Factorio 2.0 feature). Adding signals of the same type uses wire merging instead of extra combinators.

Wire routing is handled intelligently. The compiler assigns red and green wire colors to avoid signal conflicts, and inserts medium electric poles as relays when combinators are too far apart. For complex patterns like balanced loaders, it ensures signals merge correctly.

Type checking catches mistakes early:

ERROR: Type mismatch: Memory 'buffer' expects 'iron-plate' but write provides 'copper-plate'
WARNING: Mixed signal types in binary operation: 'iron-plate' + 'copper-plate'

Documentation

For deeper coverage, see the documentation in the doc/ directory:

The complete language reference is in LANGUAGE_SPEC.md, and all available entities and their properties are documented in doc/ENTITY_REFERENCE.md.


Command Line Usage

# Compile a file
factompile program.facto                    # Output blueprint to stdout
factompile program.facto -o output.txt      # Save to file

# Compile directly from a string (no file needed!)
factompile -i 'Signal x = 42;'              # Quick one-liner
factompile --input 'Memory m: "signal-A"; m.write(m.read() + 1);'

# Options
factompile program.facto --power-poles medium   # Add power poles
factompile program.facto --name "My Circuit"    # Custom blueprint name
factompile program.facto --log-level debug      # Debug output
factompile program.facto --json                 # Output raw JSON
factompile program.facto --no-optimize          # Skip optimizations

The -i / --input flag is great for quick experiments—try out circuit ideas without creating a file.


Practical Examples

Here are some useful circuits you can build with Facto. If you want a broader range of examples, check the example_programs/ directory for more.

RGB Color Cycling Display

A 5×5 lamp grid that smoothly cycles through all colors:

Memory hue: "signal-H";
hue.write((hue.read() + 1) % 1530);  # Full HSV cycle

Signal h = hue.read();
Signal sector = h / 255;
Signal pos = h % 255;

# HSV to RGB conversion using conditional values
Signal r = ((sector == 0 || sector == 5) : 255)
         + ((sector == 1) : (255 - pos))
         + ((sector == 4) : pos);
Signal g = ((sector == 0) : pos)
         + ((sector == 1 || sector == 2) : 255)
         + ((sector == 3) : (255 - pos));
Signal b = ((sector == 2) : pos)
         + ((sector == 3 || sector == 4) : 255)
         + ((sector == 5) : (255 - pos));

for y in 0..5 {
    for x in 0..5 {
        Entity lamp = place("small-lamp", x, y, 
            {use_colors: 1, always_on: 1, color_mode: 1});
        lamp.r = r | "signal-red";
        lamp.g = g | "signal-green";
        lamp.b = b | "signal-blue";
    }
}

RGB color cycling lamp grid in Factorio

Backup Steam Power

A circuit that turns on backup steam when accumulators drop below 20%, and keeps it running until they reach 80%. The hysteresis prevents flickering when power hovers near the threshold:

Signal battery = ("signal-A", 0);  # Wire this to your accumulator

Memory steam_on: "signal-A";
steam_on.write(1, set=battery < 20, reset=battery >= 80);

Entity steam_switch = place("power-switch", 0, 0);
steam_switch.enable = steam_on.read() > 0;

Resource warning lamps in Factorio

Resource Warning System

A simple bundle-based monitor that lights up when any resource runs low:

# Wire these from your storage chests
Bundle resources = { ("iron-plate", 0), ("copper-plate", 0), ("coal", 0) };

Entity warning_lamp = place("small-lamp", 0, 0);
Entity good_lamp = place("small-lamp", 2, 0);

warning_lamp.enable = any(resources) < 100;
good_lamp.enable = all(resources) > 500;

Resource warning lamps in Factorio

Balanced Train Loader

The classic MadZuri pattern—inserters only activate when their chest is below the average:

Entity c1 = place("steel-chest", 0, 0);
Entity c2 = place("steel-chest", 1, 0);
Entity c3 = place("steel-chest", 2, 0);

Bundle total = {c1.output, c2.output, c3.output};
Bundle neg_avg = total / -3;

Entity i1 = place("fast-inserter", 0, 1);
Bundle diff1 = {neg_avg, c1.output};
i1.enable = any(diff1) < 0;

Entity i2 = place("fast-inserter", 1, 1);
Bundle diff2 = {neg_avg, c2.output};
i2.enable = any(diff2) < 0;

Entity i3 = place("fast-inserter", 2, 1);
Bundle diff3 = {neg_avg, c3.output};
i3.enable = any(diff3) < 0;

Balanced train loader in Factorio


Development

git clone https://github.com/Snagnar/Factompiler.git
cd Factompiler
pip install -e ".[dev]"

pytest -n auto                    # Run all tests
pytest -m "not end2end" -n auto   # Skip slow end-to-end tests
ruff check . && ruff format .     # Lint and format

If you are using VS Code, you can install install this facto vscode extension, for code highlighting and basic language support.


Contributing

Contributions are welcome—bug fixes, new features, documentation improvements, or additional test cases. Check GitHub Issues for open tasks or open a new issue to discuss what you'd like to work on.


Acknowledgments

Facto builds on factorio-draftsman, the Python library that makes blueprint generation possible. Thanks to the Factorio community for endless circuit inspiration, and to Wube Software for making the best factory game ever.


The factory must grow. Now it can grow faster.

GitHubPyPILanguage Spec

About

A compiled language for Factorio Blueprints.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages