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