Первый фикс

Пачки некоторых позиций увеличены
This commit is contained in:
2024-03-01 20:53:32 +03:00
commit 7c9c708c92
23653 changed files with 767936 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
---@class FPBeacon
---@field proto FPBeaconPrototype
---@field amount number
---@field total_amount number
---@field module_set FPModuleSet
---@field total_effects ModuleEffects
---@field effects_tooltip string
---@field valid boolean
---@field parent FPLine
---@field class "Beacon"
-- This is a 'class' representing a (group of) beacon(s) and the modules attached to it
Beacon = {}
function Beacon.init(beacon_proto, beacon_amount, total_amount, parent)
local beacon = {
proto = beacon_proto,
amount = beacon_amount or 0,
total_amount = total_amount, -- can be nil
module_set = nil, -- set right below
total_effects = nil,
effects_tooltip = "",
valid = true,
parent = parent,
class = "Beacon"
}
beacon.module_set = ModuleSet.init(beacon)
return beacon
end
function Beacon.summarize_effects(self)
local effect_multiplier = self.proto.effectivity * self.amount
local effects = self.module_set.total_effects
for name, effect in pairs(effects) do
effects[name] = effect * effect_multiplier
end
self.total_effects = effects
self.effects_tooltip = util.gui.format_module_effects(effects, false)
Line.summarize_effects(self.parent)
end
function Beacon.check_module_compatibility(self, module_proto)
local recipe_proto, machine_proto = self.parent.recipe.proto, self.parent.machine.proto
if next(module_proto.limitations) and recipe_proto.use_limitations
and not module_proto.limitations[recipe_proto.name] then
return false
end
local machine_effects, beacon_effects = machine_proto.allowed_effects, self.proto.allowed_effects
if machine_effects == nil or beacon_effects == nil then
return false
else
for effect_name, _ in pairs(module_proto.effects) do
if machine_effects[effect_name] == false or beacon_effects[effect_name] == false then
return false
end
end
end
return true
end
function Beacon.paste(self, object)
if object.class == "Beacon" then
object.parent = self.parent
self.parent.beacon = object
Line.summarize_effects(self.parent)
return true, nil
elseif object.class == "Module" and self.module_set ~= nil then
-- Only allow modules to be pasted if this is a non-fake beacon
return ModuleSet.paste(self.module_set, object)
else
return false, "incompatible_class"
end
end
function Beacon.clone(self)
if not self then return nil end
local clone = Beacon.unpack(Beacon.pack(self))
clone.parent = self.parent
Beacon.validate(clone)
return clone
end
function Beacon.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, nil),
amount = self.amount,
total_amount = self.total_amount,
module_set = ModuleSet.pack(self.module_set),
class = self.class
}
end
function Beacon.unpack(packed_self)
local self = packed_self
self.module_set = ModuleSet.unpack(packed_self.module_set)
self.module_set.parent = self
return self
end
-- Needs validation: proto, module_set
function Beacon.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, nil)
self.valid = (not self.proto.simplified)
local machine = self.parent.machine -- make sure the machine can still be influenced by beacons
if machine.valid then self.valid = (machine.proto.allowed_effects ~= nil) and self.valid end
if BEACON_OVERLOAD_ACTIVE then self.amount = 1 end
self.valid = ModuleSet.validate(self.module_set) and self.valid
return self.valid
end
-- Needs repair: module_set
function Beacon.repair(self, _)
if self.proto.simplified then -- if still simplified, the beacon can't be repaired and needs to be removed
return false
else -- otherwise, the modules need to be checked and removed if necessary
-- Remove invalid modules and normalize the remaining ones
self.valid = ModuleSet.repair(self.module_set)
if self.module_set.module_count == 0 then return false end -- if the beacon is empty, remove it
end
self.valid = true -- if it gets to here, the beacon was successfully repaired
return true
end

View File

@@ -0,0 +1,209 @@
---@class FPCollection<T>
---@field datasets { [integer]: `T` }
---@field index integer
---@field count integer
---@field class "Collection"
-- 'Class' representing a list of objects/datasets with some useful methods
-- (An object only becomes a dataset once it is added to the collection)
Collection = {}
function Collection.init()
return {
datasets = {},
index = 0,
count = 0,
class = "Collection"
}
end
-- Adds given object to the end of the collection
function Collection.add(self, object)
if not object then error("Can't insert nil dataset") end
self.index = self.index + 1
object.id = self.index
self.datasets[self.index] = object
self.count = self.count + 1
object.gui_position = self.count
return object -- Returning it here feels nice
end
-- Inserts the given object at the given position, shifting other elements down
function Collection.insert_at(self, gui_position, object)
if not object then error("Can't insert nil dataset") end
if not gui_position then error("Can't insert at nil position") end
self.index = self.index + 1
object.id = self.index
self.count = self.count + 1
object.gui_position = gui_position
for _, dataset in pairs(self.datasets) do
if dataset.gui_position >= gui_position then
dataset.gui_position = dataset.gui_position + 1
end
end
self.datasets[self.index] = object
return object
end
function Collection.remove(self, dataset)
if not dataset then error("Can't remove nil dataset") end
-- Move positions of datasets after the deleted one down by one
for _, d in pairs(self.datasets) do
if d.gui_position > dataset.gui_position then
d.gui_position = d.gui_position - 1
end
end
self.count = self.count - 1
self.datasets[dataset.id] = nil
if self.count ~= table_size(self.datasets) then error("Dataset count incorrect") end
-- Returning the deleted position here to allow for GUI adjustments
return dataset.gui_position
end
-- Replaces the dataset with the new object in-place
function Collection.replace(self, dataset, object)
if not dataset then error("Can't replace nil dataset") end
if not object then error("Can't replace with nil object") end
object.id = dataset.id
object.gui_position = dataset.gui_position
self.datasets[dataset.id] = object
return object -- Returning it here feels nice
end
function Collection.get(self, object_id)
return self.datasets[object_id]
end
-- For when order doesn't matter
function Collection.get_all(self)
return self.datasets
end
-- Return format: {[gui_position] = dataset}
function Collection.get_in_order(self, reverse)
local ordered_datasets = {}
for _, dataset in pairs(self.datasets) do
local table_position = (reverse) and (self.count - dataset.gui_position + 1) or dataset.gui_position
ordered_datasets[table_position] = dataset
end
return ordered_datasets
end
-- Returns the dataset specified by the gui_position
function Collection.get_by_gui_position(self, gui_position)
if gui_position == 0 then return nil end
for _, dataset in pairs(self.datasets) do
if dataset.gui_position == gui_position then
return dataset
end
end
end
-- Returns the dataset with the given name, nil if it doesn't exist
function Collection.get_by_name(self, name)
for _, dataset in pairs(self.datasets) do
-- Check against the prototype, if it exists
local check_against = dataset.proto or dataset
if check_against.name == name then
return dataset
end
end
return nil
end
-- Returns the dataset with the given type and name, nil if it doesn't exist
function Collection.get_by_type_and_name(self, type_name, name)
for _, dataset in pairs(self.datasets) do
-- Check against the prototype, if it exists
local check_against = dataset.proto or dataset
if check_against.type == type_name and check_against.name == name then
return dataset
end
end
return nil
end
function Collection.shift(self, main_dataset, first_position, direction, spots)
if not main_dataset then error("Can't shift nil dataset") end
if not(direction == "negative" or direction == "positive") then error("Can't shift in invalid direction") end
local original_position = main_dataset.gui_position
local new_position = nil
if spots == nil then -- means shift-to-end
new_position = (direction == "positive") and self.count or first_position
else
if direction == "positive" then
new_position = math.min(original_position + spots, self.count)
else
new_position = math.max(original_position - spots, first_position)
end
end
Collection.remove(self, main_dataset)
Collection.insert_at(self, new_position, main_dataset)
end
-- Packs every dataset in this collection
function Collection.pack(self, object_class)
local packed_collection = {
objects = {},
class = self.class
}
for _, dataset in ipairs(Collection.get_in_order(self)) do
table.insert(packed_collection.objects, object_class.pack(dataset))
end
return packed_collection
end
-- Unpacks every dataset in this collection
function Collection.unpack(packed_self, parent, object_class)
local self = Collection.init()
self.class = packed_self.class
for _, object in ipairs(packed_self.objects) do -- packed objects already in array order
local dataset = Collection.add(self, object_class.unpack(object))
dataset.parent = parent
end
return self
end
-- Updates the validity of all datasets in this collection
function Collection.validate_datasets(self, object_class)
local valid = true
for _, dataset in pairs(self.datasets) do
-- Stays true until a single dataset is invalid, then stays false
valid = object_class.validate(dataset) and valid
end
return valid
end
-- Removes any invalid, unrepairable datasets from the collection
function Collection.repair_datasets(self, player, object_class)
for _, dataset in pairs(self.datasets) do
if not dataset.valid and not object_class.repair(dataset, player) then
_G[dataset.parent.class].remove(dataset.parent, dataset)
end
end
end

View File

@@ -0,0 +1,104 @@
---@class FPFactory
---@field Subfactory FPCollection<FPSubfactory>
---@field selected_subfactory FPSubfactory?
---@field export_modset ModToVersion
---@field class "Factory"
-- 'Class' representing the whole of a players actual data, including all subfactories
Factory = {}
---@return FPFactory
function Factory.init()
return {
Subfactory = Collection.init(),
selected_subfactory = nil,
-- A Factory can not become invalid
class = "Factory"
}
end
function Factory.add(self, object)
object.parent = self
return Collection.add(self[object.class], object)
end
function Factory.insert_at(self, gui_position, object)
object.parent = self
return Collection.insert_at(self[object.class], gui_position, object)
end
function Factory.remove(self, dataset)
local removed = Collection.remove(self[dataset.class], dataset)
if self.selected_subfactory and self.selected_subfactory.id == dataset.id then
self.selected_subfactory = self.Subfactory.datasets[1] -- can be nil
end
return removed
end
---@return FPSubfactory
function Factory.get(self, class, dataset_id)
return Collection.get(self[class], dataset_id)
end
---@return FPSubfactory[]
function Factory.get_all(self, class)
return Collection.get_all(self[class])
end
---@return FPSubfactory[]
function Factory.get_in_order(self, class, reverse)
return Collection.get_in_order(self[class], reverse)
end
function Factory.get_by_gui_position(self, class, gui_position)
return Collection.get_by_gui_position(self[class], gui_position)
end
function Factory.shift(self, dataset, first_position, direction, spots)
Collection.shift(self[dataset.class], dataset, first_position, direction, spots)
end
function Factory.count(self, class) return self[class].count end
-- Imports every subfactory in the given string to this Factory, returning a reference to the first one
---@return FPSubfactory
function Factory.import_by_string(self, export_string)
local import_factory = util.porter.process_export_string(export_string) ---@cast import_factory -nil
-- No error handling here, as the export_string for this will always be known to work
local first_subfactory = nil
for _, subfactory in pairs(Factory.get_in_order(import_factory, "Subfactory")) do
Factory.add(self, subfactory)
first_subfactory = first_subfactory or subfactory
end
return first_subfactory
end
-- Updates every top level product of this Factory to the given product definition type
function Factory.update_product_definitions(self, new_defined_by)
for _, subfactory in ipairs(Factory.get_in_order(self, "Subfactory")) do
Subfactory.update_product_definitions(subfactory, new_defined_by)
end
end
-- Updates the ingredient satisfaction data on every subfactory
function Factory.update_ingredient_satisfactions(self)
for _, subfactory in ipairs(Factory.get_in_order(self, "Subfactory")) do
solver.determine_ingredient_satisfaction(subfactory)
end
end
function Factory.update_calculations(self, player)
for _, subfactory in ipairs(Factory.get_in_order(self, "Subfactory")) do
solver.update(player, subfactory)
end
end
-- Needs validation: Subfactory
function Factory.validate(self)
Collection.validate_datasets(self.Subfactory, Subfactory)
-- Factories can't be invalid, this is just to cleanly validate the subfactories
end

View File

@@ -0,0 +1,187 @@
---@class FPFloor
---@field level integer
---@field origin_line FPLine?
---@field defining_line FPLine?
---@field Line FPCollection<FPLine>
---@field valid boolean
---@field id integer
---@field parent FPSubfactory
---@field class "Floor"
-- 'Class' representing a floor of a subfactory with individual assembly lines
Floor = {}
function Floor.init(creating_line)
local floor = {
level = 1, -- top floor has a level of 1, it's initialized with Floor.init(nil)
origin_line = nil, -- set below, only if level > 1. The line this subfloor is attached to
defining_line = nil, -- set below, only if level > 1. First line of this subfloor
Line = Collection.init(),
valid = true,
id = nil, -- set by collection
parent = nil, -- set by parent
class = "Floor"
}
-- Move given line, if it exists, to the subfloor, and create a new origin line
if creating_line ~= nil then
-- Subfloors have a level that is 1 higher than their creating_line's floor
floor.level = creating_line.parent.level + 1
floor.parent = creating_line.parent
local origin_line = Line.init(nil) -- No need to set a machine in this case
origin_line.subfloor = floor -- Link up the newly created origin_line with its subfloor
floor.origin_line = origin_line -- and vice versa
-- Replace the creating_line on its floor with the newly created origin_line
Floor.replace(creating_line.parent, creating_line, origin_line)
Floor.add(floor, creating_line) -- Add the creating_line to the subfloor in the first spot
floor.defining_line = creating_line -- which makes it the defining_line on this floor
end
return floor
end
function Floor.add(self, object)
object.parent = self
return Collection.add(self[object.class], object)
end
function Floor.insert_at(self, gui_position, object)
object.parent = self
return Collection.insert_at(self[object.class], gui_position, object)
end
function Floor.remove(self, dataset)
-- Remove the subfloor(s) associated to a line recursively, so they don't hang around
if dataset.class == "Line" and dataset.subfloor ~= nil then
for _, line in pairs(Floor.get_in_order(dataset.subfloor, "Line")) do
if line.subfloor then Floor.remove(dataset.subfloor, line) end
end
Collection.remove(self.parent.Floor, dataset.subfloor)
end
return Collection.remove(self[dataset.class], dataset)
end
-- Call only on subfloor; deletes itself while leaving defining_line intact
function Floor.reset(self)
local origin_line = self.origin_line
Floor.replace(origin_line.parent, origin_line, self.defining_line)
Subfactory.remove(self.parent, self)
end
function Floor.replace(self, dataset, object)
object.parent = self
return Collection.replace(self[dataset.class], dataset, object)
end
function Floor.get(self, class, dataset_id)
return Collection.get(self[class], dataset_id)
end
function Floor.get_all(self, class)
return Collection.get_all(self[class])
end
function Floor.get_in_order(self, class, reverse)
return Collection.get_in_order(self[class], reverse)
end
function Floor.get_by_gui_position(self, class, gui_position)
return Collection.get_by_gui_position(self[class], gui_position)
end
function Floor.shift(self, dataset, first_position, direction, spots)
Collection.shift(self[dataset.class], dataset, first_position, direction, spots)
end
function Floor.count(self, class) return self[class].count end
-- Returns the machines and modules needed to actually build this floor
function Floor.get_component_data(self, component_table)
local components = component_table or {machines={}, modules={}}
local function add_component(table, proto, amount)
local component = table[proto.name]
if component == nil then
table[proto.name] = {proto = proto, amount = amount}
else
component.amount = component.amount + amount
end
end
local function add_machine(entity_proto, amount)
if not entity_proto.built_by_item then return end
add_component(components.machines, entity_proto.built_by_item, amount)
end
-- Doesn't count subfloors when looking at this specific floors. Maybe it should, which
-- would mean the subfactory machine total is equal to the floor total of the top floor
for _, line in pairs(Floor.get_in_order(self, "Line")) do
if line.subfloor == nil then
local machine = line.machine
local ceil_machine_count = math.ceil(machine.count - 0.001)
add_machine(machine.proto, ceil_machine_count)
for _, module in pairs(ModuleSet.get_in_order(machine.module_set)) do
add_component(components.modules, module.proto, ceil_machine_count * module.amount)
end
local beacon = line.beacon
if beacon and beacon.total_amount then
local ceil_total_amount = math.ceil(beacon.total_amount - 0.001)
add_machine(beacon.proto, ceil_total_amount)
for _, module in pairs(ModuleSet.get_all(beacon.module_set)) do
add_component(components.modules, module.proto, ceil_total_amount * module.amount)
end
end
end
end
return components
end
function Floor.pack(self)
return {
Line = Collection.pack(self.Line, Line),
level = self.level,
class = self.class
}
end
-- This unpack-function differs in that it gets called with the floor already existing
-- This function should thus unpack itself into that floor, instead of creating a new one
function Floor.unpack(packed_self, self)
-- This can't use Collection.unpack for its lines because of its recursive nature
-- The calling function also needs to update its Subfactory to include the new subfloor references
for _, packed_line in pairs(packed_self.Line.objects) do
Floor.add(self, Line.unpack(packed_line, packed_self.level))
end
-- return value is not needed here
end
-- Needs validation: Line
function Floor.validate(self)
self.valid = Collection.validate_datasets(self.Line, Line)
return self.valid
end
-- Needs repair: Line
function Floor.repair(self, player)
-- Unrepairable lines get removed, so the subfactory will always be valid afterwards
Collection.repair_datasets(self.Line, player, Line)
self.valid = true
-- Make this floor remove itself if it's empty after repairs
if self.level > 1 and self.Line.count < 2 then Floor.reset(self) end
end

View File

@@ -0,0 +1,71 @@
---@class FPFuel
---@field proto FPFuelPrototype
---@field amount number
---@field satisfied_amount number
---@field valid boolean
---@field parent FPLine
---@field class "Fuel"
-- This is essentially just a wrapper-'class' for a fuel prototype to add some data to it
Fuel = {}
function Fuel.init(proto)
return {
proto = proto,
amount = 0, -- produced amount
satisfied_amount = 0, -- used with ingredient satisfaction
valid = true,
parent = nil,
class = "Fuel"
}
end
function Fuel.paste(self, object)
if object.class == "Fuel" then
local burner = self.parent.proto.burner -- will exist if there is fuel to paste on
for category_name, _ in pairs(burner.categories) do
if self.proto.category == category_name then
self.proto = object.proto
return true, nil
end
end
return false, "incompatible"
else
return false, "incompatible_class"
end
end
function Fuel.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.category),
amount = self.amount, -- conserve for cloning
class = self.class
}
end
function Fuel.unpack(packed_self)
return packed_self
end
-- Needs validation: proto
function Fuel.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "category")
self.valid = (not self.proto.simplified)
-- Make sure the fuel categories are still compatible
if self.valid and self.parent.valid then
local burner = self.parent.proto.burner
self.valid = burner and burner.categories[self.proto.category] ~= nil
end
return self.valid
end
-- Needs repair:
function Fuel.repair(_, _)
-- If the fuel-proto is still simplified, validate couldn't repair it, so it has to be removed
return false -- the parent machine will try to replace it with another fuel of the same category
end

View File

@@ -0,0 +1,157 @@
---@class RequiredAmount
---@field defined_by "amount" | "belts" | "lanes"
---@field amount number
---@field belt_proto FPBeltPrototype
---@class FPItem
---@field proto FPItemPrototype
---@field amount number
---@field required_amount RequiredAmount
---@field satisfied_amount number
---@field top_level boolean
---@field valid boolean
---@field id integer
---@field gui_position integer
---@field parent FPSubfactory | FPLine
---@field class "Product" | "Byproduct" | "Ingredient"
-- 'Class' representing an item in the general sense
Item = {}
Product, Byproduct, Ingredient = Item, Item, Item -- allows _G[class] to work for all items
-- Initialised by passing a prototype from the all_items global table
-- This is set up as a top-level item if a required_amount is given
function Item.init(proto, class, amount, required_amount)
-- Special case for non-product top level items
if required_amount == 0 then required_amount = {defined_by="amount", amount=0} end
return {
proto = proto,
amount = amount or 0, -- produced amount
required_amount = required_amount, -- is a table
satisfied_amount = 0, -- used with ingredient satisfaction
top_level = (required_amount ~= nil),
valid = true,
id = nil, -- set by collection
gui_position = nil, -- set by collection
parent = nil, -- set by parent
class = class
}
end
-- Returns the converted numeric required_amount for this (top level) item
function Item.required_amount(self)
local req_amount = self.required_amount
if req_amount.defined_by == "amount" then
return req_amount.amount
else -- defined_by == "belts"/"lanes"
-- If this is defined by lanes, only half of the throughput of a full belt needs to be considered
local multiplier = (req_amount.defined_by == "belts") and 1 or 0.5
local timescale = self.parent.timescale
return req_amount.amount * (req_amount.belt_proto.throughput * multiplier) * timescale
end
end
function Item.paste(self, object) -- this is implicitly only called on top level items
if object.class == "Product" or object.class == "Byproduct"
or object.class == "Ingredient" or object.class == "Fuel" then
local existing_item = Subfactory.get_by_name(self.parent, self.class, object.proto.name)
-- Avoid duplicate items, but allow pasting over the same item proto
if existing_item and existing_item.proto.name == object.proto.name
and not (self.proto.name == object.proto.name) then
return false, "already_exists"
end
-- Convert object into the appropriate top-level form if necessary
if not (object.top_level and object.class == self.class) then
local required_amount = {defined_by = "amount", amount = object.amount}
object = Item.init(object.proto, self.class, 0, required_amount)
end
-- Detect when this is called on a fake item and add instead of replacing
if not self.amount then Subfactory.add(self.parent, object)
else Subfactory.replace(self.parent, self, object) end
return true, nil
elseif object.class == "Line" then
local relevant_line = (object.subfloor) and object.subfloor.defining_line or object
for _, product in pairs(Line.get_in_order(relevant_line, "Product")) do
local fake_item = {proto={name=""}, parent=self.parent, class=self.class}
Item.paste(fake_item, product) -- avoid duplicating existing items
end
local top_floor = Subfactory.get(self.parent, "Floor", 1) -- line count can be 0
if object.subfloor then -- if the line has a subfloor, paste its contents on the top floor
local fake_line = {parent=top_floor, class="Line", gui_position=top_floor.Line.count}
for _, line in pairs(Floor.get_in_order(object.subfloor, "Line")) do
Line.paste(fake_line, line)
fake_line.gui_position = fake_line.gui_position + 1
end
else -- if the line has no subfloor, just straight paste it onto the top floor
local fake_line = {parent=top_floor, class="Line", gui_position=top_floor.Line.count}
Line.paste(fake_line, object)
end
return true, nil
else
return false, "incompatible_class"
end
end
function Item.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.type),
amount = self.amount, -- conserve for cloning non-product items
required_amount = (self.top_level) and {
defined_by = self.required_amount.defined_by,
amount = self.required_amount.amount,
belt_proto = (self.required_amount.defined_by ~= "amount")
and prototyper.util.simplify_prototype(self.required_amount.belt_proto, nil)
} or nil,
top_level = self.top_level,
class = self.class
}
end
function Item.unpack(packed_self)
return packed_self
end
-- Needs validation: proto, required_amount
function Item.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "type")
self.valid = (not self.proto.simplified)
-- Validate the belt_proto if the item proto is still valid, ie not simplified
local req_amount = self.required_amount
if req_amount and req_amount.defined_by ~= "amount" then
local belt_throughput = req_amount.belt_proto.throughput
req_amount.belt_proto = prototyper.util.validate_prototype_object(req_amount.belt_proto, nil)
self.valid = (not req_amount.belt_proto.simplified) and self.valid
-- If the proto has to be simplified, conserve the throughput, so repair can convert it to an amount-spec
if req_amount.belt_proto.simplified then req_amount.belt_proto.throughput = belt_throughput end
end
return self.valid
end
-- Needs repair: required_amount
-- This will only be called on top level items, so they can be treated as such
function Item.repair(self, _)
-- If the item-proto is still simplified, validate couldn't repair it, so it has to be removed
if self.proto.simplified then return false end
-- If the item is fine, the belt_proto has to be the things that is invalid. Thus, we will repair
-- this item by converting it to be defined by amount, so it can be preserved in some form
self.required_amount = {
defined_by = "amount",
amount = Item.required_amount(self)
}
self.valid = true
return self.valid
end

View File

@@ -0,0 +1,428 @@
---@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

View File

@@ -0,0 +1,189 @@
---@class FPMachine
---@field proto FPMachinePrototype
---@field count number
---@field limit number?
---@field force_limit boolean
---@field fuel FPFuel?
---@field module_set FPModuleSet
---@field total_effects ModuleEffects
---@field effects_tooltip string
---@field valid boolean
---@field parent FPLine
---@field class "Machine"
-- Class representing a machine with its attached modules and fuel
Machine = {}
function Machine.init(proto, parent)
local machine = {
proto = proto,
count = 0,
limit = nil, -- will be set by the user
force_limit = true, -- ignored if limit is not set
fuel = nil, -- needs to be set by calling Machine.find_fuel afterwards
module_set = nil, -- set right below
total_effects = nil,
effects_tooltip = "",
valid = true,
parent = parent,
class = "Machine"
}
machine.module_set = ModuleSet.init(machine)
return machine
end
function Machine.normalize_fuel(self, player)
if self.proto.energy_type ~= "burner" then self.fuel = nil; return end
-- no need to continue if this machine doesn't have a burner
local burner = self.proto.burner
-- Check if fuel has a valid category for this machine, replace otherwise
if self.fuel and not burner.categories[self.fuel.proto.category] then self.fuel = nil end
-- If this machine has fuel already, don't replace it
if self.fuel == nil then
-- Use the first category of this machine's burner as the default one
local fuel_category_name, _ = next(burner.categories, nil)
local fuel_category_id = PROTOTYPE_MAPS.fuels[fuel_category_name].id
local default_fuel_proto = prototyper.defaults.get(player, "fuels", fuel_category_id)
self.fuel = Fuel.init(default_fuel_proto)
self.fuel.parent = self
end
end
function Machine.summarize_effects(self, mining_prod)
local effects = self.module_set.total_effects
effects["base_prod"] = self.proto.base_productivity or nil
effects["mining_prod"] = mining_prod or nil
self.total_effects = effects
self.effects_tooltip = util.gui.format_module_effects(effects, false)
Line.summarize_effects(self.parent)
end
function Machine.check_module_compatibility(self, module_proto)
local recipe = self.parent.recipe
if self.proto.module_limit == 0 then return false end
if next(module_proto.limitations) and recipe.proto.use_limitations
and not module_proto.limitations[recipe.proto.name] then
return false
end
local allowed_effects = self.proto.allowed_effects
if allowed_effects == nil then
return false
else
for effect_name, _ in pairs(module_proto.effects) do
if allowed_effects[effect_name] == false then
return false
end
end
end
return true
end
function Machine.paste(self, object)
if object.class == "Machine" then
-- See if the pasted machine also exists in this machine's category, and use it if compatible
local found_machine = prototyper.util.find_prototype("machines", object.proto.name, self.proto.category)
if found_machine and Line.is_machine_applicable(self.parent, found_machine) then
self.parent.machine = object
object.parent = self.parent
object.proto = found_machine -- make sure to set this to the right category machine
ModuleSet.normalize(object.module_set, {compatibility=true, effects=true})
Line.summarize_effects(object.parent)
return true, nil
else
return false, "incompatible"
end
elseif object.class == "Module" then
return ModuleSet.paste(self.module_set, object)
else
return false, "incompatible_class"
end
end
function Machine.clone(self)
local clone = Machine.unpack(Machine.pack(self))
clone.parent = self.parent
clone.count = self.count -- keep around to avoid recalc being needed
Machine.validate(clone)
return clone
end
function Machine.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.category),
limit = self.limit,
force_limit = self.force_limit,
fuel = (self.fuel) and Fuel.pack(self.fuel) or nil,
module_set = ModuleSet.pack(self.module_set),
class = self.class
}
end
function Machine.unpack(packed_self)
local self = packed_self
self.fuel = (packed_self.fuel) and Fuel.unpack(packed_self.fuel) or nil
if self.fuel then self.fuel.parent = self end
self.module_set = ModuleSet.unpack(packed_self.module_set)
self.module_set.parent = self
return self
end
-- Needs validation: proto, fuel, module_set
function Machine.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "category")
self.valid = (not self.proto.simplified)
local parent_line = self.parent
if self.valid and parent_line.valid and parent_line.recipe.valid then
self.valid = Line.is_machine_applicable(parent_line, self.proto)
end
-- If the machine changed to not use a burner, remove its fuel
if not self.proto.burner then self.fuel = nil end
if self.fuel then self.valid = Fuel.validate(self.fuel) and self.valid end
self.valid = ModuleSet.validate(self.module_set) and self.valid
return self.valid
end
-- Needs repair: proto, fuel, module_set
function Machine.repair(self, player)
-- If the prototype is still simplified, it couldn't be fixed by validate
-- A final possible fix is to replace this machine with the default for its category
if self.proto.simplified and not Line.change_machine_to_default(self.parent, player) then
return false -- if this happens, the whole line can not be salvaged
end
self.valid = true -- if it gets to this, change_machine was successful and the machine is valid
-- It just might need to cleanup some fuel and/or modules
if self.fuel and not self.fuel.valid and not Fuel.repair(self.fuel) then
-- If fuel is unrepairable, replace it with a default value
self.fuel = nil
Machine.normalize_fuel(self, player)
end
-- Remove invalid modules and normalize the remaining ones
ModuleSet.repair(self.module_set)
return true
end

View File

@@ -0,0 +1,104 @@
---@class FPModule
---@field proto FPModulePrototype
---@field amount integer
---@field total_effects ModuleEffects
---@field effects_tooltip string
---@field valid boolean
---@field id integer
---@field gui_position integer
---@field parent FPModuleSet
---@field class "Module"
-- 'Class' representing an module
Module = {}
-- Initialised by passing a prototype from the all_moduless global table
function Module.init(proto, amount, parent)
local module = {
proto = proto,
amount = amount,
total_effects = nil,
effects_tooltip = "",
valid = true,
id = nil, -- set by collection
gui_position = nil, -- set by collection
parent = parent,
class = "Module"
}
Module.summarize_effects(module)
return module
end
function Module.set_amount(self, new_amount)
self.amount = new_amount
ModuleSet.count_modules(self.parent)
Module.summarize_effects(self)
end
function Module.summarize_effects(self)
local effects = {consumption = 0, speed = 0, productivity = 0, pollution = 0}
for name, effect in pairs(self.proto.effects) do
effects[name] = effect.bonus * self.amount
end
self.total_effects = effects
self.effects_tooltip = util.gui.format_module_effects(effects, false)
end
function Module.paste(self, object)
if object.class == "Module" then
if ModuleSet.check_compatibility(self.parent, object.proto) then
if ModuleSet.get_by_name(self.parent, object.proto.name) and object.proto.name ~= self.proto.name then
return false, "already_exists"
else
object.amount = math.min(object.amount, self.amount + self.parent.empty_slots)
Module.summarize_effects(object)
ModuleSet.replace(self.parent, self, object)
ModuleSet.summarize_effects(self.parent)
return true, nil
end
else
return false, "incompatible"
end
else
return false, "incompatible_class"
end
end
function Module.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, self.proto.category),
amount = self.amount,
class = self.class
}
end
function Module.unpack(packed_self)
return packed_self
end
-- Needs validation: proto
function Module.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, "category")
self.valid = (not self.proto.simplified)
-- Check whether the module is still compatible with its machine or beacon
if self.valid and self.parent.valid then
self.valid = _G[self.parent.parent.class].check_module_compatibility(self.parent.parent, self.proto)
end
if self.valid then Module.summarize_effects(self) end
return self.valid
end
-- Needs repair:
function Module.repair(_, _)
-- If the prototype is still simplified, it couldn't be fixed by validate, so it has to be removed
return false
end

View File

@@ -0,0 +1,240 @@
---@class FPModuleSet
---@field modules FPCollection<FPModule>
---@field module_count integer
---@field module_limit integer
---@field empty_slots integer
---@field total_effects ModuleEffects
---@field valid boolean
---@field parent FPMachine | FPBeacon
---@field class "ModuleSet"
-- 'Class' representing a group of modules in a machine or beacon
ModuleSet = {}
function ModuleSet.init(parent)
return {
modules = Collection.init(),
module_count = 0,
module_limit = parent.proto.module_limit,
empty_slots = parent.proto.module_limit,
total_effects = nil, -- summarized by calling function
valid = true,
parent = parent,
class = "ModuleSet"
}
end
function ModuleSet.add(self, proto, amount)
local object = Module.init(proto, amount, self)
local dataset = Collection.add(self.modules, object)
ModuleSet.count_modules(self)
return dataset
end
function ModuleSet.remove(self, dataset)
Collection.remove(self.modules, dataset)
ModuleSet.count_modules(self)
end
function ModuleSet.replace(self, dataset, object)
object.parent = self
local replacement = Collection.replace(self.modules, dataset, object)
ModuleSet.count_modules(self)
return replacement
end
function ModuleSet.clear(self)
self.modules = Collection.init()
ModuleSet.count_modules(self)
end
function ModuleSet.get(self, dataset_id)
return Collection.get(self.modules, dataset_id)
end
function ModuleSet.get_by_name(self, name)
return Collection.get_by_name(self.modules, name)
end
function ModuleSet.get_all(self)
return Collection.get_all(self.modules)
end
function ModuleSet.get_in_order(self, reverse)
return Collection.get_in_order(self.modules, reverse)
end
function ModuleSet.get_module_kind_amount(self)
return self.modules.count
end
function ModuleSet.normalize(self, features)
self.module_limit = self.parent.proto.module_limit
if features.compatibility then ModuleSet.verify_compatibility(self) end
if features.trim then ModuleSet.trim(self) end
if features.sort then ModuleSet.sort(self) end
if features.effects then ModuleSet.summarize_effects(self) end
ModuleSet.count_modules(self)
end
function ModuleSet.count_modules(self)
local count = 0
for _, module in pairs(self.modules.datasets) do
count = count + module.amount
end
self.module_count = count
self.empty_slots = self.module_limit - self.module_count
end
function ModuleSet.verify_compatibility(self)
local modules_to_remove = {}
for _, module in ipairs(ModuleSet.get_in_order(self)) do
if not ModuleSet.check_compatibility(self, module.proto) then
table.insert(modules_to_remove, module)
end
end
-- Actually remove incompatible modules; counts updated by calling function
for _, module in pairs(modules_to_remove) do ModuleSet.remove(self, module) end
end
function ModuleSet.trim(self)
local module_count, module_limit = self.module_count, self.module_limit
-- Return if the module count is within limits
if module_count <= module_limit then return end
local modules_to_remove = {}
-- Traverse modules in reverse to trim them off the end
for _, module in ipairs(ModuleSet.get_in_order(self, true)) do
-- Remove a whole module if it brings the count to >= limit
if (module_count - module.amount) >= module_limit then
table.insert(modules_to_remove, module)
module_count = module_count - module.amount
else -- Otherwise, diminish the amount on the module appropriately and break
local new_amount = module.amount - (module_count - module_limit)
Module.set_amount(module, new_amount)
break
end
end
-- Actually remove superfluous modules; counts updated by calling function
for _, module in pairs(modules_to_remove) do ModuleSet.remove(self, module) end
end
-- Sorts modules in a deterministic fashion so they are in the same order for every line
function ModuleSet.sort(self)
local modules_by_name = {}
for _, module in pairs(ModuleSet.get_all(self)) do
modules_by_name[module.proto.name] = module
end
local next_position = 1
for _, category in ipairs(global.prototypes.modules) do
for _, module_proto in ipairs(category.members) do
local module = modules_by_name[module_proto.name]
if module then
module.gui_position = next_position
next_position = next_position + 1
end
end
end
end
function ModuleSet.summarize_effects(self)
local effects = {consumption = 0, speed = 0, productivity = 0, pollution = 0}
for _, module in pairs(self.modules.datasets) do
for name, effect in pairs(module.total_effects) do
effects[name] = effects[name] + effect
end
end
self.total_effects = effects
_G[self.parent.class].summarize_effects(self.parent)
end
function ModuleSet.check_compatibility(self, module_proto)
return _G[self.parent.class].check_module_compatibility(self.parent, module_proto)
end
function ModuleSet.compile_filter(self)
local compatible_modules = {}
for module_name, module_proto in pairs(MODULE_NAME_MAP) do
if ModuleSet.check_compatibility(self, module_proto) then
table.insert(compatible_modules, module_name)
end
end
local existing_modules = {}
for _, module in pairs(self.modules.datasets) do
table.insert(existing_modules, module.proto.name)
end
return {{filter="name", name=compatible_modules},
{filter="name", mode="and", invert=true, name=existing_modules}}
end
function ModuleSet.paste(self, module)
if not ModuleSet.check_compatibility(self, module.proto) then
return false, "incompatible"
elseif self.empty_slots == 0 then
return false, "no_empty_slots"
end
local desired_amount = math.min(module.amount, self.empty_slots)
local existing_module = ModuleSet.get_by_name(self, module.proto.name)
if existing_module then
Module.set_amount(existing_module, existing_module.amount + desired_amount)
else
ModuleSet.add(self, module.proto, desired_amount)
end
ModuleSet.normalize(self, {sort=true, effects=true})
return true, nil
end
function ModuleSet.pack(self)
return {
modules = Collection.pack(self.modules, Module),
-- count, limit, and empty_slots restored by ensuing validation
class = self.class
}
end
function ModuleSet.unpack(packed_self)
local self = packed_self
self.modules = Collection.unpack(packed_self.modules, self, Module)
return self
end
-- Needs validation: modules
function ModuleSet.validate(self)
self.valid = Collection.validate_datasets(self.modules, Module)
if self.valid and self.parent.valid then
if not self.module_count or not self.empty_slots then -- when validating an unpacked ModuleSet
self.module_limit = self.parent.proto.module_limit
ModuleSet.count_modules(self)
end
-- .normalize doesn't remove incompatible modules here, the above validation already marks them
ModuleSet.normalize(self, {trim=true, sort=true, effects=true})
end
return self.valid
end
-- Needs repair: modules
function ModuleSet.repair(self, _)
Collection.repair_datasets(self.modules, nil, Module)
ModuleSet.normalize(self, {trim=true, sort=true, effects=true})
self.valid = true -- repairing invalid modules removes them, making this set valid
return true
end

View File

@@ -0,0 +1,44 @@
---@class FPRecipe
---@field proto FPRecipePrototype
---@field production_type "output" | "input"
---@field valid boolean
---@field class "Recipe"
-- This is essentially just a wrapper-'class' for a recipe prototype to add some data to it
Recipe = {}
function Recipe.init_by_id(recipe_id, production_type)
return {
proto = global.prototypes.recipes[recipe_id],
production_type = production_type,
valid = true,
class = "Recipe"
}
end
function Recipe.pack(self)
return {
proto = prototyper.util.simplify_prototype(self.proto, nil),
production_type = self.production_type,
class = self.class
}
end
function Recipe.unpack(packed_self)
return packed_self
end
-- Needs validation: proto
function Recipe.validate(self)
self.proto = prototyper.util.validate_prototype_object(self.proto, nil)
self.valid = (not self.proto.simplified)
return self.valid
end
-- Needs repair:
function Recipe.repair(_, _)
-- If the prototype is still simplified, it couldn't be fixed by validate, so it has to be removed
return false
end

View File

@@ -0,0 +1,301 @@
---@class FPSubfactory
---@field name string
---@field timescale Timescale
---@field energy_consumption number
---@field pollution number
---@field notes string
---@field mining_productivity number?
---@field blueprints string[]
---@field Product FPCollection<FPItem>
---@field Byproduct FPCollection<FPItem>
---@field Ingredient FPCollection<FPItem>
---@field Floor FPCollection<FPFloor>
---@field matrix_free_items FPItemPrototype[]?
---@field linearly_dependant boolean
---@field selected_floor FPFloor
---@field item_request_proxy LuaEntity?
---@field tick_of_deletion uint?
---@field last_valid_modset { [string]: string }?
---@field mod_version string
---@field valid boolean
---@field id integer
---@field gui_position integer
---@field parent FPFactory
---@field class "Subfactory"
---@class FPPackedSubfactory
-- 'Class' representing a independent part of the factory with in- and outputs
Subfactory = {}
function Subfactory.init(name)
local subfactory = {
name = name,
timescale = nil, -- set after init
energy_consumption = 0,
pollution = 0,
notes = "",
mining_productivity = nil,
blueprints = {},
Product = Collection.init(),
Byproduct = Collection.init(),
Ingredient = Collection.init(),
Floor = Collection.init(),
matrix_free_items = nil,
linearly_dependant = false, -- determined by the solver
selected_floor = nil,
item_request_proxy = nil,
tick_of_deletion = nil, -- ignored on export/import
last_valid_modset = nil,
mod_version = global.mod_version,
valid = true,
id = nil, -- set by collection
gui_position = nil, -- set by collection
parent = nil, -- set by parent
class = "Subfactory"
}
-- Initialize the subfactory with an empty top floor
subfactory.selected_floor = Floor.init(nil)
Subfactory.add(subfactory, subfactory.selected_floor)
return subfactory
end
function Subfactory.tostring(self, attach_products, export_format)
local caption, tooltip = self.name, nil -- don't return a tooltip for the export_format
if attach_products and self.valid then
local product_string = ""
for _, item in pairs(Subfactory.get_in_order(self, "Product")) do
product_string = product_string .. "[img=" .. item.proto.sprite .. "]"
end
if product_string ~= "" then product_string = product_string .. " " end
caption = product_string .. caption
end
if not export_format then
local status_string = ""
if self.tick_of_deletion then status_string = status_string .. "[img=fp_sprite_trash_red] " end
if not self.valid then status_string = status_string .. "[img=fp_sprite_warning_red] " end
caption = status_string .. caption
local trashed_string = "" ---@type LocalisedString
if self.tick_of_deletion then
local ticks_left_in_trash = self.tick_of_deletion - game.tick
local minutes_left_in_trash = math.ceil(ticks_left_in_trash / 3600)
trashed_string = {"fp.subfactory_trashed", minutes_left_in_trash}
end
local invalid_string = (not self.valid) and {"fp.subfactory_invalid"} or ""
tooltip = {"", {"fp.tt_title", caption}, trashed_string, invalid_string}
end
return caption, tooltip
end
function Subfactory.add(self, object)
object.parent = self
return Collection.add(self[object.class], object)
end
function Subfactory.remove(self, dataset)
return Collection.remove(self[dataset.class], dataset)
end
function Subfactory.replace(self, dataset, object)
object.parent = self
return Collection.replace(self[dataset.class], dataset, object)
end
function Subfactory.clear(self, class)
self[class] = Collection.init()
end
function Subfactory.get(self, class, dataset_id)
return Collection.get(self[class], dataset_id)
end
function Subfactory.get_all(self, class)
return Collection.get_all(self[class])
end
function Subfactory.get_in_order(self, class, reverse)
return Collection.get_in_order(self[class], reverse)
end
-- Floors don't have an inherent order, so this makes sense for them
function Subfactory.get_all_floors(self)
return self.Floor.datasets
end
function Subfactory.get_by_name(self, class, name)
return Collection.get_by_name(self[class], name)
end
-- Returns the machines and modules needed to actually build this subfactory
function Subfactory.get_component_data(self)
local components = {machines={}, modules={}}
for _, floor in pairs(Subfactory.get_in_order(self, "Floor")) do
-- Relies on the floor-function to do the heavy lifting
Floor.get_component_data(floor, components)
end
return components
end
-- Updates every top level product of this Subfactory to the given product definition type
function Subfactory.update_product_definitions(self, new_defined_by)
for _, product in pairs(Subfactory.get_in_order(self, "Product")) do
local req_amount = product.required_amount
local current_defined_by = req_amount.defined_by
if current_defined_by ~= "amount" and new_defined_by ~= current_defined_by then
req_amount.defined_by = new_defined_by
local multiplier = (new_defined_by == "belts") and 0.5 or 2
req_amount.amount = req_amount.amount * multiplier
end
end
end
function Subfactory.validate_item_request_proxy(self)
local item_request_proxy = self.item_request_proxy
if item_request_proxy then
if not item_request_proxy.valid or not next(item_request_proxy.item_requests) then
Subfactory.destroy_item_request_proxy(self)
end
end
end
function Subfactory.destroy_item_request_proxy(self)
self.item_request_proxy.destroy{raise_destroy=false}
self.item_request_proxy = nil
end
-- Given line has to have a subfloor; recursively adds references for all subfloors to list
function Subfactory.add_subfloor_references(self, line)
Subfactory.add(self, line.subfloor)
for _, sub_line in pairs(Floor.get_all(line.subfloor, "Line")) do
if sub_line.subfloor then Subfactory.add_subfloor_references(self, sub_line) end
end
end
function Subfactory.clone(self)
local clone = Subfactory.unpack(Subfactory.pack(self))
clone.parent = self.parent
Subfactory.validate(clone)
return clone
end
function Subfactory.pack(self)
local packed_free_items = (self.matrix_free_items) and {} or nil
for index, proto in pairs(self.matrix_free_items or {}) do
packed_free_items[index] = prototyper.util.simplify_prototype(proto, proto.type)
end
return {
name = self.name,
timescale = self.timescale,
notes = self.notes,
mining_productivity = self.mining_productivity,
blueprints = self.blueprints,
Product = Collection.pack(self.Product, Item),
matrix_free_items = packed_free_items,
-- Floors get packed by recursive nesting, which is necessary for a json-type data
-- structure. It will need to be unpacked into the regular structure 'manually'.
top_floor = Floor.pack(Subfactory.get(self, "Floor", 1)),
class = self.class
}
end
function Subfactory.unpack(packed_self)
local self = Subfactory.init(packed_self.name)
self.timescale = packed_self.timescale
self.notes = packed_self.notes
self.mining_productivity = packed_self.mining_productivity
self.blueprints = packed_self.blueprints
self.Product = Collection.unpack(packed_self.Product, self, Item)
if packed_self.matrix_free_items then
self.matrix_free_items = {}
for index, proto in pairs(packed_self.matrix_free_items) do
-- Prototypes will be automatically unpacked by the validation process
self.matrix_free_items[index] = proto
end
end
-- Floor unpacking is called on the top floor, which recursively goes through its subfloors
local top_floor = self.selected_floor ---@cast top_floor -nil
Floor.unpack(packed_self.top_floor, top_floor)
-- Make sure to create references to all subfloors after unpacking
for _, line in pairs(Floor.get_all(top_floor, "Line")) do
if line.subfloor then Subfactory.add_subfloor_references(self, line) end
end
return self
end
-- Needs validation: Product, Floor
function Subfactory.validate(self)
local previous_validity = self.valid
self.valid = Collection.validate_datasets(self.Product, Item)
-- Validating matrix_free_items is a bit messy with the current functions,
-- it might be worth it to change it into a Collection at some point
for index, _ in pairs(self.matrix_free_items or {}) do
self.matrix_free_items[index] = prototyper.util.validate_prototype_object(self.matrix_free_items[index], "type")
self.valid = (not self.matrix_free_items[index].simplified) and self.valid
end
-- Floor validation is called on the top floor, which recursively goes through its subfloors
local top_floor = Subfactory.get(self, "Floor", 1)
self.valid = Floor.validate(top_floor) and self.valid
Subfactory.validate_item_request_proxy(self)
if self.valid then self.last_valid_modset = nil
-- If this subfactory became invalid with the current configuration, retain the modset before the current one
-- The one in global is still the previous one as it's only updated after migrations
elseif previous_validity and not self.valid then self.last_valid_modset = global.installed_mods end
-- return value is not needed here
end
-- Needs repair: Product, Floor, selected_floor
function Subfactory.repair(self, player)
local top_floor = Subfactory.get(self, "Floor", 1)
self.selected_floor = top_floor -- reset the selected floor to the one that's guaranteed to exist
-- Unrepairable item-objects get removed, so the subfactory will always be valid afterwards
Collection.repair_datasets(self.Product, nil, Item)
-- Clear item prototypes so we don't need to rely on the solver to remove them
Subfactory.clear(self, "Byproduct")
Subfactory.clear(self, "Ingredient")
-- Remove any unrepairable free item so the subfactory remains valid
local free_items = self.matrix_free_items or {}
for index = #free_items, 1, -1 do
if free_items[index].simplified then table.remove(free_items, index) end
end
-- Floor repair is called on the top floor, which recursively goes through its subfloors
Floor.repair(top_floor, player)
self.last_valid_modset = nil
self.valid = true
-- return value is not needed here
end