977 lines
41 KiB
Lua
977 lines
41 KiB
Lua
local generator_util = require("backend.handlers.generator_util")
|
|
|
|
local generator = {
|
|
machines = {},
|
|
recipes = {},
|
|
items = {},
|
|
fuels = {},
|
|
belts = {},
|
|
wagons = {},
|
|
modules = {},
|
|
beacons = {}
|
|
}
|
|
|
|
|
|
---@class FPPrototype
|
|
---@field id integer
|
|
---@field data_type DataType
|
|
---@field name string
|
|
---@field localised_name LocalisedString
|
|
---@field sprite SpritePath
|
|
|
|
---@class FPPrototypeWithCategory: FPPrototype
|
|
---@field category_id integer
|
|
|
|
---@alias AnyFPPrototype FPPrototype | FPPrototypeWithCategory
|
|
|
|
|
|
---@param list AnyNamedPrototypes
|
|
---@param prototype FPPrototype
|
|
---@param category string?
|
|
local function insert_prototype(list, prototype, category)
|
|
if category == nil then
|
|
---@cast list NamedPrototypes<FPPrototype>
|
|
list[prototype.name] = prototype
|
|
else
|
|
---@cast list NamedPrototypesWithCategory<FPPrototype>
|
|
list[category] = list[category] or { name = category, members = {} }
|
|
list[category].members[prototype.name] = prototype
|
|
end
|
|
end
|
|
|
|
---@param list AnyNamedPrototypes
|
|
---@param name string
|
|
---@param category string?
|
|
local function remove_prototype(list, name, category)
|
|
if category == nil then
|
|
---@cast list NamedPrototypes<FPPrototype>
|
|
list[name] = nil
|
|
else
|
|
---@cast list NamedPrototypesWithCategory<FPPrototype>
|
|
list[category].members[name] = nil
|
|
if next(list[category].members) == nil then list[category] = nil end
|
|
end
|
|
end
|
|
|
|
|
|
---@class FPMachinePrototype: FPPrototypeWithCategory
|
|
---@field data_type "machines"
|
|
---@field category string
|
|
---@field ingredient_limit integer
|
|
---@field fluid_channels FluidChannels
|
|
---@field speed double
|
|
---@field energy_type "burner" | "electric" | "void"
|
|
---@field energy_usage double
|
|
---@field energy_drain double
|
|
---@field emissions double
|
|
---@field built_by_item FPItemPrototype?
|
|
---@field base_productivity double
|
|
---@field allowed_effects AllowedEffects?
|
|
---@field module_limit integer
|
|
---@field launch_sequence_time number?
|
|
---@field burner MachineBurner?
|
|
|
|
---@class FluidChannels
|
|
---@field input integer
|
|
---@field output integer
|
|
|
|
---@class MachineBurner
|
|
---@field effectivity double
|
|
---@field categories { [string]: boolean }
|
|
|
|
-- Generates a table containing all machines for all categories
|
|
---@return NamedPrototypesWithCategory<FPMachinePrototype>
|
|
function generator.machines.generate()
|
|
local machines = {} ---@type NamedPrototypesWithCategory<FPMachinePrototype>
|
|
|
|
---@param category string
|
|
---@param proto LuaEntityPrototype
|
|
---@return FPMachinePrototype?
|
|
local function generate_category_entry(category, proto)
|
|
-- First, determine if there is a valid sprite for this machine
|
|
local sprite = generator_util.determine_entity_sprite(proto)
|
|
if sprite == nil then return end
|
|
|
|
-- If it is a miner, set speed to mining_speed so the machine_count-formula works out
|
|
local speed = proto.crafting_categories and proto.crafting_speed or proto.mining_speed
|
|
|
|
-- Determine data related to the energy source
|
|
local energy_type, emissions = "", 0 -- emissions remain at 0 if no energy source is present
|
|
local burner = nil ---@type MachineBurner
|
|
local energy_usage, energy_drain = (proto.energy_usage or proto.max_energy_usage or 0), 0
|
|
|
|
-- Determine the name of the item that actually builds this machine for the item requester
|
|
-- There can technically be more than one, but bots use the first one, so I do too
|
|
local built_by_item = (proto.items_to_place_this) and proto.items_to_place_this[1].name or nil
|
|
|
|
-- Determine the details of this entities energy source
|
|
local burner_prototype, fluid_burner_prototype = proto.burner_prototype, proto.fluid_energy_source_prototype
|
|
if burner_prototype then
|
|
energy_type = "burner"
|
|
emissions = burner_prototype.emissions
|
|
burner = {effectivity = burner_prototype.effectivity, categories = burner_prototype.fuel_categories}
|
|
|
|
-- Only supports fluid energy that burns_fluid for now, as it works the same way as solid burners
|
|
-- Also doesn't respect scale_fluid_usage and fluid_usage_per_tick for now, let the reports come
|
|
elseif fluid_burner_prototype then
|
|
emissions = fluid_burner_prototype.emissions
|
|
|
|
if fluid_burner_prototype.burns_fluid and not fluid_burner_prototype.fluid_box.filter then
|
|
energy_type = "burner"
|
|
burner = {effectivity = fluid_burner_prototype.effectivity, categories = {["fluid-fuel"] = true}}
|
|
|
|
else -- Avoid adding this type of complex fluid energy as electrical energy
|
|
energy_type = "void"
|
|
end
|
|
|
|
elseif proto.electric_energy_source_prototype then
|
|
energy_type = "electric"
|
|
energy_drain = proto.electric_energy_source_prototype.drain
|
|
emissions = proto.electric_energy_source_prototype.emissions
|
|
|
|
elseif proto.void_energy_source_prototype then
|
|
energy_type = "void"
|
|
emissions = proto.void_energy_source_prototype.emissions
|
|
end
|
|
|
|
-- Determine fluid input/output channels
|
|
local fluid_channels = {input = 0, output = 0}
|
|
if fluid_burner_prototype then fluid_channels.input = fluid_channels.input - 1 end
|
|
|
|
for _, fluidbox in pairs(proto.fluidbox_prototypes) do
|
|
if fluidbox.production_type == "output" then
|
|
fluid_channels.output = fluid_channels.output + 1
|
|
else -- "input" and "input-output"
|
|
fluid_channels.input = fluid_channels.input + 1
|
|
end
|
|
end
|
|
|
|
local machine = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = sprite,
|
|
category = category,
|
|
ingredient_limit = (proto.ingredient_count or 255),
|
|
fluid_channels = fluid_channels,
|
|
speed = speed,
|
|
energy_type = energy_type,
|
|
energy_usage = energy_usage,
|
|
energy_drain = energy_drain,
|
|
emissions = emissions,
|
|
built_by_item = built_by_item,
|
|
base_productivity = (proto.base_productivity or 0),
|
|
allowed_effects = generator_util.format_allowed_effects(proto.allowed_effects),
|
|
module_limit = (proto.module_inventory_size or 0),
|
|
launch_sequence_time = generator_util.determine_launch_sequence_time(proto),
|
|
burner = burner
|
|
}
|
|
|
|
return machine
|
|
end
|
|
|
|
for _, proto in pairs(game.entity_prototypes) do
|
|
if not proto.has_flag("hidden") and proto.crafting_categories and proto.energy_usage ~= nil
|
|
and not generator_util.is_irrelevant_machine(proto) then
|
|
for category, _ in pairs(proto.crafting_categories) do
|
|
local machine = generate_category_entry(category, proto)
|
|
if machine then insert_prototype(machines, machine, machine.category) end
|
|
end
|
|
|
|
-- Add mining machines
|
|
elseif proto.resource_categories then
|
|
if not proto.has_flag("hidden") and proto.type ~= "character" then
|
|
for category, enabled in pairs(proto.resource_categories) do
|
|
-- Only supports solid mining recipes for now (no oil, etc.)
|
|
if enabled and category ~= "basic-fluid" then
|
|
local machine = generate_category_entry(category, proto)
|
|
if machine then
|
|
machine.mining = true
|
|
insert_prototype(machines, machine, machine.category)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add offshore pumps
|
|
elseif proto.fluid then
|
|
local machine = generate_category_entry(proto.name, proto)
|
|
if machine then
|
|
machine.speed = 1 -- pumping speed included in the recipe product-amount
|
|
machine.category = proto.name -- unique category for every offshore pump
|
|
insert_prototype(machines, machine, machine.category)
|
|
end
|
|
end
|
|
|
|
-- Add machines that produce steam (ie. boilers)
|
|
for _, fluidbox in ipairs(proto.fluidbox_prototypes) do
|
|
if fluidbox.production_type == "output" and fluidbox.filter
|
|
and fluidbox.filter.name == "steam" and proto.target_temperature ~= nil then
|
|
-- Exclude any boilers that use heat as their energy source
|
|
if proto.burner_prototype or proto.electric_energy_source_prototype then
|
|
-- Find the corresponding input fluidbox
|
|
local input_fluidbox = nil ---@type LuaFluidBoxPrototype
|
|
for _, fb in ipairs(proto.fluidbox_prototypes) do
|
|
if fb.production_type == "input-output" or fb.production_type == "input" then
|
|
input_fluidbox = fb
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Add the machine if it has a valid input fluidbox
|
|
if input_fluidbox ~= nil then
|
|
local category = "steam-" .. proto.target_temperature
|
|
local machine = generate_category_entry(category, proto)
|
|
if machine then
|
|
local temp_diff = proto.target_temperature - input_fluidbox.filter.default_temperature
|
|
local energy_per_unit = input_fluidbox.filter.heat_capacity * temp_diff
|
|
machine.speed = machine.energy_usage / energy_per_unit
|
|
|
|
insert_prototype(machines, machine, machine.category)
|
|
|
|
-- Add every boiler to the general steam category (steam without temperature)
|
|
local general_machine = fancytable.deep_copy(machine)
|
|
general_machine.category = "general-steam"
|
|
insert_prototype(machines, general_machine, general_machine.category)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return machines
|
|
end
|
|
|
|
---@param machines NamedPrototypesWithCategory<FPMachinePrototype>
|
|
function generator.machines.second_pass(machines)
|
|
-- Go over all recipes to find unused categories
|
|
local used_category_names = {} ---@type { [string]: boolean }
|
|
for _, recipe_proto in pairs(global.prototypes.recipes) do
|
|
used_category_names[recipe_proto.category] = true
|
|
end
|
|
|
|
for _, machine_category in pairs(machines) do
|
|
if used_category_names[machine_category.name] == nil then
|
|
machines[machine_category.name] = nil
|
|
end
|
|
end
|
|
|
|
-- Filter out burner machines that don't have any valid fuel categories
|
|
for _, machine_category in pairs(machines) do
|
|
for _, machine_proto in pairs(machine_category.members) do
|
|
if machine_proto.energy_type == "burner" then
|
|
local category_found = false
|
|
for fuel_category in pairs(machine_proto.burner.categories) do
|
|
if global.prototypes.fuels[fuel_category] then category_found = true; break end
|
|
end
|
|
if not category_found then remove_prototype(machines, machine_proto.name, machine_category.name) end
|
|
end
|
|
end
|
|
|
|
-- If the category ends up empty because of this, make sure to remove it
|
|
if not next(machine_category.members) then machines[machine_category.name] = nil end
|
|
end
|
|
|
|
|
|
-- Replace built_by_item names with prototype references
|
|
local item_prototypes = global.prototypes.items["item"].members ---@type { [string]: FPItemPrototype }
|
|
for _, machine_category in pairs(machines) do
|
|
for _, machine_proto in pairs(machine_category.members) do
|
|
if machine_proto.built_by_item then
|
|
machine_proto.built_by_item = item_prototypes[machine_proto.built_by_item]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param a FPMachinePrototype
|
|
---@param b FPMachinePrototype
|
|
---@return boolean
|
|
function generator.machines.sorting_function(a, b)
|
|
if a.speed < b.speed then return true
|
|
elseif a.speed > b.speed then return false
|
|
elseif a.module_limit < b.module_limit then return true
|
|
elseif a.module_limit > b.module_limit then return false
|
|
elseif a.energy_usage < b.energy_usage then return true
|
|
elseif a.energy_usage > b.energy_usage then return false end
|
|
return false
|
|
end
|
|
|
|
|
|
---@class FPUnformattedRecipePrototype: FPPrototype
|
|
---@field data_type "recipes"
|
|
---@field category string
|
|
---@field energy double
|
|
---@field emissions_multiplier double
|
|
---@field ingredients FPIngredient[]
|
|
---@field products Product[]
|
|
---@field main_product Product?
|
|
---@field type_counts { ingredients: ItemTypeCounts, products: ItemTypeCounts }
|
|
---@field recycling boolean
|
|
---@field barreling boolean
|
|
---@field enabling_technologies string[]
|
|
---@field use_limitations boolean
|
|
---@field custom boolean
|
|
---@field enabled_from_the_start boolean
|
|
---@field hidden boolean
|
|
---@field order string
|
|
---@field group ItemGroup
|
|
---@field subgroup ItemGroup
|
|
|
|
---@class FPRecipePrototype: FPUnformattedRecipePrototype
|
|
---@field ingredients FormattedRecipeItem[]
|
|
---@field products FormattedRecipeItem[]
|
|
---@field main_product FormattedRecipeItem?
|
|
|
|
---@class FPIngredient: Ingredient
|
|
---@field ignore_productivity boolean
|
|
|
|
-- Returns all standard recipes + custom mining, steam and rocket recipes
|
|
---@return NamedPrototypes<FPRecipePrototype>
|
|
function generator.recipes.generate()
|
|
local recipes = {} ---@type NamedPrototypes<FPRecipePrototype>
|
|
|
|
---@return FPUnformattedRecipePrototype
|
|
local function custom_recipe()
|
|
return {
|
|
custom = true,
|
|
enabled_from_the_start = true,
|
|
hidden = false,
|
|
group = {name="intermediate-products", order="c", valid=true,
|
|
localised_name={"item-group-name.intermediate-products"}},
|
|
type_counts = {},
|
|
enabling_technologies = nil,
|
|
use_limitations = false,
|
|
emissions_multiplier = 1
|
|
}
|
|
end
|
|
|
|
|
|
-- Determine researchable recipes
|
|
local researchable_recipes = {} ---@type { [string]: string[] }
|
|
local tech_filter = {{filter="hidden", invert=true}, {filter="has-effects", mode="and"}}
|
|
for _, tech_proto in pairs(game.get_filtered_technology_prototypes(tech_filter)) do
|
|
for _, effect in pairs(tech_proto.effects) do
|
|
if effect.type == "unlock-recipe" then
|
|
local recipe_name = effect.recipe
|
|
researchable_recipes[recipe_name] = researchable_recipes[recipe_name] or {}
|
|
table.insert(researchable_recipes[recipe_name], tech_proto.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add all standard recipes
|
|
local recipe_filter = {{filter="energy", comparison=">", value=0},
|
|
{filter="energy", comparison="<", value=1e+21, mode="and"}}
|
|
for recipe_name, proto in pairs(game.get_filtered_recipe_prototypes(recipe_filter)) do
|
|
local machine_category = global.prototypes.machines[proto.category] ---@type { [string]: FPMachinePrototype }
|
|
-- Avoid any recipes that have no machine to produce them, or are irrelevant
|
|
if machine_category ~= nil and not generator_util.is_irrelevant_recipe(proto) then
|
|
local recipe = { ---@type FPUnformattedRecipePrototype
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = "recipe/" .. proto.name,
|
|
category = proto.category,
|
|
energy = proto.energy,
|
|
emissions_multiplier = proto.emissions_multiplier,
|
|
ingredients = proto.ingredients,
|
|
products = proto.products,
|
|
main_product = proto.main_product,
|
|
type_counts = {}, -- filled out by format_* below
|
|
recycling = generator_util.is_recycling_recipe(proto),
|
|
barreling = generator_util.is_compacting_recipe(proto),
|
|
enabling_technologies = researchable_recipes[recipe_name], -- can be nil
|
|
use_limitations = true,
|
|
custom = false,
|
|
enabled_from_the_start = proto.enabled,
|
|
hidden = proto.hidden,
|
|
order = proto.order,
|
|
group = generator_util.generate_group_table(proto.group),
|
|
subgroup = generator_util.generate_group_table(proto.subgroup)
|
|
}
|
|
|
|
generator_util.format_recipe_products_and_ingredients(recipe)
|
|
---@cast recipe FPRecipePrototype
|
|
insert_prototype(recipes, recipe, nil)
|
|
end
|
|
end
|
|
|
|
|
|
-- Determine all the items that can be inserted usefully into a rocket silo
|
|
local launch_products_filter = {{filter="has-rocket-launch-products"}}
|
|
local rocket_silo_inputs = {} ---@type LuaItemPrototype[]
|
|
for _, item in pairs(game.get_filtered_item_prototypes(launch_products_filter)) do
|
|
if next(item.rocket_launch_products) then
|
|
table.insert(rocket_silo_inputs, item)
|
|
end
|
|
end
|
|
|
|
-- Localize them here so they don't have to be recreated over and over
|
|
local item_prototypes, recipe_prototypes = game.item_prototypes, game.recipe_prototypes
|
|
|
|
-- Add mining recipes
|
|
for _, proto in pairs(game.entity_prototypes) do
|
|
-- Add all mining recipes. Only supports solids for now.
|
|
if proto.mineable_properties and proto.resource_category then
|
|
local products = proto.mineable_properties.products
|
|
if not products then goto incompatible_proto end
|
|
|
|
local produces_solid = false
|
|
for _, product in pairs(products) do -- detects all solid mining recipes
|
|
if product.type == "item" then produces_solid = true; break end
|
|
end
|
|
if not produces_solid then goto incompatible_proto end
|
|
|
|
if produces_solid then
|
|
local recipe = custom_recipe()
|
|
recipe.name = "impostor-" .. proto.name
|
|
recipe.localised_name = proto.localised_name
|
|
recipe.sprite = products[1].type .. "/" .. products[1].name
|
|
recipe.order = proto.order
|
|
recipe.subgroup = {name="mining", order="y", valid=true}
|
|
recipe.category = proto.resource_category
|
|
recipe.mining = true
|
|
-- Set energy to mining time so the forumla for the machine_count works out
|
|
recipe.energy = proto.mineable_properties.mining_time
|
|
recipe.ingredients = {{type="entity", name=proto.name, amount=1}}
|
|
recipe.products = products
|
|
recipe.main_product = recipe.products[1]
|
|
|
|
-- Add mining fluid, if required
|
|
if proto.mineable_properties.required_fluid then
|
|
table.insert(recipe.ingredients, {
|
|
type = "fluid",
|
|
name = proto.mineable_properties.required_fluid,
|
|
-- fluid_amount is given for a 'set' of mining ops, with a set being 10 ore
|
|
amount = proto.mineable_properties.fluid_amount / 10
|
|
})
|
|
end
|
|
|
|
generator_util.format_recipe_products_and_ingredients(recipe)
|
|
---@cast recipe FPRecipePrototype
|
|
generator_util.add_recipe_tooltip(recipe)
|
|
insert_prototype(recipes, recipe, nil)
|
|
|
|
--else
|
|
-- crude-oil and angels-natural-gas go here (not interested atm)
|
|
end
|
|
|
|
::incompatible_proto::
|
|
|
|
-- Add offshore-pump fluid recipes
|
|
elseif proto.fluid then
|
|
local recipe = custom_recipe()
|
|
recipe.name = "impostor-" .. proto.fluid.name .. "-" .. proto.name
|
|
recipe.localised_name = proto.fluid.localised_name
|
|
recipe.sprite = "fluid/" .. proto.fluid.name
|
|
recipe.order = proto.order
|
|
recipe.subgroup = {name="fluids", order="z", valid=true}
|
|
recipe.category = proto.name -- use proto name so every pump has it's own category
|
|
recipe.energy = 1
|
|
recipe.ingredients = {}
|
|
recipe.products = {{type="fluid", name=proto.fluid.name, amount=(proto.pumping_speed * 60)}}
|
|
recipe.main_product = recipe.products[1]
|
|
|
|
generator_util.format_recipe_products_and_ingredients(recipe)
|
|
---@cast recipe FPRecipePrototype
|
|
generator_util.add_recipe_tooltip(recipe)
|
|
insert_prototype(recipes, recipe, nil)
|
|
|
|
-- Detect all the implicit rocket silo recipes
|
|
elseif proto.rocket_parts_required ~= nil then
|
|
local fixed_recipe = recipe_prototypes[proto.fixed_recipe --[[@as string]]]
|
|
if fixed_recipe ~= nil then
|
|
-- Add recipe for all 'launchable' items
|
|
for _, silo_input in pairs(rocket_silo_inputs) do
|
|
local silo_product = table_size(silo_input.rocket_launch_products) > 1
|
|
and item_prototypes[silo_input.rocket_launch_products[1].name] or silo_input
|
|
|
|
local recipe = custom_recipe()
|
|
recipe.name = "impostor-silo-" .. proto.name .. "-item-" .. silo_input.name
|
|
recipe.localised_name = silo_product.localised_name
|
|
recipe.sprite = "item/" .. silo_product.name
|
|
recipe.category = next(proto.crafting_categories, nil) -- hopefully this stays working
|
|
recipe.energy = fixed_recipe.energy * proto.rocket_parts_required --[[@as number]]
|
|
recipe.subgroup = {name="science-pack", order="g", valid=true}
|
|
recipe.order = "x-silo-" .. proto.order .. "-" .. silo_input.order
|
|
|
|
recipe.ingredients = fixed_recipe.ingredients
|
|
for _, ingredient in pairs(recipe.ingredients) do
|
|
ingredient.amount = ingredient.amount * proto.rocket_parts_required
|
|
end
|
|
table.insert(recipe.ingredients, {type="item", name=silo_input.name,
|
|
amount=1, ignore_productivity=true})
|
|
recipe.products = silo_input.rocket_launch_products
|
|
recipe.main_product = recipe.products[1]
|
|
|
|
generator_util.format_recipe_products_and_ingredients(recipe)
|
|
---@cast recipe FPRecipePrototype
|
|
generator_util.add_recipe_tooltip(recipe)
|
|
insert_prototype(recipes, recipe, nil)
|
|
end
|
|
|
|
-- Modify recipe for all rocket parts so they represent a full launch
|
|
-- This is needed so the launch sequence times can be incorporated correctly
|
|
local rocket_part_recipe = recipes[fixed_recipe.name]
|
|
if rocket_part_recipe then
|
|
generator_util.multiply_recipe(rocket_part_recipe, proto.rocket_parts_required)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add a recipe for producing steam from a boiler
|
|
local existing_recipe_names = {} ---@type { [string]: boolean }
|
|
for _, fluidbox in ipairs(proto.fluidbox_prototypes) do
|
|
if fluidbox.production_type == "output" and fluidbox.filter
|
|
and fluidbox.filter.name == "steam" and proto.target_temperature ~= nil then
|
|
-- Exclude any boilers that use heat or fluid as their energy source
|
|
if proto.burner_prototype or proto.electric_energy_source_prototype then
|
|
local temperature = proto.target_temperature
|
|
local recipe_name = "impostor-steam-" .. temperature
|
|
|
|
-- Prevent duplicate recipes, in case more than one boiler produces the same temperature of steam
|
|
if existing_recipe_names[recipe_name] == nil then
|
|
existing_recipe_names[recipe_name] = true
|
|
|
|
local recipe = custom_recipe()
|
|
recipe.name = recipe_name
|
|
recipe.localised_name = {"fp.fluid_at_temperature", {"fluid-name.steam"},
|
|
temperature, {"fp.unit_celsius"}}
|
|
recipe.sprite = "fluid/steam"
|
|
recipe.category = "steam-" .. temperature
|
|
recipe.order = "z-" .. temperature
|
|
recipe.subgroup = {name="fluids", order="z", valid=true}
|
|
recipe.energy = 1
|
|
recipe.ingredients = {{type="fluid", name="water", amount=60}}
|
|
recipe.products = {{type="fluid", name="steam", amount=60, temperature=temperature}}
|
|
recipe.main_product = recipe.products[1]
|
|
|
|
generator_util.format_recipe_products_and_ingredients(recipe)
|
|
---@cast recipe FPRecipePrototype
|
|
generator_util.add_recipe_tooltip(recipe)
|
|
insert_prototype(recipes, recipe, nil)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add a general steam recipe that works with every boiler
|
|
if game["fluid_prototypes"]["steam"] then -- make sure the steam prototype exists
|
|
local recipe = custom_recipe()
|
|
recipe.name = "fp-general-steam"
|
|
recipe.localised_name = {"fluid-name.steam"}
|
|
recipe.sprite = "fluid/steam"
|
|
recipe.category = "general-steam"
|
|
recipe.order = "z-0"
|
|
recipe.subgroup = {name="fluids", order="z", valid=true}
|
|
recipe.energy = 1
|
|
recipe.ingredients = {{type="fluid", name="water", amount=60}}
|
|
recipe.products = {{type="fluid", name="steam", amount=60}}
|
|
recipe.main_product = recipe.products[1]
|
|
|
|
generator_util.format_recipe_products_and_ingredients(recipe)
|
|
---@cast recipe FPRecipePrototype
|
|
generator_util.add_recipe_tooltip(recipe)
|
|
insert_prototype(recipes, recipe, nil)
|
|
end
|
|
|
|
-- Custom handling for Space Exploration Arcosphere recipes
|
|
local se_split_recipes = {"se-arcosphere-fracture", "se-naquium-processor", "se-naquium-tessaract",
|
|
"se-space-dilation-data", "se-space-fold-data", "se-space-injection-data", "se-space-warp-data"}
|
|
for _, recipe_name in pairs(se_split_recipes) do
|
|
local recipe, alt_recipe = recipes[recipe_name], recipes[recipe_name .. "-alt"]
|
|
if recipe and alt_recipe then
|
|
recipe.custom = true
|
|
generator_util.combine_recipes(recipe, alt_recipe)
|
|
generator_util.multiply_recipe(recipe, 0.5)
|
|
generator_util.add_recipe_tooltip(recipe)
|
|
remove_prototype(recipes, alt_recipe.name, nil)
|
|
end
|
|
end
|
|
|
|
return recipes
|
|
end
|
|
|
|
---@param recipes NamedPrototypes<FPRecipePrototype>
|
|
function generator.recipes.second_pass(recipes)
|
|
local machines = global.prototypes.machines
|
|
-- Check again if all recipes still have a machine to produce them after machine second pass
|
|
for _, recipe in pairs(recipes) do
|
|
if not machines[recipe.category] then
|
|
remove_prototype(recipes, recipe.name, nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
---@class FPItemPrototype: FPPrototypeWithCategory
|
|
---@field data_type "items"
|
|
---@field type "item" | "fluid" | "entity"
|
|
---@field hidden boolean
|
|
---@field stack_size uint?
|
|
---@field ingredient_only boolean
|
|
---@field temperature number
|
|
---@field order string
|
|
---@field group ItemGroup
|
|
---@field subgroup ItemGroup
|
|
|
|
---@class RelevantItem
|
|
---@field proto FormattedRecipeItem
|
|
---@field is_product boolean
|
|
---@field is_rocket_part boolean
|
|
---@field temperature number?
|
|
|
|
---@alias RelevantItems { [ItemType]: { [ItemName]: RelevantItem } }
|
|
|
|
-- Returns all relevant items and fluids
|
|
---@return NamedPrototypesWithCategory<FPItemPrototype>
|
|
function generator.items.generate()
|
|
local items = {} ---@type NamedPrototypesWithCategory<FPItemPrototype>
|
|
|
|
---@param table RelevantItems
|
|
---@param item RelevantItem
|
|
local function add_item(table, item)
|
|
local type = item.proto.type
|
|
local name = item.proto.name
|
|
table[type] = table[type] or {}
|
|
table[type][name] = table[type][name] or {}
|
|
local item_details = table[type][name]
|
|
-- Determine whether this item is used as a product at least once
|
|
item_details.is_product = item_details.is_product or item.is_product
|
|
item_details.is_rocket_part = item_details.is_rocket_part or item.is_rocket_part
|
|
item_details.temperature = item.proto.temperature
|
|
end
|
|
|
|
-- Create a table containing every item that is either a product or an ingredient to at least one recipe
|
|
local relevant_items = {} ---@type RelevantItems
|
|
for _, recipe_proto in pairs(global.prototypes.recipes) do
|
|
for _, product in pairs(recipe_proto.products) do
|
|
local is_rocket_part = (recipe_proto.category == "rocket-building")
|
|
add_item(relevant_items, {proto=product, is_product=true, is_rocket_part=is_rocket_part})
|
|
end
|
|
for _, ingredient in pairs(recipe_proto.ingredients) do
|
|
add_item(relevant_items, {proto=ingredient, is_product=false, is_rocket_part=false})
|
|
end
|
|
end
|
|
|
|
-- Add all standard items
|
|
for type, item_table in pairs(relevant_items) do
|
|
for item_name, item_details in pairs(item_table) do
|
|
local proto_name = generator_util.format_temperature_name(item_details, item_name)
|
|
local proto = game[type .. "_prototypes"][proto_name] ---@type LuaItemPrototype | LuaFluidPrototype
|
|
if proto == nil then goto skip_item end
|
|
|
|
local localised_name = generator_util.format_temperature_localised_name(item_details, proto)
|
|
local stack_size = (type == "item") and proto.stack_size or nil
|
|
local order = (item_details.temperature) and (proto.order .. item_details.temperature) or proto.order
|
|
|
|
local hidden = false -- "entity" types are never hidden
|
|
if type == "item" then hidden = proto.has_flag("hidden")
|
|
elseif type == "fluid" then hidden = proto.hidden end
|
|
if item_details.is_rocket_part then hidden = false end
|
|
|
|
local item = {
|
|
name = item_name,
|
|
localised_name = localised_name,
|
|
sprite = type .. "/" .. proto.name,
|
|
type = type,
|
|
hidden = hidden,
|
|
stack_size = stack_size,
|
|
ingredient_only = not item_details.is_product,
|
|
temperature = item_details.temperature,
|
|
order = order,
|
|
group = generator_util.generate_group_table(proto.group),
|
|
subgroup = generator_util.generate_group_table(proto.subgroup)
|
|
}
|
|
|
|
insert_prototype(items, item, item.type)
|
|
|
|
::skip_item::
|
|
end
|
|
end
|
|
|
|
return items
|
|
end
|
|
|
|
|
|
---@class FPFuelPrototype: FPPrototypeWithCategory
|
|
---@field data_type "fuels"
|
|
---@field type "item" | "fluid"
|
|
---@field category string | "fluid-fuel"
|
|
---@field fuel_value float
|
|
---@field stack_size uint?
|
|
---@field emissions_multiplier double
|
|
|
|
-- Generates a table containing all fuels that can be used in a burner
|
|
---@return NamedPrototypesWithCategory<FPFuelPrototype>
|
|
function generator.fuels.generate()
|
|
local fuels = {} ---@type NamedPrototypesWithCategory<FPFuelPrototype>
|
|
|
|
-- Determine all the fuel categories that the machine prototypes use
|
|
local used_fuel_categories = {} ---@type { [string]: boolean}
|
|
for _, machine_category in pairs(global.prototypes.machines) do
|
|
for _, machine_proto in pairs(machine_category.members) do
|
|
if machine_proto.burner then
|
|
for category_name, _ in pairs(machine_proto.burner.categories) do
|
|
used_fuel_categories[category_name] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local fuel_filter = {{filter="fuel-value", comparison=">", value=0},
|
|
{filter="fuel-value", comparison="<", value=1e+21, mode="and"}}
|
|
|
|
-- Add solid fuels
|
|
local item_list = global.prototypes.items["item"].members ---@type NamedPrototypesWithCategory<FPItemPrototype>
|
|
local item_fuel_filter = fancytable.shallow_copy(fuel_filter)
|
|
table.insert(item_fuel_filter, {filter="flag", flag="hidden", invert=true, mode="and"})
|
|
|
|
for _, proto in pairs(game.get_filtered_item_prototypes(item_fuel_filter)) do
|
|
-- Only use fuels that were actually detected/accepted to be items and find use in at least one machine
|
|
if item_list[proto.name] and used_fuel_categories[proto.fuel_category] ~= nil then
|
|
local fuel = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = "item/" .. proto.name,
|
|
type = "item",
|
|
category = proto.fuel_category,
|
|
fuel_value = proto.fuel_value,
|
|
stack_size = proto.stack_size,
|
|
emissions_multiplier = proto.fuel_emissions_multiplier
|
|
}
|
|
insert_prototype(fuels, fuel, fuel.category)
|
|
end
|
|
end
|
|
|
|
-- Add liquid fuels
|
|
local fluid_list = global.prototypes.items["fluid"].members ---@type NamedPrototypesWithCategory<FPItemPrototype>
|
|
local fluid_fuel_filter = fancytable.shallow_copy(fuel_filter)
|
|
table.insert(fluid_fuel_filter, {filter="hidden", invert=true, mode="and"})
|
|
|
|
for _, proto in pairs(game.get_filtered_fluid_prototypes(fluid_fuel_filter)) do
|
|
-- Only use fuels that have actually been detected/accepted as fluids
|
|
if fluid_list[proto.name] then
|
|
local fuel = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = "fluid/" .. proto.name,
|
|
type = "fluid",
|
|
category = "fluid-fuel",
|
|
fuel_value = proto.fuel_value,
|
|
stack_size = nil,
|
|
emissions_multiplier = proto.emissions_multiplier
|
|
}
|
|
insert_prototype(fuels, fuel, fuel.category)
|
|
end
|
|
end
|
|
|
|
return fuels
|
|
end
|
|
|
|
---@param a FPFuelPrototype
|
|
---@param b FPFuelPrototype
|
|
---@return boolean
|
|
function generator.fuels.sorting_function(a, b)
|
|
if a.fuel_value < b.fuel_value then return true
|
|
elseif a.fuel_value > b.fuel_value then return false
|
|
elseif a.emissions_multiplier < b.emissions_multiplier then return true
|
|
elseif a.emissions_multiplier > b.emissions_multiplier then return false end
|
|
return false
|
|
end
|
|
|
|
|
|
---@class FPBeltPrototype: FPPrototype
|
|
---@field data_type "belts"
|
|
---@field rich_text string
|
|
---@field throughput double
|
|
|
|
-- Generates a table containing all available transport belts
|
|
---@return NamedPrototypes<FPBeltPrototype>
|
|
function generator.belts.generate()
|
|
local belts = {} ---@type NamedPrototypes<FPBeltPrototype>
|
|
|
|
local belt_filter = {{filter="type", type="transport-belt"},
|
|
{filter="flag", flag="hidden", invert=true, mode="and"}}
|
|
for _, proto in pairs(game.get_filtered_entity_prototypes(belt_filter)) do
|
|
local sprite = generator_util.determine_entity_sprite(proto)
|
|
if sprite ~= nil then
|
|
local belt = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = sprite,
|
|
rich_text = "[entity=" .. proto.name .. "]",
|
|
throughput = proto.belt_speed * 480
|
|
}
|
|
insert_prototype(belts, belt, nil)
|
|
end
|
|
end
|
|
|
|
return belts
|
|
end
|
|
|
|
---@param a FPBeltPrototype
|
|
---@param b FPBeltPrototype
|
|
---@return boolean
|
|
function generator.belts.sorting_function(a, b)
|
|
if a.throughput < b.throughput then return true
|
|
elseif a.throughput > b.throughput then return false end
|
|
return false
|
|
end
|
|
|
|
|
|
---@class FPWagonPrototype: FPPrototypeWithCategory
|
|
---@field data_type "wagons"
|
|
---@field rich_text string
|
|
---@field category "cargo-wagon" | "fluid-wagon"
|
|
---@field storage number
|
|
|
|
-- Generates a table containing all available cargo and fluid wagons
|
|
---@return NamedPrototypesWithCategory<FPWagonPrototype>
|
|
function generator.wagons.generate()
|
|
local wagons = {} ---@type NamedPrototypesWithCategory<FPWagonPrototype>
|
|
|
|
-- Add cargo wagons
|
|
local cargo_wagon_filter = {{filter="type", type="cargo-wagon"},
|
|
{filter="flag", flag="hidden", invert=true, mode="and"}}
|
|
for _, proto in pairs(game.get_filtered_entity_prototypes(cargo_wagon_filter)) do
|
|
local inventory_size = proto.get_inventory_size(defines.inventory.cargo_wagon)
|
|
if inventory_size > 0 then
|
|
local wagon = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = generator_util.determine_entity_sprite(proto),
|
|
rich_text = "[entity=" .. proto.name .. "]",
|
|
category = "cargo-wagon",
|
|
storage = inventory_size
|
|
}
|
|
insert_prototype(wagons, wagon, wagon.category)
|
|
end
|
|
end
|
|
|
|
-- Add fluid wagons
|
|
local fluid_wagon_filter = {{filter="type", type="fluid-wagon"},
|
|
{filter="flag", flag="hidden", invert=true, mode="and"}}
|
|
for _, proto in pairs(game.get_filtered_entity_prototypes(fluid_wagon_filter)) do
|
|
if proto.fluid_capacity > 0 then
|
|
local wagon = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = generator_util.determine_entity_sprite(proto),
|
|
rich_text = "[entity=" .. proto.name .. "]",
|
|
category = "fluid-wagon",
|
|
storage = proto.fluid_capacity
|
|
}
|
|
insert_prototype(wagons, wagon, wagon.category)
|
|
end
|
|
end
|
|
|
|
return wagons
|
|
end
|
|
|
|
---@param a FPWagonPrototype
|
|
---@param b FPWagonPrototype
|
|
---@return boolean
|
|
function generator.wagons.sorting_function(a, b)
|
|
if a.storage < b.storage then return true
|
|
elseif a.storage > b.storage then return false end
|
|
return false
|
|
end
|
|
|
|
|
|
---@class FPModulePrototype: FPPrototypeWithCategory
|
|
---@field data_type "modules"
|
|
---@field category string
|
|
---@field tier uint
|
|
---@field effects ModuleEffects
|
|
---@field limitations { [string]: true }
|
|
|
|
-- Generates a table containing all available modules
|
|
---@return NamedPrototypesWithCategory<FPModulePrototype>
|
|
function generator.modules.generate()
|
|
local modules = {} ---@type NamedPrototypesWithCategory<FPModulePrototype>
|
|
|
|
local module_filter = {{filter="type", type="module"}, {filter="flag", flag="hidden", invert=true, mode="and"}}
|
|
for _, proto in pairs(game.get_filtered_item_prototypes(module_filter)) do
|
|
local limitations = {} ---@type ModuleLimitations
|
|
for _, recipe_name in pairs(proto.limitations) do limitations[recipe_name] = true end
|
|
|
|
local sprite = "item/" .. proto.name
|
|
if game.is_valid_sprite_path(sprite) then
|
|
local module = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = sprite,
|
|
category = proto.category,
|
|
tier = proto.tier,
|
|
effects = proto.module_effects or {},
|
|
limitations = limitations
|
|
}
|
|
insert_prototype(modules, module, module.category)
|
|
end
|
|
end
|
|
|
|
return modules
|
|
end
|
|
|
|
|
|
---@class FPBeaconPrototype: FPPrototype
|
|
---@field data_type "beacons"
|
|
---@field category "fp_beacon"
|
|
---@field built_by_item FPItemPrototype
|
|
---@field allowed_effects AllowedEffects
|
|
---@field module_limit uint
|
|
---@field effectivity double
|
|
---@field energy_usage double
|
|
|
|
-- Generates a table containing all available beacons
|
|
---@return NamedPrototypes<FPBeaconPrototype>
|
|
function generator.beacons.generate()
|
|
local beacons = {} ---@type NamedPrototypes<FPBeaconPrototype>
|
|
|
|
---@type NamedPrototypesWithCategory<FPItemPrototype>
|
|
local item_prototypes = global.prototypes.items["item"].members
|
|
|
|
local beacon_filter = {{filter="type", type="beacon"}, {filter="flag", flag="hidden", invert=true, mode="and"}}
|
|
for _, proto in pairs(game.get_filtered_entity_prototypes(beacon_filter)) do
|
|
local sprite = generator_util.determine_entity_sprite(proto)
|
|
if sprite ~= nil and proto.module_inventory_size and proto.distribution_effectivity > 0 then
|
|
-- Beacons can refer to the actual item prototype right away because they are built after items are
|
|
local items_to_place_this = proto.items_to_place_this
|
|
local built_by_item = (items_to_place_this) and item_prototypes[items_to_place_this[1].name] or nil
|
|
|
|
local beacon = {
|
|
name = proto.name,
|
|
localised_name = proto.localised_name,
|
|
sprite = sprite,
|
|
category = "fp_beacon", -- custom category to be similar to machines
|
|
built_by_item = built_by_item,
|
|
allowed_effects = generator_util.format_allowed_effects(proto.allowed_effects),
|
|
module_limit = proto.module_inventory_size,
|
|
effectivity = proto.distribution_effectivity,
|
|
energy_usage = proto.energy_usage or proto.max_energy_usage or 0
|
|
}
|
|
insert_prototype(beacons, beacon, nil)
|
|
end
|
|
end
|
|
|
|
return beacons
|
|
end
|
|
|
|
---@param a FPBeaconPrototype
|
|
---@param b FPBeaconPrototype
|
|
---@return boolean
|
|
function generator.beacons.sorting_function(a, b)
|
|
if a.module_limit < b.module_limit then return true
|
|
elseif a.module_limit > b.module_limit then return false
|
|
elseif a.effectivity < b.effectivity then return true
|
|
elseif a.effectivity > b.effectivity then return false
|
|
elseif a.energy_usage < b.energy_usage then return true
|
|
elseif a.energy_usage > b.energy_usage then return false end
|
|
return false
|
|
end
|
|
|
|
|
|
return generator
|