771 lines
32 KiB
Lua

require "math.Matrix"
require "math.SolverMatrix"
require "math.SolverMatrixAlgebra"
require "math.SolverMatrixSimplex"
require "math.Solver"
require "math.SolverAlgebra"
require "math.SolverSimplex"
------------------------------------------------------------------------------
---Description of the module.
---@class ModelCompute
local ModelCompute = {
classname = "HMModelCompute",
capEnergy = -0.8,
capSpeed = -0.8,
capPollution = -0.8,
waste_value = 0.00001,
new_solver = false,
cap_reason = {
speed = {
cycle = 1,
module_low = 2,
module_high = 4
},
productivity = {
module_low = 1
},
consumption = {
module_low = 1
},
pollution = {
module_low = 1
}
}
}
-------------------------------------------------------------------------------
---Check and valid unlinked all blocks
---@param model table
function ModelCompute.checkUnlinkedBlocks(model)
if model.blocks ~= nil then
for _, block in spairs(model.blocks, function(t, a, b) return t[b].index > t[a].index end) do
ModelCompute.checkUnlinkedBlock(model, block)
end
end
end
-------------------------------------------------------------------------------
---Check and valid unlinked block
---@param model table
---@param block table
function ModelCompute.checkUnlinkedBlock(model, block)
local unlinked = true
local recipe = Player.getPlayerRecipe(block.name)
if recipe ~= nil then
if model.blocks ~= nil then
for _, current_block in spairs(model.blocks, function(t, a, b) return t[b].index > t[a].index end) do
if current_block.id == block.id then
break
end
for _, ingredient in pairs(current_block.ingredients) do
for _, product in pairs(recipe.products) do
if product.name == ingredient.name then
unlinked = false
end
end
end
if current_block.id ~= block.id and current_block.name == block.name then
unlinked = true
end
end
end
block.unlinked = unlinked
else
---not a recipe
block.unlinked = true
end
end
-------------------------------------------------------------------------------
---Update model
---@param model table
function ModelCompute.try_update(model)
local ok , err = pcall(function()
ModelCompute.update(model)
end)
if not(ok) then
log(err)
end
end
-------------------------------------------------------------------------------
---Update model
---@param model table
function ModelCompute.update(model)
if model ~= nil and model.blocks ~= nil then
Model.appendParameters(model)
---calcul les blocks
local input = {}
for _, block in spairs(model.blocks, function(t, a, b) return t[b].index > t[a].index end) do
block.time = model.time
---premiere recette
local _, recipe = next(block.recipes)
if recipe == nil then
block.ingredients = {}
block.products = {}
else
---prepare bloc
ModelCompute.prepareBlock(block)
---state = 0 => produit
---state = 1 => produit pilotant
---state = 2 => produit restant
---prepare input
if not (block.unlinked) then
if block.products == nil then
ModelCompute.computeBlock(block)
end
---prepare les inputs
local factor = -1
local block_elements = block.products
if block.by_product == false then
block_elements = block.ingredients
factor = 1
end
if block_elements ~= nil then
for _, element in pairs(block_elements) do
local element_key = Product(element):getTableKey()
if (element.state ~= nil and element.state == 1) or (block.products_linked ~= nil and block.products_linked[element_key] == true) then
if input[element_key] ~= nil then
element.input = (input[element_key] or 0) * factor
--element.state = 0
end
else
element.input = 0
end
end
end
end
ModelCompute.computeBlockCleanInput(block)
ModelCompute.computeBlock(block, model.parameters)
---consomme les ingredients
for _, product in pairs(block.products) do
local element_key = Product(product):getTableKey()
if input[element_key] == nil then
input[element_key] = product.count
elseif input[element_key] ~= nil then
input[element_key] = input[element_key] + product.count
end
end
---compte les ingredients
for _, ingredient in pairs(block.ingredients) do
local element_key = Product(ingredient):getTableKey()
if input[element_key] == nil then
input[element_key] = -ingredient.count
else
input[element_key] = input[element_key] - ingredient.count
end
end
---consume energy
local element_key = "energy"
if input[element_key] == nil then
input[element_key] = -block.power
else
input[element_key] = input[element_key] - block.power
end
end
end
ModelCompute.computeInputOutput(model)
ModelCompute.computeResources(model)
---genere un bilan
ModelCompute.createSummary(model)
model.version = Model.version
end
end
-------------------------------------------------------------------------------
---Compute production block
---@param block table
function ModelCompute.computeBlockCleanInput(block)
local recipes = block.recipes
if recipes ~= nil then
if block.input ~= nil then
---state = 0 => produit
---state = 1 => produit pilotant
---state = 2 => produit restant
for product_name, quantity in pairs(block.input) do
if block.products[product_name] == nil or not (bit32.band(block.products[product_name].state, 1)) then
block.input[product_name] = nil
end
end
end
end
end
-------------------------------------------------------------------------------
---Prepare production block
---@param block table
function ModelCompute.prepareBlock(block)
local recipes = block.recipes
if recipes ~= nil then
local block_products = {}
local block_ingredients = {}
---preparation
for _, recipe in spairs(recipes, function(t, a, b) return t[b].index > t[a].index end) do
local recipe_prototype = RecipePrototype(recipe)
for i, lua_product in pairs(recipe_prototype:getProducts(recipe.factory)) do
local product_key = Product(lua_product):getTableKey()
block_products[product_key] = {
name = lua_product.name,
type = lua_product.type,
count = 0,
temperature = lua_product.temperature,
minimum_temperature = lua_product.minimum_temperature,
maximum_temperature = lua_product.maximum_temperature
}
end
for i, lua_ingredient in pairs(recipe_prototype:getIngredients(recipe.factory)) do
local ingredient_key = Product(lua_ingredient):getTableKey()
block_ingredients[ingredient_key] = {
name = lua_ingredient.name,
type = lua_ingredient.type,
count = 0,
temperature = lua_ingredient.temperature,
minimum_temperature = lua_ingredient.minimum_temperature,
maximum_temperature = lua_ingredient.maximum_temperature
}
end
end
---preparation state
---state = 0 => produit
---state = 1 => produit pilotant
---state = 2 => produit restant
for i, block_product in pairs(block_products) do
local product_key = Product(block_product):getTableKey()
---recopie la valeur input
if block.by_factory ~= true and block.products ~= nil and block.products[product_key] ~= nil then
block_product.input = block.products[product_key].input
end
---pose le status
if block_ingredients[product_key] == nil then
block_product.state = 1
else
block_product.state = 0
end
end
for i, block_ingredient in pairs(block_ingredients) do
local ingredient_key = Product(block_ingredient):getTableKey()
---recopie la valeur input
if block.by_factory ~= true and block.ingredients ~= nil and block.ingredients[ingredient_key] ~= nil then
block_ingredient.input = block.ingredients[ingredient_key].input
end
---pose le status
if block_products[ingredient_key] == nil then
block_ingredient.state = 1
else
block_ingredient.state = 0
end
end
block.products = block_products
block.ingredients = block_ingredients
end
end
-------------------------------------------------------------------------------
---Compute production block
---@param block table
function ModelCompute.computeBlock(block, parameters)
local recipes = block.recipes
block.power = 0
block.count = 1
block.pollution_total = 0
if recipes ~= nil then
local my_solver
local debug = User.getModGlobalSetting("debug_solver")
local selected_solvers = { algebra = SolverAlgebra, simplex = SolverSimplex }
local solver_selected = User.getParameter("solver_selected") or defines.constant.default_solver
if solver_selected ~= defines.constant.solvers.normal then
selected_solvers = { algebra = SolverMatrixAlgebra, simplex = SolverMatrixSimplex }
end
if block.solver == true and block.by_factory ~= true then
my_solver = selected_solvers.simplex()
else
my_solver = selected_solvers.algebra()
end
my_solver:solve(block, parameters, debug)
end
end
--------------------------------------------------------------------------------
---Compute module effects of factory
---@param recipe table
---@param parameters ParametersData
---@return table
function ModelCompute.computeModuleEffects(recipe, parameters)
local factory = recipe.factory
factory.effects = { speed = 0, productivity = 0, consumption = 0, pollution = 0 }
if parameters ~= nil then
factory.effects.speed = parameters.effects.speed
factory.effects.productivity = parameters.effects.productivity
factory.effects.consumption = parameters.effects.consumption
factory.effects.pollution = parameters.effects.pollution
end
factory.cap = { speed = 0, productivity = 0, consumption = 0, pollution = 0 }
local factory_prototype = EntityPrototype(factory)
factory.effects.productivity = factory.effects.productivity + factory_prototype:getBaseProductivity()
---effet module factory
if factory.modules ~= nil then
for module, value in pairs(factory.modules) do
local speed_bonus = Player.getModuleBonus(module, "speed")
local productivity_bonus = Player.getModuleBonus(module, "productivity")
local consumption_bonus = Player.getModuleBonus(module, "consumption")
local pollution_bonus = Player.getModuleBonus(module, "pollution")
factory.effects.speed = factory.effects.speed + value * speed_bonus
factory.effects.productivity = factory.effects.productivity + value * productivity_bonus
factory.effects.consumption = factory.effects.consumption + value * consumption_bonus
factory.effects.pollution = factory.effects.pollution + value * pollution_bonus
end
end
---effet module beacon
if recipe.beacons ~= nil then
for _, beacon in pairs(recipe.beacons) do
if beacon.modules ~= nil then
for module, value in pairs(beacon.modules) do
local speed_bonus = Player.getModuleBonus(module, "speed")
local productivity_bonus = Player.getModuleBonus(module, "productivity")
local consumption_bonus = Player.getModuleBonus(module, "consumption")
local pollution_bonus = Player.getModuleBonus(module, "pollution")
local distribution_effectivity = EntityPrototype(beacon):getDistributionEffectivity()
factory.effects.speed = factory.effects.speed + value * speed_bonus * distribution_effectivity * beacon
.combo
factory.effects.productivity = factory.effects.productivity +
value * productivity_bonus * distribution_effectivity * beacon.combo
factory.effects.consumption = factory.effects.consumption +
value * consumption_bonus * distribution_effectivity * beacon.combo
factory.effects.pollution = factory.effects.pollution +
value * pollution_bonus * distribution_effectivity * beacon.combo
end
end
end
end
if recipe.type == "resource" then
local bonus = Player.getForce().mining_drill_productivity_bonus
factory.effects.productivity = factory.effects.productivity + bonus
end
if recipe.type == "technology" then
local bonus = Player.getForce().laboratory_speed_modifier or 0
factory.effects.speed = factory.effects.speed + bonus * (1 + factory.effects.speed)
end
---nuclear reactor
if factory_prototype:getType() == "reactor" then
local bonus = factory_prototype:getNeighbourBonus()
factory.effects.consumption = factory.effects.consumption + bonus
end
---cap la productivite
if factory.effects.productivity < 0 then
factory.effects.productivity = 0
factory.cap.productivity = bit32.bor(factory.cap.productivity, ModelCompute.cap_reason.productivity.module_low)
end
---cap la vitesse a self.capSpeed
if factory.effects.speed < ModelCompute.capSpeed then
factory.effects.speed = ModelCompute.capSpeed
factory.cap.speed = bit32.bor(factory.cap.speed, ModelCompute.cap_reason.speed.module_low)
end
---cap short integer max for %
---@see https://fr.wikipedia.org/wiki/Entier_court
if factory.effects.speed * 100 > 32767 then
factory.effects.speed = 32767 / 100
factory.cap.speed = bit32.bor(factory.cap.speed, ModelCompute.cap_reason.speed.module_high)
end
---cap l'energy a self.capEnergy
if factory.effects.consumption < ModelCompute.capEnergy then
factory.effects.consumption = ModelCompute.capEnergy
factory.cap.consumption = bit32.bor(factory.cap.consumption, ModelCompute.cap_reason.consumption.module_low)
end
---cap la pollution a self.capPollution
if factory.effects.pollution < ModelCompute.capPollution then
factory.effects.pollution = ModelCompute.capPollution
factory.cap.pollution = bit32.bor(factory.cap.pollution, ModelCompute.cap_reason.pollution.module_low)
end
return recipe
end
-------------------------------------------------------------------------------
---Compute energy, speed, number of factory for recipes
---@param recipe table
function ModelCompute.computeFactory(recipe)
local recipe_prototype = RecipePrototype(recipe)
local factory_prototype = EntityPrototype(recipe.factory)
recipe.time = recipe_prototype:getEnergy(recipe.factory)
---effet speed
recipe.factory.speed_total = factory_prototype:speedFactory(recipe) * (1 + recipe.factory.effects.speed)
if recipe.type == "rocket" then
local speed_penalty = recipe_prototype:getRocketPenalty(recipe.factory)
recipe.factory.speed_total = recipe.factory.speed_total * speed_penalty
end
recipe.factory.speed = recipe.factory.speed_total
---cap speed creation maximum de 1 cycle par tick
---seulement sur les recipes normaux
if recipe.type == "recipe" and recipe.time / recipe.factory.speed < 1 / 60 then
recipe.factory.speed = 60 * recipe.time
recipe.factory.cap.speed = bit32.bor(recipe.factory.cap.speed, ModelCompute.cap_reason.speed.cycle)
end
---effet consumption
local energy_type = factory_prototype:getEnergyType()
recipe.factory.energy = factory_prototype:getEnergyConsumption() * (1 + recipe.factory.effects.consumption)
---effet pollution
recipe.factory.pollution = factory_prototype:getPollution() * (1 + recipe.factory.effects.pollution) *
(1 + recipe.factory.effects.consumption)
---compte le nombre de machines necessaires
---[ratio recipe] * [effort necessaire du recipe] / ([la vitesse de la factory] * [le temps en second])
local count = recipe.count * recipe.time / (recipe.factory.speed * recipe.base_time)
if recipe.factory.speed == 0 then count = 0 end
recipe.factory.count = count
if energy_type ~= "electric" then
recipe.factory.energy_total = 0
else
recipe.factory.energy_total = recipe.factory.count * recipe.factory.energy
local drain = factory_prototype:getMinEnergyUsage()
recipe.factory.energy_total = math.ceil(recipe.factory.energy_total + (math.ceil(recipe.factory.count) * drain))
recipe.factory.energy = recipe.factory.energy + drain
end
---arrondi des valeurs
recipe.factory.speed = recipe.factory.speed
recipe.factory.energy = math.ceil(recipe.factory.energy)
local beacons_energy_total = 0
if recipe.beacons ~= nil then
for _, beacon in pairs(recipe.beacons) do
if Model.countModulesModel(beacon) > 0 then
local variant = beacon.per_factory or 0
local constant = beacon.per_factory_constant or 0
beacon.count = count * variant + constant
else
beacon.count = 0
end
local beacon_prototype = EntityPrototype(beacon)
beacon.energy = beacon_prototype:getEnergyUsage()
beacon.energy_total = math.ceil(beacon.count * beacon.energy)
beacon.energy = math.ceil(beacon.energy)
beacons_energy_total = beacons_energy_total + beacon.energy_total
end
end
--- totaux
recipe.factory.pollution_total = recipe.factory.pollution * recipe.factory.count * recipe.base_time
recipe.pollution_total = recipe.factory.pollution_total * recipe_prototype:getEmissionsMultiplier()
recipe.energy_total = recipe.factory.energy_total + beacons_energy_total
end
-------------------------------------------------------------------------------
---Compute energy factory for recipes
---@param recipe table
function ModelCompute.computeEnergyFactory(recipe)
local recipe_prototype = RecipePrototype(recipe)
local factory_prototype = EntityPrototype(recipe.factory)
local recipe_energy = recipe_prototype:getEnergy(recipe.factory)
---effet speed
recipe.factory.speed = factory_prototype:speedFactory(recipe) * (1 + recipe.factory.effects.speed)
---cap speed creation maximum de 1 cycle par tick
---seulement sur les recipes normaux
if recipe.type == "recipe" and recipe_energy / recipe.factory.speed < 1 / 60 then
recipe.factory.speed = 60 * recipe_energy
end
---effet consumption
local energy_prototype = factory_prototype:getEnergySource()
local energy_type = factory_prototype:getEnergyType()
local gameDay = { day = 12500, dusk = 5000, night = 2500, dawn = 2500 }
if factory_prototype:getType() == "accumulator" then
local dark_time = (gameDay.dusk / 2 + gameDay.night + gameDay.dawn / 2)
--recipe_energy = dark_time
end
recipe.factory.energy = factory_prototype:getEnergyConsumption() * (1 + recipe.factory.effects.consumption)
---effet pollution
recipe.factory.pollution = factory_prototype:getPollution() * (1 + recipe.factory.effects.pollution)
---compte le nombre de machines necessaires
---[ratio recipe] * [effort necessaire du recipe] / ([la vitesse de la factory]
local count = recipe.count * recipe_energy / (recipe.factory.speed * recipe.base_time)
if recipe.factory.speed == 0 then count = 0 end
recipe.factory.count = count
---calcul des totaux
if energy_type == "electric" then
recipe.factory.energy_total = 0
else
recipe.factory.energy_total = 0
end
recipe.factory.pollution_total = recipe.factory.pollution * recipe.factory.count * recipe.base_time
recipe.energy_total = recipe.factory.energy_total
recipe.pollution_total = recipe.factory.pollution_total * recipe_prototype:getEmissionsMultiplier()
---arrondi des valeurs
recipe.factory.speed = recipe.factory.speed
recipe.factory.energy = math.ceil(recipe.factory.energy)
if recipe.beacons then
for _, beacon in pairs(recipe.beacons) do
beacon.energy_total = 0
beacon.energy = 0
end
end
recipe.time = 1
end
-------------------------------------------------------------------------------
---Compute input and output
---@param model table
function ModelCompute.computeInputOutput(model)
model.products = {}
model.ingredients = {}
local index = 1
for _, element in spairs(model.blocks, function(t, a, b) return t[b].index > t[a].index end) do
---count product
if element.products ~= nil and table.size(element.products) then
for key, product in pairs(element.products) do
if model.products[key] == nil then
model.products[key] = Model.newIngredient(product.name, product.type)
model.products[key].temperature = product.temperature
model.products[key].minimum_temperature = product.minimum_temperature
model.products[key].maximum_temperature = product.maximum_temperature
model.products[key].index = index
index = index + 1
end
model.products[key].count = model.products[key].count + product.count
end
end
---count ingredient
if element.ingredients ~= nil and table.size(element.ingredients) then
for key, ingredient in pairs(element.ingredients) do
if model.ingredients[key] == nil then
model.ingredients[key] = Model.newIngredient(ingredient.name, ingredient.type)
model.ingredients[key].temperature = ingredient.temperature
model.ingredients[key].minimum_temperature = ingredient.minimum_temperature
model.ingredients[key].maximum_temperature = ingredient.maximum_temperature
model.ingredients[key].index = index
index = index + 1
end
model.ingredients[key].count = model.ingredients[key].count + ingredient.count
end
end
end
for _, element in spairs(model.blocks, function(t, a, b) return t[b].index > t[a].index end) do
---consomme les produits
if element.ingredients ~= nil and table.size(element.ingredients) then
for key, ingredient in pairs(element.ingredients) do
if element.mining_ingredient ~= ingredient.name then
if model.products[key] ~= nil then
model.products[key].count = model.products[key].count - ingredient.count
end
end
end
end
---consomme les ingredients
if element.products ~= nil and table.size(element.products) then
for key, product in pairs(element.products) do
if model.ingredients[key] ~= nil then
model.ingredients[key].count = model.ingredients[key].count - product.count
end
end
end
end
for key, ingredient in pairs(model.ingredients) do
if ingredient.count < 0.01 then
model.ingredients[key] = nil
end
end
for key, product in pairs(model.products) do
if product.count < 0.01 then
model.products[key] = nil
end
end
end
-------------------------------------------------------------------------------
---Compute resources
---@param model table
function ModelCompute.computeResources(model)
local resources = {}
---calcul resource
for k, ingredient in pairs(model.ingredients) do
if ingredient.resource_category ~= nil or ingredient.name == "water" then
local resource = model.resources[ingredient.name]
if resource ~= nil then
resource.count = ingredient.count
else
resource = Model.newResource(model, ingredient.name, ingredient.type, ingredient.count)
end
if ingredient.resource_category == "basic-solid" then
resource.category = "basic-solid"
end
if ingredient.name == "water" then
resource.category = "basic-fluid"
end
if ingredient.name == "crude-oil" then
resource.category = "basic-fluid"
end
resource.blocks = 1
resource.wagon = nil
resource.storage = nil
local ratio = 1
---compute storage
if resource.category == "basic-solid" then
resource.wagon = { type = "item", name = "cargo-wagon" }
resource.wagon.count = math.ceil(resource.count / 2000)
resource.wagon.limit_count = math.ceil(resource.wagon.count * ratio)
resource.storage = { type = "item", name = "steel-chest" }
resource.storage.count = math.ceil(resource.count / (48 * 50))
resource.storage.limit_count = math.ceil(resource.storage.count * ratio)
elseif resource.category == "basic-fluid" then
--resource.wagon = {type="item", name="cargo-wagon"}
--resource.wagon.count = math.ceil(resource.count/2000)
resource.storage = { type = "item", name = "storage-tank" }
resource.storage.count = math.ceil(resource.count / 2400)
resource.storage.limit_count = math.ceil(resource.storage.count * ratio)
end
resources[resource.name] = resource
end
end
model.resources = resources
end
-------------------------------------------------------------------------------
---Compute energy, speed, number total
---@param model table
function ModelCompute.createSummary(model)
model.summary = {}
model.summary.factories = {}
model.summary.beacons = {}
model.summary.modules = {}
model.summary.building = 0
local energy = 0
local pollution = 0
local building = 0
for _, block in pairs(model.blocks) do
energy = energy + block.power
pollution = pollution + (block.pollution_total or 0)
ModelCompute.computeSummaryFactory(block)
building = building + block.summary.building
for _, type in pairs({ "factories", "beacons", "modules" }) do
for _, element in pairs(block.summary[type]) do
if model.summary[type][element.name] == nil then
model.summary[type][element.name] = {
name = element.name,
type = "item",
count = 0
}
end
model.summary[type][element.name].count = model.summary[type][element.name].count + element.count
end
end
end
model.summary.energy = energy
model.summary.pollution = pollution
model.summary.building = building
model.generators = {}
---formule 20 accumulateur /24 panneau solaire/1 MW
model.generators["accumulator"] = { name = "accumulator", type = "item",
count = 20 * math.ceil(energy / (1000 * 1000)) }
model.generators["solar-panel"] = { name = "solar-panel", type = "item",
count = 24 * math.ceil(energy / (1000 * 1000)) }
model.generators["steam-engine"] = { name = "steam-engine", type = "item", count = math.ceil(energy / (510 * 1000)) }
end
-------------------------------------------------------------------------------
---Compute summary factory
---@param block table
function ModelCompute.computeSummaryFactory(block)
if block ~= nil then
block.summary = { building = 0, factories = {}, beacons = {}, modules = {} }
for _, recipe in pairs(block.recipes) do
---calcul nombre factory
local factory = recipe.factory
if block.summary.factories[factory.name] == nil then
block.summary.factories[factory.name] = {
name = factory.name,
type = factory.type or "entity",
count = 0
}
end
block.summary.factories[factory.name].count = block.summary.factories[factory.name].count +
math.ceil(factory.count)
block.summary.building = block.summary.building + math.ceil(factory.count)
---calcul nombre de module factory
if factory.modules ~= nil then
for module, value in pairs(factory.modules) do
if block.summary.modules[module] == nil then
block.summary.modules[module] = {
name = module,
type = "item",
count = 0
}
end
block.summary.modules[module].count = block.summary.modules[module].count +
value * math.ceil(factory.count)
end
end
---calcul nombre beacon
local beacons = recipe.beacons
if beacons ~= nil then
for _, beacon in pairs(beacons) do
if block.summary.beacons[beacon.name] == nil then
block.summary.beacons[beacon.name] = {
name = beacon.name,
type = beacon.type or "entity",
count = 0
}
end
block.summary.beacons[beacon.name].count = block.summary.beacons[beacon.name].count + math.ceil(beacon.count)
block.summary.building = block.summary.building + math.ceil(beacon.count)
---calcul nombre de module beacon
if beacon.modules ~= nil then
for module, value in pairs(beacon.modules) do
if block.summary.modules[module] == nil then
block.summary.modules[module] = {
name = module,
type = "item",
count = 0
}
end
block.summary.modules[module].count = block.summary.modules[module].count +
value * math.ceil(beacon.count)
end
end
end
end
end
end
end
return ModelCompute