586 lines
20 KiB
Lua

local flib_bounding_box = require("__flib__/bounding-box")
local flib_math = require("__flib__/math")
local flib_table = require("__flib__/table")
--- @alias RateCategory
--- | "output"
--- | "input"
--- @class ResourceData
--- @field occurrences uint
--- @field products Product[]
--- @field required_fluid Product?
--- @field mining_time double
--- @alias Timescale
--- | "per-second",
--- | "per-minute",
--- | "per-hour",
--- | "transport-belts",
--- | "inserters",
--- @class CalcUtil
local calc_util = {}
--- @param set CalculationSet
--- @param error CalculationError
function calc_util.add_error(set, error)
set.errors[error] = true
end
--- @param set CalculationSet
--- @param category RateCategory
--- @param type string
--- @param name string
--- @param amount double
--- @param invert boolean
--- @param machine_name string?
--- @param temperature double?
function calc_util.add_rate(set, category, type, name, amount, invert, machine_name, temperature)
local set_rates = set.rates
local path = type .. "/" .. name .. (temperature or "")
local rates = set_rates[path]
if not rates then
if invert then
return -- Don't remove from rates that don't exist.
end
--- @type Rates
rates = {
type = type,
name = name,
temperature = temperature,
output = { machines = 0, machine_counts = {}, rate = 0 },
input = { machines = 0, machine_counts = {}, rate = 0 },
}
set_rates[path] = rates
end
if invert then
amount = -amount
end
--- @type Rate
local rate = rates[category]
if machine_name then
local counts = rate.machine_counts
-- Don't remove a machine that doesn't exist
if not counts[machine_name] and invert then
goto no_rate
end
counts[machine_name] = (counts[machine_name] or 0) + (invert and -1 or 1)
if counts[machine_name] == 0 then
counts[machine_name] = nil
end
end
rate.rate = math.max(rate.rate + amount, 0)
rate.machines = rate.machines + (invert and -1 or 1)
-- Account for floating-point imprecision
if rate.rate < 0.00001 then
rate.rate = 0
end
::no_rate::
if rates.input.machines == 0 and rates.output.machines == 0 then
set_rates[path] = nil
end
end
--- Source: https://github.com/ClaudeMetz/FactoryPlanner/blob/0f0aeae03386f78290d932cf51130bbcb2afa83d/modfiles/data/handlers/generator_util.lua#L364
--- @param prototype LuaEntityPrototype
--- @return number?
function calc_util.get_seconds_per_rocket_launch(prototype)
local rocket_prototype = prototype.rocket_entity_prototype
if not rocket_prototype then
return nil
end
local rocket_flight_threshold = 0.5 -- hardcoded in the game files
local launch_steps = {
lights_blinking_open = (1 / prototype.light_blinking_speed) + 1,
doors_opening = (1 / prototype.door_opening_speed) + 1,
doors_opened = prototype.rocket_rising_delay + 1,
rocket_rising = (1 / rocket_prototype.rising_speed) + 1,
rocket_ready = 14, -- estimate for satellite insertion delay
launch_started = prototype.launch_wait_time + 1,
engine_starting = (1 / rocket_prototype.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_prototype.flying_acceleration / rocket_prototype.flying_speed
) / math.log(1 + rocket_prototype.flying_acceleration),
lights_blinking_close = (1 / prototype.light_blinking_speed) + 1,
doors_closing = (1 / prototype.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
--- @param entity LuaEntity
--- @param crafts_per_second double
--- @return double
function calc_util.get_rocket_adjusted_crafts_per_second(entity, crafts_per_second)
local prototype = entity.prototype
local seconds_per_launch = calc_util.get_seconds_per_rocket_launch(prototype)
local normal_crafts = prototype.rocket_parts_required
local missed_crafts = seconds_per_launch * crafts_per_second * (entity.productivity_bonus + 1)
local ratio = normal_crafts / (normal_crafts + missed_crafts)
return crafts_per_second * ratio
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
--- @param emissions_per_second double
--- @return double
function calc_util.process_burner(set, entity, invert, emissions_per_second)
local entity_prototype = entity.prototype
local burner_prototype = entity_prototype.burner_prototype --[[@as LuaBurnerPrototype]]
local burner = entity.burner --[[@as LuaBurner]]
local currently_burning = burner.currently_burning
if not currently_burning then
local item_name = next(burner.inventory.get_contents())
if item_name then
currently_burning = game.item_prototypes[item_name]
end
end
if not currently_burning then
calc_util.add_error(set, "no-fuel")
return emissions_per_second
end
local max_energy_usage = entity_prototype.max_energy_usage * (entity.consumption_bonus + 1)
local burns_per_second = 1 / (currently_burning.fuel_value / (max_energy_usage / burner_prototype.effectivity) / 60)
calc_util.add_rate(set, "input", "item", currently_burning.name, burns_per_second, invert, entity.name)
local burnt_result = currently_burning.burnt_result
if burnt_result then
calc_util.add_rate(set, "output", "item", burnt_result.name, burns_per_second, invert, entity.name)
end
local emissions = burner_prototype.emissions * 60 * max_energy_usage * currently_burning.fuel_emissions_multiplier
return emissions_per_second + emissions
end
--- @param fluidbox LuaFluidBox
--- @param index uint
--- @return LuaFluidPrototype?
local function get_fluid(fluidbox, index)
local fluid = fluidbox.get_filter(index)
if not fluid then
fluid = fluidbox[index] --[[@as FluidBoxFilter?]]
end
if fluid then
return game.fluid_prototypes[fluid.name]
end
end
--- @param set CalculationSet
--- @param entity LuaEntity
function calc_util.process_beacon(set, entity)
if entity.status == defines.entity_status.no_power then
calc_util.add_error(set, "no-power")
end
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
function calc_util.process_boiler(set, entity, invert)
local entity_prototype = entity.prototype
local fluidbox = entity.fluidbox
local input_fluid = get_fluid(fluidbox, 1)
if not input_fluid then
calc_util.add_error(set, "no-input-fluid")
return
end
local minimum_temperature = fluidbox.get_prototype(1).minimum_temperature or input_fluid.default_temperature
local energy_per_amount = (entity_prototype.target_temperature - minimum_temperature) * input_fluid.heat_capacity
local fluid_usage = entity_prototype.max_energy_usage / energy_per_amount * 60
calc_util.add_rate(set, "input", "fluid", input_fluid.name, fluid_usage, invert, entity.name)
local output_fluid = get_fluid(fluidbox, 2)
if not output_fluid then
return
end
calc_util.add_rate(set, "output", "fluid", output_fluid.name, fluid_usage, invert, entity.name)
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
--- @return double
function calc_util.process_crafter(set, entity, invert, emissions_per_second)
local recipe = entity.get_recipe()
if not recipe and entity.type == "furnace" then
recipe = entity.previous_recipe
end
if not recipe then
calc_util.add_error(set, "no-recipe")
return emissions_per_second
end
local crafts_per_second = entity.crafting_speed / recipe.energy
-- The game engine has a hard limit of one craft per tick, or 60 crafts per second
if crafts_per_second > 60 then
crafts_per_second = 60
calc_util.add_error(set, "max-crafting-speed")
end
-- Rocket silos will lose time to the launch animation
if entity.type == "rocket-silo" then
crafts_per_second = calc_util.get_rocket_adjusted_crafts_per_second(entity, crafts_per_second)
end
for _, ingredient in pairs(recipe.ingredients) do
local amount = ingredient.amount * crafts_per_second
calc_util.add_rate(set, "input", ingredient.type, ingredient.name, amount, invert, entity.name)
end
local productivity = entity.productivity_bonus + 1
for _, product in pairs(recipe.products) do
local adjusted_crafts_per_second = crafts_per_second * (product.probability or 1)
-- Take the average amount if there is a min and max
local amount = product.amount or (product.amount_max - ((product.amount_max - product.amount_min) / 2))
local catalyst_amount = product.catalyst_amount or 0
-- Catalysts are not affected by productivity
local amount = (catalyst_amount + ((amount - catalyst_amount) * productivity)) * adjusted_crafts_per_second
calc_util.add_rate(set, "output", product.type, product.name, amount, invert, entity.name, product.temperature)
end
return emissions_per_second * recipe.prototype.emissions_multiplier * (1 + entity.pollution_bonus)
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
--- @param emissions_per_second double
--- @return double
function calc_util.process_electric_energy_source(set, entity, invert, emissions_per_second)
local entity_prototype = entity.prototype
-- Electric energy interfaces can have their settings adjusted at runtime, so checking the energy source is pointless.
if entity.type == "electric-energy-interface" then
local production = entity.power_production * 60
if production > 0 then
calc_util.add_rate(set, "output", "item", "rcalc-power-dummy", production, invert, entity.name)
end
local usage = entity.power_usage * 60
if usage > 0 then
calc_util.add_rate(set, "input", "item", "rcalc-power-dummy", usage, invert, entity.name)
end
return emissions_per_second
end
local electric_energy_source_prototype = entity_prototype.electric_energy_source_prototype --[[@as LuaElectricEnergySourcePrototype]]
local added_emissions = 0
local max_energy_usage = entity_prototype.max_energy_usage or 0
if max_energy_usage > 0 and max_energy_usage < flib_math.max_int53 then
local consumption_bonus = (entity.consumption_bonus + 1)
local drain = electric_energy_source_prototype.drain
local amount = max_energy_usage * consumption_bonus
if max_energy_usage ~= drain then
amount = amount + drain
end
calc_util.add_rate(set, "input", "item", "rcalc-power-dummy", amount * 60, invert, entity.name)
if entity.status == defines.entity_status.no_power then
calc_util.add_error(set, "no-power")
end
added_emissions = electric_energy_source_prototype.emissions * (max_energy_usage * consumption_bonus) * 60
end
local max_energy_production = entity_prototype.max_energy_production
if max_energy_production > 0 and max_energy_production < flib_math.max_int53 then
if entity.type == "solar-panel" then
max_energy_production = max_energy_production * entity.surface.solar_power_multiplier
end
calc_util.add_rate(set, "output", "item", "rcalc-power-dummy", max_energy_production * 60, invert, entity.name)
end
return emissions_per_second + added_emissions
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
--- @param emissions_per_second double
--- @return double
function calc_util.process_fluid_energy_source(set, entity, invert, emissions_per_second)
--- @type LuaEntityPrototype
local entity_prototype = entity.prototype
local fluid_energy_source_prototype = entity_prototype.fluid_energy_source_prototype --[[@as LuaFluidEnergySourcePrototype]]
local max_fluid_usage = fluid_energy_source_prototype.fluid_usage_per_tick
local fluidbox = entity.fluidbox
-- The fluid energy source fluidbox will always be the last one
local fluid_prototype = get_fluid(fluidbox, #fluidbox)
if not fluid_prototype then
calc_util.add_error(set, "no-input-fluid")
return emissions_per_second
end
local max_energy_usage = entity_prototype.max_energy_usage * (entity.consumption_bonus + 1)
local value
if fluid_energy_source_prototype.scale_fluid_usage then
if fluid_energy_source_prototype.burns_fluid and fluid_prototype.fuel_value > 0 then
local fluid_usage_now = max_energy_usage
/ (fluid_prototype.fuel_value / 60)
/ fluid_energy_source_prototype.effectivity
if max_fluid_usage > 0 then
value = math.min(fluid_usage_now, max_fluid_usage)
else
value = fluid_usage_now
end
else
-- Now we need the actual fluid to get its temperature
local fluid = fluidbox[#fluidbox]
if not fluid then
calc_util.add_error(set, "no-input-fluid")
return emissions_per_second
end
-- If the fluid is equal to its default temperature, then nothing will happen
local temperature_value = fluid.temperature - fluid_prototype.default_temperature
if temperature_value > 0 then
value = max_energy_usage / (temperature_value * fluid_prototype.heat_capacity) * 60
end
end
else
value = max_fluid_usage * 60
end
if not value then
return emissions_per_second -- No error, but not rate either
end
calc_util.add_rate(set, "input", "fluid", fluid_prototype.name, value, invert, entity.name)
return fluid_energy_source_prototype.emissions * max_energy_usage * 60
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
function calc_util.process_generator(set, entity, invert)
local entity_prototype = entity.prototype
local fluid = get_fluid(entity.fluidbox, 1)
if not fluid then
calc_util.add_error(set, "no-input-fluid")
return
end
calc_util.add_rate(set, "input", "fluid", fluid.name, entity_prototype.fluid_usage_per_tick * 60, invert, entity.name)
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
function calc_util.process_heat_energy_source(set, entity, invert)
calc_util.add_rate(
set,
"input",
"item",
"rcalc-heat-dummy",
entity.prototype.max_energy_usage * (1 + entity.consumption_bonus) * 60,
invert,
entity.name
)
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
function calc_util.process_lab(set, entity, invert)
local research_data = set.research_data
if not research_data then
calc_util.add_error(set, "no-active-research")
return
end
local research_multiplier = research_data.multiplier
local researching_speed = entity.prototype.researching_speed
local speed_modifier = research_data.speed_modifier
-- XXX: Due to a bug with entity_speed_bonus, we must subtract the force's lab speed bonus and convert it to a
-- multiplicative relationship
local lab_multiplier = research_multiplier
* ((entity.speed_bonus + 1 - speed_modifier) * (speed_modifier + 1))
* researching_speed
local inputs = flib_table.invert(entity.prototype.lab_inputs)
for _, ingredient in pairs(research_data.ingredients) do
if not inputs[ingredient.name] then
calc_util.add_error(set, "incompatible-science-packs")
return
end
end
for _, ingredient in ipairs(research_data.ingredients) do
local amount = ((ingredient.amount * lab_multiplier) / game.item_prototypes[ingredient.name].durability)
calc_util.add_rate(set, "input", "item", ingredient.name, amount, invert, entity.name)
end
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
function calc_util.process_mining_drill(set, entity, invert)
local entity_prototype = entity.prototype
local entity_productivity_bonus = entity.productivity_bonus
local entity_speed_bonus = entity.speed_bonus
-- Look for resource entities under the drill
local radius = entity_prototype.mining_drill_radius + 0.01
local box = flib_bounding_box.from_dimensions(entity.position, radius * 2, radius * 2)
local resource_entities = entity.surface.find_entities_filtered({ area = box })
local resource_entities_len = #resource_entities
if resource_entities_len == 0 then
calc_util.add_error(set, "no-mineable-resources")
return
end
--- @type table<string, ResourceData>
local resources = {}
local num_resource_entities = 0
local has_fluidbox = next(entity_prototype.fluidbox_prototypes) and true or false
local resource_categories = entity_prototype.resource_categories or {}
for i = 1, resource_entities_len do
local resource = resource_entities[i]
local resource_name = resource.name
-- If this resource has already been processed
local resource_data = resources[resource_name]
if resource_data then
resource_data.occurrences = resource_data.occurrences + 1
num_resource_entities = num_resource_entities + 1
goto continue
end
local resource_prototype = resource.prototype
if not resource_categories[resource_prototype.resource_category] then
goto continue
end
num_resource_entities = num_resource_entities + 1
local mineable_properties = resource_prototype.mineable_properties
local required_fluid = mineable_properties.required_fluid
if required_fluid and not has_fluidbox then
goto continue
end
resource_data = {
occurrences = 1,
products = mineable_properties.products,
mining_time = mineable_properties.mining_time,
}
if resource_prototype.infinite_resource then
resource_data.mining_time = resource_data.mining_time
/ (resource.amount / resource_prototype.normal_resource_amount)
end
if required_fluid then
resource_data.required_fluid = {
name = required_fluid,
amount = mineable_properties.fluid_amount / 10, -- Ten mining operations per fluid consumed
}
end
resources[resource_name] = resource_data
::continue::
end
if num_resource_entities == 0 then
calc_util.add_error(set, "no-mineable-resources")
return
end
-- Process resource entities
local adjusted_mining_speed = entity_prototype.mining_speed
* (entity_speed_bonus + 1)
* (entity_productivity_bonus + 1)
for _, resource_data in pairs(resources) do
local resource_multiplier = (adjusted_mining_speed / resource_data.mining_time)
* (resource_data.occurrences / num_resource_entities)
-- Add required fluid to inputs
local required_fluid = resource_data.required_fluid
if required_fluid then
-- Productivity does not apply to ingredients
local fluid_per_second = required_fluid.amount * resource_multiplier / (entity_productivity_bonus + 1)
-- Add to inputs table
local fluid_name = required_fluid.name
calc_util.add_rate(set, "input", "fluid", fluid_name, fluid_per_second, invert, entity.name)
end
-- Iterate each product
for _, product in pairs(resource_data.products or {}) do
-- Get rate per second for this product on this drill
local product_per_second
if product.amount then
product_per_second = product.amount * resource_multiplier
else
product_per_second = product.amount_max - (product.amount_max - product.amount_min) / 2 * resource_multiplier
end
-- Account for probability
local adjusted_product_per_second = product_per_second * (product.probability or 1)
-- Add to outputs table
calc_util.add_rate(
set,
"output",
product.type,
product.name,
adjusted_product_per_second,
invert,
entity.name,
product.temperature
)
end
end
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
function calc_util.process_offshore_pump(set, entity, invert)
local entity_prototype = entity.prototype
local fluid_prototype = entity_prototype.fluid --[[@as LuaFluidPrototype]]
calc_util.add_rate(
set,
"output",
"fluid",
fluid_prototype.name,
entity_prototype.pumping_speed * 60,
invert,
entity.name
)
end
--- @param set CalculationSet
--- @param entity LuaEntity
--- @param invert boolean
function calc_util.process_reactor(set, entity, invert)
calc_util.add_rate(
set,
"output",
"item",
"rcalc-heat-dummy",
entity.prototype.max_energy_usage * (1 + entity.neighbour_bonus) * (1 + entity.consumption_bonus) * 60,
invert,
entity.name
)
end
return calc_util