429 lines
15 KiB
Lua
429 lines
15 KiB
Lua
---@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
|