Первый фикс

Пачки некоторых позиций увеличены
This commit is contained in:
2024-03-01 20:53:32 +03:00
commit 7c9c708c92
23653 changed files with 767936 additions and 0 deletions

View File

@@ -0,0 +1,891 @@
--[[
Author: Scott Sullivan 2/23/2020
github: scottmsul
Algorithm Overview
------------------
The algorithm is based on the post here: https://kirkmcdonald.github.io/posts/calculation.html
We solve the matrix equation Ax = b, where:
- A is a matrix whose entry in row i and col j is the output/timescale/building for item i and recipe j
(negative is input, positive is output)
- x is the vector of unknowns that we're solving for, and whose jth entry will be the # buildings needed for recipe j
- b is the vector whose ith entry is the desired output/timescale for item i
Note the current implementation requires a square matrix_engine.
If there are more recipes than items, the problem is under-constrained and some recipes must be deleted.
If there are more items than recipes, the problem is over-constrained (this is more common).
In this case we can construct "pseudo-recipes" for certrain items that produce 1/timescale/"building".
Items with pseudo-recipes will be "free" variables that will have some constrained non-zero input or
output after solving.
The solved "number of buildings" will be equal to the extra input or output needed for that item.
Typically these pseudo-recipes will be for external inputs or non-fully-recycled byproducts.
Currently the algorithm assumes any item which is part of at least one input and one output in any recipe
is not a free variable, though the user can click on constrained items in the matrix dialog to make
them free variables.
The dialog calls constrained intermediate items "eliminated" since their output is constrained to zero.
If a recipe has loops, typically the user needs to make voids or free variables.
--]]
local structures = require("backend.calculation.structures")
local matrix_engine = {}
function matrix_engine.get_recipe_protos(recipe_ids)
local recipe_protos = {}
for i, recipe_id in ipairs(recipe_ids) do
local recipe_proto = global.prototypes.recipes[recipe_id]
recipe_protos[i] = recipe_proto
end
return recipe_protos
end
function matrix_engine.get_item_protos(item_keys)
local item_protos = {}
for i, item_key in ipairs(item_keys) do
local item_proto = matrix_engine.get_item(item_key)
item_protos[i] = item_proto
end
return item_protos
end
-- for our purposes the string "(item type id)_(item id)" is what we're calling the "item_key"
function matrix_engine.get_item_key(item_type_name, item_name)
local item = prototyper.util.find_prototype("items", item_name, item_type_name)
return tostring(item.category_id) .. '_' .. tostring(item.id)
end
function matrix_engine.get_item(item_key)
local split_str = util.split_string(item_key, "_")
local item_type_id, item_id = split_str[1], split_str[2]
return global.prototypes.items[item_type_id].members[item_id]
end
-- this is really only used for debugging
function matrix_engine.get_item_name(item_key)
local split_str = util.split_string(item_key, "_")
local item_type_id, item_id = split_str[1], split_str[2]
local item_info = global.prototypes.items[item_type_id].members[item_id]
return item_info.type.."_"..item_info.name
end
function matrix_engine.print_rows(rows)
local s = 'ROWS\n'
for i, k in ipairs(rows.values) do
local item_name = matrix_engine.get_item_name(k)
s = s..'ROW '..i..': '..item_name..'\n'
end
llog(s)
end
function matrix_engine.print_columns(columns)
local s = 'COLUMNS\n'
for i, k in ipairs(columns.values) do
local col_split_str = util.split_string(k, "_")
if col_split_str[1]=="line" then
s = s..'COL '..i..': '..k..'\n'
else
local item_key = col_split_str[2].."_"..col_split_str[3]
local item_name = matrix_engine.get_item_name(item_key)
s = s..'COL '..i..': '..item_name..'\n'
end
end
llog(s)
end
function matrix_engine.print_items_set(items)
local item_name_set = {}
for k, _ in pairs(items) do
local item_name = matrix_engine.get_item_name(k)
item_name_set[item_name] = k
end
llog(item_name_set)
end
function matrix_engine.print_items_list(items)
local item_name_set = {}
for _, k in ipairs(items) do
local item_name = matrix_engine.get_item_name(k)
item_name_set[item_name] = k
end
llog(item_name_set)
end
function matrix_engine.set_diff(a, b)
local result = {}
for k, _ in pairs(a) do
if not b[k] then
result[k] = true
end
end
return result
end
function matrix_engine.union_sets(...)
local arg = {...}
local result = {}
for _, set in pairs(arg) do
for val, _ in pairs(set) do
result[val] = true
end
end
return result
end
function matrix_engine.intersect_sets(...)
local arg = {...}
local counts = {}
local num_sets = #arg
for _, set in pairs(arg) do
for val, _ in pairs(set) do
if not counts[val] then
counts[val] = 1
else
counts[val] = counts[val] + 1
end
end
end
local result = {}
for k, count in pairs(counts) do
if count==num_sets then
result[k] = true
end
end
return result
end
function matrix_engine.num_elements(...)
local arg = {...}
local count = 0
for _, set in pairs(arg) do
for _, _ in pairs(set) do
count = count + 1
end
end
return count
end
function matrix_engine.get_matrix_solver_metadata(subfactory_data)
local eliminated_items = {}
local free_items = {}
local subfactory_metadata = matrix_engine.get_subfactory_metadata(subfactory_data)
local recipes = subfactory_metadata.recipes
local all_items = subfactory_metadata.all_items
local raw_inputs = subfactory_metadata.raw_inputs
local byproducts = subfactory_metadata.byproducts
local unproduced_outputs = subfactory_metadata.unproduced_outputs
local produced_outputs = matrix_engine.set_diff(subfactory_metadata.desired_outputs, unproduced_outputs)
local free_variables = matrix_engine.union_sets(raw_inputs, byproducts, unproduced_outputs)
local intermediate_items = matrix_engine.set_diff(all_items, free_variables)
if subfactory_data.matrix_free_items == nil then
eliminated_items = intermediate_items
else
-- by default when a subfactory is updated, add any new variables to eliminated and let the user select free.
local free_items_list = subfactory_data.matrix_free_items
for _, free_item in ipairs(free_items_list) do
local identifier = free_item.category_id.."_"..free_item.id
free_items[identifier] = true
end
-- make sure that any items that no longer exist are removed
free_items = matrix_engine.intersect_sets(free_items, intermediate_items)
eliminated_items = matrix_engine.set_diff(intermediate_items, free_items)
end
local num_rows = matrix_engine.num_elements(raw_inputs, byproducts, eliminated_items, free_items)
local num_cols = matrix_engine.num_elements(recipes, raw_inputs, byproducts, free_items)
local result = {
recipes = subfactory_metadata.recipes,
ingredients = matrix_engine.get_item_protos(matrix_engine.set_to_ordered_list(subfactory_metadata.raw_inputs)),
products = matrix_engine.get_item_protos(matrix_engine.set_to_ordered_list(produced_outputs)),
byproducts = matrix_engine.get_item_protos(matrix_engine.set_to_ordered_list(subfactory_metadata.byproducts)),
eliminated_items = matrix_engine.get_item_protos(matrix_engine.set_to_ordered_list(eliminated_items)),
free_items = matrix_engine.get_item_protos(matrix_engine.set_to_ordered_list(free_items)),
num_rows = num_rows,
num_cols = num_cols
}
return result
end
function matrix_engine.transpose(m)
local transposed = {}
if #m == 0 then
return transposed
end
for i=1, #m[1] do
local row = {}
for j=1, #m do
table.insert(row, m[j][i])
end
table.insert(transposed, row)
end
return transposed
end
function matrix_engine.get_linear_dependence_data(subfactory_data, matrix_metadata)
local num_rows = matrix_metadata.num_rows
local num_cols = matrix_metadata.num_cols
local linearly_dependent_recipes = {}
local linearly_dependent_items = {}
local allowed_free_items = {}
local linearly_dependent_cols = matrix_engine.run_matrix_solver(subfactory_data, true)
for col_name, _ in pairs(linearly_dependent_cols) do
local col_split_str = util.split_string(col_name, "_")
if col_split_str[1] == "recipe" then
local recipe_key = col_split_str[2]
linearly_dependent_recipes[recipe_key] = true
else -- "item"
local item_key = col_split_str[2].."_"..col_split_str[3]
linearly_dependent_items[item_key] = true
end
end
-- check which eliminated items could be made free while still retaining linear independence
if #linearly_dependent_cols == 0 and num_cols < num_rows then
local matrix_data = matrix_engine.get_matrix_data(subfactory_data)
local items = matrix_data.rows
local col_to_item = {}
for k, v in pairs(items.map) do
col_to_item[v] = k
end
local t_matrix = matrix_engine.transpose(matrix_data.matrix)
table.remove(t_matrix)
matrix_engine.to_reduced_row_echelon_form(t_matrix)
local t_linearly_dependent = matrix_engine.find_linearly_dependent_cols(t_matrix, false)
local eliminated_items = matrix_metadata.eliminated_items
local eliminated_keys = {}
for _, eliminated_item in ipairs(eliminated_items) do
local key = matrix_engine.get_item_key(eliminated_item.type, eliminated_item.name)
eliminated_keys[key] = eliminated_item
end
for col, _ in pairs(t_linearly_dependent) do
local item = col_to_item[col]
if eliminated_keys[item] then
allowed_free_items[item] = true
end
end
end
local result = {
linearly_dependent_recipes = matrix_engine.get_recipe_protos(
matrix_engine.set_to_ordered_list(linearly_dependent_recipes)),
linearly_dependent_items = matrix_engine.get_item_protos(
matrix_engine.set_to_ordered_list(linearly_dependent_items)),
allowed_free_items = matrix_engine.get_item_protos(
matrix_engine.set_to_ordered_list(allowed_free_items))
}
return result
end
function matrix_engine.get_matrix_data(subfactory_data)
local matrix_metadata = matrix_engine.get_matrix_solver_metadata(subfactory_data)
local matrix_free_items = matrix_metadata.free_items
local subfactory_metadata = matrix_engine.get_subfactory_metadata(subfactory_data)
local all_items = subfactory_metadata.all_items
local rows = matrix_engine.get_mapping_struct(all_items)
-- storing the line keys as "line_(lines index 1)_(lines index 2)_..." for arbitrary depths of subfloors
local function get_line_names(prefix, lines)
local line_names = {}
for i, line in ipairs(lines) do
local line_key = prefix.."_"..i
-- these are exclusive because only actual recipes are allowed to be inputs to the matrix solver
if line.subfloor == nil then
line_names[line_key] = true
else
local subfloor_line_names = get_line_names(line_key, line.subfloor.lines)
line_names = matrix_engine.union_sets(line_names, subfloor_line_names)
end
end
return line_names
end
local line_names = get_line_names("line", subfactory_data.top_floor.lines)
local raw_free_variables = matrix_engine.union_sets(subfactory_metadata.raw_inputs, subfactory_metadata.byproducts)
local free_variables = {}
for k, _ in pairs(raw_free_variables) do
free_variables["item_"..k] = true
end
for _, v in ipairs(matrix_free_items) do
local item_key = matrix_engine.get_item_key(v.type, v.name)
free_variables["item_"..item_key] = true
end
local col_set = matrix_engine.union_sets(line_names, free_variables)
local columns = matrix_engine.get_mapping_struct(col_set)
local matrix = matrix_engine.get_matrix(subfactory_data, rows, columns)
return {
matrix = matrix,
rows = rows,
columns = columns,
free_variables = free_variables,
matrix_free_items = matrix_free_items,
}
end
function matrix_engine.run_matrix_solver(subfactory_data, check_linear_dependence)
-- run through get_matrix_solver_metadata to check against recipe changes
local subfactory_metadata = matrix_engine.get_subfactory_metadata(subfactory_data)
local matrix_data = matrix_engine.get_matrix_data(subfactory_data)
local matrix = matrix_data.matrix
local columns = matrix_data.columns
local free_variables = matrix_data.free_variables
local matrix_free_items = matrix_data.matrix_free_items
matrix_engine.to_reduced_row_echelon_form(matrix)
if check_linear_dependence then
local linearly_dependent_cols = matrix_engine.find_linearly_dependent_cols(matrix, true)
local linearly_dependent_variables = {}
for col, _ in pairs(linearly_dependent_cols) do
local col_name = columns.values[col]
local col_split_str = util.split_string(col_name, "_")
if col_split_str[1] == "line" then
local floor = subfactory_data.top_floor
for i=2, #col_split_str-1 do
local line_table_id = col_split_str[i]
floor = floor.lines[line_table_id].subfloor
end
local line_table_id = col_split_str[#col_split_str]
local line = floor.lines[line_table_id]
local recipe_id = line.recipe_proto.id
linearly_dependent_variables["recipe_"..recipe_id] = true
else -- item
linearly_dependent_variables[col_name] = true
end
end
return linearly_dependent_variables
end
local function set_line_results(prefix, floor)
local floor_aggregate = structures.aggregate.init(subfactory_data.player_index, floor.id)
for i, line in ipairs(floor.lines) do
local line_key = prefix.."_"..i
local line_aggregate = nil
if line.subfloor == nil then
local col_num = columns.map[line_key]
-- want the j-th entry in the last column (output of row-reduction)
local machine_count = matrix[col_num][#columns.values+1]
line_aggregate = matrix_engine.get_line_aggregate(line, subfactory_data.player_index, floor.id,
machine_count, false, subfactory_metadata, free_variables)
else
line_aggregate = set_line_results(prefix.."_"..i, line.subfloor)
matrix_engine.consolidate(line_aggregate)
end
-- Lines with subfloors show actual number of machines to build, so each counts are rounded up when summed
floor_aggregate.machine_count = floor_aggregate.machine_count +
math.ceil(line_aggregate.machine_count - 0.001)
structures.aggregate.add_aggregate(line_aggregate, floor_aggregate)
solver.set_line_result{
player_index = subfactory_data.player_index,
floor_id = floor.id,
line_id = line.id,
machine_count = line_aggregate.machine_count,
energy_consumption = line_aggregate.energy_consumption,
pollution = line_aggregate.pollution,
production_ratio = line_aggregate.production_ratio,
uncapped_production_ratio = line_aggregate.uncapped_production_ratio,
Product = line_aggregate.Product,
Byproduct = line_aggregate.Byproduct,
Ingredient = line_aggregate.Ingredient,
fuel_amount = line_aggregate.fuel_amount
}
end
return floor_aggregate
end
local top_floor_aggregate = set_line_results("line", subfactory_data.top_floor)
local main_aggregate = structures.aggregate.init(subfactory_data.player_index, 1)
-- set main_aggregate free variables
for item_line_key, _ in pairs(free_variables) do
local col_num = columns.map[item_line_key]
local split_str = util.split_string(item_line_key, "_")
local item_key = split_str[2].."_"..split_str[3]
local item = matrix_engine.get_item(item_key)
local amount = matrix[col_num][#columns.values+1]
if amount < 0 then
-- counterintuitively, a negative amount means we have a negative number of "pseudo-buildings",
-- implying the item must be consumed to balance the matrix, hence it is a byproduct.
-- The opposite is true for ingredients.
structures.aggregate.add(main_aggregate, "Byproduct", item, -amount)
else
structures.aggregate.add(main_aggregate, "Ingredient", item, amount)
end
end
-- set products for unproduced items
for _, product in pairs(subfactory_data.top_level_products) do
local item_key = matrix_engine.get_item_key(product.proto.type, product.proto.name)
if subfactory_metadata.unproduced_outputs[item_key] then
local item = matrix_engine.get_item(item_key)
structures.aggregate.add(main_aggregate, "Product", item, product.amount)
end
end
solver.set_subfactory_result {
player_index = subfactory_data.player_index,
energy_consumption = top_floor_aggregate.energy_consumption,
pollution = top_floor_aggregate.pollution,
Product = main_aggregate.Product,
Byproduct = main_aggregate.Byproduct,
Ingredient = main_aggregate.Ingredient,
matrix_free_items = matrix_free_items
}
end
-- If an aggregate has items that are both inputs and outputs, deletes whichever is smaller and saves the net amount.
-- If the input and output are identical to within rounding error, delete from both.
-- This is mainly for calculating line aggregates with subfloors for the matrix solver.
function matrix_engine.consolidate(aggregate)
-- Items cannot be both products or byproducts, but they can be both ingredients and fuels.
-- In the case that an item appears as an output, an ingredient, and a fuel, delete from fuel first.
local function compare_classes(input_class, output_class)
for type, type_table in pairs(aggregate[output_class]) do
for item, output_amount in pairs(type_table) do
local item_table = {
type=type,
name=item
}
if aggregate[input_class][type] ~= nil then
if aggregate[input_class][type][item] ~= nil then
local input_amount = aggregate[input_class][type][item]
local net_amount = output_amount - input_amount
if net_amount > 0 then
structures.aggregate.subtract(aggregate, input_class, item_table, input_amount)
structures.aggregate.subtract(aggregate, output_class, item_table, input_amount)
else
structures.aggregate.subtract(aggregate, input_class, item_table, output_amount)
structures.aggregate.subtract(aggregate, output_class, item_table, output_amount)
end
end
end
end
end
end
compare_classes("Ingredient", "Product")
compare_classes("Ingredient", "Byproduct")
end
-- finds inputs and outputs for each line and desired outputs
function matrix_engine.get_subfactory_metadata(subfactory_data)
local desired_outputs = {}
for _, product in pairs(subfactory_data.top_level_products) do
local item_key = matrix_engine.get_item_key(product.proto.type, product.proto.name)
desired_outputs[item_key] = true
end
local lines_metadata = matrix_engine.get_lines_metadata(subfactory_data.top_floor.lines,
subfactory_data.player_index)
local line_inputs = lines_metadata.line_inputs
local line_outputs = lines_metadata.line_outputs
local unproduced_outputs = matrix_engine.set_diff(desired_outputs, line_outputs)
local all_items = matrix_engine.union_sets(line_inputs, line_outputs)
local raw_inputs = matrix_engine.set_diff(line_inputs, line_outputs)
local byproducts = matrix_engine.set_diff(matrix_engine.set_diff(line_outputs, line_inputs), desired_outputs)
return {
recipes = lines_metadata.line_recipes,
desired_outputs = desired_outputs,
all_items = all_items,
raw_inputs = raw_inputs,
byproducts = byproducts,
unproduced_outputs = unproduced_outputs
}
end
function matrix_engine.get_lines_metadata(lines, player_index)
local line_recipes = {}
local line_inputs = {}
local line_outputs = {}
for _, line in pairs(lines) do
if line.subfloor ~= nil then
local floor_metadata = matrix_engine.get_lines_metadata(line.subfloor.lines, player_index)
for _, subfloor_line_recipe in pairs(floor_metadata.line_recipes) do
table.insert(line_recipes, subfloor_line_recipe)
end
line_inputs = matrix_engine.union_sets(line_inputs, floor_metadata.line_inputs)
line_outputs = matrix_engine.union_sets(line_outputs, floor_metadata.line_outputs)
else
local line_aggregate = matrix_engine.get_line_aggregate(line, player_index, 1, 1, true)
for item_type_name, item_data in pairs(line_aggregate.Ingredient) do
for item_name, _ in pairs(item_data) do
local item_key = matrix_engine.get_item_key(item_type_name, item_name)
line_inputs[item_key] = true
end
end
for item_type_name, item_data in pairs(line_aggregate.Product) do
for item_name, _ in pairs(item_data) do
local item_key = matrix_engine.get_item_key(item_type_name, item_name)
line_outputs[item_key] = true
end
end
table.insert(line_recipes, line.recipe_proto.id)
end
end
return {
line_recipes = line_recipes,
line_inputs = line_inputs,
line_outputs = line_outputs
}
end
function matrix_engine.get_matrix(subfactory_data, rows, columns)
-- Returns the matrix to be solved.
-- Format is a list of lists, where outer lists are rows and inner lists are columns.
-- Rows are items and columns are recipes (or pseudo-recipes in the case of free items).
-- Elements have units of items/timescale/building, and are positive for outputs and negative for inputs.
-- initialize matrix to all zeros
local matrix = {}
for _=1, #rows.values do
local row = {}
for _=1, #columns.values+1 do -- extra +1 for desired output column
table.insert(row, 0)
end
table.insert(matrix, row)
end
-- loop over columns since it's easier to look up items for lines/free vars than vice-versa
for col_num=1, #columns.values do
local col_str = columns.values[col_num]
local col_split_str = util.split_string(col_str, "_")
local col_type = col_split_str[1]
if col_type == "item" then
local item_id = col_split_str[2].."_"..col_split_str[3]
local row_num = rows.map[item_id]
matrix[row_num][col_num] = 1
else -- "line"
local floor = subfactory_data.top_floor
for i=2, #col_split_str-1 do
local line_table_id = col_split_str[i]
floor = floor.lines[line_table_id].subfloor
end
local line_table_id = col_split_str[#col_split_str]
local line = floor.lines[line_table_id]
-- use amounts for 1 building as matrix entries
local line_aggregate = matrix_engine.get_line_aggregate(line, subfactory_data.player_index,
floor.id, 1, true)
for item_type_name, items in pairs(line_aggregate.Product) do
for item_name, amount in pairs(items) do
local item_key = matrix_engine.get_item_key(item_type_name, item_name)
local row_num = rows.map[item_key]
matrix[row_num][col_num] = matrix[row_num][col_num] + amount
end
end
for item_type_name, items in pairs(line_aggregate.Ingredient) do
for item_name, amount in pairs(items) do
local item_key = matrix_engine.get_item_key(item_type_name, item_name)
local row_num = rows.map[item_key]
matrix[row_num][col_num] = matrix[row_num][col_num] - amount
end
end
end
end
-- final column for desired output. Don't have to explicitly set constrained vars to zero
-- since matrix is initialized with zeros.
for _, product in ipairs(subfactory_data.top_level_products) do
local item_id = product.proto.category_id .. "_" .. product.proto.id
local row_num = rows.map[item_id]
-- will be nil for unproduced outputs
if row_num ~= nil then
local amount = product.amount
matrix[row_num][#columns.values+1] = amount
end
end
return matrix
end
function matrix_engine.get_line_aggregate(line_data, player_index, floor_id, machine_count, include_fuel_ingredient, subfactory_metadata, free_variables)
local line_aggregate = structures.aggregate.init(player_index, floor_id)
line_aggregate.machine_count = machine_count
-- the index in the subfactory_data.top_floor.lines table can be different from the line_id!
local recipe_proto = line_data.recipe_proto
local timescale = line_data.timescale
local total_effects = line_data.total_effects
local machine_speed = line_data.machine_proto.speed
local speed_multiplier = (1 + math.max(line_data.total_effects.speed, -0.8))
local energy = recipe_proto.energy
-- hacky workaround for recipes with zero energy - this really messes up the matrix
if energy==0 then energy=0.000000001 end
local time_per_craft = energy / (machine_speed * speed_multiplier)
local launch_sequence_time = line_data.machine_proto.launch_sequence_time
if launch_sequence_time then
time_per_craft = time_per_craft + launch_sequence_time
end
local unmodified_crafts_per_second = 1 / time_per_craft
local in_game_crafts_per_second = math.min(unmodified_crafts_per_second, 60)
local total_crafts_per_timescale = timescale * machine_count * in_game_crafts_per_second
line_aggregate.production_ratio = total_crafts_per_timescale
line_aggregate.uncapped_production_ratio = total_crafts_per_timescale
for _, product in pairs(recipe_proto.products) do
local prodded_amount = solver_util.determine_prodded_amount(product, unmodified_crafts_per_second, total_effects)
local item_key = matrix_engine.get_item_key(product.type, product.name)
if subfactory_metadata~= nil and (subfactory_metadata.byproducts[item_key] or free_variables["item_"..item_key]) then
structures.aggregate.add(line_aggregate, "Byproduct", product, prodded_amount * total_crafts_per_timescale)
else
structures.aggregate.add(line_aggregate, "Product", product, prodded_amount * total_crafts_per_timescale)
end
end
for _, ingredient in pairs(recipe_proto.ingredients) do
local amount = ingredient.amount
if ingredient.ignore_productivity then
amount = solver_util.determine_prodded_amount(ingredient, unmodified_crafts_per_second, total_effects)
end
structures.aggregate.add(line_aggregate, "Ingredient", ingredient, amount * total_crafts_per_timescale)
end
-- Determine energy consumption (including potential fuel needs) and pollution
local fuel_proto = line_data.fuel_proto
local energy_consumption, pollution = solver_util.determine_energy_consumption_and_pollution(
line_data.machine_proto, line_data.recipe_proto, line_data.fuel_proto, machine_count, line_data.total_effects)
local fuel_amount = nil
if fuel_proto ~= nil then -- Seeing a fuel_proto here means it needs to be re-calculated
fuel_amount = solver_util.determine_fuel_amount(energy_consumption, line_data.machine_proto.burner,
line_data.fuel_proto.fuel_value, timescale)
if include_fuel_ingredient then
local fuel = {type=fuel_proto.type, name=fuel_proto.name, amount=fuel_amount}
structures.aggregate.add(line_aggregate, "Ingredient", fuel, fuel_amount)
end
energy_consumption = 0 -- set electrical consumption to 0 when fuel is used
elseif line_data.machine_proto.energy_type == "void" then
energy_consumption = 0 -- set electrical consumption to 0 while still polluting
end
line_aggregate.energy_consumption = energy_consumption
line_aggregate.pollution = pollution
matrix_engine.consolidate(line_aggregate)
-- needed for interface.set_line_result
line_aggregate.fuel_amount = fuel_amount
return line_aggregate
end
function matrix_engine.print_matrix(m)
local s = ""
s = s.."{\n"
for _, row in ipairs(m) do
s = s.." {"
for j,col in ipairs(row) do
s = s..(col)
if j<#row then
s = s.." "
end
end
s = s.."}\n"
end
s = s.."}"
llog(s)
end
function matrix_engine.get_mapping_struct(input_set)
-- turns a set into a mapping struct (eg matrix rows or columns)
-- a "mapping struct" consists of a table with:
-- key "values" - array of set values in sort order
-- key "map" - map from input_set values to integers, where the integer is the position in "values"
local values = matrix_engine.set_to_ordered_list(input_set)
local map = {}
for i,k in ipairs(values) do
map[k] = i
end
local result = {
values = values,
map = map
}
return result
end
function matrix_engine.set_to_ordered_list(s)
local result = {}
for k, _ in pairs(s) do table.insert(result, k) end
table.sort(result)
return result
end
-- Contains the raw matrix solver. Converts an NxN+1 matrix to reduced row-echelon form.
-- Based on the algorithm from octave: https://fossies.org/dox/FreeMat-4.2-Source/rref_8m_source.html
function matrix_engine.to_reduced_row_echelon_form(m)
local num_rows = #m
if #m==0 then return m end
local num_cols = #m[1]
-- set tolerance based on max value in matrix
local max_value = 0
for i = 1, num_rows do
for j = 1, num_cols do
if math.abs(m[i][j]) > max_value then
max_value = math.abs(m[i][j])
end
end
end
local tolerance = 1e-10 * max_value
local pivot_row = 1
for curr_col = 1, num_cols do
-- find row with highest value in curr col as next pivot
local max_pivot_index = pivot_row
local max_pivot_value = m[pivot_row][curr_col]
for curr_row = pivot_row+1, num_rows do -- does this need an if-wrapper?
local curr_pivot_value = math.abs(m[curr_row][curr_col])
if math.abs(m[curr_row][curr_col]) > math.abs(max_pivot_value) then
max_pivot_index = curr_row
max_pivot_value = curr_pivot_value
end
end
if math.abs(max_pivot_value) < tolerance then
-- if highest value is approximately zero, set this row and all rows below to zero
for zero_row = pivot_row, num_rows do
m[zero_row][curr_col] = 0
end
else
-- swap current row with highest value row
for swap_col = curr_col, num_cols do
local temp = m[pivot_row][swap_col]
m[pivot_row][swap_col] = m[max_pivot_index][swap_col]
m[max_pivot_index][swap_col] = temp
end
-- normalize pivot row
local factor = m[pivot_row][curr_col]
for normalize_col = curr_col, num_cols do
m[pivot_row][normalize_col] = m[pivot_row][normalize_col] / factor
end
-- find nonzero cols in this row for the elimination step
nonzero_pivot_cols = {}
for update_col = curr_col+1, num_cols do
curr_pivot_col_value = m[pivot_row][update_col]
if curr_pivot_col_value ~= 0 then
nonzero_pivot_cols[update_col] = curr_pivot_col_value
end
end
-- eliminate current column from other rows
for update_row = 1, pivot_row - 1 do
if m[update_row][curr_col] ~= 0 then
for update_col, pivot_col_value in pairs(nonzero_pivot_cols) do
m[update_row][update_col] = m[update_row][update_col] - m[update_row][curr_col]*pivot_col_value
end
m[update_row][curr_col] = 0
end
end
for update_row = pivot_row+1, num_rows do
if m[update_row][curr_col] ~= 0 then
for update_col, pivot_col_value in pairs(nonzero_pivot_cols) do
m[update_row][update_col] = m[update_row][update_col] - m[update_row][curr_col]*pivot_col_value
end
m[update_row][curr_col] = 0
end
end
-- only add 1 if there is another leading 1 row
pivot_row = pivot_row + 1
if pivot_row > num_rows then
break
end
end
end
end
function matrix_engine.find_linearly_dependent_cols(matrix, ignore_last)
-- Returns linearly dependent columns from a row-reduced matrix
-- Algorithm works as follows:
-- For each column:
-- If this column has a leading 1, track which row maps to this column using the ones_map variable (eg cols 1, 2, 3, 5)
-- Otherwise, this column is linearly dependent (eg col 4)
-- For any non-zero rows in this col, the col which contains that row's leading 1 is also linearly dependent
-- (eg for col 4, we have row 2 -> col 2 and row 3 -> col 3)
-- The example below would give cols 2, 3, 4 as being linearly dependent (x's are non-zeros)
-- 1 0 0 0 0
-- 0 1 x x 0
-- 0 0 1 x 0
-- 0 0 0 0 1
-- I haven't proven this is 100% correct, this is just something I came up with
local row_index = 1
local num_rows = #matrix
local num_cols = #matrix[1]
if ignore_last then
num_cols = num_cols - 1
end
local ones_map = {}
local col_set = {}
for col_index=1, num_cols do
if (row_index <= num_rows) and (matrix[row_index][col_index]==1) then
ones_map[row_index] = col_index
row_index = row_index+1
else
col_set[col_index] = true
for i=1, row_index-1 do
if matrix[i][col_index] ~= 0 then
col_set[ones_map[i]] = true
end
end
end
end
return col_set
end
-- utility function that removes from a sorted array in place
function matrix_engine.remove(orig_table, value)
local i = 1
local found = false
while i<=#orig_table and (not found) do
local curr = orig_table[i]
if curr >= value then
found = true
end
if curr == value then
table.remove(orig_table, i)
end
i = i+1
end
end
-- utility function that inserts into a sorted array in place
function matrix_engine.insert(orig_table, value)
local i = 1
local found = false
while i<=#orig_table and (not found) do
local curr = orig_table[i]
if curr >= value then
found=true
end
if curr > value then
table.insert(orig_table, i, value)
end
i = i+1
end
if not found then
table.insert(orig_table, value)
end
end
-- Shallowly and naively copys the base level of the given table
function matrix_engine.shallowcopy(table)
local copy = {}
for key, value in pairs(table) do
copy[key] = value
end
return copy
end
return matrix_engine

View File

@@ -0,0 +1,282 @@
local structures = require("backend.calculation.structures")
-- Contains the 'meat and potatoes' calculation model that struggles with some more complex setups
local sequential_engine = {}
-- ** LOCAL UTIL **
local function update_line(line_data, aggregate)
local recipe_proto, machine_proto = line_data.recipe_proto, line_data.machine_proto
local total_effects, timescale = line_data.total_effects, line_data.timescale
-- Determine relevant products
local relevant_products, byproducts = {}, {}
for _, product in pairs(recipe_proto.products) do
if aggregate.Product[product.type][product.name] ~= nil then
table.insert(relevant_products, product)
else
table.insert(byproducts, product)
end
end
-- Determine production ratio
local production_ratio, uncapped_production_ratio = 0, 0
local crafts_per_tick = solver_util.determine_crafts_per_tick(machine_proto, recipe_proto, total_effects)
-- Determines the production ratio that would be needed to fully satisfy the given product
local function determine_production_ratio(relevant_product)
local demand = aggregate.Product[relevant_product.type][relevant_product.name]
local prodded_amount = solver_util.determine_prodded_amount(relevant_product,
crafts_per_tick, total_effects)
return (demand * (line_data.percentage / 100)) / prodded_amount
end
local relevant_product_count = #relevant_products
if relevant_product_count == 1 then
local relevant_product = relevant_products[1]
production_ratio = determine_production_ratio(relevant_product)
elseif relevant_product_count >= 2 then
local priority_proto = line_data.priority_product_proto
for _, relevant_product in ipairs(relevant_products) do
if priority_proto ~= nil then -- Use the priority product to determine the production ratio, if it's set
if relevant_product.type == priority_proto.type and relevant_product.name == priority_proto.name then
production_ratio = determine_production_ratio(relevant_product)
break
end
else -- Otherwise, determine the highest production ratio needed to fulfill every demand
local ratio = determine_production_ratio(relevant_product)
production_ratio = math.max(production_ratio, ratio)
end
end
end
uncapped_production_ratio = production_ratio -- retain the uncapped ratio for line_data
-- Limit the machine_count by reducing the production_ratio, if necessary
local machine_limit = line_data.machine_limit
if machine_limit.limit ~= nil then
local capped_production_ratio = solver_util.determine_production_ratio(crafts_per_tick,
machine_limit.limit, timescale, machine_proto.launch_sequence_time)
production_ratio = machine_limit.force_limit and capped_production_ratio
or math.min(production_ratio, capped_production_ratio)
end
-- Determines the amount of the given item, considering productivity
local function determine_amount_with_productivity(item)
local prodded_amount = solver_util.determine_prodded_amount(item, crafts_per_tick, total_effects)
return prodded_amount * production_ratio
end
-- Determine byproducts
local Byproduct = structures.class.init()
for _, byproduct in pairs(byproducts) do
local byproduct_amount = determine_amount_with_productivity(byproduct)
structures.class.add(Byproduct, byproduct, byproduct_amount)
structures.aggregate.add(aggregate, "Byproduct", byproduct, byproduct_amount)
end
-- Determine products
local Product = structures.class.init()
for _, product in ipairs(relevant_products) do
local product_amount = determine_amount_with_productivity(product)
local product_demand = aggregate.Product[product.type][product.name] or 0
if product_amount > product_demand then
local overflow_amount = product_amount - product_demand
structures.class.add(Byproduct, product, overflow_amount)
structures.aggregate.add(aggregate, "Byproduct", product, overflow_amount)
product_amount = product_demand -- desired amount
end
structures.class.add(Product, product, product_amount)
structures.aggregate.subtract(aggregate, "Product", product, product_amount)
end
-- Determine ingredients
local Ingredient = structures.class.init()
for _, ingredient in pairs(recipe_proto.ingredients) do
-- If productivity is to be ignored, un-apply it by applying the product-productivity to an ingredient,
-- effectively reversing the effect (this is way simpler than doing it properly)
local ingredient_amount = (ingredient.ignore_productivity) and determine_amount_with_productivity(ingredient)
or (ingredient.amount * production_ratio)
structures.class.add(Ingredient, ingredient, ingredient_amount)
-- Reduce the line-byproducts and -ingredients so only the net amounts remain
local byproduct_amount = Byproduct[ingredient.type][ingredient.name]
if byproduct_amount ~= nil then
structures.class.subtract(Byproduct, ingredient, ingredient_amount)
structures.class.subtract(Ingredient, ingredient, byproduct_amount)
end
end
structures.class.balance_items(Ingredient, aggregate, "Byproduct", "Product")
-- Determine machine count
local machine_count = solver_util.determine_machine_count(crafts_per_tick, production_ratio,
timescale, machine_proto.launch_sequence_time)
-- Add the integer machine count to the aggregate so it can be displayed on the origin_line
aggregate.machine_count = aggregate.machine_count + math.ceil(machine_count - 0.001)
-- Determine energy consumption (including potential fuel needs) and pollution
local fuel_proto = line_data.fuel_proto
local energy_consumption, pollution = solver_util.determine_energy_consumption_and_pollution(
machine_proto, recipe_proto, fuel_proto, machine_count, total_effects)
local fuel_amount = nil
if fuel_proto ~= nil then -- Seeing a fuel_proto here means it needs to be re-calculated
fuel_amount = solver_util.determine_fuel_amount(energy_consumption, machine_proto.burner,
fuel_proto.fuel_value, timescale)
local fuel_class = structures.class.init()
local fuel = {type=fuel_proto.type, name=fuel_proto.name, amount=fuel_amount}
structures.class.add(fuel_class, fuel)
-- Add fuel to the aggregate, consuming this line's byproducts first, if possible
structures.class.balance_items(fuel_class, aggregate, "Byproduct", "Product")
energy_consumption = 0 -- set electrical consumption to 0 when fuel is used
elseif machine_proto.energy_type == "void" then
energy_consumption = 0 -- set electrical consumption to 0 while still polluting
end
-- Include beacon energy consumption
energy_consumption = energy_consumption + line_data.beacon_consumption
aggregate.energy_consumption = aggregate.energy_consumption + energy_consumption
aggregate.pollution = aggregate.pollution + pollution
-- Update the actual line with the calculated results
solver.set_line_result {
player_index = aggregate.player_index,
floor_id = aggregate.floor_id,
line_id = line_data.id,
machine_count = machine_count,
energy_consumption = energy_consumption,
pollution = pollution,
production_ratio = production_ratio,
uncapped_production_ratio = uncapped_production_ratio,
Product = Product,
Byproduct = Byproduct,
Ingredient = Ingredient,
fuel_amount = fuel_amount
}
end
local function update_floor(floor_data, aggregate)
local desired_products = structures.class.copy(aggregate.Product)
for _, line_data in ipairs(floor_data.lines) do
local subfloor = line_data.subfloor
if subfloor ~= nil then
-- Convert proto product table to class for easier and faster access
local proto_products = structures.class.init()
for _, product in pairs(line_data.recipe_proto.products) do
proto_products[product.type][product.name] = true
end
-- Determine the products that are relevant for this subfloor
local subfloor_aggregate = structures.aggregate.init(aggregate.player_index, subfloor.id)
for _, product in ipairs(structures.class.to_array(aggregate.Product)) do
local type, name = product.type, product.name
if proto_products[type][name] ~= nil then
subfloor_aggregate.Product[type][name] = aggregate.Product[type][name]
end
end
local floor_products = structures.class.to_array(subfloor_aggregate.Product)
update_floor(subfloor, subfloor_aggregate) -- updates aggregate
-- Convert the internal product-format into positive products for the line and main aggregate
for _, product in pairs(floor_products) do
local aggregate_product_amount = subfloor_aggregate.Product[product.type][product.name] or 0
local production_difference = product.amount - aggregate_product_amount
if production_difference > 0 then
subfloor_aggregate.Product[product.type][product.name] = production_difference
else -- if the difference is negative or 0, the item turns out to consume more of this than it produces
structures.aggregate.subtract(subfloor_aggregate, "Product", product, aggregate_product_amount)
end
end
-- Update the main aggregate with the results
aggregate.machine_count = aggregate.machine_count + subfloor_aggregate.machine_count
aggregate.energy_consumption = aggregate.energy_consumption + subfloor_aggregate.energy_consumption
aggregate.pollution = aggregate.pollution + subfloor_aggregate.pollution
-- Subtract subfloor products as produced
for _, item in ipairs(structures.class.to_array(subfloor_aggregate.Product)) do
structures.aggregate.subtract(aggregate, "Product", item)
end
structures.class.balance_items(subfloor_aggregate.Ingredient, aggregate, "Byproduct", "Product")
structures.class.balance_items(subfloor_aggregate.Byproduct, aggregate, "Product", "Byproduct")
-- Update the parent line of the subfloor with the results from the subfloor aggregate
solver.set_line_result {
player_index = aggregate.player_index,
floor_id = aggregate.floor_id,
line_id = line_data.id,
machine_count = subfloor_aggregate.machine_count,
energy_consumption = subfloor_aggregate.energy_consumption,
pollution = subfloor_aggregate.pollution,
production_ratio = nil,
uncapped_production_ratio = nil,
Product = subfloor_aggregate.Product,
Byproduct = subfloor_aggregate.Byproduct,
Ingredient = subfloor_aggregate.Ingredient,
fuel_amount = nil
}
else
-- Update aggregate according to the current line, which also adjusts the respective line object
update_line(line_data, aggregate) -- updates aggregate
end
end
-- Convert all outstanding non-desired products to ingredients
for _, product in pairs(structures.class.to_array(aggregate.Product)) do
if desired_products[product.type][product.name] == nil then
structures.aggregate.add(aggregate, "Ingredient", product)
structures.aggregate.subtract(aggregate, "Product", product)
else
-- Add top level products that are also ingredients to the ingredients
local negative_amount = product.amount - desired_products[product.type][product.name]
if negative_amount > 0 then
structures.aggregate.add(aggregate, "Ingredient", product, negative_amount)
end
end
end
end
-- ** TOP LEVEL **
function sequential_engine.update_subfactory(subfactory_data)
-- Initialize aggregate with the top level items
local aggregate = structures.aggregate.init(subfactory_data.player_index, 1)
for _, product in ipairs(subfactory_data.top_level_products) do
structures.aggregate.add(aggregate, "Product", product)
end
update_floor(subfactory_data.top_floor, aggregate) -- updates aggregate
-- Fuels are combined with Ingredients for top-level purposes
solver.set_subfactory_result {
player_index = subfactory_data.player_index,
energy_consumption = aggregate.energy_consumption,
pollution = aggregate.pollution,
Product = aggregate.Product,
Byproduct = aggregate.Byproduct,
Ingredient = aggregate.Ingredient
}
end
return sequential_engine

View File

@@ -0,0 +1,360 @@
local sequential_engine = require("backend.calculation.sequential_engine")
local matrix_engine = require("backend.calculation.matrix_engine")
local structures = require("backend.calculation.structures")
solver, solver_util = {}, {}
-- ** LOCAL UTIL **
local function set_blank_line(player, floor, line)
local blank_class = structures.class.init()
solver.set_line_result{
player_index = player.index,
floor_id = floor.id,
line_id = line.id,
machine_count = 0,
energy_consumption = 0,
pollution = 0,
production_ratio = (not line.subfloor) and 0 or nil,
uncapped_production_ratio = (not line.subfloor) and 0 or nil,
Product = blank_class,
Byproduct = blank_class,
Ingredient = blank_class,
fuel_amount = 0
}
end
local function set_blank_subfactory(player, subfactory)
local blank_class = structures.class.init()
solver.set_subfactory_result {
player_index = player.index,
energy_consumption = 0,
pollution = 0,
Product = blank_class,
Byproduct = blank_class,
Ingredient = blank_class,
matrix_free_items = subfactory.matrix_free_items
}
-- Subfactory structure does not matter as every line just needs to be blanked out
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
set_blank_line(player, floor, line)
end
end
end
-- Generates structured data of the given floor for calculation
local function generate_floor_data(player, subfactory, floor)
local floor_data = {
id = floor.id,
lines = {}
}
local mining_productivity = (subfactory.mining_productivity ~= nil)
and (subfactory.mining_productivity / 100) or player.force.mining_drill_productivity_bonus
for _, line in ipairs(Floor.get_in_order(floor, "Line")) do
local line_data = { id = line.id }
if line.subfloor ~= nil then
line_data.recipe_proto = line.subfloor.defining_line.recipe.proto
line_data.subfloor = generate_floor_data(player, subfactory, line.subfloor)
table.insert(floor_data.lines, line_data)
else
local relevant_line = (line.parent.level > 1) and line.parent.defining_line or nil
-- If a line has a percentage of zero or is inactive, it is not useful to the result of the subfactory
-- Alternatively, if this line is on a subfloor and the top line of the floor is useless, it is useless too
if (relevant_line and (relevant_line.percentage == 0 or not relevant_line.active))
or line.percentage == 0 or not line.active then
set_blank_line(player, floor, line) -- useless lines don't need to run through the solver
else
line_data.recipe_proto = line.recipe.proto
line_data.timescale = subfactory.timescale
line_data.percentage = line.percentage -- non-zero
line_data.production_type = line.recipe.production_type
line_data.machine_limit = {limit=line.machine.limit, force_limit=line.machine.force_limit}
line_data.beacon_consumption = 0
line_data.priority_product_proto = line.priority_product_proto
line_data.machine_proto = line.machine.proto
-- Effects - update effects first if mining prod is relevant
if line.machine.proto.mining then Machine.summarize_effects(line.machine, mining_productivity) end
line_data.total_effects = line.total_effects
-- Fuel prototype
if line.machine.fuel ~= nil then line_data.fuel_proto = line.machine.fuel.proto end
-- Beacon total - can be calculated here, which is faster and simpler
if line.beacon ~= nil and line.beacon.total_amount ~= nil then
line_data.beacon_consumption = line.beacon.proto.energy_usage * line.beacon.total_amount * 60
end
table.insert(floor_data.lines, line_data)
end
end
end
return floor_data
end
-- Replaces the items of the given object (of given class) using the given result
local function update_object_items(object, item_class, item_results)
local object_class = _G[object.class]
object_class.clear(object, item_class)
for _, item_result in pairs(structures.class.to_array(item_results)) do
local required_amount = (object.class == "Subfactory") and 0 or nil
local item_proto = prototyper.util.find_prototype("items", item_result.name, item_result.type)
local item = Item.init(item_proto, item_class, item_result.amount, required_amount)
object_class.add(object, item)
end
end
local function set_zeroed_items(line, item_class, items)
Line.clear(line, item_class)
for _, item in pairs(items) do
local item_proto = prototyper.util.find_prototype("items", item.name, item.type)
Line.add(line, Item.init(item_proto, item_class, 0))
end
end
-- Goes through every line and setting their satisfied_amounts appropriately
local function update_ingredient_satisfaction(floor, product_class)
product_class = product_class or structures.class.init()
local function determine_satisfaction(ingredient)
local product_amount = product_class[ingredient.proto.type][ingredient.proto.name]
if product_amount ~= nil then
if product_amount >= (ingredient.amount or 0) then -- TODO dirty fix
ingredient.satisfied_amount = ingredient.amount
structures.class.subtract(product_class, ingredient)
else -- product_amount < ingredient.amount
ingredient.satisfied_amount = product_amount
structures.class.subtract(product_class, ingredient, product_amount)
end
else
ingredient.satisfied_amount = 0
end
end
-- Iterates the lines from the bottom up, setting satisfaction amounts along the way
for _, line in ipairs(Floor.get_in_order(floor, "Line", true)) do
if line.subfloor ~= nil then
local subfloor_product_class = structures.class.copy(product_class)
update_ingredient_satisfaction(line.subfloor, subfloor_product_class)
elseif line.machine.fuel then
determine_satisfaction(line.machine.fuel)
end
for _, ingredient in pairs(Line.get_in_order(line, "Ingredient")) do
if ingredient.proto.type ~= "entity" then
determine_satisfaction(ingredient)
end
end
-- Products and byproducts just get added to the list as being produced
for _, class_name in pairs{"Product", "Byproduct"} do
for _, product in pairs(Line.get_in_order(line, class_name)) do
structures.class.add(product_class, product)
end
end
end
end
-- ** TOP LEVEL **
-- Updates the whole subfactory calculations from top to bottom
function solver.update(player, subfactory)
if subfactory ~= nil and subfactory.valid then
local player_table = util.globals.player_table(player)
-- Save the active subfactory in global so the solver doesn't have to pass it around
player_table.active_subfactory = subfactory
local subfactory_data = solver.generate_subfactory_data(player, subfactory)
if subfactory.matrix_free_items ~= nil then -- meaning the matrix solver is active
local matrix_metadata = matrix_engine.get_matrix_solver_metadata(subfactory_data)
if matrix_metadata.num_cols > matrix_metadata.num_rows and #subfactory.matrix_free_items > 0 then
subfactory.matrix_free_items = {}
subfactory_data = solver.generate_subfactory_data(player, subfactory)
matrix_metadata = matrix_engine.get_matrix_solver_metadata(subfactory_data)
end
if matrix_metadata.num_rows ~= 0 then -- don't run calculations if the subfactory has no lines
local linear_dependence_data = matrix_engine.get_linear_dependence_data(subfactory_data, matrix_metadata)
if matrix_metadata.num_rows == matrix_metadata.num_cols
and #linear_dependence_data.linearly_dependent_recipes == 0 then
matrix_engine.run_matrix_solver(subfactory_data, false)
subfactory.linearly_dependant = false
else
set_blank_subfactory(player, subfactory) -- reset subfactory by blanking everything
-- Don't open the dialog if calculations are run during migration etc.
if main_dialog.is_in_focus(player) or player_table.ui_state.modal_dialog_type ~= nil then
util.raise.open_dialog(player, {dialog="matrix", allow_queueing=true})
end
end
else -- reset top level items
set_blank_subfactory(player, subfactory)
end
else
sequential_engine.update_subfactory(subfactory_data)
end
player_table.active_subfactory = nil -- reset after calculations have been carried out
end
end
-- Updates the given subfactory's ingredient satisfactions
function solver.determine_ingredient_satisfaction(subfactory)
update_ingredient_satisfaction(Subfactory.get(subfactory, "Floor", 1), nil)
end
-- ** INTERFACE **
-- Returns a table containing all the data needed to run the calculations for the given subfactory
function solver.generate_subfactory_data(player, subfactory)
local subfactory_data = {
player_index = player.index,
top_level_products = {},
top_floor = nil,
matrix_free_items = subfactory.matrix_free_items
}
for _, product in ipairs(Subfactory.get_in_order(subfactory, "Product")) do
local product_data = {
proto = product.proto, -- reference
amount = Item.required_amount(product)
}
table.insert(subfactory_data.top_level_products, product_data)
end
local top_floor = Subfactory.get(subfactory, "Floor", 1)
subfactory_data.top_floor = generate_floor_data(player, subfactory, top_floor)
return subfactory_data
end
-- Updates the active subfactories top-level data with the given result
function solver.set_subfactory_result(result)
local player_table = global.players[result.player_index]
local subfactory = player_table.active_subfactory
subfactory.energy_consumption = result.energy_consumption
subfactory.pollution = result.pollution
subfactory.matrix_free_items = result.matrix_free_items
-- If products are not present in the result, it means they have been produced
for _, product in pairs(Subfactory.get_in_order(subfactory, "Product")) do
local product_result_amount = result.Product[product.proto.type][product.proto.name] or 0
product.amount = Item.required_amount(product) - product_result_amount
end
update_object_items(subfactory, "Ingredient", result.Ingredient)
update_object_items(subfactory, "Byproduct", result.Byproduct)
-- Determine satisfaction-amounts for all line ingredients
if player_table.preferences.ingredient_satisfaction then
solver.determine_ingredient_satisfaction(subfactory)
end
end
-- Updates the given line of the given floor of the active subfactory
function solver.set_line_result(result)
local subfactory = global.players[result.player_index].active_subfactory
if subfactory == nil then return end
local floor = Subfactory.get(subfactory, "Floor", result.floor_id)
local line = Floor.get(floor, "Line", result.line_id)
if line.subfloor ~= nil then
line.machine = { count = result.machine_count }
else
line.machine.count = result.machine_count
if line.machine.fuel ~= nil then line.machine.fuel.amount = result.fuel_amount end
line.production_ratio = result.production_ratio
line.uncapped_production_ratio = result.uncapped_production_ratio
end
line.energy_consumption = result.energy_consumption
line.pollution = result.pollution
if line.production_ratio == 0 and line.subfloor == nil then
local recipe_proto = line.recipe.proto
set_zeroed_items(line, "Product", recipe_proto.products)
set_zeroed_items(line, "Ingredient", recipe_proto.ingredients)
else
update_object_items(line, "Product", result.Product)
update_object_items(line, "Byproduct", result.Byproduct)
update_object_items(line, "Ingredient", result.Ingredient)
end
end
-- **** UTIL ****
-- Speed can't go lower than 20%, or higher than 32676% due to the engine limit
local function cap_effect(value)
return math.min(math.max(value, MAGIC_NUMBERS.effects_lower_bound), MAGIC_NUMBERS.effects_upper_bound)
end
-- Determines the number of crafts per tick for the given data
function solver_util.determine_crafts_per_tick(machine_proto, recipe_proto, total_effects)
local machine_speed = machine_proto.speed * (1 + cap_effect(total_effects.speed))
return machine_speed / recipe_proto.energy
end
-- Determine the amount of machines needed to produce the given recipe in the given context
function solver_util.determine_machine_count(crafts_per_tick, production_ratio, timescale, launch_sequence_time)
crafts_per_tick = math.min(crafts_per_tick, 60) -- crafts_per_tick need to be limited for these calculations
return (production_ratio * (crafts_per_tick * (launch_sequence_time or 0) + 1)) / (crafts_per_tick * timescale)
end
-- Calculates the production ratio that the given amount of machines would result in
-- Formula derived from determine_machine_count(), isolating production_ratio and using machine_limit as machine_count
function solver_util.determine_production_ratio(crafts_per_tick, machine_limit, timescale, launch_sequence_time)
crafts_per_tick = math.min(crafts_per_tick, 60) -- crafts_per_tick need to be limited for these calculations
-- If launch_sequence_time is 0, the forumla is elegantly simplified to only the numerator
return (crafts_per_tick * machine_limit * timescale) / (crafts_per_tick * (launch_sequence_time or 0) + 1)
end
-- Calculates the product amount after applying productivity bonuses
function solver_util.determine_prodded_amount(item, crafts_per_tick, total_effects)
local productivity = math.max(total_effects.productivity, 0) -- no negative productivity
if productivity == 0 then return item.amount end
if crafts_per_tick > 60 then productivity = ((1/60) * productivity) * crafts_per_tick end
-- Return formula is a simplification of the following formula:
-- item.amount - item.proddable_amount + (item.proddable_amount * (productivity + 1))
return item.amount + (item.proddable_amount * productivity)
end
-- Determines the amount of energy needed for a machine and the pollution that produces
function solver_util.determine_energy_consumption_and_pollution(machine_proto, recipe_proto,
fuel_proto, machine_count, total_effects)
local consumption_multiplier = 1 + cap_effect(total_effects.consumption)
local energy_consumption = machine_count * (machine_proto.energy_usage * 60) * consumption_multiplier
local drain = math.ceil(machine_count - 0.001) * (machine_proto.energy_drain * 60)
local fuel_multiplier = (fuel_proto ~= nil) and fuel_proto.emissions_multiplier or 1
local pollution_multiplier = 1 + cap_effect(total_effects.pollution)
local pollution = energy_consumption * (machine_proto.emissions * 60) * pollution_multiplier
* fuel_multiplier * recipe_proto.emissions_multiplier
return (energy_consumption + drain), pollution
end
-- Determines the amount of fuel needed in the given context
function solver_util.determine_fuel_amount(energy_consumption, burner, fuel_value, timescale)
return ((energy_consumption / burner.effectivity) / fuel_value) * timescale
end

View File

@@ -0,0 +1,122 @@
-- Contains some structures and their 'methods' that are helpful during the calculation process
local structures = {
aggregate = {},
class = {}
}
function structures.aggregate.init(player_index, floor_id)
return {
player_index = player_index,
floor_id = floor_id,
machine_count = 0,
energy_consumption = 0,
pollution = 0,
production_ratio = nil,
uncapped_production_ratio = nil,
Product = structures.class.init(),
Byproduct = structures.class.init(),
Ingredient = structures.class.init()
}
end
-- Item might be an Item-object or a simple item {type, name, amount}
function structures.aggregate.add(aggregate, class_name, item, amount)
structures.class.add(aggregate[class_name], item, amount)
end
function structures.aggregate.subtract(aggregate, class_name, item, amount)
structures.class.add(aggregate[class_name], item, -(amount or item.amount))
end
-- Adds the first given aggregate to the second
function structures.aggregate.add_aggregate(from_aggregate, to_aggregate)
to_aggregate.energy_consumption = to_aggregate.energy_consumption + from_aggregate.energy_consumption
to_aggregate.pollution = to_aggregate.pollution + from_aggregate.pollution
for _, class in ipairs{"Product", "Byproduct", "Ingredient"} do
for _, item in ipairs(structures.class.to_array(from_aggregate[class])) do
structures.aggregate.add(to_aggregate, class, item)
end
end
end
function structures.class.init()
return {
item = {},
fluid = {},
entity = {}
}
end
-- Item might be an Item-object or a simple item {type, name, amount}
function structures.class.add(class, item, amount)
local type = (item.proto ~= nil) and item.proto.type or item.type
local name = (item.proto ~= nil) and item.proto.name or item.name
local amount_to_add = amount or item.amount
local type_table = class[type]
type_table[name] = (type_table[name] or 0) + amount_to_add
if type_table[name] == 0 then type_table[name] = nil end
end
function structures.class.subtract(class, item, amount)
structures.class.add(class, item, -(amount or item.amount))
end
-- Puts the items into their destination-class in the given aggregate, stopping for balancing
-- at the depot-class (Naming is hard, and that explanation is crap)
function structures.class.balance_items(class, aggregate, depot, destination)
for _, item in ipairs(structures.class.to_array(class)) do
local depot_amount = aggregate[depot][item.type][item.name]
if depot_amount ~= nil then -- Use up depot items, if available
if depot_amount >= item.amount then
structures.aggregate.subtract(aggregate, depot, item)
else
structures.aggregate.subtract(aggregate, depot, item, depot_amount)
structures.aggregate.add(aggregate, destination, item, (item.amount - depot_amount))
end
else -- add to destination if this item is not present in the depot
structures.aggregate.add(aggregate, destination, item)
end
end
end
-- Returns an array that contains every item in the given data structure
function structures.class.to_array(class)
local array = {}
for type, items_of_type in pairs(class) do
for name, amount in pairs(items_of_type) do
table.insert(array, {
name = name,
type = type,
amount = amount
})
end
end
return array
end
-- 'Deepcopies' the given class, with better performance than the generic deep copy
function structures.class.copy(class)
local copy = structures.class.init()
for type_name, type in pairs(class) do
local copy_type = copy[type_name]
for name, amount in pairs(type) do
copy_type[name] = amount
end
end
return copy
end
-- Counts the elements contained in the given class
function structures.class.count(class)
local n = 0
for _, items_of_type in pairs(class) do
n = n + table_size(items_of_type)
end
return n
end
return structures

View File

@@ -0,0 +1,139 @@
---@class FPBeacon
---@field proto FPBeaconPrototype
---@field amount number
---@field total_amount number
---@field module_set FPModuleSet
---@field total_effects ModuleEffects
---@field effects_tooltip string
---@field valid boolean
---@field parent FPLine
---@field class "Beacon"
-- This is a 'class' representing a (group of) beacon(s) and the modules attached to it
Beacon = {}
function Beacon.init(beacon_proto, beacon_amount, total_amount, parent)
local beacon = {
proto = beacon_proto,
amount = beacon_amount or 0,
total_amount = total_amount, -- can be nil
module_set = nil, -- set right below
total_effects = nil,
effects_tooltip = "",
valid = true,
parent = parent,
class = "Beacon"
}
beacon.module_set = ModuleSet.init(beacon)
return beacon
end
function Beacon.summarize_effects(self)
local effect_multiplier = self.proto.effectivity * self.amount
local effects = self.module_set.total_effects
for name, effect in pairs(effects) do
effects[name] = effect * effect_multiplier
end
self.total_effects = effects
self.effects_tooltip = util.gui.format_module_effects(effects, false)
Line.summarize_effects(self.parent)
end
function Beacon.check_module_compatibility(self, module_proto)
local recipe_proto, machine_proto = self.parent.recipe.proto, self.parent.machine.proto
if next(module_proto.limitations) and recipe_proto.use_limitations
and not module_proto.limitations[recipe_proto.name] then
return false
end
local machine_effects, beacon_effects = machine_proto.allowed_effects, self.proto.allowed_effects
if machine_effects == nil or beacon_effects == nil then
return false
else
for effect_name, _ in pairs(module_proto.effects) do
if machine_effects[effect_name] == false or beacon_effects[effect_name] == false then
return false
end
end
end
return true
end
function Beacon.paste(self, object)
if object.class == "Beacon" then
object.parent = self.parent
self.parent.beacon = object
Line.summarize_effects(self.parent)
return true, nil
elseif object.class == "Module" and self.module_set ~= nil then
-- Only allow modules to be pasted if this is a non-fake beacon
return ModuleSet.paste(self.module_set, object)
else
return false, "incompatible_class"
end
end
function Beacon.clone(self)
if not self then return nil end
local clone = Beacon.unpack(Beacon.pack(self))
clone.parent = self.parent
Beacon.validate(clone)
return clone
end
function Beacon.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, nil),
amount = self.amount,
total_amount = self.total_amount,
module_set = ModuleSet.pack(self.module_set),
class = self.class
}
end
function Beacon.unpack(packed_self)
local self = packed_self
self.module_set = ModuleSet.unpack(packed_self.module_set)
self.module_set.parent = self
return self
end
-- Needs validation: proto, module_set
function Beacon.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, nil)
self.valid = (not self.proto.simplified)
local machine = self.parent.machine -- make sure the machine can still be influenced by beacons
if machine.valid then self.valid = (machine.proto.allowed_effects ~= nil) and self.valid end
if BEACON_OVERLOAD_ACTIVE then self.amount = 1 end
self.valid = ModuleSet.validate(self.module_set) and self.valid
return self.valid
end
-- Needs repair: module_set
function Beacon.repair(self, _)
if self.proto.simplified then -- if still simplified, the beacon can't be repaired and needs to be removed
return false
else -- otherwise, the modules need to be checked and removed if necessary
-- Remove invalid modules and normalize the remaining ones
self.valid = ModuleSet.repair(self.module_set)
if self.module_set.module_count == 0 then return false end -- if the beacon is empty, remove it
end
self.valid = true -- if it gets to here, the beacon was successfully repaired
return true
end

View File

@@ -0,0 +1,209 @@
---@class FPCollection<T>
---@field datasets { [integer]: `T` }
---@field index integer
---@field count integer
---@field class "Collection"
-- 'Class' representing a list of objects/datasets with some useful methods
-- (An object only becomes a dataset once it is added to the collection)
Collection = {}
function Collection.init()
return {
datasets = {},
index = 0,
count = 0,
class = "Collection"
}
end
-- Adds given object to the end of the collection
function Collection.add(self, object)
if not object then error("Can't insert nil dataset") end
self.index = self.index + 1
object.id = self.index
self.datasets[self.index] = object
self.count = self.count + 1
object.gui_position = self.count
return object -- Returning it here feels nice
end
-- Inserts the given object at the given position, shifting other elements down
function Collection.insert_at(self, gui_position, object)
if not object then error("Can't insert nil dataset") end
if not gui_position then error("Can't insert at nil position") end
self.index = self.index + 1
object.id = self.index
self.count = self.count + 1
object.gui_position = gui_position
for _, dataset in pairs(self.datasets) do
if dataset.gui_position >= gui_position then
dataset.gui_position = dataset.gui_position + 1
end
end
self.datasets[self.index] = object
return object
end
function Collection.remove(self, dataset)
if not dataset then error("Can't remove nil dataset") end
-- Move positions of datasets after the deleted one down by one
for _, d in pairs(self.datasets) do
if d.gui_position > dataset.gui_position then
d.gui_position = d.gui_position - 1
end
end
self.count = self.count - 1
self.datasets[dataset.id] = nil
if self.count ~= table_size(self.datasets) then error("Dataset count incorrect") end
-- Returning the deleted position here to allow for GUI adjustments
return dataset.gui_position
end
-- Replaces the dataset with the new object in-place
function Collection.replace(self, dataset, object)
if not dataset then error("Can't replace nil dataset") end
if not object then error("Can't replace with nil object") end
object.id = dataset.id
object.gui_position = dataset.gui_position
self.datasets[dataset.id] = object
return object -- Returning it here feels nice
end
function Collection.get(self, object_id)
return self.datasets[object_id]
end
-- For when order doesn't matter
function Collection.get_all(self)
return self.datasets
end
-- Return format: {[gui_position] = dataset}
function Collection.get_in_order(self, reverse)
local ordered_datasets = {}
for _, dataset in pairs(self.datasets) do
local table_position = (reverse) and (self.count - dataset.gui_position + 1) or dataset.gui_position
ordered_datasets[table_position] = dataset
end
return ordered_datasets
end
-- Returns the dataset specified by the gui_position
function Collection.get_by_gui_position(self, gui_position)
if gui_position == 0 then return nil end
for _, dataset in pairs(self.datasets) do
if dataset.gui_position == gui_position then
return dataset
end
end
end
-- Returns the dataset with the given name, nil if it doesn't exist
function Collection.get_by_name(self, name)
for _, dataset in pairs(self.datasets) do
-- Check against the prototype, if it exists
local check_against = dataset.proto or dataset
if check_against.name == name then
return dataset
end
end
return nil
end
-- Returns the dataset with the given type and name, nil if it doesn't exist
function Collection.get_by_type_and_name(self, type_name, name)
for _, dataset in pairs(self.datasets) do
-- Check against the prototype, if it exists
local check_against = dataset.proto or dataset
if check_against.type == type_name and check_against.name == name then
return dataset
end
end
return nil
end
function Collection.shift(self, main_dataset, first_position, direction, spots)
if not main_dataset then error("Can't shift nil dataset") end
if not(direction == "negative" or direction == "positive") then error("Can't shift in invalid direction") end
local original_position = main_dataset.gui_position
local new_position = nil
if spots == nil then -- means shift-to-end
new_position = (direction == "positive") and self.count or first_position
else
if direction == "positive" then
new_position = math.min(original_position + spots, self.count)
else
new_position = math.max(original_position - spots, first_position)
end
end
Collection.remove(self, main_dataset)
Collection.insert_at(self, new_position, main_dataset)
end
-- Packs every dataset in this collection
function Collection.pack(self, object_class)
local packed_collection = {
objects = {},
class = self.class
}
for _, dataset in ipairs(Collection.get_in_order(self)) do
table.insert(packed_collection.objects, object_class.pack(dataset))
end
return packed_collection
end
-- Unpacks every dataset in this collection
function Collection.unpack(packed_self, parent, object_class)
local self = Collection.init()
self.class = packed_self.class
for _, object in ipairs(packed_self.objects) do -- packed objects already in array order
local dataset = Collection.add(self, object_class.unpack(object))
dataset.parent = parent
end
return self
end
-- Updates the validity of all datasets in this collection
function Collection.validate_datasets(self, object_class)
local valid = true
for _, dataset in pairs(self.datasets) do
-- Stays true until a single dataset is invalid, then stays false
valid = object_class.validate(dataset) and valid
end
return valid
end
-- Removes any invalid, unrepairable datasets from the collection
function Collection.repair_datasets(self, player, object_class)
for _, dataset in pairs(self.datasets) do
if not dataset.valid and not object_class.repair(dataset, player) then
_G[dataset.parent.class].remove(dataset.parent, dataset)
end
end
end

View File

@@ -0,0 +1,104 @@
---@class FPFactory
---@field Subfactory FPCollection<FPSubfactory>
---@field selected_subfactory FPSubfactory?
---@field export_modset ModToVersion
---@field class "Factory"
-- 'Class' representing the whole of a players actual data, including all subfactories
Factory = {}
---@return FPFactory
function Factory.init()
return {
Subfactory = Collection.init(),
selected_subfactory = nil,
-- A Factory can not become invalid
class = "Factory"
}
end
function Factory.add(self, object)
object.parent = self
return Collection.add(self[object.class], object)
end
function Factory.insert_at(self, gui_position, object)
object.parent = self
return Collection.insert_at(self[object.class], gui_position, object)
end
function Factory.remove(self, dataset)
local removed = Collection.remove(self[dataset.class], dataset)
if self.selected_subfactory and self.selected_subfactory.id == dataset.id then
self.selected_subfactory = self.Subfactory.datasets[1] -- can be nil
end
return removed
end
---@return FPSubfactory
function Factory.get(self, class, dataset_id)
return Collection.get(self[class], dataset_id)
end
---@return FPSubfactory[]
function Factory.get_all(self, class)
return Collection.get_all(self[class])
end
---@return FPSubfactory[]
function Factory.get_in_order(self, class, reverse)
return Collection.get_in_order(self[class], reverse)
end
function Factory.get_by_gui_position(self, class, gui_position)
return Collection.get_by_gui_position(self[class], gui_position)
end
function Factory.shift(self, dataset, first_position, direction, spots)
Collection.shift(self[dataset.class], dataset, first_position, direction, spots)
end
function Factory.count(self, class) return self[class].count end
-- Imports every subfactory in the given string to this Factory, returning a reference to the first one
---@return FPSubfactory
function Factory.import_by_string(self, export_string)
local import_factory = util.porter.process_export_string(export_string) ---@cast import_factory -nil
-- No error handling here, as the export_string for this will always be known to work
local first_subfactory = nil
for _, subfactory in pairs(Factory.get_in_order(import_factory, "Subfactory")) do
Factory.add(self, subfactory)
first_subfactory = first_subfactory or subfactory
end
return first_subfactory
end
-- Updates every top level product of this Factory to the given product definition type
function Factory.update_product_definitions(self, new_defined_by)
for _, subfactory in ipairs(Factory.get_in_order(self, "Subfactory")) do
Subfactory.update_product_definitions(subfactory, new_defined_by)
end
end
-- Updates the ingredient satisfaction data on every subfactory
function Factory.update_ingredient_satisfactions(self)
for _, subfactory in ipairs(Factory.get_in_order(self, "Subfactory")) do
solver.determine_ingredient_satisfaction(subfactory)
end
end
function Factory.update_calculations(self, player)
for _, subfactory in ipairs(Factory.get_in_order(self, "Subfactory")) do
solver.update(player, subfactory)
end
end
-- Needs validation: Subfactory
function Factory.validate(self)
Collection.validate_datasets(self.Subfactory, Subfactory)
-- Factories can't be invalid, this is just to cleanly validate the subfactories
end

View File

@@ -0,0 +1,187 @@
---@class FPFloor
---@field level integer
---@field origin_line FPLine?
---@field defining_line FPLine?
---@field Line FPCollection<FPLine>
---@field valid boolean
---@field id integer
---@field parent FPSubfactory
---@field class "Floor"
-- 'Class' representing a floor of a subfactory with individual assembly lines
Floor = {}
function Floor.init(creating_line)
local floor = {
level = 1, -- top floor has a level of 1, it's initialized with Floor.init(nil)
origin_line = nil, -- set below, only if level > 1. The line this subfloor is attached to
defining_line = nil, -- set below, only if level > 1. First line of this subfloor
Line = Collection.init(),
valid = true,
id = nil, -- set by collection
parent = nil, -- set by parent
class = "Floor"
}
-- Move given line, if it exists, to the subfloor, and create a new origin line
if creating_line ~= nil then
-- Subfloors have a level that is 1 higher than their creating_line's floor
floor.level = creating_line.parent.level + 1
floor.parent = creating_line.parent
local origin_line = Line.init(nil) -- No need to set a machine in this case
origin_line.subfloor = floor -- Link up the newly created origin_line with its subfloor
floor.origin_line = origin_line -- and vice versa
-- Replace the creating_line on its floor with the newly created origin_line
Floor.replace(creating_line.parent, creating_line, origin_line)
Floor.add(floor, creating_line) -- Add the creating_line to the subfloor in the first spot
floor.defining_line = creating_line -- which makes it the defining_line on this floor
end
return floor
end
function Floor.add(self, object)
object.parent = self
return Collection.add(self[object.class], object)
end
function Floor.insert_at(self, gui_position, object)
object.parent = self
return Collection.insert_at(self[object.class], gui_position, object)
end
function Floor.remove(self, dataset)
-- Remove the subfloor(s) associated to a line recursively, so they don't hang around
if dataset.class == "Line" and dataset.subfloor ~= nil then
for _, line in pairs(Floor.get_in_order(dataset.subfloor, "Line")) do
if line.subfloor then Floor.remove(dataset.subfloor, line) end
end
Collection.remove(self.parent.Floor, dataset.subfloor)
end
return Collection.remove(self[dataset.class], dataset)
end
-- Call only on subfloor; deletes itself while leaving defining_line intact
function Floor.reset(self)
local origin_line = self.origin_line
Floor.replace(origin_line.parent, origin_line, self.defining_line)
Subfactory.remove(self.parent, self)
end
function Floor.replace(self, dataset, object)
object.parent = self
return Collection.replace(self[dataset.class], dataset, object)
end
function Floor.get(self, class, dataset_id)
return Collection.get(self[class], dataset_id)
end
function Floor.get_all(self, class)
return Collection.get_all(self[class])
end
function Floor.get_in_order(self, class, reverse)
return Collection.get_in_order(self[class], reverse)
end
function Floor.get_by_gui_position(self, class, gui_position)
return Collection.get_by_gui_position(self[class], gui_position)
end
function Floor.shift(self, dataset, first_position, direction, spots)
Collection.shift(self[dataset.class], dataset, first_position, direction, spots)
end
function Floor.count(self, class) return self[class].count end
-- Returns the machines and modules needed to actually build this floor
function Floor.get_component_data(self, component_table)
local components = component_table or {machines={}, modules={}}
local function add_component(table, proto, amount)
local component = table[proto.name]
if component == nil then
table[proto.name] = {proto = proto, amount = amount}
else
component.amount = component.amount + amount
end
end
local function add_machine(entity_proto, amount)
if not entity_proto.built_by_item then return end
add_component(components.machines, entity_proto.built_by_item, amount)
end
-- Doesn't count subfloors when looking at this specific floors. Maybe it should, which
-- would mean the subfactory machine total is equal to the floor total of the top floor
for _, line in pairs(Floor.get_in_order(self, "Line")) do
if line.subfloor == nil then
local machine = line.machine
local ceil_machine_count = math.ceil(machine.count - 0.001)
add_machine(machine.proto, ceil_machine_count)
for _, module in pairs(ModuleSet.get_in_order(machine.module_set)) do
add_component(components.modules, module.proto, ceil_machine_count * module.amount)
end
local beacon = line.beacon
if beacon and beacon.total_amount then
local ceil_total_amount = math.ceil(beacon.total_amount - 0.001)
add_machine(beacon.proto, ceil_total_amount)
for _, module in pairs(ModuleSet.get_all(beacon.module_set)) do
add_component(components.modules, module.proto, ceil_total_amount * module.amount)
end
end
end
end
return components
end
function Floor.pack(self)
return {
Line = Collection.pack(self.Line, Line),
level = self.level,
class = self.class
}
end
-- This unpack-function differs in that it gets called with the floor already existing
-- This function should thus unpack itself into that floor, instead of creating a new one
function Floor.unpack(packed_self, self)
-- This can't use Collection.unpack for its lines because of its recursive nature
-- The calling function also needs to update its Subfactory to include the new subfloor references
for _, packed_line in pairs(packed_self.Line.objects) do
Floor.add(self, Line.unpack(packed_line, packed_self.level))
end
-- return value is not needed here
end
-- Needs validation: Line
function Floor.validate(self)
self.valid = Collection.validate_datasets(self.Line, Line)
return self.valid
end
-- Needs repair: Line
function Floor.repair(self, player)
-- Unrepairable lines get removed, so the subfactory will always be valid afterwards
Collection.repair_datasets(self.Line, player, Line)
self.valid = true
-- Make this floor remove itself if it's empty after repairs
if self.level > 1 and self.Line.count < 2 then Floor.reset(self) end
end

View File

@@ -0,0 +1,71 @@
---@class FPFuel
---@field proto FPFuelPrototype
---@field amount number
---@field satisfied_amount number
---@field valid boolean
---@field parent FPLine
---@field class "Fuel"
-- This is essentially just a wrapper-'class' for a fuel prototype to add some data to it
Fuel = {}
function Fuel.init(proto)
return {
proto = proto,
amount = 0, -- produced amount
satisfied_amount = 0, -- used with ingredient satisfaction
valid = true,
parent = nil,
class = "Fuel"
}
end
function Fuel.paste(self, object)
if object.class == "Fuel" then
local burner = self.parent.proto.burner -- will exist if there is fuel to paste on
for category_name, _ in pairs(burner.categories) do
if self.proto.category == category_name then
self.proto = object.proto
return true, nil
end
end
return false, "incompatible"
else
return false, "incompatible_class"
end
end
function Fuel.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.category),
amount = self.amount, -- conserve for cloning
class = self.class
}
end
function Fuel.unpack(packed_self)
return packed_self
end
-- Needs validation: proto
function Fuel.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "category")
self.valid = (not self.proto.simplified)
-- Make sure the fuel categories are still compatible
if self.valid and self.parent.valid then
local burner = self.parent.proto.burner
self.valid = burner and burner.categories[self.proto.category] ~= nil
end
return self.valid
end
-- Needs repair:
function Fuel.repair(_, _)
-- If the fuel-proto is still simplified, validate couldn't repair it, so it has to be removed
return false -- the parent machine will try to replace it with another fuel of the same category
end

View File

@@ -0,0 +1,157 @@
---@class RequiredAmount
---@field defined_by "amount" | "belts" | "lanes"
---@field amount number
---@field belt_proto FPBeltPrototype
---@class FPItem
---@field proto FPItemPrototype
---@field amount number
---@field required_amount RequiredAmount
---@field satisfied_amount number
---@field top_level boolean
---@field valid boolean
---@field id integer
---@field gui_position integer
---@field parent FPSubfactory | FPLine
---@field class "Product" | "Byproduct" | "Ingredient"
-- 'Class' representing an item in the general sense
Item = {}
Product, Byproduct, Ingredient = Item, Item, Item -- allows _G[class] to work for all items
-- Initialised by passing a prototype from the all_items global table
-- This is set up as a top-level item if a required_amount is given
function Item.init(proto, class, amount, required_amount)
-- Special case for non-product top level items
if required_amount == 0 then required_amount = {defined_by="amount", amount=0} end
return {
proto = proto,
amount = amount or 0, -- produced amount
required_amount = required_amount, -- is a table
satisfied_amount = 0, -- used with ingredient satisfaction
top_level = (required_amount ~= nil),
valid = true,
id = nil, -- set by collection
gui_position = nil, -- set by collection
parent = nil, -- set by parent
class = class
}
end
-- Returns the converted numeric required_amount for this (top level) item
function Item.required_amount(self)
local req_amount = self.required_amount
if req_amount.defined_by == "amount" then
return req_amount.amount
else -- defined_by == "belts"/"lanes"
-- If this is defined by lanes, only half of the throughput of a full belt needs to be considered
local multiplier = (req_amount.defined_by == "belts") and 1 or 0.5
local timescale = self.parent.timescale
return req_amount.amount * (req_amount.belt_proto.throughput * multiplier) * timescale
end
end
function Item.paste(self, object) -- this is implicitly only called on top level items
if object.class == "Product" or object.class == "Byproduct"
or object.class == "Ingredient" or object.class == "Fuel" then
local existing_item = Subfactory.get_by_name(self.parent, self.class, object.proto.name)
-- Avoid duplicate items, but allow pasting over the same item proto
if existing_item and existing_item.proto.name == object.proto.name
and not (self.proto.name == object.proto.name) then
return false, "already_exists"
end
-- Convert object into the appropriate top-level form if necessary
if not (object.top_level and object.class == self.class) then
local required_amount = {defined_by = "amount", amount = object.amount}
object = Item.init(object.proto, self.class, 0, required_amount)
end
-- Detect when this is called on a fake item and add instead of replacing
if not self.amount then Subfactory.add(self.parent, object)
else Subfactory.replace(self.parent, self, object) end
return true, nil
elseif object.class == "Line" then
local relevant_line = (object.subfloor) and object.subfloor.defining_line or object
for _, product in pairs(Line.get_in_order(relevant_line, "Product")) do
local fake_item = {proto={name=""}, parent=self.parent, class=self.class}
Item.paste(fake_item, product) -- avoid duplicating existing items
end
local top_floor = Subfactory.get(self.parent, "Floor", 1) -- line count can be 0
if object.subfloor then -- if the line has a subfloor, paste its contents on the top floor
local fake_line = {parent=top_floor, class="Line", gui_position=top_floor.Line.count}
for _, line in pairs(Floor.get_in_order(object.subfloor, "Line")) do
Line.paste(fake_line, line)
fake_line.gui_position = fake_line.gui_position + 1
end
else -- if the line has no subfloor, just straight paste it onto the top floor
local fake_line = {parent=top_floor, class="Line", gui_position=top_floor.Line.count}
Line.paste(fake_line, object)
end
return true, nil
else
return false, "incompatible_class"
end
end
function Item.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.type),
amount = self.amount, -- conserve for cloning non-product items
required_amount = (self.top_level) and {
defined_by = self.required_amount.defined_by,
amount = self.required_amount.amount,
belt_proto = (self.required_amount.defined_by ~= "amount")
and prototyper.util.simplify_prototype(self.required_amount.belt_proto, nil)
} or nil,
top_level = self.top_level,
class = self.class
}
end
function Item.unpack(packed_self)
return packed_self
end
-- Needs validation: proto, required_amount
function Item.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "type")
self.valid = (not self.proto.simplified)
-- Validate the belt_proto if the item proto is still valid, ie not simplified
local req_amount = self.required_amount
if req_amount and req_amount.defined_by ~= "amount" then
local belt_throughput = req_amount.belt_proto.throughput
req_amount.belt_proto = prototyper.util.validate_prototype_object(req_amount.belt_proto, nil)
self.valid = (not req_amount.belt_proto.simplified) and self.valid
-- If the proto has to be simplified, conserve the throughput, so repair can convert it to an amount-spec
if req_amount.belt_proto.simplified then req_amount.belt_proto.throughput = belt_throughput end
end
return self.valid
end
-- Needs repair: required_amount
-- This will only be called on top level items, so they can be treated as such
function Item.repair(self, _)
-- If the item-proto is still simplified, validate couldn't repair it, so it has to be removed
if self.proto.simplified then return false end
-- If the item is fine, the belt_proto has to be the things that is invalid. Thus, we will repair
-- this item by converting it to be defined by amount, so it can be preserved in some form
self.required_amount = {
defined_by = "amount",
amount = Item.required_amount(self)
}
self.valid = true
return self.valid
end

View File

@@ -0,0 +1,428 @@
---@class FPLine
---@field recipe FPRecipe?
---@field active boolean?
---@field done boolean
---@field percentage number
---@field machine FPMachine?
---@field beacon FPBeacon?
---@field total_effects ModuleEffects
---@field effects_tooltip LocalisedString
---@field energy_consumption number
---@field pollution number
---@field Product FPCollection<FPItem>
---@field Byproduct FPCollection<FPItem>
---@field Ingredient FPCollection<FPItem>
---@field priority_product_proto FPItemPrototype
---@field comment string?
---@field production_ratio number?
---@field uncapped_production_ratio number?
---@field subfloor FPFloor?
---@field valid boolean
---@field id integer
---@field gui_position integer
---@field parent FPFloor
---@field class "Line"
-- 'Class' representing an assembly line producing a recipe or representing a subfloor
Line = {}
function Line.init(recipe)
local is_standalone_line = (recipe ~= nil)
return {
recipe = recipe, -- can be nil
active = (is_standalone_line) and true or nil,
done = false,
percentage = (is_standalone_line) and 100 or nil,
machine = nil,
beacon = nil,
total_effects = nil,
effects_tooltip = "",
energy_consumption = 0,
pollution = 0,
Product = Collection.init(),
Byproduct = Collection.init(),
Ingredient = Collection.init(),
priority_product_proto = nil, -- set by the user
comment = nil,
production_ratio = (is_standalone_line) and 0 or nil,
uncapped_production_ratio = (is_standalone_line) and 0 or nil,
subfloor = nil,
valid = true,
id = nil, -- set by collection
gui_position = nil, -- set by collection
parent = nil, -- set by parent
class = "Line"
}
end
function Line.add(self, object)
object.parent = self
return Collection.add(self[object.class], object)
end
function Line.remove(self, dataset)
return Collection.remove(self[dataset.class], dataset)
end
function Line.replace(self, dataset, object)
object.parent = self
return Collection.replace(self[dataset.class], dataset, object)
end
function Line.clear(self, class)
self[class] = Collection.init()
end
function Line.get(self, class, dataset_id)
return Collection.get(self[class], dataset_id)
end
function Line.get_all(self, class)
return Collection.get_all(self[class])
end
function Line.get_in_order(self, class, reverse)
return Collection.get_in_order(self[class], reverse)
end
function Line.get_by_gui_position(self, class, gui_position)
return Collection.get_by_gui_position(self[class], gui_position)
end
function Line.get_by_name(self, class, name)
return Collection.get_by_name(self[class], name)
end
function Line.get_by_type_and_name(self, class, type, name)
return Collection.get_by_type_and_name(self[class], type, name)
end
-- Returns whether the given machine can be used for this line/recipe
function Line.is_machine_applicable(self, machine_proto)
local recipe_proto = self.recipe.proto
local valid_ingredient_count = (machine_proto.ingredient_limit >= recipe_proto.type_counts.ingredients.items)
local valid_input_channels = (machine_proto.fluid_channels.input >= recipe_proto.type_counts.ingredients.fluids)
local valid_output_channels = (machine_proto.fluid_channels.output >= recipe_proto.type_counts.products.fluids)
return (valid_ingredient_count and valid_input_channels and valid_output_channels)
end
-- Sets this line's machine to be the given prototype
function Line.change_machine_to_proto(self, player, proto)
if not self.machine then
self.machine = Machine.init(proto, self)
ModuleSet.summarize_effects(self.machine.module_set)
else
self.machine.proto = proto
ModuleSet.normalize(self.machine.module_set, {compatibility=true, trim=true, effects=true})
if self.machine.proto.allowed_effects == nil then Line.set_beacon(self, nil) end
end
-- Make sure the machine's fuel still applies
Machine.normalize_fuel(self.machine, player)
return true
end
-- Up- or downgrades this line's machine, if possible
-- Returns false if no compatible machine can be found, true otherwise
function Line.change_machine_by_action(self, player, action, current_proto)
local current_machine_proto = current_proto or self.machine.proto
local machines_category = PROTOTYPE_MAPS.machines[current_machine_proto.category]
local category_machines = global.prototypes.machines[machines_category.id].members
if action == "upgrade" then
local max_machine_id = #category_machines
while current_machine_proto.id < max_machine_id do
current_machine_proto = category_machines[current_machine_proto.id + 1]
if Line.is_machine_applicable(self, current_machine_proto) then
Line.change_machine_to_proto(self, player, current_machine_proto)
return true
end
end
else -- action == "downgrade"
while current_machine_proto.id > 1 do
current_machine_proto = category_machines[current_machine_proto.id - 1]
if Line.is_machine_applicable(self, current_machine_proto) then
Line.change_machine_to_proto(self, player, current_machine_proto)
return true
end
end
end
return false -- if the above loop didn't return, no machine could be found, so we return false
end
-- Changes this line's machine to its default, if possible
-- Returns false if no compatible machine can be found, true otherwise
function Line.change_machine_to_default(self, player)
local machine_category_id = PROTOTYPE_MAPS.machines[self.recipe.proto.category].id
-- All categories are guaranteed to have at least one machine, so this is never nil
local default_machine_proto = prototyper.defaults.get(player, "machines", machine_category_id)
-- If the default is applicable, just set it straight away
if Line.is_machine_applicable(self, default_machine_proto) then
Line.change_machine_to_proto(self, player, default_machine_proto)
return true
-- Otherwise, go up, then down the category to find an alternative
elseif Line.change_machine_by_action(self, player, "upgrade", default_machine_proto) then
return true
elseif Line.change_machine_by_action(self, player, "downgrade", default_machine_proto) then
return true
else -- if no machine in the whole category is applicable, return false
return false
end
end
function Line.set_beacon(self, beacon)
self.beacon = beacon -- can be nil
if beacon ~= nil then
self.beacon.parent = self
ModuleSet.normalize(self.beacon.module_set, {sort=true, effects=true})
else
Line.summarize_effects(self)
end
end
function Line.apply_mb_defaults(self, player)
ModuleSet.clear(self.machine.module_set)
Line.set_beacon(self, nil)
local mb_defaults = util.globals.preferences(player).mb_defaults
local machine_module, secondary_module = mb_defaults.machine, mb_defaults.machine_secondary
local module_set, module_limit = self.machine.module_set, self.machine.proto.module_limit
local message = nil
if machine_module and Machine.check_module_compatibility(self.machine, machine_module) then
ModuleSet.add(module_set, machine_module, module_limit)
elseif secondary_module and Machine.check_module_compatibility(self.machine, secondary_module) then
ModuleSet.add(module_set, secondary_module, module_limit)
elseif machine_module then -- only show an error if any module default is actually set
message = {text={"fp.warning_module_not_compatible", {"fp.pl_module", 1}}, category="warning"}
end
ModuleSet.summarize_effects(self.machine.module_set)
-- Add default beacon modules, if desired by the user
local beacon_module_proto, beacon_count = mb_defaults.beacon, mb_defaults.beacon_count
if BEACON_OVERLOAD_ACTIVE then beacon_count = 1 end
local beacon_proto = prototyper.defaults.get(player, "beacons") -- this will always exist
if beacon_module_proto ~= nil and beacon_count ~= nil then
local blank_beacon = Beacon.init(beacon_proto, beacon_count, nil, self)
if Beacon.check_module_compatibility(blank_beacon, beacon_module_proto) then
ModuleSet.add(blank_beacon.module_set, beacon_module_proto, beacon_proto.module_limit)
Line.set_beacon(self, blank_beacon) -- summarizes effects on its own
elseif message == nil then -- don't overwrite previous message, if it exists
message = {text={"fp.warning_module_not_compatible", {"fp.pl_beacon", 1}}, category="warning"}
end
end
return message
end
function Line.summarize_effects(self)
local beacon_effects = (self.beacon) and self.beacon.total_effects or nil
local effects = {consumption = 0, speed = 0, productivity = 0, pollution = 0}
for _, effect_table in pairs({self.machine.total_effects, beacon_effects}) do
for name, effect in pairs(effect_table) do
if name == "base_prod" or name == "mining_prod" then
effects["productivity"] = effects["productivity"] + effect
else
effects[name] = effects[name] + effect
end
end
end
self.total_effects = effects
self.effects_tooltip = util.gui.format_module_effects(effects, true)
end
-- Checks whether the given recipe's products are used on the given floor
-- The triple loop is crappy, but it's the simplest way to check
local function check_product_compatibiltiy(floor, recipe)
for _, product in pairs(recipe.proto.products) do
for _, line in pairs(Floor.get_all(floor, "Line")) do
for _, ingredient in pairs(Line.get_all(line, "Ingredient")) do
if ingredient.proto.type == product.type and ingredient.proto.name == product.name then
return true
end
end
end
end
return false
end
function Line.paste(self, object)
if object.class == "Line" then
if self.parent.level > 1 then -- make sure the recipe is allowed on this floor
local relevant_line = (object.subfloor) and object.subfloor.defining_line or object
if not check_product_compatibiltiy(self.parent, relevant_line.recipe) then
return false, "recipe_irrelevant" -- found no use for the recipe's products
end
end
if object.subfloor then Subfactory.add_subfloor_references(self.parent.parent, object) end
Floor.insert_at(self.parent, self.gui_position + 1, object)
return true, nil
else
return false, "incompatible_class"
end
end
function Line.pack(self)
local packed_line = {
comment = self.comment,
class = self.class
}
if self.subfloor ~= nil then
packed_line.subfloor = Floor.pack(self.subfloor)
else
packed_line.recipe = Recipe.pack(self.recipe)
packed_line.active = self.active
packed_line.done = self.done
packed_line.percentage = self.percentage
packed_line.machine = Machine.pack(self.machine)
packed_line.beacon = (self.beacon) and Beacon.pack(self.beacon) or nil
-- If this line has no priority_product, the function will return nil
local priority_proto = self.priority_product_proto
if priority_proto ~= nil then
packed_line.priority_product_proto = prototyper.util.simplify_prototype(priority_proto, priority_proto.type)
end
packed_line.Product = Collection.pack(self.Product, Item) -- conserve for cloning
end
return packed_line
end
function Line.unpack(packed_self, parent_level)
if packed_self.subfloor ~= nil then
local self = Line.init() -- empty line which gets the subfloor
local subfloor = Floor.init() -- empty floor to be manually adjusted as necessary
subfloor.level = parent_level + 1
subfloor.origin_line = self
self.subfloor = subfloor
Floor.unpack(packed_self.subfloor, subfloor)
subfloor.defining_line = subfloor.Line.datasets[1]
return self
else
local self = Line.init(packed_self.recipe)
self.active = packed_self.active
self.done = packed_self.done
self.percentage = packed_self.percentage
self.machine = Machine.unpack(packed_self.machine)
self.machine.parent = self
self.beacon = (packed_self.beacon) and Beacon.unpack(packed_self.beacon) or nil
if self.beacon then self.beacon.parent = self end
-- Effects summarized by the ensuing validation
-- The prototype will be automatically unpacked by the validation process
self.priority_product_proto = packed_self.priority_product_proto
self.comment = packed_self.comment
self.Product = Collection.unpack(packed_self.Product, self, Item) -- conserved for cloning
return self
end
end
-- Needs validation: recipe, machine, beacon, priority_product_proto, subfloor
function Line.validate(self)
self.valid = true
if self.subfloor then -- when this line has a subfloor, only the subfloor need to be checked
self.valid = Floor.validate(self.subfloor) and self.valid
else
self.valid = Recipe.validate(self.recipe) and self.valid
self.valid = Machine.validate(self.machine) and self.valid
if self.beacon then self.valid = Beacon.validate(self.beacon) and self.valid end
if self.priority_product_proto then
self.priority_product_proto = prototyper.util.validate_prototype_object(self.priority_product_proto, "type")
self.valid = (not self.priority_product_proto.simplified) and self.valid
end
self.valid = Collection.validate_datasets(self.Product, Item) and self.valid -- conserved for cloning
-- Effects summarized by machine/beacon validation
end
return self.valid
end
-- Needs repair: recipe, machine, beacon, priority_product_proto, subfloor
function Line.repair(self, player)
self.valid = true
if self.subfloor then
local subfloor = self.subfloor
if not subfloor.valid then
if not subfloor.defining_line.valid then
self.valid = false -- if the defining line is invalid, this whole thing is toast
else
Floor.repair(self.subfloor, player)
end
end
else
if not self.recipe.valid then
self.valid = Recipe.repair(self.recipe, nil)
end
if self.valid and not self.machine.valid then
self.valid = Machine.repair(self.machine, player)
end
if self.valid and self.beacon and not self.beacon.valid then
-- Repairing a beacon always either fixes or gets it removed, so no influence on validity
if not Beacon.repair(self.beacon, nil) then self.beacon = nil end
end
if self.valid and self.priority_product_proto and self.priority_product_proto.simplified then
self.priority_product_proto = nil
end
-- effects summarized by machine/beacon repair
end
-- Clear item prototypes so we don't need to rely on the solver to remove them
Line.clear(self, "Product")
Line.clear(self, "Byproduct")
Line.clear(self, "Ingredient")
return self.valid
end

View File

@@ -0,0 +1,189 @@
---@class FPMachine
---@field proto FPMachinePrototype
---@field count number
---@field limit number?
---@field force_limit boolean
---@field fuel FPFuel?
---@field module_set FPModuleSet
---@field total_effects ModuleEffects
---@field effects_tooltip string
---@field valid boolean
---@field parent FPLine
---@field class "Machine"
-- Class representing a machine with its attached modules and fuel
Machine = {}
function Machine.init(proto, parent)
local machine = {
proto = proto,
count = 0,
limit = nil, -- will be set by the user
force_limit = true, -- ignored if limit is not set
fuel = nil, -- needs to be set by calling Machine.find_fuel afterwards
module_set = nil, -- set right below
total_effects = nil,
effects_tooltip = "",
valid = true,
parent = parent,
class = "Machine"
}
machine.module_set = ModuleSet.init(machine)
return machine
end
function Machine.normalize_fuel(self, player)
if self.proto.energy_type ~= "burner" then self.fuel = nil; return end
-- no need to continue if this machine doesn't have a burner
local burner = self.proto.burner
-- Check if fuel has a valid category for this machine, replace otherwise
if self.fuel and not burner.categories[self.fuel.proto.category] then self.fuel = nil end
-- If this machine has fuel already, don't replace it
if self.fuel == nil then
-- Use the first category of this machine's burner as the default one
local fuel_category_name, _ = next(burner.categories, nil)
local fuel_category_id = PROTOTYPE_MAPS.fuels[fuel_category_name].id
local default_fuel_proto = prototyper.defaults.get(player, "fuels", fuel_category_id)
self.fuel = Fuel.init(default_fuel_proto)
self.fuel.parent = self
end
end
function Machine.summarize_effects(self, mining_prod)
local effects = self.module_set.total_effects
effects["base_prod"] = self.proto.base_productivity or nil
effects["mining_prod"] = mining_prod or nil
self.total_effects = effects
self.effects_tooltip = util.gui.format_module_effects(effects, false)
Line.summarize_effects(self.parent)
end
function Machine.check_module_compatibility(self, module_proto)
local recipe = self.parent.recipe
if self.proto.module_limit == 0 then return false end
if next(module_proto.limitations) and recipe.proto.use_limitations
and not module_proto.limitations[recipe.proto.name] then
return false
end
local allowed_effects = self.proto.allowed_effects
if allowed_effects == nil then
return false
else
for effect_name, _ in pairs(module_proto.effects) do
if allowed_effects[effect_name] == false then
return false
end
end
end
return true
end
function Machine.paste(self, object)
if object.class == "Machine" then
-- See if the pasted machine also exists in this machine's category, and use it if compatible
local found_machine = prototyper.util.find_prototype("machines", object.proto.name, self.proto.category)
if found_machine and Line.is_machine_applicable(self.parent, found_machine) then
self.parent.machine = object
object.parent = self.parent
object.proto = found_machine -- make sure to set this to the right category machine
ModuleSet.normalize(object.module_set, {compatibility=true, effects=true})
Line.summarize_effects(object.parent)
return true, nil
else
return false, "incompatible"
end
elseif object.class == "Module" then
return ModuleSet.paste(self.module_set, object)
else
return false, "incompatible_class"
end
end
function Machine.clone(self)
local clone = Machine.unpack(Machine.pack(self))
clone.parent = self.parent
clone.count = self.count -- keep around to avoid recalc being needed
Machine.validate(clone)
return clone
end
function Machine.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.category),
limit = self.limit,
force_limit = self.force_limit,
fuel = (self.fuel) and Fuel.pack(self.fuel) or nil,
module_set = ModuleSet.pack(self.module_set),
class = self.class
}
end
function Machine.unpack(packed_self)
local self = packed_self
self.fuel = (packed_self.fuel) and Fuel.unpack(packed_self.fuel) or nil
if self.fuel then self.fuel.parent = self end
self.module_set = ModuleSet.unpack(packed_self.module_set)
self.module_set.parent = self
return self
end
-- Needs validation: proto, fuel, module_set
function Machine.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "category")
self.valid = (not self.proto.simplified)
local parent_line = self.parent
if self.valid and parent_line.valid and parent_line.recipe.valid then
self.valid = Line.is_machine_applicable(parent_line, self.proto)
end
-- If the machine changed to not use a burner, remove its fuel
if not self.proto.burner then self.fuel = nil end
if self.fuel then self.valid = Fuel.validate(self.fuel) and self.valid end
self.valid = ModuleSet.validate(self.module_set) and self.valid
return self.valid
end
-- Needs repair: proto, fuel, module_set
function Machine.repair(self, player)
-- If the prototype is still simplified, it couldn't be fixed by validate
-- A final possible fix is to replace this machine with the default for its category
if self.proto.simplified and not Line.change_machine_to_default(self.parent, player) then
return false -- if this happens, the whole line can not be salvaged
end
self.valid = true -- if it gets to this, change_machine was successful and the machine is valid
-- It just might need to cleanup some fuel and/or modules
if self.fuel and not self.fuel.valid and not Fuel.repair(self.fuel) then
-- If fuel is unrepairable, replace it with a default value
self.fuel = nil
Machine.normalize_fuel(self, player)
end
-- Remove invalid modules and normalize the remaining ones
ModuleSet.repair(self.module_set)
return true
end

View File

@@ -0,0 +1,104 @@
---@class FPModule
---@field proto FPModulePrototype
---@field amount integer
---@field total_effects ModuleEffects
---@field effects_tooltip string
---@field valid boolean
---@field id integer
---@field gui_position integer
---@field parent FPModuleSet
---@field class "Module"
-- 'Class' representing an module
Module = {}
-- Initialised by passing a prototype from the all_moduless global table
function Module.init(proto, amount, parent)
local module = {
proto = proto,
amount = amount,
total_effects = nil,
effects_tooltip = "",
valid = true,
id = nil, -- set by collection
gui_position = nil, -- set by collection
parent = parent,
class = "Module"
}
Module.summarize_effects(module)
return module
end
function Module.set_amount(self, new_amount)
self.amount = new_amount
ModuleSet.count_modules(self.parent)
Module.summarize_effects(self)
end
function Module.summarize_effects(self)
local effects = {consumption = 0, speed = 0, productivity = 0, pollution = 0}
for name, effect in pairs(self.proto.effects) do
effects[name] = effect.bonus * self.amount
end
self.total_effects = effects
self.effects_tooltip = util.gui.format_module_effects(effects, false)
end
function Module.paste(self, object)
if object.class == "Module" then
if ModuleSet.check_compatibility(self.parent, object.proto) then
if ModuleSet.get_by_name(self.parent, object.proto.name) and object.proto.name ~= self.proto.name then
return false, "already_exists"
else
object.amount = math.min(object.amount, self.amount + self.parent.empty_slots)
Module.summarize_effects(object)
ModuleSet.replace(self.parent, self, object)
ModuleSet.summarize_effects(self.parent)
return true, nil
end
else
return false, "incompatible"
end
else
return false, "incompatible_class"
end
end
function Module.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.category),
amount = self.amount,
class = self.class
}
end
function Module.unpack(packed_self)
return packed_self
end
-- Needs validation: proto
function Module.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "category")
self.valid = (not self.proto.simplified)
-- Check whether the module is still compatible with its machine or beacon
if self.valid and self.parent.valid then
self.valid = _G[self.parent.parent.class].check_module_compatibility(self.parent.parent, self.proto)
end
if self.valid then Module.summarize_effects(self) end
return self.valid
end
-- Needs repair:
function Module.repair(_, _)
-- If the prototype is still simplified, it couldn't be fixed by validate, so it has to be removed
return false
end

View File

@@ -0,0 +1,240 @@
---@class FPModuleSet
---@field modules FPCollection<FPModule>
---@field module_count integer
---@field module_limit integer
---@field empty_slots integer
---@field total_effects ModuleEffects
---@field valid boolean
---@field parent FPMachine | FPBeacon
---@field class "ModuleSet"
-- 'Class' representing a group of modules in a machine or beacon
ModuleSet = {}
function ModuleSet.init(parent)
return {
modules = Collection.init(),
module_count = 0,
module_limit = parent.proto.module_limit,
empty_slots = parent.proto.module_limit,
total_effects = nil, -- summarized by calling function
valid = true,
parent = parent,
class = "ModuleSet"
}
end
function ModuleSet.add(self, proto, amount)
local object = Module.init(proto, amount, self)
local dataset = Collection.add(self.modules, object)
ModuleSet.count_modules(self)
return dataset
end
function ModuleSet.remove(self, dataset)
Collection.remove(self.modules, dataset)
ModuleSet.count_modules(self)
end
function ModuleSet.replace(self, dataset, object)
object.parent = self
local replacement = Collection.replace(self.modules, dataset, object)
ModuleSet.count_modules(self)
return replacement
end
function ModuleSet.clear(self)
self.modules = Collection.init()
ModuleSet.count_modules(self)
end
function ModuleSet.get(self, dataset_id)
return Collection.get(self.modules, dataset_id)
end
function ModuleSet.get_by_name(self, name)
return Collection.get_by_name(self.modules, name)
end
function ModuleSet.get_all(self)
return Collection.get_all(self.modules)
end
function ModuleSet.get_in_order(self, reverse)
return Collection.get_in_order(self.modules, reverse)
end
function ModuleSet.get_module_kind_amount(self)
return self.modules.count
end
function ModuleSet.normalize(self, features)
self.module_limit = self.parent.proto.module_limit
if features.compatibility then ModuleSet.verify_compatibility(self) end
if features.trim then ModuleSet.trim(self) end
if features.sort then ModuleSet.sort(self) end
if features.effects then ModuleSet.summarize_effects(self) end
ModuleSet.count_modules(self)
end
function ModuleSet.count_modules(self)
local count = 0
for _, module in pairs(self.modules.datasets) do
count = count + module.amount
end
self.module_count = count
self.empty_slots = self.module_limit - self.module_count
end
function ModuleSet.verify_compatibility(self)
local modules_to_remove = {}
for _, module in ipairs(ModuleSet.get_in_order(self)) do
if not ModuleSet.check_compatibility(self, module.proto) then
table.insert(modules_to_remove, module)
end
end
-- Actually remove incompatible modules; counts updated by calling function
for _, module in pairs(modules_to_remove) do ModuleSet.remove(self, module) end
end
function ModuleSet.trim(self)
local module_count, module_limit = self.module_count, self.module_limit
-- Return if the module count is within limits
if module_count <= module_limit then return end
local modules_to_remove = {}
-- Traverse modules in reverse to trim them off the end
for _, module in ipairs(ModuleSet.get_in_order(self, true)) do
-- Remove a whole module if it brings the count to >= limit
if (module_count - module.amount) >= module_limit then
table.insert(modules_to_remove, module)
module_count = module_count - module.amount
else -- Otherwise, diminish the amount on the module appropriately and break
local new_amount = module.amount - (module_count - module_limit)
Module.set_amount(module, new_amount)
break
end
end
-- Actually remove superfluous modules; counts updated by calling function
for _, module in pairs(modules_to_remove) do ModuleSet.remove(self, module) end
end
-- Sorts modules in a deterministic fashion so they are in the same order for every line
function ModuleSet.sort(self)
local modules_by_name = {}
for _, module in pairs(ModuleSet.get_all(self)) do
modules_by_name[module.proto.name] = module
end
local next_position = 1
for _, category in ipairs(global.prototypes.modules) do
for _, module_proto in ipairs(category.members) do
local module = modules_by_name[module_proto.name]
if module then
module.gui_position = next_position
next_position = next_position + 1
end
end
end
end
function ModuleSet.summarize_effects(self)
local effects = {consumption = 0, speed = 0, productivity = 0, pollution = 0}
for _, module in pairs(self.modules.datasets) do
for name, effect in pairs(module.total_effects) do
effects[name] = effects[name] + effect
end
end
self.total_effects = effects
_G[self.parent.class].summarize_effects(self.parent)
end
function ModuleSet.check_compatibility(self, module_proto)
return _G[self.parent.class].check_module_compatibility(self.parent, module_proto)
end
function ModuleSet.compile_filter(self)
local compatible_modules = {}
for module_name, module_proto in pairs(MODULE_NAME_MAP) do
if ModuleSet.check_compatibility(self, module_proto) then
table.insert(compatible_modules, module_name)
end
end
local existing_modules = {}
for _, module in pairs(self.modules.datasets) do
table.insert(existing_modules, module.proto.name)
end
return {{filter="name", name=compatible_modules},
{filter="name", mode="and", invert=true, name=existing_modules}}
end
function ModuleSet.paste(self, module)
if not ModuleSet.check_compatibility(self, module.proto) then
return false, "incompatible"
elseif self.empty_slots == 0 then
return false, "no_empty_slots"
end
local desired_amount = math.min(module.amount, self.empty_slots)
local existing_module = ModuleSet.get_by_name(self, module.proto.name)
if existing_module then
Module.set_amount(existing_module, existing_module.amount + desired_amount)
else
ModuleSet.add(self, module.proto, desired_amount)
end
ModuleSet.normalize(self, {sort=true, effects=true})
return true, nil
end
function ModuleSet.pack(self)
return {
modules = Collection.pack(self.modules, Module),
-- count, limit, and empty_slots restored by ensuing validation
class = self.class
}
end
function ModuleSet.unpack(packed_self)
local self = packed_self
self.modules = Collection.unpack(packed_self.modules, self, Module)
return self
end
-- Needs validation: modules
function ModuleSet.validate(self)
self.valid = Collection.validate_datasets(self.modules, Module)
if self.valid and self.parent.valid then
if not self.module_count or not self.empty_slots then -- when validating an unpacked ModuleSet
self.module_limit = self.parent.proto.module_limit
ModuleSet.count_modules(self)
end
-- .normalize doesn't remove incompatible modules here, the above validation already marks them
ModuleSet.normalize(self, {trim=true, sort=true, effects=true})
end
return self.valid
end
-- Needs repair: modules
function ModuleSet.repair(self, _)
Collection.repair_datasets(self.modules, nil, Module)
ModuleSet.normalize(self, {trim=true, sort=true, effects=true})
self.valid = true -- repairing invalid modules removes them, making this set valid
return true
end

View File

@@ -0,0 +1,44 @@
---@class FPRecipe
---@field proto FPRecipePrototype
---@field production_type "output" | "input"
---@field valid boolean
---@field class "Recipe"
-- This is essentially just a wrapper-'class' for a recipe prototype to add some data to it
Recipe = {}
function Recipe.init_by_id(recipe_id, production_type)
return {
proto = global.prototypes.recipes[recipe_id],
production_type = production_type,
valid = true,
class = "Recipe"
}
end
function Recipe.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, nil),
production_type = self.production_type,
class = self.class
}
end
function Recipe.unpack(packed_self)
return packed_self
end
-- Needs validation: proto
function Recipe.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, nil)
self.valid = (not self.proto.simplified)
return self.valid
end
-- Needs repair:
function Recipe.repair(_, _)
-- If the prototype is still simplified, it couldn't be fixed by validate, so it has to be removed
return false
end

View File

@@ -0,0 +1,301 @@
---@class FPSubfactory
---@field name string
---@field timescale Timescale
---@field energy_consumption number
---@field pollution number
---@field notes string
---@field mining_productivity number?
---@field blueprints string[]
---@field Product FPCollection<FPItem>
---@field Byproduct FPCollection<FPItem>
---@field Ingredient FPCollection<FPItem>
---@field Floor FPCollection<FPFloor>
---@field matrix_free_items FPItemPrototype[]?
---@field linearly_dependant boolean
---@field selected_floor FPFloor
---@field item_request_proxy LuaEntity?
---@field tick_of_deletion uint?
---@field last_valid_modset { [string]: string }?
---@field mod_version string
---@field valid boolean
---@field id integer
---@field gui_position integer
---@field parent FPFactory
---@field class "Subfactory"
---@class FPPackedSubfactory
-- 'Class' representing a independent part of the factory with in- and outputs
Subfactory = {}
function Subfactory.init(name)
local subfactory = {
name = name,
timescale = nil, -- set after init
energy_consumption = 0,
pollution = 0,
notes = "",
mining_productivity = nil,
blueprints = {},
Product = Collection.init(),
Byproduct = Collection.init(),
Ingredient = Collection.init(),
Floor = Collection.init(),
matrix_free_items = nil,
linearly_dependant = false, -- determined by the solver
selected_floor = nil,
item_request_proxy = nil,
tick_of_deletion = nil, -- ignored on export/import
last_valid_modset = nil,
mod_version = global.mod_version,
valid = true,
id = nil, -- set by collection
gui_position = nil, -- set by collection
parent = nil, -- set by parent
class = "Subfactory"
}
-- Initialize the subfactory with an empty top floor
subfactory.selected_floor = Floor.init(nil)
Subfactory.add(subfactory, subfactory.selected_floor)
return subfactory
end
function Subfactory.tostring(self, attach_products, export_format)
local caption, tooltip = self.name, nil -- don't return a tooltip for the export_format
if attach_products and self.valid then
local product_string = ""
for _, item in pairs(Subfactory.get_in_order(self, "Product")) do
product_string = product_string .. "[img=" .. item.proto.sprite .. "]"
end
if product_string ~= "" then product_string = product_string .. " " end
caption = product_string .. caption
end
if not export_format then
local status_string = ""
if self.tick_of_deletion then status_string = status_string .. "[img=fp_sprite_trash_red] " end
if not self.valid then status_string = status_string .. "[img=fp_sprite_warning_red] " end
caption = status_string .. caption
local trashed_string = "" ---@type LocalisedString
if self.tick_of_deletion then
local ticks_left_in_trash = self.tick_of_deletion - game.tick
local minutes_left_in_trash = math.ceil(ticks_left_in_trash / 3600)
trashed_string = {"fp.subfactory_trashed", minutes_left_in_trash}
end
local invalid_string = (not self.valid) and {"fp.subfactory_invalid"} or ""
tooltip = {"", {"fp.tt_title", caption}, trashed_string, invalid_string}
end
return caption, tooltip
end
function Subfactory.add(self, object)
object.parent = self
return Collection.add(self[object.class], object)
end
function Subfactory.remove(self, dataset)
return Collection.remove(self[dataset.class], dataset)
end
function Subfactory.replace(self, dataset, object)
object.parent = self
return Collection.replace(self[dataset.class], dataset, object)
end
function Subfactory.clear(self, class)
self[class] = Collection.init()
end
function Subfactory.get(self, class, dataset_id)
return Collection.get(self[class], dataset_id)
end
function Subfactory.get_all(self, class)
return Collection.get_all(self[class])
end
function Subfactory.get_in_order(self, class, reverse)
return Collection.get_in_order(self[class], reverse)
end
-- Floors don't have an inherent order, so this makes sense for them
function Subfactory.get_all_floors(self)
return self.Floor.datasets
end
function Subfactory.get_by_name(self, class, name)
return Collection.get_by_name(self[class], name)
end
-- Returns the machines and modules needed to actually build this subfactory
function Subfactory.get_component_data(self)
local components = {machines={}, modules={}}
for _, floor in pairs(Subfactory.get_in_order(self, "Floor")) do
-- Relies on the floor-function to do the heavy lifting
Floor.get_component_data(floor, components)
end
return components
end
-- Updates every top level product of this Subfactory to the given product definition type
function Subfactory.update_product_definitions(self, new_defined_by)
for _, product in pairs(Subfactory.get_in_order(self, "Product")) do
local req_amount = product.required_amount
local current_defined_by = req_amount.defined_by
if current_defined_by ~= "amount" and new_defined_by ~= current_defined_by then
req_amount.defined_by = new_defined_by
local multiplier = (new_defined_by == "belts") and 0.5 or 2
req_amount.amount = req_amount.amount * multiplier
end
end
end
function Subfactory.validate_item_request_proxy(self)
local item_request_proxy = self.item_request_proxy
if item_request_proxy then
if not item_request_proxy.valid or not next(item_request_proxy.item_requests) then
Subfactory.destroy_item_request_proxy(self)
end
end
end
function Subfactory.destroy_item_request_proxy(self)
self.item_request_proxy.destroy{raise_destroy=false}
self.item_request_proxy = nil
end
-- Given line has to have a subfloor; recursively adds references for all subfloors to list
function Subfactory.add_subfloor_references(self, line)
Subfactory.add(self, line.subfloor)
for _, sub_line in pairs(Floor.get_all(line.subfloor, "Line")) do
if sub_line.subfloor then Subfactory.add_subfloor_references(self, sub_line) end
end
end
function Subfactory.clone(self)
local clone = Subfactory.unpack(Subfactory.pack(self))
clone.parent = self.parent
Subfactory.validate(clone)
return clone
end
function Subfactory.pack(self)
local packed_free_items = (self.matrix_free_items) and {} or nil
for index, proto in pairs(self.matrix_free_items or {}) do
packed_free_items[index] = prototyper.util.simplify_prototype(proto, proto.type)
end
return {
name = self.name,
timescale = self.timescale,
notes = self.notes,
mining_productivity = self.mining_productivity,
blueprints = self.blueprints,
Product = Collection.pack(self.Product, Item),
matrix_free_items = packed_free_items,
-- Floors get packed by recursive nesting, which is necessary for a json-type data
-- structure. It will need to be unpacked into the regular structure 'manually'.
top_floor = Floor.pack(Subfactory.get(self, "Floor", 1)),
class = self.class
}
end
function Subfactory.unpack(packed_self)
local self = Subfactory.init(packed_self.name)
self.timescale = packed_self.timescale
self.notes = packed_self.notes
self.mining_productivity = packed_self.mining_productivity
self.blueprints = packed_self.blueprints
self.Product = Collection.unpack(packed_self.Product, self, Item)
if packed_self.matrix_free_items then
self.matrix_free_items = {}
for index, proto in pairs(packed_self.matrix_free_items) do
-- Prototypes will be automatically unpacked by the validation process
self.matrix_free_items[index] = proto
end
end
-- Floor unpacking is called on the top floor, which recursively goes through its subfloors
local top_floor = self.selected_floor ---@cast top_floor -nil
Floor.unpack(packed_self.top_floor, top_floor)
-- Make sure to create references to all subfloors after unpacking
for _, line in pairs(Floor.get_all(top_floor, "Line")) do
if line.subfloor then Subfactory.add_subfloor_references(self, line) end
end
return self
end
-- Needs validation: Product, Floor
function Subfactory.validate(self)
local previous_validity = self.valid
self.valid = Collection.validate_datasets(self.Product, Item)
-- Validating matrix_free_items is a bit messy with the current functions,
-- it might be worth it to change it into a Collection at some point
for index, _ in pairs(self.matrix_free_items or {}) do
self.matrix_free_items[index] = prototyper.util.validate_prototype_object(self.matrix_free_items[index], "type")
self.valid = (not self.matrix_free_items[index].simplified) and self.valid
end
-- Floor validation is called on the top floor, which recursively goes through its subfloors
local top_floor = Subfactory.get(self, "Floor", 1)
self.valid = Floor.validate(top_floor) and self.valid
Subfactory.validate_item_request_proxy(self)
if self.valid then self.last_valid_modset = nil
-- If this subfactory became invalid with the current configuration, retain the modset before the current one
-- The one in global is still the previous one as it's only updated after migrations
elseif previous_validity and not self.valid then self.last_valid_modset = global.installed_mods end
-- return value is not needed here
end
-- Needs repair: Product, Floor, selected_floor
function Subfactory.repair(self, player)
local top_floor = Subfactory.get(self, "Floor", 1)
self.selected_floor = top_floor -- reset the selected floor to the one that's guaranteed to exist
-- Unrepairable item-objects get removed, so the subfactory will always be valid afterwards
Collection.repair_datasets(self.Product, nil, Item)
-- Clear item prototypes so we don't need to rely on the solver to remove them
Subfactory.clear(self, "Byproduct")
Subfactory.clear(self, "Ingredient")
-- Remove any unrepairable free item so the subfactory remains valid
local free_items = self.matrix_free_items or {}
for index = #free_items, 1, -1 do
if free_items[index].simplified then table.remove(free_items, index) end
end
-- Floor repair is called on the top floor, which recursively goes through its subfloors
Floor.repair(top_floor, player)
self.last_valid_modset = nil
self.valid = true
-- return value is not needed here
end

View File

@@ -0,0 +1,976 @@
local generator_util = require("backend.handlers.generator_util")
local generator = {
machines = {},
recipes = {},
items = {},
fuels = {},
belts = {},
wagons = {},
modules = {},
beacons = {}
}
---@class FPPrototype
---@field id integer
---@field data_type DataType
---@field name string
---@field localised_name LocalisedString
---@field sprite SpritePath
---@class FPPrototypeWithCategory: FPPrototype
---@field category_id integer
---@alias AnyFPPrototype FPPrototype | FPPrototypeWithCategory
---@param list AnyNamedPrototypes
---@param prototype FPPrototype
---@param category string?
local function insert_prototype(list, prototype, category)
if category == nil then
---@cast list NamedPrototypes<FPPrototype>
list[prototype.name] = prototype
else
---@cast list NamedPrototypesWithCategory<FPPrototype>
list[category] = list[category] or { name = category, members = {} }
list[category].members[prototype.name] = prototype
end
end
---@param list AnyNamedPrototypes
---@param name string
---@param category string?
local function remove_prototype(list, name, category)
if category == nil then
---@cast list NamedPrototypes<FPPrototype>
list[name] = nil
else
---@cast list NamedPrototypesWithCategory<FPPrototype>
list[category].members[name] = nil
if next(list[category].members) == nil then list[category] = nil end
end
end
---@class FPMachinePrototype: FPPrototypeWithCategory
---@field data_type "machines"
---@field category string
---@field ingredient_limit integer
---@field fluid_channels FluidChannels
---@field speed double
---@field energy_type "burner" | "electric" | "void"
---@field energy_usage double
---@field energy_drain double
---@field emissions double
---@field built_by_item FPItemPrototype?
---@field base_productivity double
---@field allowed_effects AllowedEffects?
---@field module_limit integer
---@field launch_sequence_time number?
---@field burner MachineBurner?
---@class FluidChannels
---@field input integer
---@field output integer
---@class MachineBurner
---@field effectivity double
---@field categories { [string]: boolean }
-- Generates a table containing all machines for all categories
---@return NamedPrototypesWithCategory<FPMachinePrototype>
function generator.machines.generate()
local machines = {} ---@type NamedPrototypesWithCategory<FPMachinePrototype>
---@param category string
---@param proto LuaEntityPrototype
---@return FPMachinePrototype?
local function generate_category_entry(category, proto)
-- First, determine if there is a valid sprite for this machine
local sprite = generator_util.determine_entity_sprite(proto)
if sprite == nil then return end
-- If it is a miner, set speed to mining_speed so the machine_count-formula works out
local speed = proto.crafting_categories and proto.crafting_speed or proto.mining_speed
-- Determine data related to the energy source
local energy_type, emissions = "", 0 -- emissions remain at 0 if no energy source is present
local burner = nil ---@type MachineBurner
local energy_usage, energy_drain = (proto.energy_usage or proto.max_energy_usage or 0), 0
-- Determine the name of the item that actually builds this machine for the item requester
-- There can technically be more than one, but bots use the first one, so I do too
local built_by_item = (proto.items_to_place_this) and proto.items_to_place_this[1].name or nil
-- Determine the details of this entities energy source
local burner_prototype, fluid_burner_prototype = proto.burner_prototype, proto.fluid_energy_source_prototype
if burner_prototype then
energy_type = "burner"
emissions = burner_prototype.emissions
burner = {effectivity = burner_prototype.effectivity, categories = burner_prototype.fuel_categories}
-- Only supports fluid energy that burns_fluid for now, as it works the same way as solid burners
-- Also doesn't respect scale_fluid_usage and fluid_usage_per_tick for now, let the reports come
elseif fluid_burner_prototype then
emissions = fluid_burner_prototype.emissions
if fluid_burner_prototype.burns_fluid and not fluid_burner_prototype.fluid_box.filter then
energy_type = "burner"
burner = {effectivity = fluid_burner_prototype.effectivity, categories = {["fluid-fuel"] = true}}
else -- Avoid adding this type of complex fluid energy as electrical energy
energy_type = "void"
end
elseif proto.electric_energy_source_prototype then
energy_type = "electric"
energy_drain = proto.electric_energy_source_prototype.drain
emissions = proto.electric_energy_source_prototype.emissions
elseif proto.void_energy_source_prototype then
energy_type = "void"
emissions = proto.void_energy_source_prototype.emissions
end
-- Determine fluid input/output channels
local fluid_channels = {input = 0, output = 0}
if fluid_burner_prototype then fluid_channels.input = fluid_channels.input - 1 end
for _, fluidbox in pairs(proto.fluidbox_prototypes) do
if fluidbox.production_type == "output" then
fluid_channels.output = fluid_channels.output + 1
else -- "input" and "input-output"
fluid_channels.input = fluid_channels.input + 1
end
end
local machine = {
name = proto.name,
localised_name = proto.localised_name,
sprite = sprite,
category = category,
ingredient_limit = (proto.ingredient_count or 255),
fluid_channels = fluid_channels,
speed = speed,
energy_type = energy_type,
energy_usage = energy_usage,
energy_drain = energy_drain,
emissions = emissions,
built_by_item = built_by_item,
base_productivity = (proto.base_productivity or 0),
allowed_effects = generator_util.format_allowed_effects(proto.allowed_effects),
module_limit = (proto.module_inventory_size or 0),
launch_sequence_time = generator_util.determine_launch_sequence_time(proto),
burner = burner
}
return machine
end
for _, proto in pairs(game.entity_prototypes) do
if not proto.has_flag("hidden") and proto.crafting_categories and proto.energy_usage ~= nil
and not generator_util.is_irrelevant_machine(proto) then
for category, _ in pairs(proto.crafting_categories) do
local machine = generate_category_entry(category, proto)
if machine then insert_prototype(machines, machine, machine.category) end
end
-- Add mining machines
elseif proto.resource_categories then
if not proto.has_flag("hidden") and proto.type ~= "character" then
for category, enabled in pairs(proto.resource_categories) do
-- Only supports solid mining recipes for now (no oil, etc.)
if enabled and category ~= "basic-fluid" then
local machine = generate_category_entry(category, proto)
if machine then
machine.mining = true
insert_prototype(machines, machine, machine.category)
end
end
end
end
-- Add offshore pumps
elseif proto.fluid then
local machine = generate_category_entry(proto.name, proto)
if machine then
machine.speed = 1 -- pumping speed included in the recipe product-amount
machine.category = proto.name -- unique category for every offshore pump
insert_prototype(machines, machine, machine.category)
end
end
-- Add machines that produce steam (ie. boilers)
for _, fluidbox in ipairs(proto.fluidbox_prototypes) do
if fluidbox.production_type == "output" and fluidbox.filter
and fluidbox.filter.name == "steam" and proto.target_temperature ~= nil then
-- Exclude any boilers that use heat as their energy source
if proto.burner_prototype or proto.electric_energy_source_prototype then
-- Find the corresponding input fluidbox
local input_fluidbox = nil ---@type LuaFluidBoxPrototype
for _, fb in ipairs(proto.fluidbox_prototypes) do
if fb.production_type == "input-output" or fb.production_type == "input" then
input_fluidbox = fb
break
end
end
-- Add the machine if it has a valid input fluidbox
if input_fluidbox ~= nil then
local category = "steam-" .. proto.target_temperature
local machine = generate_category_entry(category, proto)
if machine then
local temp_diff = proto.target_temperature - input_fluidbox.filter.default_temperature
local energy_per_unit = input_fluidbox.filter.heat_capacity * temp_diff
machine.speed = machine.energy_usage / energy_per_unit
insert_prototype(machines, machine, machine.category)
-- Add every boiler to the general steam category (steam without temperature)
local general_machine = fancytable.deep_copy(machine)
general_machine.category = "general-steam"
insert_prototype(machines, general_machine, general_machine.category)
end
end
end
end
end
end
return machines
end
---@param machines NamedPrototypesWithCategory<FPMachinePrototype>
function generator.machines.second_pass(machines)
-- Go over all recipes to find unused categories
local used_category_names = {} ---@type { [string]: boolean }
for _, recipe_proto in pairs(global.prototypes.recipes) do
used_category_names[recipe_proto.category] = true
end
for _, machine_category in pairs(machines) do
if used_category_names[machine_category.name] == nil then
machines[machine_category.name] = nil
end
end
-- Filter out burner machines that don't have any valid fuel categories
for _, machine_category in pairs(machines) do
for _, machine_proto in pairs(machine_category.members) do
if machine_proto.energy_type == "burner" then
local category_found = false
for fuel_category in pairs(machine_proto.burner.categories) do
if global.prototypes.fuels[fuel_category] then category_found = true; break end
end
if not category_found then remove_prototype(machines, machine_proto.name, machine_category.name) end
end
end
-- If the category ends up empty because of this, make sure to remove it
if not next(machine_category.members) then machines[machine_category.name] = nil end
end
-- Replace built_by_item names with prototype references
local item_prototypes = global.prototypes.items["item"].members ---@type { [string]: FPItemPrototype }
for _, machine_category in pairs(machines) do
for _, machine_proto in pairs(machine_category.members) do
if machine_proto.built_by_item then
machine_proto.built_by_item = item_prototypes[machine_proto.built_by_item]
end
end
end
end
---@param a FPMachinePrototype
---@param b FPMachinePrototype
---@return boolean
function generator.machines.sorting_function(a, b)
if a.speed < b.speed then return true
elseif a.speed > b.speed then return false
elseif a.module_limit < b.module_limit then return true
elseif a.module_limit > b.module_limit then return false
elseif a.energy_usage < b.energy_usage then return true
elseif a.energy_usage > b.energy_usage then return false end
return false
end
---@class FPUnformattedRecipePrototype: FPPrototype
---@field data_type "recipes"
---@field category string
---@field energy double
---@field emissions_multiplier double
---@field ingredients FPIngredient[]
---@field products Product[]
---@field main_product Product?
---@field type_counts { ingredients: ItemTypeCounts, products: ItemTypeCounts }
---@field recycling boolean
---@field barreling boolean
---@field enabling_technologies string[]
---@field use_limitations boolean
---@field custom boolean
---@field enabled_from_the_start boolean
---@field hidden boolean
---@field order string
---@field group ItemGroup
---@field subgroup ItemGroup
---@class FPRecipePrototype: FPUnformattedRecipePrototype
---@field ingredients FormattedRecipeItem[]
---@field products FormattedRecipeItem[]
---@field main_product FormattedRecipeItem?
---@class FPIngredient: Ingredient
---@field ignore_productivity boolean
-- Returns all standard recipes + custom mining, steam and rocket recipes
---@return NamedPrototypes<FPRecipePrototype>
function generator.recipes.generate()
local recipes = {} ---@type NamedPrototypes<FPRecipePrototype>
---@return FPUnformattedRecipePrototype
local function custom_recipe()
return {
custom = true,
enabled_from_the_start = true,
hidden = false,
group = {name="intermediate-products", order="c", valid=true,
localised_name={"item-group-name.intermediate-products"}},
type_counts = {},
enabling_technologies = nil,
use_limitations = false,
emissions_multiplier = 1
}
end
-- Determine researchable recipes
local researchable_recipes = {} ---@type { [string]: string[] }
local tech_filter = {{filter="hidden", invert=true}, {filter="has-effects", mode="and"}}
for _, tech_proto in pairs(game.get_filtered_technology_prototypes(tech_filter)) do
for _, effect in pairs(tech_proto.effects) do
if effect.type == "unlock-recipe" then
local recipe_name = effect.recipe
researchable_recipes[recipe_name] = researchable_recipes[recipe_name] or {}
table.insert(researchable_recipes[recipe_name], tech_proto.name)
end
end
end
-- Add all standard recipes
local recipe_filter = {{filter="energy", comparison=">", value=0},
{filter="energy", comparison="<", value=1e+21, mode="and"}}
for recipe_name, proto in pairs(game.get_filtered_recipe_prototypes(recipe_filter)) do
local machine_category = global.prototypes.machines[proto.category] ---@type { [string]: FPMachinePrototype }
-- Avoid any recipes that have no machine to produce them, or are irrelevant
if machine_category ~= nil and not generator_util.is_irrelevant_recipe(proto) then
local recipe = { ---@type FPUnformattedRecipePrototype
name = proto.name,
localised_name = proto.localised_name,
sprite = "recipe/" .. proto.name,
category = proto.category,
energy = proto.energy,
emissions_multiplier = proto.emissions_multiplier,
ingredients = proto.ingredients,
products = proto.products,
main_product = proto.main_product,
type_counts = {}, -- filled out by format_* below
recycling = generator_util.is_recycling_recipe(proto),
barreling = generator_util.is_compacting_recipe(proto),
enabling_technologies = researchable_recipes[recipe_name], -- can be nil
use_limitations = true,
custom = false,
enabled_from_the_start = proto.enabled,
hidden = proto.hidden,
order = proto.order,
group = generator_util.generate_group_table(proto.group),
subgroup = generator_util.generate_group_table(proto.subgroup)
}
generator_util.format_recipe_products_and_ingredients(recipe)
---@cast recipe FPRecipePrototype
insert_prototype(recipes, recipe, nil)
end
end
-- Determine all the items that can be inserted usefully into a rocket silo
local launch_products_filter = {{filter="has-rocket-launch-products"}}
local rocket_silo_inputs = {} ---@type LuaItemPrototype[]
for _, item in pairs(game.get_filtered_item_prototypes(launch_products_filter)) do
if next(item.rocket_launch_products) then
table.insert(rocket_silo_inputs, item)
end
end
-- Localize them here so they don't have to be recreated over and over
local item_prototypes, recipe_prototypes = game.item_prototypes, game.recipe_prototypes
-- Add mining recipes
for _, proto in pairs(game.entity_prototypes) do
-- Add all mining recipes. Only supports solids for now.
if proto.mineable_properties and proto.resource_category then
local products = proto.mineable_properties.products
if not products then goto incompatible_proto end
local produces_solid = false
for _, product in pairs(products) do -- detects all solid mining recipes
if product.type == "item" then produces_solid = true; break end
end
if not produces_solid then goto incompatible_proto end
if produces_solid then
local recipe = custom_recipe()
recipe.name = "impostor-" .. proto.name
recipe.localised_name = proto.localised_name
recipe.sprite = products[1].type .. "/" .. products[1].name
recipe.order = proto.order
recipe.subgroup = {name="mining", order="y", valid=true}
recipe.category = proto.resource_category
recipe.mining = true
-- Set energy to mining time so the forumla for the machine_count works out
recipe.energy = proto.mineable_properties.mining_time
recipe.ingredients = {{type="entity", name=proto.name, amount=1}}
recipe.products = products
recipe.main_product = recipe.products[1]
-- Add mining fluid, if required
if proto.mineable_properties.required_fluid then
table.insert(recipe.ingredients, {
type = "fluid",
name = proto.mineable_properties.required_fluid,
-- fluid_amount is given for a 'set' of mining ops, with a set being 10 ore
amount = proto.mineable_properties.fluid_amount / 10
})
end
generator_util.format_recipe_products_and_ingredients(recipe)
---@cast recipe FPRecipePrototype
generator_util.add_recipe_tooltip(recipe)
insert_prototype(recipes, recipe, nil)
--else
-- crude-oil and angels-natural-gas go here (not interested atm)
end
::incompatible_proto::
-- Add offshore-pump fluid recipes
elseif proto.fluid then
local recipe = custom_recipe()
recipe.name = "impostor-" .. proto.fluid.name .. "-" .. proto.name
recipe.localised_name = proto.fluid.localised_name
recipe.sprite = "fluid/" .. proto.fluid.name
recipe.order = proto.order
recipe.subgroup = {name="fluids", order="z", valid=true}
recipe.category = proto.name -- use proto name so every pump has it's own category
recipe.energy = 1
recipe.ingredients = {}
recipe.products = {{type="fluid", name=proto.fluid.name, amount=(proto.pumping_speed * 60)}}
recipe.main_product = recipe.products[1]
generator_util.format_recipe_products_and_ingredients(recipe)
---@cast recipe FPRecipePrototype
generator_util.add_recipe_tooltip(recipe)
insert_prototype(recipes, recipe, nil)
-- Detect all the implicit rocket silo recipes
elseif proto.rocket_parts_required ~= nil then
local fixed_recipe = recipe_prototypes[proto.fixed_recipe --[[@as string]]]
if fixed_recipe ~= nil then
-- Add recipe for all 'launchable' items
for _, silo_input in pairs(rocket_silo_inputs) do
local silo_product = table_size(silo_input.rocket_launch_products) > 1
and item_prototypes[silo_input.rocket_launch_products[1].name] or silo_input
local recipe = custom_recipe()
recipe.name = "impostor-silo-" .. proto.name .. "-item-" .. silo_input.name
recipe.localised_name = silo_product.localised_name
recipe.sprite = "item/" .. silo_product.name
recipe.category = next(proto.crafting_categories, nil) -- hopefully this stays working
recipe.energy = fixed_recipe.energy * proto.rocket_parts_required --[[@as number]]
recipe.subgroup = {name="science-pack", order="g", valid=true}
recipe.order = "x-silo-" .. proto.order .. "-" .. silo_input.order
recipe.ingredients = fixed_recipe.ingredients
for _, ingredient in pairs(recipe.ingredients) do
ingredient.amount = ingredient.amount * proto.rocket_parts_required
end
table.insert(recipe.ingredients, {type="item", name=silo_input.name,
amount=1, ignore_productivity=true})
recipe.products = silo_input.rocket_launch_products
recipe.main_product = recipe.products[1]
generator_util.format_recipe_products_and_ingredients(recipe)
---@cast recipe FPRecipePrototype
generator_util.add_recipe_tooltip(recipe)
insert_prototype(recipes, recipe, nil)
end
-- Modify recipe for all rocket parts so they represent a full launch
-- This is needed so the launch sequence times can be incorporated correctly
local rocket_part_recipe = recipes[fixed_recipe.name]
if rocket_part_recipe then
generator_util.multiply_recipe(rocket_part_recipe, proto.rocket_parts_required)
end
end
end
-- Add a recipe for producing steam from a boiler
local existing_recipe_names = {} ---@type { [string]: boolean }
for _, fluidbox in ipairs(proto.fluidbox_prototypes) do
if fluidbox.production_type == "output" and fluidbox.filter
and fluidbox.filter.name == "steam" and proto.target_temperature ~= nil then
-- Exclude any boilers that use heat or fluid as their energy source
if proto.burner_prototype or proto.electric_energy_source_prototype then
local temperature = proto.target_temperature
local recipe_name = "impostor-steam-" .. temperature
-- Prevent duplicate recipes, in case more than one boiler produces the same temperature of steam
if existing_recipe_names[recipe_name] == nil then
existing_recipe_names[recipe_name] = true
local recipe = custom_recipe()
recipe.name = recipe_name
recipe.localised_name = {"fp.fluid_at_temperature", {"fluid-name.steam"},
temperature, {"fp.unit_celsius"}}
recipe.sprite = "fluid/steam"
recipe.category = "steam-" .. temperature
recipe.order = "z-" .. temperature
recipe.subgroup = {name="fluids", order="z", valid=true}
recipe.energy = 1
recipe.ingredients = {{type="fluid", name="water", amount=60}}
recipe.products = {{type="fluid", name="steam", amount=60, temperature=temperature}}
recipe.main_product = recipe.products[1]
generator_util.format_recipe_products_and_ingredients(recipe)
---@cast recipe FPRecipePrototype
generator_util.add_recipe_tooltip(recipe)
insert_prototype(recipes, recipe, nil)
end
end
end
end
end
-- Add a general steam recipe that works with every boiler
if game["fluid_prototypes"]["steam"] then -- make sure the steam prototype exists
local recipe = custom_recipe()
recipe.name = "fp-general-steam"
recipe.localised_name = {"fluid-name.steam"}
recipe.sprite = "fluid/steam"
recipe.category = "general-steam"
recipe.order = "z-0"
recipe.subgroup = {name="fluids", order="z", valid=true}
recipe.energy = 1
recipe.ingredients = {{type="fluid", name="water", amount=60}}
recipe.products = {{type="fluid", name="steam", amount=60}}
recipe.main_product = recipe.products[1]
generator_util.format_recipe_products_and_ingredients(recipe)
---@cast recipe FPRecipePrototype
generator_util.add_recipe_tooltip(recipe)
insert_prototype(recipes, recipe, nil)
end
-- Custom handling for Space Exploration Arcosphere recipes
local se_split_recipes = {"se-arcosphere-fracture", "se-naquium-processor", "se-naquium-tessaract",
"se-space-dilation-data", "se-space-fold-data", "se-space-injection-data", "se-space-warp-data"}
for _, recipe_name in pairs(se_split_recipes) do
local recipe, alt_recipe = recipes[recipe_name], recipes[recipe_name .. "-alt"]
if recipe and alt_recipe then
recipe.custom = true
generator_util.combine_recipes(recipe, alt_recipe)
generator_util.multiply_recipe(recipe, 0.5)
generator_util.add_recipe_tooltip(recipe)
remove_prototype(recipes, alt_recipe.name, nil)
end
end
return recipes
end
---@param recipes NamedPrototypes<FPRecipePrototype>
function generator.recipes.second_pass(recipes)
local machines = global.prototypes.machines
-- Check again if all recipes still have a machine to produce them after machine second pass
for _, recipe in pairs(recipes) do
if not machines[recipe.category] then
remove_prototype(recipes, recipe.name, nil)
end
end
end
---@class FPItemPrototype: FPPrototypeWithCategory
---@field data_type "items"
---@field type "item" | "fluid" | "entity"
---@field hidden boolean
---@field stack_size uint?
---@field ingredient_only boolean
---@field temperature number
---@field order string
---@field group ItemGroup
---@field subgroup ItemGroup
---@class RelevantItem
---@field proto FormattedRecipeItem
---@field is_product boolean
---@field is_rocket_part boolean
---@field temperature number?
---@alias RelevantItems { [ItemType]: { [ItemName]: RelevantItem } }
-- Returns all relevant items and fluids
---@return NamedPrototypesWithCategory<FPItemPrototype>
function generator.items.generate()
local items = {} ---@type NamedPrototypesWithCategory<FPItemPrototype>
---@param table RelevantItems
---@param item RelevantItem
local function add_item(table, item)
local type = item.proto.type
local name = item.proto.name
table[type] = table[type] or {}
table[type][name] = table[type][name] or {}
local item_details = table[type][name]
-- Determine whether this item is used as a product at least once
item_details.is_product = item_details.is_product or item.is_product
item_details.is_rocket_part = item_details.is_rocket_part or item.is_rocket_part
item_details.temperature = item.proto.temperature
end
-- Create a table containing every item that is either a product or an ingredient to at least one recipe
local relevant_items = {} ---@type RelevantItems
for _, recipe_proto in pairs(global.prototypes.recipes) do
for _, product in pairs(recipe_proto.products) do
local is_rocket_part = (recipe_proto.category == "rocket-building")
add_item(relevant_items, {proto=product, is_product=true, is_rocket_part=is_rocket_part})
end
for _, ingredient in pairs(recipe_proto.ingredients) do
add_item(relevant_items, {proto=ingredient, is_product=false, is_rocket_part=false})
end
end
-- Add all standard items
for type, item_table in pairs(relevant_items) do
for item_name, item_details in pairs(item_table) do
local proto_name = generator_util.format_temperature_name(item_details, item_name)
local proto = game[type .. "_prototypes"][proto_name] ---@type LuaItemPrototype | LuaFluidPrototype
if proto == nil then goto skip_item end
local localised_name = generator_util.format_temperature_localised_name(item_details, proto)
local stack_size = (type == "item") and proto.stack_size or nil
local order = (item_details.temperature) and (proto.order .. item_details.temperature) or proto.order
local hidden = false -- "entity" types are never hidden
if type == "item" then hidden = proto.has_flag("hidden")
elseif type == "fluid" then hidden = proto.hidden end
if item_details.is_rocket_part then hidden = false end
local item = {
name = item_name,
localised_name = localised_name,
sprite = type .. "/" .. proto.name,
type = type,
hidden = hidden,
stack_size = stack_size,
ingredient_only = not item_details.is_product,
temperature = item_details.temperature,
order = order,
group = generator_util.generate_group_table(proto.group),
subgroup = generator_util.generate_group_table(proto.subgroup)
}
insert_prototype(items, item, item.type)
::skip_item::
end
end
return items
end
---@class FPFuelPrototype: FPPrototypeWithCategory
---@field data_type "fuels"
---@field type "item" | "fluid"
---@field category string | "fluid-fuel"
---@field fuel_value float
---@field stack_size uint?
---@field emissions_multiplier double
-- Generates a table containing all fuels that can be used in a burner
---@return NamedPrototypesWithCategory<FPFuelPrototype>
function generator.fuels.generate()
local fuels = {} ---@type NamedPrototypesWithCategory<FPFuelPrototype>
-- Determine all the fuel categories that the machine prototypes use
local used_fuel_categories = {} ---@type { [string]: boolean}
for _, machine_category in pairs(global.prototypes.machines) do
for _, machine_proto in pairs(machine_category.members) do
if machine_proto.burner then
for category_name, _ in pairs(machine_proto.burner.categories) do
used_fuel_categories[category_name] = true
end
end
end
end
local fuel_filter = {{filter="fuel-value", comparison=">", value=0},
{filter="fuel-value", comparison="<", value=1e+21, mode="and"}}
-- Add solid fuels
local item_list = global.prototypes.items["item"].members ---@type NamedPrototypesWithCategory<FPItemPrototype>
local item_fuel_filter = fancytable.shallow_copy(fuel_filter)
table.insert(item_fuel_filter, {filter="flag", flag="hidden", invert=true, mode="and"})
for _, proto in pairs(game.get_filtered_item_prototypes(item_fuel_filter)) do
-- Only use fuels that were actually detected/accepted to be items and find use in at least one machine
if item_list[proto.name] and used_fuel_categories[proto.fuel_category] ~= nil then
local fuel = {
name = proto.name,
localised_name = proto.localised_name,
sprite = "item/" .. proto.name,
type = "item",
category = proto.fuel_category,
fuel_value = proto.fuel_value,
stack_size = proto.stack_size,
emissions_multiplier = proto.fuel_emissions_multiplier
}
insert_prototype(fuels, fuel, fuel.category)
end
end
-- Add liquid fuels
local fluid_list = global.prototypes.items["fluid"].members ---@type NamedPrototypesWithCategory<FPItemPrototype>
local fluid_fuel_filter = fancytable.shallow_copy(fuel_filter)
table.insert(fluid_fuel_filter, {filter="hidden", invert=true, mode="and"})
for _, proto in pairs(game.get_filtered_fluid_prototypes(fluid_fuel_filter)) do
-- Only use fuels that have actually been detected/accepted as fluids
if fluid_list[proto.name] then
local fuel = {
name = proto.name,
localised_name = proto.localised_name,
sprite = "fluid/" .. proto.name,
type = "fluid",
category = "fluid-fuel",
fuel_value = proto.fuel_value,
stack_size = nil,
emissions_multiplier = proto.emissions_multiplier
}
insert_prototype(fuels, fuel, fuel.category)
end
end
return fuels
end
---@param a FPFuelPrototype
---@param b FPFuelPrototype
---@return boolean
function generator.fuels.sorting_function(a, b)
if a.fuel_value < b.fuel_value then return true
elseif a.fuel_value > b.fuel_value then return false
elseif a.emissions_multiplier < b.emissions_multiplier then return true
elseif a.emissions_multiplier > b.emissions_multiplier then return false end
return false
end
---@class FPBeltPrototype: FPPrototype
---@field data_type "belts"
---@field rich_text string
---@field throughput double
-- Generates a table containing all available transport belts
---@return NamedPrototypes<FPBeltPrototype>
function generator.belts.generate()
local belts = {} ---@type NamedPrototypes<FPBeltPrototype>
local belt_filter = {{filter="type", type="transport-belt"},
{filter="flag", flag="hidden", invert=true, mode="and"}}
for _, proto in pairs(game.get_filtered_entity_prototypes(belt_filter)) do
local sprite = generator_util.determine_entity_sprite(proto)
if sprite ~= nil then
local belt = {
name = proto.name,
localised_name = proto.localised_name,
sprite = sprite,
rich_text = "[entity=" .. proto.name .. "]",
throughput = proto.belt_speed * 480
}
insert_prototype(belts, belt, nil)
end
end
return belts
end
---@param a FPBeltPrototype
---@param b FPBeltPrototype
---@return boolean
function generator.belts.sorting_function(a, b)
if a.throughput < b.throughput then return true
elseif a.throughput > b.throughput then return false end
return false
end
---@class FPWagonPrototype: FPPrototypeWithCategory
---@field data_type "wagons"
---@field rich_text string
---@field category "cargo-wagon" | "fluid-wagon"
---@field storage number
-- Generates a table containing all available cargo and fluid wagons
---@return NamedPrototypesWithCategory<FPWagonPrototype>
function generator.wagons.generate()
local wagons = {} ---@type NamedPrototypesWithCategory<FPWagonPrototype>
-- Add cargo wagons
local cargo_wagon_filter = {{filter="type", type="cargo-wagon"},
{filter="flag", flag="hidden", invert=true, mode="and"}}
for _, proto in pairs(game.get_filtered_entity_prototypes(cargo_wagon_filter)) do
local inventory_size = proto.get_inventory_size(defines.inventory.cargo_wagon)
if inventory_size > 0 then
local wagon = {
name = proto.name,
localised_name = proto.localised_name,
sprite = generator_util.determine_entity_sprite(proto),
rich_text = "[entity=" .. proto.name .. "]",
category = "cargo-wagon",
storage = inventory_size
}
insert_prototype(wagons, wagon, wagon.category)
end
end
-- Add fluid wagons
local fluid_wagon_filter = {{filter="type", type="fluid-wagon"},
{filter="flag", flag="hidden", invert=true, mode="and"}}
for _, proto in pairs(game.get_filtered_entity_prototypes(fluid_wagon_filter)) do
if proto.fluid_capacity > 0 then
local wagon = {
name = proto.name,
localised_name = proto.localised_name,
sprite = generator_util.determine_entity_sprite(proto),
rich_text = "[entity=" .. proto.name .. "]",
category = "fluid-wagon",
storage = proto.fluid_capacity
}
insert_prototype(wagons, wagon, wagon.category)
end
end
return wagons
end
---@param a FPWagonPrototype
---@param b FPWagonPrototype
---@return boolean
function generator.wagons.sorting_function(a, b)
if a.storage < b.storage then return true
elseif a.storage > b.storage then return false end
return false
end
---@class FPModulePrototype: FPPrototypeWithCategory
---@field data_type "modules"
---@field category string
---@field tier uint
---@field effects ModuleEffects
---@field limitations { [string]: true }
-- Generates a table containing all available modules
---@return NamedPrototypesWithCategory<FPModulePrototype>
function generator.modules.generate()
local modules = {} ---@type NamedPrototypesWithCategory<FPModulePrototype>
local module_filter = {{filter="type", type="module"}, {filter="flag", flag="hidden", invert=true, mode="and"}}
for _, proto in pairs(game.get_filtered_item_prototypes(module_filter)) do
local limitations = {} ---@type ModuleLimitations
for _, recipe_name in pairs(proto.limitations) do limitations[recipe_name] = true end
local sprite = "item/" .. proto.name
if game.is_valid_sprite_path(sprite) then
local module = {
name = proto.name,
localised_name = proto.localised_name,
sprite = sprite,
category = proto.category,
tier = proto.tier,
effects = proto.module_effects or {},
limitations = limitations
}
insert_prototype(modules, module, module.category)
end
end
return modules
end
---@class FPBeaconPrototype: FPPrototype
---@field data_type "beacons"
---@field category "fp_beacon"
---@field built_by_item FPItemPrototype
---@field allowed_effects AllowedEffects
---@field module_limit uint
---@field effectivity double
---@field energy_usage double
-- Generates a table containing all available beacons
---@return NamedPrototypes<FPBeaconPrototype>
function generator.beacons.generate()
local beacons = {} ---@type NamedPrototypes<FPBeaconPrototype>
---@type NamedPrototypesWithCategory<FPItemPrototype>
local item_prototypes = global.prototypes.items["item"].members
local beacon_filter = {{filter="type", type="beacon"}, {filter="flag", flag="hidden", invert=true, mode="and"}}
for _, proto in pairs(game.get_filtered_entity_prototypes(beacon_filter)) do
local sprite = generator_util.determine_entity_sprite(proto)
if sprite ~= nil and proto.module_inventory_size and proto.distribution_effectivity > 0 then
-- Beacons can refer to the actual item prototype right away because they are built after items are
local items_to_place_this = proto.items_to_place_this
local built_by_item = (items_to_place_this) and item_prototypes[items_to_place_this[1].name] or nil
local beacon = {
name = proto.name,
localised_name = proto.localised_name,
sprite = sprite,
category = "fp_beacon", -- custom category to be similar to machines
built_by_item = built_by_item,
allowed_effects = generator_util.format_allowed_effects(proto.allowed_effects),
module_limit = proto.module_inventory_size,
effectivity = proto.distribution_effectivity,
energy_usage = proto.energy_usage or proto.max_energy_usage or 0
}
insert_prototype(beacons, beacon, nil)
end
end
return beacons
end
---@param a FPBeaconPrototype
---@param b FPBeaconPrototype
---@return boolean
function generator.beacons.sorting_function(a, b)
if a.module_limit < b.module_limit then return true
elseif a.module_limit > b.module_limit then return false
elseif a.effectivity < b.effectivity then return true
elseif a.effectivity > b.effectivity then return false
elseif a.energy_usage < b.energy_usage then return true
elseif a.energy_usage > b.energy_usage then return false end
return false
end
return generator

View File

@@ -0,0 +1,456 @@
local generator_util = {}
-- ** LOCAL UTIL **
---@class FormattedRecipeItem
---@field name string
---@field type string
---@field amount number
---@field proddable_amount number?
---@field temperature number?
---@field ignore_productivity boolean?
---@alias IndexedItemList { [ItemType]: { [ItemName]: { index: number, item: FormattedRecipeItem } } }
---@alias ItemList { [ItemType]: { [ItemName]: FormattedRecipeItem } }
---@alias ItemTypeCounts { items: number, fluids: number }
-- Determines the actual amount of items that a recipe product or ingredient equates to
---@param base_item Ingredient | Product
---@param type "ingredient" | "product"
---@return FormattedRecipeItem
local function generate_formatted_item(base_item, type)
local base_amount = 0
if base_item.amount_max ~= nil and base_item.amount_min ~= nil then
base_amount = (base_item.amount_max + base_item.amount_min) / 2
else
base_amount = base_item.amount
end
local probability = (base_item.probability or 1)
local proddable_amount = (type == "product")
and (base_amount - (base_item.catalyst_amount or 0)) * probability or nil
-- This will probably screw up the main_product detection down the line
if base_item.temperature ~= nil then
base_item.name = base_item.name .. "-" .. base_item.temperature
end
return {
name = base_item.name,
type = base_item.type,
amount = (base_amount * probability),
proddable_amount = proddable_amount,
temperature = base_item.temperature
}
end
-- Combines items that occur more than once into one entry
---@param item_list FormattedRecipeItem[]
local function combine_identical_products(item_list)
local touched_items = {item = {}, fluid = {}, entity = {}} ---@type ItemList
for index=#item_list, 1, -1 do
local item = item_list[index]
if item.temperature == nil then -- don't care to deal with temperature crap
local touched_item = touched_items[item.type][item.name]
if touched_item ~= nil then
touched_item.amount = touched_item.amount + item.amount
if touched_item.proddable_amount then
touched_item.proddable_amount = touched_item.proddable_amount + item.proddable_amount
end
-- Using the table.remove function to preserve array-format
table.remove(item_list, index)
else
touched_items[item.type][item.name] = item
end
end
end
end
---@param item_list FormattedRecipeItem[]
---@return IndexedItemList
local function create_type_indexed_list(item_list)
local indexed_list = {item = {}, fluid = {}, entity = {}} ---@type IndexedItemList
for index, item in pairs(item_list) do
indexed_list[item.type][item.name] = {index = index, item = fancytable.shallow_copy(item)}
end
return indexed_list
end
---@param indexed_items IndexedItemList
---@return ItemTypeCounts
local function determine_item_type_counts(indexed_items)
return {
items = table_size(indexed_items.item),
fluids = table_size(indexed_items.fluid)
}
end
-- ** TOP LEVEL **
-- Formats the products/ingredients of a recipe for more convenient use
---@param recipe_proto FPUnformattedRecipePrototype
function generator_util.format_recipe_products_and_ingredients(recipe_proto)
local ingredients = {} ---@type FormattedRecipeItem[]
for _, base_ingredient in pairs(recipe_proto.ingredients) do
local formatted_ingredient = generate_formatted_item(base_ingredient, "ingredient")
if formatted_ingredient.amount > 0 then
-- Productivity applies to all ingredients by default, some exceptions apply (ex. satellite)
-- Also add proddable_amount so productivity bonus can be un-applied later in the model
if base_ingredient.ignore_productivity then
formatted_ingredient.ignore_productivity = true
formatted_ingredient.proddable_amount = formatted_ingredient.amount
end
table.insert(ingredients, formatted_ingredient)
end
end
local indexed_ingredients = create_type_indexed_list(ingredients)
recipe_proto.type_counts.ingredients = determine_item_type_counts(indexed_ingredients)
local products = {} ---@type FormattedRecipeItem[]
for _, base_product in pairs(recipe_proto.products) do
local formatted_product = generate_formatted_item(base_product, "product")
if formatted_product.amount > 0 then
table.insert(products, formatted_product)
-- Update the main product as well, if present
if recipe_proto.main_product ~= nil
and formatted_product.type == recipe_proto.main_product.type
and formatted_product.name == recipe_proto.main_product.name then
recipe_proto.main_product = formatted_product --[[@as Product]]
end
end
end
combine_identical_products(products) -- only needed products, ingredients can't have duplicates
local indexed_products = create_type_indexed_list(products)
recipe_proto.type_counts.products = determine_item_type_counts(indexed_products)
-- Reduce item amounts for items that are both an ingredient and a product
for _, items_of_type in pairs(indexed_ingredients) do
for _, ingredient in pairs(items_of_type) do
local peer_product = indexed_products[ingredient.item.type][ingredient.item.name]
if peer_product then
local difference = ingredient.item.amount - peer_product.item.amount
if difference < 0 then
ingredients[ingredient.index].amount = nil
products[peer_product.index].amount = -difference
elseif difference > 0 then
ingredients[ingredient.index].amount = difference
products[peer_product.index].amount = nil
else
ingredients[ingredient.index].amount = nil
products[peer_product.index].amount = nil
end
end
end
end
-- Remove items after the fact so the iteration above doesn't break
for _, item_table in pairs{ingredients, products} do
for i = #item_table, 1, -1 do
if item_table[i].amount == nil then table.remove(item_table, i) end
end
end
recipe_proto.ingredients = ingredients
recipe_proto.products = products
end
-- Multiplies recipe products and ingredients by the given amount
---@param recipe_proto FPRecipePrototype
---@param factor number
function generator_util.multiply_recipe(recipe_proto, factor)
---@param item_list FormattedRecipeItem[]
local function multiply_items(item_list)
for _, item in pairs(item_list) do
item.amount = item.amount * factor
if item.proddable_amount ~= nil then
item.proddable_amount = item.proddable_amount * factor
end
end
end
multiply_items(recipe_proto.products)
multiply_items(recipe_proto.ingredients)
recipe_proto.energy = recipe_proto.energy * factor
end
-- Adds the additional proto's ingredients, products and energy to the main proto
---@param main_proto FPRecipePrototype
---@param additional_proto FPRecipePrototype
function generator_util.combine_recipes(main_proto, additional_proto)
---@param item_category "products" | "ingredients"
local function add_items_to_main_proto(item_category)
local additional_items = additional_proto[item_category] ---@type FormattedRecipeItem[]
for _, item in pairs(additional_items) do
table.insert(main_proto[item_category], item)
end
combine_identical_products(main_proto[item_category])
end
add_items_to_main_proto("products")
add_items_to_main_proto("ingredients")
main_proto.energy = main_proto.energy + additional_proto.energy
end
-- Active mods table needed for the funtions below
local active_mods = script.active_mods
-- Determines whether this recipe is a recycling one or not
local recycling_recipe_mods = {
["IndustrialRevolution"] = {"^scrap%-.*"},
["space-exploration"] = {"^se%-recycle%-.*"},
["angelspetrochem"] = {"^converter%-.*"},
["reverse-factory"] = {"^rf%-.*"},
["ZRecycling"] = {"^dry411srev%-.*"}
}
local active_recycling_recipe_mods = {} ---@type string[]
for modname, patterns in pairs(recycling_recipe_mods) do
for _, pattern in pairs(patterns) do
if active_mods[modname] then
table.insert(active_recycling_recipe_mods, pattern)
end
end
end
---@param proto LuaRecipePrototype
---@return boolean
function generator_util.is_recycling_recipe(proto)
for _, pattern in pairs(active_recycling_recipe_mods) do
if string.match(proto.name, pattern) then return true end
end
return false
end
-- Determines whether the given recipe is a barreling or stacking one
local compacting_recipe_mods = {
["base"] = {"^fill%-.*", "^empty%-.*"},
["deadlock-beltboxes-loaders"] = {"^deadlock%-stacks%-.*", "^deadlock%-packrecipe%-.*",
"^deadlock%-unpackrecipe%-.*"},
["DeadlockCrating"] = {"^deadlock%-packrecipe%-.*", "^deadlock%-unpackrecipe%-.*"},
["IntermodalContainers"] = {"^ic%-load%-.*", "^ic%-unload%-.*"},
["space-exploration"] = {"^se%-delivery%-cannon%-pack%-.*"},
["Satisfactorio"] = {"^packaged%-.*", "^unpack%-.*"}
}
local active_compacting_recipe_mods = {} ---@type string[]
for modname, patterns in pairs(compacting_recipe_mods) do
for _, pattern in pairs(patterns) do
if active_mods[modname] then
table.insert(active_compacting_recipe_mods, pattern)
end
end
end
---@param proto LuaRecipePrototype
---@return boolean
function generator_util.is_compacting_recipe(proto)
for _, pattern in pairs(active_compacting_recipe_mods) do
if string.match(proto.name, pattern) then return true end
end
return false
end
-- Determines whether this recipe is irrelevant or not and should thus be excluded
local irrelevant_recipe_categories = {
["Transport_Drones"] = {"transport-drone-request", "transport-fluid-request"},
["Mining_Drones"] = {"mining-depot"},
["Deep_Storage_Unit"] = {"deep-storage-item", "deep-storage-fluid",
"deep-storage-item-big", "deep-storage-fluid-big",
"deep-storage-item-mk2/3", "deep-storage-fluid-mk2/3"},
["Satisfactorio"] = {"craft-bench", "equipment", "awesome-shop",
"resource-scanner", "object-scanner", "building",
"hub-progressing", "space-elevator", "mam"}
}
local irrelevant_recipe_categories_lookup = {} ---@type { [string] : true }
for mod, categories in pairs(irrelevant_recipe_categories) do
for _, category in pairs(categories) do
if active_mods[mod] then
irrelevant_recipe_categories_lookup[category] = true
end
end
end
---@param recipe LuaRecipePrototype
---@return boolean
function generator_util.is_irrelevant_recipe(recipe)
return irrelevant_recipe_categories_lookup[recipe.category]
end
-- Determines whether this machine is irrelevant or not and should thus be excluded
local irrelevant_machine_mods = {
["GhostOnWater"] = {"waterGhost%-.*"}
}
local irrelevant_machines_lookup = {} ---@type string[]
for modname, patterns in pairs(irrelevant_machine_mods) do
for _, pattern in pairs(patterns) do
if active_mods[modname] then
table.insert(irrelevant_machines_lookup, pattern)
end
end
end
---@param proto LuaEntityPrototype
---@return boolean
function generator_util.is_irrelevant_machine(proto)
for _, pattern in pairs(irrelevant_machines_lookup) do
if string.match(proto.name, pattern) then return true end
end
return false
end
-- Finds a sprite for the given entity prototype
---@param proto LuaEntityPrototype
---@return SpritePath | nil
function generator_util.determine_entity_sprite(proto)
local entity_sprite = "entity/" .. proto.name ---@type SpritePath
if game.is_valid_sprite_path(entity_sprite) then
return entity_sprite
end
local items_to_place_this = proto.items_to_place_this
if items_to_place_this and next(items_to_place_this) then
local item_sprite = "item/" .. items_to_place_this[1].name ---@type SpritePath
if game.is_valid_sprite_path(item_sprite) then
return item_sprite
end
end
return nil
end
-- Determines how long a rocket takes to launch for the given rocket silo prototype
-- These stages mirror the in-game progression and timing exactly. Most steps take an additional tick (+1)
-- due to how the game code is written. If one stage is completed, you can only progress to the next one
-- in the next tick. No stages can be skipped, meaning a minimal sequence time is around 10 ticks long.
---@param silo_proto LuaEntityPrototype
---@return number? launch_sequence_time
function generator_util.determine_launch_sequence_time(silo_proto)
local rocket_proto = silo_proto.rocket_entity_prototype
if not rocket_proto then return nil end -- meaning this isn't a rocket silo proto
local rocket_flight_threshold = 0.5 -- hardcoded in the game files
local launch_steps = {
lights_blinking_open = (1 / silo_proto.light_blinking_speed) + 1,
doors_opening = (1 / silo_proto.door_opening_speed) + 1,
doors_opened = silo_proto.rocket_rising_delay + 1,
rocket_rising = (1 / rocket_proto.rising_speed) + 1,
rocket_ready = 14, -- estimate for satellite insertion delay
launch_started = silo_proto.launch_wait_time + 1,
engine_starting = (1 / rocket_proto.engine_starting_speed) + 1,
-- This calculates a fractional amount of ticks. Also, math.log(x) calculates the natural logarithm
rocket_flying = math.log(1 + rocket_flight_threshold * rocket_proto.flying_acceleration
/ rocket_proto.flying_speed) / math.log(1 + rocket_proto.flying_acceleration),
lights_blinking_close = (1 / silo_proto.light_blinking_speed) + 1,
doors_closing = (1 / silo_proto.door_opening_speed) + 1
}
local total_ticks = 0
for _, ticks_taken in pairs(launch_steps) do
total_ticks = total_ticks + ticks_taken
end
return (total_ticks / 60) -- retured value is in seconds
end
-- Returns nil if no effect is true, returns the effects otherwise
---@param allowed_effects AllowedEffects
---@return AllowedEffects? allowed_effects
function generator_util.format_allowed_effects(allowed_effects)
if allowed_effects == nil then return nil end
for _, allowed in pairs(allowed_effects) do
if allowed == true then return allowed_effects end
end
return nil -- all effects are false
end
-- Returns the appropriate prototype name for the given item, incorporating temperature
---@param item FormattedRecipeItem | RelevantItem
---@param name string
---@return string
function generator_util.format_temperature_name(item, name)
-- Optionally two dashes to account for negative temperatures
return (item.temperature) and string.gsub(name, "%-+[0-9]+$", "") or name
end
-- Returns the appropriate localised string for the given item, incorporating temperature
---@param item FormattedRecipeItem | RelevantItem
---@param proto LuaItemPrototype | LuaFluidPrototype
---@return LocalisedString
function generator_util.format_temperature_localised_name(item, proto)
if item.temperature then
return {"", proto.localised_name, " (", item.temperature, " ", {"fp.unit_celsius"}, ")"}
else
return proto.localised_name
end
end
-- Adds the tooltip for the given recipe
---@param recipe FPRecipePrototype
function generator_util.add_recipe_tooltip(recipe)
local tooltip = {"", {"fp.tt_title", recipe.localised_name}} ---@type LocalisedString
local current_table, next_index = tooltip, 3
if recipe.energy ~= nil then
current_table, next_index = util.build_localised_string(
{"", "\n ", {"fp.crafting_time"}, ": ", recipe.energy}, current_table, next_index)
end
for _, item_type in ipairs{"ingredients", "products"} do
local locale_key = (item_type == "ingredients") and "fp.pu_ingredient" or "fp.pu_product"
current_table, next_index = util.build_localised_string(
{"", "\n ", {locale_key, 2}, ":"}, current_table, next_index)
if not next(recipe[item_type]) then
current_table, next_index = util.build_localised_string({
"\n ", {"fp.none"}}, current_table, next_index)
else
local items = recipe[item_type] ---@type FormattedRecipeItem[]
for _, item in ipairs(items) do
local name = generator_util.format_temperature_name(item, item.name)
local proto = game[item.type .. "_prototypes"][name] ---@type LuaItemPrototype | LuaFluidPrototype
local localised_name = generator_util.format_temperature_localised_name(item, proto)
current_table, next_index = util.build_localised_string({("\n " .. "[" .. item.type .. "="
.. name .. "] " .. item.amount .. "x "), localised_name}, current_table, next_index)
end
end
end
recipe.tooltip = tooltip
end
---@class ItemGroup
---@field name string
---@field localised_name LocalisedString
---@field order string
---@field valid boolean
-- Generates a table imitating LuaGroup to avoid lua-cpp bridging
---@param group LuaGroup
---@return ItemGroup group_table
function generator_util.generate_group_table(group)
return {name=group.name, localised_name=group.localised_name, order=group.order, valid=true}
end
return generator_util

View File

@@ -0,0 +1,263 @@
-- The loader contains the code that runs on_load, pre-caching some data structures that are needed later
local loader = {}
---@alias RecipeMap { [ItemCategoryID]: { [ItemID]: { [RecipeID]: true } } }
---@alias ItemCategoryID integer
---@alias ItemID integer
---@alias RecipeID integer
---@alias ModuleMap { [string]: FPModulePrototype }
-- ** LOCAL UTIL **
-- Returns a list of recipe groups in their proper order
---@return ItemGroup[]
local function ordered_recipe_groups()
-- Make a dict with all recipe groups
local group_dict = {} ---@type { [string]: ItemGroup }
for _, recipe in pairs(global.prototypes.recipes) do
if group_dict[recipe.group.name] == nil then
group_dict[recipe.group.name] = recipe.group
end
end
-- Invert it
local groups = {} ---@type ItemGroup[]
for _, group in pairs(group_dict) do
table.insert(groups, group)
end
-- Sort it
---@param a ItemGroup
---@param b ItemGroup
---@return boolean
local function sorting_function(a, b)
if a.order < b.order then return true
elseif a.order > b.order then return false end
return false
end
table.sort(groups, sorting_function)
return groups
end
-- Maps all items to the recipes that produce or consume them ([item_type][item_name] = {[recipe_id] = true}
---@param item_type "products" | "ingredients"
---@return RecipeMap
local function recipe_map_from(item_type)
local map = {} ---@type RecipeMap
for _, recipe in pairs(global.prototypes.recipes) do
for _, item in ipairs(recipe[item_type] --[[@as FormattedRecipeItem[] ]]) do
local item_proto = prototyper.util.find_prototype("items", item.name, item.type) ---@cast item_proto -nil
map[item_proto.category_id] = map[item_proto.category_id] or {}
map[item_proto.category_id][item_proto.id] = map[item_proto.category_id][item_proto.id] or {}
map[item_proto.category_id][item_proto.id][recipe.id] = true
end
end
return map
end
-- Generates a list of all items, sorted for display in the picker
---@return FPItemPrototype[]
local function sorted_items()
local items = {}
for _, type in pairs{"item", "fluid"} do
for _, item in pairs(PROTOTYPE_MAPS.items[type].members) do
-- Silly checks needed here for migration purposes
if item.group.valid and item.subgroup.valid then table.insert(items, item) end
end
end
-- Sorts the objects according to their group, subgroup and order
---@param a FPItemPrototype
---@param b FPItemPrototype
---@return boolean
local function sorting_function(a, b)
if a.group.order < b.group.order then return true
elseif a.group.order > b.group.order then return false
elseif a.subgroup.order < b.subgroup.order then return true
elseif a.subgroup.order > b.subgroup.order then return false
elseif a.order < b.order then return true
elseif a.order > b.order then return false end
return false
end
table.sort(items, sorting_function)
return items
end
-- Generates a table mapping modules to their prototype by name
---@return ModuleMap
local function module_name_map()
local map = {} ---@type ModuleMap
for _, category in pairs(global.prototypes.modules) do
for _, module in pairs(category.members) do
map[module.name] = module
end
end
return map
end
local attribute_generators = {} ---@type { [string]: function }
---@param belt FPBeltPrototype
---@return LocalisedString[]
function attribute_generators.belts(belt)
local throughput_string = {"", belt.throughput .. " ", {"fp.pl_item", 2}, "/", {"fp.unit_second"}}
return {"fp.attribute_line", {"fp.throughput"}, throughput_string}
end
---@param beacon FPBeaconPrototype
---@return LocalisedString[]
function attribute_generators.beacons(beacon)
return {"", {"fp.attribute_line", {"fp.module_slots"}, beacon.module_limit},
{"fp.attribute_line", {"fp.effectivity"}, (beacon.effectivity * 100) .. "%"},
{"fp.attribute_line", {"fp.energy_consumption"}, util.format.SI_value(beacon.energy_usage * 60, "W", 3)}}
end
---@param wagon FPWagonPrototype
---@return LocalisedString[]
function attribute_generators.wagons(wagon)
local storage_unit = (wagon.category == "cargo-wagon") and {"fp.pl_stack", wagon.storage} or {"fp.l_fluid"}
return {"fp.attribute_line", {"fp.storage"}, {"", util.format.number(wagon.storage, 3) .. " ", storage_unit}}
end
---@param fuel FPFuelPrototype
---@return LocalisedString[]
function attribute_generators.fuels(fuel)
return {"", {"fp.attribute_line", {"fp.fuel_value"}, util.format.SI_value(fuel.fuel_value, "J", 3)},
{"fp.attribute_line", {"fp.emissions_multiplier"}, fuel.emissions_multiplier}}
end
---@param machine FPMachinePrototype
---@return LocalisedString[]
function attribute_generators.machines(machine)
local pollution = machine.energy_usage * (machine.emissions * 60) * 60
return {"", {"fp.attribute_line", {"fp.crafting_speed"}, util.format.number(machine.speed, 3)},
{"fp.attribute_line", {"fp.energy_consumption"}, util.format.SI_value(machine.energy_usage * 60, "W", 3)},
{"fp.attribute_line", {"fp.pollution"}, {"", util.format.SI_value(pollution, "P/m", 3)}},
{"fp.attribute_line", {"fp.module_slots"}, machine.module_limit}}
end
---@alias PrototypeAttributes { [DataType]: { [integer]: LocalisedString } }
---@alias PrototypeAttributesWithCategory { [DataType]: { [integer]: { [integer]: LocalisedString } } }
-- Generates the attribute strings for some types of prototypes
---@return PrototypeAttributes | PrototypeAttributesWithCategory
local function prototype_attributes()
local relevant_prototypes = {"belts", "beacons", "wagons", "fuels", "machines"}
local attributes = {} ---@type PrototypeAttributes | PrototypeAttributesWithCategory
for _, data_type in pairs(relevant_prototypes) do
local prototypes = global.prototypes[data_type] ---@type AnyIndexedPrototypes
local generator_function = attribute_generators[data_type]
attributes[data_type] = {}
if prototyper.data_types[data_type] == false then
---@cast prototypes IndexedPrototypes<FPPrototype>
---@cast attributes PrototypeAttributes
for proto_id, prototype in pairs(prototypes) do
attributes[data_type][proto_id] = generator_function(prototype)
end
else
---@cast prototypes IndexedPrototypesWithCategory<FPPrototypeWithCategory>
---@cast attributes PrototypeAttributesWithCategory
for category_id, category in pairs(prototypes) do
attributes[data_type][category_id] = {}
local attribute_category = attributes[data_type][category_id]
for proto_id, prototype in pairs(category.members) do
attribute_category[proto_id] = generator_function(prototype)
end
end
end
end
return attributes
end
---@alias MappedPrototypes<T> { [string]: T }
---@alias MappedPrototypesWithCategory<T> { [string]: { id: integer, name: string, members: { [string]: T } } }
---@alias MappedCategory { id: integer, name: string, members: { [string]: table } }
---@class PrototypeMaps: { [DataType]: table }
---@field machines MappedPrototypesWithCategory<FPMachinePrototype>
---@field recipes MappedPrototypes<FPRecipePrototype>
---@field items MappedPrototypesWithCategory<FPItemPrototype>
---@field fuels MappedPrototypesWithCategory<FPFuelPrototype>
---@field belts MappedPrototypes<FPBeltPrototype>
---@field wagons MappedPrototypesWithCategory<FPWagonPrototype>
---@field modules MappedPrototypesWithCategory<FPModulePrototype>
---@field beacons MappedPrototypes<FPBeaconPrototype>
---@param data_types { [DataType]: boolean }
---@return PrototypeMaps
local function prototype_maps(data_types)
local maps = {} ---@type PrototypeMaps
for data_type, has_categories in pairs(data_types) do
local map = {}
if not has_categories then
---@cast map MappedPrototypes<FPPrototype>
---@type IndexedPrototypes<FPPrototype>
local prototypes = global.prototypes[data_type]
for _, prototype in pairs(prototypes) do
map[prototype.name] = prototype
end
else
---@cast map MappedPrototypesWithCategory<FPPrototypeWithCategory>
---@type IndexedPrototypesWithCategory<FPPrototypeWithCategory>
local prototypes = global.prototypes[data_type]
for _, category in pairs(prototypes) do
map[category.name] = { name=category.name, id=category.id, members={} }
for _, prototype in pairs(category.members) do
map[category.name].members[prototype.name] = prototype
end
end
end
maps[data_type] = map
end
return maps
end
-- ** TOP LEVEL **
---@param skip_check boolean Whether the mod version check is skipped
function loader.run(skip_check)
-- If the mod version changed, the loader will be re-run after migration anyways
if not skip_check and script.active_mods["factoryplanner"] ~= global.mod_version then return end
util.nth_tick.register_all()
PROTOTYPE_MAPS = prototype_maps(prototyper.data_types)
PROTOTYPE_ATTRIBUTES = prototype_attributes()
ORDERED_RECIPE_GROUPS = ordered_recipe_groups()
RECIPE_MAPS = {
produce = recipe_map_from("products"),
consume = recipe_map_from("ingredients")
}
SORTED_ITEMS = sorted_items()
MODULE_NAME_MAP = module_name_map()
end
return loader

View File

@@ -0,0 +1,152 @@
-- This code handles the general migration process of the mod's global table
-- It decides whether and which migrations should be applied, in appropriate order
local migrator = {}
---@alias MigrationMasterList { [integer]: { version: VersionString, migration: Migration } }
---@alias Migration { global: function, player_table: function, subfactory: function, packed_subfactory: function }
---@alias MigrationObject PlayerTable | FPSubfactory | FPPackedSubfactory
-- Returns a table containing all existing migrations in order
local migration_masterlist = { ---@type MigrationMasterList
[1] = {version="0.18.20", migration=require("backend.migrations.migration_0_18_20")},
[2] = {version="0.18.27", migration=require("backend.migrations.migration_0_18_27")},
[3] = {version="0.18.29", migration=require("backend.migrations.migration_0_18_29")},
[4] = {version="0.18.38", migration=require("backend.migrations.migration_0_18_38")},
[5] = {version="0.18.42", migration=require("backend.migrations.migration_0_18_42")},
[6] = {version="0.18.45", migration=require("backend.migrations.migration_0_18_45")},
[7] = {version="0.18.48", migration=require("backend.migrations.migration_0_18_48")},
[8] = {version="0.18.49", migration=require("backend.migrations.migration_0_18_49")},
[9] = {version="0.18.51", migration=require("backend.migrations.migration_0_18_51")},
[10] = {version="1.0.6", migration=require("backend.migrations.migration_1_0_6")},
[11] = {version="1.1.5", migration=require("backend.migrations.migration_1_1_5")},
[12] = {version="1.1.6", migration=require("backend.migrations.migration_1_1_6")},
[13] = {version="1.1.8", migration=require("backend.migrations.migration_1_1_8")},
[14] = {version="1.1.14", migration=require("backend.migrations.migration_1_1_14")},
[15] = {version="1.1.19", migration=require("backend.migrations.migration_1_1_19")},
[16] = {version="1.1.21", migration=require("backend.migrations.migration_1_1_21")},
[17] = {version="1.1.25", migration=require("backend.migrations.migration_1_1_25")},
[18] = {version="1.1.26", migration=require("backend.migrations.migration_1_1_26")},
[19] = {version="1.1.27", migration=require("backend.migrations.migration_1_1_27")},
[20] = {version="1.1.42", migration=require("backend.migrations.migration_1_1_42")},
[21] = {version="1.1.43", migration=require("backend.migrations.migration_1_1_43")},
[22] = {version="1.1.59", migration=require("backend.migrations.migration_1_1_59")},
[23] = {version="1.1.61", migration=require("backend.migrations.migration_1_1_61")},
[24] = {version="1.1.65", migration=require("backend.migrations.migration_1_1_65")},
[25] = {version="1.1.66", migration=require("backend.migrations.migration_1_1_66")},
[26] = {version="1.1.67", migration=require("backend.migrations.migration_1_1_67")},
}
-- ** LOCAL UTIL **
-- Compares two mod versions, returns true if v1 is an earlier version than v2 (v1 < v2)
-- Version numbers have to be of the same structure: same amount of numbers, separated by a '.'
---@param v1 VersionString
---@param v2 VersionString
---@return boolean
local function compare_versions(v1, v2)
local split_v1 = util.split_string(v1, ".")
local split_v2 = util.split_string(v2, ".")
for i = 1, #split_v1 do
if split_v1[i] == split_v2[i] then
-- continue
elseif split_v1[i] < split_v2[i] then
return true
else
return false
end
end
return false -- return false if both versions are the same
end
-- Applies given migrations to the object
---@param migrations Migration[]
---@param function_name string
---@param object MigrationObject?
---@param player LuaPlayer?
local function apply_migrations(migrations, function_name, object, player)
for _, migration in ipairs(migrations) do
local migration_function = migration[function_name]
if migration_function ~= nil then
local migration_message = migration_function(object, player) ---@type string
-- If no message is returned, everything went fine
if migration_message == "removed" then break end
end
end
end
-- Determines whether a migration needs to take place, and if so, returns the appropriate range of the
-- migration_masterlist. If the version changed, but no migrations apply, it returns an empty array.
---@param previous_version VersionString
---@return Migration[]
local function determine_migrations(previous_version)
local migrations = {}
local found = false
for _, migration in ipairs(migration_masterlist) do
if compare_versions(previous_version, migration.version) then found = true end
if found then table.insert(migrations, migration.migration) end
end
return migrations
end
-- ** TOP LEVEL **
-- Applies any appropriate migrations to the global table
function migrator.migrate_global()
local migrations = determine_migrations(global.mod_version)
apply_migrations(migrations, "global", nil, nil)
global.mod_version = script.active_mods["factoryplanner"]
end
-- Applies any appropriate migrations to the given factory
---@param player LuaPlayer
function migrator.migrate_player_table(player)
local player_table = util.globals.player_table(player)
if player_table ~= nil then -- don't apply migrations to new players
local migrations = determine_migrations(player_table.mod_version)
-- General migrations
local old_version = player_table.mod_version -- keep for comparison below
apply_migrations(migrations, "player_table", player_table, player)
player_table.mod_version = global.mod_version
-- Subfactory migrations
for _, factory_name in pairs({"factory", "archive"}) do
--local outdated_subfactories = {}
for _, subfactory in pairs(Factory.get_all(player_table[factory_name], "Subfactory")) do
if subfactory.mod_version ~= old_version then -- out-of-sync subfactory
--error("Out of date subfactory, please report this to the mod author including the save file")
--table.insert(outdated_subfactories, subfactory)
else
apply_migrations(migrations, "subfactory", subfactory, player)
subfactory.mod_version = global.mod_version
end
end
--[[ -- Remove subfactories who weren't migrated along properly for some reason
-- TODO This is likely due to an old, now-fixed bug that left them behind
for _, subfactory in pairs(outdated_subfactories) do
Factory.remove(player_table[factory_name], subfactory)
end ]]
end
end
end
-- Applies any appropriate migrations to the given export_table's subfactories
---@param export_table ExportTable
function migrator.migrate_export_table(export_table)
local migrations = determine_migrations(export_table.mod_version)
for _, packed_subfactory in pairs(export_table.subfactories) do
-- This migration type won't need the player argument, and removing it allows
-- us to run imports without having a player attached
apply_migrations(migrations, "packed_subfactory", packed_subfactory, nil)
end
export_table.mod_version = global.mod_version
end
return migrator

View File

@@ -0,0 +1,326 @@
local generator = require("backend.handlers.generator")
prototyper = {
util = {},
defaults = {}
}
-- The purpose of the prototyper is to recreate the global tables containing all relevant data types.
-- It also handles some other things related to prototypes, such as updating preferred ones, etc.
-- Its purpose is to not lose any data, so if a dataset of a factory-dataset doesn't exist anymore
-- in the newly loaded global tables, it saves the name in string-form instead and makes the
-- concerned factory-dataset invalid. This accomplishes that invalid data is only permanently
-- removed when the user tells the subfactory to repair itself, giving him a chance to re-add the
-- missing mods. It is also a better separation of responsibilities and avoids some redundant code.
-- Load order is important here: machines->recipes->items->fuels
-- The boolean indicates whether this prototype has categories or not
---@type { [DataType]: boolean }
prototyper.data_types = {machines = true, recipes = false, items = true, fuels = true,
belts = false, wagons = true, modules = true, beacons = false}
---@alias DataType "machines" | "recipes" | "items" | "fuels" | "belts" | "wagons" | "modules" | "beacons"
---@alias NamedPrototypes<T> { [string]: T }
---@alias NamedPrototypesWithCategory<T> { [string]: { name: string, members: { [string]: T } } } }
---@alias NamedCategory { name: string, members: { [string]: table } }
---@alias AnyNamedPrototypes NamedPrototypes | NamedPrototypesWithCategory
---@alias IndexedPrototypes<T> { [integer]: T }
---@alias IndexedPrototypesWithCategory<T> { [integer]: { id: integer, name: string, members: { [integer]: T } } }
---@alias IndexedCategory { id: integer, name: string, members: { [integer]: table } }
---@alias AnyIndexedPrototypes IndexedPrototypes | IndexedPrototypesWithCategory
---@class PrototypeLists: { [DataType]: table }
---@field machines IndexedPrototypesWithCategory<FPMachinePrototype>
---@field recipes IndexedPrototypes<FPRecipePrototype>
---@field items IndexedPrototypesWithCategory<FPItemPrototype>
---@field fuels IndexedPrototypesWithCategory<FPFuelPrototype>
---@field belts IndexedPrototypes<FPBeltPrototype>
---@field wagons IndexedPrototypesWithCategory<FPWagonPrototype>
---@field modules IndexedPrototypesWithCategory<FPModulePrototype>
---@field beacons IndexedPrototypes<FPBeaconPrototype>
-- Converts given prototype list to use ids as keys, and sorts it if desired
---@param data_type DataType
---@param prototype_sorting_function function
---@return AnyIndexedPrototypes
local function convert_and_sort(data_type, prototype_sorting_function)
local final_list = {}
---@param list AnyNamedPrototypes[]
---@param sorting_function function
---@param category_id integer?
---@return AnyIndexedPrototypes
local function apply(list, sorting_function, category_id)
local new_list = {} ---@type (IndexedPrototypes | IndexedCategory)[]
for _, member in pairs(list) do table.insert(new_list, member) end
if sorting_function then table.sort(new_list, sorting_function) end
for id, member in pairs(new_list) do
member.id = id
member.category_id = category_id -- can be nil
member.data_type = data_type
end
return new_list
end
---@param a NamedCategory
---@param b NamedCategory
---@return boolean
local function category_sorting_function(a, b)
if a.name < b.name then return true
elseif a.name > b.name then return false end
return false
end
if prototyper.data_types[data_type] == false then
final_list = apply(global.prototypes[data_type], prototype_sorting_function, nil)
---@cast final_list IndexedPrototypes<FPPrototype>
else
final_list = apply(global.prototypes[data_type], category_sorting_function, nil)
---@cast final_list IndexedPrototypesWithCategory<FPPrototypeWithCategory>
for id, category in pairs(final_list) do
category.members = apply(category.members, prototype_sorting_function, id)
end
end
return final_list
end
function prototyper.build()
global.prototypes = {} ---@type PrototypeLists
local prototypes = global.prototypes
for data_type, _ in pairs(prototyper.data_types) do
---@type AnyNamedPrototypes
prototypes[data_type] = generator[data_type].generate()
end
-- Second pass to do some things that can't be done in the first pass due to the strict sequencing
for data_type, _ in pairs(prototyper.data_types) do
local second_pass = generator[data_type].second_pass ---@type function
if second_pass ~= nil then second_pass(prototypes[data_type]) end
end
-- Finish up generation by converting lists to use ids as keys, and sort if desired
for data_type, _ in pairs(prototyper.data_types) do
local sorting_function = generator[data_type].sorting_function ---@type function
prototypes[data_type] = convert_and_sort(data_type, sorting_function) ---@type AnyIndexedPrototypes
end
end
-- ** UTIL **
-- Returns the attribute string for the given prototype
---@param prototype AnyFPPrototype
---@return LocalisedString
function prototyper.util.get_attributes(prototype)
if prototype.category_id == nil then
---@cast prototype FPPrototype
return PROTOTYPE_ATTRIBUTES[prototype.data_type][prototype.id]
else
---@cast prototype FPPrototypeWithCategory
return PROTOTYPE_ATTRIBUTES[prototype.data_type][prototype.category_id][prototype.id]
end
end
-- Finds the given prototype by name. Can use the loader cache since it'll exist at this point.
---@param data_type DataType
---@param prototype_name string
---@param category_name string?
---@return AnyFPPrototype?
function prototyper.util.find_prototype(data_type, prototype_name, category_name)
--if data_type == nil then llog(prototype_name, category_name) end
local prototype_map = PROTOTYPE_MAPS[data_type]
if category_name == nil then
return prototype_map[prototype_name] -- can be nil
else
local category = prototype_map[category_name] ---@type MappedCategory
if category == nil then return nil end
return category.members[prototype_name] -- can be nil
end
end
---@class FPPackedPrototype
---@field name string
---@field category string
---@field data_type DataType
---@field simplified boolean
-- Returns a new table that only contains the given prototypes' identifiers
---@param proto AnyFPPrototype
---@param category string?
---@return FPPackedPrototype
function prototyper.util.simplify_prototype(proto, category)
return { name = proto.name, category = category, data_type = proto.data_type, simplified = true }
end
-- Validates given object with prototype, which includes trying to find the correct
-- new reference for its prototype, if able. Returns valid-status at the end.
---@param prototype AnyFPPrototype | FPPackedPrototype
---@param category_designation ("category" | "type")?
---@return AnyFPPrototype | FPPackedPrototype
function prototyper.util.validate_prototype_object(prototype, category_designation)
local updated_proto = prototype
if prototype.simplified then -- try to unsimplify, otherwise it stays that way
---@cast prototype FPPackedPrototype
local new_proto = prototyper.util.find_prototype(prototype.data_type, prototype.name, prototype.category)
if new_proto then updated_proto = new_proto end
else
---@cast prototype AnyFPPrototype
local category = prototype[category_designation] ---@type string
local new_proto = prototyper.util.find_prototype(prototype.data_type, prototype.name, category)
updated_proto = new_proto or prototyper.util.simplify_prototype(prototype, category)
end
return updated_proto
end
-- Build the necessary RawDictionaries for translation
function prototyper.util.build_translation_dictionaries()
for _, item_category in ipairs(global.prototypes.items) do
translator.new(item_category.name)
for _, proto in pairs(item_category.members) do
translator.add(item_category.name, proto.name, proto.localised_name)
end
end
translator.new("recipe")
for _, proto in pairs(global.prototypes.recipes) do
translator.add("recipe", proto.name, proto.localised_name)
end
end
-- Migrates the prototypes for default beacons and modules
---@param player_table PlayerTable
function prototyper.util.migrate_mb_defaults(player_table)
local mb_defaults = player_table.preferences.mb_defaults
local find = prototyper.util.find_prototype
local machine = mb_defaults.machine
if machine then
mb_defaults.machine = find("modules", machine.name, machine.category) --[[@as FPModulePrototype ]]
end
local second = mb_defaults.machine_secondary
if second then
mb_defaults.machine_secondary = find("modules", second.name, second.category) --[[@as FPModulePrototype ]]
end
local beacon = mb_defaults.beacon
if beacon then
mb_defaults.beacon = find("beacons", beacon.name, nil) --[[@as FPBeaconPrototype ]]
end
end
-- ** DEFAULTS **
---@alias PrototypeDefault FPPrototype
---@alias PrototypeWithCategoryDefault { [integer]: FPPrototypeWithCategory }
---@alias AnyPrototypeDefault PrototypeDefault | PrototypeWithCategoryDefault
-- Returns the default prototype for the given type, incorporating the category, if given
---@param player LuaPlayer
---@param data_type DataType
---@param category_id integer?
---@return AnyPrototypeDefault
function prototyper.defaults.get(player, data_type, category_id)
---@type AnyPrototypeDefault
local default = util.globals.preferences(player).default_prototypes[data_type]
return (category_id == nil) and default or default[category_id]
end
-- Sets the default prototype for the given type, incorporating the category, if given
---@param player LuaPlayer
---@param data_type DataType
---@param prototype_id integer
---@param category_id integer?
function prototyper.defaults.set(player, data_type, prototype_id, category_id)
local default_prototypes = util.globals.preferences(player).default_prototypes
local prototypes = global.prototypes[data_type] ---@type AnyIndexedPrototypes
if category_id == nil then
---@type PrototypeDefault
default_prototypes[data_type] = prototypes[prototype_id]
else
---@type PrototypeWithCategoryDefault
default_prototypes[data_type][category_id] = prototypes[category_id].members[prototype_id]
end
end
-- Returns the fallback default for the given type of prototype
---@param data_type DataType
---@return AnyPrototypeDefault
function prototyper.defaults.get_fallback(data_type)
local prototypes = global.prototypes[data_type] ---@type AnyIndexedPrototypes
local fallback = {}
if prototyper.data_types[data_type] == false then
---@cast prototypes IndexedPrototypes<FPPrototype>
fallback = prototypes[1]
else
---@cast prototypes IndexedPrototypesWithCategory<FPPrototypeWithCategory>
fallback = {} ---@type PrototypeWithCategoryDefault
for _, category in pairs(prototypes) do
fallback[category.id] = category.members[1]
end
end
return fallback
end
-- Kinda unclean that I have to do this, but it's better than storing it elsewhere
local category_designations = {machines="category", items="type",
fuels="category", wagons="category", modules="category"}
-- Migrates the default_prototypes preferences, trying to preserve the users choices
-- When this is called, the loader cache will already exist
---@param player_table PlayerTable
function prototyper.defaults.migrate(player_table)
local default_prototypes = player_table.preferences.default_prototypes
for data_type, has_categories in pairs(prototyper.data_types) do
if default_prototypes[data_type] ~= nil then
if not has_categories then
-- Use the same prototype if an equivalent can be found, use fallback otherwise
local default_proto = default_prototypes[data_type] ---@type PrototypeDefault
local equivalent_proto = prototyper.util.find_prototype(data_type, default_proto.name, nil)
---@cast equivalent_proto PrototypeDefault
default_prototypes[data_type] = equivalent_proto ---@type PrototypeDefault
or prototyper.defaults.get_fallback(data_type)
else
local new_defaults = {} ---@type PrototypeWithCategoryDefault
local fallback = prototyper.defaults.get_fallback(data_type)
local default_map = {} ---@type { [string]: FPPrototype }
for _, default_proto in pairs(default_prototypes[data_type]--[[@as PrototypeWithCategoryDefault]]) do
local category_name = default_proto[category_designations[data_type]] ---@type string
default_map[category_name] = default_proto
end
---@type IndexedPrototypesWithCategory<FPPrototypeWithCategory>
local categories = global.prototypes[data_type]
for _, category in pairs(categories) do
local previous_category = default_map[category.name]
if previous_category then -- category existed previously
local proto_name = previous_category.name
---@type PrototypeWithCategoryDefault
new_defaults[category.id] = prototyper.util.find_prototype(data_type, proto_name, category.name)
end
new_defaults[category.id] = new_defaults[category.id] or fallback[category.id]
end
default_prototypes[data_type] = new_defaults ---@type PrototypeWithCategoryDefault
end
end
end
end

View File

@@ -0,0 +1,207 @@
---@diagnostic disable
-- This file contains functionality to rig the interface for various setups
-- that make for good screenshots. It provides a remote interface that its
-- companion scenario calls to actually take the screenshots.
-- This code is terrible and uses some functions completely inappropriately,
-- but it needs to do that to manipulate the interface because GUI events
-- can't be raised manually anymore.
local mod_gui = require("mod-gui")
local handler_requires = {"ui.base.compact_dialog", "ui.base.modal_dialog", "ui.main.title_bar",
"ui.dialogs.picker_dialog", "ui.dialogs.porter_dialog"}
local handlers = {} -- Need to require these here since it can't be done inside an event
for _, path in pairs(handler_requires) do handlers[path] = require(path) end
local function return_dimensions(scene, frame)
local dimensions = {actual_size=frame.actual_size, location=frame.location}
-- We do this on teardown so the frame has time to adjust all its sizes
remote.call("screenshotter_output", "return_dimensions", scene, dimensions)
end
local function open_modal(player, dialog, modal_data)
main_dialog.toggle(player)
util.globals.main_elements(player).main_frame.location = player.display_resolution -- hack city
util.raise.open_dialog(player, {dialog=dialog, modal_data=modal_data, skip_dimmer=true})
end
local function modal_teardown(player, scene)
return_dimensions(scene, util.globals.modal_elements(player).modal_frame)
util.raise.close_dialog(player, "cancel")
end
local function get_handler(path, index, event, name)
local gui_handlers = handlers[path][index].gui[event]
for _, handler_table in pairs(gui_handlers) do
if handler_table.name == name then return handler_table.handler end
end
end
local function set_machine_default(player, proto_name, category_name)
local proto = prototyper.util.find_prototype("machines", proto_name, category_name)
prototyper.defaults.set(player, "machines", proto.id, proto.category_id)
end
local actions = {
player_setup = function(player)
local player_table = util.globals.player_table(player)
-- Mod settings
settings.get_player_settings(player)["fp_display_gui_button"] = {value = false}
settings.get_player_settings(player)["fp_products_per_row"] = {value = 5}
settings.get_player_settings(player)["fp_subfactory_list_rows"] = {value = 18}
-- Preferences
player_table.preferences.recipe_filters = {disabled = true, hidden = false}
player_table.preferences.ignore_barreling_recipes = true
player_table.preferences.ignore_recycling_recipes = true
player_table.preferences.done_column = true
player_table.preferences.mb_defaults = { -- naughty use of the prototyper function
machine = prototyper.util.find_prototype("modules", "productivity-module-3", "productivity"),
machine_secondary = nil,
beacon = prototyper.util.find_prototype("modules", "speed-module-3", "speed"),
beacon_count = 8
}
prototyper.defaults.set(player, "belts", PROTOTYPE_MAPS.belts["fast-transport-belt"].id)
set_machine_default(player, "electric-mining-drill", "basic-solid")
set_machine_default(player, "steel-furnace", "smelting")
set_machine_default(player, "assembling-machine-2", "crafting")
set_machine_default(player, "assembling-machine-3", "advanced-crafting")
set_machine_default(player, "assembling-machine-2", "crafting-with-fluid")
-- Subfactories
local export_string = "eNrtWmlTGk0Q/iup/WrAmdk5dqzKBy9CMBqOiMGURe0uA67uxR4CWv73t4dDycELmIQchR8smenu6e55+hp5MIKo075TSepFobFn4CIucma8NtQwjpKsDbupyoy9B8OxUzUlsBAQdH3Pgc+oiEmR68+2m0XJKPbtMFTJs6jH10aaO5NdT6XG3ucHI7QDLeuzF/TeeJkKdr0wVUmmkqtX9SjKUhCXeYFKXdsHOo5eG2GUaV4Ddhw/V3HihUC29wDSq0nUyd2xjpFzo9zZuuvbqWY5jHwfVrV9sJpFcbvrR1Gi6d97oVqRz1d3yjf28PN+aSxljqExM3MEq09GNgLlg2Wjf8mm54uLJ4qCwELqeip0VSG23durV8dDO4jB0lWt/mwgddZv3kh00rvfP61nlo+HzaDcFajptlo8eVdHpXr/ZFiN89Fu89BUoi8Pq4KNbt+a9aPB6W1d3e87528lOwirTuukZWaoerJ7FF2/Ld2822kcVvYDUS4djM6Sd42oV/Uq5fsD1izv1O5rOzHF6CD1OlV1MBpUZat8LUpHlZ3+Rxm3hmrwqey0LPDrIPU/XKDz89HlWcm9rJ8PT6yb3olrssagVcnNPOf3fdmMeD5Cl4qjo9LOvtsXF6XBG7AW7Ivxze2xtq9WV7sjPCz135ea936zciyc2ik/bjQuDvuN/F4NA8L52zsxYHfm5en5IHnfov7gw30PtfrlXInBbqW+M0rywfta7ejopn5U60asMtxJz6ulVtit3bxr9Afmh9uzc8t29m/D3qBaa5YvysllBZXdXkgjfNBKjuWnZlgJT68vKubZSdTyW0ooJ7xonX/68Gm3l3thtWPfJQ2eVT/azZvd/YPgGF1+NK6+D1AIbIBDFunFKVAWwAO8kXoAD6/rqY6xlyW5ApyMYs2iYaURaAdRHsIB2ALQJKqfe4nqtGerD0ZHdeFGOm1nBEzT5a+4HIi89lSlru2nahorU9xPjp0hfWbR49XLwgysn7GN9yZpbw3yRLlePCb6ES+6dqZ6Olz3DDexu5kX9rTuzyLaUz9PVvTBMz3qEwW074HyTs1EdiKt+9iBIEglrgozuwdLGIGTA9u9nhr3td4gVgWODyoUplQFcx2luxGc1fa9wMuezoeClPuqPS1Kk0/pqjC887JRYcKzXJN5pnlA0mePnY5F/Q9kptq6T5wqiLNRO/V1kdtDX0tqgFVzyfd06tpHDWXb1QX6W9umO0uM6cbtKeGcJdYL3ZnGSnVW9uOYev5Y8mIHkjUdePBk8obz1VyWmeJ21TSjq+zKiSGxPf9vzALk12eBxV3Pl5BCX0GKrB6TK2Jq4SUtAhCxWFFgSixEKDItaRFmUUsKS1KKCRESE4mIZRJkYsI2BjYv0VGReZsuPJO/t4hbHXFLbmoB7jglRYsB8BAmCEuqYUYoAI8Kgjg2JeOUYYo5woxvEHdKsyWeW+jmSWiPgbNNeH8y/Fa6sAUgnGGQcEG5kJwJRpngnOlUxyWzLAoZUDDCLcYot8TmcPidJnILxRe1jOQPbhnXh/uquPgzEH+15DGIfPMYtFaQ2J07G7rmTsH1Ejf3su2Mup1RtzPqT51RVwqxhcnGKn6RbEwC0wVGGHMuucmEIAIxJCy88QYvCqFj+C1ZYztgvKzDW3pjC0BoEV5kJuIwVGBpUsEtJDiCYkctRAljkhNTYKDC8HuDDZ4bxQCCgms7m+7stghcF4FL72oB9kyBaBFZMMSaFpWSYkakhHlWYxCZmJuYcikI3+iEm2ZK+YXYh6taWqj0fxN/Z7+07jQ+r/Cf2Sv9ltli2yttpFdaFlmLnmCRwEXolXRegMYIEgST1OSwYkLVohAjkCEYIpLIjT5BpBmEbcFJVniD/bmJYv0CNfH8T0oS3Vx79juHRL7XKYx3l9W+axV4ru3P3zIr6kd2aIcJklxfMSGMmJxgCiO3ZSFMqcDQqEhMoVvhFI1rwtN0rE99/JVV8+c/VSzDz6pTgyUhMBiBMBACXCSJfjSWQjBTok0WTjAgApuSwtiwZRhw7BTq1hgzvysqnqpn4IW6d+sknu+vqfe/9yaHfw3Q18r40AwWpf4xOTYFRD9kA667Q0ZgOKFMSAmjCcwlSJq/+AnuR76PlcZeRyV6Nrt69S9948z++425evwPtGH0aA=="
util.porter.add_by_string(player, export_string)
local trash = Factory.get_by_gui_position(player_table.factory, "Subfactory", 5)
Factory.remove(player_table.factory, trash)
Factory.add(player_table.archive, trash)
local hotness = Factory.get_by_gui_position(player_table.factory, "Subfactory", 3)
util.context.set_subfactory(player, hotness)
solver.update(player, hotness)
util.raise.refresh(player, "all", nil)
-- Research
player.force.technologies["oil-processing"].researched=true
player.force.technologies["coal-liquefaction"].researched=true
-- Player inventory
player.insert{name="assembling-machine-3", count=9}
player.insert{name="assembling-machine-2", count=1}
player.insert{name="electric-mining-drill", count=29}
player.insert{name="speed-module-3", count=14}
player.insert{name="speed-module-2", count=1}
player.insert{name="chemical-plant", count=6}
end,
setup_01_main_interface = function(player)
local translation_progress = mod_gui.get_frame_flow(player)["flib_translation_progress"]
if translation_progress then translation_progress.visible = false end
main_dialog.toggle(player)
end,
teardown_01_main_interface = function(player)
local main_frame = util.globals.main_elements(player).main_frame
return_dimensions("01_main_interface", main_frame)
end,
setup_02_compact_interface = function(player)
util.globals.main_elements(player).main_frame.location = player.display_resolution -- hack city
view_state.select(player, 2)
local toggle_handler = get_handler("ui.main.title_bar", 1, "on_gui_click", "switch_to_compact_view")
toggle_handler(player, nil, nil)
end,
teardown_02_compact_interface = function(player)
local compact_frame = util.globals.ui_state(player).compact_elements.compact_frame
return_dimensions("02_compact_interface", compact_frame)
local toggle_handler = get_handler("ui.base.compact_dialog", 2, "on_gui_click", "switch_to_main_view")
toggle_handler(player, nil, nil)
end,
setup_03_item_picker = function(player)
local modal_data = {item_id=nil, item_category="product"}
open_modal(player, "picker", modal_data)
local modal_elements = util.globals.modal_elements(player)
modal_elements.search_textfield.text = "f"
local search_handler = get_handler("ui.base.modal_dialog", 1, "on_gui_text_changed", "modal_searchfield")
search_handler(player, nil, {text="f"})
local group_handler = get_handler("ui.dialogs.picker_dialog", 1, "on_gui_click", "select_picker_item_group")
group_handler(player, {group_id=3}, nil)
modal_elements.item_choice_button.sprite = "item/raw-fish"
modal_elements.belt_amount_textfield.text = "0.5"
modal_elements.belt_choice_button.elem_value = "fast-transport-belt"
local belt_handler = get_handler("ui.dialogs.picker_dialog", 1, "on_gui_elem_changed", "picker_choose_belt")
belt_handler(player, nil, {element=modal_elements.belt_choice_button, elem_value="fast-transport-belt"})
modal_elements.search_textfield.focus()
end,
teardown_03_item_picker = (function(player) modal_teardown(player, "03_item_picker") end),
setup_04_recipe_picker = function(player)
local product_proto = prototyper.util.find_prototype("items", "petroleum-gas", "fluid")
---@cast product_proto -nil
open_modal(player, "recipe", {category_id=product_proto.category_id,
product_id=product_proto.id, production_type="produce"})
end,
teardown_04_recipe_picker = (function(player) modal_teardown(player, "04_recipe_picker") end),
setup_05_machine = function(player)
local floor = util.globals.context(player).floor
local line = Collection.get_by_gui_position(floor.Line, 2) ---@cast line -nil
local modal_data = {floor_id=floor.id, line_id=line.id, recipe_name=line.recipe.proto.localised_name}
open_modal(player, "machine", modal_data)
end,
teardown_05_machine = (function(player) modal_teardown(player, "05_machine") end),
setup_06_import = function(player)
open_modal(player, "import", nil)
local export_string = "eNrtWt+P4jYQ/lfcPG+2hG2ra6Q+tJUqVepJp+Ohqu5Q5DiT3Wn9I7UdVIT43ztOzIK4RRDKHnBF4gHb4/HM941n7CSLRJmqmIF1aHSSJ9l9dv/tKLlL4J/GWF/QqAOf5Iuk5A5WAt+TQC2xpPboPqNfaHPhjZ03kmsNdq1qeZe4tuxHEVySf1gkmqug6wOqxx/Qg/oatQPrwU7Ze2O8I3UeFTjBJcl9N7pLtPFhbkIj76ypWtHZZMo/QZB4vqBV+kYhJHdB8lfSS9Kr5s9GShoOXpKsN01RS2Ns0PIbatinrZPZpU3CDGSSZ+vxXzrdy3XHZAXBnHqfAZgokOT1/P/i75rwpneKFKZOIGgBacPFX1P2B/AnNum7vtqNS8reUaAx03rmaCKwqIWtFX/UKfvx7xYtMM5qdE/MG1a2KCs2abCieLO90O8oZZhX8lLOmQaomDI0C2mccVrAKNJrUPsdbFBE03RvQmf0dId/pMChaiTWCFWSe9sCuThvwpSAS4CQK9PSUnn2hvy10HlQFaveRVJBTeRURTmnSbF7a1ZJYVVEk2ouHcQQiMT1y66oWnm0nJ4ypgiTrVgKWWCAuAWBTSf0X7AV3MNjiMI8EZbXHvVjsH2toojo9z2bMf++NyAwQpIzWKmsTLC9g5UUgRWgPX+krmxE0CsunqJz23aTWlClJBPSKJU+DDG6NrRWIVGhf16f8nMroYg5um+5Q4Nzhn6e9nP2W7I5aTNMv1kj9rZT9VIgxZFdoRS9EM8aQTV+XjgZakE+2l5hQt5u5Jq3EfJlCHwuQh371Oc4ssfJuimi4IaHb46E2TWUSg7Gt5PeXHZ8cmDHA4H9KUIRMvjBu9NylNe4FcevvxVfqLOD+Btt8Tc+dGMMIjCUvdR5vM6MeqMxYgNhfYsirVurecfDjcxrJfOFin3j86g6PL7COryOmelRF7XxJxe1QcHHqxmnQ3aVCrSiRX87aN8O2reD9tkP2n2BN5pK/Fk2Zv//VuFPU+GFaQjKVPDyc5f2a+Hx9Bs/O/nGz7ZiJXuVWHEeQKaNJNf2+hyeMJ+zFg+9hmwa/CXvd+cJ0bS0B1yzbxxe6pMS1Rji0aYdmftQKbkjCJ2RWJ2fSYU6pO/KopQDDb+Ue9kL2fvusCP4+Ngj+OlPig9HXwUHPT4gQU8cltzurdNPoND57r3dWQ4XnQGCd/VN+0HmXkaueXgVCvtNaJBwsUaAcyG/7gFnS/pMhAYrbPfOsnvRPdDiy7yLP1xOIrjku/j4mu/i08/+UYR7/ixhyr7sT0Kmy38BcOORig=="
local modal_elements = util.globals.modal_elements(player)
modal_elements.import_textfield.text = export_string
local textfield_handler = get_handler("ui.dialogs.porter_dialog", 1, "on_gui_text_changed", "import_string")
textfield_handler(player, nil, {element=modal_elements.import_textfield, text=export_string})
local import_handler = get_handler("ui.dialogs.porter_dialog", 1, "on_gui_click", "import_subfactories")
import_handler(player, nil, nil)
modal_elements.subfactory_checkboxes["tmp_1"].state = false
modal_elements.subfactory_checkboxes["tmp_3"].state = false
modal_elements.master_checkbox.state = false
end,
teardown_06_import = (function(player) modal_teardown(player, "06_import") end),
setup_07_utility = function(player)
open_modal(player, "utility", nil)
end,
teardown_07_utility = (function(player) modal_teardown(player, "07_utility") end),
setup_08_preferences = function(player)
open_modal(player, "preferences", nil)
end,
teardown_08_preferences = (function(player) modal_teardown(player, "08_preferences") end)
}
local function initial_setup()
DEV_ACTIVE = false -- desync city, but it's fiiine. Avoids any accidental artifacts.
return {"09_settings"}
end
local function execute_action(player_index, action_name)
local player = game.get_player(player_index)
actions[action_name](player)
end
remote.add_interface("screenshotter_input", {
initial_setup = initial_setup,
execute_action = execute_action
})

View File

@@ -0,0 +1,391 @@
require("backend.classes.Collection")
require("backend.classes.Factory")
require("backend.classes.Subfactory")
require("backend.classes.Floor")
require("backend.classes.Line")
require("backend.classes.Recipe")
require("backend.classes.Machine")
require("backend.classes.Beacon")
require("backend.classes.ModuleSet")
require("backend.classes.Module")
require("backend.classes.Item")
require("backend.classes.Fuel")
local loader = require("backend.handlers.loader")
local migrator = require("backend.handlers.migrator")
require("backend.handlers.prototyper")
require("backend.handlers.screenshotter")
require("backend.calculation.solver")
---@class PlayerTable
---@field preferences PreferencesTable
---@field settings SettingsTable
---@field ui_state UIStateTable
---@field mod_version VersionString
---@field index PlayerIndex
---@field factory FPFactory
---@field archive FPFactory
---@field translation_tables { [string]: TranslatedDictionary }?
---@field clipboard ClipboardEntry?
---@class PreferencesTable
---@field pause_on_interface boolean
---@field tutorial_mode boolean
---@field utility_scopes { components: "Subfactory" | "Floor" }
---@field recipe_filters { disabled: boolean, hidden: boolean }
---@field attach_subfactory_products boolean
---@field show_floor_items boolean
---@field fold_out_subfloors boolean
---@field ingredient_satisfaction boolean
---@field round_button_numbers boolean
---@field ignore_barreling_recipes boolean
---@field ignore_recycling_recipes boolean
---@field done_column boolean
---@field pollution_column boolean
---@field line_comment_column boolean
---@field mb_defaults MBDefaults
---@field default_prototypes DefaultPrototypes
---@class MBDefaults
---@field machine FPModulePrototype
---@field machine_secondary FPModulePrototype
---@field beacon FPBeaconPrototype
---@field beacon_count number
---@class DefaultPrototypes
---@field machines PrototypeWithCategoryDefault
---@field fuels PrototypeWithCategoryDefault
---@field belts PrototypeDefault
---@field wagons PrototypeWithCategoryDefault
---@field beacons PrototypeDefault
-- ** LOCAL UTIL **
---@param player LuaPlayer
local function reload_preferences(player)
-- Reloads the user preferences, incorporating previous preferences if possible
local preferences = global.players[player.index].preferences
preferences.pause_on_interface = preferences.pause_on_interface or false
if preferences.tutorial_mode == nil then preferences.tutorial_mode = true end
preferences.utility_scopes = preferences.utility_scopes or {components = "Subfactory"}
preferences.recipe_filters = preferences.recipe_filters or {disabled = false, hidden = false}
preferences.attach_subfactory_products = preferences.attach_subfactory_products or false
preferences.show_floor_items = preferences.show_floor_items or false
preferences.fold_out_subfloors = preferences.fold_out_subfloors or false
preferences.ingredient_satisfaction = preferences.ingredient_satisfaction or false
preferences.round_button_numbers = preferences.round_button_numbers or false
preferences.ignore_barreling_recipes = preferences.ignore_barreling_recipes or false
preferences.ignore_recycling_recipes = preferences.ignore_recycling_recipes or false
preferences.done_column = preferences.done_column or false
preferences.pollution_column = preferences.pollution_column or false
preferences.line_comment_column = preferences.line_comment_column or false
preferences.mb_defaults = preferences.mb_defaults
or {machine = nil, machine_secondary = nil, beacon = nil, beacon_count = nil}
preferences.default_prototypes = preferences.default_prototypes or {}
preferences.default_prototypes = {
machines = preferences.default_prototypes.machines or prototyper.defaults.get_fallback("machines"),
fuels = preferences.default_prototypes.fuels or prototyper.defaults.get_fallback("fuels"),
belts = preferences.default_prototypes.belts or prototyper.defaults.get_fallback("belts"),
wagons = preferences.default_prototypes.wagons or prototyper.defaults.get_fallback("wagons"),
beacons = preferences.default_prototypes.beacons or prototyper.defaults.get_fallback("beacons")
}
end
---@class SettingsTable
---@field show_gui_button boolean
---@field products_per_row integer
---@field subfactory_list_rows integer
---@field default_timescale integer
---@field belts_or_lanes string
---@field prefer_product_picker boolean
---@field prefer_matrix_solver boolean
---@param player LuaPlayer
local function reload_settings(player)
-- Writes the current user mod settings to their player_table, for read-performance
local settings = settings.get_player_settings(player)
local settings_table = {}
local timescale_to_number = {one_second = 1, one_minute = 60, one_hour = 3600}
settings_table.show_gui_button = settings["fp_display_gui_button"].value
settings_table.products_per_row = tonumber(settings["fp_products_per_row"].value)
settings_table.subfactory_list_rows = tonumber(settings["fp_subfactory_list_rows"].value)
settings_table.default_timescale = timescale_to_number[settings["fp_default_timescale"].value] ---@type integer
settings_table.belts_or_lanes = settings["fp_view_belts_or_lanes"].value
settings_table.prefer_product_picker = settings["fp_prefer_product_picker"].value
settings_table.prefer_matrix_solver = settings["fp_prefer_matrix_solver"].value
global.players[player.index].settings = settings_table
end
---@class UIStateTable
---@field main_dialog_dimensions DisplayResolution
---@field last_action string
---@field view_states ViewStates
---@field messages PlayerMessage[]
---@field main_elements { [string]: LuaGuiElement }
---@field compact_elements { [string]: LuaGuiElement }
---@field context Context
---@field last_selected_picker_group integer?
---@field modal_dialog_type ModalDialogType?
---@field modal_data ModalData?
---@field queued_dialog_metadata ModalData?
---@field flags UIStateFlags
---@class UIStateFlags
---@field archive_open boolean
---@field selection_mode boolean
---@field compact_view boolean
---@field recalculate_on_subfactory_change boolean
---@param player LuaPlayer
local function reset_ui_state(player)
local ui_state_table = {}
ui_state_table.main_dialog_dimensions = nil ---@type DisplayResolution Can only be calculated after on_init
ui_state_table.last_action = nil ---@type string The last user action (used for rate limiting)
ui_state_table.view_states = nil ---@type ViewStates The state of the production views
ui_state_table.messages = {} ---@type PlayerMessage[] The general message/warning list
ui_state_table.main_elements = {} -- References to UI elements in the main interface
ui_state_table.compact_elements = {} -- References to UI elements in the compact interface
ui_state_table.context = util.context.create(player) -- The currently displayed set of data
ui_state_table.last_selected_picker_group = nil ---@type integer The item picker category that was last selected
ui_state_table.modal_dialog_type = nil ---@type ModalDialogType The internal modal dialog type
ui_state_table.modal_data = nil ---@type ModalData Data that can be set for a modal dialog to use
ui_state_table.queued_dialog_metadata = nil ---@type ModalData Info on dialog to open after the current one closes
ui_state_table.flags = {
archive_open = false, -- Wether the players subfactory archive is currently open
selection_mode = false, -- Whether the player is currently using a selector
compact_view = false, -- Whether the user has switched to the compact main view
recalculate_on_subfactory_change = false -- Whether calculations should re-run
}
-- The UI table gets replaced because the whole interface is reset
global.players[player.index].ui_state = ui_state_table
end
---@param player LuaPlayer
local function create_player_table(player)
global.players[player.index] = {}
local player_table = global.players[player.index]
player_table.mod_version = global.mod_version
player_table.index = player.index
player_table.factory = Factory.init()
player_table.archive = Factory.init()
player_table.preferences = {}
reload_preferences(player)
reload_settings(player)
reset_ui_state(player)
util.messages.raise(player, "hint", {"fp.hint_tutorial"}, 12)
end
---@param player LuaPlayer
local function refresh_player_table(player)
local player_table = global.players[player.index]
reload_preferences(player)
reload_settings(player)
reset_ui_state(player)
-- This whole reset thing will be moved ...
local archive_subfactories = Factory.get_in_order(player_table.archive, "Subfactory")
player_table.archive.selected_subfactory = archive_subfactories[1] -- can be nil
local factory = player_table.factory
local subfactories = Factory.get_in_order(factory, "Subfactory")
local subfactory_to_select = subfactories[1] -- can be nil
if factory.selected_subfactory ~= nil then
-- Get the selected subfactory from the factory to make sure it still exists
local selected_subfactory = Factory.get(factory, "Subfactory", factory.selected_subfactory.id)
if selected_subfactory ~= nil then subfactory_to_select = selected_subfactory end
end
util.context.set_subfactory(player, subfactory_to_select)
player_table.translation_tables = nil
player_table.clipboard = nil
end
---@return FPSubfactory?
local function import_tutorial_subfactory()
local imported_tutorial_factory, error = util.porter.process_export_string(TUTORIAL_EXPORT_STRING)
if error then return nil end
return Factory.get(imported_tutorial_factory --[[@as FPSubfactory]], "Subfactory", 1)
end
local function global_init()
-- Set up a new save for development if necessary
local freeplay = remote.interfaces["freeplay"]
if DEV_ACTIVE and freeplay then -- Disable freeplay popup-message
if freeplay["set_skip_intro"] then remote.call("freeplay", "set_skip_intro", true) end
if freeplay["set_disable_crashsite"] then remote.call("freeplay", "set_disable_crashsite", true) end
end
-- Initiates all factorio-global variables
global.mod_version = script.active_mods["factoryplanner"] ---@type VersionString
global.players = {} ---@type { [PlayerIndex]: PlayerTable }
-- Save metadata about currently registered on_nth_tick events
global.nth_tick_events = {} ---@type { [Tick]: NthTickEvent }
prototyper.build() -- Generate all relevant prototypes and save them in global
loader.run(true) -- Run loader which creates useful caches of prototype data
-- Retain current modset to detect mod changes for subfactories that became invalid
global.installed_mods = script.active_mods ---@type ModToVersion
-- Import the tutorial subfactory so it's 'cached'
global.tutorial_subfactory = import_tutorial_subfactory()
-- Initialize flib's translation module
translator.on_init()
prototyper.util.build_translation_dictionaries()
-- Create player tables for all existing players
for _, player in pairs(game.players) do create_player_table(player) end
end
-- Prompts migrations, a GUI and prototype reload, and a validity check on all subfactories
local function handle_configuration_change()
prototyper.build() -- Run prototyper
loader.run(true) -- Re-run the loader to update with the new prototypes
migrator.migrate_global() -- Migrate global
-- Runs through all players, even new ones without player_table
for _, player in pairs(game.players) do
-- Migrate player_table data if it exists
migrator.migrate_player_table(player)
-- Create or update player_table
refresh_player_table(player)
local player_table = global.players[player.index]
-- Migrate the prototypes used in the player's preferences
prototyper.defaults.migrate(player_table)
prototyper.util.migrate_mb_defaults(player_table)
-- Update the validity of the entire factory and archive
Factory.validate(player_table.factory)
Factory.validate(player_table.archive)
end
global.installed_mods = script.active_mods
global.tutorial_subfactory = import_tutorial_subfactory()
translator.on_configuration_changed()
prototyper.util.build_translation_dictionaries()
for index, player in pairs(game.players) do
util.gui.reset_player(player) -- Destroys all existing GUI's
util.gui.toggle_mod_gui(player) -- Recreates the mod-GUI if necessary
-- Update factory and archive calculations in case prototypes changed in a relevant way
local player_table = global.players[index] ---@type PlayerTable
Factory.update_calculations(player_table.factory, player)
Factory.update_calculations(player_table.archive, player)
end
end
-- ** TOP LEVEL **
script.on_init(global_init)
script.on_configuration_changed(handle_configuration_change)
script.on_load(loader.run)
-- ** PLAYER DATA **
script.on_event(defines.events.on_player_created, function(event)
local player = game.get_player(event.player_index) ---@cast player -nil
-- Sets up the player_table for the new player
create_player_table(player)
-- Sets up the mod-GUI for the new player if necessary
util.gui.toggle_mod_gui(player)
-- Add the subfactories that are handy for development
if DEV_ACTIVE then util.porter.add_by_string(player, DEV_EXPORT_STRING) end
end)
script.on_event(defines.events.on_player_removed, function(event)
global.players[event.player_index] = nil
end)
script.on_event(defines.events.on_player_joined_game, translator.on_player_joined_game)
script.on_event(defines.events.on_runtime_mod_setting_changed, function(event)
if event.setting_type == "runtime-per-user" then -- this mod only has per-user settings
local player = game.get_player(event.player_index) ---@cast player -nil
reload_settings(player)
if event.setting == "fp_display_gui_button" then
util.gui.toggle_mod_gui(player)
elseif event.setting == "fp_products_per_row"
or event.setting == "fp_subfactory_list_rows"
or event.setting == "fp_prefer_product_picker" then
main_dialog.rebuild(player, false)
elseif event.setting == "fp_view_belts_or_lanes" then
local player_table = util.globals.player_table(player)
-- Goes through every subfactory's top level products and updates their defined_by
local defined_by = player_table.settings.belts_or_lanes
Factory.update_product_definitions(player_table.factory, defined_by)
Factory.update_product_definitions(player_table.archive, defined_by)
local subfactory = player_table.ui_state.context.subfactory
solver.update(player, subfactory)
main_dialog.rebuild(player, false)
end
end
end)
-- ** TRANSLATION **
-- Required by flib's translation module
script.on_event(defines.events.on_tick, translator.on_tick)
-- Keep translation going
script.on_event(defines.events.on_string_translated, translator.on_string_translated)
---@param event GuiEvent
local function dictionaries_ready(event)
local player = game.get_player(event.player_index) ---@cast player -nil
local player_table = util.globals.player_table(player)
player_table.translation_tables = translator.get_all(event.player_index)
modal_dialog.set_searchfield_state(player) -- enables searchfields if possible
end
-- Save translations once they are complete
script.on_event(translator.on_player_dictionaries_ready, dictionaries_ready)
-- ** COMMANDS **
commands.add_command("fp-reset-prototypes", {"command-help.fp_reset_prototypes"}, handle_configuration_change)
commands.add_command("fp-restart-translation", {"command-help.fp_restart_translation"}, function()
translator.on_init()
prototyper.util.build_translation_dictionaries()
end)
commands.add_command("fp-shrinkwrap-interface", {"command-help.fp_shrinkwrap_interface"}, function(command)
if command.player_index then main_dialog.shrinkwrap_interface(game.get_player(command.player_index)) end
end)

View File

@@ -0,0 +1,28 @@
[
"0.18.20",
"0.18.27",
"0.18.29",
"0.18.38",
"0.18.42",
"0.18.45",
"0.18.48",
"0.18.49",
"0.18.51",
"1.0.6",
"1.1.5",
"1.1.6",
"1.1.8",
"1.1.14",
"1.1.19",
"1.1.21",
"1.1.25",
"1.1.26",
"1.1.27",
"1.1.42",
"1.1.43",
"1.1.59",
"1.1.61",
"1.1.65",
"1.1.66",
"1.1.67"
]

View File

@@ -0,0 +1,17 @@
---@diagnostic disable
local migration = {}
function migration.global()
end
function migration.player_table(player_table)
end
function migration.subfactory(subfactory)
end
function migration.packed_subfactory(packed_subfactory)
end
return migration

View File

@@ -0,0 +1,18 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
local types = {"Ingredient", "Product", "Byproduct"}
for _, type in pairs(types) do
for _, item in pairs(Subfactory.get_in_order(subfactory, type)) do
local req_amount = {
defined_by = "amount",
amount = item.required_amount
}
item.required_amount = req_amount
end
end
end
return migration

View File

@@ -0,0 +1,30 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
player_table.preferences.default_prototypes = {
belts = {structure_type="simple", prototype=player_table.preferences.preferred_belt},
beacons = {structure_type="simple", prototype=player_table.preferences.preferred_beacon},
machines = {structure_type="complex", prototypes=player_table.preferences.default_machines.categories}
}
player_table.preferences.preferred_belt = nil
player_table.preferences.preferred_beacon = nil
player_table.preferences.preferred_fuel = nil
player_table.preferences.default_machines = nil
end
function migration.subfactory(subfactory)
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
local fuel = Line.get_by_gui_position(line, "Fuel", 1)
if fuel ~= nil and fuel.valid and fuel.proto ~= nil then
fuel.category = fuel.proto.fuel_category
line.fuel = fuel
end
line.Fuel = nil
end
end
end
return migration

View File

@@ -0,0 +1,16 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory, player)
if util.globals.settings(player).belts_or_lanes == "lanes" then
for _, product in pairs(Subfactory.get_in_order(subfactory, "Product")) do
if product.required_amount.defined_by == "belts" then
product.required_amount.defined_by = "lanes"
product.required_amount.amount = product.required_amount.amount * 2
end
end
end
end
return migration

View File

@@ -0,0 +1,60 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
player_table.factory.valid = nil
player_table.archive.valid = nil
end
function migration.subfactory(subfactory)
if not subfactory.valid then
Factory.remove(subfactory.parent, subfactory)
return "removed"
end
for _, item in pairs(Subfactory.get_in_order(subfactory, "Ingredient")) do item.type = nil end
for _, item in pairs(Subfactory.get_in_order(subfactory, "Product")) do item.type = nil end
for _, item in pairs(Subfactory.get_in_order(subfactory, "Byproduct")) do item.type = nil end
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
line.machine.parent = line
local module_count = 0
for _, module in pairs(Line.get_in_order(line, "Module")) do
module_count = module_count + module.amount
module.category = nil
module.parent = line.machine
end
line.machine.category = nil
line.machine.Module = line.Module
line.Module = nil
line.machine.module_count = module_count
line.machine.total_effects = {consumption = 0, speed = 0, productivity = 0, pollution = 0}
if line.beacon then
line.beacon.module.category = nil
end
if line.fuel then
line.fuel.category = nil
line.fuel.parent = line.machine
line.machine.fuel = line.fuel
line.fuel = nil
end
if line.subfloor then
line.machine = nil
line.beacon = nil
line.priority_product_proto = nil
line.production_ratio = nil
line.uncapped_production_ratio = nil
end
end
end
end
return migration

View File

@@ -0,0 +1,42 @@
---@diagnostic disable
local migration = {}
local function migrate_collection(collection, object_class)
collection.class = collection.type
collection.type = nil
collection.object_class = object_class
end
function migration.player_table(player_table)
migrate_collection(player_table.factory.Subfactory, "Subfactory")
migrate_collection(player_table.archive.Subfactory, "Subfactory")
end
function migration.subfactory(subfactory)
migrate_collection(subfactory.Product, "Item")
migrate_collection(subfactory.Byproduct, "Item")
migrate_collection(subfactory.Ingredient, "Item")
migrate_collection(subfactory.Floor, "Floor")
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
migrate_collection(floor.Line, "Line")
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
if line.subfloor then
line.recipe = nil
line.percentage = nil
line.production_ratio = nil
line.uncapped_production_ratio = nil
else
migrate_collection(line.machine.Module, "Module")
end
migrate_collection(line.Product, "Item")
migrate_collection(line.Byproduct, "Item")
migrate_collection(line.Ingredient, "Item")
end
end
end
return migration

View File

@@ -0,0 +1,21 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
player_table.ui_state.current_activity = nil
end
function migration.subfactory(subfactory)
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
if line.machine and line.machine.fuel then line.machine.fuel.satisfied_amount = 0 end
line.Product = Collection.init()
line.Byproduct = Collection.init()
line.Ingredient = Collection.init()
end
end
end
return migration

View File

@@ -0,0 +1,17 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
subfactory.scopes = {}
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
line.Product = Collection.init()
line.Byproduct = Collection.init()
line.Ingredient = Collection.init()
end
end
end
return migration

View File

@@ -0,0 +1,22 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
local preferences = player_table.preferences
local mb_defaults = preferences.mb_defaults
if mb_defaults then
mb_defaults.machine = mb_defaults.module
mb_defaults.module = nil
end
local optional_columns = preferences.optional_production_columns
if optional_columns then
preferences.pollution_column = optional_columns.pollution_column
preferences.line_comment_column = optional_columns.line_comments
preferences.optional_production_columns = nil
end
end
return migration

View File

@@ -0,0 +1,17 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
if subfactory.icon and subfactory.icon.type == "virtual-signal" then
subfactory.icon.type = "virtual"
end
end
function migration.packed_subfactory(packed_subfactory)
if packed_subfactory.icon and packed_subfactory.icon.type == "virtual-signal" then
packed_subfactory.icon.type = "virtual"
end
end
return migration

View File

@@ -0,0 +1,17 @@
---@diagnostic disable
local migration = {} -- same migration as 0.18.51, not sure why that one didn't take
function migration.subfactory(subfactory)
if subfactory.icon and subfactory.icon.type == "virtual-signal" then
subfactory.icon.type = "virtual"
end
end
function migration.packed_subfactory(packed_subfactory)
if packed_subfactory.icon and packed_subfactory.icon.type == "virtual-signal" then
packed_subfactory.icon.type = "virtual"
end
end
return migration

View File

@@ -0,0 +1,34 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
if not line.subfloor then
line.done = false
line.machine.force_limit = line.machine.hard_limit
line.machine.hard_limit = nil
end
end
end
end
function migration.packed_subfactory(packed_subfactory)
local function update_lines(floor)
for _, packed_line in ipairs(floor.Line.objects) do
if packed_line.subfloor then
update_lines(packed_line.subfloor)
else
packed_line.done = false
packed_line.machine.force_limit = packed_line.machine.hard_limit
packed_line.machine.hard_limit = nil
end
end
end
update_lines(packed_subfactory.top_floor)
end
return migration

View File

@@ -0,0 +1,9 @@
---@diagnostic disable
local migration = {}
function migration.global()
global.nth_tick_events = {}
end
return migration

View File

@@ -0,0 +1,10 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
local Subfactory = player_table.archive.Subfactory
Subfactory.count = table_size(Subfactory.datasets)
end
return migration

View File

@@ -0,0 +1,13 @@
---@diagnostic disable
local migration = {}
function migration.global()
for _, event_data in pairs(global.nth_tick_events) do
if event_data.handler_name == "delete_subfactory" then
event_data.handler_name = "delete_subfactory_for_good"
end
end
end
return migration

View File

@@ -0,0 +1,15 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
if line.machine and line.machine.fuel and line.machine.fuel.proto == nil then
Floor.remove(floor, line) -- needs to be fully removed to fix the issue
end
end
end
end
return migration

View File

@@ -0,0 +1,39 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_all(floor, "Line")) do
local beacon = line.beacon
if beacon and beacon.module then
beacon.Module = Collection.init()
beacon.module_count = 0
beacon.module.parent = beacon
Collection.add(beacon.Module, beacon.module)
beacon.module_count = beacon.module_count + beacon.module.amount
beacon.module = nil
end
end
end
end
function migration.packed_subfactory(packed_subfactory)
local function update_lines(floor)
for _, packed_line in ipairs(floor.Line.objects) do
if packed_line.subfloor then
update_lines(packed_line.subfloor)
elseif packed_line.beacon and packed_line.beacon.module then
local beacon = packed_line.beacon
local module = Module.unpack(beacon.module)
local modules = Collection.init()
Collection.add(modules, module)
beacon.Module = Collection.pack(modules, Module)
beacon.module_count = module.amount
end
end
end
update_lines(packed_subfactory.top_floor)
end
return migration

View File

@@ -0,0 +1,72 @@
---@diagnostic disable
local migration = {}
local function migrate_modules(object)
object.module_count = nil
if object.proto.simplified then object.proto = {module_limit = 0} end
local module_set = ModuleSet.init(object)
for _, module in pairs(object.Module.datasets) do
ModuleSet.add(module_set, module.proto, module.amount)
end
object.Module = nil
object.module_set = module_set
end
local function migrate_packed_modules(packed_object)
local module_set = {
modules = packed_object.Module,
module_count = packed_object.module_count,
empty_slots = 0, -- updated later
class = "ModuleSet"
}
packed_object.Module = nil
packed_object.module_set = module_set
end
function migration.global()
global.alt_actions = nil
end
function migration.player_table(player_table)
player_table.clipboard = nil
player_table.preferences.tutorial_mode = true
end
function migration.subfactory(subfactory)
if subfactory.icon then
local icon_path = subfactory.icon.type .. "/" .. subfactory.icon.name
subfactory.name = "[img=" .. icon_path .. "] " .. subfactory.name
subfactory.icon = nil
end
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_all(floor, "Line")) do
line.effects_tooltip = ""
if not line.subfloor then migrate_modules(line.machine) end
if line.beacon then migrate_modules(line.beacon) end
end
end
end
function migration.packed_subfactory(packed_subfactory)
if packed_subfactory.icon then
local icon_path = packed_subfactory.icon.type .. "/" .. packed_subfactory.icon.name
packed_subfactory.name = "[img=" .. icon_path .. "] " .. packed_subfactory.name
packed_subfactory.icon = nil
end
local function update_lines(floor)
for _, packed_line in ipairs(floor.Line.objects) do
if packed_line.subfloor then
update_lines(packed_line.subfloor)
else
migrate_packed_modules(packed_line.machine)
if packed_line.beacon then migrate_packed_modules(packed_line.beacon) end
end
end
end
update_lines(packed_subfactory.top_floor)
end
return migration

View File

@@ -0,0 +1,19 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
for _, factory_name in pairs({"factory", "archive"}) do
local factory = player_table[factory_name]
local subfactories = Factory.get_in_order(factory, "Subfactory")
if table_size(subfactories) ~= factory.Subfactory.count then
local gui_position = 1
for _, subfactory in pairs(factory.Subfactory.datasets) do
subfactory.gui_position = gui_position
gui_position = gui_position + 1
end
end
end
end
return migration

View File

@@ -0,0 +1,45 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
player_table.ui_state.view_states = player_table.ui_state.view_state
player_table.preferences.toggle_column = false
end
function migration.subfactory(subfactory)
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
if floor.level > 1 then floor.defining_line = floor.Line.datasets[1] end
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
if not line.subfloor then
line.machine.effects_tooltip = ""
for _, module in pairs(line.machine.Module.datasets) do
module.effects_tooltip = ""
end
line.active = true
end
if line.beacon then
line.beacon.effects_tooltip = ""
line.beacon.module.effects_tooltip = "" -- not strictly necessary yet
end
end
end
end
function migration.packed_subfactory(packed_subfactory)
local function update_lines(floor)
for _, packed_line in ipairs(floor.Line.objects) do
if packed_line.subfloor then
update_lines(packed_line.subfloor)
else
packed_line.active = true
end
end
end
update_lines(packed_subfactory.top_floor)
end
return migration

View File

@@ -0,0 +1,18 @@
---@diagnostic disable
local migration = {}
function migration.packed_subfactory(packed_subfactory)
local function update_lines(floor)
for _, packed_line in ipairs(floor.Line.objects) do
if packed_line.subfloor then
update_lines(packed_line.subfloor)
else
packed_line.Product = Collection.pack(Collection.init(), Item)
end
end
end
update_lines(packed_subfactory.top_floor)
end
return migration

View File

@@ -0,0 +1,13 @@
---@diagnostic disable
local migration = {}
function migration.global()
global.tutorial_subfactory = nil
end
function migration.subfactory(subfactory)
subfactory.linearly_dependant = false
end
return migration

View File

@@ -0,0 +1,13 @@
---@diagnostic disable
local migration = {}
function migration.global()
for _, event_data in pairs(global.nth_tick_events) do
if event_data.handler_name == "adjust_interface_dimensions" then
event_data.handler_name = "shrinkwrap_interface"
end
end
end
return migration

View File

@@ -0,0 +1,13 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
subfactory.blueprints = {}
end
function migration.packed_subfactory(packed_subfactory)
packed_subfactory.blueprints = {}
end
return migration

View File

@@ -0,0 +1,16 @@
---@diagnostic disable
local migration = {}
function migration.player_table(player_table)
local Subfactory = player_table.archive.Subfactory
Subfactory.count = table_size(Subfactory.datasets)
local gui_position = 1
for _, subfactory in pairs(Subfactory.datasets) do
subfactory.gui_position = gui_position
gui_position = gui_position + 1
end
end
return migration

View File

@@ -0,0 +1,181 @@
---@diagnostic disable
local migration = {}
function migration.global()
global.tutorial_subfactory_validity = nil
local data_types = {"machines", "recipes", "items", "fuels", "belts", "wagons", "modules", "beacons"}
for _, data_type in pairs(data_types) do global["all_" .. data_type] = nil end
end
function migration.player_table(player_table)
local default_prototypes = player_table.preferences.default_prototypes
default_prototypes["machines"] = default_prototypes["machines"].prototypes
default_prototypes["fuels"] = default_prototypes["fuels"].prototypes
default_prototypes["belts"] = default_prototypes["belts"].prototype
default_prototypes["wagons"] = default_prototypes["wagons"].prototypes
default_prototypes["beacons"] = default_prototypes["beacons"].prototype
end
function migration.subfactory(subfactory)
for _, product in pairs(Subfactory.get_all(subfactory, "Product")) do
if product.proto.simplified then
product.proto = {name=product.proto.name, category=product.proto.type, data_type="items", simplified=true}
else
product.proto.data_type = "items"
end
local belt_proto = product.required_amount.belt_proto
if belt_proto then
if belt_proto.simplified then
product.required_amount.belt_proto = {name=belt_proto.name, data_type="belts", simplified=true}
else
product.required_amount.belt_proto.data_type = "belts"
end
end
end
for index, _ in pairs(subfactory.matrix_free_items or {}) do
local item_proto = subfactory.matrix_free_items[index]
if item_proto.simplified then
subfactory.matrix_free_items[index] =
{name=item_proto.name, category=item_proto.type, data_type="items", simplified=true}
else
item_proto.data_type = "items"
end
end
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_all(floor, "Line")) do
if line.subfloor then goto skip end
local recipe_proto = line.recipe.proto
if recipe_proto.simplified then
line.recipe.proto = {name=recipe_proto.name, data_type="recipes", simplified=true}
else
recipe_proto.data_type = "recipes"
end
local machine_proto = line.machine.proto
if machine_proto.simplified then
line.machine.proto =
{name=machine_proto.name, category=machine_proto.category, data_type="machines", simplified=true}
else
machine_proto.data_type = "machines"
end
local machine_module_set = line.machine.module_set
for _, module in pairs(machine_module_set.modules.datasets) do
if module.proto.simplified then
module.proto = {name=module.proto.name, category=module.proto.category,
data_type="modules", simplified=true}
else
module.proto.data_type = "modules"
end
end
if line.machine.fuel then
local fuel_proto = line.machine.fuel.proto
if fuel_proto.simplified then
line.machine.fuel.proto =
{name=fuel_proto.name, category=fuel_proto.category, data_type="fuels", simplified=true}
else
fuel_proto.data_type = "fuels"
end
end
if line.beacon then
local beacon_proto = line.beacon.proto
if beacon_proto.simplified then
line.beacon.proto = {name=beacon_proto.name, data_type="beacons", simplified=true}
else
beacon_proto.data_type = "beacons"
end
local beacon_module_set = line.beacon.module_set
for _, module in pairs(beacon_module_set.modules.datasets) do
if module.proto.simplified then
module.proto = {name=module.proto.name, category=module.proto.category,
data_type="modules", simplified=true}
else
module.proto.data_type = "modules"
end
end
end
if line.priority_product_proto then
local priority_product_proto = line.priority_product_proto
if priority_product_proto.simplified then
line.priority_product_proto = {name=priority_product_proto.name,
category=priority_product_proto.type, data_type="items", simplified=true}
else
priority_product_proto.data_type = "items"
end
end
for _, product in pairs(line.Product.datasets) do
if product.proto.simplified then
product.proto =
{name=product.proto.name, category=product.proto.type, data_type="items", simplified=true}
else
product.proto.data_type = "items"
end
end
::skip::
end
end
end
function migration.packed_subfactory(packed_subfactory)
for _, product in pairs(packed_subfactory.Product.objects) do
product.proto = {name=product.proto.name, category=product.proto.type, data_type="items", simplified=true}
if product.required_amount.belt_proto then
local belt_proto = product.required_amount.belt_proto
product.required_amount.belt_proto = {name=belt_proto.name, data_type="belts", simplified=true}
end
end
if packed_subfactory.matrix_free_items then
for index, proto in pairs(packed_subfactory.matrix_free_items) do
packed_subfactory.matrix_free_items[index] =
{name=proto.name, category=proto.type, data_type="items", simplified=true}
end
end
local function update_lines(floor)
for _, packed_line in ipairs(floor.Line.objects) do
if packed_line.subfloor then
update_lines(packed_line.subfloor)
else
local recipe_proto = packed_line.recipe.proto
packed_line.recipe.proto = {name=recipe_proto.name, data_type="recipes", simplified=true}
local machine_proto = packed_line.machine.proto
packed_line.machine.proto =
{name=machine_proto.name, category=machine_proto.category, data_type="machines", simplified=true}
local module_set = packed_line.machine.module_set
for _, module in pairs(module_set.modules.objects) do
module.proto =
{name=module.proto.name, category=module.proto.category, data_type="modules", simplified=true}
end
if packed_line.machine.fuel then
local fuel_proto = packed_line.machine.fuel.proto
packed_line.machine.fuel.proto =
{name=fuel_proto.name, category=fuel_proto.category, data_type="fuels", simplified=true}
end
if packed_line.beacon then
local beacon_proto = packed_line.beacon.proto
packed_line.beacon.proto = {name=beacon_proto.name, data_type="beacons", simplified=true}
local module_set = packed_line.beacon.module_set
for _, module in pairs(module_set.modules.objects) do
module.proto = {name=module.proto.name, category=module.proto.category,
data_type="modules", simplified=true}
end
end
if packed_line.priority_product_proto then
local priority_product_proto = packed_line.priority_product_proto
packed_line.priority_product_proto = {name=priority_product_proto.name,
category=priority_product_proto.type, data_type="items", simplified=true}
end
for _, product in pairs(packed_line.Product.objects) do
product.proto =
{name=product.proto.name, category=product.proto.type, data_type="items", simplified=true}
end
end
end
end
update_lines(packed_subfactory.top_floor)
end
return migration

View File

@@ -0,0 +1,14 @@
---@diagnostic disable
local migration = {}
function migration.subfactory(subfactory)
-- Revert all the crap I did with the previous version
for _, floor in pairs(Subfactory.get_all_floors(subfactory)) do
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
if line.subfloor then line.machine = nil end
end
end
end
return migration