361 lines
16 KiB
Lua
361 lines
16 KiB
Lua
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
|