Первый фикс

Пачки некоторых позиций увеличены
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,976 @@
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

View File

@@ -0,0 +1,456 @@
local generator_util = {}
-- ** LOCAL UTIL **
---@class FormattedRecipeItem
---@field name string
---@field type string
---@field amount number
---@field proddable_amount number?
---@field temperature number?
---@field ignore_productivity boolean?
---@alias IndexedItemList { [ItemType]: { [ItemName]: { index: number, item: FormattedRecipeItem } } }
---@alias ItemList { [ItemType]: { [ItemName]: FormattedRecipeItem } }
---@alias ItemTypeCounts { items: number, fluids: number }
-- Determines the actual amount of items that a recipe product or ingredient equates to
---@param base_item Ingredient | Product
---@param type "ingredient" | "product"
---@return FormattedRecipeItem
local function generate_formatted_item(base_item, type)
local base_amount = 0
if base_item.amount_max ~= nil and base_item.amount_min ~= nil then
base_amount = (base_item.amount_max + base_item.amount_min) / 2
else
base_amount = base_item.amount
end
local probability = (base_item.probability or 1)
local proddable_amount = (type == "product")
and (base_amount - (base_item.catalyst_amount or 0)) * probability or nil
-- This will probably screw up the main_product detection down the line
if base_item.temperature ~= nil then
base_item.name = base_item.name .. "-" .. base_item.temperature
end
return {
name = base_item.name,
type = base_item.type,
amount = (base_amount * probability),
proddable_amount = proddable_amount,
temperature = base_item.temperature
}
end
-- Combines items that occur more than once into one entry
---@param item_list FormattedRecipeItem[]
local function combine_identical_products(item_list)
local touched_items = {item = {}, fluid = {}, entity = {}} ---@type ItemList
for index=#item_list, 1, -1 do
local item = item_list[index]
if item.temperature == nil then -- don't care to deal with temperature crap
local touched_item = touched_items[item.type][item.name]
if touched_item ~= nil then
touched_item.amount = touched_item.amount + item.amount
if touched_item.proddable_amount then
touched_item.proddable_amount = touched_item.proddable_amount + item.proddable_amount
end
-- Using the table.remove function to preserve array-format
table.remove(item_list, index)
else
touched_items[item.type][item.name] = item
end
end
end
end
---@param item_list FormattedRecipeItem[]
---@return IndexedItemList
local function create_type_indexed_list(item_list)
local indexed_list = {item = {}, fluid = {}, entity = {}} ---@type IndexedItemList
for index, item in pairs(item_list) do
indexed_list[item.type][item.name] = {index = index, item = fancytable.shallow_copy(item)}
end
return indexed_list
end
---@param indexed_items IndexedItemList
---@return ItemTypeCounts
local function determine_item_type_counts(indexed_items)
return {
items = table_size(indexed_items.item),
fluids = table_size(indexed_items.fluid)
}
end
-- ** TOP LEVEL **
-- Formats the products/ingredients of a recipe for more convenient use
---@param recipe_proto FPUnformattedRecipePrototype
function generator_util.format_recipe_products_and_ingredients(recipe_proto)
local ingredients = {} ---@type FormattedRecipeItem[]
for _, base_ingredient in pairs(recipe_proto.ingredients) do
local formatted_ingredient = generate_formatted_item(base_ingredient, "ingredient")
if formatted_ingredient.amount > 0 then
-- Productivity applies to all ingredients by default, some exceptions apply (ex. satellite)
-- Also add proddable_amount so productivity bonus can be un-applied later in the model
if base_ingredient.ignore_productivity then
formatted_ingredient.ignore_productivity = true
formatted_ingredient.proddable_amount = formatted_ingredient.amount
end
table.insert(ingredients, formatted_ingredient)
end
end
local indexed_ingredients = create_type_indexed_list(ingredients)
recipe_proto.type_counts.ingredients = determine_item_type_counts(indexed_ingredients)
local products = {} ---@type FormattedRecipeItem[]
for _, base_product in pairs(recipe_proto.products) do
local formatted_product = generate_formatted_item(base_product, "product")
if formatted_product.amount > 0 then
table.insert(products, formatted_product)
-- Update the main product as well, if present
if recipe_proto.main_product ~= nil
and formatted_product.type == recipe_proto.main_product.type
and formatted_product.name == recipe_proto.main_product.name then
recipe_proto.main_product = formatted_product --[[@as Product]]
end
end
end
combine_identical_products(products) -- only needed products, ingredients can't have duplicates
local indexed_products = create_type_indexed_list(products)
recipe_proto.type_counts.products = determine_item_type_counts(indexed_products)
-- Reduce item amounts for items that are both an ingredient and a product
for _, items_of_type in pairs(indexed_ingredients) do
for _, ingredient in pairs(items_of_type) do
local peer_product = indexed_products[ingredient.item.type][ingredient.item.name]
if peer_product then
local difference = ingredient.item.amount - peer_product.item.amount
if difference < 0 then
ingredients[ingredient.index].amount = nil
products[peer_product.index].amount = -difference
elseif difference > 0 then
ingredients[ingredient.index].amount = difference
products[peer_product.index].amount = nil
else
ingredients[ingredient.index].amount = nil
products[peer_product.index].amount = nil
end
end
end
end
-- Remove items after the fact so the iteration above doesn't break
for _, item_table in pairs{ingredients, products} do
for i = #item_table, 1, -1 do
if item_table[i].amount == nil then table.remove(item_table, i) end
end
end
recipe_proto.ingredients = ingredients
recipe_proto.products = products
end
-- Multiplies recipe products and ingredients by the given amount
---@param recipe_proto FPRecipePrototype
---@param factor number
function generator_util.multiply_recipe(recipe_proto, factor)
---@param item_list FormattedRecipeItem[]
local function multiply_items(item_list)
for _, item in pairs(item_list) do
item.amount = item.amount * factor
if item.proddable_amount ~= nil then
item.proddable_amount = item.proddable_amount * factor
end
end
end
multiply_items(recipe_proto.products)
multiply_items(recipe_proto.ingredients)
recipe_proto.energy = recipe_proto.energy * factor
end
-- Adds the additional proto's ingredients, products and energy to the main proto
---@param main_proto FPRecipePrototype
---@param additional_proto FPRecipePrototype
function generator_util.combine_recipes(main_proto, additional_proto)
---@param item_category "products" | "ingredients"
local function add_items_to_main_proto(item_category)
local additional_items = additional_proto[item_category] ---@type FormattedRecipeItem[]
for _, item in pairs(additional_items) do
table.insert(main_proto[item_category], item)
end
combine_identical_products(main_proto[item_category])
end
add_items_to_main_proto("products")
add_items_to_main_proto("ingredients")
main_proto.energy = main_proto.energy + additional_proto.energy
end
-- Active mods table needed for the funtions below
local active_mods = script.active_mods
-- Determines whether this recipe is a recycling one or not
local recycling_recipe_mods = {
["IndustrialRevolution"] = {"^scrap%-.*"},
["space-exploration"] = {"^se%-recycle%-.*"},
["angelspetrochem"] = {"^converter%-.*"},
["reverse-factory"] = {"^rf%-.*"},
["ZRecycling"] = {"^dry411srev%-.*"}
}
local active_recycling_recipe_mods = {} ---@type string[]
for modname, patterns in pairs(recycling_recipe_mods) do
for _, pattern in pairs(patterns) do
if active_mods[modname] then
table.insert(active_recycling_recipe_mods, pattern)
end
end
end
---@param proto LuaRecipePrototype
---@return boolean
function generator_util.is_recycling_recipe(proto)
for _, pattern in pairs(active_recycling_recipe_mods) do
if string.match(proto.name, pattern) then return true end
end
return false
end
-- Determines whether the given recipe is a barreling or stacking one
local compacting_recipe_mods = {
["base"] = {"^fill%-.*", "^empty%-.*"},
["deadlock-beltboxes-loaders"] = {"^deadlock%-stacks%-.*", "^deadlock%-packrecipe%-.*",
"^deadlock%-unpackrecipe%-.*"},
["DeadlockCrating"] = {"^deadlock%-packrecipe%-.*", "^deadlock%-unpackrecipe%-.*"},
["IntermodalContainers"] = {"^ic%-load%-.*", "^ic%-unload%-.*"},
["space-exploration"] = {"^se%-delivery%-cannon%-pack%-.*"},
["Satisfactorio"] = {"^packaged%-.*", "^unpack%-.*"}
}
local active_compacting_recipe_mods = {} ---@type string[]
for modname, patterns in pairs(compacting_recipe_mods) do
for _, pattern in pairs(patterns) do
if active_mods[modname] then
table.insert(active_compacting_recipe_mods, pattern)
end
end
end
---@param proto LuaRecipePrototype
---@return boolean
function generator_util.is_compacting_recipe(proto)
for _, pattern in pairs(active_compacting_recipe_mods) do
if string.match(proto.name, pattern) then return true end
end
return false
end
-- Determines whether this recipe is irrelevant or not and should thus be excluded
local irrelevant_recipe_categories = {
["Transport_Drones"] = {"transport-drone-request", "transport-fluid-request"},
["Mining_Drones"] = {"mining-depot"},
["Deep_Storage_Unit"] = {"deep-storage-item", "deep-storage-fluid",
"deep-storage-item-big", "deep-storage-fluid-big",
"deep-storage-item-mk2/3", "deep-storage-fluid-mk2/3"},
["Satisfactorio"] = {"craft-bench", "equipment", "awesome-shop",
"resource-scanner", "object-scanner", "building",
"hub-progressing", "space-elevator", "mam"}
}
local irrelevant_recipe_categories_lookup = {} ---@type { [string] : true }
for mod, categories in pairs(irrelevant_recipe_categories) do
for _, category in pairs(categories) do
if active_mods[mod] then
irrelevant_recipe_categories_lookup[category] = true
end
end
end
---@param recipe LuaRecipePrototype
---@return boolean
function generator_util.is_irrelevant_recipe(recipe)
return irrelevant_recipe_categories_lookup[recipe.category]
end
-- Determines whether this machine is irrelevant or not and should thus be excluded
local irrelevant_machine_mods = {
["GhostOnWater"] = {"waterGhost%-.*"}
}
local irrelevant_machines_lookup = {} ---@type string[]
for modname, patterns in pairs(irrelevant_machine_mods) do
for _, pattern in pairs(patterns) do
if active_mods[modname] then
table.insert(irrelevant_machines_lookup, pattern)
end
end
end
---@param proto LuaEntityPrototype
---@return boolean
function generator_util.is_irrelevant_machine(proto)
for _, pattern in pairs(irrelevant_machines_lookup) do
if string.match(proto.name, pattern) then return true end
end
return false
end
-- Finds a sprite for the given entity prototype
---@param proto LuaEntityPrototype
---@return SpritePath | nil
function generator_util.determine_entity_sprite(proto)
local entity_sprite = "entity/" .. proto.name ---@type SpritePath
if game.is_valid_sprite_path(entity_sprite) then
return entity_sprite
end
local items_to_place_this = proto.items_to_place_this
if items_to_place_this and next(items_to_place_this) then
local item_sprite = "item/" .. items_to_place_this[1].name ---@type SpritePath
if game.is_valid_sprite_path(item_sprite) then
return item_sprite
end
end
return nil
end
-- Determines how long a rocket takes to launch for the given rocket silo prototype
-- These stages mirror the in-game progression and timing exactly. Most steps take an additional tick (+1)
-- due to how the game code is written. If one stage is completed, you can only progress to the next one
-- in the next tick. No stages can be skipped, meaning a minimal sequence time is around 10 ticks long.
---@param silo_proto LuaEntityPrototype
---@return number? launch_sequence_time
function generator_util.determine_launch_sequence_time(silo_proto)
local rocket_proto = silo_proto.rocket_entity_prototype
if not rocket_proto then return nil end -- meaning this isn't a rocket silo proto
local rocket_flight_threshold = 0.5 -- hardcoded in the game files
local launch_steps = {
lights_blinking_open = (1 / silo_proto.light_blinking_speed) + 1,
doors_opening = (1 / silo_proto.door_opening_speed) + 1,
doors_opened = silo_proto.rocket_rising_delay + 1,
rocket_rising = (1 / rocket_proto.rising_speed) + 1,
rocket_ready = 14, -- estimate for satellite insertion delay
launch_started = silo_proto.launch_wait_time + 1,
engine_starting = (1 / rocket_proto.engine_starting_speed) + 1,
-- This calculates a fractional amount of ticks. Also, math.log(x) calculates the natural logarithm
rocket_flying = math.log(1 + rocket_flight_threshold * rocket_proto.flying_acceleration
/ rocket_proto.flying_speed) / math.log(1 + rocket_proto.flying_acceleration),
lights_blinking_close = (1 / silo_proto.light_blinking_speed) + 1,
doors_closing = (1 / silo_proto.door_opening_speed) + 1
}
local total_ticks = 0
for _, ticks_taken in pairs(launch_steps) do
total_ticks = total_ticks + ticks_taken
end
return (total_ticks / 60) -- retured value is in seconds
end
-- Returns nil if no effect is true, returns the effects otherwise
---@param allowed_effects AllowedEffects
---@return AllowedEffects? allowed_effects
function generator_util.format_allowed_effects(allowed_effects)
if allowed_effects == nil then return nil end
for _, allowed in pairs(allowed_effects) do
if allowed == true then return allowed_effects end
end
return nil -- all effects are false
end
-- Returns the appropriate prototype name for the given item, incorporating temperature
---@param item FormattedRecipeItem | RelevantItem
---@param name string
---@return string
function generator_util.format_temperature_name(item, name)
-- Optionally two dashes to account for negative temperatures
return (item.temperature) and string.gsub(name, "%-+[0-9]+$", "") or name
end
-- Returns the appropriate localised string for the given item, incorporating temperature
---@param item FormattedRecipeItem | RelevantItem
---@param proto LuaItemPrototype | LuaFluidPrototype
---@return LocalisedString
function generator_util.format_temperature_localised_name(item, proto)
if item.temperature then
return {"", proto.localised_name, " (", item.temperature, " ", {"fp.unit_celsius"}, ")"}
else
return proto.localised_name
end
end
-- Adds the tooltip for the given recipe
---@param recipe FPRecipePrototype
function generator_util.add_recipe_tooltip(recipe)
local tooltip = {"", {"fp.tt_title", recipe.localised_name}} ---@type LocalisedString
local current_table, next_index = tooltip, 3
if recipe.energy ~= nil then
current_table, next_index = util.build_localised_string(
{"", "\n ", {"fp.crafting_time"}, ": ", recipe.energy}, current_table, next_index)
end
for _, item_type in ipairs{"ingredients", "products"} do
local locale_key = (item_type == "ingredients") and "fp.pu_ingredient" or "fp.pu_product"
current_table, next_index = util.build_localised_string(
{"", "\n ", {locale_key, 2}, ":"}, current_table, next_index)
if not next(recipe[item_type]) then
current_table, next_index = util.build_localised_string({
"\n ", {"fp.none"}}, current_table, next_index)
else
local items = recipe[item_type] ---@type FormattedRecipeItem[]
for _, item in ipairs(items) do
local name = generator_util.format_temperature_name(item, item.name)
local proto = game[item.type .. "_prototypes"][name] ---@type LuaItemPrototype | LuaFluidPrototype
local localised_name = generator_util.format_temperature_localised_name(item, proto)
current_table, next_index = util.build_localised_string({("\n " .. "[" .. item.type .. "="
.. name .. "] " .. item.amount .. "x "), localised_name}, current_table, next_index)
end
end
end
recipe.tooltip = tooltip
end
---@class ItemGroup
---@field name string
---@field localised_name LocalisedString
---@field order string
---@field valid boolean
-- Generates a table imitating LuaGroup to avoid lua-cpp bridging
---@param group LuaGroup
---@return ItemGroup group_table
function generator_util.generate_group_table(group)
return {name=group.name, localised_name=group.localised_name, order=group.order, valid=true}
end
return generator_util

View File

@@ -0,0 +1,263 @@
-- The loader contains the code that runs on_load, pre-caching some data structures that are needed later
local loader = {}
---@alias RecipeMap { [ItemCategoryID]: { [ItemID]: { [RecipeID]: true } } }
---@alias ItemCategoryID integer
---@alias ItemID integer
---@alias RecipeID integer
---@alias ModuleMap { [string]: FPModulePrototype }
-- ** LOCAL UTIL **
-- Returns a list of recipe groups in their proper order
---@return ItemGroup[]
local function ordered_recipe_groups()
-- Make a dict with all recipe groups
local group_dict = {} ---@type { [string]: ItemGroup }
for _, recipe in pairs(global.prototypes.recipes) do
if group_dict[recipe.group.name] == nil then
group_dict[recipe.group.name] = recipe.group
end
end
-- Invert it
local groups = {} ---@type ItemGroup[]
for _, group in pairs(group_dict) do
table.insert(groups, group)
end
-- Sort it
---@param a ItemGroup
---@param b ItemGroup
---@return boolean
local function sorting_function(a, b)
if a.order < b.order then return true
elseif a.order > b.order then return false end
return false
end
table.sort(groups, sorting_function)
return groups
end
-- Maps all items to the recipes that produce or consume them ([item_type][item_name] = {[recipe_id] = true}
---@param item_type "products" | "ingredients"
---@return RecipeMap
local function recipe_map_from(item_type)
local map = {} ---@type RecipeMap
for _, recipe in pairs(global.prototypes.recipes) do
for _, item in ipairs(recipe[item_type] --[[@as FormattedRecipeItem[] ]]) do
local item_proto = prototyper.util.find_prototype("items", item.name, item.type) ---@cast item_proto -nil
map[item_proto.category_id] = map[item_proto.category_id] or {}
map[item_proto.category_id][item_proto.id] = map[item_proto.category_id][item_proto.id] or {}
map[item_proto.category_id][item_proto.id][recipe.id] = true
end
end
return map
end
-- Generates a list of all items, sorted for display in the picker
---@return FPItemPrototype[]
local function sorted_items()
local items = {}
for _, type in pairs{"item", "fluid"} do
for _, item in pairs(PROTOTYPE_MAPS.items[type].members) do
-- Silly checks needed here for migration purposes
if item.group.valid and item.subgroup.valid then table.insert(items, item) end
end
end
-- Sorts the objects according to their group, subgroup and order
---@param a FPItemPrototype
---@param b FPItemPrototype
---@return boolean
local function sorting_function(a, b)
if a.group.order < b.group.order then return true
elseif a.group.order > b.group.order then return false
elseif a.subgroup.order < b.subgroup.order then return true
elseif a.subgroup.order > b.subgroup.order then return false
elseif a.order < b.order then return true
elseif a.order > b.order then return false end
return false
end
table.sort(items, sorting_function)
return items
end
-- Generates a table mapping modules to their prototype by name
---@return ModuleMap
local function module_name_map()
local map = {} ---@type ModuleMap
for _, category in pairs(global.prototypes.modules) do
for _, module in pairs(category.members) do
map[module.name] = module
end
end
return map
end
local attribute_generators = {} ---@type { [string]: function }
---@param belt FPBeltPrototype
---@return LocalisedString[]
function attribute_generators.belts(belt)
local throughput_string = {"", belt.throughput .. " ", {"fp.pl_item", 2}, "/", {"fp.unit_second"}}
return {"fp.attribute_line", {"fp.throughput"}, throughput_string}
end
---@param beacon FPBeaconPrototype
---@return LocalisedString[]
function attribute_generators.beacons(beacon)
return {"", {"fp.attribute_line", {"fp.module_slots"}, beacon.module_limit},
{"fp.attribute_line", {"fp.effectivity"}, (beacon.effectivity * 100) .. "%"},
{"fp.attribute_line", {"fp.energy_consumption"}, util.format.SI_value(beacon.energy_usage * 60, "W", 3)}}
end
---@param wagon FPWagonPrototype
---@return LocalisedString[]
function attribute_generators.wagons(wagon)
local storage_unit = (wagon.category == "cargo-wagon") and {"fp.pl_stack", wagon.storage} or {"fp.l_fluid"}
return {"fp.attribute_line", {"fp.storage"}, {"", util.format.number(wagon.storage, 3) .. " ", storage_unit}}
end
---@param fuel FPFuelPrototype
---@return LocalisedString[]
function attribute_generators.fuels(fuel)
return {"", {"fp.attribute_line", {"fp.fuel_value"}, util.format.SI_value(fuel.fuel_value, "J", 3)},
{"fp.attribute_line", {"fp.emissions_multiplier"}, fuel.emissions_multiplier}}
end
---@param machine FPMachinePrototype
---@return LocalisedString[]
function attribute_generators.machines(machine)
local pollution = machine.energy_usage * (machine.emissions * 60) * 60
return {"", {"fp.attribute_line", {"fp.crafting_speed"}, util.format.number(machine.speed, 3)},
{"fp.attribute_line", {"fp.energy_consumption"}, util.format.SI_value(machine.energy_usage * 60, "W", 3)},
{"fp.attribute_line", {"fp.pollution"}, {"", util.format.SI_value(pollution, "P/m", 3)}},
{"fp.attribute_line", {"fp.module_slots"}, machine.module_limit}}
end
---@alias PrototypeAttributes { [DataType]: { [integer]: LocalisedString } }
---@alias PrototypeAttributesWithCategory { [DataType]: { [integer]: { [integer]: LocalisedString } } }
-- Generates the attribute strings for some types of prototypes
---@return PrototypeAttributes | PrototypeAttributesWithCategory
local function prototype_attributes()
local relevant_prototypes = {"belts", "beacons", "wagons", "fuels", "machines"}
local attributes = {} ---@type PrototypeAttributes | PrototypeAttributesWithCategory
for _, data_type in pairs(relevant_prototypes) do
local prototypes = global.prototypes[data_type] ---@type AnyIndexedPrototypes
local generator_function = attribute_generators[data_type]
attributes[data_type] = {}
if prototyper.data_types[data_type] == false then
---@cast prototypes IndexedPrototypes<FPPrototype>
---@cast attributes PrototypeAttributes
for proto_id, prototype in pairs(prototypes) do
attributes[data_type][proto_id] = generator_function(prototype)
end
else
---@cast prototypes IndexedPrototypesWithCategory<FPPrototypeWithCategory>
---@cast attributes PrototypeAttributesWithCategory
for category_id, category in pairs(prototypes) do
attributes[data_type][category_id] = {}
local attribute_category = attributes[data_type][category_id]
for proto_id, prototype in pairs(category.members) do
attribute_category[proto_id] = generator_function(prototype)
end
end
end
end
return attributes
end
---@alias MappedPrototypes<T> { [string]: T }
---@alias MappedPrototypesWithCategory<T> { [string]: { id: integer, name: string, members: { [string]: T } } }
---@alias MappedCategory { id: integer, name: string, members: { [string]: table } }
---@class PrototypeMaps: { [DataType]: table }
---@field machines MappedPrototypesWithCategory<FPMachinePrototype>
---@field recipes MappedPrototypes<FPRecipePrototype>
---@field items MappedPrototypesWithCategory<FPItemPrototype>
---@field fuels MappedPrototypesWithCategory<FPFuelPrototype>
---@field belts MappedPrototypes<FPBeltPrototype>
---@field wagons MappedPrototypesWithCategory<FPWagonPrototype>
---@field modules MappedPrototypesWithCategory<FPModulePrototype>
---@field beacons MappedPrototypes<FPBeaconPrototype>
---@param data_types { [DataType]: boolean }
---@return PrototypeMaps
local function prototype_maps(data_types)
local maps = {} ---@type PrototypeMaps
for data_type, has_categories in pairs(data_types) do
local map = {}
if not has_categories then
---@cast map MappedPrototypes<FPPrototype>
---@type IndexedPrototypes<FPPrototype>
local prototypes = global.prototypes[data_type]
for _, prototype in pairs(prototypes) do
map[prototype.name] = prototype
end
else
---@cast map MappedPrototypesWithCategory<FPPrototypeWithCategory>
---@type IndexedPrototypesWithCategory<FPPrototypeWithCategory>
local prototypes = global.prototypes[data_type]
for _, category in pairs(prototypes) do
map[category.name] = { name=category.name, id=category.id, members={} }
for _, prototype in pairs(category.members) do
map[category.name].members[prototype.name] = prototype
end
end
end
maps[data_type] = map
end
return maps
end
-- ** TOP LEVEL **
---@param skip_check boolean Whether the mod version check is skipped
function loader.run(skip_check)
-- If the mod version changed, the loader will be re-run after migration anyways
if not skip_check and script.active_mods["factoryplanner"] ~= global.mod_version then return end
util.nth_tick.register_all()
PROTOTYPE_MAPS = prototype_maps(prototyper.data_types)
PROTOTYPE_ATTRIBUTES = prototype_attributes()
ORDERED_RECIPE_GROUPS = ordered_recipe_groups()
RECIPE_MAPS = {
produce = recipe_map_from("products"),
consume = recipe_map_from("ingredients")
}
SORTED_ITEMS = sorted_items()
MODULE_NAME_MAP = module_name_map()
end
return loader

View File

@@ -0,0 +1,152 @@
-- This code handles the general migration process of the mod's global table
-- It decides whether and which migrations should be applied, in appropriate order
local migrator = {}
---@alias MigrationMasterList { [integer]: { version: VersionString, migration: Migration } }
---@alias Migration { global: function, player_table: function, subfactory: function, packed_subfactory: function }
---@alias MigrationObject PlayerTable | FPSubfactory | FPPackedSubfactory
-- Returns a table containing all existing migrations in order
local migration_masterlist = { ---@type MigrationMasterList
[1] = {version="0.18.20", migration=require("backend.migrations.migration_0_18_20")},
[2] = {version="0.18.27", migration=require("backend.migrations.migration_0_18_27")},
[3] = {version="0.18.29", migration=require("backend.migrations.migration_0_18_29")},
[4] = {version="0.18.38", migration=require("backend.migrations.migration_0_18_38")},
[5] = {version="0.18.42", migration=require("backend.migrations.migration_0_18_42")},
[6] = {version="0.18.45", migration=require("backend.migrations.migration_0_18_45")},
[7] = {version="0.18.48", migration=require("backend.migrations.migration_0_18_48")},
[8] = {version="0.18.49", migration=require("backend.migrations.migration_0_18_49")},
[9] = {version="0.18.51", migration=require("backend.migrations.migration_0_18_51")},
[10] = {version="1.0.6", migration=require("backend.migrations.migration_1_0_6")},
[11] = {version="1.1.5", migration=require("backend.migrations.migration_1_1_5")},
[12] = {version="1.1.6", migration=require("backend.migrations.migration_1_1_6")},
[13] = {version="1.1.8", migration=require("backend.migrations.migration_1_1_8")},
[14] = {version="1.1.14", migration=require("backend.migrations.migration_1_1_14")},
[15] = {version="1.1.19", migration=require("backend.migrations.migration_1_1_19")},
[16] = {version="1.1.21", migration=require("backend.migrations.migration_1_1_21")},
[17] = {version="1.1.25", migration=require("backend.migrations.migration_1_1_25")},
[18] = {version="1.1.26", migration=require("backend.migrations.migration_1_1_26")},
[19] = {version="1.1.27", migration=require("backend.migrations.migration_1_1_27")},
[20] = {version="1.1.42", migration=require("backend.migrations.migration_1_1_42")},
[21] = {version="1.1.43", migration=require("backend.migrations.migration_1_1_43")},
[22] = {version="1.1.59", migration=require("backend.migrations.migration_1_1_59")},
[23] = {version="1.1.61", migration=require("backend.migrations.migration_1_1_61")},
[24] = {version="1.1.65", migration=require("backend.migrations.migration_1_1_65")},
[25] = {version="1.1.66", migration=require("backend.migrations.migration_1_1_66")},
[26] = {version="1.1.67", migration=require("backend.migrations.migration_1_1_67")},
}
-- ** LOCAL UTIL **
-- Compares two mod versions, returns true if v1 is an earlier version than v2 (v1 < v2)
-- Version numbers have to be of the same structure: same amount of numbers, separated by a '.'
---@param v1 VersionString
---@param v2 VersionString
---@return boolean
local function compare_versions(v1, v2)
local split_v1 = util.split_string(v1, ".")
local split_v2 = util.split_string(v2, ".")
for i = 1, #split_v1 do
if split_v1[i] == split_v2[i] then
-- continue
elseif split_v1[i] < split_v2[i] then
return true
else
return false
end
end
return false -- return false if both versions are the same
end
-- Applies given migrations to the object
---@param migrations Migration[]
---@param function_name string
---@param object MigrationObject?
---@param player LuaPlayer?
local function apply_migrations(migrations, function_name, object, player)
for _, migration in ipairs(migrations) do
local migration_function = migration[function_name]
if migration_function ~= nil then
local migration_message = migration_function(object, player) ---@type string
-- If no message is returned, everything went fine
if migration_message == "removed" then break end
end
end
end
-- Determines whether a migration needs to take place, and if so, returns the appropriate range of the
-- migration_masterlist. If the version changed, but no migrations apply, it returns an empty array.
---@param previous_version VersionString
---@return Migration[]
local function determine_migrations(previous_version)
local migrations = {}
local found = false
for _, migration in ipairs(migration_masterlist) do
if compare_versions(previous_version, migration.version) then found = true end
if found then table.insert(migrations, migration.migration) end
end
return migrations
end
-- ** TOP LEVEL **
-- Applies any appropriate migrations to the global table
function migrator.migrate_global()
local migrations = determine_migrations(global.mod_version)
apply_migrations(migrations, "global", nil, nil)
global.mod_version = script.active_mods["factoryplanner"]
end
-- Applies any appropriate migrations to the given factory
---@param player LuaPlayer
function migrator.migrate_player_table(player)
local player_table = util.globals.player_table(player)
if player_table ~= nil then -- don't apply migrations to new players
local migrations = determine_migrations(player_table.mod_version)
-- General migrations
local old_version = player_table.mod_version -- keep for comparison below
apply_migrations(migrations, "player_table", player_table, player)
player_table.mod_version = global.mod_version
-- Subfactory migrations
for _, factory_name in pairs({"factory", "archive"}) do
--local outdated_subfactories = {}
for _, subfactory in pairs(Factory.get_all(player_table[factory_name], "Subfactory")) do
if subfactory.mod_version ~= old_version then -- out-of-sync subfactory
--error("Out of date subfactory, please report this to the mod author including the save file")
--table.insert(outdated_subfactories, subfactory)
else
apply_migrations(migrations, "subfactory", subfactory, player)
subfactory.mod_version = global.mod_version
end
end
--[[ -- Remove subfactories who weren't migrated along properly for some reason
-- TODO This is likely due to an old, now-fixed bug that left them behind
for _, subfactory in pairs(outdated_subfactories) do
Factory.remove(player_table[factory_name], subfactory)
end ]]
end
end
end
-- Applies any appropriate migrations to the given export_table's subfactories
---@param export_table ExportTable
function migrator.migrate_export_table(export_table)
local migrations = determine_migrations(export_table.mod_version)
for _, packed_subfactory in pairs(export_table.subfactories) do
-- This migration type won't need the player argument, and removing it allows
-- us to run imports without having a player attached
apply_migrations(migrations, "packed_subfactory", packed_subfactory, nil)
end
export_table.mod_version = global.mod_version
end
return migrator

View File

@@ -0,0 +1,326 @@
local generator = require("backend.handlers.generator")
prototyper = {
util = {},
defaults = {}
}
-- The purpose of the prototyper is to recreate the global tables containing all relevant data types.
-- It also handles some other things related to prototypes, such as updating preferred ones, etc.
-- Its purpose is to not lose any data, so if a dataset of a factory-dataset doesn't exist anymore
-- in the newly loaded global tables, it saves the name in string-form instead and makes the
-- concerned factory-dataset invalid. This accomplishes that invalid data is only permanently
-- removed when the user tells the subfactory to repair itself, giving him a chance to re-add the
-- missing mods. It is also a better separation of responsibilities and avoids some redundant code.
-- Load order is important here: machines->recipes->items->fuels
-- The boolean indicates whether this prototype has categories or not
---@type { [DataType]: boolean }
prototyper.data_types = {machines = true, recipes = false, items = true, fuels = true,
belts = false, wagons = true, modules = true, beacons = false}
---@alias DataType "machines" | "recipes" | "items" | "fuels" | "belts" | "wagons" | "modules" | "beacons"
---@alias NamedPrototypes<T> { [string]: T }
---@alias NamedPrototypesWithCategory<T> { [string]: { name: string, members: { [string]: T } } } }
---@alias NamedCategory { name: string, members: { [string]: table } }
---@alias AnyNamedPrototypes NamedPrototypes | NamedPrototypesWithCategory
---@alias IndexedPrototypes<T> { [integer]: T }
---@alias IndexedPrototypesWithCategory<T> { [integer]: { id: integer, name: string, members: { [integer]: T } } }
---@alias IndexedCategory { id: integer, name: string, members: { [integer]: table } }
---@alias AnyIndexedPrototypes IndexedPrototypes | IndexedPrototypesWithCategory
---@class PrototypeLists: { [DataType]: table }
---@field machines IndexedPrototypesWithCategory<FPMachinePrototype>
---@field recipes IndexedPrototypes<FPRecipePrototype>
---@field items IndexedPrototypesWithCategory<FPItemPrototype>
---@field fuels IndexedPrototypesWithCategory<FPFuelPrototype>
---@field belts IndexedPrototypes<FPBeltPrototype>
---@field wagons IndexedPrototypesWithCategory<FPWagonPrototype>
---@field modules IndexedPrototypesWithCategory<FPModulePrototype>
---@field beacons IndexedPrototypes<FPBeaconPrototype>
-- Converts given prototype list to use ids as keys, and sorts it if desired
---@param data_type DataType
---@param prototype_sorting_function function
---@return AnyIndexedPrototypes
local function convert_and_sort(data_type, prototype_sorting_function)
local final_list = {}
---@param list AnyNamedPrototypes[]
---@param sorting_function function
---@param category_id integer?
---@return AnyIndexedPrototypes
local function apply(list, sorting_function, category_id)
local new_list = {} ---@type (IndexedPrototypes | IndexedCategory)[]
for _, member in pairs(list) do table.insert(new_list, member) end
if sorting_function then table.sort(new_list, sorting_function) end
for id, member in pairs(new_list) do
member.id = id
member.category_id = category_id -- can be nil
member.data_type = data_type
end
return new_list
end
---@param a NamedCategory
---@param b NamedCategory
---@return boolean
local function category_sorting_function(a, b)
if a.name < b.name then return true
elseif a.name > b.name then return false end
return false
end
if prototyper.data_types[data_type] == false then
final_list = apply(global.prototypes[data_type], prototype_sorting_function, nil)
---@cast final_list IndexedPrototypes<FPPrototype>
else
final_list = apply(global.prototypes[data_type], category_sorting_function, nil)
---@cast final_list IndexedPrototypesWithCategory<FPPrototypeWithCategory>
for id, category in pairs(final_list) do
category.members = apply(category.members, prototype_sorting_function, id)
end
end
return final_list
end
function prototyper.build()
global.prototypes = {} ---@type PrototypeLists
local prototypes = global.prototypes
for data_type, _ in pairs(prototyper.data_types) do
---@type AnyNamedPrototypes
prototypes[data_type] = generator[data_type].generate()
end
-- Second pass to do some things that can't be done in the first pass due to the strict sequencing
for data_type, _ in pairs(prototyper.data_types) do
local second_pass = generator[data_type].second_pass ---@type function
if second_pass ~= nil then second_pass(prototypes[data_type]) end
end
-- Finish up generation by converting lists to use ids as keys, and sort if desired
for data_type, _ in pairs(prototyper.data_types) do
local sorting_function = generator[data_type].sorting_function ---@type function
prototypes[data_type] = convert_and_sort(data_type, sorting_function) ---@type AnyIndexedPrototypes
end
end
-- ** UTIL **
-- Returns the attribute string for the given prototype
---@param prototype AnyFPPrototype
---@return LocalisedString
function prototyper.util.get_attributes(prototype)
if prototype.category_id == nil then
---@cast prototype FPPrototype
return PROTOTYPE_ATTRIBUTES[prototype.data_type][prototype.id]
else
---@cast prototype FPPrototypeWithCategory
return PROTOTYPE_ATTRIBUTES[prototype.data_type][prototype.category_id][prototype.id]
end
end
-- Finds the given prototype by name. Can use the loader cache since it'll exist at this point.
---@param data_type DataType
---@param prototype_name string
---@param category_name string?
---@return AnyFPPrototype?
function prototyper.util.find_prototype(data_type, prototype_name, category_name)
--if data_type == nil then llog(prototype_name, category_name) end
local prototype_map = PROTOTYPE_MAPS[data_type]
if category_name == nil then
return prototype_map[prototype_name] -- can be nil
else
local category = prototype_map[category_name] ---@type MappedCategory
if category == nil then return nil end
return category.members[prototype_name] -- can be nil
end
end
---@class FPPackedPrototype
---@field name string
---@field category string
---@field data_type DataType
---@field simplified boolean
-- Returns a new table that only contains the given prototypes' identifiers
---@param proto AnyFPPrototype
---@param category string?
---@return FPPackedPrototype
function prototyper.util.simplify_prototype(proto, category)
return { name = proto.name, category = category, data_type = proto.data_type, simplified = true }
end
-- Validates given object with prototype, which includes trying to find the correct
-- new reference for its prototype, if able. Returns valid-status at the end.
---@param prototype AnyFPPrototype | FPPackedPrototype
---@param category_designation ("category" | "type")?
---@return AnyFPPrototype | FPPackedPrototype
function prototyper.util.validate_prototype_object(prototype, category_designation)
local updated_proto = prototype
if prototype.simplified then -- try to unsimplify, otherwise it stays that way
---@cast prototype FPPackedPrototype
local new_proto = prototyper.util.find_prototype(prototype.data_type, prototype.name, prototype.category)
if new_proto then updated_proto = new_proto end
else
---@cast prototype AnyFPPrototype
local category = prototype[category_designation] ---@type string
local new_proto = prototyper.util.find_prototype(prototype.data_type, prototype.name, category)
updated_proto = new_proto or prototyper.util.simplify_prototype(prototype, category)
end
return updated_proto
end
-- Build the necessary RawDictionaries for translation
function prototyper.util.build_translation_dictionaries()
for _, item_category in ipairs(global.prototypes.items) do
translator.new(item_category.name)
for _, proto in pairs(item_category.members) do
translator.add(item_category.name, proto.name, proto.localised_name)
end
end
translator.new("recipe")
for _, proto in pairs(global.prototypes.recipes) do
translator.add("recipe", proto.name, proto.localised_name)
end
end
-- Migrates the prototypes for default beacons and modules
---@param player_table PlayerTable
function prototyper.util.migrate_mb_defaults(player_table)
local mb_defaults = player_table.preferences.mb_defaults
local find = prototyper.util.find_prototype
local machine = mb_defaults.machine
if machine then
mb_defaults.machine = find("modules", machine.name, machine.category) --[[@as FPModulePrototype ]]
end
local second = mb_defaults.machine_secondary
if second then
mb_defaults.machine_secondary = find("modules", second.name, second.category) --[[@as FPModulePrototype ]]
end
local beacon = mb_defaults.beacon
if beacon then
mb_defaults.beacon = find("beacons", beacon.name, nil) --[[@as FPBeaconPrototype ]]
end
end
-- ** DEFAULTS **
---@alias PrototypeDefault FPPrototype
---@alias PrototypeWithCategoryDefault { [integer]: FPPrototypeWithCategory }
---@alias AnyPrototypeDefault PrototypeDefault | PrototypeWithCategoryDefault
-- Returns the default prototype for the given type, incorporating the category, if given
---@param player LuaPlayer
---@param data_type DataType
---@param category_id integer?
---@return AnyPrototypeDefault
function prototyper.defaults.get(player, data_type, category_id)
---@type AnyPrototypeDefault
local default = util.globals.preferences(player).default_prototypes[data_type]
return (category_id == nil) and default or default[category_id]
end
-- Sets the default prototype for the given type, incorporating the category, if given
---@param player LuaPlayer
---@param data_type DataType
---@param prototype_id integer
---@param category_id integer?
function prototyper.defaults.set(player, data_type, prototype_id, category_id)
local default_prototypes = util.globals.preferences(player).default_prototypes
local prototypes = global.prototypes[data_type] ---@type AnyIndexedPrototypes
if category_id == nil then
---@type PrototypeDefault
default_prototypes[data_type] = prototypes[prototype_id]
else
---@type PrototypeWithCategoryDefault
default_prototypes[data_type][category_id] = prototypes[category_id].members[prototype_id]
end
end
-- Returns the fallback default for the given type of prototype
---@param data_type DataType
---@return AnyPrototypeDefault
function prototyper.defaults.get_fallback(data_type)
local prototypes = global.prototypes[data_type] ---@type AnyIndexedPrototypes
local fallback = {}
if prototyper.data_types[data_type] == false then
---@cast prototypes IndexedPrototypes<FPPrototype>
fallback = prototypes[1]
else
---@cast prototypes IndexedPrototypesWithCategory<FPPrototypeWithCategory>
fallback = {} ---@type PrototypeWithCategoryDefault
for _, category in pairs(prototypes) do
fallback[category.id] = category.members[1]
end
end
return fallback
end
-- Kinda unclean that I have to do this, but it's better than storing it elsewhere
local category_designations = {machines="category", items="type",
fuels="category", wagons="category", modules="category"}
-- Migrates the default_prototypes preferences, trying to preserve the users choices
-- When this is called, the loader cache will already exist
---@param player_table PlayerTable
function prototyper.defaults.migrate(player_table)
local default_prototypes = player_table.preferences.default_prototypes
for data_type, has_categories in pairs(prototyper.data_types) do
if default_prototypes[data_type] ~= nil then
if not has_categories then
-- Use the same prototype if an equivalent can be found, use fallback otherwise
local default_proto = default_prototypes[data_type] ---@type PrototypeDefault
local equivalent_proto = prototyper.util.find_prototype(data_type, default_proto.name, nil)
---@cast equivalent_proto PrototypeDefault
default_prototypes[data_type] = equivalent_proto ---@type PrototypeDefault
or prototyper.defaults.get_fallback(data_type)
else
local new_defaults = {} ---@type PrototypeWithCategoryDefault
local fallback = prototyper.defaults.get_fallback(data_type)
local default_map = {} ---@type { [string]: FPPrototype }
for _, default_proto in pairs(default_prototypes[data_type]--[[@as PrototypeWithCategoryDefault]]) do
local category_name = default_proto[category_designations[data_type]] ---@type string
default_map[category_name] = default_proto
end
---@type IndexedPrototypesWithCategory<FPPrototypeWithCategory>
local categories = global.prototypes[data_type]
for _, category in pairs(categories) do
local previous_category = default_map[category.name]
if previous_category then -- category existed previously
local proto_name = previous_category.name
---@type PrototypeWithCategoryDefault
new_defaults[category.id] = prototyper.util.find_prototype(data_type, proto_name, category.name)
end
new_defaults[category.id] = new_defaults[category.id] or fallback[category.id]
end
default_prototypes[data_type] = new_defaults ---@type PrototypeWithCategoryDefault
end
end
end
end

View File

@@ -0,0 +1,207 @@
---@diagnostic disable
-- This file contains functionality to rig the interface for various setups
-- that make for good screenshots. It provides a remote interface that its
-- companion scenario calls to actually take the screenshots.
-- This code is terrible and uses some functions completely inappropriately,
-- but it needs to do that to manipulate the interface because GUI events
-- can't be raised manually anymore.
local mod_gui = require("mod-gui")
local handler_requires = {"ui.base.compact_dialog", "ui.base.modal_dialog", "ui.main.title_bar",
"ui.dialogs.picker_dialog", "ui.dialogs.porter_dialog"}
local handlers = {} -- Need to require these here since it can't be done inside an event
for _, path in pairs(handler_requires) do handlers[path] = require(path) end
local function return_dimensions(scene, frame)
local dimensions = {actual_size=frame.actual_size, location=frame.location}
-- We do this on teardown so the frame has time to adjust all its sizes
remote.call("screenshotter_output", "return_dimensions", scene, dimensions)
end
local function open_modal(player, dialog, modal_data)
main_dialog.toggle(player)
util.globals.main_elements(player).main_frame.location = player.display_resolution -- hack city
util.raise.open_dialog(player, {dialog=dialog, modal_data=modal_data, skip_dimmer=true})
end
local function modal_teardown(player, scene)
return_dimensions(scene, util.globals.modal_elements(player).modal_frame)
util.raise.close_dialog(player, "cancel")
end
local function get_handler(path, index, event, name)
local gui_handlers = handlers[path][index].gui[event]
for _, handler_table in pairs(gui_handlers) do
if handler_table.name == name then return handler_table.handler end
end
end
local function set_machine_default(player, proto_name, category_name)
local proto = prototyper.util.find_prototype("machines", proto_name, category_name)
prototyper.defaults.set(player, "machines", proto.id, proto.category_id)
end
local actions = {
player_setup = function(player)
local player_table = util.globals.player_table(player)
-- Mod settings
settings.get_player_settings(player)["fp_display_gui_button"] = {value = false}
settings.get_player_settings(player)["fp_products_per_row"] = {value = 5}
settings.get_player_settings(player)["fp_subfactory_list_rows"] = {value = 18}
-- Preferences
player_table.preferences.recipe_filters = {disabled = true, hidden = false}
player_table.preferences.ignore_barreling_recipes = true
player_table.preferences.ignore_recycling_recipes = true
player_table.preferences.done_column = true
player_table.preferences.mb_defaults = { -- naughty use of the prototyper function
machine = prototyper.util.find_prototype("modules", "productivity-module-3", "productivity"),
machine_secondary = nil,
beacon = prototyper.util.find_prototype("modules", "speed-module-3", "speed"),
beacon_count = 8
}
prototyper.defaults.set(player, "belts", PROTOTYPE_MAPS.belts["fast-transport-belt"].id)
set_machine_default(player, "electric-mining-drill", "basic-solid")
set_machine_default(player, "steel-furnace", "smelting")
set_machine_default(player, "assembling-machine-2", "crafting")
set_machine_default(player, "assembling-machine-3", "advanced-crafting")
set_machine_default(player, "assembling-machine-2", "crafting-with-fluid")
-- Subfactories
local export_string = "eNrtWmlTGk0Q/iup/WrAmdk5dqzKBy9CMBqOiMGURe0uA67uxR4CWv73t4dDycELmIQchR8smenu6e55+hp5MIKo075TSepFobFn4CIucma8NtQwjpKsDbupyoy9B8OxUzUlsBAQdH3Pgc+oiEmR68+2m0XJKPbtMFTJs6jH10aaO5NdT6XG3ucHI7QDLeuzF/TeeJkKdr0wVUmmkqtX9SjKUhCXeYFKXdsHOo5eG2GUaV4Ddhw/V3HihUC29wDSq0nUyd2xjpFzo9zZuuvbqWY5jHwfVrV9sJpFcbvrR1Gi6d97oVqRz1d3yjf28PN+aSxljqExM3MEq09GNgLlg2Wjf8mm54uLJ4qCwELqeip0VSG23durV8dDO4jB0lWt/mwgddZv3kh00rvfP61nlo+HzaDcFajptlo8eVdHpXr/ZFiN89Fu89BUoi8Pq4KNbt+a9aPB6W1d3e87528lOwirTuukZWaoerJ7FF2/Ld2822kcVvYDUS4djM6Sd42oV/Uq5fsD1izv1O5rOzHF6CD1OlV1MBpUZat8LUpHlZ3+Rxm3hmrwqey0LPDrIPU/XKDz89HlWcm9rJ8PT6yb3olrssagVcnNPOf3fdmMeD5Cl4qjo9LOvtsXF6XBG7AW7Ivxze2xtq9WV7sjPCz135ea936zciyc2ik/bjQuDvuN/F4NA8L52zsxYHfm5en5IHnfov7gw30PtfrlXInBbqW+M0rywfta7ejopn5U60asMtxJz6ulVtit3bxr9Afmh9uzc8t29m/D3qBaa5YvysllBZXdXkgjfNBKjuWnZlgJT68vKubZSdTyW0ooJ7xonX/68Gm3l3thtWPfJQ2eVT/azZvd/YPgGF1+NK6+D1AIbIBDFunFKVAWwAO8kXoAD6/rqY6xlyW5ApyMYs2iYaURaAdRHsIB2ALQJKqfe4nqtGerD0ZHdeFGOm1nBEzT5a+4HIi89lSlru2nahorU9xPjp0hfWbR49XLwgysn7GN9yZpbw3yRLlePCb6ES+6dqZ6Olz3DDexu5kX9rTuzyLaUz9PVvTBMz3qEwW074HyTs1EdiKt+9iBIEglrgozuwdLGIGTA9u9nhr3td4gVgWODyoUplQFcx2luxGc1fa9wMuezoeClPuqPS1Kk0/pqjC887JRYcKzXJN5pnlA0mePnY5F/Q9kptq6T5wqiLNRO/V1kdtDX0tqgFVzyfd06tpHDWXb1QX6W9umO0uM6cbtKeGcJdYL3ZnGSnVW9uOYev5Y8mIHkjUdePBk8obz1VyWmeJ21TSjq+zKiSGxPf9vzALk12eBxV3Pl5BCX0GKrB6TK2Jq4SUtAhCxWFFgSixEKDItaRFmUUsKS1KKCRESE4mIZRJkYsI2BjYv0VGReZsuPJO/t4hbHXFLbmoB7jglRYsB8BAmCEuqYUYoAI8Kgjg2JeOUYYo5woxvEHdKsyWeW+jmSWiPgbNNeH8y/Fa6sAUgnGGQcEG5kJwJRpngnOlUxyWzLAoZUDDCLcYot8TmcPidJnILxRe1jOQPbhnXh/uquPgzEH+15DGIfPMYtFaQ2J07G7rmTsH1Ejf3su2Mup1RtzPqT51RVwqxhcnGKn6RbEwC0wVGGHMuucmEIAIxJCy88QYvCqFj+C1ZYztgvKzDW3pjC0BoEV5kJuIwVGBpUsEtJDiCYkctRAljkhNTYKDC8HuDDZ4bxQCCgms7m+7stghcF4FL72oB9kyBaBFZMMSaFpWSYkakhHlWYxCZmJuYcikI3+iEm2ZK+YXYh6taWqj0fxN/Z7+07jQ+r/Cf2Sv9ltli2yttpFdaFlmLnmCRwEXolXRegMYIEgST1OSwYkLVohAjkCEYIpLIjT5BpBmEbcFJVniD/bmJYv0CNfH8T0oS3Vx79juHRL7XKYx3l9W+axV4ru3P3zIr6kd2aIcJklxfMSGMmJxgCiO3ZSFMqcDQqEhMoVvhFI1rwtN0rE99/JVV8+c/VSzDz6pTgyUhMBiBMBACXCSJfjSWQjBTok0WTjAgApuSwtiwZRhw7BTq1hgzvysqnqpn4IW6d+sknu+vqfe/9yaHfw3Q18r40AwWpf4xOTYFRD9kA667Q0ZgOKFMSAmjCcwlSJq/+AnuR76PlcZeRyV6Nrt69S9948z++425evwPtGH0aA=="
util.porter.add_by_string(player, export_string)
local trash = Factory.get_by_gui_position(player_table.factory, "Subfactory", 5)
Factory.remove(player_table.factory, trash)
Factory.add(player_table.archive, trash)
local hotness = Factory.get_by_gui_position(player_table.factory, "Subfactory", 3)
util.context.set_subfactory(player, hotness)
solver.update(player, hotness)
util.raise.refresh(player, "all", nil)
-- Research
player.force.technologies["oil-processing"].researched=true
player.force.technologies["coal-liquefaction"].researched=true
-- Player inventory
player.insert{name="assembling-machine-3", count=9}
player.insert{name="assembling-machine-2", count=1}
player.insert{name="electric-mining-drill", count=29}
player.insert{name="speed-module-3", count=14}
player.insert{name="speed-module-2", count=1}
player.insert{name="chemical-plant", count=6}
end,
setup_01_main_interface = function(player)
local translation_progress = mod_gui.get_frame_flow(player)["flib_translation_progress"]
if translation_progress then translation_progress.visible = false end
main_dialog.toggle(player)
end,
teardown_01_main_interface = function(player)
local main_frame = util.globals.main_elements(player).main_frame
return_dimensions("01_main_interface", main_frame)
end,
setup_02_compact_interface = function(player)
util.globals.main_elements(player).main_frame.location = player.display_resolution -- hack city
view_state.select(player, 2)
local toggle_handler = get_handler("ui.main.title_bar", 1, "on_gui_click", "switch_to_compact_view")
toggle_handler(player, nil, nil)
end,
teardown_02_compact_interface = function(player)
local compact_frame = util.globals.ui_state(player).compact_elements.compact_frame
return_dimensions("02_compact_interface", compact_frame)
local toggle_handler = get_handler("ui.base.compact_dialog", 2, "on_gui_click", "switch_to_main_view")
toggle_handler(player, nil, nil)
end,
setup_03_item_picker = function(player)
local modal_data = {item_id=nil, item_category="product"}
open_modal(player, "picker", modal_data)
local modal_elements = util.globals.modal_elements(player)
modal_elements.search_textfield.text = "f"
local search_handler = get_handler("ui.base.modal_dialog", 1, "on_gui_text_changed", "modal_searchfield")
search_handler(player, nil, {text="f"})
local group_handler = get_handler("ui.dialogs.picker_dialog", 1, "on_gui_click", "select_picker_item_group")
group_handler(player, {group_id=3}, nil)
modal_elements.item_choice_button.sprite = "item/raw-fish"
modal_elements.belt_amount_textfield.text = "0.5"
modal_elements.belt_choice_button.elem_value = "fast-transport-belt"
local belt_handler = get_handler("ui.dialogs.picker_dialog", 1, "on_gui_elem_changed", "picker_choose_belt")
belt_handler(player, nil, {element=modal_elements.belt_choice_button, elem_value="fast-transport-belt"})
modal_elements.search_textfield.focus()
end,
teardown_03_item_picker = (function(player) modal_teardown(player, "03_item_picker") end),
setup_04_recipe_picker = function(player)
local product_proto = prototyper.util.find_prototype("items", "petroleum-gas", "fluid")
---@cast product_proto -nil
open_modal(player, "recipe", {category_id=product_proto.category_id,
product_id=product_proto.id, production_type="produce"})
end,
teardown_04_recipe_picker = (function(player) modal_teardown(player, "04_recipe_picker") end),
setup_05_machine = function(player)
local floor = util.globals.context(player).floor
local line = Collection.get_by_gui_position(floor.Line, 2) ---@cast line -nil
local modal_data = {floor_id=floor.id, line_id=line.id, recipe_name=line.recipe.proto.localised_name}
open_modal(player, "machine", modal_data)
end,
teardown_05_machine = (function(player) modal_teardown(player, "05_machine") end),
setup_06_import = function(player)
open_modal(player, "import", nil)
local export_string = "eNrtWt+P4jYQ/lfcPG+2hG2ra6Q+tJUqVepJp+Ohqu5Q5DiT3Wn9I7UdVIT43ztOzIK4RRDKHnBF4gHb4/HM941n7CSLRJmqmIF1aHSSJ9l9dv/tKLlL4J/GWF/QqAOf5Iuk5A5WAt+TQC2xpPboPqNfaHPhjZ03kmsNdq1qeZe4tuxHEVySf1gkmqug6wOqxx/Qg/oatQPrwU7Ze2O8I3UeFTjBJcl9N7pLtPFhbkIj76ypWtHZZMo/QZB4vqBV+kYhJHdB8lfSS9Kr5s9GShoOXpKsN01RS2Ns0PIbatinrZPZpU3CDGSSZ+vxXzrdy3XHZAXBnHqfAZgokOT1/P/i75rwpneKFKZOIGgBacPFX1P2B/AnNum7vtqNS8reUaAx03rmaCKwqIWtFX/UKfvx7xYtMM5qdE/MG1a2KCs2abCieLO90O8oZZhX8lLOmQaomDI0C2mccVrAKNJrUPsdbFBE03RvQmf0dId/pMChaiTWCFWSe9sCuThvwpSAS4CQK9PSUnn2hvy10HlQFaveRVJBTeRURTmnSbF7a1ZJYVVEk2ouHcQQiMT1y66oWnm0nJ4ypgiTrVgKWWCAuAWBTSf0X7AV3MNjiMI8EZbXHvVjsH2toojo9z2bMf++NyAwQpIzWKmsTLC9g5UUgRWgPX+krmxE0CsunqJz23aTWlClJBPSKJU+DDG6NrRWIVGhf16f8nMroYg5um+5Q4Nzhn6e9nP2W7I5aTNMv1kj9rZT9VIgxZFdoRS9EM8aQTV+XjgZakE+2l5hQt5u5Jq3EfJlCHwuQh371Oc4ssfJuimi4IaHb46E2TWUSg7Gt5PeXHZ8cmDHA4H9KUIRMvjBu9NylNe4FcevvxVfqLOD+Btt8Tc+dGMMIjCUvdR5vM6MeqMxYgNhfYsirVurecfDjcxrJfOFin3j86g6PL7COryOmelRF7XxJxe1QcHHqxmnQ3aVCrSiRX87aN8O2reD9tkP2n2BN5pK/Fk2Zv//VuFPU+GFaQjKVPDyc5f2a+Hx9Bs/O/nGz7ZiJXuVWHEeQKaNJNf2+hyeMJ+zFg+9hmwa/CXvd+cJ0bS0B1yzbxxe6pMS1Rji0aYdmftQKbkjCJ2RWJ2fSYU6pO/KopQDDb+Ue9kL2fvusCP4+Ngj+OlPig9HXwUHPT4gQU8cltzurdNPoND57r3dWQ4XnQGCd/VN+0HmXkaueXgVCvtNaJBwsUaAcyG/7gFnS/pMhAYrbPfOsnvRPdDiy7yLP1xOIrjku/j4mu/i08/+UYR7/ixhyr7sT0Kmy38BcOORig=="
local modal_elements = util.globals.modal_elements(player)
modal_elements.import_textfield.text = export_string
local textfield_handler = get_handler("ui.dialogs.porter_dialog", 1, "on_gui_text_changed", "import_string")
textfield_handler(player, nil, {element=modal_elements.import_textfield, text=export_string})
local import_handler = get_handler("ui.dialogs.porter_dialog", 1, "on_gui_click", "import_subfactories")
import_handler(player, nil, nil)
modal_elements.subfactory_checkboxes["tmp_1"].state = false
modal_elements.subfactory_checkboxes["tmp_3"].state = false
modal_elements.master_checkbox.state = false
end,
teardown_06_import = (function(player) modal_teardown(player, "06_import") end),
setup_07_utility = function(player)
open_modal(player, "utility", nil)
end,
teardown_07_utility = (function(player) modal_teardown(player, "07_utility") end),
setup_08_preferences = function(player)
open_modal(player, "preferences", nil)
end,
teardown_08_preferences = (function(player) modal_teardown(player, "08_preferences") end)
}
local function initial_setup()
DEV_ACTIVE = false -- desync city, but it's fiiine. Avoids any accidental artifacts.
return {"09_settings"}
end
local function execute_action(player_index, action_name)
local player = game.get_player(player_index)
actions[action_name](player)
end
remote.add_interface("screenshotter_input", {
initial_setup = initial_setup,
execute_action = execute_action
})