├── Makefile
├── README.md
├── recipes-all.pdf
├── recipes-filtered.pdf
├── recipes.lua
├── screenshot.png
├── techtree-all.pdf
├── techtree.lua
└── utils.lua
/Makefile:
--------------------------------------------------------------------------------
1 | # Where to find Factorio files
2 | FACTORIO_ROOT="$(HOME)/Library/Application Support/Steam/SteamApps/common/Factorio/factorio.app/Contents/"
3 |
4 | # Output file format. PDF recommended.
5 | # SVG can render tooltips - but they don't contain anything other than debug information.
6 | OUTFORMAT=pdf
7 |
8 | # LUA interpreter
9 | LUA=lua
10 |
11 | # GraphViz commands (brew install graphviz)
12 | DOT=dot
13 | UNFLATTEN=unflatten
14 |
15 |
16 | all: recipes-all.$(OUTFORMAT) recipes-filtered.$(OUTFORMAT) techtree-all.$(OUTFORMAT)
17 |
18 | recipes-all.$(OUTFORMAT): recipes.lua utils.lua
19 | $(LUA) -e 'FACTORIO_ROOT=$(FACTORIO_ROOT)' recipes.lua | $(UNFLATTEN) | $(DOT) -T $(OUTFORMAT) -o $@
20 |
21 | recipes-filtered.$(OUTFORMAT): recipes.lua utils.lua
22 | $(LUA) -e 'FACTORIO_ROOT=$(FACTORIO_ROOT)' -e 'FILTER=true' recipes.lua | $(UNFLATTEN) | $(DOT) -T $(OUTFORMAT) -o $@
23 |
24 | techtree-all.$(OUTFORMAT): techtree.lua utils.lua
25 | $(LUA) -e 'FACTORIO_ROOT=$(FACTORIO_ROOT)' $? | $(DOT) -T $(OUTFORMAT) -o $@
26 |
27 | clean:
28 | rm -f recipes-filtered.* recipes-all.* techtree-all.*
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Factorio Trees
2 |
3 | This is a collection of Lua scripts that parse Factorio's data files and then use GraphViz to generate trees for recipe and technology dependencies. It does not add woody perennial plants to Factorio, sorry.
4 |
5 | 
6 |
7 |
8 | # Prerequisites
9 |
10 | * Lua
11 | * GraphViz
12 |
13 | ## Mac OS X
14 |
15 | brew install lua graphviz
16 |
17 | # Run
18 |
19 | git clone https://github.com/ingmar/factorio-trees.git
20 | cd factorio-trees
21 | # Check FACTORIO_ROOT in the Makefile
22 | make
23 | open *.pdf
24 |
--------------------------------------------------------------------------------
/recipes-all.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ingmar/factorio-trees/4196fbbb7d6dfa0ce7121ee4596e5ec7ec08e487/recipes-all.pdf
--------------------------------------------------------------------------------
/recipes-filtered.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ingmar/factorio-trees/4196fbbb7d6dfa0ce7121ee4596e5ec7ec08e487/recipes-filtered.pdf
--------------------------------------------------------------------------------
/recipes.lua:
--------------------------------------------------------------------------------
1 | -- Factorio recipe tree
2 |
3 | require "utils"
4 |
5 |
6 | -- Recipes to load
7 | RECIPE_FILES = {
8 | "ammo.lua",
9 | "capsule.lua",
10 | "demo-furnace-recipe.lua",
11 | "demo-recipe.lua",
12 | "demo-turret.lua",
13 | "equipment.lua",
14 | "fluid-recipe.lua",
15 | "furnace-recipe.lua",
16 | "inserter.lua",
17 | "module.lua",
18 | "recipe.lua",
19 | "turret.lua",
20 | }
21 | -- Which string translation sections to use (now always in base.cfg)
22 | LANGUAGE_SECTIONS = {
23 | ["item-name"] = true,
24 | ["entity-name"] = true,
25 | ["fluid-name"] = true,
26 | ["equipment-name"] = true,
27 | }
28 | -- Recipes to exclude from graph
29 | RECIPE_EXCLUDE = {
30 | -- Recipes that have better alternatives
31 | ["basic-armor"] = true,
32 | ["basic-bullet-magazine"] = true,
33 | ["heavy-armor"] = true,
34 | ["iron-axe"] = true,
35 | ["iron-chest"] = true,
36 | ["shotgun"] = true,
37 | ["shotgun-shell"] = true,
38 | ["small-electric-pole"] = true,
39 | ["wooden-chest"] = true,
40 | ["power-armor"] = true,
41 | ["basic-modular-armor"] = true,
42 |
43 | -- Outdated tech
44 | ["burner-mining-drill"] = true,
45 | ["pistol"] = true,
46 | ["steel-furnace"] = true,
47 | ["stone-furnace"] = true,
48 |
49 | -- Unknown Use
50 | ["player-port"] = true,
51 | ["railgun-dart"] = true,
52 | ["railgun"] = true,
53 | ["small-plane"] = true,
54 |
55 | -- Filter away for factory plan
56 | ["wood"] = true,
57 | ["combat-shotgun"] = true,
58 | ["basic-oil-processing"] = true,
59 | ["fill-crude-oil-barrel"] = true,
60 | ["iron-plate"] = true,
61 | ["copper-plate"] = true,
62 | ["flame-thrower-ammo"] = true,
63 | ["steel-axe"] = true,
64 | ["storage-tank"] = true,
65 | ["boiler"] = true,
66 | ["cargo-wagon"] = true,
67 | ["steel-chest"] = true,
68 | ["oil-refinery"] = true,
69 | ["flame-thrower"] = true,
70 | ["chemical-plant"] = true,
71 | ["small-lamp"] = true,
72 | ["steam-engine"] = true,
73 | ["pumpjack"] = true,
74 | ["train-stop"] = true,
75 | ["offshore-pump"] = true,
76 | ["combat-shotgun"] = true,
77 | ["rocket-launcher"] = true,
78 | ["basic-mining-drill"] = true,
79 | ["radar"] = true,
80 | ["submachine-gun"] = true,
81 | ["assembling-machine-1"] = true,
82 | ["gun-turret"] = true,
83 | ["basic-electric-discharge-defense-remote"] = true,
84 | ["green-wire"] = true,
85 | ["red-wire"] = true,
86 | ["rail-signal"] = true,
87 | ["car"] = true,
88 | ["basic-transport-belt-to-ground"] = true,
89 | ["diesel-locomotive"] = true,
90 | ["smart-chest"] = true,
91 | ["lab"] = true,
92 | ["basic-splitter"] = true,
93 | ["assembling-machine-2"] = true,
94 | ["land-mine"] = true,
95 | ["electric-furnace"] = true,
96 | ["fast-transport-belt-to-ground"] = true,
97 | ["roboport"] = true,
98 | --["substation"] = true,
99 | ["basic-beacon"] = true,
100 | ["energy-shield-equipment"] = true,
101 | ["deconstruction-planner"] = true,
102 | ["blueprint"] = true,
103 | ["night-vision-equipment"] = true,
104 | ["logistic-chest-requester"] = true,
105 | ["logistic-chest-storage"] = true,
106 | ["logistic-chest-active-provider"] = true,
107 | ["logistic-chest-passive-provider"] = true,
108 | ["fast-splitter"] = true,
109 | ["small-pump"] = true,
110 | ["basic-exoskeleton-equipment"] = true,
111 | ["solar-panel-equipment"] = true,
112 | ["express-transport-belt-to-ground"] = true,
113 | ["energy-shield-mk2-equipment"] = true,
114 | ["express-splitter"] = true,
115 | ["battery-equipment"] = true,
116 | ["assembling-machine-3"] = true,
117 | ["fusion-reactor-equipment"] = true,
118 | ["battery-mk2-equipment"] = true,
119 | ["basic-laser-defense-equipment"] = true,
120 | ["basic-electric-discharge-defense-equipment"] = true,
121 | ["power-armor-mk2"] = true,
122 | ["rocket-defense"] = true,
123 | ["arithmetic-combinator"] = true,
124 | ["decider-combinator"] = true,
125 | ["constant-combinator"] = true,
126 | ["arithmetic-combinator"] = true,
127 | ["arithmetic-combinator"] = true,
128 | ["arithmetic-combinator"] = true,
129 | ["arithmetic-combinator"] = true,
130 | }
131 |
132 | -- Ingredients that are basic resources
133 | RESOURCES = {
134 | ["copper-ore"] = true,
135 | ["crude-oil"] = true,
136 | ["iron-ore"] = true,
137 | ["stone"] = true,
138 | ["raw-wood"] = true,
139 | ["water"] = true,
140 | }
141 |
142 | -- Try and map recipe categories to the (minimum) type of crafting station needed
143 | CATEGORY_LABEL = {
144 | default = Img(FACTORIO_ROOT.."data/base/graphics/icons/assembling-machine-1.png"),
145 | crafting = Img(FACTORIO_ROOT.."data/base/graphics/icons/assembling-machine-1.png"),
146 | ["crafting-with-fluid"] = Img(FACTORIO_ROOT.."data/base/graphics/icons/assembling-machine-2.png"),
147 | ["advanced-crafting"] = Img(FACTORIO_ROOT.."data/base/graphics/icons/assembling-machine-2.png"),
148 | smelting = Img(FACTORIO_ROOT.."data/base/graphics/icons/stone-furnace.png"),
149 | ["oil-processing"] = Img(FACTORIO_ROOT.."data/base/graphics/icons/oil-refinery.png"),
150 | chemistry = Img(FACTORIO_ROOT.."data/base/graphics/icons/chemical-plant.png"),
151 | }
152 |
153 |
154 | load_data(RECIPE_FILES, "data/base/prototypes/recipe/")
155 | load_translations(LANGUAGE_SECTIONS)
156 |
157 |
158 | -- Graphviz output
159 | print('strict digraph factorio {')
160 | -- Change rankdir to LR or TB to change direction of graph
161 | print('layout=dot; splines=polyline; rankdir=LR; color="#ffffff"; bgcolor="#332200"; ratio=auto; ranksep=2.0; nodesep=0.15;')
162 | -- Node default attributes
163 | node_default = {}
164 | node_default.color = '"#e4e4e4"'
165 | node_default.fontname = '"TitilliumWeb-SemiBold"'
166 | node_default.fontcolor = '"#ffffff"'
167 | node_default.shape = 'box'
168 | node_default.style = 'filled'
169 | print(string.format('node [%s]', VizAttr(node_default)))
170 | -- Edge default attributes
171 | edge_default = {}
172 | edge_default.penwidth = 2
173 | edge_default.color = '"#DDDD22"'
174 | edge_default.fontname = node_default.fontname
175 | edge_default.fontcolor = node_default.fontcolor
176 | print(string.format('edge [%s]', VizAttr(edge_default)))
177 |
178 | -- Raw resources go on the top/leftmost rank on their own
179 | print('{ rank=source;')
180 | for res in pairs(RESOURCES) do
181 | print(string.format('"%s";', res))
182 | end
183 | print('}')
184 |
185 | for id, recipe in pairs(data) do
186 | -- First do a few sanity checks
187 | if recipe.type ~= "recipe" then
188 | io.stderr:write(string.format('Found unknown type "%s" instead of "recipe" for %s', recipe.type, recipe.name), "\n")
189 | os.exit(1)
190 | end
191 | if recipe.enabled ~= "false" then
192 | print("// ENABLED:", recipe.name) -- Initially unlocked recipes?
193 | end
194 |
195 | if not FILTER or (FILTER and not RECIPE_EXCLUDE[recipe.name]) then
196 | -- Some recipes have multiple sets of ingredients based on difficulty level.
197 | if recipe.ingredients == nil then
198 | -- Assume "normal"" difficulty instead of "expensive"
199 | recipe.ingredients = recipe.normal.ingredients
200 | recipe.result = recipe.normal.result
201 | end
202 |
203 | -- Recipe has .result data, convert to new .results format for easier handling
204 | if recipe.result ~= nil then
205 | recipe.results = {{name = recipe.result, amount = recipe.result_count}}
206 | end
207 |
208 | -- Define the recipe node first
209 | attr = {}
210 | -- If energy_required isn't specified, it defaults to 0.5
211 | if recipe.energy_required == nil then recipe.energy_required = 0.5 end
212 | attr.label = HtmlLabel(CATEGORY_LABEL[recipe.category or 'default'], nil, recipe.energy_required)
213 | attr.tooltip = string.format('"%s"', recipe.name) -- Put the untranslated name into the tooltip
214 | attr.fillcolor = '"#6d7235"'
215 | attr.color = attr.fillcolor
216 | attr.shape = "cds"
217 | print(string.format('"Recipe: %s" [%s];', recipe.name, VizAttr(attr)))
218 |
219 | -- Make edges from each ingredient to the recipe
220 | print(string.format(" // Ingredients"))
221 | for ing_id, ing in pairs(recipe.ingredients) do
222 | -- Convert old array syntax into new descriptive one
223 | if ing.type == nil then
224 | ing.name = ing[1]
225 | ing.amount = ing[2]
226 | end
227 |
228 | -- Define ingredient node
229 | attr = {}
230 | attr.label = HtmlLabel(T(ing.name), GetIcon(ing))
231 | if ing.type == "fluid" then
232 | attr.shape = "ellipse"
233 | attr.fillcolor = '"#3d3c6e"'
234 | else
235 | attr.fillcolor = '"#8f8f90"'
236 | end
237 | attr.color = attr.fillcolor
238 | print(string.format('"%s" [%s];', ing.name, VizAttr(attr)))
239 |
240 | -- Ingredient -> Recipe edge
241 | attr = {}
242 | attr.label = string.format('"x%d"', ing.amount)
243 | if ing.type == "fluid" then
244 | attr.color = '"#45A7F3"'
245 | elseif ing.name == "copper-plate" then
246 | attr.color = '"#C77362"'
247 | elseif ing.name == "iron-plate" then
248 | attr.color = '"#838588"'
249 | elseif ing.name == "steel-plate" then
250 | attr.color = '"#96ff8B"'
251 | else
252 | attr.color = edge_default.color
253 | end
254 | -- For raw resources, set fixed rank
255 | if RESOURCES[ing.name] ~= nil then
256 | attr.rank="source"
257 | end
258 | print(string.format(' "%s" -> "Recipe: %s" [%s];', ing.name, recipe.name, VizAttr(attr)))
259 | end
260 |
261 | -- And from the recipe to each result
262 | print(" // Results")
263 | for res_id, res in pairs(recipe.results) do
264 | -- Define result node
265 | attr = {}
266 | attr.label = HtmlLabel(T(res.name), GetIcon(res))
267 | if res.type == "fluid" then
268 | attr.fillcolor = '"#3d3c6e"'
269 | else
270 | attr.fillcolor = '"#8f8f90"'
271 | end
272 | attr.color = attr.fillcolor
273 | print(string.format('"%s" [%s];', res.name, VizAttr(attr)))
274 |
275 | -- Recipe -> Result edge
276 | attr = {}
277 | attr.weight = 100 -- Shorten result edges so results are close to recipe/factory
278 | attr.label = string.format('x%d', res.amount or 1)
279 | if res.type == "fluid" then
280 | attr.color = '"#9999ff"'
281 | else
282 | attr.color = edge_default.color
283 | end
284 | print(string.format(' "Recipe: %s" -> "%s" [%s];', recipe.name, res.name, VizAttr(attr)))
285 | end
286 | print("")
287 | end
288 | end
289 | print("}") -- Done!
290 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ingmar/factorio-trees/4196fbbb7d6dfa0ce7121ee4596e5ec7ec08e487/screenshot.png
--------------------------------------------------------------------------------
/techtree-all.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ingmar/factorio-trees/4196fbbb7d6dfa0ce7121ee4596e5ec7ec08e487/techtree-all.pdf
--------------------------------------------------------------------------------
/techtree.lua:
--------------------------------------------------------------------------------
1 | -- Factorio tech tree
2 |
3 | require "utils"
4 |
5 | -- Recipes to load
6 | TECH_FILES = {
7 | "inserter.lua",
8 | "technology.lua",
9 | }
10 | -- Which string translation sections to use (now always in base.cfg)
11 | LANGUAGE_SECTIONS = {
12 | ["technology-name"] = true,
13 | }
14 |
15 |
16 | load_data(TECH_FILES, "data/base/prototypes/technology/")
17 | load_translations(LANGUAGE_SECTIONS)
18 |
19 |
20 | -- Graphviz output
21 | print('strict digraph factorio {')
22 | -- Change rankdir to RL or BT or omit entirely to change direction of graph
23 | print('layout=dot; rankdir=LR; color="#ffffff"; bgcolor="#002233"; ratio=auto; ranksep=2.0; nodesep=0.15;')
24 | -- Node default attributes
25 | node_default = {}
26 | node_default.color = '"#a8a9a8"'
27 | node_default.fontname = '"TitilliumWeb-SemiBold"'
28 | node_default.fontcolor = '"#ffffff"'
29 | node_default.shape = 'box'
30 | node_default.style = 'filled'
31 | print(string.format('node [%s]', VizAttr(node_default)))
32 | -- Edge default attributes
33 | edge_default = {}
34 | edge_default.penwidth = 2
35 | edge_default.color = '"#808080"'
36 | edge_default.fontname = node_default.fontname
37 | edge_default.fontcolor = node_default.fontcolor
38 | print(string.format('edge [%s]', VizAttr(edge_default)))
39 |
40 | for id, tech in pairs(data) do
41 | -- Define the technology node first
42 | if tech.name:match('.*-%d+$') then
43 | -- Multi-level technologies don't have separate string translations, so we have to extract the level and reassemble
44 | translation_name, level = tech.name:match('(.*)-(%d+)$')
45 | label_name = string.format("%s %d", T(translation_name), level)
46 | else
47 | label_name = T(tech.name)
48 | end
49 | attr = {}
50 | attr.label = HtmlLabel(label_name, GetIcon(tech))
51 | attr.tooltip = string.format('"%s\nenergy_required: %s"', tech.name, tech.energy_required or "nil") -- Put the untranslated name into the tooltip
52 | attr.fillcolor = '"#6d7235"'
53 | attr.color = attr.fillcolor
54 | attr.shape = "cds"
55 | print(string.format('"Tech: %s" [%s];', tech.name, VizAttr(attr)))
56 |
57 | if tech.prerequisites ~= nil then
58 | -- Make edges from each prerequisite to this technology
59 | print(string.format(" // Prerequisites"))
60 | for prereq_id, prereq in pairs(tech.prerequisites) do
61 | -- Prerequisite -> Tech edge
62 | attr = {}
63 | print(string.format(' "Tech: %s" -> "Tech: %s" [%s];', prereq, tech.name, VizAttr(attr)))
64 | end
65 | end
66 |
67 | if tech.effects ~= nil then
68 | -- Recipes unlocked by researching this tech
69 | print(string.format(" // Unlocks"))
70 | for effect_id, effect in pairs(tech.effects) do
71 | if effect.type == "unlock-recipe" then
72 | attr = {}
73 | print(string.format('// "Tech: %s" -> "Recipe: %s" [%s];', tech.name, effect.recipe, VizAttr(attr)))
74 | else
75 | --io.stderr:write("Unknown effect.type: "..effect.type.."\n")
76 | end
77 | end
78 | end
79 |
80 | end
81 | print("}") -- Done!
82 |
--------------------------------------------------------------------------------
/utils.lua:
--------------------------------------------------------------------------------
1 | ---- These defaults can be overwritten from the command line using lua -e 'BLA=foo' (or just set them in Makefile)
2 | -- Where the Factorio data/ folder lives
3 | FACTORIO_ROOT = FACTORIO_ROOT or "/Applications/factorio.app/Contents/"
4 | -- Choose your language here
5 | LANGUAGE = LANGUAGE or "en"
6 | -- Set to false to disable string translation
7 | TRANSLATE = TRANSLATE or true
8 |
9 | translations = {}
10 |
11 | function Img(src)
12 | -- Convenience function for IMG tags
13 | return string.format([[]], src)
14 | end
15 |
16 | function VizAttr(tbl)
17 | -- Convert a table of attributes and values into Graphviz syntax
18 | pair_array = {}
19 | for attr, value in pairs(tbl) do
20 | table.insert(pair_array, string.format('%s=%s', attr, value))
21 | end
22 | return table.concat(pair_array, ",")
23 | end
24 |
25 | -- Extendable table (for the data:extend calls in Factorio LUA)
26 | ExtendTable = {}
27 | function ExtendTable:new(o)
28 | o = o or {} -- create object if user does not provide one
29 | setmetatable(o, self)
30 | self.__index = self
31 | return o
32 | end
33 | function ExtendTable:extend(other)
34 | -- Merge tables
35 | for key, value in pairs(other) do
36 | while self[key] ~= nil do
37 | key = key + math.random(1024) -- Nasty hack to resolve key conflicts.
38 | end
39 | self[key] = value
40 | end
41 | end
42 |
43 | function trim(s)
44 | return s:match('^%s*(.*%S)') or ''
45 | end
46 |
47 | function file_exists(name)
48 | local f = io.open(name, "r")
49 | if f ~= nil then
50 | f:close()
51 | return true
52 | else
53 | return false
54 | end
55 | end
56 |
57 | data = ExtendTable:new({})
58 | function load_data(files, rel_path)
59 | for _, filename in ipairs(files) do
60 | path = FACTORIO_ROOT..rel_path..filename
61 | dofile(path)
62 | end
63 | end
64 |
65 | -- Load string translations
66 | function load_translations(language_sections)
67 | language_files = {
68 | "base.cfg",
69 | }
70 | for _, filename in ipairs(language_files) do
71 | path = FACTORIO_ROOT.."data/base/locale/"..LANGUAGE.."/"..filename
72 | local f = io.open(path, "r")
73 | local section
74 | while true do
75 | local line = f:read("*line")
76 | if line == nil then break end
77 | line = trim(line)
78 | if line ~= "" then
79 | if line:sub(1, 1) == "[" then
80 | section = line:sub(2, -2)
81 | -- io.stderr:write("[", section, "]\n")
82 | else
83 | -- io.stderr:write("-- ", line, "\n")
84 | if language_sections[section] then
85 | local eq_pos = line:find("=")
86 | local lookup = line:sub(1, eq_pos-1)
87 | local translation = line:sub(eq_pos+1, -1)
88 | translations[lookup] = translation
89 | -- io.stderr:write(lookup, "=", translation, "\n")
90 | end
91 | end
92 | end
93 | end
94 | f:close()
95 | end
96 | end
97 |
98 | function T(lookup)
99 | if not TRANSLATE then
100 | return lookup
101 | end
102 | -- Look up translation for string, returning lookup if none was found
103 | local translation = translations[lookup]
104 | if translation == nil then
105 | io.stderr:write('No translation found for ', lookup, "\n")
106 | return lookup
107 | end
108 | return translation
109 | end
110 |
111 | function HtmlLabel(name, img, energy)
112 | -- Cough up a Graphviz label in their HTML-like syntax
113 | local clock_icon = FACTORIO_ROOT.."data/core/graphics/clock-icon.png"
114 | local img_tag = ""
115 | local energy_tag = ""
116 | if img ~= nil then img_tag = string.format([[
]], img) end
117 | -- energy_required seems to refer to the time it needs (clock icon)
118 | if energy ~= nil then energy_tag = string.format([[
%s | %s | %s