From f2a114955c57344f9e75ee64e468437cb860505b Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Fri, 10 May 2024 10:06:19 -0500 Subject: [PATCH 01/25] pyPP better error messaging --- changelog.txt | 5 ++++ prototypes/functions/auto_tech.lua | 35 ++++++++++++++------------- prototypes/functions/data_parser.lua | 13 ---------- prototypes/functions/fz_topo_sort.lua | 15 ++++++++---- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4f61805..10b18bf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,9 @@ --------------------------------------------------------------------------------------------------- +Version: 0.2.24 +Date: 2024-??-?? + Changes: + - Whenever pyPP encounters a dependency cycle, it will now print the items involved in the cycle to the logs. +--------------------------------------------------------------------------------------------------- Version: 0.2.23 Date: 2024-4-25 Changes: diff --git a/prototypes/functions/auto_tech.lua b/prototypes/functions/auto_tech.lua index c20e3cf..7a9e2cc 100644 --- a/prototypes/functions/auto_tech.lua +++ b/prototypes/functions/auto_tech.lua @@ -123,30 +123,30 @@ function auto_tech:run() sp_ts:run() local fg2 = fg:copy() - local error_found + local error_message - error_found, ts = self:topo_sort_with_sp(fg, spg, parser.science_packs) + error_message, ts = self:topo_sort_with_sp(fg, spg, parser.science_packs) - if error_found then - local msg = "\n\nERROR: Dependency loop detected in step 1\n" + if error_message then + local msg = "\n\nERROR: Dependency loop detected\n" .. error_message error(msg) end self:add_original_prerequisites(fg, fg2, ts.level) - error_found, ts = self:topo_sort_with_sp(fg2, spg, parser.science_packs) + error_message, ts = self:topo_sort_with_sp(fg2, spg, parser.science_packs) - if error_found then - local msg = "\n\nERROR: Dependency loop detected in step 2\n" + if error_message then + local msg = "\n\nERROR: Dependency loop detected\n" .. error_message error(msg) end local tg = self:extract_tech_graph(fg2) local tech_ts = fz_topo.create(tg) - error_found = tech_ts:run(false, false) + error_message = tech_ts:run(false, false) - if error_found then - local msg = "\n\nERROR: Dependency loop detected in step 3\n" + if error_message then + local msg = "\n\nERROR: Dependency loop detected\n" .. error_message error(msg) end @@ -304,20 +304,21 @@ function auto_tech:topo_sort_with_sp(fg, sp_graph, science_packs) end end - local ts = fz_topo.create(fg) - local error_found = ts:run(false, self.verbose_logging) - for _, link in pairs(sp_links) do fg:remove_link(link.from, link.to, link.from.name) end + local ts = fz_topo.create(fg) + local error_found, recipes_with_issues = ts:run(false, self.verbose_logging) + + local error_message = '' if error_found then - log("RESTARTING without SP links") - ts = fz_topo.create(fg) - error_found = ts:run(false, self.verbose_logging) + for key, _ in pairs(recipes_with_issues) do + error_message = error_message .. "There was a dependency loop involving: " .. key .. "\n" + end end - return error_found, ts + return error_message, ts end diff --git a/prototypes/functions/data_parser.lua b/prototypes/functions/data_parser.lua index d89df98..fedd7da 100644 --- a/prototypes/functions/data_parser.lua +++ b/prototypes/functions/data_parser.lua @@ -512,19 +512,6 @@ function data_parser:add_entity_dependencies(entity, recipe_node, recipe_name, i end end end - elseif energy_source.type == "electric" and not config.ELECTRICITY_PRODUCER_PROTOTYPES:contains(entity.type) then - recipe_node:add_label(LABEL_FUEL) - local fuel_node = self.fg:get_node(FUEL_ELECTRICITY, fz_graph.NT_ITEM) - self.fg:add_link(fuel_node, recipe_node, LABEL_FUEL) - elseif energy_source.type == "heat" then - recipe_node:add_label(LABEL_FUEL) - - for _, temp in pairs(self.heat_temps) do - if temp >= (energy_source.min_working_temperature or 15) and temp <= energy_source.max_temperature then - local fuel_node = self.fg:get_node(data_parser.get_fluid_name(FUEL_HEAT, temp), fz_graph.NT_FLUID) - self.fg:add_link(fuel_node, recipe_node, LABEL_FUEL) - end - end end end diff --git a/prototypes/functions/fz_topo_sort.lua b/prototypes/functions/fz_topo_sort.lua index 1ae6b3f..168bbad 100644 --- a/prototypes/functions/fz_topo_sort.lua +++ b/prototypes/functions/fz_topo_sort.lua @@ -25,6 +25,7 @@ end function fz_topo:run(check_ancestry, logging) self.queue(self.work_graph.start_node) self.level[self.work_graph.start_node.key] = 1 + local recipes_with_issues = {} while not queue.is_empty(self.queue) do local node = self.queue() @@ -89,16 +90,20 @@ function fz_topo:run(check_ancestry, logging) self.queue(to_node) self.level[to_node.key] = self.level[node.key] + 1 if logging then log(" - Queued: " .. to_key) end - elseif logging then - log(" - Not queued: " .. to_key) - for _, e in self.work_graph:iter_links_to(to_node) do - log(" - " .. e:from() .. " : " .. e.label) + recipes_with_issues[to_key] = nil + else + recipes_with_issues[to_key] = true + if logging then + log(" - Not queued: " .. to_key) + for _, e in self.work_graph:iter_links_to(to_node) do + log(" - " .. e:from() .. " : " .. e.label) + end end end end end - return table.any(self.graph.nodes, function (n) return not n.ignore_for_dependencies and not self.sorted[n.key] end) + return table.any(self.graph.nodes, function (n) return not n.ignore_for_dependencies and not self.sorted[n.key] end), recipes_with_issues end From 5895c7fbd4bec79b2ce223ec6c425102ac89f795 Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Fri, 10 May 2024 10:18:48 -0500 Subject: [PATCH 02/25] Fix crash --- prototypes/functions/auto_tech.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prototypes/functions/auto_tech.lua b/prototypes/functions/auto_tech.lua index 7a9e2cc..f0cc215 100644 --- a/prototypes/functions/auto_tech.lua +++ b/prototypes/functions/auto_tech.lua @@ -311,8 +311,9 @@ function auto_tech:topo_sort_with_sp(fg, sp_graph, science_packs) local ts = fz_topo.create(fg) local error_found, recipes_with_issues = ts:run(false, self.verbose_logging) - local error_message = '' + local error_message if error_found then + error_message = "" for key, _ in pairs(recipes_with_issues) do error_message = error_message .. "There was a dependency loop involving: " .. key .. "\n" end From 3f250c4921e90750db45a29af4ec1e1bc7a1efa2 Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Fri, 10 May 2024 15:17:47 -0500 Subject: [PATCH 03/25] Error handling v2 --- prototypes/functions/auto_tech.lua | 2 +- prototypes/functions/data_parser.lua | 13 +++++++++++++ prototypes/functions/fz_topo_sort.lua | 20 +++++++++++++++++++- settings-updates.lua | 4 ++-- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/prototypes/functions/auto_tech.lua b/prototypes/functions/auto_tech.lua index f0cc215..52e78e6 100644 --- a/prototypes/functions/auto_tech.lua +++ b/prototypes/functions/auto_tech.lua @@ -315,7 +315,7 @@ function auto_tech:topo_sort_with_sp(fg, sp_graph, science_packs) if error_found then error_message = "" for key, _ in pairs(recipes_with_issues) do - error_message = error_message .. "There was a dependency loop involving: " .. key .. "\n" + error_message = error_message .. "Impossible to craft: " .. key .. "\n" end end diff --git a/prototypes/functions/data_parser.lua b/prototypes/functions/data_parser.lua index fedd7da..d89df98 100644 --- a/prototypes/functions/data_parser.lua +++ b/prototypes/functions/data_parser.lua @@ -512,6 +512,19 @@ function data_parser:add_entity_dependencies(entity, recipe_node, recipe_name, i end end end + elseif energy_source.type == "electric" and not config.ELECTRICITY_PRODUCER_PROTOTYPES:contains(entity.type) then + recipe_node:add_label(LABEL_FUEL) + local fuel_node = self.fg:get_node(FUEL_ELECTRICITY, fz_graph.NT_ITEM) + self.fg:add_link(fuel_node, recipe_node, LABEL_FUEL) + elseif energy_source.type == "heat" then + recipe_node:add_label(LABEL_FUEL) + + for _, temp in pairs(self.heat_temps) do + if temp >= (energy_source.min_working_temperature or 15) and temp <= energy_source.max_temperature then + local fuel_node = self.fg:get_node(data_parser.get_fluid_name(FUEL_HEAT, temp), fz_graph.NT_FLUID) + self.fg:add_link(fuel_node, recipe_node, LABEL_FUEL) + end + end end end diff --git a/prototypes/functions/fz_topo_sort.lua b/prototypes/functions/fz_topo_sort.lua index 168bbad..f284d3a 100644 --- a/prototypes/functions/fz_topo_sort.lua +++ b/prototypes/functions/fz_topo_sort.lua @@ -103,7 +103,25 @@ function fz_topo:run(check_ancestry, logging) end end - return table.any(self.graph.nodes, function (n) return not n.ignore_for_dependencies and not self.sorted[n.key] end), recipes_with_issues + local has_error = table.any(self.graph.nodes, function (n) return not n.ignore_for_dependencies and not self.sorted[n.key] end) + + local errors = {} + if has_error then + for _, n in pairs(self.graph.nodes) do + if not n.ignore_for_dependencies and self.sorted[n.key] and n.key:find('recipe') then + for _, e in self.graph:iter_links_from(n) do + local a = self.graph:get_node(e:from()) + local b = self.graph:get_node(e:to()) + local node = a == n and b or a + if not node.ignore_for_dependencies and not self.sorted[node.key] then + errors[n.key] = true + end + end + end + end + end + + return has_error, errors end diff --git a/settings-updates.lua b/settings-updates.lua index 499563e..b04de4d 100644 --- a/settings-updates.lua +++ b/settings-updates.lua @@ -1,5 +1,5 @@ -data.raw["bool-setting"]["pypp-dev-mode"].forced_value = false -data.raw["bool-setting"]["pypp-create-cache"].forced_value = false +data.raw["bool-setting"]["pypp-dev-mode"].forced_value = true +data.raw["bool-setting"]["pypp-create-cache"].forced_value = true data.raw["bool-setting"]["pypp-verbose-logging"].forced_value = false From 3e9d74cab6fc2b43f67ac2722eb00c13ad1a9689 Mon Sep 17 00:00:00 2001 From: Alex ten Brink Date: Sat, 11 May 2024 18:32:59 +0200 Subject: [PATCH 04/25] Disable dev & cache mode that was accidentally enabled --- settings-updates.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings-updates.lua b/settings-updates.lua index b04de4d..499563e 100644 --- a/settings-updates.lua +++ b/settings-updates.lua @@ -1,5 +1,5 @@ -data.raw["bool-setting"]["pypp-dev-mode"].forced_value = true -data.raw["bool-setting"]["pypp-create-cache"].forced_value = true +data.raw["bool-setting"]["pypp-dev-mode"].forced_value = false +data.raw["bool-setting"]["pypp-create-cache"].forced_value = false data.raw["bool-setting"]["pypp-verbose-logging"].forced_value = false From 5b2b48a139a7ef34edc2fafd339ee297b8ce7e72 Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Sun, 12 May 2024 08:45:05 -0500 Subject: [PATCH 05/25] Error handling v3 --- data-final-fixes.lua | 34 +++++++++++++++++---------- prototypes/config.lua | 2 ++ prototypes/functions/auto_tech.lua | 16 +++++-------- prototypes/functions/fz_topo_sort.lua | 23 +++++++++++------- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/data-final-fixes.lua b/data-final-fixes.lua index b0917db..b73d7f4 100644 --- a/data-final-fixes.lua +++ b/data-final-fixes.lua @@ -242,18 +242,26 @@ if dev_mode then science_packs[pack.name or pack[1]] = true end - add_science_pack_dep(tech, 'utility-science-pack', 'military-science-pack') - - if mods['pyalienlife'] then - add_science_pack_dep(tech, 'utility-science-pack', 'py-science-pack-4') - add_science_pack_dep(tech, 'production-science-pack', 'py-science-pack-3') - add_science_pack_dep(tech, 'chemical-science-pack', 'py-science-pack-2') - add_science_pack_dep(tech, 'logistic-science-pack', 'py-science-pack-1') - add_science_pack_dep(tech, 'py-science-pack-4', 'military-science-pack') - end - - if mods['pyalternativeenergy'] then - add_science_pack_dep(tech, 'production-science-pack', 'military-science-pack') + if mods.pystellarexpedition then + for i = 1, #config.SCIENCE_PACKS - 1 do + local pack = config.SCIENCE_PACKS[i] + local next = config.SCIENCE_PACKS[i + 1] + add_science_pack_dep(tech, next, pack) + end + else + add_science_pack_dep(tech, 'utility-science-pack', 'military-science-pack') + + if mods['pyalienlife'] then + add_science_pack_dep(tech, 'utility-science-pack', 'py-science-pack-4') + add_science_pack_dep(tech, 'production-science-pack', 'py-science-pack-3') + add_science_pack_dep(tech, 'chemical-science-pack', 'py-science-pack-2') + add_science_pack_dep(tech, 'logistic-science-pack', 'py-science-pack-1') + add_science_pack_dep(tech, 'py-science-pack-4', 'military-science-pack') + end + + if mods['pyalternativeenergy'] then + add_science_pack_dep(tech, 'production-science-pack', 'military-science-pack') + end end end @@ -300,7 +308,7 @@ for _, tech in pairs(data.raw.technology) do tech_ingredients_to_use[pack] = ingredient.amount or ingredient[2] end end - + -- Add any missing ingredients that we want present for _, ingredient in pairs(config.TC_TECH_INGREDIENTS_PER_LEVEL[highest_science_pack]) do tech_ingredients_to_use[ingredient.name or ingredient[1]] = ingredient.amount or ingredient[2] diff --git a/prototypes/config.lua b/prototypes/config.lua index f5dba15..81e5b9b 100644 --- a/prototypes/config.lua +++ b/prototypes/config.lua @@ -113,6 +113,8 @@ if mods.pystellarexpedition then end config.STARTING_ITEMS:add('ice') config.STARTING_ITEMS:add('silicate-stone') + config.STARTING_ITEMS:add('cobalt-ore') + config.STARTING_ITEMS:add('organic-nexelit') end if mods['pyalternativeenergy'] then diff --git a/prototypes/functions/auto_tech.lua b/prototypes/functions/auto_tech.lua index 52e78e6..cfc04cc 100644 --- a/prototypes/functions/auto_tech.lua +++ b/prototypes/functions/auto_tech.lua @@ -156,12 +156,12 @@ function auto_tech:run() -- Set science pack order for _, node in pairs(spg.nodes) do - science_pack_order(node.name, string.format("%03d-%06d", sp_ts.level[node.key], ts.level[node.key])) + science_pack_order(node.name, string.format("%03d-%06d", sp_ts.level[node.key] or 0, ts.level[node.key])) local sp = data.raw.tool[node.name] sp.subgroup = "science-pack" - sp.order = string.format("%03d-%06d", sp_ts.level[node.key], ts.level[node.key]) - sp_level[node.name] = sp_ts.level[node.key] + sp.order = string.format("%03d-%06d", sp_ts.level[node.key] or 0, ts.level[node.key]) + sp_level[node.name] = sp_ts.level[node.key] or 0 if sp_level[sp.name] > max_level then max_level = sp_level[sp.name] @@ -304,18 +304,14 @@ function auto_tech:topo_sort_with_sp(fg, sp_graph, science_packs) end end - for _, link in pairs(sp_links) do - fg:remove_link(link.from, link.to, link.from.name) - end - local ts = fz_topo.create(fg) - local error_found, recipes_with_issues = ts:run(false, self.verbose_logging) + local error_found, errors = ts:run(false, self.verbose_logging) local error_message if error_found then error_message = "" - for key, _ in pairs(recipes_with_issues) do - error_message = error_message .. "Impossible to craft: " .. key .. "\n" + for _, key in pairs(errors) do + error_message = error_message .. key .. "\n" end end diff --git a/prototypes/functions/fz_topo_sort.lua b/prototypes/functions/fz_topo_sort.lua index f284d3a..0d86496 100644 --- a/prototypes/functions/fz_topo_sort.lua +++ b/prototypes/functions/fz_topo_sort.lua @@ -107,16 +107,23 @@ function fz_topo:run(check_ancestry, logging) local errors = {} if has_error then - for _, n in pairs(self.graph.nodes) do - if not n.ignore_for_dependencies and self.sorted[n.key] and n.key:find('recipe') then - for _, e in self.graph:iter_links_from(n) do - local a = self.graph:get_node(e:from()) - local b = self.graph:get_node(e:to()) - local node = a == n and b or a - if not node.ignore_for_dependencies and not self.sorted[node.key] then - errors[n.key] = true + for to_key, _ in pairs(recipes_with_issues) do + if to_key:find('item|') then + local node = self.graph:get_node(to_key) + local i = 0 + local j = 0 + local has = '' + local missing = '' + for _, e in self.graph:iter_links_to(node) do + if not self.sorted[e:from()] then + j = j + 1 + missing = missing .. e:from() .. ' ' + else + has = has .. e:from() .. ' ' + i = i + 1 end end + errors[#errors+1] = (to_key .. " has " .. i .. " dependencies and " .. j .. " missing dependencies. Missing: " .. missing) end end end From fb151ffcae3793994478a45c344069748a479ca7 Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Sun, 12 May 2024 09:12:54 -0500 Subject: [PATCH 06/25] Error handling v4 --- prototypes/functions/auto_tech.lua | 11 ++++++++++- prototypes/functions/fz_topo_sort.lua | 26 +------------------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/prototypes/functions/auto_tech.lua b/prototypes/functions/auto_tech.lua index cfc04cc..0facb88 100644 --- a/prototypes/functions/auto_tech.lua +++ b/prototypes/functions/auto_tech.lua @@ -307,10 +307,19 @@ function auto_tech:topo_sort_with_sp(fg, sp_graph, science_packs) local ts = fz_topo.create(fg) local error_found, errors = ts:run(false, self.verbose_logging) + if error_found then + log('RESTARTING WITHOUT SP LINKS') + for _, link in pairs(sp_links) do + fg:remove_link(link.from, link.to, link.from.name) + end + ts = fz_topo.create(fg) + error_found, errors = ts:run(false, self.verbose_logging) + end + local error_message if error_found then error_message = "" - for _, key in pairs(errors) do + for key, _ in pairs(errors) do error_message = error_message .. key .. "\n" end end diff --git a/prototypes/functions/fz_topo_sort.lua b/prototypes/functions/fz_topo_sort.lua index 0d86496..9c23ef4 100644 --- a/prototypes/functions/fz_topo_sort.lua +++ b/prototypes/functions/fz_topo_sort.lua @@ -104,31 +104,7 @@ function fz_topo:run(check_ancestry, logging) end local has_error = table.any(self.graph.nodes, function (n) return not n.ignore_for_dependencies and not self.sorted[n.key] end) - - local errors = {} - if has_error then - for to_key, _ in pairs(recipes_with_issues) do - if to_key:find('item|') then - local node = self.graph:get_node(to_key) - local i = 0 - local j = 0 - local has = '' - local missing = '' - for _, e in self.graph:iter_links_to(node) do - if not self.sorted[e:from()] then - j = j + 1 - missing = missing .. e:from() .. ' ' - else - has = has .. e:from() .. ' ' - i = i + 1 - end - end - errors[#errors+1] = (to_key .. " has " .. i .. " dependencies and " .. j .. " missing dependencies. Missing: " .. missing) - end - end - end - - return has_error, errors + return has_error, recipes_with_issues end From 38e5f1a9770811e19c08bb02fba0b10ef12c3022 Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Fri, 17 May 2024 14:16:02 -0500 Subject: [PATCH 07/25] Stop ftmk compaining --- data.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data.lua b/data.lua index 5d991af..f2fc12a 100644 --- a/data.lua +++ b/data.lua @@ -1,4 +1,4 @@ -pypp_registered_cache_files = {} +_G.pypp_registered_cache_files = {} -- Usage example (add in data-updates phase): -- register_cache_file({"pycoalprocessing", "pyfusionenergy"}, "__pyfusionenergy__/cached-configs/pycoalprocessing+pyfusionenergy.lua") From 1d49275382a9bc8d4c2da5f8704f2da3dcb6abad Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Wed, 22 May 2024 20:35:58 -0500 Subject: [PATCH 08/25] volcanoes --- lib.lua | 533 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 lib.lua diff --git a/lib.lua b/lib.lua new file mode 100644 index 0000000..0b363fe --- /dev/null +++ b/lib.lua @@ -0,0 +1,533 @@ +if py then return py end +local py = {} + +py.gravitational_constant = 6.67408e-11 -- m^3 kg^-1 s^-2 + +local events = {} +py.on_event = function(event, f) + if event == 'on_built' then + py.on_event({defines.events.on_built_entity, + defines.events.on_robot_built_entity, + defines.events.script_raised_built, + defines.events.script_raised_revive + }, f) + return + end + if event == 'on_destroyed' then + py.on_event({ + defines.events.on_player_mined_entity, + defines.events.on_robot_mined_entity, + defines.events.on_entity_died, + defines.events.script_raised_destroy + }, f) + return + end + for _, event in pairs(type(event) == 'table' and event or {event}) do + event = tostring(event) + events[event] = events[event] or {} + table.insert(events[event], f) + end +end + +py.on_nth_tick = function(event, f) + events[event] = events[event] or {} + table.insert(events[event], f) +end + +local function one_function_from_many(functions) + local l = #functions + if l == 1 then return functions[1] end + + return function(arg) + for i = 1, l do + functions[i](arg) + end + end +end + +py.finalize_events = function() + for event, functions in pairs(events) do + local f = one_function_from_many(functions) + if type(event) == 'number' then + script.on_nth_tick(event, f) + elseif event == 'on_init' then + script.on_init(f) + script.on_configuration_changed(f) + else + script.on_event(tonumber(event) or event, f) + end + end +end + +local random = math.random +py.randomize_position = function(position, factor) + local x = position.x or position[1] + local y = position.y or position[2] + factor = factor or 1 + return {x = x + factor * (random() - 0.5), y = y + factor * (random() - 0.5)} +end + +py.empty_image = function() return { + filename = '__pystellarexpeditiongraphics__/graphics/empty.png', + size = 1, + priority = 'high', + direction_count = 1, + frame_count = 1, + line_length = 1 +} end + +py.merge = function(old, new) + old = table.deepcopy(old) + for k, v in pairs(new) do + if v == 'nil' then + old[k] = nil + else + old[k] = v + end + end + return old +end + +py.add_to_description = function(type, prototype, localised_string) + if prototype.localised_description and prototype.localised_description ~= '' then + prototype.localised_description = {'', prototype.localised_description, '\n', localised_string} + return + end + + local place_result = prototype.place_result or prototype.placed_as_equipment_result + if type == 'item' and place_result then + for _, machine in pairs(data.raw) do + machine = machine[place_result] + if machine and machine.localised_description then + prototype.localised_description = { + '?', + {'', machine.localised_description, '\n', localised_string}, + localised_string + } + return + end + end + + local entity_type = prototype.place_result and 'entity' or 'equipment' + prototype.localised_description = { + '?', + {'', {entity_type .. '-description.' .. place_result}, '\n', localised_string}, + {'', {type .. '-description.' .. prototype.name}, '\n', localised_string}, + localised_string + } + else + prototype.localised_description = { + '?', + {'', {type .. '-description.' .. prototype.name}, '\n', localised_string}, + localised_string + } + end +end + +py.cancel_creation = function(entity, player_index, message) + local inserted = 0 + local item_to_place = entity.prototype.items_to_place_this[1] + local surface = entity.surface + local position = entity.position + + if player_index then + local player = game.get_player(player_index) + if player.mine_entity(entity, false) then + inserted = 1 + elseif item_to_place then + inserted = player.insert(item_to_place) + end + end + + if inserted == 0 and item_to_place then + surface.spill_item_stack( + position, + item_to_place, + true, + entity.force_index, + false + ) + end + + entity.destroy{raise_destroy = true} + + if not message then return end + + local tick = game.tick + local last_message = global.last_cancel_creation_message or 0 + if last_message + 60 < tick then + surface.create_entity{ + name = 'flying-text', + position = position, + text = message, + render_player_index = player_index + } + global.last_cancel_creation_message = game.tick + end +end + +local function parse_restriction_condition(condition) + local function helper() + local type = condition.type + if type == 'placed-on' then + return {'placement-restriction.placed-on', {'entity-name.' .. condition.entity}} + elseif type == 'surface-type' then + return {'placement-restriction.surface-type', {'surface-type.' .. condition.surface_type}} + elseif type == 'surface-tag' then + return {'placement-restriction.surface-tag', {'surface-tag-name.' .. condition.tag}} + elseif type == 'distance' then + return {'placement-restriction.distance', {'surface-distance.' .. condition.distance}} + end + + -- greater than less than + local args + if condition.min_amount and condition.max_amount then + args = {'placement-restriction.' .. type .. '-3', condition.min_amount, condition.max_amount} + elseif condition.max_amount then + args = {'placement-restriction.' .. type .. '-2', condition.max_amount} + elseif condition.min_amount then + args = {'placement-restriction.' .. type .. '-1', condition.min_amount} + else + error('min_amount or max_amount missing from placement restriction of type: ' .. type) + end + + if type == 'atmosphere' then + if not condition.gas then error('No gas provided for atomspheric condition') end + if not data.raw.fluid[condition.gas] then error('Invalid gas: ' .. condition.gas) end + for i = 2, #args do + args[i] = args[i] * 100 + end + table.insert(args, 2, {data.raw.fluid[condition.gas].localised_name or ('fluid-name.' .. condition.gas)}) + table.insert(args, 2, '[fluid=' .. condition.gas .. ']') + end + + return args + end + + local localised_string = helper() + if condition.NOT then localised_string = {'placement-restriction.not', localised_string} end + return localised_string +end + +local function placement_restriction_description_helper(i, restriction, parens) + if i == #restriction then + if data then + return {'placement-restriction.dot', parse_restriction_condition(restriction[i])} + else + return parse_restriction_condition(restriction[i]) + end + end + return { + parens, + parse_restriction_condition(restriction[i]), + {'placement-restriction.' .. restriction[i + 1]}, + placement_restriction_description_helper(i + 2, restriction, parens) + } +end + +py.placement_restriction_description = function(restriction) + if #restriction % 2 == 0 then error('Placement restriction length must be odd') end + local parens = data and 'placement-restriction.parens-dot' or 'placement-restriction.parens' + return {'placement-restriction.header', placement_restriction_description_helper(1, restriction, parens)} +end + +py.stringsplit = function(s, sep) + if sep == nil then sep = '%s' end + local t = {} + for str in string.gmatch(s, '([^' .. sep .. ']+)') do + table.insert(t, str) + end + return t +end + +py.distance = function(x, y) + return (x ^ 2 + y ^ 2) ^ 0.5 +end + +py.tints = { + {r = 1.0, g = 1.0, b = 0.0, a = 1.0}, + {r = 1.0, g = 0.0, b = 0.0, a = 1.0}, + {r = 0.223, g = 0.490, b = 0.858, a = 1.0}, + {r = 1.0, g = 0.0, b = 1.0, a = 1.0} +} + +py.light_tints = {} +for i, tint in pairs(py.tints) do + py.light_tints[i] = {} + for color, amount in pairs(tint) do + py.light_tints[i][color] = (amount - 0.5) / 2 + 0.5 + end + py.light_tints[i].a = 1 +end + +---@param color Color +---@return Color +function py.color_normalize(color) + local r = color.r or color[1] + local g = color.g or color[2] + local b = color.b or color[3] + local a = color.a or color[4] or 1 + if r > 1 then r = r / 255 end + if g > 1 then g = g / 255 end + if b > 1 then b = b / 255 end + if a > 1 then a = a / 255 end + return {r = r, g = g, b = b, a = a} +end + +---@param a Color +---@param b Color +---@param percent number +---@return Color +function py.color_combine(a, b, percent) + a = py.color_normalize(a) + b = py.color_normalize(b) + + return { + r = a.r * percent + b.r * (1 - percent), + g = a.g * percent + b.g * (1 - percent), + b = a.b * percent + b.b * (1 - percent), + a = a.a * percent + b.a * (1 - percent) + } +end + +if script then + _G.gui_events = { + [defines.events.on_gui_click] = {}, + [defines.events.on_gui_confirmed] = {}, + [defines.events.on_gui_text_changed] = {}, + [defines.events.on_gui_checked_state_changed] = {}, + [defines.events.on_gui_selection_state_changed] = {}, + [defines.events.on_gui_checked_state_changed] = {}, + [defines.events.on_gui_elem_changed] = {}, + [defines.events.on_gui_value_changed] = {}, + [defines.events.on_gui_location_changed] = {}, + [defines.events.on_gui_selected_tab_changed] = {}, + [defines.events.on_gui_switch_state_changed] = {} + } + local function process_gui_event(event) + if event.element and event.element.valid then + for pattern, f in pairs(gui_events[event.name]) do + if event.element.name:match(pattern) then f(event); return end + end + end + end + + for event, _ in pairs(gui_events) do + script.on_event(event, process_gui_event) + end +end + +function py.reseed(generator) + return game.create_random_generator(generator(341, 2147483647)) +end + +function py.find_grandparent(element, name) + while element do + if element.name == name then return element end + element = element.parent + end + error('Could not find parent gui element with name: ' .. name) +end + +-- data stage. adds a glow layer to any prototype with the 'icons' field. +function py.make_item_glowing(prototype) + if not prototype then + error('No prototype provided') + end + if prototype.pictures then + for _, picture in pairs(prototype.pictures) do + picture.draw_as_glow = true + end + return + end + if prototype.icon and not prototype.icons then + prototype.icons = {{icon = prototype.icon, icon_size = prototype.icon_size, icon_mipmaps = prototype.icon_mipmaps}} + prototype.icon = nil + end + if not prototype.icons then + error('No icon found for ' .. prototype.name) + end + local pictures = {} + for _, picture in pairs(table.deepcopy(prototype.icons)) do + picture.draw_as_glow = true + local icon_size = picture.icon_size or prototype.icon_size + picture.filename = picture.icon + picture.shift = {0, 0} + picture.width = icon_size + picture.height = icon_size + picture.scale = 16 / icon_size + picture.icon = nil + picture.icon_size = nil + picture.icon_mipmaps = nil + pictures[#pictures + 1] = picture + end + prototype.pictures = pictures +end + +function py.starts_with(str, start) + return str:sub(1, #start) == start +end + +local seconds_per_year = 60 * 60 * 24 * 365.25 +local seconds_per_day = 60 * 60 * 24 +local seconds_per_hour = 60 * 60 +local seconds_per_minute = 60 +function py.format_large_time(seconds) + if not seconds then return end + local result = '' + if seconds >= seconds_per_year then + local years = math.floor(seconds / seconds_per_year) + result = result .. years .. 'y ' + seconds = seconds % seconds_per_year + end + + if seconds >= seconds_per_day or result ~= '' then + local days = math.floor(seconds / seconds_per_day) + result = result .. days .. 'd ' + seconds = seconds % seconds_per_day + end + + if seconds >= seconds_per_hour or result ~= '' then + local hours = math.floor(seconds / seconds_per_hour) + result = result .. hours .. ':' + seconds = seconds % seconds_per_hour + end + + local minutes = math.floor(seconds / seconds_per_minute) + if minutes < 10 then + result = result .. '0' .. minutes .. ':' + else + result = result .. minutes .. ':' + end + seconds = seconds % seconds_per_minute + + seconds = math.ceil(seconds) + if seconds < 10 then + result = result .. '0' .. seconds + else + result = result .. seconds + end + + return result +end + +function py.reverse(t) + for i = 1, #t / 2, 1 do + t[i], t[#t - i + 1] = t[#t - i + 1], t[i] + end + return t +end + +py.opposite_direction = { + [defines.direction.north] = defines.direction.south, + [defines.direction.northeast] = defines.direction.southwest, + [defines.direction.east] = defines.direction.west, + [defines.direction.southeast] = defines.direction.northwest, + [defines.direction.south] = defines.direction.north, + [defines.direction.southwest] = defines.direction.northeast, + [defines.direction.west] = defines.direction.east, + [defines.direction.northwest] = defines.direction.southeast +} + +py.invert_table = function(t) + local inverted = {} + for k, v in pairs(t) do + inverted[v] = k + end + return inverted +end + +local noise = require 'noise' +local tne = noise.to_noise_expression + +py.set_noise_constant = function(i, surface, data) + local mgs = surface.map_gen_settings + mgs.autoplace_controls = mgs.autoplace_controls or {} + mgs.autoplace_controls['py-autoplace-control-' .. i] = mgs.autoplace_controls['py-autoplace-control-' .. i] or {} + mgs.autoplace_controls['py-autoplace-control-' .. i].richness = data + surface.map_gen_settings = mgs +end + +py.get_noise_constant = function(i) + return noise.get_control_setting('py-autoplace-control-' .. i).richness_multiplier +end + +py.basis_noise = function(x, y, seed, zoom) + return { + type = 'function-application', + function_name = 'factorio-basis-noise', + arguments = { + x = x, + y = y, + seed0 = tne(noise.var('map_seed')), + seed1 = tne(seed), + input_scale = tne(0.9999728452) / zoom, + output_scale = tne(1.2 / 1.7717819213867) + } + } +end + +--[[ +use this to execute a script after a delay +example: + +py.delayed_functions.my_delayed_func = function(param1, param2, param3) ... end +py.execute_later('my_delayed_func', 60, param1, param2, param3) + +The above code will execute my_delayed_func after waiting for 60 ticks +]] +local powers_of_two = {} +for i = 0, 20 do + powers_of_two[i] = 2 ^ i +end +if data then + local delays = {} + for _, n in pairs(powers_of_two) do + delays[#delays + 1] = { + name = 'py-ticked-script-delay-' .. n, + type = 'flying-text', + time_to_live = n, + speed = 0, + } + end + data:extend(delays) +else + py.delayed_functions = {} + py.on_event('on_init', function() + global._delayed_functions = global._delayed_functions or {} + end) + function py.execute_later(function_key, ticks, ...) + if ticks < 1 or ticks % 1 ~= 0 then error('Invalid delay: ' .. ticks) end + local highest = 1 + for _, n in pairs(powers_of_two) do + if n <= ticks then + highest = n + else break end + end + local flying_text = game.surfaces.nauvis.create_entity{ + name = 'py-ticked-script-delay-' .. highest, + position = {0, 0}, + create_build_effect_smoke = false, + text = '' + } + global._delayed_functions[script.register_on_entity_destroyed(flying_text)] = {function_key, ticks - highest, {...}} + end + py.on_event(defines.events.on_entity_destroyed, function(event) + local data = global._delayed_functions[event.registration_number] + if not data then return end + global._delayed_functions[event.registration_number] = nil + + local function_key = data[1] + local ticks = data[2] + + if ticks == 0 then + local f = py.delayed_functions[function_key] + if not f then error('No function found for key: ' .. function_key) end + f(table.unpack(data[3])) + else + py.execute_later(function_key, ticks, table.unpack(data[3])) + end + end) +end + +return py \ No newline at end of file From cae4a914d679fbc1ea6aac87626b2538573b65aa Mon Sep 17 00:00:00 2001 From: notnotmelon Date: Mon, 3 Jun 2024 13:24:09 -0500 Subject: [PATCH 09/25] Added an incompatiblity with the science-pack-dependencies mod. --- changelog.txt | 1 + info.json | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 10b18bf..b2f1d68 100644 --- a/changelog.txt +++ b/changelog.txt @@ -3,6 +3,7 @@ Version: 0.2.24 Date: 2024-??-?? Changes: - Whenever pyPP encounters a dependency cycle, it will now print the items involved in the cycle to the logs. + - Added an incompatiblity with the science-pack-dependencies mod. (https://github.com/pyanodon/pybugreports/issues/505) --------------------------------------------------------------------------------------------------- Version: 0.2.23 Date: 2024-4-25 diff --git a/info.json b/info.json index dd9f660..019e0de 100644 --- a/info.json +++ b/info.json @@ -31,6 +31,7 @@ "? DeadlocksStackingForPyanadon", "? LightedPolesPlus", "! ResearchFog", + "! science-pack-dependencies", "? jetpack" ], "package": { From 70f04100c48fe9efd6221125102e4ff3f7dd7ab2 Mon Sep 17 00:00:00 2001 From: przemo1232 <79700515+przemo1232@users.noreply.github.com> Date: Tue, 2 Jul 2024 22:10:58 +0200 Subject: [PATCH 10/25] adjust entity craft times --- changelog.txt | 1 + data-final-fixes.lua | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/changelog.txt b/changelog.txt index b2f1d68..207beaf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,6 +4,7 @@ Date: 2024-??-?? Changes: - Whenever pyPP encounters a dependency cycle, it will now print the items involved in the cycle to the logs. - Added an incompatiblity with the science-pack-dependencies mod. (https://github.com/pyanodon/pybugreports/issues/505) + - adjusted most entity recipe times for easier copy pasting to requester chests --------------------------------------------------------------------------------------------------- Version: 0.2.23 Date: 2024-4-25 diff --git a/data-final-fixes.lua b/data-final-fixes.lua index b73d7f4..028be3e 100644 --- a/data-final-fixes.lua +++ b/data-final-fixes.lua @@ -342,6 +342,11 @@ end for _, type in pairs{'furnace', 'assembling-machine'} do for _, prototype in pairs(data.raw[type]) do prototype.match_animation_speed_to_activity = false + local name = prototype.name + local tier = tonumber(string.sub(name, -1)) + if tier and data.raw.recipe[name] and data.raw.recipe[name].energy_required == .5 then + data.raw.recipe[name].energy_required = tier + end end end From 7b1ae6891696a8c3ecd8ef6170f2dfa34276262e Mon Sep 17 00:00:00 2001 From: przemo1232 <79700515+przemo1232@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:17:24 +0200 Subject: [PATCH 11/25] craft times part 2 --- data-final-fixes.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/data-final-fixes.lua b/data-final-fixes.lua index 028be3e..4f401f8 100644 --- a/data-final-fixes.lua +++ b/data-final-fixes.lua @@ -342,9 +342,15 @@ end for _, type in pairs{'furnace', 'assembling-machine'} do for _, prototype in pairs(data.raw[type]) do prototype.match_animation_speed_to_activity = false + end +end + +-- infrastructure times scale with tiers, makes pasting to requester chests and buffering inside assemblers better +for _, type in pairs{'furnace', 'assembling-machine', 'mining-drill', 'lab'} do + for _, prototype in pairs(data.raw[type]) do local name = prototype.name - local tier = tonumber(string.sub(name, -1)) - if tier and data.raw.recipe[name] and data.raw.recipe[name].energy_required == .5 then + local tier = tonumber(string.sub(name, -1)) or 1 + if data.raw.recipe[name] and data.raw.recipe[name].energy_required == .5 then data.raw.recipe[name].energy_required = tier end end From 911aafb712c8fa9c76956151c16d7d60621e8082 Mon Sep 17 00:00:00 2001 From: kingarthur91 Date: Mon, 29 Jul 2024 23:27:31 -0400 Subject: [PATCH 12/25] version and changelog --- changelog.txt | 2 +- info.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 207beaf..fdf87b6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ --------------------------------------------------------------------------------------------------- Version: 0.2.24 -Date: 2024-??-?? +Date: 2024-7-29 Changes: - Whenever pyPP encounters a dependency cycle, it will now print the items involved in the cycle to the logs. - Added an incompatiblity with the science-pack-dependencies mod. (https://github.com/pyanodon/pybugreports/issues/505) diff --git a/info.json b/info.json index 019e0de..8a74c24 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "name": "pypostprocessing", - "version": "0.2.23", + "version": "0.2.24", "factorio_version": "1.1", "title": "Pyanodons Post-processing", "author": "Pyanodon, Shadowglass", From a659a042295ea2df109451d0b681f039cb9c4d66 Mon Sep 17 00:00:00 2001 From: przemo1232 <79700515+przemo1232@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:17:07 +0200 Subject: [PATCH 13/25] fix possible 0s craft time --- data-final-fixes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data-final-fixes.lua b/data-final-fixes.lua index 4f401f8..ac36a57 100644 --- a/data-final-fixes.lua +++ b/data-final-fixes.lua @@ -351,7 +351,7 @@ for _, type in pairs{'furnace', 'assembling-machine', 'mining-drill', 'lab'} do local name = prototype.name local tier = tonumber(string.sub(name, -1)) or 1 if data.raw.recipe[name] and data.raw.recipe[name].energy_required == .5 then - data.raw.recipe[name].energy_required = tier + data.raw.recipe[name].energy_required = math.max(tier, 1) end end end From 45618551636aba957d0ef38225fbcbbda7d590aa Mon Sep 17 00:00:00 2001 From: Alex ten Brink Date: Mon, 29 Jul 2024 22:27:29 +0200 Subject: [PATCH 14/25] DRAFT added new auto-tech --- prototypes/new_auto_tech/README.txt | 158 +++++++++++ .../new_auto_tech/ammo_category_node.lua | 9 + prototypes/new_auto_tech/data/deque.lua | 51 ++++ prototypes/new_auto_tech/electricity_node.lua | 9 + .../new_auto_tech/electricity_verbs.lua | 3 + prototypes/new_auto_tech/entity_node.lua | 55 ++++ prototypes/new_auto_tech/entity_verbs.lua | 4 + .../new_auto_tech/equipment_grid_node.lua | 9 + prototypes/new_auto_tech/fluid_fuel_node.lua | 9 + prototypes/new_auto_tech/fluid_fuel_verbs.lua | 3 + prototypes/new_auto_tech/fluid_node.lua | 14 + prototypes/new_auto_tech/fluid_verbs.lua | 3 + .../new_auto_tech/fuel_category_node.lua | 9 + .../new_auto_tech/fuel_category_verbs.lua | 3 + prototypes/new_auto_tech/item_node.lua | 22 ++ prototypes/new_auto_tech/item_verbs.lua | 3 + prototypes/new_auto_tech/new_auto_tech.lua | 192 ++++++++++++++ prototypes/new_auto_tech/node_types.lua | 15 ++ prototypes/new_auto_tech/object_node_base.lua | 247 ++++++++++++++++++ .../new_auto_tech/recipe_category_node.lua | 9 + .../new_auto_tech/recipe_category_verbs.lua | 3 + prototypes/new_auto_tech/recipe_node.lua | 24 ++ prototypes/new_auto_tech/recipe_verbs.lua | 3 + .../new_auto_tech/resource_category_node.lua | 9 + .../new_auto_tech/resource_category_verbs.lua | 3 + prototypes/new_auto_tech/start_node.lua | 9 + prototypes/new_auto_tech/technology_node.lua | 23 ++ 27 files changed, 901 insertions(+) create mode 100644 prototypes/new_auto_tech/README.txt create mode 100644 prototypes/new_auto_tech/ammo_category_node.lua create mode 100644 prototypes/new_auto_tech/data/deque.lua create mode 100644 prototypes/new_auto_tech/electricity_node.lua create mode 100644 prototypes/new_auto_tech/electricity_verbs.lua create mode 100644 prototypes/new_auto_tech/entity_node.lua create mode 100644 prototypes/new_auto_tech/entity_verbs.lua create mode 100644 prototypes/new_auto_tech/equipment_grid_node.lua create mode 100644 prototypes/new_auto_tech/fluid_fuel_node.lua create mode 100644 prototypes/new_auto_tech/fluid_fuel_verbs.lua create mode 100644 prototypes/new_auto_tech/fluid_node.lua create mode 100644 prototypes/new_auto_tech/fluid_verbs.lua create mode 100644 prototypes/new_auto_tech/fuel_category_node.lua create mode 100644 prototypes/new_auto_tech/fuel_category_verbs.lua create mode 100644 prototypes/new_auto_tech/item_node.lua create mode 100644 prototypes/new_auto_tech/item_verbs.lua create mode 100644 prototypes/new_auto_tech/new_auto_tech.lua create mode 100644 prototypes/new_auto_tech/node_types.lua create mode 100644 prototypes/new_auto_tech/object_node_base.lua create mode 100644 prototypes/new_auto_tech/recipe_category_node.lua create mode 100644 prototypes/new_auto_tech/recipe_category_verbs.lua create mode 100644 prototypes/new_auto_tech/recipe_node.lua create mode 100644 prototypes/new_auto_tech/recipe_verbs.lua create mode 100644 prototypes/new_auto_tech/resource_category_node.lua create mode 100644 prototypes/new_auto_tech/resource_category_verbs.lua create mode 100644 prototypes/new_auto_tech/start_node.lua create mode 100644 prototypes/new_auto_tech/technology_node.lua diff --git a/prototypes/new_auto_tech/README.txt b/prototypes/new_auto_tech/README.txt new file mode 100644 index 0000000..d9e498f --- /dev/null +++ b/prototypes/new_auto_tech/README.txt @@ -0,0 +1,158 @@ +The autotech module has several main goals. The first two are shared with the old autotech, the last ones are new: +- adapt the tech tree so if you research a tech, you will be able to use all the recipes from that tech right away. +- change the research costs to slowly ramp up as you get further in the tech tree. +- report all unreachable items/recipes/techs, and in particular whether the victory tech is reachable. +- enforce that if a tech dependency of a tech has a particular science pack in its costs, then that tech will have at least the same science packs (monotonicity of science pack costs, as checked by the python tests). +- report as much useful information as possible to help development. Do cheap checks for errors early. Have an option to turn on verbose logging to debug more complicated issues. +- allow the code to be run in a unit test scenario on a dumped version of the Factorio data, to help development and allow automated testing +- support more advanced Py features, like inter-surface logistics, differing tech trees based on a starting scenario, optional side techs, etc + +There are two versions of this explanation. The first gives all the details including a rationale but assumes a computer science background, the second only assumes you know what a directed graph is and is mainly tailored to users of autotech. + +== Explanation assuming a computer science background == + +The second point, changing the tech costs, is fairly easy, so let's get it out of the way first. The first phase results in a tech tree with a linear ordering. We can use this linear ordering to find the longest path from every science pack to every tech as follows. For every science pack, loop over the techs in this linear order, and then find the longest distance by taking the highest longest distance of all its dependencies plus one (zero if it's the source, infinity if it's not the source and has no dependencies or if it doesn't have the science pack as a cost). For every tech we can then compute the tech costs by taking these longest distances for the science packs it costs and inputting those into the science cost formula. This generalises the old autotech code where there was a linear ordering between science packs (automation, p1, logi, p2, etc). This generalisation allows for new side science packs such as the energy science pack. + +My proposal for a tech cost formula is to scale the overall tech cost with the smallest longest distance among the science packs. Furthermore, the individual pack count can be chosen by taking the longest distances from smallest to largest, and then assigning a pack count to each, higher counts for the farther away science packs. This will probably need some tweaking in practice. + +The first point, adapting the tech tree, is tricky, hence this README. Let's first list the phases of autotech, and then explain the steps one by one: +1 construct recipe graph +2 linearise the recipe graph, ignoring cycles + 2a canonise all choices along the way, report these choices + 2b error if victory tech unreachable from starting tech + 2c report but don't error if other things are unreachable or if a choice cannot be made due to cycles + 2d report but don't error on cycles found in this graph +3 construct true tech dependency graph from unlinearised but canonised recipe graph +4 linearise resulting tech graph, error out on cycle and report it +5 compute transitive reduction of unlinearised true tech dependency graph +6 attach tech costs to techs +7 edit Factorio tech dependencies to match unlinearised reduced true tech dependency graph and match new tech costs + +A cache file can replace all these steps. Note that cache files may now remove dependencies due to the transitive reduction, which the old autotech did not do. We can leave a setting to allow (modified versions of) steps 4, 5 and 6 to be executed despite a cache file being used for those that want autotech to affect other mods. + +The 'recipe graph' is a graph where the nodes are Factorio concepts, such as items, recipes, fluids, recipe categories, etc, and the edges are Factorio dependencies between these. For example, the node 'iron plate' may have an edge 'is crafted by' to the node 'iron plate smelting'. (yes, this means the name 'recipe graph' is incorrect, as it contains things that are not recipes too.) We generate this graph in two phases: we first create all the nodes, and then we create all the edges. The reason for the two phases is that during the edge phase we know all nodes already exist and we can just look them up by name, so we don't need to worry about the order we generate the nodes or edges in. + +There is one major complication however. We often have several ways to get something, for example, there could be several recipes to make an item. This means we have to make this recipe graph a 'disjunctive graph'. In such a graph, an edge from node A has multiple targets B_i, instead of just one target B like in a normal graph, representing that node A needs at least one of the B_i nodes. For example, the Ralesia item requires one or more of its recipes to be unlocked (one edge, several targets), as well as the crafting category (one edge, one target), so it has at least these two edges. More formally, every node depends on a conjunction of a disjunction of targets (an AND with OR arguments), which is sort of equivalent to a conjunctive normal form. Indeed, this makes a disjunctive graph effectively a monotone boolean circuit with unbounded fan-in and fan-out (only OR and AND gates, no NOT gates). (don't worry if you don't know what these are) + +Let's now define the 'true tech dependency graph' as the graph where the nodes are the techs, and the edges are the 'true' dependencies between the techs, that is, the dependencies according to the recipe graph. For example, if tech A gives a building that requires the item Intermetallics, then it has a 'true dependency' on the tech that gives you the recipe to make Intermetallics. We also include the already existing tech dependencies and science pack requirements in this graph. This essentially comes down to doing a reachability query for every tech on the recipe graph. + +It's tempting to now try to compute this true tech dependency graph directly from the recipe graph. Unfortunately, there are two major problems, which both stem from the fact that this computed true tech graph will also be a disjunctive graph. First, turning a monotone boolean circuit into a conjunctive normal form (which is basically what we're trying to do) involves exponential blowup of the formula size (it's NP-hard). In other words, your true tech graph may be exponentially larger than the recipe graph. You'd have to add a bunch of heuristics to try and stop the exponential blowup for practical examples, but that's a lot of complicated code that will not necessarily work, and it's going to be slow whatever you do. + +Second, there's a fundamental problem with this true tech graph: Factorio does not allow disjunctive dependencies between techs. This means we'll have to make a choice for every edge no matter how we go about it, and just computing the disjunctive true tech dependency graph does not help us solve this problem at all. Luckily, this second problem also shows us the path how to resolve both problems. + +Since we have to make a choice at some point anyway, why not make our choice in the recipe graph already? We can just linearise the recipe graph, and for every disjunctive edge, choose the target that's the earliest in the linear ordering. We'll call this 'canonising' of the graph, where we choose a 'canon' target for every disjunctive edge. With this canonisation, our disjunctive recipe graph turns into a normal graph, so computing a normal true tech dependency graph out of this is straightforward using for example BFS. Note that this solution is not perfect: since there are several possible linearisations, it's possible we choose a linearisation that leads to a canonicalisation that leads to a cycle in the true tech dependency graph while a different linearisation would have led to no cycle in the tech dependency graph. This is what the old autotech also did, and I expect this to not be a problem in practice. Worst case, we can add a mechanism to force a specific canonicalisation. + +With that, the major problem is out of the way, and all that remains are implementation details. Note that 'report' and 'log' are different in that there's different settings to turn on either one, and that 'report's indicates something is potentially wrong. + +Step 1: the code will log the entire recipe graph, to make it clear how it interpreted the code. Two details about the recipe graph: we'll need a file with 'scripted dependencies' like guano, and we'll need to respect the flag we have for e.g. soot sorting that should not lead to seeing soot as a source of silver. + +Step 2: we apply the standard Kahn's algorithm for linearisation, which can be adapted easily for disjunctive graphs, see here: https://math.stackexchange.com/q/2449379. We linearise as much as possible and leave any cycles in the graph for later steps. We log the entire linear ordering. + +Step 2a: we partially canonise the choices due to the partial linear ordering from earlier. All choices are logged. + +Step 2b: we do a BFS from the starting node which only uses linearised nodes, and see if the victory tech is reachable. This step comes early to aid the development process and error out as early as possible. + +Step 2c: we can use the BFS from 2b to see what nodes are not reachable and report those. We can also report all the edges that did not get a canon target. + +Step 2d: We can report a cycle with the standard cycle reporting algorithm: start in any unlinearised node and repeatedly follow any edge while keeping track of what nodes you've visited until you reach a node you've visited before, then report the stack of nodes you've followed until you get back to that node. This will work even in a disjunctive graph. Note that none of these reports are errors - a bunch of these could be due to later Py mods disabling earlier items etc. Also note we can probably report a cycle for every connected component of the unlinearised part of the graph. Reporting more than one is probably not a good idea, because if you have one you tend to have many with duplicate nodes. Worst case, we can report a cycle, delete all the nodes involved, then continue Kahn's algorithm again, report another cycle, etc. + +Step 3: the entire tech graph is logged. An inefficient way to construct the graph this is to perform a BFS from every tech, and then make an edge for every reachable tech. We can make two performance improvements: + +The first optimisation is to make use of the observation that the BFS will not just find the dependent techs but all their transitive tech dependencies too, and that this is unnecessary. If we do a BFS on tech A, and we find tech B as dependency, which itself has tech C as dependency, then we don't need to add C as a dependency for A, because the BFS from B will already find C. We can achieve this by breaking up the tech and recipe nodes in the recipe graph. We make two nodes per tech: the 'unlock' node and the 'dependency' node. We do the same for recipes: we make an 'unlock' and 'dependency' node per recipe. The idea is that the BFS will start at the 'unlock' tech node, jump to the 'unlock' recipe nodes, traverse the rest of the graph, end up at the 'dependency' nodes for the required recipes, and then jump to the 'dependency' nodes for the techs. These 'dependency' nodes don't have any edges for the BFS to follow afterwards, so the BFS won't try to find the transitive tech dependencies. + +The second optimisation is to make use of the linearisation to compute all the tech dependencies simultaneously instead of in many BFS passes. We can go in linear order through the nodes and propagate the techs that every node depends on by looking at all its edges and merging the tech dependencies of their targets. Note that this is not better in the worst case, if techs depend on a linear number of other techs, because then most of your time you're merging dependencies no matter whether you're using BFSses or not. + +Step 4: just like step 2d, it's possible to report a cycle in the tech graph. + +Step 5: the transitive reduction of the graph is the graph where you take away the largest number of edges without changing the transitive dependencies. For example, if tech A depends on C, but A also depends on B which depends on C already, then that dependency from A to C is pointless and can be removed. This is a slightly expensive step to compute but should make the tech tree more readable. We're already past all the steps that can give errors at this point though. There'll be a setting to turn this off it it takes too much time. All removed edges are logged. + +Steps 6 and 7 are straightforward and discussed earlier respectively. + +== Explanation without assuming a computer science background == + +=== Linear orderings === + +The autotech code makes heavy use of a concept called a 'linear ordering', so we're going to explain that first. Let's take the Factorio example of the automation science pack, which needs an iron wheel and copper plate to make. We can make a dependency graph between these items for the base game: + +- iron ore -> iron plate +- copper ore -> copper plate +- iron plate -> iron wheel +- iron wheel -> automation science pack +- copper plate -> automation science pack + +Thse five dependencies define what is called a 'partial ordering' on these nodes. It's an ordering because it tells you what nodes need to 'happen' before what other nodes, and it's partial because it leaves freedom as to the exact orderings that conform to the partial ordering. For example, these are some orderings that respect this partial ordering: + +- iron ore, iron plate, iron wheel, copper ore, copper plate, automation science pack +- iron ore, copper ore, iron plate, copper plate, iron wheel, automation science pack +- copper ore, copper plate, iron ore, iron plate, iron wheel, automation science pack + +These three examples each represent a specific ways to get to the automation science pack from scratch. They respect the partial ordering because for every dependency A -> B in the partial ordering, A comes before B in the examples. However, an ordering where for example the iron wheel comes first does not respect the partial ordering, and indeed it cannot correspond to a way to get to the automation science pack. + +Actually, these three examples are in fact also examples of a 'linear order', because they fully pin down the ordering of events, as opposed to a partial order, which leaves some freedom. It's called 'linear' because if you consider it a graph, it's just a line. There's actually many names for it, such as 'complete ordering' or 'total ordering'. Every partial order can be respected by a lot of different linear orders. + +Now, if you think about that dependency graph from earlier as a graph, you may notice it's a directed acyclic graph. This is true in general: every directed acyclic graph corresponds to a partial ordering, they're basically the same thing but in a different context. Cycles are obviously bad for orderings, because no 'solutions' exist - if A depends on B depends on C depends on A, then no ordering of A, B and C can respect those dependencies, hence the need to have an acyclic graph. + +'Linearisation' is the process of turning a partial order into a linear order. Intuitively, this means we pick a possible 'path' of the nodes in the dependency graph that respects the partial ordering. Usually, linearisation algorithm do a bit more than just compute a linearisation: they also detect cycles. They start with a directed graph, detect cycles and if no cycle exist they output a linearisation. Note there can be multiple or even many possible linearisations of a partial order (as seen in our example): linearisation arbitrarily picks one of them. + +=== Disjunctive graphs === + +The second concept important to autotech is a 'disjunctive graph'. It solves the problem that in Factorio, you often have a choice to make something, so a strict dependency graph like we presented earlier doesn't work. For example: in vanilla Factorio, water can come from an offshore pump but also from emptying a water barrel. Petroleum can come from basic oil processing, advanced oil processing, light oil cracking and emptying a petroleum barrel. This is even more egregious in Pyanodon, because items can have many recipes and there will be playthroughs A and B that obtain an item exclusively through differing recipes. The strict dependencies as described in the previous section are therefore not good enough - you cannot say that making Vrauks necessarily comes from the simpler or the more complex recipe to make it. + +Enter the disjunctive graph. Instead of having edges of the form A -> B, so "You need B to make A", you can have edges of the form A -> {B, C}, so "You need B _or_ C to make A". Conceptually, this is pretty simple, but it makes autotech significantly harder. The good news is that linearisation, cycle detection and reachability (eg 'can I get from the starting point to the final technology') are all easy with disjunctive graphs, as the normal algorithms keep working with minimal changes. The bad news is that the ultimate thing we'd like to know, namely 'what techs do I need in order to be able to use a recipe', becomes an algorithm with an exponential runtime if we want to compute it fully correctly. Luckily, we can take a shortcut that will work in all practical cases. + +=== Recipe graph and tech graph === + +Let's first describe the exponential algorithm, since it helps understanding the final algorithm. We will skip the explanation why the last step is exponential. We first make the 'recipe graph': we make a node for every Factorio 'thing', so items, recipes, crafting categories, entities, fuel categories, etc. We then add edges and disjunctive edges representing how Factorio works. For example, every item has a disjunctive edge to the recipes that make it, the ore entities that it can be mined from or the fuels it is the burnt result from, etc. Every recipe has a normal edge to the crafting category for that recipe, which in turn has a disjunctive edge to all the entities that can craft that crafting category, etc. This makes the recipe graph a disjunctive graph that describes the partial order of all possible playthroughs of Factorio (given a modset). Every actual playthrough is a linear ordering of the Factorio 'things' that adheres to the recipe graph. + +Yes, the name 'recipe graph' is not correct as it contains more than just recipes, but calling it a 'Factorio thing graph' doesn't sound as good. + +Making the recipe graph is fast and super useful, so we will also do it in the actual final algorithm. It allows us to figure out a bunch of things: what items and recipes are impossible to get in a playthrough, either because they are fundamentally unreachable or because they're part of a dependency loop? Can we research the victory tech at all? Note that not all unreachable items are bad: they may just be disabled items that are not actually needed, for example 'organic material' in Py before PyAL, which gets disabled and replaced with 'biomatter' when PyAL is turned on. The new autotech reports these unreachable things in the log, but doesn't error out on them, which should help debug issues. + +The ultimate thing that autotech aims to figure out is what techs you need in order to be able to run all the recipes unlocked by a tech. We therefore want to make a second graph, the 'technology graph', where the nodes are techs, and the edges are these dependencies. To spell that out a bit more: let's say technology A unlocks recipe X. The aim is that when you unlock A, no matter what techs you've chosen before, you should be able to use the recipes you have at that point to be able to run recipe X in your factory. You need to be able to make every ingredient of X, you need to be able to have a building that can run X, etc. + +This is a question that the recipe graph can answer. However, both reachability and cycle detection are not good enough for our goals. Reachability especially just tells you that there is _some_ way to go from a tech to a recipe, but it's allowed to assume you research other techs, so it's not good enough. Similarly, linearisation will just tell you _some_ set of techs that will allow you to use that recipe, but not the minimal number of techs. There is an algorithm that computes this 'technology graph' correctly, however, I will not describe it here due to the problems described in the next paragraph. + +There are two problems with this approach (not just the algorithm): the first is that this technology graph is likely to be exponentially large (no matter the algorithm used to compute it), and the second is that it's still a disjunctive graph, so it allows for choices between tech dependencies, and it's unclear what to do with this information. Factorio does not allow disjunctive dependencies between techs - you can't say that technology A can be unlocked by researching either technology B or C - so we can't just apply the technology graph to Factorio. We'd have to turn it into a non-disjunctive dependency graph first somehow. + +=== The solution === + +The solution to the above problem is to de-disjunctify the recipe graph. The plan is to pick for every disjunctive edge a 'canonical' choice, and transform the edge into a normal one. For example, for petroleum in the base game, we can get it via several recipes like cracking, but we will choose basic oil processing as the 'canonical' choice, because it's the first recipe you can get that can make petroleum. Furthermore, all the other recipes that make petroleum need petroleum themselves to research (for advanced oil processing) or to make one of the ingredients (barrel of petroleum), so they are not valid canonical choices. The resulting non-disjunctive recipe graph no longer allows all possible playthroughs of the modpack, but it still has useful information about the dependencies between Factorio things. Examples where information is 'lost' are basically those where multiple options exist to get something, for example multiple recipes to make PyAL animals, or things like acid gas which you can obtain in several ways. In practice I don't think this matters though. + +After picking a canonical choice for all edges in the recipe graph, it has become a normal directed graph, and we can compute the tech graph from it much faster than in exponential time. This tech graph is now a normal graph as well, which means we can use it as a basis for computing tech dependencies as intended. + +We do have to pick canonical chocies in such a way that the graph stays useful as mentioned earlier. The way we will do this is by computing a linear order first, and then resolving all disjunctive edges according to the first element in the computed linear order. In other words, for the case of choosing which recipe to use for your first Vrauk, we look at what the linear order did, and then choose that recipe. The advantage of this is that that linear order will still be a valid linear order on the resulting graph, so if the victory tech was reachable before, it would still be reachable after this de-disjunctivication. + +So, in summary: +- compute the disjunctive recipe graph +- compute a linear order on this graph +- canonise the disjunctive recipe graph into a normal recipe graph according to the linear order +- compute the tech graph out of this normal recipe graph +- correct Factorio tech graph to match this tech graph, adapt tech costs + +That's the gist of the algorithm. There's some more smaller details we'll go over now. + +Two details about the recipe graph: we'll need a file that adds 'scripted dependencies' like guano to the recipe graph, and we'll need to respect the flag we have for e.g. soot sorting that should not lead to seeing soot as a source of silver. + +=== Logging versus reporting versus error === + +The new autotech will have 3 reporting levels. Error means something is broken and we cannot continue, which only happens in two scenarioes I believe: +- the victory tech is unreachable +- there is a dependency loop between technologies + +The second level is 'reporting', which is when it finds something suspicious but not broken, for example unreachable items or dependency loops outside the tech graph. These will be printed but will not stop the autotech process. + +The third level is 'logging', which is where it will print out every decision it makes. It will print out the full recipe graph, linear order, canonicalisation choices, etc. This should help in investigating dependency loops. + +I have an algorithm to report a dependency loop when one is found, which should make debugging a lot easier. + +=== Transitive reduction === + +A lot of people know the term 'transitive closure', but that concept has a brother called 'transitive reduction'. The transitive reduction of a graph A is the graph where the most edges are removed without changing the reachability in the graph. For example, if A depends on B, B depends on C and A depends on C, then you can remove the 'A depends on C', because A can already reach C via B. This basically removes 'clutter' edges in the tech graph that don't actually do anything. + +I plan to have this be applied to the tech graph computed by autotech. This means it may actually end up _removing_ superfluous dependencies between techs, which is something the current autotech does not do. + +=== Unit tests === + +I want to make code to dump the data.raw table into a JSON file, so we can then run autotech on just this JSON file. This has several advantages: +- easier development, because you don't have to start the game every time you want to run the code +- automated regression testing becomes easier - you can just run some Python tests if you're modifying autotech after it already works diff --git a/prototypes/new_auto_tech/ammo_category_node.lua b/prototypes/new_auto_tech/ammo_category_node.lua new file mode 100644 index 0000000..0c9b1f0 --- /dev/null +++ b/prototypes/new_auto_tech/ammo_category_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local ammo_category_node = object_node_base:create_object_class("ammo category", node_types.ammo_category_node) + +function ammo_category_node:register_dependencies(nodes) +end + +return ammo_category_node diff --git a/prototypes/new_auto_tech/data/deque.lua b/prototypes/new_auto_tech/data/deque.lua new file mode 100644 index 0000000..926d5d5 --- /dev/null +++ b/prototypes/new_auto_tech/data/deque.lua @@ -0,0 +1,51 @@ +local push_left = function(self, value) + local first = self.first - 1 + self.first = first + self[first] = value +end + +local push_right = function(self, value) + local last = self.last + 1 + self.last = last + self[last] = value +end + +local pop_left = function(self) + local first = self.first + if first > self.last then error("self is empty") end + local value = self[first] + self[first] = nil + self.first = first + 1 + return value +end + +local pop_right = function(self) + local last = self.last + if self.first > last then error("self is empty") end + local value = self[last] + self[last] = nil + self.last = last - 1 + return value +end + + +local is_empty = function(self) + return self.last + 1 == self.first +end + +local methods = { + push_right = push_right, + push_left = push_left, + pop_right = pop_right, + pop_left = pop_left, + is_empty = is_empty, +} + +local new = function() + local r = { first = 0, last = -1 } + return setmetatable(r, { __index = methods }) +end + +return { + new = new, +} diff --git a/prototypes/new_auto_tech/electricity_node.lua b/prototypes/new_auto_tech/electricity_node.lua new file mode 100644 index 0000000..ba4ab73 --- /dev/null +++ b/prototypes/new_auto_tech/electricity_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local electricity_node = object_node_base:create_unique_class("electricity", node_types.electricity_node) + +function electricity_node:register_dependencies(nodes) +end + +return electricity_node diff --git a/prototypes/new_auto_tech/electricity_verbs.lua b/prototypes/new_auto_tech/electricity_verbs.lua new file mode 100644 index 0000000..c588c81 --- /dev/null +++ b/prototypes/new_auto_tech/electricity_verbs.lua @@ -0,0 +1,3 @@ +return { + generate = "generate", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/entity_node.lua b/prototypes/new_auto_tech/entity_node.lua new file mode 100644 index 0000000..40c5fa7 --- /dev/null +++ b/prototypes/new_auto_tech/entity_node.lua @@ -0,0 +1,55 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" +local entity_verbs = require "prototypes.new_auto_tech.entity_verbs" +local resource_category_verbs = require "prototypes.new_auto_tech.resource_category_verbs" +local recipe_category_verbs = require "prototypes.new_auto_tech.recipe_category_verbs" +local item_verbs = require "prototypes.new_auto_tech.item_verbs" +local fluid_verbs = require "prototypes.new_auto_tech.fluid_verbs" +local electricity_verbs = require "prototypes.new_auto_tech.electricity_verbs" + +local entity_node = object_node_base:create_object_class("entity", node_types.entity_node) + +function entity_node:register_dependencies(nodes) + local entity = self.object + + if entity.type == "resource" then + self:add_dependency(nodes, node_types.resource_category_node, entity.category or "basic-solid", "resource category", "mine") + elseif entity.type == "mining-drill" then + self:add_disjunctive_dependent(nodes, node_types.resource_category_node, entity.resource_categories, "can mine", resource_category_verbs.instantiate) + elseif entity.type == "offshore-pump" then + self:add_disjunctive_dependent(nodes, node_types.fluid_node, entity.fluid, "pumps", fluid_verbs.create) + end + local minable = entity.minable + if minable ~= nil then + self:add_dependency(nodes, node_types.fluid_node, minable.required_fluid, "required fluid", "mine") + self:add_productlike_disjunctive_dependent(nodes, minable.result, minable.results, "mining result") + end + self:add_disjunctive_dependent(nodes, node_types.entity_node, entity.remains_when_mined, "remains when mined", entity_verbs.instantiate) + self:add_disjunctive_dependency(nodes, node_types.item_node, entity.placeable_by, "placeable by", entity_verbs.instantiate, "item") + self:add_disjunctive_dependent(nodes, node_types.item_node, entity.loot, "loot", item_verbs.create, "item") + self:add_disjunctive_dependent(nodes, node_types.entity_node, entity.corpse, "corpse", entity_verbs.instantiate) + if entity.energy_usage then + self:add_dependency(nodes, node_types.electricity_node, 1, "requires electricity", "power") + end + if entity.energy_source then + local energy_source = entity.energy_source + local type = energy_source.type + if type == "electric" then + self:add_disjunctive_dependent(nodes, node_types.electricity_node, 1, "generates electricity", electricity_verbs.generate) + elseif type == "burner" then + self:add_disjunctive_dependency(nodes, node_types.fuel_category_node, energy_source.fuel_category, "requires fuel", entity_verbs.fuel) + self:add_disjunctive_dependency(nodes, node_types.fuel_category_node, energy_source.fuel_categories, "requires fuel", entity_verbs.fuel) + elseif type == "heat" then + elseif type == "fluid" then + else + assert(type == "void", "Unknown energy source type") + end + end + self:add_disjunctive_dependency(nodes, node_types.fuel_category_node, entity.burner, "requires fuel", entity_verbs.fuel, "fuel_category") + self:add_disjunctive_dependent(nodes, node_types.recipe_category_node, entity.crafting_categories, "can craft", recipe_category_verbs.instantiate) + --fluid_boxes + --allowed_effects, module_specification + --inputs (labs) +end + +return entity_node diff --git a/prototypes/new_auto_tech/entity_verbs.lua b/prototypes/new_auto_tech/entity_verbs.lua new file mode 100644 index 0000000..bf2b69b --- /dev/null +++ b/prototypes/new_auto_tech/entity_verbs.lua @@ -0,0 +1,4 @@ +return { + instantiate = "instantiate", + fuel = "fuel", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/equipment_grid_node.lua b/prototypes/new_auto_tech/equipment_grid_node.lua new file mode 100644 index 0000000..5b3d414 --- /dev/null +++ b/prototypes/new_auto_tech/equipment_grid_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local equipment_grid_node = object_node_base:create_object_class("equipment grid", node_types.equipment_grid_node) + +function equipment_grid_node:register_dependencies(nodes) +end + +return equipment_grid_node diff --git a/prototypes/new_auto_tech/fluid_fuel_node.lua b/prototypes/new_auto_tech/fluid_fuel_node.lua new file mode 100644 index 0000000..78c485a --- /dev/null +++ b/prototypes/new_auto_tech/fluid_fuel_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local fluid_fuel_node = object_node_base:create_unique_class("fluid fuel", node_types.fluid_fuel_node) + +function fluid_fuel_node:register_dependencies(nodes) +end + +return fluid_fuel_node diff --git a/prototypes/new_auto_tech/fluid_fuel_verbs.lua b/prototypes/new_auto_tech/fluid_fuel_verbs.lua new file mode 100644 index 0000000..fe4a18d --- /dev/null +++ b/prototypes/new_auto_tech/fluid_fuel_verbs.lua @@ -0,0 +1,3 @@ +return { + instantiate = "allow the use of", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/fluid_node.lua b/prototypes/new_auto_tech/fluid_node.lua new file mode 100644 index 0000000..2090c79 --- /dev/null +++ b/prototypes/new_auto_tech/fluid_node.lua @@ -0,0 +1,14 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" +local fluid_fuel_verbs = require "prototypes.new_auto_tech.fluid_fuel_verbs" + +local fluid_node = object_node_base:create_object_class("fluid", node_types.fluid_node) + +function fluid_node:register_dependencies(nodes) + local fluid = self.object + if fluid.fuel_value ~= nil then + self:add_disjunctive_dependent(nodes, node_types.fluid_fuel_node, 1, "fuel value", fluid_fuel_verbs.instantiate) + end +end + +return fluid_node diff --git a/prototypes/new_auto_tech/fluid_verbs.lua b/prototypes/new_auto_tech/fluid_verbs.lua new file mode 100644 index 0000000..90d1be9 --- /dev/null +++ b/prototypes/new_auto_tech/fluid_verbs.lua @@ -0,0 +1,3 @@ +return { + create = "create", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/fuel_category_node.lua b/prototypes/new_auto_tech/fuel_category_node.lua new file mode 100644 index 0000000..67a51fd --- /dev/null +++ b/prototypes/new_auto_tech/fuel_category_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local fuel_category_node = object_node_base:create_object_class("fuel category", node_types.fuel_category_node) + +function fuel_category_node:register_dependencies(nodes) +end + +return fuel_category_node diff --git a/prototypes/new_auto_tech/fuel_category_verbs.lua b/prototypes/new_auto_tech/fuel_category_verbs.lua new file mode 100644 index 0000000..fe4a18d --- /dev/null +++ b/prototypes/new_auto_tech/fuel_category_verbs.lua @@ -0,0 +1,3 @@ +return { + instantiate = "allow the use of", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/item_node.lua b/prototypes/new_auto_tech/item_node.lua new file mode 100644 index 0000000..256c92d --- /dev/null +++ b/prototypes/new_auto_tech/item_node.lua @@ -0,0 +1,22 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" +local entity_verbs = require "prototypes.new_auto_tech.entity_verbs" +local item_verbs = require "prototypes.new_auto_tech.item_verbs" +local fuel_category_verbs = require "prototypes.new_auto_tech.fuel_category_verbs" + +local item_node = object_node_base:create_object_class("item", node_types.item_node) + +function item_node:register_dependencies(nodes) + local item = self.object + self:add_disjunctive_dependent(nodes, node_types.entity_node, item.place_result, "place result", entity_verbs.instantiate) + --placed_as_equipment_result optional :: EquipmentID + self:add_disjunctive_dependent(nodes, node_types.fuel_category_node, item.fuel_category, "fuel category", fuel_category_verbs.instantiate) + self:add_disjunctive_dependent(nodes, node_types.item_node, item.burnt_result, "burnt result", item_verbs.create) + self:add_productlike_disjunctive_dependent(nodes, item.rocket_launch_product, item.rocket_launch_products, "rocket launch product") + + --AmmoItemPrototype - 'ammo' + --GunPrototype - 'gun' + --ArmorPrototype - 'armor' +end + +return item_node diff --git a/prototypes/new_auto_tech/item_verbs.lua b/prototypes/new_auto_tech/item_verbs.lua new file mode 100644 index 0000000..90d1be9 --- /dev/null +++ b/prototypes/new_auto_tech/item_verbs.lua @@ -0,0 +1,3 @@ +return { + create = "create", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/new_auto_tech.lua b/prototypes/new_auto_tech/new_auto_tech.lua new file mode 100644 index 0000000..505ec25 --- /dev/null +++ b/prototypes/new_auto_tech/new_auto_tech.lua @@ -0,0 +1,192 @@ +local configuration = { + verbose_logging = settings.startup["pypp-verbose-logging"].value, +} + +local deque = require "prototypes.new_auto_tech.data.deque" + +local node_types = require "prototypes.new_auto_tech.node_types" + +local ammo_category_node = require "prototypes.new_auto_tech.ammo_category_node" +local electricity_node = require "prototypes.new_auto_tech.electricity_node" +local entity_node = require "prototypes.new_auto_tech.entity_node" +local equipment_grid_node = require "prototypes.new_auto_tech.equipment_grid_node" +local fluid_fuel_node = require "prototypes.new_auto_tech.fluid_fuel_node" +local fluid_node = require "prototypes.new_auto_tech.fluid_node" +local fuel_category_node = require "prototypes.new_auto_tech.fuel_category_node" +local item_node = require "prototypes.new_auto_tech.item_node" +local recipe_category_node = require "prototypes.new_auto_tech.recipe_category_node" +local recipe_node = require "prototypes.new_auto_tech.recipe_node" +local resource_category_node = require "prototypes.new_auto_tech.resource_category_node" +local start_node = require "prototypes.new_auto_tech.start_node" +local technology_node = require "prototypes.new_auto_tech.technology_node" + +local auto_tech = {} +auto_tech.__index = auto_tech + +function auto_tech.create() + local a = {} + setmetatable(a, auto_tech) + + a.nodes_per_node_type = {} + + return a +end + +function auto_tech:run_phase(phase_function, phase_name) + log("Starting " .. phase_name) + phase_function(self) + log("Finished " .. phase_name) +end + +function auto_tech:run() + -- TODO: + -- armor and gun stuff, military entities + -- ignore soot results + -- miner with fluidbox + -- resources on map + -- fluid boxes on crafting entities + -- modules on crafting entities + -- robots and roboports + -- heat + -- labs + -- temperatures for fluids, boilers + -- techs enabled at start + + -- nodes to finish: + -- tech + + -- nodes finished: + -- recipe + -- item + -- fluid + -- resource + + self:run_phase(function() + self:run_phase(self.create_nodes, "recipe graph node creation") + self:run_phase(self.link_nodes, "recipe graph link creation") + self:run_phase(self.linearise_recipe_graph, "recipe graph linearisation") + self:run_phase(self.verify_end_tech_reachable, "verify end tech reachable") + self:run_phase(self.construct_tech_graph, "constructing tech graph") + self:run_phase(self.linearise_tech_graph, "tech graph linearisation") + self:run_phase(self.calculate_transitive_reduction, "transitive reduction calculation") + self:run_phase(self.adapt_tech_links, "adapting tech links") + self:run_phase(self.set_tech_costs, "tech cost setting") + end, "autotech") +end + +function auto_tech:create_nodes() + for _, node_type in pairs(node_types) do + self.nodes_per_node_type[node_type] = {} + end + + start_node:create(self.nodes_per_node_type, configuration) + electricity_node:create(self.nodes_per_node_type, configuration) + fluid_fuel_node:create(self.nodes_per_node_type, configuration) + + local function process_type(table, node_type) + for _, object in pairs(table) do + node_type:create(object, self.nodes_per_node_type, configuration) + end + end + + process_type(data.raw["ammo-category"], ammo_category_node) + process_type(data.raw["equipment-grid"], equipment_grid_node) + process_type(data.raw["fluid"], fluid_node) + process_type(data.raw["fuel-category"], fuel_category_node) + process_type(data.raw["recipe-category"], recipe_category_node) + process_type(data.raw["recipe"], recipe_node) + process_type(data.raw["resource-category"], resource_category_node) + process_type(data.raw["resource"], entity_node) + process_type(data.raw["technology"], technology_node) + + process_type(data.raw["armor"], item_node) + process_type(data.raw["ammo"], item_node) + process_type(data.raw["capsule"], item_node) + process_type(data.raw["gun"], item_node) + process_type(data.raw["item"], item_node) + process_type(data.raw["item-with-entity-data"], item_node) + process_type(data.raw["item-with-inventory"], item_node) + process_type(data.raw["item-with-label"], item_node) + process_type(data.raw["item-with-tags"], item_node) + process_type(data.raw["mining-tool"], item_node) + process_type(data.raw["module"], item_node) + process_type(data.raw["spidertron-remote"], item_node) + process_type(data.raw["rail-planner"], item_node) + process_type(data.raw["repair-tool"], item_node) + process_type(data.raw["tool"], item_node) + + for entity_name, _ in pairs(defines.prototypes.entity) do + for _, value in pairs(data.raw[entity_name]) do + entity_node:create(value, self.nodes_per_node_type, configuration) + end + end +end + +function auto_tech:link_nodes() + for _, node_type in pairs(self.nodes_per_node_type) do + for _, node in pairs(node_type) do + node:register_dependencies(self.nodes_per_node_type) + end + end +end + +function auto_tech:linearise_recipe_graph() + local verbose_logging = configuration.verbose_logging + local q = deque.new() + for _, node_type in pairs(self.nodes_per_node_type) do + for _, node in pairs(node_type) do + if node:has_no_more_dependencies() then + q:push_right(node) + if verbose_logging then + log("Node " .. node.printable_name .. " starts with no dependencies.") + end + end + end + end + + while (not q:is_empty()) do + local next = q:pop_left() + if verbose_logging then + log("Node " .. next.printable_name .. " is next in the linearisation.") + end + + local newly_independent_nodes = next:release_dependents() + for _, node in pairs(newly_independent_nodes) do + q:push_right(node) + end + end + + for _, node_type in pairs(self.nodes_per_node_type) do + for _, node in pairs(node_type) do + if not node:has_no_more_dependencies() then + log("Node " .. node.printable_name .. " still has unresolved dependencies: " .. node:print_dependencies()) + end + end + end +end + +function auto_tech:verify_end_tech_reachable() + +end + +function auto_tech:calculate_transitive_reduction() + +end + +function auto_tech:construct_tech_graph() + +end + +function auto_tech:linearise_tech_graph() + +end + +function auto_tech:adapt_tech_links() + +end + +function auto_tech:set_tech_costs() + +end + +return auto_tech diff --git a/prototypes/new_auto_tech/node_types.lua b/prototypes/new_auto_tech/node_types.lua new file mode 100644 index 0000000..7777428 --- /dev/null +++ b/prototypes/new_auto_tech/node_types.lua @@ -0,0 +1,15 @@ +return { + ammo_category_node = 1, + electricity_node = 2, + entity_node = 3, + equipment_grid_node = 4, + fluid_fuel_node = 5, + fluid_node = 6, + fuel_category_node = 7, + item_node = 8, + recipe_category_node = 9, + recipe_node = 10, + resource_category_node = 11, + start_node = 12, + technology_node = 13, +} diff --git a/prototypes/new_auto_tech/object_node_base.lua b/prototypes/new_auto_tech/object_node_base.lua new file mode 100644 index 0000000..95e44bc --- /dev/null +++ b/prototypes/new_auto_tech/object_node_base.lua @@ -0,0 +1,247 @@ +local node_types = require "prototypes.new_auto_tech.node_types" +local item_verbs = require "prototypes.new_auto_tech.item_verbs" +local fluid_verbs = require "prototypes.new_auto_tech.fluid_verbs" + +local object_node_base = {} +object_node_base.__index = object_node_base + +function object_node_base:create_class_impl(type_name, node_type, create_method) + local object_class = { type_name = type_name } + object_class.__index = object_class + setmetatable(object_class, {__index = object_node_base}) + object_class.node_type = node_type + object_class.create = create_method + return object_class +end + +function object_node_base:create_unique_class(type_name, node_type) + return object_node_base:create_class_impl(type_name, node_type, object_node_base.create_unique) +end + +function object_node_base:create_object_class(type_name, node_type) + return object_node_base:create_class_impl(type_name, node_type, object_node_base.create_object) +end + +function object_node_base:create_object(object, nodes, configuration) + local s = {} + -- Yes, we're setting up the inheritance in the base class, this skips all the cumbersome base class calls we don't need anyway + setmetatable(s, self.__index) + s.configuration = configuration + s.depends = {} + s.reverse_depends = {} + s.disjunctive_depends = {} + s.disjunctive_depends_count = 0 + s.reverse_disjunctive_depends = {} + if object == nil then + s.printable_name = self.type_name + nodes[self.node_type][1] = s + else + s.object = object + s.printable_name = object.name .. " (" .. self.type_name .. ")" + nodes[self.node_type][object.name] = s + end + + -- These get changed in the linearisation + s.depends_count = 0 + s.canonicalised_choices = {} + s.canonicalised_choices_count = 0 + + if configuration.verbose_logging then + log("Created node for " .. s.printable_name) + end + + return s +end + +function object_node_base:has_no_more_dependencies() + return self.depends_count == 0 and self.disjunctive_depends_count == self.canonicalised_choices_count +end + +function object_node_base:print_dependencies() + local result = self.depends_count .. " fixed dependencies" + + if self.disjunctive_depends_count ~= self.canonicalised_choices_count then + result = result .. " and these disjunctive dependencies: " + for verb, _ in pairs(self.disjunctive_depends) do + if self.canonicalised_choices[verb] == nil then + result = result .. verb .. ", " + end + end + end + + return result +end + +function object_node_base:release_dependents() + local newly_independent_nodes = {} + local verbose_logging = self.configuration.verbose_logging + + for _, data in pairs(self.reverse_depends) do + local node = data[1] + local verb = data[2] + local dependency_type = data[3] + + node.depends_count = node.depends_count - 1 + if verbose_logging then + log("Virtually removing dependency from " .. node.printable_name .. " on " .. self.printable_name .. " to " .. verb .. " via " .. dependency_type) + end + if node:has_no_more_dependencies() then + newly_independent_nodes[#newly_independent_nodes+1] = node + if verbose_logging then + log("Node " .. node.printable_name .. " has no more dependencies.") + end + end + end + + for _, data in pairs(self.reverse_disjunctive_depends) do + local node = data[1] + local verb = data[2] + local dependency_type = data[3] + if node.canonicalised_choices[verb] ~= nil then + node.canonicalised_choices[verb] = {self, dependency_type} + node.canonicalised_choices_count = node.canonicalised_choices_count + 1 + if verbose_logging then + log("Canonising the dependency for " .. node.printable_name .. " for " .. verb .. " to be on " .. self.printable_name .. " via " .. dependency_type) + end + + if not node:still_has_dependencies() then + newly_independent_nodes[#newly_independent_nodes+1] = node + if verbose_logging then + log("Node " .. node.printable_name .. " has no more dependencies.") + end + end + end + end + + return newly_independent_nodes +end + +function object_node_base:create_unique(nodes, configuration) + return self:create_object(nil, nodes, configuration) +end + +function object_node_base:lookup_dependency(nodes, node_type, node_name) + local dependency = nodes[node_type][node_name] + if dependency == nil then + error("Could not find dependency " .. node_name .. " of type " .. node_type.type_name .. ", this is probably a bug in the data parser.") + end + return dependency +end + +function object_node_base:add_dependency_impl(dependency, dependency_type, verb) + local depends = self.depends + depends[#depends+1] = {dependency, dependency_type} + self.depends_count = self.depends_count + 1 + local reverse_depends = dependency.reverse_depends + reverse_depends[#reverse_depends+1] = {self, verb, dependency_type} + if self.configuration.verbose_logging then + log("In order to " .. verb .. " " .. self.printable_name .. " you require " .. dependency.printable_name .. " via " .. dependency_type) + end +end + +function object_node_base:add_disjunctive_dependency_impl(dependency, dependency_type, verb) + if self.disjunctive_depends[verb] == nil then + self.disjunctive_depends[verb] = {} + self.disjunctive_depends_count = self.disjunctive_depends_count + 1 + end + local target = self.disjunctive_depends[verb] + target[#target+1] = {dependency, dependency_type} + local reverse_disjunctive_depends = dependency.reverse_disjunctive_depends + reverse_disjunctive_depends[#reverse_disjunctive_depends+1] = {self, verb, dependency_type} + if self.configuration.verbose_logging then + log("In order to " .. verb .. " " .. self.printable_name .. " you could use " .. dependency.printable_name .. " via " .. dependency_type) + end +end + +function loop_if_table_ignore_nil(func, node_name, optional_inner_name) + function doCall(actual_node_name) + if optional_inner_name == nil then + func(actual_node_name) + else + func(actual_node_name[optional_inner_name]) + end + end + function doCallOnObject() + doCall(node_name) + end + function doCallOnTable() + for _, actual_node_name in pairs(node_name) do + doCall(actual_node_name) + end + end + + if node_name == nil then + return + end + if type(node_name) == "table" then + if optional_inner_name ~= nil then + -- have to distinguish between { item='fish', count=5 } and a table of such entries + if node_name[optional_inner_name] == nil then + doCallOnTable() + else + doCallOnObject() + end + else + doCallOnTable() + end + else + doCallOnObject() + end +end + +function object_node_base:add_dependency(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) + loop_if_table_ignore_nil(function (node_name_inner) + self:add_dependency_impl(self:lookup_dependency(nodes, node_type, node_name_inner), dependency_type, verb) + end, node_name, optional_inner_name) +end + +function object_node_base:add_disjunctive_dependency(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) + loop_if_table_ignore_nil(function (node_name_inner) + self:add_disjunctive_dependency_impl(self:lookup_dependency(nodes, node_type, node_name_inner), dependency_type, verb) + end, node_name, optional_inner_name) +end + +function object_node_base:add_dependent(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) + loop_if_table_ignore_nil(function (node_name_inner) + self:lookup_dependency(nodes, node_type, node_name_inner):add_dependency_impl(self, dependency_type, verb) + end, node_name, optional_inner_name) +end + +function object_node_base:add_disjunctive_dependent(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) + loop_if_table_ignore_nil(function (node_name_inner) + self:lookup_dependency(nodes, node_type, node_name_inner):add_disjunctive_dependency_impl(self, dependency_type, verb) + end, node_name, optional_inner_name) +end + +function object_node_base:add_productlike_dependency(nodes, single_product, table_product, dependency_type, verb) + self:add_productlike_dependency_impl(nodes, single_product, table_product, dependency_type, function (self2, nodes2, node_type2, node_name2, dependency_type2, verb2) + self2:add_dependency(nodes2, node_type2, node_name2, dependency_type2, verb) + end) +end + +function object_node_base:add_productlike_disjunctive_dependent(nodes, single_product, table_product, dependency_type) + self:add_productlike_dependency_impl(nodes, single_product, table_product, dependency_type, self.add_disjunctive_dependent) +end + +function object_node_base:add_productlike_dependency_impl(nodes, single_product, table_product, dependency_type, dependency_function) + local function unwrap_result(wrapped_result) + return type(wrapped_result) == "table" and (wrapped_result.name or wrapped_result[1]) or wrapped_result + end + + if table_product ~= nil then + for _, result in pairs(table_product) do + local result_name = unwrap_result(result) + if result.type == "fluid" then + dependency_function(self, nodes, node_types.fluid_node, result_name, dependency_type, fluid_verbs.create) + else + dependency_function(self, nodes, node_types.item_node, result_name, dependency_type, item_verbs.create) + end + end + end + if single_product ~= nil then + local result_name = unwrap_result(single_product) + dependency_function(self, nodes, node_types.item_node, result_name, dependency_type, item_verbs.create) + end +end + +return object_node_base diff --git a/prototypes/new_auto_tech/recipe_category_node.lua b/prototypes/new_auto_tech/recipe_category_node.lua new file mode 100644 index 0000000..3ad5fd8 --- /dev/null +++ b/prototypes/new_auto_tech/recipe_category_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local recipe_category_node = object_node_base:create_object_class("recipe category", node_types.recipe_category_node) + +function recipe_category_node:register_dependencies(nodes) +end + +return recipe_category_node diff --git a/prototypes/new_auto_tech/recipe_category_verbs.lua b/prototypes/new_auto_tech/recipe_category_verbs.lua new file mode 100644 index 0000000..fe4a18d --- /dev/null +++ b/prototypes/new_auto_tech/recipe_category_verbs.lua @@ -0,0 +1,3 @@ +return { + instantiate = "allow the use of", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/recipe_node.lua b/prototypes/new_auto_tech/recipe_node.lua new file mode 100644 index 0000000..0a426db --- /dev/null +++ b/prototypes/new_auto_tech/recipe_node.lua @@ -0,0 +1,24 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" +local recipe_verbs = require "prototypes.new_auto_tech.recipe_verbs" + +local recipe_node = object_node_base:create_object_class("recipe", node_types.recipe_node) + +function recipe_node:register_dependencies(nodes) + local recipe = self.object + self:add_dependency(nodes, node_types.recipe_category_node, recipe.category or "crafting", "crafting category", "craft") + + local recipe_data = (type(recipe.normal) == "table" and (recipe.normal or recipe.expensive) or recipe) + + self:add_productlike_dependency(nodes, recipe_data.ingredient, recipe_data.ingredients, "ingredient", "craft") + + self:add_productlike_disjunctive_dependent(nodes, recipe_data.result, recipe_data.results, "result") + + if recipe_data.enabled ~= false then + self:add_disjunctive_dependency(nodes, node_types.start_node, 1, "starts enabled", recipe_verbs.enable) + end +end + +setmetatable(recipe_node, object_node_base); + +return recipe_node diff --git a/prototypes/new_auto_tech/recipe_verbs.lua b/prototypes/new_auto_tech/recipe_verbs.lua new file mode 100644 index 0000000..b634142 --- /dev/null +++ b/prototypes/new_auto_tech/recipe_verbs.lua @@ -0,0 +1,3 @@ +return { + enable = "enable", +} diff --git a/prototypes/new_auto_tech/resource_category_node.lua b/prototypes/new_auto_tech/resource_category_node.lua new file mode 100644 index 0000000..3016670 --- /dev/null +++ b/prototypes/new_auto_tech/resource_category_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local resource_category_node = object_node_base:create_object_class("resource category", node_types.resource_category_node) + +function resource_category_node:register_dependencies(nodes) +end + +return resource_category_node diff --git a/prototypes/new_auto_tech/resource_category_verbs.lua b/prototypes/new_auto_tech/resource_category_verbs.lua new file mode 100644 index 0000000..fe4a18d --- /dev/null +++ b/prototypes/new_auto_tech/resource_category_verbs.lua @@ -0,0 +1,3 @@ +return { + instantiate = "allow the use of", +} \ No newline at end of file diff --git a/prototypes/new_auto_tech/start_node.lua b/prototypes/new_auto_tech/start_node.lua new file mode 100644 index 0000000..e05ea02 --- /dev/null +++ b/prototypes/new_auto_tech/start_node.lua @@ -0,0 +1,9 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" + +local start_node = object_node_base:create_unique_class("start", node_types.start_node) + +function start_node:register_dependencies(nodes) +end + +return start_node diff --git a/prototypes/new_auto_tech/technology_node.lua b/prototypes/new_auto_tech/technology_node.lua new file mode 100644 index 0000000..97e5311 --- /dev/null +++ b/prototypes/new_auto_tech/technology_node.lua @@ -0,0 +1,23 @@ +local object_node_base = require "prototypes.new_auto_tech.object_node_base" +local node_types = require "prototypes.new_auto_tech.node_types" +local item_verbs = require "prototypes.new_auto_tech.item_verbs" +local recipe_verbs = require "prototypes.new_auto_tech.recipe_verbs" + +local technology_node = object_node_base:create_object_class("technology", node_types.technology_node) + +function technology_node:register_dependencies(nodes) + local tech = self.object + local tech_data = (type(tech.normal) == "table" and (tech.normal or tech.expensive) or tech) + + self:add_dependency(nodes, node_types.technology_node, tech_data.prerequisites, "prerequisite", "enable") + + for _, modifier in pairs(tech_data.effects or {}) do + if modifier.type == "give-item" then + self:add_disjunctive_dependent(nodes, node_types.item_node, modifier.item, "given by tech", item_verbs.create) + elseif modifier.type == "unlock-recipe" then + self:add_disjunctive_dependent(nodes, node_types.recipe_node, modifier.recipe, "enabled by tech", recipe_verbs.enable) + end + end +end + +return technology_node From 1b70940af222411abb3cbb495a64e6e6c4b0d59e Mon Sep 17 00:00:00 2001 From: Alex ten Brink Date: Thu, 1 Aug 2024 21:55:11 +0200 Subject: [PATCH 15/25] Fix GitHub workflow files --- .github/workflows/factoriotest-pr.yml | 3 ++- .github/workflows/factoriotest-push.yml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/factoriotest-pr.yml b/.github/workflows/factoriotest-pr.yml index 94e4e42..4dc9067 100644 --- a/.github/workflows/factoriotest-pr.yml +++ b/.github/workflows/factoriotest-pr.yml @@ -14,8 +14,9 @@ permissions: jobs: test-pull-request: name: PR - uses: pyanodon/pyanodontests/.github/workflows/pytest.yml@v1 + uses: pyanodon/pyanodontests/.github/workflows/pytest.yml@v1.5.0 with: repository: ${{ github.repository }} ref: ${{ github.event.pull_request.head.sha }} + test_branch: ${{ github.head_ref || github.ref_name }} secrets: inherit diff --git a/.github/workflows/factoriotest-push.yml b/.github/workflows/factoriotest-push.yml index 294ee78..7a0c787 100644 --- a/.github/workflows/factoriotest-push.yml +++ b/.github/workflows/factoriotest-push.yml @@ -14,9 +14,9 @@ permissions: jobs: test-push: name: Push - uses: pyanodon/pyanodontests/.github/workflows/pytest.yml@v1 + uses: pyanodon/pyanodontests/.github/workflows/pytest.yml@v1.5.0 with: repository: ${{ github.repository }} ref: ${{ github.sha }} + test_branch: ${{ github.head_ref || github.ref_name }} secrets: inherit - From 4b794994df3cd4b93cbe665561dbd347c44faab1 Mon Sep 17 00:00:00 2001 From: Alex ten Brink Date: Thu, 1 Aug 2024 21:56:29 +0200 Subject: [PATCH 16/25] Revert "DRAFT added new auto-tech" This reverts commit 45618551636aba957d0ef38225fbcbbda7d590aa. --- prototypes/new_auto_tech/README.txt | 158 ----------- .../new_auto_tech/ammo_category_node.lua | 9 - prototypes/new_auto_tech/data/deque.lua | 51 ---- prototypes/new_auto_tech/electricity_node.lua | 9 - .../new_auto_tech/electricity_verbs.lua | 3 - prototypes/new_auto_tech/entity_node.lua | 55 ---- prototypes/new_auto_tech/entity_verbs.lua | 4 - .../new_auto_tech/equipment_grid_node.lua | 9 - prototypes/new_auto_tech/fluid_fuel_node.lua | 9 - prototypes/new_auto_tech/fluid_fuel_verbs.lua | 3 - prototypes/new_auto_tech/fluid_node.lua | 14 - prototypes/new_auto_tech/fluid_verbs.lua | 3 - .../new_auto_tech/fuel_category_node.lua | 9 - .../new_auto_tech/fuel_category_verbs.lua | 3 - prototypes/new_auto_tech/item_node.lua | 22 -- prototypes/new_auto_tech/item_verbs.lua | 3 - prototypes/new_auto_tech/new_auto_tech.lua | 192 -------------- prototypes/new_auto_tech/node_types.lua | 15 -- prototypes/new_auto_tech/object_node_base.lua | 247 ------------------ .../new_auto_tech/recipe_category_node.lua | 9 - .../new_auto_tech/recipe_category_verbs.lua | 3 - prototypes/new_auto_tech/recipe_node.lua | 24 -- prototypes/new_auto_tech/recipe_verbs.lua | 3 - .../new_auto_tech/resource_category_node.lua | 9 - .../new_auto_tech/resource_category_verbs.lua | 3 - prototypes/new_auto_tech/start_node.lua | 9 - prototypes/new_auto_tech/technology_node.lua | 23 -- 27 files changed, 901 deletions(-) delete mode 100644 prototypes/new_auto_tech/README.txt delete mode 100644 prototypes/new_auto_tech/ammo_category_node.lua delete mode 100644 prototypes/new_auto_tech/data/deque.lua delete mode 100644 prototypes/new_auto_tech/electricity_node.lua delete mode 100644 prototypes/new_auto_tech/electricity_verbs.lua delete mode 100644 prototypes/new_auto_tech/entity_node.lua delete mode 100644 prototypes/new_auto_tech/entity_verbs.lua delete mode 100644 prototypes/new_auto_tech/equipment_grid_node.lua delete mode 100644 prototypes/new_auto_tech/fluid_fuel_node.lua delete mode 100644 prototypes/new_auto_tech/fluid_fuel_verbs.lua delete mode 100644 prototypes/new_auto_tech/fluid_node.lua delete mode 100644 prototypes/new_auto_tech/fluid_verbs.lua delete mode 100644 prototypes/new_auto_tech/fuel_category_node.lua delete mode 100644 prototypes/new_auto_tech/fuel_category_verbs.lua delete mode 100644 prototypes/new_auto_tech/item_node.lua delete mode 100644 prototypes/new_auto_tech/item_verbs.lua delete mode 100644 prototypes/new_auto_tech/new_auto_tech.lua delete mode 100644 prototypes/new_auto_tech/node_types.lua delete mode 100644 prototypes/new_auto_tech/object_node_base.lua delete mode 100644 prototypes/new_auto_tech/recipe_category_node.lua delete mode 100644 prototypes/new_auto_tech/recipe_category_verbs.lua delete mode 100644 prototypes/new_auto_tech/recipe_node.lua delete mode 100644 prototypes/new_auto_tech/recipe_verbs.lua delete mode 100644 prototypes/new_auto_tech/resource_category_node.lua delete mode 100644 prototypes/new_auto_tech/resource_category_verbs.lua delete mode 100644 prototypes/new_auto_tech/start_node.lua delete mode 100644 prototypes/new_auto_tech/technology_node.lua diff --git a/prototypes/new_auto_tech/README.txt b/prototypes/new_auto_tech/README.txt deleted file mode 100644 index d9e498f..0000000 --- a/prototypes/new_auto_tech/README.txt +++ /dev/null @@ -1,158 +0,0 @@ -The autotech module has several main goals. The first two are shared with the old autotech, the last ones are new: -- adapt the tech tree so if you research a tech, you will be able to use all the recipes from that tech right away. -- change the research costs to slowly ramp up as you get further in the tech tree. -- report all unreachable items/recipes/techs, and in particular whether the victory tech is reachable. -- enforce that if a tech dependency of a tech has a particular science pack in its costs, then that tech will have at least the same science packs (monotonicity of science pack costs, as checked by the python tests). -- report as much useful information as possible to help development. Do cheap checks for errors early. Have an option to turn on verbose logging to debug more complicated issues. -- allow the code to be run in a unit test scenario on a dumped version of the Factorio data, to help development and allow automated testing -- support more advanced Py features, like inter-surface logistics, differing tech trees based on a starting scenario, optional side techs, etc - -There are two versions of this explanation. The first gives all the details including a rationale but assumes a computer science background, the second only assumes you know what a directed graph is and is mainly tailored to users of autotech. - -== Explanation assuming a computer science background == - -The second point, changing the tech costs, is fairly easy, so let's get it out of the way first. The first phase results in a tech tree with a linear ordering. We can use this linear ordering to find the longest path from every science pack to every tech as follows. For every science pack, loop over the techs in this linear order, and then find the longest distance by taking the highest longest distance of all its dependencies plus one (zero if it's the source, infinity if it's not the source and has no dependencies or if it doesn't have the science pack as a cost). For every tech we can then compute the tech costs by taking these longest distances for the science packs it costs and inputting those into the science cost formula. This generalises the old autotech code where there was a linear ordering between science packs (automation, p1, logi, p2, etc). This generalisation allows for new side science packs such as the energy science pack. - -My proposal for a tech cost formula is to scale the overall tech cost with the smallest longest distance among the science packs. Furthermore, the individual pack count can be chosen by taking the longest distances from smallest to largest, and then assigning a pack count to each, higher counts for the farther away science packs. This will probably need some tweaking in practice. - -The first point, adapting the tech tree, is tricky, hence this README. Let's first list the phases of autotech, and then explain the steps one by one: -1 construct recipe graph -2 linearise the recipe graph, ignoring cycles - 2a canonise all choices along the way, report these choices - 2b error if victory tech unreachable from starting tech - 2c report but don't error if other things are unreachable or if a choice cannot be made due to cycles - 2d report but don't error on cycles found in this graph -3 construct true tech dependency graph from unlinearised but canonised recipe graph -4 linearise resulting tech graph, error out on cycle and report it -5 compute transitive reduction of unlinearised true tech dependency graph -6 attach tech costs to techs -7 edit Factorio tech dependencies to match unlinearised reduced true tech dependency graph and match new tech costs - -A cache file can replace all these steps. Note that cache files may now remove dependencies due to the transitive reduction, which the old autotech did not do. We can leave a setting to allow (modified versions of) steps 4, 5 and 6 to be executed despite a cache file being used for those that want autotech to affect other mods. - -The 'recipe graph' is a graph where the nodes are Factorio concepts, such as items, recipes, fluids, recipe categories, etc, and the edges are Factorio dependencies between these. For example, the node 'iron plate' may have an edge 'is crafted by' to the node 'iron plate smelting'. (yes, this means the name 'recipe graph' is incorrect, as it contains things that are not recipes too.) We generate this graph in two phases: we first create all the nodes, and then we create all the edges. The reason for the two phases is that during the edge phase we know all nodes already exist and we can just look them up by name, so we don't need to worry about the order we generate the nodes or edges in. - -There is one major complication however. We often have several ways to get something, for example, there could be several recipes to make an item. This means we have to make this recipe graph a 'disjunctive graph'. In such a graph, an edge from node A has multiple targets B_i, instead of just one target B like in a normal graph, representing that node A needs at least one of the B_i nodes. For example, the Ralesia item requires one or more of its recipes to be unlocked (one edge, several targets), as well as the crafting category (one edge, one target), so it has at least these two edges. More formally, every node depends on a conjunction of a disjunction of targets (an AND with OR arguments), which is sort of equivalent to a conjunctive normal form. Indeed, this makes a disjunctive graph effectively a monotone boolean circuit with unbounded fan-in and fan-out (only OR and AND gates, no NOT gates). (don't worry if you don't know what these are) - -Let's now define the 'true tech dependency graph' as the graph where the nodes are the techs, and the edges are the 'true' dependencies between the techs, that is, the dependencies according to the recipe graph. For example, if tech A gives a building that requires the item Intermetallics, then it has a 'true dependency' on the tech that gives you the recipe to make Intermetallics. We also include the already existing tech dependencies and science pack requirements in this graph. This essentially comes down to doing a reachability query for every tech on the recipe graph. - -It's tempting to now try to compute this true tech dependency graph directly from the recipe graph. Unfortunately, there are two major problems, which both stem from the fact that this computed true tech graph will also be a disjunctive graph. First, turning a monotone boolean circuit into a conjunctive normal form (which is basically what we're trying to do) involves exponential blowup of the formula size (it's NP-hard). In other words, your true tech graph may be exponentially larger than the recipe graph. You'd have to add a bunch of heuristics to try and stop the exponential blowup for practical examples, but that's a lot of complicated code that will not necessarily work, and it's going to be slow whatever you do. - -Second, there's a fundamental problem with this true tech graph: Factorio does not allow disjunctive dependencies between techs. This means we'll have to make a choice for every edge no matter how we go about it, and just computing the disjunctive true tech dependency graph does not help us solve this problem at all. Luckily, this second problem also shows us the path how to resolve both problems. - -Since we have to make a choice at some point anyway, why not make our choice in the recipe graph already? We can just linearise the recipe graph, and for every disjunctive edge, choose the target that's the earliest in the linear ordering. We'll call this 'canonising' of the graph, where we choose a 'canon' target for every disjunctive edge. With this canonisation, our disjunctive recipe graph turns into a normal graph, so computing a normal true tech dependency graph out of this is straightforward using for example BFS. Note that this solution is not perfect: since there are several possible linearisations, it's possible we choose a linearisation that leads to a canonicalisation that leads to a cycle in the true tech dependency graph while a different linearisation would have led to no cycle in the tech dependency graph. This is what the old autotech also did, and I expect this to not be a problem in practice. Worst case, we can add a mechanism to force a specific canonicalisation. - -With that, the major problem is out of the way, and all that remains are implementation details. Note that 'report' and 'log' are different in that there's different settings to turn on either one, and that 'report's indicates something is potentially wrong. - -Step 1: the code will log the entire recipe graph, to make it clear how it interpreted the code. Two details about the recipe graph: we'll need a file with 'scripted dependencies' like guano, and we'll need to respect the flag we have for e.g. soot sorting that should not lead to seeing soot as a source of silver. - -Step 2: we apply the standard Kahn's algorithm for linearisation, which can be adapted easily for disjunctive graphs, see here: https://math.stackexchange.com/q/2449379. We linearise as much as possible and leave any cycles in the graph for later steps. We log the entire linear ordering. - -Step 2a: we partially canonise the choices due to the partial linear ordering from earlier. All choices are logged. - -Step 2b: we do a BFS from the starting node which only uses linearised nodes, and see if the victory tech is reachable. This step comes early to aid the development process and error out as early as possible. - -Step 2c: we can use the BFS from 2b to see what nodes are not reachable and report those. We can also report all the edges that did not get a canon target. - -Step 2d: We can report a cycle with the standard cycle reporting algorithm: start in any unlinearised node and repeatedly follow any edge while keeping track of what nodes you've visited until you reach a node you've visited before, then report the stack of nodes you've followed until you get back to that node. This will work even in a disjunctive graph. Note that none of these reports are errors - a bunch of these could be due to later Py mods disabling earlier items etc. Also note we can probably report a cycle for every connected component of the unlinearised part of the graph. Reporting more than one is probably not a good idea, because if you have one you tend to have many with duplicate nodes. Worst case, we can report a cycle, delete all the nodes involved, then continue Kahn's algorithm again, report another cycle, etc. - -Step 3: the entire tech graph is logged. An inefficient way to construct the graph this is to perform a BFS from every tech, and then make an edge for every reachable tech. We can make two performance improvements: - -The first optimisation is to make use of the observation that the BFS will not just find the dependent techs but all their transitive tech dependencies too, and that this is unnecessary. If we do a BFS on tech A, and we find tech B as dependency, which itself has tech C as dependency, then we don't need to add C as a dependency for A, because the BFS from B will already find C. We can achieve this by breaking up the tech and recipe nodes in the recipe graph. We make two nodes per tech: the 'unlock' node and the 'dependency' node. We do the same for recipes: we make an 'unlock' and 'dependency' node per recipe. The idea is that the BFS will start at the 'unlock' tech node, jump to the 'unlock' recipe nodes, traverse the rest of the graph, end up at the 'dependency' nodes for the required recipes, and then jump to the 'dependency' nodes for the techs. These 'dependency' nodes don't have any edges for the BFS to follow afterwards, so the BFS won't try to find the transitive tech dependencies. - -The second optimisation is to make use of the linearisation to compute all the tech dependencies simultaneously instead of in many BFS passes. We can go in linear order through the nodes and propagate the techs that every node depends on by looking at all its edges and merging the tech dependencies of their targets. Note that this is not better in the worst case, if techs depend on a linear number of other techs, because then most of your time you're merging dependencies no matter whether you're using BFSses or not. - -Step 4: just like step 2d, it's possible to report a cycle in the tech graph. - -Step 5: the transitive reduction of the graph is the graph where you take away the largest number of edges without changing the transitive dependencies. For example, if tech A depends on C, but A also depends on B which depends on C already, then that dependency from A to C is pointless and can be removed. This is a slightly expensive step to compute but should make the tech tree more readable. We're already past all the steps that can give errors at this point though. There'll be a setting to turn this off it it takes too much time. All removed edges are logged. - -Steps 6 and 7 are straightforward and discussed earlier respectively. - -== Explanation without assuming a computer science background == - -=== Linear orderings === - -The autotech code makes heavy use of a concept called a 'linear ordering', so we're going to explain that first. Let's take the Factorio example of the automation science pack, which needs an iron wheel and copper plate to make. We can make a dependency graph between these items for the base game: - -- iron ore -> iron plate -- copper ore -> copper plate -- iron plate -> iron wheel -- iron wheel -> automation science pack -- copper plate -> automation science pack - -Thse five dependencies define what is called a 'partial ordering' on these nodes. It's an ordering because it tells you what nodes need to 'happen' before what other nodes, and it's partial because it leaves freedom as to the exact orderings that conform to the partial ordering. For example, these are some orderings that respect this partial ordering: - -- iron ore, iron plate, iron wheel, copper ore, copper plate, automation science pack -- iron ore, copper ore, iron plate, copper plate, iron wheel, automation science pack -- copper ore, copper plate, iron ore, iron plate, iron wheel, automation science pack - -These three examples each represent a specific ways to get to the automation science pack from scratch. They respect the partial ordering because for every dependency A -> B in the partial ordering, A comes before B in the examples. However, an ordering where for example the iron wheel comes first does not respect the partial ordering, and indeed it cannot correspond to a way to get to the automation science pack. - -Actually, these three examples are in fact also examples of a 'linear order', because they fully pin down the ordering of events, as opposed to a partial order, which leaves some freedom. It's called 'linear' because if you consider it a graph, it's just a line. There's actually many names for it, such as 'complete ordering' or 'total ordering'. Every partial order can be respected by a lot of different linear orders. - -Now, if you think about that dependency graph from earlier as a graph, you may notice it's a directed acyclic graph. This is true in general: every directed acyclic graph corresponds to a partial ordering, they're basically the same thing but in a different context. Cycles are obviously bad for orderings, because no 'solutions' exist - if A depends on B depends on C depends on A, then no ordering of A, B and C can respect those dependencies, hence the need to have an acyclic graph. - -'Linearisation' is the process of turning a partial order into a linear order. Intuitively, this means we pick a possible 'path' of the nodes in the dependency graph that respects the partial ordering. Usually, linearisation algorithm do a bit more than just compute a linearisation: they also detect cycles. They start with a directed graph, detect cycles and if no cycle exist they output a linearisation. Note there can be multiple or even many possible linearisations of a partial order (as seen in our example): linearisation arbitrarily picks one of them. - -=== Disjunctive graphs === - -The second concept important to autotech is a 'disjunctive graph'. It solves the problem that in Factorio, you often have a choice to make something, so a strict dependency graph like we presented earlier doesn't work. For example: in vanilla Factorio, water can come from an offshore pump but also from emptying a water barrel. Petroleum can come from basic oil processing, advanced oil processing, light oil cracking and emptying a petroleum barrel. This is even more egregious in Pyanodon, because items can have many recipes and there will be playthroughs A and B that obtain an item exclusively through differing recipes. The strict dependencies as described in the previous section are therefore not good enough - you cannot say that making Vrauks necessarily comes from the simpler or the more complex recipe to make it. - -Enter the disjunctive graph. Instead of having edges of the form A -> B, so "You need B to make A", you can have edges of the form A -> {B, C}, so "You need B _or_ C to make A". Conceptually, this is pretty simple, but it makes autotech significantly harder. The good news is that linearisation, cycle detection and reachability (eg 'can I get from the starting point to the final technology') are all easy with disjunctive graphs, as the normal algorithms keep working with minimal changes. The bad news is that the ultimate thing we'd like to know, namely 'what techs do I need in order to be able to use a recipe', becomes an algorithm with an exponential runtime if we want to compute it fully correctly. Luckily, we can take a shortcut that will work in all practical cases. - -=== Recipe graph and tech graph === - -Let's first describe the exponential algorithm, since it helps understanding the final algorithm. We will skip the explanation why the last step is exponential. We first make the 'recipe graph': we make a node for every Factorio 'thing', so items, recipes, crafting categories, entities, fuel categories, etc. We then add edges and disjunctive edges representing how Factorio works. For example, every item has a disjunctive edge to the recipes that make it, the ore entities that it can be mined from or the fuels it is the burnt result from, etc. Every recipe has a normal edge to the crafting category for that recipe, which in turn has a disjunctive edge to all the entities that can craft that crafting category, etc. This makes the recipe graph a disjunctive graph that describes the partial order of all possible playthroughs of Factorio (given a modset). Every actual playthrough is a linear ordering of the Factorio 'things' that adheres to the recipe graph. - -Yes, the name 'recipe graph' is not correct as it contains more than just recipes, but calling it a 'Factorio thing graph' doesn't sound as good. - -Making the recipe graph is fast and super useful, so we will also do it in the actual final algorithm. It allows us to figure out a bunch of things: what items and recipes are impossible to get in a playthrough, either because they are fundamentally unreachable or because they're part of a dependency loop? Can we research the victory tech at all? Note that not all unreachable items are bad: they may just be disabled items that are not actually needed, for example 'organic material' in Py before PyAL, which gets disabled and replaced with 'biomatter' when PyAL is turned on. The new autotech reports these unreachable things in the log, but doesn't error out on them, which should help debug issues. - -The ultimate thing that autotech aims to figure out is what techs you need in order to be able to run all the recipes unlocked by a tech. We therefore want to make a second graph, the 'technology graph', where the nodes are techs, and the edges are these dependencies. To spell that out a bit more: let's say technology A unlocks recipe X. The aim is that when you unlock A, no matter what techs you've chosen before, you should be able to use the recipes you have at that point to be able to run recipe X in your factory. You need to be able to make every ingredient of X, you need to be able to have a building that can run X, etc. - -This is a question that the recipe graph can answer. However, both reachability and cycle detection are not good enough for our goals. Reachability especially just tells you that there is _some_ way to go from a tech to a recipe, but it's allowed to assume you research other techs, so it's not good enough. Similarly, linearisation will just tell you _some_ set of techs that will allow you to use that recipe, but not the minimal number of techs. There is an algorithm that computes this 'technology graph' correctly, however, I will not describe it here due to the problems described in the next paragraph. - -There are two problems with this approach (not just the algorithm): the first is that this technology graph is likely to be exponentially large (no matter the algorithm used to compute it), and the second is that it's still a disjunctive graph, so it allows for choices between tech dependencies, and it's unclear what to do with this information. Factorio does not allow disjunctive dependencies between techs - you can't say that technology A can be unlocked by researching either technology B or C - so we can't just apply the technology graph to Factorio. We'd have to turn it into a non-disjunctive dependency graph first somehow. - -=== The solution === - -The solution to the above problem is to de-disjunctify the recipe graph. The plan is to pick for every disjunctive edge a 'canonical' choice, and transform the edge into a normal one. For example, for petroleum in the base game, we can get it via several recipes like cracking, but we will choose basic oil processing as the 'canonical' choice, because it's the first recipe you can get that can make petroleum. Furthermore, all the other recipes that make petroleum need petroleum themselves to research (for advanced oil processing) or to make one of the ingredients (barrel of petroleum), so they are not valid canonical choices. The resulting non-disjunctive recipe graph no longer allows all possible playthroughs of the modpack, but it still has useful information about the dependencies between Factorio things. Examples where information is 'lost' are basically those where multiple options exist to get something, for example multiple recipes to make PyAL animals, or things like acid gas which you can obtain in several ways. In practice I don't think this matters though. - -After picking a canonical choice for all edges in the recipe graph, it has become a normal directed graph, and we can compute the tech graph from it much faster than in exponential time. This tech graph is now a normal graph as well, which means we can use it as a basis for computing tech dependencies as intended. - -We do have to pick canonical chocies in such a way that the graph stays useful as mentioned earlier. The way we will do this is by computing a linear order first, and then resolving all disjunctive edges according to the first element in the computed linear order. In other words, for the case of choosing which recipe to use for your first Vrauk, we look at what the linear order did, and then choose that recipe. The advantage of this is that that linear order will still be a valid linear order on the resulting graph, so if the victory tech was reachable before, it would still be reachable after this de-disjunctivication. - -So, in summary: -- compute the disjunctive recipe graph -- compute a linear order on this graph -- canonise the disjunctive recipe graph into a normal recipe graph according to the linear order -- compute the tech graph out of this normal recipe graph -- correct Factorio tech graph to match this tech graph, adapt tech costs - -That's the gist of the algorithm. There's some more smaller details we'll go over now. - -Two details about the recipe graph: we'll need a file that adds 'scripted dependencies' like guano to the recipe graph, and we'll need to respect the flag we have for e.g. soot sorting that should not lead to seeing soot as a source of silver. - -=== Logging versus reporting versus error === - -The new autotech will have 3 reporting levels. Error means something is broken and we cannot continue, which only happens in two scenarioes I believe: -- the victory tech is unreachable -- there is a dependency loop between technologies - -The second level is 'reporting', which is when it finds something suspicious but not broken, for example unreachable items or dependency loops outside the tech graph. These will be printed but will not stop the autotech process. - -The third level is 'logging', which is where it will print out every decision it makes. It will print out the full recipe graph, linear order, canonicalisation choices, etc. This should help in investigating dependency loops. - -I have an algorithm to report a dependency loop when one is found, which should make debugging a lot easier. - -=== Transitive reduction === - -A lot of people know the term 'transitive closure', but that concept has a brother called 'transitive reduction'. The transitive reduction of a graph A is the graph where the most edges are removed without changing the reachability in the graph. For example, if A depends on B, B depends on C and A depends on C, then you can remove the 'A depends on C', because A can already reach C via B. This basically removes 'clutter' edges in the tech graph that don't actually do anything. - -I plan to have this be applied to the tech graph computed by autotech. This means it may actually end up _removing_ superfluous dependencies between techs, which is something the current autotech does not do. - -=== Unit tests === - -I want to make code to dump the data.raw table into a JSON file, so we can then run autotech on just this JSON file. This has several advantages: -- easier development, because you don't have to start the game every time you want to run the code -- automated regression testing becomes easier - you can just run some Python tests if you're modifying autotech after it already works diff --git a/prototypes/new_auto_tech/ammo_category_node.lua b/prototypes/new_auto_tech/ammo_category_node.lua deleted file mode 100644 index 0c9b1f0..0000000 --- a/prototypes/new_auto_tech/ammo_category_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local ammo_category_node = object_node_base:create_object_class("ammo category", node_types.ammo_category_node) - -function ammo_category_node:register_dependencies(nodes) -end - -return ammo_category_node diff --git a/prototypes/new_auto_tech/data/deque.lua b/prototypes/new_auto_tech/data/deque.lua deleted file mode 100644 index 926d5d5..0000000 --- a/prototypes/new_auto_tech/data/deque.lua +++ /dev/null @@ -1,51 +0,0 @@ -local push_left = function(self, value) - local first = self.first - 1 - self.first = first - self[first] = value -end - -local push_right = function(self, value) - local last = self.last + 1 - self.last = last - self[last] = value -end - -local pop_left = function(self) - local first = self.first - if first > self.last then error("self is empty") end - local value = self[first] - self[first] = nil - self.first = first + 1 - return value -end - -local pop_right = function(self) - local last = self.last - if self.first > last then error("self is empty") end - local value = self[last] - self[last] = nil - self.last = last - 1 - return value -end - - -local is_empty = function(self) - return self.last + 1 == self.first -end - -local methods = { - push_right = push_right, - push_left = push_left, - pop_right = pop_right, - pop_left = pop_left, - is_empty = is_empty, -} - -local new = function() - local r = { first = 0, last = -1 } - return setmetatable(r, { __index = methods }) -end - -return { - new = new, -} diff --git a/prototypes/new_auto_tech/electricity_node.lua b/prototypes/new_auto_tech/electricity_node.lua deleted file mode 100644 index ba4ab73..0000000 --- a/prototypes/new_auto_tech/electricity_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local electricity_node = object_node_base:create_unique_class("electricity", node_types.electricity_node) - -function electricity_node:register_dependencies(nodes) -end - -return electricity_node diff --git a/prototypes/new_auto_tech/electricity_verbs.lua b/prototypes/new_auto_tech/electricity_verbs.lua deleted file mode 100644 index c588c81..0000000 --- a/prototypes/new_auto_tech/electricity_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - generate = "generate", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/entity_node.lua b/prototypes/new_auto_tech/entity_node.lua deleted file mode 100644 index 40c5fa7..0000000 --- a/prototypes/new_auto_tech/entity_node.lua +++ /dev/null @@ -1,55 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" -local entity_verbs = require "prototypes.new_auto_tech.entity_verbs" -local resource_category_verbs = require "prototypes.new_auto_tech.resource_category_verbs" -local recipe_category_verbs = require "prototypes.new_auto_tech.recipe_category_verbs" -local item_verbs = require "prototypes.new_auto_tech.item_verbs" -local fluid_verbs = require "prototypes.new_auto_tech.fluid_verbs" -local electricity_verbs = require "prototypes.new_auto_tech.electricity_verbs" - -local entity_node = object_node_base:create_object_class("entity", node_types.entity_node) - -function entity_node:register_dependencies(nodes) - local entity = self.object - - if entity.type == "resource" then - self:add_dependency(nodes, node_types.resource_category_node, entity.category or "basic-solid", "resource category", "mine") - elseif entity.type == "mining-drill" then - self:add_disjunctive_dependent(nodes, node_types.resource_category_node, entity.resource_categories, "can mine", resource_category_verbs.instantiate) - elseif entity.type == "offshore-pump" then - self:add_disjunctive_dependent(nodes, node_types.fluid_node, entity.fluid, "pumps", fluid_verbs.create) - end - local minable = entity.minable - if minable ~= nil then - self:add_dependency(nodes, node_types.fluid_node, minable.required_fluid, "required fluid", "mine") - self:add_productlike_disjunctive_dependent(nodes, minable.result, minable.results, "mining result") - end - self:add_disjunctive_dependent(nodes, node_types.entity_node, entity.remains_when_mined, "remains when mined", entity_verbs.instantiate) - self:add_disjunctive_dependency(nodes, node_types.item_node, entity.placeable_by, "placeable by", entity_verbs.instantiate, "item") - self:add_disjunctive_dependent(nodes, node_types.item_node, entity.loot, "loot", item_verbs.create, "item") - self:add_disjunctive_dependent(nodes, node_types.entity_node, entity.corpse, "corpse", entity_verbs.instantiate) - if entity.energy_usage then - self:add_dependency(nodes, node_types.electricity_node, 1, "requires electricity", "power") - end - if entity.energy_source then - local energy_source = entity.energy_source - local type = energy_source.type - if type == "electric" then - self:add_disjunctive_dependent(nodes, node_types.electricity_node, 1, "generates electricity", electricity_verbs.generate) - elseif type == "burner" then - self:add_disjunctive_dependency(nodes, node_types.fuel_category_node, energy_source.fuel_category, "requires fuel", entity_verbs.fuel) - self:add_disjunctive_dependency(nodes, node_types.fuel_category_node, energy_source.fuel_categories, "requires fuel", entity_verbs.fuel) - elseif type == "heat" then - elseif type == "fluid" then - else - assert(type == "void", "Unknown energy source type") - end - end - self:add_disjunctive_dependency(nodes, node_types.fuel_category_node, entity.burner, "requires fuel", entity_verbs.fuel, "fuel_category") - self:add_disjunctive_dependent(nodes, node_types.recipe_category_node, entity.crafting_categories, "can craft", recipe_category_verbs.instantiate) - --fluid_boxes - --allowed_effects, module_specification - --inputs (labs) -end - -return entity_node diff --git a/prototypes/new_auto_tech/entity_verbs.lua b/prototypes/new_auto_tech/entity_verbs.lua deleted file mode 100644 index bf2b69b..0000000 --- a/prototypes/new_auto_tech/entity_verbs.lua +++ /dev/null @@ -1,4 +0,0 @@ -return { - instantiate = "instantiate", - fuel = "fuel", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/equipment_grid_node.lua b/prototypes/new_auto_tech/equipment_grid_node.lua deleted file mode 100644 index 5b3d414..0000000 --- a/prototypes/new_auto_tech/equipment_grid_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local equipment_grid_node = object_node_base:create_object_class("equipment grid", node_types.equipment_grid_node) - -function equipment_grid_node:register_dependencies(nodes) -end - -return equipment_grid_node diff --git a/prototypes/new_auto_tech/fluid_fuel_node.lua b/prototypes/new_auto_tech/fluid_fuel_node.lua deleted file mode 100644 index 78c485a..0000000 --- a/prototypes/new_auto_tech/fluid_fuel_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local fluid_fuel_node = object_node_base:create_unique_class("fluid fuel", node_types.fluid_fuel_node) - -function fluid_fuel_node:register_dependencies(nodes) -end - -return fluid_fuel_node diff --git a/prototypes/new_auto_tech/fluid_fuel_verbs.lua b/prototypes/new_auto_tech/fluid_fuel_verbs.lua deleted file mode 100644 index fe4a18d..0000000 --- a/prototypes/new_auto_tech/fluid_fuel_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - instantiate = "allow the use of", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/fluid_node.lua b/prototypes/new_auto_tech/fluid_node.lua deleted file mode 100644 index 2090c79..0000000 --- a/prototypes/new_auto_tech/fluid_node.lua +++ /dev/null @@ -1,14 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" -local fluid_fuel_verbs = require "prototypes.new_auto_tech.fluid_fuel_verbs" - -local fluid_node = object_node_base:create_object_class("fluid", node_types.fluid_node) - -function fluid_node:register_dependencies(nodes) - local fluid = self.object - if fluid.fuel_value ~= nil then - self:add_disjunctive_dependent(nodes, node_types.fluid_fuel_node, 1, "fuel value", fluid_fuel_verbs.instantiate) - end -end - -return fluid_node diff --git a/prototypes/new_auto_tech/fluid_verbs.lua b/prototypes/new_auto_tech/fluid_verbs.lua deleted file mode 100644 index 90d1be9..0000000 --- a/prototypes/new_auto_tech/fluid_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - create = "create", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/fuel_category_node.lua b/prototypes/new_auto_tech/fuel_category_node.lua deleted file mode 100644 index 67a51fd..0000000 --- a/prototypes/new_auto_tech/fuel_category_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local fuel_category_node = object_node_base:create_object_class("fuel category", node_types.fuel_category_node) - -function fuel_category_node:register_dependencies(nodes) -end - -return fuel_category_node diff --git a/prototypes/new_auto_tech/fuel_category_verbs.lua b/prototypes/new_auto_tech/fuel_category_verbs.lua deleted file mode 100644 index fe4a18d..0000000 --- a/prototypes/new_auto_tech/fuel_category_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - instantiate = "allow the use of", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/item_node.lua b/prototypes/new_auto_tech/item_node.lua deleted file mode 100644 index 256c92d..0000000 --- a/prototypes/new_auto_tech/item_node.lua +++ /dev/null @@ -1,22 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" -local entity_verbs = require "prototypes.new_auto_tech.entity_verbs" -local item_verbs = require "prototypes.new_auto_tech.item_verbs" -local fuel_category_verbs = require "prototypes.new_auto_tech.fuel_category_verbs" - -local item_node = object_node_base:create_object_class("item", node_types.item_node) - -function item_node:register_dependencies(nodes) - local item = self.object - self:add_disjunctive_dependent(nodes, node_types.entity_node, item.place_result, "place result", entity_verbs.instantiate) - --placed_as_equipment_result optional :: EquipmentID - self:add_disjunctive_dependent(nodes, node_types.fuel_category_node, item.fuel_category, "fuel category", fuel_category_verbs.instantiate) - self:add_disjunctive_dependent(nodes, node_types.item_node, item.burnt_result, "burnt result", item_verbs.create) - self:add_productlike_disjunctive_dependent(nodes, item.rocket_launch_product, item.rocket_launch_products, "rocket launch product") - - --AmmoItemPrototype - 'ammo' - --GunPrototype - 'gun' - --ArmorPrototype - 'armor' -end - -return item_node diff --git a/prototypes/new_auto_tech/item_verbs.lua b/prototypes/new_auto_tech/item_verbs.lua deleted file mode 100644 index 90d1be9..0000000 --- a/prototypes/new_auto_tech/item_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - create = "create", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/new_auto_tech.lua b/prototypes/new_auto_tech/new_auto_tech.lua deleted file mode 100644 index 505ec25..0000000 --- a/prototypes/new_auto_tech/new_auto_tech.lua +++ /dev/null @@ -1,192 +0,0 @@ -local configuration = { - verbose_logging = settings.startup["pypp-verbose-logging"].value, -} - -local deque = require "prototypes.new_auto_tech.data.deque" - -local node_types = require "prototypes.new_auto_tech.node_types" - -local ammo_category_node = require "prototypes.new_auto_tech.ammo_category_node" -local electricity_node = require "prototypes.new_auto_tech.electricity_node" -local entity_node = require "prototypes.new_auto_tech.entity_node" -local equipment_grid_node = require "prototypes.new_auto_tech.equipment_grid_node" -local fluid_fuel_node = require "prototypes.new_auto_tech.fluid_fuel_node" -local fluid_node = require "prototypes.new_auto_tech.fluid_node" -local fuel_category_node = require "prototypes.new_auto_tech.fuel_category_node" -local item_node = require "prototypes.new_auto_tech.item_node" -local recipe_category_node = require "prototypes.new_auto_tech.recipe_category_node" -local recipe_node = require "prototypes.new_auto_tech.recipe_node" -local resource_category_node = require "prototypes.new_auto_tech.resource_category_node" -local start_node = require "prototypes.new_auto_tech.start_node" -local technology_node = require "prototypes.new_auto_tech.technology_node" - -local auto_tech = {} -auto_tech.__index = auto_tech - -function auto_tech.create() - local a = {} - setmetatable(a, auto_tech) - - a.nodes_per_node_type = {} - - return a -end - -function auto_tech:run_phase(phase_function, phase_name) - log("Starting " .. phase_name) - phase_function(self) - log("Finished " .. phase_name) -end - -function auto_tech:run() - -- TODO: - -- armor and gun stuff, military entities - -- ignore soot results - -- miner with fluidbox - -- resources on map - -- fluid boxes on crafting entities - -- modules on crafting entities - -- robots and roboports - -- heat - -- labs - -- temperatures for fluids, boilers - -- techs enabled at start - - -- nodes to finish: - -- tech - - -- nodes finished: - -- recipe - -- item - -- fluid - -- resource - - self:run_phase(function() - self:run_phase(self.create_nodes, "recipe graph node creation") - self:run_phase(self.link_nodes, "recipe graph link creation") - self:run_phase(self.linearise_recipe_graph, "recipe graph linearisation") - self:run_phase(self.verify_end_tech_reachable, "verify end tech reachable") - self:run_phase(self.construct_tech_graph, "constructing tech graph") - self:run_phase(self.linearise_tech_graph, "tech graph linearisation") - self:run_phase(self.calculate_transitive_reduction, "transitive reduction calculation") - self:run_phase(self.adapt_tech_links, "adapting tech links") - self:run_phase(self.set_tech_costs, "tech cost setting") - end, "autotech") -end - -function auto_tech:create_nodes() - for _, node_type in pairs(node_types) do - self.nodes_per_node_type[node_type] = {} - end - - start_node:create(self.nodes_per_node_type, configuration) - electricity_node:create(self.nodes_per_node_type, configuration) - fluid_fuel_node:create(self.nodes_per_node_type, configuration) - - local function process_type(table, node_type) - for _, object in pairs(table) do - node_type:create(object, self.nodes_per_node_type, configuration) - end - end - - process_type(data.raw["ammo-category"], ammo_category_node) - process_type(data.raw["equipment-grid"], equipment_grid_node) - process_type(data.raw["fluid"], fluid_node) - process_type(data.raw["fuel-category"], fuel_category_node) - process_type(data.raw["recipe-category"], recipe_category_node) - process_type(data.raw["recipe"], recipe_node) - process_type(data.raw["resource-category"], resource_category_node) - process_type(data.raw["resource"], entity_node) - process_type(data.raw["technology"], technology_node) - - process_type(data.raw["armor"], item_node) - process_type(data.raw["ammo"], item_node) - process_type(data.raw["capsule"], item_node) - process_type(data.raw["gun"], item_node) - process_type(data.raw["item"], item_node) - process_type(data.raw["item-with-entity-data"], item_node) - process_type(data.raw["item-with-inventory"], item_node) - process_type(data.raw["item-with-label"], item_node) - process_type(data.raw["item-with-tags"], item_node) - process_type(data.raw["mining-tool"], item_node) - process_type(data.raw["module"], item_node) - process_type(data.raw["spidertron-remote"], item_node) - process_type(data.raw["rail-planner"], item_node) - process_type(data.raw["repair-tool"], item_node) - process_type(data.raw["tool"], item_node) - - for entity_name, _ in pairs(defines.prototypes.entity) do - for _, value in pairs(data.raw[entity_name]) do - entity_node:create(value, self.nodes_per_node_type, configuration) - end - end -end - -function auto_tech:link_nodes() - for _, node_type in pairs(self.nodes_per_node_type) do - for _, node in pairs(node_type) do - node:register_dependencies(self.nodes_per_node_type) - end - end -end - -function auto_tech:linearise_recipe_graph() - local verbose_logging = configuration.verbose_logging - local q = deque.new() - for _, node_type in pairs(self.nodes_per_node_type) do - for _, node in pairs(node_type) do - if node:has_no_more_dependencies() then - q:push_right(node) - if verbose_logging then - log("Node " .. node.printable_name .. " starts with no dependencies.") - end - end - end - end - - while (not q:is_empty()) do - local next = q:pop_left() - if verbose_logging then - log("Node " .. next.printable_name .. " is next in the linearisation.") - end - - local newly_independent_nodes = next:release_dependents() - for _, node in pairs(newly_independent_nodes) do - q:push_right(node) - end - end - - for _, node_type in pairs(self.nodes_per_node_type) do - for _, node in pairs(node_type) do - if not node:has_no_more_dependencies() then - log("Node " .. node.printable_name .. " still has unresolved dependencies: " .. node:print_dependencies()) - end - end - end -end - -function auto_tech:verify_end_tech_reachable() - -end - -function auto_tech:calculate_transitive_reduction() - -end - -function auto_tech:construct_tech_graph() - -end - -function auto_tech:linearise_tech_graph() - -end - -function auto_tech:adapt_tech_links() - -end - -function auto_tech:set_tech_costs() - -end - -return auto_tech diff --git a/prototypes/new_auto_tech/node_types.lua b/prototypes/new_auto_tech/node_types.lua deleted file mode 100644 index 7777428..0000000 --- a/prototypes/new_auto_tech/node_types.lua +++ /dev/null @@ -1,15 +0,0 @@ -return { - ammo_category_node = 1, - electricity_node = 2, - entity_node = 3, - equipment_grid_node = 4, - fluid_fuel_node = 5, - fluid_node = 6, - fuel_category_node = 7, - item_node = 8, - recipe_category_node = 9, - recipe_node = 10, - resource_category_node = 11, - start_node = 12, - technology_node = 13, -} diff --git a/prototypes/new_auto_tech/object_node_base.lua b/prototypes/new_auto_tech/object_node_base.lua deleted file mode 100644 index 95e44bc..0000000 --- a/prototypes/new_auto_tech/object_node_base.lua +++ /dev/null @@ -1,247 +0,0 @@ -local node_types = require "prototypes.new_auto_tech.node_types" -local item_verbs = require "prototypes.new_auto_tech.item_verbs" -local fluid_verbs = require "prototypes.new_auto_tech.fluid_verbs" - -local object_node_base = {} -object_node_base.__index = object_node_base - -function object_node_base:create_class_impl(type_name, node_type, create_method) - local object_class = { type_name = type_name } - object_class.__index = object_class - setmetatable(object_class, {__index = object_node_base}) - object_class.node_type = node_type - object_class.create = create_method - return object_class -end - -function object_node_base:create_unique_class(type_name, node_type) - return object_node_base:create_class_impl(type_name, node_type, object_node_base.create_unique) -end - -function object_node_base:create_object_class(type_name, node_type) - return object_node_base:create_class_impl(type_name, node_type, object_node_base.create_object) -end - -function object_node_base:create_object(object, nodes, configuration) - local s = {} - -- Yes, we're setting up the inheritance in the base class, this skips all the cumbersome base class calls we don't need anyway - setmetatable(s, self.__index) - s.configuration = configuration - s.depends = {} - s.reverse_depends = {} - s.disjunctive_depends = {} - s.disjunctive_depends_count = 0 - s.reverse_disjunctive_depends = {} - if object == nil then - s.printable_name = self.type_name - nodes[self.node_type][1] = s - else - s.object = object - s.printable_name = object.name .. " (" .. self.type_name .. ")" - nodes[self.node_type][object.name] = s - end - - -- These get changed in the linearisation - s.depends_count = 0 - s.canonicalised_choices = {} - s.canonicalised_choices_count = 0 - - if configuration.verbose_logging then - log("Created node for " .. s.printable_name) - end - - return s -end - -function object_node_base:has_no_more_dependencies() - return self.depends_count == 0 and self.disjunctive_depends_count == self.canonicalised_choices_count -end - -function object_node_base:print_dependencies() - local result = self.depends_count .. " fixed dependencies" - - if self.disjunctive_depends_count ~= self.canonicalised_choices_count then - result = result .. " and these disjunctive dependencies: " - for verb, _ in pairs(self.disjunctive_depends) do - if self.canonicalised_choices[verb] == nil then - result = result .. verb .. ", " - end - end - end - - return result -end - -function object_node_base:release_dependents() - local newly_independent_nodes = {} - local verbose_logging = self.configuration.verbose_logging - - for _, data in pairs(self.reverse_depends) do - local node = data[1] - local verb = data[2] - local dependency_type = data[3] - - node.depends_count = node.depends_count - 1 - if verbose_logging then - log("Virtually removing dependency from " .. node.printable_name .. " on " .. self.printable_name .. " to " .. verb .. " via " .. dependency_type) - end - if node:has_no_more_dependencies() then - newly_independent_nodes[#newly_independent_nodes+1] = node - if verbose_logging then - log("Node " .. node.printable_name .. " has no more dependencies.") - end - end - end - - for _, data in pairs(self.reverse_disjunctive_depends) do - local node = data[1] - local verb = data[2] - local dependency_type = data[3] - if node.canonicalised_choices[verb] ~= nil then - node.canonicalised_choices[verb] = {self, dependency_type} - node.canonicalised_choices_count = node.canonicalised_choices_count + 1 - if verbose_logging then - log("Canonising the dependency for " .. node.printable_name .. " for " .. verb .. " to be on " .. self.printable_name .. " via " .. dependency_type) - end - - if not node:still_has_dependencies() then - newly_independent_nodes[#newly_independent_nodes+1] = node - if verbose_logging then - log("Node " .. node.printable_name .. " has no more dependencies.") - end - end - end - end - - return newly_independent_nodes -end - -function object_node_base:create_unique(nodes, configuration) - return self:create_object(nil, nodes, configuration) -end - -function object_node_base:lookup_dependency(nodes, node_type, node_name) - local dependency = nodes[node_type][node_name] - if dependency == nil then - error("Could not find dependency " .. node_name .. " of type " .. node_type.type_name .. ", this is probably a bug in the data parser.") - end - return dependency -end - -function object_node_base:add_dependency_impl(dependency, dependency_type, verb) - local depends = self.depends - depends[#depends+1] = {dependency, dependency_type} - self.depends_count = self.depends_count + 1 - local reverse_depends = dependency.reverse_depends - reverse_depends[#reverse_depends+1] = {self, verb, dependency_type} - if self.configuration.verbose_logging then - log("In order to " .. verb .. " " .. self.printable_name .. " you require " .. dependency.printable_name .. " via " .. dependency_type) - end -end - -function object_node_base:add_disjunctive_dependency_impl(dependency, dependency_type, verb) - if self.disjunctive_depends[verb] == nil then - self.disjunctive_depends[verb] = {} - self.disjunctive_depends_count = self.disjunctive_depends_count + 1 - end - local target = self.disjunctive_depends[verb] - target[#target+1] = {dependency, dependency_type} - local reverse_disjunctive_depends = dependency.reverse_disjunctive_depends - reverse_disjunctive_depends[#reverse_disjunctive_depends+1] = {self, verb, dependency_type} - if self.configuration.verbose_logging then - log("In order to " .. verb .. " " .. self.printable_name .. " you could use " .. dependency.printable_name .. " via " .. dependency_type) - end -end - -function loop_if_table_ignore_nil(func, node_name, optional_inner_name) - function doCall(actual_node_name) - if optional_inner_name == nil then - func(actual_node_name) - else - func(actual_node_name[optional_inner_name]) - end - end - function doCallOnObject() - doCall(node_name) - end - function doCallOnTable() - for _, actual_node_name in pairs(node_name) do - doCall(actual_node_name) - end - end - - if node_name == nil then - return - end - if type(node_name) == "table" then - if optional_inner_name ~= nil then - -- have to distinguish between { item='fish', count=5 } and a table of such entries - if node_name[optional_inner_name] == nil then - doCallOnTable() - else - doCallOnObject() - end - else - doCallOnTable() - end - else - doCallOnObject() - end -end - -function object_node_base:add_dependency(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) - loop_if_table_ignore_nil(function (node_name_inner) - self:add_dependency_impl(self:lookup_dependency(nodes, node_type, node_name_inner), dependency_type, verb) - end, node_name, optional_inner_name) -end - -function object_node_base:add_disjunctive_dependency(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) - loop_if_table_ignore_nil(function (node_name_inner) - self:add_disjunctive_dependency_impl(self:lookup_dependency(nodes, node_type, node_name_inner), dependency_type, verb) - end, node_name, optional_inner_name) -end - -function object_node_base:add_dependent(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) - loop_if_table_ignore_nil(function (node_name_inner) - self:lookup_dependency(nodes, node_type, node_name_inner):add_dependency_impl(self, dependency_type, verb) - end, node_name, optional_inner_name) -end - -function object_node_base:add_disjunctive_dependent(nodes, node_type, node_name, dependency_type, verb, optional_inner_name) - loop_if_table_ignore_nil(function (node_name_inner) - self:lookup_dependency(nodes, node_type, node_name_inner):add_disjunctive_dependency_impl(self, dependency_type, verb) - end, node_name, optional_inner_name) -end - -function object_node_base:add_productlike_dependency(nodes, single_product, table_product, dependency_type, verb) - self:add_productlike_dependency_impl(nodes, single_product, table_product, dependency_type, function (self2, nodes2, node_type2, node_name2, dependency_type2, verb2) - self2:add_dependency(nodes2, node_type2, node_name2, dependency_type2, verb) - end) -end - -function object_node_base:add_productlike_disjunctive_dependent(nodes, single_product, table_product, dependency_type) - self:add_productlike_dependency_impl(nodes, single_product, table_product, dependency_type, self.add_disjunctive_dependent) -end - -function object_node_base:add_productlike_dependency_impl(nodes, single_product, table_product, dependency_type, dependency_function) - local function unwrap_result(wrapped_result) - return type(wrapped_result) == "table" and (wrapped_result.name or wrapped_result[1]) or wrapped_result - end - - if table_product ~= nil then - for _, result in pairs(table_product) do - local result_name = unwrap_result(result) - if result.type == "fluid" then - dependency_function(self, nodes, node_types.fluid_node, result_name, dependency_type, fluid_verbs.create) - else - dependency_function(self, nodes, node_types.item_node, result_name, dependency_type, item_verbs.create) - end - end - end - if single_product ~= nil then - local result_name = unwrap_result(single_product) - dependency_function(self, nodes, node_types.item_node, result_name, dependency_type, item_verbs.create) - end -end - -return object_node_base diff --git a/prototypes/new_auto_tech/recipe_category_node.lua b/prototypes/new_auto_tech/recipe_category_node.lua deleted file mode 100644 index 3ad5fd8..0000000 --- a/prototypes/new_auto_tech/recipe_category_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local recipe_category_node = object_node_base:create_object_class("recipe category", node_types.recipe_category_node) - -function recipe_category_node:register_dependencies(nodes) -end - -return recipe_category_node diff --git a/prototypes/new_auto_tech/recipe_category_verbs.lua b/prototypes/new_auto_tech/recipe_category_verbs.lua deleted file mode 100644 index fe4a18d..0000000 --- a/prototypes/new_auto_tech/recipe_category_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - instantiate = "allow the use of", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/recipe_node.lua b/prototypes/new_auto_tech/recipe_node.lua deleted file mode 100644 index 0a426db..0000000 --- a/prototypes/new_auto_tech/recipe_node.lua +++ /dev/null @@ -1,24 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" -local recipe_verbs = require "prototypes.new_auto_tech.recipe_verbs" - -local recipe_node = object_node_base:create_object_class("recipe", node_types.recipe_node) - -function recipe_node:register_dependencies(nodes) - local recipe = self.object - self:add_dependency(nodes, node_types.recipe_category_node, recipe.category or "crafting", "crafting category", "craft") - - local recipe_data = (type(recipe.normal) == "table" and (recipe.normal or recipe.expensive) or recipe) - - self:add_productlike_dependency(nodes, recipe_data.ingredient, recipe_data.ingredients, "ingredient", "craft") - - self:add_productlike_disjunctive_dependent(nodes, recipe_data.result, recipe_data.results, "result") - - if recipe_data.enabled ~= false then - self:add_disjunctive_dependency(nodes, node_types.start_node, 1, "starts enabled", recipe_verbs.enable) - end -end - -setmetatable(recipe_node, object_node_base); - -return recipe_node diff --git a/prototypes/new_auto_tech/recipe_verbs.lua b/prototypes/new_auto_tech/recipe_verbs.lua deleted file mode 100644 index b634142..0000000 --- a/prototypes/new_auto_tech/recipe_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - enable = "enable", -} diff --git a/prototypes/new_auto_tech/resource_category_node.lua b/prototypes/new_auto_tech/resource_category_node.lua deleted file mode 100644 index 3016670..0000000 --- a/prototypes/new_auto_tech/resource_category_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local resource_category_node = object_node_base:create_object_class("resource category", node_types.resource_category_node) - -function resource_category_node:register_dependencies(nodes) -end - -return resource_category_node diff --git a/prototypes/new_auto_tech/resource_category_verbs.lua b/prototypes/new_auto_tech/resource_category_verbs.lua deleted file mode 100644 index fe4a18d..0000000 --- a/prototypes/new_auto_tech/resource_category_verbs.lua +++ /dev/null @@ -1,3 +0,0 @@ -return { - instantiate = "allow the use of", -} \ No newline at end of file diff --git a/prototypes/new_auto_tech/start_node.lua b/prototypes/new_auto_tech/start_node.lua deleted file mode 100644 index e05ea02..0000000 --- a/prototypes/new_auto_tech/start_node.lua +++ /dev/null @@ -1,9 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" - -local start_node = object_node_base:create_unique_class("start", node_types.start_node) - -function start_node:register_dependencies(nodes) -end - -return start_node diff --git a/prototypes/new_auto_tech/technology_node.lua b/prototypes/new_auto_tech/technology_node.lua deleted file mode 100644 index 97e5311..0000000 --- a/prototypes/new_auto_tech/technology_node.lua +++ /dev/null @@ -1,23 +0,0 @@ -local object_node_base = require "prototypes.new_auto_tech.object_node_base" -local node_types = require "prototypes.new_auto_tech.node_types" -local item_verbs = require "prototypes.new_auto_tech.item_verbs" -local recipe_verbs = require "prototypes.new_auto_tech.recipe_verbs" - -local technology_node = object_node_base:create_object_class("technology", node_types.technology_node) - -function technology_node:register_dependencies(nodes) - local tech = self.object - local tech_data = (type(tech.normal) == "table" and (tech.normal or tech.expensive) or tech) - - self:add_dependency(nodes, node_types.technology_node, tech_data.prerequisites, "prerequisite", "enable") - - for _, modifier in pairs(tech_data.effects or {}) do - if modifier.type == "give-item" then - self:add_disjunctive_dependent(nodes, node_types.item_node, modifier.item, "given by tech", item_verbs.create) - elseif modifier.type == "unlock-recipe" then - self:add_disjunctive_dependent(nodes, node_types.recipe_node, modifier.recipe, "enabled by tech", recipe_verbs.enable) - end - end -end - -return technology_node From 2ac99258196505cc99faed244d5f5a5fac5cc80e Mon Sep 17 00:00:00 2001 From: Alex ten Brink Date: Sat, 3 Aug 2024 10:10:43 +0200 Subject: [PATCH 17/25] Fix accidental removal of a bit of code in autotech --- prototypes/functions/auto_tech.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/prototypes/functions/auto_tech.lua b/prototypes/functions/auto_tech.lua index 0facb88..024845f 100644 --- a/prototypes/functions/auto_tech.lua +++ b/prototypes/functions/auto_tech.lua @@ -304,6 +304,10 @@ function auto_tech:topo_sort_with_sp(fg, sp_graph, science_packs) end end + for _, link in pairs(sp_links) do + fg:remove_link(link.from, link.to, link.from.name) + end + local ts = fz_topo.create(fg) local error_found, errors = ts:run(false, self.verbose_logging) From 18667d03b4f7d067569965be6f0eff12d5000c62 Mon Sep 17 00:00:00 2001 From: Zachary Picco Date: Sun, 4 Aug 2024 17:35:53 -0500 Subject: [PATCH 18/25] Update changelog.txt --- changelog.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.txt b/changelog.txt index fdf87b6..594a868 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,9 @@ --------------------------------------------------------------------------------------------------- +Version: 0.2.25 +Date: 2024-8-4 + Changes: + - Fix crash on load with pyblock +--------------------------------------------------------------------------------------------------- Version: 0.2.24 Date: 2024-7-29 Changes: From bf2df7c80911ae3be9b158a7bc8196dbc7c7f5c7 Mon Sep 17 00:00:00 2001 From: kingarthur91 Date: Wed, 7 Aug 2024 22:21:00 -0400 Subject: [PATCH 19/25] updating the version number to match mod portal. NOTNOTMELON!!! --- info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.json b/info.json index 8a74c24..9d0757a 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "name": "pypostprocessing", - "version": "0.2.24", + "version": "0.2.25", "factorio_version": "1.1", "title": "Pyanodons Post-processing", "author": "Pyanodon, Shadowglass", From 16e9f92feff227d1ad3590b18a7dc37520cfc8ac Mon Sep 17 00:00:00 2001 From: kingarthur91 Date: Wed, 7 Aug 2024 23:34:58 -0400 Subject: [PATCH 20/25] Update info.json fixes pyanodon/pybugreports#505 --- info.json | 1 - 1 file changed, 1 deletion(-) diff --git a/info.json b/info.json index 9d0757a..2d3f131 100644 --- a/info.json +++ b/info.json @@ -31,7 +31,6 @@ "? DeadlocksStackingForPyanadon", "? LightedPolesPlus", "! ResearchFog", - "! science-pack-dependencies", "? jetpack" ], "package": { From 7fb1bf2178d19c725384d9c466c3f5482de92ab1 Mon Sep 17 00:00:00 2001 From: kingarthur91 Date: Wed, 7 Aug 2024 23:40:17 -0400 Subject: [PATCH 21/25] version and changelog --- changelog.txt | 5 +++++ info.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 594a868..c40aad4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,9 @@ --------------------------------------------------------------------------------------------------- +Version: 0.2.26 +Date: 2024-8-7 + Changes: + - Remove incompatibility with Science pack dependencies mod +--------------------------------------------------------------------------------------------------- Version: 0.2.25 Date: 2024-8-4 Changes: diff --git a/info.json b/info.json index 2d3f131..642a57d 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "name": "pypostprocessing", - "version": "0.2.25", + "version": "0.2.26", "factorio_version": "1.1", "title": "Pyanodons Post-processing", "author": "Pyanodon, Shadowglass", From 544549c1cf1e16392d530bd04ed5a9dc67aea9cd Mon Sep 17 00:00:00 2001 From: oorzkws <65210810+oorzkws@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:38:27 -0600 Subject: [PATCH 22/25] put the prod fix on the right branch Resolves pyanodon/pybugreports#574 and pyanodon/pybugreports#478 (for real this time) --- changelog.txt | 5 +++++ data-final-fixes.lua | 28 ++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index c40aad4..5174cf5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,9 @@ --------------------------------------------------------------------------------------------------- +Version: 0.2.27 +Date: 2024-???-??? + Bugfixes: + - Fix productivity blacklist being overly general and including recipes like empty barrels +--------------------------------------------------------------------------------------------------- Version: 0.2.26 Date: 2024-8-7 Changes: diff --git a/data-final-fixes.lua b/data-final-fixes.lua index ac36a57..3cd93eb 100644 --- a/data-final-fixes.lua +++ b/data-final-fixes.lua @@ -47,24 +47,40 @@ end for _, recipe in pairs(data.raw.recipe) do recipe.always_show_products = true recipe.always_show_made_in = true + local has_logged = false if recipe.results or recipe.result then if not recipe.results then recipe.results = {{name = recipe.result, amount = recipe.result_count or 1, type = 'item'}} recipe.result = nil recipe.result_count = nil end + -- Skip if recipe only produces the item, not uses it as a catalyst. + if #recipe.results == 1 then + goto NEXT_RECIPE + end for i, result in pairs(recipe.results) do local name = result.name or result[1] local amount = result.amount or result[2] - if name and config.NON_PRODDABLE_ITEMS[name] and not result.catalyst_amount then - if result[1] then - recipe.results[i] = {type = result.type or 'item', name = name, amount = amount, catalyst_amount = amount} - else - result.catalyst_amount = amount - end + if not name or not config.NON_PRODDABLE_ITEMS[name] or result.catalyst_amount then + goto NEXT_INGREDIENT + end + -- Convert to an explicitly long-form result format + if result[1] then + recipe.results[i] = { + type = result.type or 'item', + name = name, + amount = amount, + catalyst_amount = amount, + [1] = nil, + [2] = nil + } + else -- Just set the catalyst amount + result.catalyst_amount = amount end + ::NEXT_INGREDIENT:: end end + ::NEXT_RECIPE:: end ------------------------------------------- From 63bca721f94afda3e08bb7879c7ff65ca915ea94 Mon Sep 17 00:00:00 2001 From: kingarthur91 Date: Fri, 23 Aug 2024 23:33:15 -0400 Subject: [PATCH 23/25] version and changelog and minibuffer --- changelog.txt | 3 ++- info.json | 2 +- prototypes/functions/compatibility.lua | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5174cf5..379a2d2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,8 +1,9 @@ --------------------------------------------------------------------------------------------------- Version: 0.2.27 -Date: 2024-???-??? +Date: 2024-8-23 Bugfixes: - Fix productivity blacklist being overly general and including recipes like empty barrels + - add compatibility for minibuffer so its in automation science with storage tank tech --------------------------------------------------------------------------------------------------- Version: 0.2.26 Date: 2024-8-7 diff --git a/info.json b/info.json index 642a57d..4ffbd3f 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,6 @@ { "name": "pypostprocessing", - "version": "0.2.26", + "version": "0.2.27", "factorio_version": "1.1", "title": "Pyanodons Post-processing", "author": "Pyanodon, Shadowglass", diff --git a/prototypes/functions/compatibility.lua b/prototypes/functions/compatibility.lua index ca15294..4c531aa 100644 --- a/prototypes/functions/compatibility.lua +++ b/prototypes/functions/compatibility.lua @@ -593,4 +593,8 @@ if mods['jetpack'] and mods['pyrawores'] and mods['pypetroleumhandling'] then TECHNOLOGY("jetpack-2"):set_fields{prerequisites = {"jetpack-1"}}:remove_pack("chemical-science-pack"):add_pack("py-science-pack-2"):add_prereq(mods['pyalienlife'] and 'py-science-pack-mk02' or 'logistic-science-pack') TECHNOLOGY("jetpack-3"):set_fields{prerequisites = {"jetpack-2"}}:remove_pack("production-science-pack"):remove_pack("py-science-pack-4"):remove_pack("utility-science-pack"):add_pack("py-science-pack-3"):add_prereq(mods['pyalienlife'] and 'py-science-pack-mk03' or 'chemical-science-pack') TECHNOLOGY("jetpack-4"):set_fields{prerequisites = {"jetpack-3"}}:remove_pack("space-science-pack"):add_prereq('utility-science-pack') +end + +if mods["extra-storage-tank-minibuffer"] then + TECHNOLOGY("minibuffer"):remove_pack("logistic-science-pack") end \ No newline at end of file From a2bae74b83459a455fb617246e349b07c78e55cf Mon Sep 17 00:00:00 2001 From: oorzkws <65210810+oorzkws@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:36:21 -0600 Subject: [PATCH 24/25] warn on mismatched cage recipes in dev mode probably needs more logic but it's a start --- data-final-fixes.lua | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/data-final-fixes.lua b/data-final-fixes.lua index 3cd93eb..ab5a101 100644 --- a/data-final-fixes.lua +++ b/data-final-fixes.lua @@ -62,7 +62,7 @@ for _, recipe in pairs(data.raw.recipe) do local name = result.name or result[1] local amount = result.amount or result[2] if not name or not config.NON_PRODDABLE_ITEMS[name] or result.catalyst_amount then - goto NEXT_INGREDIENT + goto NEXT_RESULT end -- Convert to an explicitly long-form result format if result[1] then @@ -77,12 +77,48 @@ for _, recipe in pairs(data.raw.recipe) do else -- Just set the catalyst amount result.catalyst_amount = amount end - ::NEXT_INGREDIENT:: + ::NEXT_RESULT:: end end ::NEXT_RECIPE:: end +-- Scan for cages +if dev_mode then + for recipe_name, recipe in pairs(data.raw.recipe) do + if not recipe.ingredients then + goto NEXT_RECIPE_CAGECHECK + end + local cage_input = false + local cage_output = false + for i, ingredient in pairs(recipe.ingredients) do + local item_name = ingredient[1] or ingredient.name + if item_name:find('caged') then + cage_input = true + break + end + end + if not cage_input then + goto NEXT_RECIPE_CAGECHECK + end + if not recipe.results then + -- Don't log, probably a voiding recipe + goto NEXT_RECIPE_CAGECHECK + end + for i, result in pairs(recipe.results) do + local item_name = result[1] or result.name + if item_name:find('cage') then -- could be the same caged animal or an empty cage + cage_output = true + break + end + end + if cage_input and not cage_output then + log(string.format('Recipe \'%s\' takes a caged animal as input but does not return a cage', recipe_name)) + end + ::NEXT_RECIPE_CAGECHECK:: + end +end + ------------------------------------------- -- Resource category locale builder -- ------------------------------------------- From 5adbe583926b4e720ad4803e69ad12684b86e83a Mon Sep 17 00:00:00 2001 From: oorzkws <65210810+oorzkws@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:58:20 -0600 Subject: [PATCH 25/25] cage check: skip void & biomass that titanium becomes biomass? worms work in mysterious ways. --- data-final-fixes.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data-final-fixes.lua b/data-final-fixes.lua index ab5a101..f102106 100644 --- a/data-final-fixes.lua +++ b/data-final-fixes.lua @@ -86,6 +86,9 @@ end -- Scan for cages if dev_mode then for recipe_name, recipe in pairs(data.raw.recipe) do + if recipe_name:find('%-pyvoid$') or recipe_name:find('^biomass%-') then + goto NEXT_RECIPE_CAGECHECK + end if not recipe.ingredients then goto NEXT_RECIPE_CAGECHECK end