Первый фикс

Пачки некоторых позиций увеличены
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,159 @@
local constants = require("constants")
local util = require("scripts.util")
local beacon = require("scripts.database.beacon")
local burning = require("scripts.database.burning")
local crafter = require("scripts.database.crafter")
local entity = require("scripts.database.entity")
local entity_state = require("scripts.database.entity-state")
local entity_type = require("scripts.database.entity-type")
local equipment_category = require("scripts.database.equipment-category")
local equipment = require("scripts.database.equipment")
local fluid = require("scripts.database.fluid")
local fuel_category = require("scripts.database.fuel-category")
local generator = require("scripts.database.generator")
local group = require("scripts.database.group")
local item = require("scripts.database.item")
local item_type = require("scripts.database.item-type")
local lab = require("scripts.database.lab")
local mining_drill = require("scripts.database.mining-drill")
local offshore_pump = require("scripts.database.offshore-pump")
local recipe_category = require("scripts.database.recipe-category")
local recipe = require("scripts.database.recipe")
local resource_category = require("scripts.database.resource-category")
local resource = require("scripts.database.resource")
local science_pack = require("scripts.database.science-pack")
local technology = require("scripts.database.technology")
local database = {}
function database.build()
-- Create class tables
for _, class in pairs(constants.classes) do
database[class] = {}
end
-- Create dictionaries
for _, class in pairs(constants.classes) do
util.new_dictionary(class)
util.new_dictionary(class .. "_description")
end
util.new_dictionary("gui", constants.gui_strings)
-- Data that is needed for generation but will not be saved
local metadata = {}
entity_type(database)
equipment_category(database)
fuel_category(database)
group(database)
item_type(database)
recipe_category(database)
resource_category(database)
science_pack(database)
equipment(database)
beacon(database, metadata)
crafter(database, metadata)
generator(database)
entity(database, metadata)
mining_drill(database)
fluid(database, metadata)
item(database, metadata)
lab(database)
offshore_pump(database)
recipe(database, metadata)
resource(database)
technology(database, metadata)
offshore_pump.check_enabled_at_start(database)
fluid.process_temperatures(database, metadata)
mining_drill.add_resources(database)
fuel_category.check_fake_category(database)
burning(database)
entity_state(database)
database.generated = true
end
local function update_launch_products(launch_products, force_index, to_value)
for _, launch_product in ipairs(launch_products) do
local product_data = database.item[launch_product.name]
if product_data.researched_forces then
product_data.researched_forces[force_index] = to_value
end
update_launch_products(database, product_data.rocket_launch_products, force_index)
end
end
function database.handle_research_updated(technology, to_value)
local force_index = technology.force.index
-- Technology
local technology_data = database.technology[technology.name]
-- Other mods can update technologies during on_configuration_changed before RB gets a chance to config change
if not technology_data then
return
end
technology_data.researched_forces[force_index] = to_value
for _, objects in pairs({
technology_data.unlocks_equipment,
technology_data.unlocks_fluids,
technology_data.unlocks_items,
technology_data.unlocks_entities,
technology_data.unlocks_recipes,
}) do
for _, obj_ident in ipairs(objects) do
local class = obj_ident.class
local obj_data = database[class][obj_ident.name]
-- Unlock this object
if obj_data.researched_forces then
obj_data.researched_forces[force_index] = to_value
end
if class == "fluid" and obj_data.temperature_ident then
-- Unlock base fluid
local base_fluid_data = database.fluid[obj_data.prototype_name]
if base_fluid_data.researched_forces then
base_fluid_data.researched_forces[force_index] = to_value
end
elseif class == "item" then
-- Unlock rocket launch products
update_launch_products(obj_data.rocket_launch_products, force_index, to_value)
elseif class == "offshore_pump" then
-- Unlock pumped fluid
local fluid = obj_data.fluid
local fluid_data = database.fluid[fluid.name]
if fluid_data.researched_forces then
fluid_data.researched_forces[force_index] = to_value
end
end
end
end
end
function database.check_force(force)
if not force.valid then
return
end
for _, technology in pairs(force.technologies) do
if technology.enabled and technology.researched then
database.handle_research_updated(technology, true)
end
end
end
function database.check_forces()
for _, force in pairs(global.forces) do
database.check_force(force)
end
end
return database

View File

@@ -0,0 +1,35 @@
local util = require("scripts.util")
return function(database, metadata)
metadata.beacon_allowed_effects = {}
for name, prototype in pairs(global.prototypes.beacon) do
local size = util.get_size(prototype) --[[@as DisplayResolution]]
database.entity[name] = {
accepted_modules = {},
blueprintable = util.is_blueprintable(prototype),
class = "entity",
distribution_effectivity = prototype.distribution_effectivity,
effect_area = {
height = size.height + (prototype.supply_area_distance * 2),
width = size.width + (prototype.supply_area_distance * 2),
},
energy_usage = prototype.energy_usage,
entity_type = { class = "entity_type", name = prototype.type },
module_slots = prototype.module_inventory_size
and prototype.module_inventory_size > 0
and prototype.module_inventory_size
or nil,
placed_by = util.process_placed_by(prototype),
prototype_name = name,
science_packs = {},
size = size,
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
metadata.beacon_allowed_effects[name] = prototype.allowed_effects
end
end

View File

@@ -0,0 +1,51 @@
local table = require("__flib__.table")
local constants = require("constants")
return function(database)
-- Compatible fuels / burned in
for _, class in pairs(constants.burner_classes) do
for name, data in pairs(database[class]) do
local can_burn = data.can_burn
if can_burn then
-- Generators might have a fluid defined here already
for _, fuel_ident in pairs(can_burn) do
local fuel_data = database[fuel_ident.class][fuel_ident.name]
fuel_data.burned_in[#fuel_data.burned_in + 1] = { class = class, name = name }
end
local fuel_filter = data.fuel_filter
if fuel_filter then
data.can_burn = { fuel_filter }
data.fuel_filter = nil
local fuel_data = database[fuel_filter.class][fuel_filter.name]
fuel_data.burned_in[#fuel_data.burned_in + 1] = { class = class, name = name }
end
for i, category_ident in pairs(data.fuel_categories or {}) do
local category_data = database.fuel_category[category_ident.name]
if category_data then
-- Add fluids and items to the compatible fuels, and add the object to the material's burned in table
for _, objects in pairs({ category_data.fluids, category_data.items }) do
for _, obj_ident in pairs(objects) do
local obj_data = database[obj_ident.class][obj_ident.name]
obj_data.burned_in[#obj_data.burned_in + 1] = { class = class, name = name }
can_burn[#can_burn + 1] = table.shallow_copy(obj_ident)
end
end
else
-- Remove this category from the entity
table.remove(data.fuel_categories, i)
end
end
end
end
end
-- Burnt results
for item_name, item_data in pairs(database.item) do
local burnt_result = item_data.burnt_result
if burnt_result then
local result_data = database.item[burnt_result.name]
result_data.burnt_result_of[#result_data.burnt_result_of + 1] = { class = "item", name = item_name }
end
end
end

View File

@@ -0,0 +1,108 @@
local util = require("scripts.util")
return function(database, metadata)
-- Characters as crafters
for name, prototype in pairs(global.prototypes.character) do
local ingredient_limit = prototype.ingredient_count
if ingredient_limit == 255 then
ingredient_limit = nil
end
database.entity[name] = {
accepted_modules = {}, -- Always empty
blueprintable = false,
can_burn = {}, -- Always empty
can_craft = {},
class = "entity",
crafting_speed = 1,
enabled = true,
entity_type = { class = "entity_type", name = prototype.type },
hidden = false,
ingredient_limit = ingredient_limit,
is_character = true,
placed_by = util.process_placed_by(prototype),
prototype_name = name,
recipe_categories_lookup = prototype.crafting_categories or {},
recipe_categories = util.convert_categories(prototype.crafting_categories or {}, "recipe_category"),
science_packs = {},
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
end
-- Actual crafters
metadata.allowed_effects = {}
metadata.crafter_fluidbox_counts = {}
metadata.fixed_recipes = {}
local rocket_silo_categories = util.unique_obj_array()
for name, prototype in pairs(global.prototypes.crafter) do
-- Fixed recipe
local fixed_recipe
if prototype.fixed_recipe then
metadata.fixed_recipes[prototype.fixed_recipe] = true
fixed_recipe = { class = "recipe", name = prototype.fixed_recipe }
end
-- Rocket silo categories
if prototype.rocket_parts_required then
for category in pairs(prototype.crafting_categories) do
table.insert(rocket_silo_categories, { class = "recipe_category", name = category })
end
end
local ingredient_limit = prototype.ingredient_count
if ingredient_limit == 255 then
ingredient_limit = nil
end
metadata.allowed_effects[name] = prototype.allowed_effects
local fluidboxes = prototype.fluidbox_prototypes
if fluidboxes then
local fluidbox_counts = { inputs = 0, outputs = 0 }
for _, fluidbox in pairs(fluidboxes) do
local type = fluidbox.production_type
if string.find(type, "input") then
fluidbox_counts.inputs = fluidbox_counts.inputs + 1
end
if string.find(type, "output") then
fluidbox_counts.outputs = fluidbox_counts.outputs + 1
end
end
metadata.crafter_fluidbox_counts[name] = fluidbox_counts
end
local is_hidden = prototype.has_flag("hidden")
local fuel_categories, fuel_filter = util.process_energy_source(prototype)
database.entity[name] = {
accepted_modules = {},
blueprintable = util.is_blueprintable(prototype),
can_burn = {},
can_craft = {},
class = "entity",
crafting_speed = prototype.crafting_speed,
entity_type = { class = "entity_type", name = prototype.type },
fixed_recipe = fixed_recipe,
fuel_categories = fuel_categories,
fuel_filter = fuel_filter,
hidden = is_hidden,
ingredient_limit = ingredient_limit,
module_slots = prototype.module_inventory_size
and prototype.module_inventory_size > 0
and prototype.module_inventory_size
or nil,
placed_by = util.process_placed_by(prototype),
prototype_name = name,
recipe_categories_lookup = prototype.crafting_categories or {},
recipe_categories = util.convert_categories(prototype.crafting_categories or {}, "recipe_category"),
rocket_parts_required = prototype.rocket_parts_required,
science_packs = {},
size = util.get_size(prototype),
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
end
metadata.rocket_silo_categories = rocket_silo_categories
end

View File

@@ -0,0 +1,17 @@
return function(database)
for _, entity_data in pairs(database.entity) do
-- Hidden / disabled for entities
if not entity_data.is_character then
local placed_by_len = #(entity_data.placed_by or {})
if placed_by_len == 0 and not entity_data.expected_resources then
entity_data.enabled = false
elseif placed_by_len == 1 then
local item_ident = entity_data.placed_by[1]
local item_data = database.item[item_ident.name]
if item_data.hidden then
entity_data.hidden = true
end
end
end
end
end

View File

@@ -0,0 +1,26 @@
local constants = require("constants")
local util = require("scripts.util")
return function(database)
for class in pairs(constants.prototypes.filtered_entities) do
if class ~= "resource" then
for name, prototype in pairs(global.prototypes[class]) do
local type = prototype.type
local type_data = database.entity_type[type]
if not type_data then
type_data = {
class = "entity_type",
entities = {},
prototype_name = type,
}
database.entity_type[type] = type_data
util.add_to_dictionary("entity_type", type, { "entity-type." .. type })
util.add_to_dictionary("entity_type_description", type, { "entity-type-description." .. type })
end
table.insert(type_data.entities, { class = "entity", name = name })
end
end
end
end

View File

@@ -0,0 +1,70 @@
local table = require("__flib__.table")
local util = require("scripts.util")
return function(database, metadata)
metadata.gathered_from = {}
--- @type table<string, LuaEntityPrototype>
local prototypes = global.prototypes.entity
for name, prototype in pairs(prototypes) do
local equipment_categories = util.unique_obj_array()
local equipment = util.unique_obj_array()
local equipment_grid = prototype.grid_prototype
if equipment_grid then
for _, equipment_category in pairs(equipment_grid.equipment_categories) do
table.insert(equipment_categories, { class = "equipment_category", name = equipment_category })
local category_data = database.equipment_category[equipment_category]
if category_data then
for _, equipment_name in pairs(category_data.equipment) do
table.insert(equipment, equipment_name)
end
end
end
end
local fuel_categories, fuel_filter = util.process_energy_source(prototype)
local expected_resources
local mineable = prototype.mineable_properties
if
mineable
and mineable.minable
and mineable.products
and #mineable.products > 0
and mineable.products[1].name ~= name
then
expected_resources = table.map(mineable.products, function(product)
if not metadata.gathered_from[product.name] then
metadata.gathered_from[product.name] = {}
end
table.insert(metadata.gathered_from[product.name], { class = "entity", name = name })
return { class = product.type, name = product.name, amount_ident = util.build_amount_ident(product) }
end)
end
database.entity[name] = {
accepted_equipment = equipment,
blueprintable = util.is_blueprintable(prototype),
can_burn = {},
class = "entity",
enabled_at_start = expected_resources and true or false, -- FIXME: This is inaccurate
entity_type = { class = "entity_type", name = prototype.type },
equipment_categories = equipment_categories,
expected_resources = expected_resources,
fuel_categories = fuel_categories,
fuel_filter = fuel_filter,
module_slots = prototype.module_inventory_size
and prototype.module_inventory_size > 0
and prototype.module_inventory_size
or nil,
placed_by = util.process_placed_by(prototype),
prototype_name = name,
science_packs = {},
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,25 @@
local util = require("scripts.util")
local equipment_category_proc = {}
function equipment_category_proc.build(database)
for name, prototype in pairs(global.prototypes.equipment_category) do
database.equipment_category[name] = {
class = "equipment_category",
enabled_at_start = true,
equipment = {},
prototype_name = name,
}
util.add_to_dictionary("equipment_category", name, prototype.localised_name)
util.add_to_dictionary("equipment_category_description", name, prototype.localised_description)
end
end
-- When calling the module directly, call equipment_category_proc.build
setmetatable(equipment_category_proc, {
__call = function(_, ...)
return equipment_category_proc.build(...)
end,
})
return equipment_category_proc

View File

@@ -0,0 +1,87 @@
local table = require("__flib__.table")
local util = require("scripts.util")
local properties_by_type = {
["active-defense-equipment"] = { { "energy_consumption", "energy" } },
["battery-equipment"] = {},
["belt-immunity-equipment"] = { { "energy_consumption", "energy" } },
["energy-shield-equipment"] = {
{ "energy_consumption", "energy" },
{ "shield", "number", "shield_points" },
{ "energy_per_shield", "energy", "energy_per_shield_point" },
},
["generator-equipment"] = { { "energy_production", "energy" } },
["movement-bonus-equipment"] = { { "energy_consumption", "energy" }, { "movement_bonus", "percent" } },
["night-vision-equipment"] = { { "energy_consumption", "energy" } },
["roboport-equipment"] = { { "energy_consumption", "energy" } },
["solar-panel-equipment"] = { { "energy_production", "energy" } },
}
local function get_equipment_property(properties, source, name, formatter, label)
local value = source[name]
if value and value > 0 then
table.insert(properties, {
type = "plain",
label = label or name,
value = value,
formatter = formatter,
})
end
end
return function(database)
--- @type table<string, LuaEquipmentPrototype>
local prototypes = global.prototypes.equipment
for name, prototype in pairs(prototypes) do
local fuel_categories
local burner = prototype.burner_prototype
if burner then
fuel_categories = util.convert_categories(burner.fuel_categories, "fuel_category")
end
for _, category in pairs(prototype.equipment_categories) do
local category_data = database.equipment_category[category]
category_data.equipment[#category_data.equipment + 1] = { class = "equipment", name = name }
end
local equipment_type = prototype.type
local properties = {}
for _, property in pairs(properties_by_type[equipment_type]) do
get_equipment_property(properties, prototype, property[1], property[2], property[3])
end
local energy_source = prototype.energy_source
if energy_source then
get_equipment_property(properties, energy_source, "buffer_capacity", "energy_storage")
end
if equipment_type == "roboport-equipment" then
local logistic_parameters = prototype.logistic_parameters
get_equipment_property(properties, logistic_parameters, "logistic_radius", "number")
get_equipment_property(properties, logistic_parameters, "construction_radius", "number")
get_equipment_property(properties, logistic_parameters, "robot_limit", "number")
get_equipment_property(properties, logistic_parameters, "charging_energy", "energy")
end
database.equipment[name] = {
can_burn = {},
class = "equipment",
enabled = true,
equipment_categories = table.map(prototype.equipment_categories, function(category)
return { class = "equipment_category", name = category }
end),
equipment_properties = properties,
fuel_categories = fuel_categories,
hidden = false,
placed_in = util.unique_obj_array(),
prototype_name = name,
science_packs = {},
size = prototype.shape and prototype.shape.width or nil, -- Equipments can have irregular shapes
take_result = prototype.take_result and { class = "item", name = prototype.take_result.name } or nil,
unlocked_by = {},
}
util.add_to_dictionary("equipment", name, prototype.localised_name)
util.add_to_dictionary("equipment_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,238 @@
local table = require("__flib__.table")
local constants = require("constants")
local util = require("scripts.util")
local fluid_proc = {}
function fluid_proc.build(database, metadata)
local localised_fluids = {}
for name, prototype in pairs(global.prototypes.fluid) do
-- Group
local group = prototype.group
local group_data = database.group[group.name]
group_data.fluids[#group_data.fluids + 1] = { class = "fluid", name = name }
-- Fake fuel category
local fuel_category
if prototype.fuel_value > 0 then
fuel_category = { class = "fuel_category", name = constants.fake_fluid_fuel_category }
local fluids = database.fuel_category[constants.fake_fluid_fuel_category].fluids
fluids[#fluids + 1] = { class = "fluid", name = name }
end
-- Save to recipe book
database.fluid[name] = {
burned_in = {},
class = "fluid",
default_temperature = prototype.default_temperature,
fuel_category = fuel_category,
fuel_pollution = prototype.fuel_value > 0
and prototype.emissions_multiplier ~= 1
and prototype.emissions_multiplier
or nil,
fuel_value = prototype.fuel_value > 0 and prototype.fuel_value or nil,
group = { class = "group", name = group.name },
hidden = prototype.hidden,
ingredient_in = {},
mined_from = {},
product_of = {},
prototype_name = name,
pumped_by = {},
recipe_categories = util.unique_obj_array(),
science_packs = {},
subgroup = { class = "group", name = prototype.subgroup.name },
temperatures = {},
unlocked_by = util.unique_obj_array(),
}
-- Don't add strings yet - they will be added in process_temperatures() to improve the ordering
localised_fluids[name] = { name = prototype.localised_name, description = prototype.localised_description }
end
metadata.localised_fluids = localised_fluids
end
-- Adds a fluid temperature definition if one doesn't exist yet
function fluid_proc.add_temperature(fluid_data, temperature_ident)
local temperature_string = temperature_ident.string
local temperatures = fluid_data.temperatures
if not temperatures[temperature_string] then
temperatures[temperature_string] = {
base_fluid = { class = "fluid", name = fluid_data.prototype_name },
class = "fluid",
default_temperature = fluid_data.default_temperature,
fuel_pollution = fluid_data.fuel_pollution,
fuel_value = fluid_data.fuel_value,
group = fluid_data.group,
hidden = fluid_data.hidden,
ingredient_in = {},
mined_from = {},
name = fluid_data.prototype_name .. "." .. temperature_string,
product_of = {},
prototype_name = fluid_data.prototype_name,
recipe_categories = util.unique_obj_array(),
science_packs = {},
subgroup = fluid_data.subgroup,
temperature_ident = temperature_ident,
unlocked_by = util.unique_obj_array(),
}
end
end
-- Returns true if `comp` is within `base`
function fluid_proc.is_within_range(base, comp, flip)
if flip then
return base.min >= comp.min and base.max <= comp.max
else
return base.min <= comp.min and base.max >= comp.max
end
end
function fluid_proc.process_temperatures(database, metadata)
-- Create a new fluids table so insertion order will neatly organize the temperature variants
local new_fluid_table = {}
for fluid_name, fluid_data in pairs(database.fluid) do
new_fluid_table[fluid_name] = fluid_data
local localised = metadata.localised_fluids[fluid_name]
util.add_to_dictionary("fluid", fluid_name, localised.name)
util.add_to_dictionary("fluid_description", fluid_name, localised.description)
local temperatures = fluid_data.temperatures
if temperatures and next(temperatures) then
-- Step 1: Add a variant for the default temperature if one does not exist
local default_temperature = fluid_data.default_temperature
local default_temperature_ident = util.build_temperature_ident({ temperature = default_temperature })
if not temperatures[default_temperature_ident.string] then
fluid_proc.add_temperature(fluid_data, default_temperature_ident)
end
-- Step 2: Sort the temperature variants
local temp = {}
for _, temperature_data in pairs(temperatures) do
table.insert(temp, temperature_data)
end
table.sort(temp, function(temp_a, temp_b)
return util.get_sorting_number(temp_a.temperature_ident) < util.get_sorting_number(temp_b.temperature_ident)
end)
-- Create a new table and insert in order
temperatures = {}
for _, temperature_data in pairs(temp) do
temperatures[temperature_data.name] = temperature_data
-- Add to database and add translation
new_fluid_table[temperature_data.name] = temperature_data
util.add_to_dictionary("fluid", temperature_data.name, {
"",
localised.name,
" (",
{ "format-degrees-c-compact", temperature_data.temperature_ident.string },
")",
})
end
fluid_data.temperatures = temperatures
-- Step 3: Add researched properties to temperature variants
for _, temperature_data in pairs(temperatures) do
temperature_data.enabled_at_start = fluid_data.enabled_at_start
if fluid_data.researched_forces then
temperature_data.researched_forces = {}
end
end
-- Step 4: Add properties from base fluid to temperature variants
-- TODO: This is an idiotic way to do this
for fluid_tbl_name, obj_table_name in pairs({
ingredient_in = "ingredients",
product_of = "products",
mined_from = "products",
}) do
for _, obj_ident in pairs(fluid_data[fluid_tbl_name]) do
local obj_data = database[obj_ident.class][obj_ident.name]
-- Get the matching fluid
local fluid_ident
-- This is kind of a slow way to do it, but I don't really care
for _, material_ident in pairs(obj_data[obj_table_name]) do
if material_ident.name == fluid_name then
fluid_ident = material_ident
break
end
end
-- Get the temperature identifier from the material table
local temperature_ident = fluid_ident.temperature_ident
if temperature_ident then
-- Change the name of the material and remove the identifier
fluid_ident.name = fluid_ident.name .. "." .. temperature_ident.string
fluid_ident.temperature_ident = nil
elseif obj_table_name == "products" then
-- Change the name of the material to the default temperature
fluid_ident.name = fluid_ident.name .. "." .. default_temperature_ident.string
fluid_ident.temperature_ident = nil
-- Use the default temperature for matching
temperature_ident = default_temperature_ident
end
-- Iterate over all temperature variants and compare their constraints
for _, temperature_data in pairs(temperatures) do
if
not temperature_ident
or fluid_proc.is_within_range(
temperature_data.temperature_ident,
temperature_ident,
fluid_tbl_name == "ingredient_in"
)
then
-- Add to recipes table
temperature_data[fluid_tbl_name][#temperature_data[fluid_tbl_name] + 1] = obj_ident
-- Recipe-specific logic
if obj_ident.class == "recipe" then
-- Add recipe category
local recipe_categories = temperature_data.recipe_categories
recipe_categories[#recipe_categories + 1] = table.shallow_copy(obj_data.recipe_category)
-- If in product_of, append to unlocked_by
-- Also add this fluid to that tech's `unlocks fluids` table
-- This is to avoid variants being "unlocked" when you can't actually get them
-- If this is an "empty X barrel" recipe, ignore it
if fluid_tbl_name == "product_of" and not string.find(obj_ident.name, "^empty%-.+%-barrel$") then
local temp_unlocked_by = temperature_data.unlocked_by
for _, technology_ident in pairs(obj_data.unlocked_by) do
temp_unlocked_by[#temp_unlocked_by + 1] = technology_ident
local technology_data = database.technology[technology_ident.name]
-- Don't use fluid_ident becuase it has an amount
technology_data.unlocks_fluids[#technology_data.unlocks_fluids + 1] = {
class = "fluid",
name = temperature_data.name,
}
end
end
end
end
end
end
end
-- Step 5: If this variant is not produced by anything, unlock with the base fluid
for _, temperature_data in pairs(temperatures) do
if #temperature_data.product_of == 0 and #temperature_data.unlocked_by == 0 then
temperature_data.unlocked_by = table.deep_copy(fluid_data.unlocked_by)
for _, technology_ident in pairs(fluid_data.unlocked_by) do
local technology_data = database.technology[technology_ident.name]
-- Don't use fluid_ident becuase it has an amount
technology_data.unlocks_fluids[#technology_data.unlocks_fluids + 1] = {
class = "fluid",
name = temperature_data.name,
}
end
end
end
end
end
database.fluid = new_fluid_table
end
-- When calling the module directly, call fluid_proc.build
setmetatable(fluid_proc, {
__call = function(_, ...)
return fluid_proc.build(...)
end,
})
return fluid_proc

View File

@@ -0,0 +1,51 @@
local constants = require("constants")
local fake_name = constants.fake_fluid_fuel_category
local util = require("scripts.util")
local fuel_category_proc = {}
function fuel_category_proc.build(database)
-- Add the actual fuel categories
for name, prototype in pairs(global.prototypes.fuel_category) do
database.fuel_category[name] = {
class = "fuel_category",
enabled_at_start = true,
fluids = {}, -- Will always be empty
items = util.unique_obj_array({}),
prototype_name = name,
}
util.add_to_dictionary("fuel_category", name, prototype.localised_name)
util.add_to_dictionary("fuel_category_description", name, prototype.localised_description)
end
-- Add our fake fuel category for fluids
database.fuel_category[fake_name] = {
class = "fuel_category",
enabled_at_start = true,
fluids = util.unique_obj_array({}),
items = {}, -- Will always be empty
prototype_name = fake_name,
}
end
function fuel_category_proc.check_fake_category(database)
local category = database.fuel_category[fake_name]
if #category.fluids > 0 then
-- Add translations
util.add_to_dictionary("fuel_category", fake_name, { "fuel-category-name." .. fake_name })
util.add_to_dictionary("fuel_category_description", fake_name, { "fuel-category-description." .. fake_name })
else
-- Remove the category
database.fuel_category[fake_name] = nil
end
end
-- When calling the module directly, call fuel_category_proc.build
setmetatable(fuel_category_proc, {
__call = function(_, ...)
return fuel_category_proc.build(...)
end,
})
return fuel_category_proc

View File

@@ -0,0 +1,34 @@
local util = require("scripts.util")
return function(database)
for name, prototype in pairs(global.prototypes.generator) do
local fluid_box = prototype.fluidbox_prototypes[1]
local can_burn = {}
local fuel_categories = {}
if fluid_box.filter then
can_burn = { { class = "fluid", name = fluid_box.filter.name } }
else
fuel_categories = { { class = "fuel_category", name = "burnable-fluid" } }
end
database.entity[name] = {
base_pollution = prototype.emissions_per_second > 0 and prototype.emissions_per_second or nil,
blueprintable = util.is_blueprintable(prototype),
can_burn = can_burn,
class = "entity",
entity_type = { class = "entity_type", name = prototype.type },
fluid_consumption = prototype.fluid_usage_per_tick * 60,
fuel_categories = fuel_categories,
max_energy_production = prototype.max_energy_production,
maximum_temperature = prototype.maximum_temperature,
minimum_temperature = fluid_box.minimum_temperature,
placed_by = util.process_placed_by(prototype),
prototype_name = name,
science_packs = {},
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,16 @@
local util = require("scripts.util")
return function(database)
for name, prototype in pairs(global.prototypes.item_group) do
database.group[name] = {
class = "group",
enabled_at_start = true,
fluids = util.unique_obj_array({}),
items = util.unique_obj_array({}),
prototype_name = name,
recipes = util.unique_obj_array({}),
}
util.add_to_dictionary("group", name, prototype.localised_name)
-- NOTE: Groups do not have descriptions
end
end

View File

@@ -0,0 +1,20 @@
local util = require("scripts.util")
return function(database)
for name, prototype in pairs(global.prototypes.item) do
local type = prototype.type
local type_data = database.item_type[type]
if not type_data then
type_data = {
class = "item_type",
items = {},
prototype_name = type,
}
database.item_type[type] = type_data
util.add_to_dictionary("item_type", type, { "item-type." .. type })
util.add_to_dictionary("item_type_description", type, { "item-type-description." .. type })
end
table.insert(type_data.items, { class = "item", name = name })
end
end

View File

@@ -0,0 +1,210 @@
local table = require("__flib__.table")
local util = require("scripts.util")
local item_proc = {}
function item_proc.build(database, metadata)
local modules = {}
local place_as_equipment_results = {}
local place_results = {}
local rocket_launch_payloads = {}
for name, prototype in pairs(global.prototypes.item) do
-- Group
local group = prototype.group
local group_data = database.group[group.name]
group_data.items[#group_data.items + 1] = { class = "item", name = name }
-- Rocket launch products
local launch_products = {}
for i, product in ipairs(prototype.rocket_launch_products or {}) do
-- Add to products table w/ amount string
local amount_ident = util.build_amount_ident(product)
launch_products[i] = {
class = product.type,
name = product.name,
amount_ident = amount_ident,
}
-- Add to payloads table
local product_payloads = rocket_launch_payloads[product.name]
local ident = { class = "item", name = name }
if product_payloads then
product_payloads[#product_payloads + 1] = ident
else
rocket_launch_payloads[product.name] = { ident }
end
end
local default_categories =
util.unique_string_array(#launch_products > 0 and table.shallow_copy(metadata.rocket_silo_categories) or {})
local place_as_equipment_result = prototype.place_as_equipment_result
if place_as_equipment_result then
place_as_equipment_result = { class = "equipment", name = place_as_equipment_result.name }
place_as_equipment_results[name] = place_as_equipment_result
end
local place_result = prototype.place_result
if place_result and database.entity[place_result.name] then
place_result = { class = "entity", name = place_result.name }
place_results[name] = place_result
else
place_result = nil
end
local burnt_result = prototype.burnt_result
if burnt_result then
burnt_result = { class = "item", name = burnt_result.name }
end
local equipment_categories = util.unique_obj_array()
local equipment = util.unique_obj_array()
local equipment_grid = prototype.equipment_grid
if equipment_grid then
for _, equipment_category in pairs(equipment_grid.equipment_categories) do
table.insert(equipment_categories, { class = "equipment_category", name = equipment_category })
local category_data = database.equipment_category[equipment_category]
if category_data then
for _, equipment_ident in pairs(category_data.equipment) do
table.insert(equipment, equipment_ident)
local equipment_data = database.equipment[equipment_ident.name]
if equipment_data then
equipment_data.placed_in[#equipment_data.placed_in + 1] = { class = "item", name = name }
end
end
end
end
end
local fuel_value = prototype.fuel_value
local has_fuel_value = prototype.fuel_value > 0
local fuel_acceleration_multiplier = prototype.fuel_acceleration_multiplier
local fuel_emissions_multiplier = prototype.fuel_emissions_multiplier
local fuel_top_speed_multiplier = prototype.fuel_top_speed_multiplier
local module_effects = {}
if prototype.type == "module" then
-- Add to internal list of modules
modules[name] = table.invert(prototype.limitations)
-- Process effects
for effect_name, effect in pairs(prototype.module_effects or {}) do
module_effects[#module_effects + 1] = {
type = "plain",
label = effect_name .. "_bonus",
value = effect.bonus,
formatter = "percent",
}
end
-- Process which beacons this module is compatible with
for beacon_name in pairs(global.prototypes.beacon) do
local beacon_data = database.entity[beacon_name]
local allowed_effects = metadata.beacon_allowed_effects[beacon_name]
local compatible = true
if allowed_effects then
for effect_name in pairs(prototype.module_effects or {}) do
if not allowed_effects[effect_name] then
compatible = false
break
end
end
end
if compatible then
beacon_data.accepted_modules[#beacon_data.accepted_modules + 1] = { class = "item", name = name }
end
end
-- Process which crafters this module is compatible with
for crafter_name in pairs(global.prototypes.crafter) do
local crafter_data = database.entity[crafter_name]
local allowed_effects = metadata.allowed_effects[crafter_name]
local compatible = true
if allowed_effects then
for effect_name in pairs(prototype.module_effects or {}) do
if not allowed_effects[effect_name] then
compatible = false
break
end
end
end
if compatible then
crafter_data.accepted_modules[#crafter_data.accepted_modules + 1] = { class = "item", name = name }
end
end
end
local fuel_category = util.convert_to_ident("fuel_category", prototype.fuel_category)
if fuel_category then
local items = database.fuel_category[fuel_category.name].items
items[#items + 1] = { class = "item", name = name }
end
--- @class ItemData
database.item[name] = {
accepted_equipment = equipment,
affects_recipes = {},
burned_in = {},
burnt_result = burnt_result,
burnt_result_of = {},
class = "item",
enabled_at_start = metadata.gathered_from[name] and true or false,
equipment_categories = equipment_categories,
fuel_acceleration_multiplier = has_fuel_value
and fuel_acceleration_multiplier ~= 1
and fuel_acceleration_multiplier
or nil,
fuel_category = fuel_category,
fuel_emissions_multiplier = has_fuel_value and fuel_emissions_multiplier ~= 1 and fuel_emissions_multiplier
or nil,
fuel_top_speed_multiplier = has_fuel_value and fuel_top_speed_multiplier ~= 1 and fuel_top_speed_multiplier
or nil,
fuel_value = has_fuel_value and fuel_value or nil,
gathered_from = metadata.gathered_from[name],
group = { class = "group", name = group.name },
hidden = prototype.has_flag("hidden"),
ingredient_in = {},
item_type = { class = "item_type", name = prototype.type },
mined_from = {},
module_category = util.convert_to_ident("module_category", prototype.category),
module_effects = module_effects,
place_as_equipment_result = place_as_equipment_result,
place_result = place_result,
product_of = {},
prototype_name = name,
recipe_categories = default_categories,
researched_in = {},
rocket_launch_product_of = {},
rocket_launch_products = launch_products,
science_packs = {},
stack_size = prototype.stack_size,
subgroup = { class = "group", name = prototype.subgroup.name },
unlocked_by = util.unique_obj_array(),
}
util.add_to_dictionary("item", name, prototype.localised_name)
util.add_to_dictionary("item_description", name, prototype.localised_description)
end
-- Add rocket launch payloads to their material tables
for product, payloads in pairs(rocket_launch_payloads) do
local product_data = database.item[product]
product_data.rocket_launch_product_of = table.array_copy(payloads)
for i = 1, #payloads do
local payload = payloads[i]
local payload_data = database.item[payload.name]
local payload_unlocked_by = payload_data.unlocked_by
for j = 1, #payload_unlocked_by do
product_data.unlocked_by[#product_data.unlocked_by + 1] = payload_unlocked_by[j]
end
end
end
metadata.modules = modules
metadata.place_as_equipment_results = place_as_equipment_results
metadata.place_results = place_results
end
-- When calling the module directly, call fluid_proc.build
setmetatable(item_proc, {
__call = function(_, ...)
return item_proc.build(...)
end,
})
return item_proc

View File

@@ -0,0 +1,41 @@
local table = require("__flib__.table")
local util = require("scripts.util")
return function(database)
for name, prototype in pairs(global.prototypes.lab) do
-- Add to items
for _, item_name in ipairs(prototype.lab_inputs) do
local item_data = database.item[item_name]
if item_data then
item_data.researched_in[#item_data.researched_in + 1] = { class = "entity", name = name }
end
end
local fuel_categories, fuel_filter = util.process_energy_source(prototype)
database.entity[name] = {
blueprintable = util.is_blueprintable(prototype),
can_burn = {},
class = "entity",
entity_type = { class = "entity_type", name = prototype.type },
fuel_categories = fuel_categories,
fuel_filter = fuel_filter,
hidden = prototype.has_flag("hidden"),
inputs = table.map(prototype.lab_inputs, function(v)
return { class = "item", name = v }
end),
module_slots = prototype.module_inventory_size
and prototype.module_inventory_size > 0
and prototype.module_inventory_size
or nil,
placed_by = util.process_placed_by(prototype),
prototype_name = name,
researching_speed = prototype.researching_speed,
science_packs = {},
size = util.get_size(prototype),
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,65 @@
local util = require("scripts.util")
local mining_drill_proc = {}
function mining_drill_proc.build(database)
for name, prototype in pairs(global.prototypes.mining_drill) do
for category in pairs(prototype.resource_categories) do
local category_data = database.resource_category[category]
category_data.mining_drills[#category_data.mining_drills + 1] = { class = "entity", name = name }
end
local fuel_categories, fuel_filter = util.process_energy_source(prototype)
database.entity[name] = {
blueprintable = util.is_blueprintable(prototype),
can_burn = {},
class = "entity",
enabled = true,
entity_type = { class = "entity_type", name = prototype.type },
fuel_categories = fuel_categories,
fuel_filter = fuel_filter,
mining_area = math.ceil(prototype.mining_drill_radius * 2),
mining_speed = prototype.mining_speed,
module_slots = prototype.module_inventory_size
and prototype.module_inventory_size > 0
and prototype.module_inventory_size
or nil,
placed_by = util.process_placed_by(prototype),
prototype_name = name,
resource_categories_lookup = prototype.resource_categories,
resource_categories = util.convert_categories(prototype.resource_categories, "resource_category"),
science_packs = {},
size = util.get_size(prototype),
supports_fluid = #prototype.fluidbox_prototypes > 0,
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
end
end
function mining_drill_proc.add_resources(database)
for name in pairs(global.prototypes.mining_drill) do
local drill_data = database.entity[name]
local can_mine = util.unique_obj_array()
for category in pairs(drill_data.resource_categories_lookup) do
local category_data = database.resource_category[category]
for _, resource_ident in pairs(category_data.resources) do
local resource_data = database.resource[resource_ident.name]
if not resource_data.required_fluid or drill_data.supports_fluid then
can_mine[#can_mine + 1] = resource_ident
end
end
end
drill_data.can_mine = can_mine
end
end
-- When calling the module directly, call fluid_proc.build
setmetatable(mining_drill_proc, {
__call = function(_, ...)
return mining_drill_proc.build(...)
end,
})
return mining_drill_proc

View File

@@ -0,0 +1,53 @@
local util = require("scripts.util")
local offshore_pump_proc = {}
function offshore_pump_proc.build(database)
-- Iterate offshore pumps
for name, prototype in pairs(global.prototypes.offshore_pump) do
-- Add to material
local fluid = prototype.fluid
local fluid_data = database.fluid[fluid.name]
if fluid_data then
fluid_data.pumped_by[#fluid_data.pumped_by + 1] = { class = "entity", name = name }
end
database.entity[name] = {
blueprintable = util.is_blueprintable(prototype),
class = "entity",
enabled = true,
entity_type = { class = "entity_type", name = prototype.type },
fluid = { class = "fluid", name = fluid.name },
hidden = prototype.has_flag("hidden"),
placed_by = util.process_placed_by(prototype),
prototype_name = name,
pumping_speed = prototype.pumping_speed * 60,
science_packs = {},
size = util.get_size(prototype),
unlocked_by = {},
}
util.add_to_dictionary("entity", name, prototype.localised_name)
util.add_to_dictionary("entity_description", name, prototype.localised_description)
end
end
function offshore_pump_proc.check_enabled_at_start(database)
for name in pairs(global.prototypes.offshore_pump) do
local pump_data = database.entity[name]
if not pump_data.researched_forces then
local fluid_data = database.fluid[pump_data.fluid.name]
fluid_data.researched_forces = nil
fluid_data.science_packs = {}
fluid_data.unlocked_by = {}
end
end
end
-- When calling the module directly, call fluid_proc.build
setmetatable(offshore_pump_proc, {
__call = function(_, ...)
return offshore_pump_proc.build(...)
end,
})
return offshore_pump_proc

View File

@@ -0,0 +1,16 @@
local util = require("scripts.util")
return function(database)
for name, prototype in pairs(global.prototypes.recipe_category) do
database.recipe_category[name] = {
class = "recipe_category",
enabled_at_start = true,
fluids = util.unique_obj_array({}),
items = util.unique_obj_array({}),
prototype_name = name,
recipes = util.unique_obj_array({}),
}
util.add_to_dictionary("recipe_category", name, prototype.localised_name)
util.add_to_dictionary("recipe_category_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,126 @@
local math = require("__flib__.math")
local constants = require("constants")
local util = require("scripts.util")
local fluid_proc = require("scripts.database.fluid")
return function(database, metadata)
for name, prototype in pairs(global.prototypes.recipe) do
local category = prototype.category
local group = prototype.group
local enabled_at_start = prototype.enabled
-- Add to recipe category
local category_data = database.recipe_category[category]
category_data.recipes[#category_data.recipes + 1] = { class = "recipe", name = name }
-- Add to group
local group_data = database.group[group.name]
group_data.recipes[#group_data.recipes + 1] = { class = "recipe", name = name }
local data = {
accepted_modules = {},
class = "recipe",
enabled_at_start = enabled_at_start,
energy = prototype.energy,
group = { class = "group", name = group.name },
hidden = prototype.hidden,
made_in = {},
pollution_multiplier = prototype.emissions_multiplier ~= 1 and prototype.emissions_multiplier or nil,
prototype_name = name,
recipe_category = { class = "recipe_category", name = category },
science_packs = {},
subgroup = { class = "group", name = prototype.subgroup.name },
unlocked_by = {},
used_as_fixed_recipe = metadata.fixed_recipes[name],
}
-- Ingredients / products
local fluids = { ingredients = 0, products = 0 }
for lookup_type, io_type in pairs({ ingredient_in = "ingredients", product_of = "products" }) do
local output = {}
for i, material in ipairs(prototype[io_type]) do
local amount_ident = util.build_amount_ident(material)
local material_io_data = {
class = material.type,
name = material.name,
amount_ident = amount_ident,
}
local material_data = database[material.type][material.name]
local lookup_table = material_data[lookup_type]
lookup_table[#lookup_table + 1] = { class = "recipe", name = name }
output[i] = material_io_data
material_data.recipe_categories[#material_data.recipe_categories + 1] = {
class = "recipe_category",
name = category,
}
-- Don't set enabled at start if this is an ignored recipe
local disabled = constants.disabled_categories.recipe_category[category]
if io_type == "products" and (not disabled or disabled ~= 0) then
local subtable = category_data[material.type .. "s"]
subtable[#subtable + 1] = { class = material.type, name = material.name }
if enabled_at_start then
material_data.enabled_at_start = true
end
end
if material.type == "fluid" then
-- Fluid temperatures
local temperature_ident = util.build_temperature_ident(material)
if temperature_ident then
material_io_data.temperature_ident = temperature_ident
fluid_proc.add_temperature(database.fluid[material.name], temperature_ident)
end
-- Add to aggregate
fluids[io_type] = fluids[io_type] + 1
end
end
data[io_type] = output
end
-- Made in
local num_item_ingredients = 0
for _, ingredient in pairs(prototype.ingredients) do
if ingredient.type == "item" then
num_item_ingredients = num_item_ingredients + 1
end
end
for _, crafters in pairs({ global.prototypes.character, global.prototypes.crafter }) do
for crafter_name in pairs(crafters) do
local crafter_data = database.entity[crafter_name]
local fluidbox_counts = metadata.crafter_fluidbox_counts[crafter_name] or { inputs = 0, outputs = 0 }
if
(crafter_data.ingredient_limit or 255) >= num_item_ingredients
and crafter_data.recipe_categories_lookup[category]
and fluidbox_counts.inputs >= fluids.ingredients
and fluidbox_counts.outputs >= fluids.products
then
local crafting_time = math.round(prototype.energy / crafter_data.crafting_speed, 0.01)
data.made_in[#data.made_in + 1] = {
class = "entity",
name = crafter_name,
amount_ident = util.build_amount_ident({ amount = crafting_time, format = "format_seconds_parenthesis" }),
}
crafter_data.can_craft[#crafter_data.can_craft + 1] = { class = "recipe", name = name }
end
end
end
-- Compatible modules
for module_name, module_limitations in pairs(metadata.modules) do
if not next(module_limitations) or module_limitations[name] then
data.accepted_modules[#data.accepted_modules + 1] = { class = "item", name = module_name }
table.insert(database.item[module_name].affects_recipes, { class = "recipe", name = name })
end
end
database.recipe[name] = data
util.add_to_dictionary("recipe", name, prototype.localised_name)
util.add_to_dictionary("recipe_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,15 @@
local util = require("scripts.util")
return function(database)
for name, prototype in pairs(global.prototypes.resource_category) do
database.resource_category[name] = {
class = "resource_category",
enabled_at_start = true,
mining_drills = {},
prototype_name = name,
resources = util.unique_obj_array({}),
}
util.add_to_dictionary("resource_category", name, prototype.localised_name)
util.add_to_dictionary("resource_category_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,79 @@
local fluid_proc = require("scripts.database.fluid")
local util = require("scripts.util")
return function(database)
--- @type LuaCustomTable<string, LuaEntityPrototype>
local prototypes = global.prototypes.resource
for name, prototype in pairs(prototypes) do
local products = prototype.mineable_properties.products
if products then
for _, product in ipairs(products) do
local product_data = database[product.type][product.name]
if product_data then
product_data.mined_from[#product_data.mined_from + 1] = { class = "resource", name = name }
end
end
end
local required_fluid
local mineable_properties = prototype.mineable_properties
if mineable_properties.required_fluid then
required_fluid = {
class = "fluid",
name = mineable_properties.required_fluid,
-- Ten mining operations per amount consumed, so divide by 10 to get the actual number
amount_ident = util.build_amount_ident({ amount = mineable_properties.fluid_amount / 10 }),
}
else
-- TODO: Validate that it's hand-mineable by checking character mineable categories (requires an API addition)
-- Enable resource items that are hand-minable
for _, product in ipairs(mineable_properties.products or {}) do
if product.type == "item" then
local product_data = database[product.type][product.name]
product_data.enabled_at_start = true
end
end
end
local products = {}
for i, product in pairs(mineable_properties.products or {}) do
products[i] = {
class = product.type,
name = product.name,
amount_ident = util.build_amount_ident(product),
}
-- Fluid temperatures
local temperature_ident = product.type == "fluid" and util.build_temperature_ident(product) or nil
if temperature_ident then
products[i].temperature_ident = temperature_ident
fluid_proc.add_temperature(database.fluid[product.name], temperature_ident)
end
end
local mined_by = {}
local resource_category = prototype.resource_category
for drill_name in pairs(global.prototypes.mining_drill) do
local drill_data = database.entity[drill_name]
if
drill_data.resource_categories_lookup[resource_category]
and (not required_fluid or drill_data.supports_fluid)
then
mined_by[#mined_by + 1] = { class = "entity", name = drill_name }
end
end
local resource_category_data = database.resource_category[resource_category]
resource_category_data.resources[#resource_category_data.resources + 1] = { class = "resource", name = name }
database.resource[name] = {
class = "resource",
mined_by = mined_by,
mining_time = mineable_properties.mining_time,
products = products,
prototype_name = name,
resource_category = { class = "resource_category", name = resource_category },
required_fluid = required_fluid,
}
util.add_to_dictionary("resource", name, prototype.localised_name)
util.add_to_dictionary("resource_description", name, prototype.localised_description)
end
end

View File

@@ -0,0 +1,17 @@
local util = require("scripts.util")
return function(database)
--- @type table<string, LuaItemPrototype>
local prototypes = global.prototypes.item
for name, prototype in pairs(prototypes) do
if prototype.type == "tool" then
database.science_pack[name] = {
class = "science_pack",
order = prototype.order,
prototype_name = name,
}
util.add_to_dictionary("science_pack", name, prototype.localised_name)
util.add_to_dictionary("science_pack_description", name, prototype.localised_description)
end
end
end

View File

@@ -0,0 +1,180 @@
local math = require("__flib__.math")
local table = require("__flib__.table")
local constants = require("constants")
local util = require("scripts.util")
local function insert_science_packs(database, obj_data, science_packs)
if #science_packs == 0 then
return
end
local existing = obj_data.science_packs
local existing_len = #existing
-- If there are no existing science packs
if #obj_data.science_packs == 0 then
obj_data.science_packs = science_packs
return
end
local existing_highest_ident = existing[existing_len]
local existing_highest_data = database.science_pack[existing_highest_ident.name]
local new_highest_ident = science_packs[#science_packs]
local new_highest_data = database.science_pack[new_highest_ident.name]
-- The object should show when the fewest possible science packs are enabled
if existing_highest_data.order > new_highest_data.order then
obj_data.science_packs = science_packs
end
end
return function(database, metadata)
for name, prototype in pairs(global.prototypes.technology) do
local unlocks_equipment = util.unique_obj_array()
local unlocks_fluids = util.unique_obj_array()
local unlocks_items = util.unique_obj_array()
local unlocks_entities = util.unique_obj_array()
local unlocks_recipes = util.unique_obj_array()
local research_ingredients_per_unit = {}
-- Research units and ingredients per unit
for _, ingredient in ipairs(prototype.research_unit_ingredients) do
research_ingredients_per_unit[#research_ingredients_per_unit + 1] = {
class = ingredient.type,
name = ingredient.name,
amount_ident = util.build_amount_ident({ amount = ingredient.amount }),
}
end
local research_unit_count
local formula = prototype.research_unit_count_formula
if not formula then
research_unit_count = prototype.research_unit_count
end
local science_packs = table.map(prototype.research_unit_ingredients, function(pack)
return { class = "science_pack", name = pack.name }
end)
-- Unlocks recipes, materials, entities
for _, modifier in ipairs(prototype.effects) do
if modifier.type == "unlock-recipe" then
local recipe_data = database.recipe[modifier.recipe]
-- Check if the category should be ignored for recipe availability
local disabled = constants.disabled_categories.recipe_category[recipe_data.recipe_category.name]
if not disabled or disabled ~= 0 then
insert_science_packs(database, recipe_data, science_packs)
recipe_data.unlocked_by[#recipe_data.unlocked_by + 1] = { class = "technology", name = name }
recipe_data.researched_forces = {}
unlocks_recipes[#unlocks_recipes + 1] = { class = "recipe", name = modifier.recipe }
for _, product in pairs(recipe_data.products) do
local product_name = product.name
local product_data = database[product.class][product_name]
local product_ident = { class = product_data.class, name = product_data.prototype_name }
-- For "empty X barrel" recipes, do not unlock the fluid with the recipe
-- This is to avoid fluids getting "unlocked" when they are in reality still 100 hours away
local is_empty_barrel_recipe = string.find(modifier.recipe, "^empty%-.+%-barrel$")
if product_data.class ~= "fluid" or not is_empty_barrel_recipe then
product_data.researched_forces = {}
insert_science_packs(database, product_data, science_packs)
product_data.unlocked_by[#product_data.unlocked_by + 1] = { class = "technology", name = name }
end
-- Materials
if product_data.class == "item" then
unlocks_items[#unlocks_items + 1] = product_ident
elseif product_data.class == "fluid" and not is_empty_barrel_recipe then
unlocks_fluids[#unlocks_fluids + 1] = product_ident
end
-- Entities
local place_result = metadata.place_results[product_name]
if place_result then
local entity_data = database.entity[place_result.name]
if entity_data then
entity_data.researched_forces = {}
insert_science_packs(database, entity_data, science_packs)
entity_data.unlocked_by[#entity_data.unlocked_by + 1] = { class = "technology", name = name }
unlocks_entities[#unlocks_entities + 1] = place_result
end
end
-- Equipment
local place_as_equipment_result = metadata.place_as_equipment_results[product_name]
if place_as_equipment_result then
local equipment_data = database.equipment[place_as_equipment_result.name]
if equipment_data then
equipment_data.researched_forces = {}
insert_science_packs(database, equipment_data, science_packs)
equipment_data.unlocked_by[#equipment_data.unlocked_by + 1] = { class = "technology", name = name }
unlocks_equipment[#unlocks_equipment + 1] = place_as_equipment_result
end
end
end
end
end
end
local level = prototype.level
local max_level = prototype.max_level
database.technology[name] = {
class = "technology",
hidden = prototype.hidden,
max_level = max_level,
min_level = level,
prerequisite_of = {},
prerequisites = {},
prototype_name = name,
researched_forces = {},
research_ingredients_per_unit = research_ingredients_per_unit,
research_unit_count_formula = formula,
research_unit_count = research_unit_count,
research_unit_energy = prototype.research_unit_energy / 60,
science_packs = science_packs,
unlocks_entities = unlocks_entities,
unlocks_equipment = unlocks_equipment,
unlocks_fluids = unlocks_fluids,
unlocks_items = unlocks_items,
unlocks_recipes = unlocks_recipes,
upgrade = prototype.upgrade,
}
-- Assemble name
local localised_name
if level ~= max_level then
localised_name = {
"",
prototype.localised_name,
" (" .. level .. "-" .. (max_level == math.max_uint and "" or max_level) .. ")",
}
else
localised_name = prototype.localised_name
end
util.add_to_dictionary("technology", prototype.name, localised_name)
util.add_to_dictionary("technology_description", name, prototype.localised_description)
end
-- Generate prerequisites and prerequisite_of
for name, technology in pairs(database.technology) do
local prototype = global.prototypes.technology[name]
if prototype.prerequisites then
for prerequisite_name in pairs(prototype.prerequisites) do
technology.prerequisites[#technology.prerequisites + 1] = { class = "technology", name = prerequisite_name }
local prerequisite_data = database.technology[prerequisite_name]
prerequisite_data.prerequisite_of[#prerequisite_data.prerequisite_of + 1] = {
class = "technology",
name = name,
}
end
end
end
end

View File

@@ -0,0 +1,645 @@
--[[
DESIGN NOTES:
- Amount strings are not pre-processed, but are generated as part of the format call (allowing for locale differences)
- Multiple caches:
- Base caption
- Base tooltip
- Tooltip contents
- Amount strings
- Control hints
- The output is assembled from these individual caches
- Perhaps the final outputs should be cached as well?
- The idea here is to avoid re-generating the entire caption and tooltip when just the amount or control hints are
different
- Consider moving the show / don't show logic to `util` instead of `formatter`, so it can be used elsewhere
- Per-instance settings:
- show_glyphs
- show_tooltip_details
- amount_only
- is_label: show_glyphs = false, show_tooltip_details = false
]]
local flib_format = require("__flib__.format")
local math = require("__flib__.math")
local table = require("__flib__.table")
local constants = require("constants")
local database = require("scripts.database")
local caches = {}
local formatter = {}
local function build_cache_key(...)
return table.concat(
table.map({ ... }, function(v)
return tostring(v)
end),
"."
)
end
local function expand_string(source, ...)
local arg = { ... }
for i = 1, #arg do
source = string.gsub(source, "__" .. i .. "__", arg[i])
end
return source
end
local function rich_text(key, value, inner)
return "["
.. key
.. "="
.. (key == "color" and constants.colors[value].str or value)
.. "]"
.. inner
.. "[/"
.. key
.. "]"
end
local function sprite(class, name)
return "[img=" .. class .. "/" .. name .. "]"
end
local function control(content, action)
return "\n" .. rich_text("color", "info", rich_text("font", "default-semibold", content .. ":")) .. " " .. action
end
local function number(value)
return flib_format.number(math.round(value, 0.01))
end
local function temperature(value, gui_translations)
return expand_string(gui_translations.format_degrees, number(value))
end
local function area(value, gui_translations)
if type(value) == "number" then
local formatted = number(value)
return expand_string(gui_translations.format_area, formatted, formatted)
else
return expand_string(gui_translations.format_area, number(value.width), number(value.height))
end
end
local function energy(value, gui_translations)
return flib_format.number(value * 60, true, 3) .. gui_translations.si_watt
end
local function energy_storage(value, gui_translations)
return flib_format.number(value, true, 2) .. gui_translations.si_joule
end
local function fuel_value(value, gui_translations)
return flib_format.number(value, true, 3) .. gui_translations.si_joule
end
local function percent(value, gui_translations)
return expand_string(gui_translations.format_percent, number(value * 100))
end
local function seconds(value, gui_translations)
return expand_string(gui_translations.format_seconds, number(value * 60))
end
local function seconds_from_ticks(value, gui_translations)
return seconds(value / 60, gui_translations)
end
local function per_second(value, gui_translations)
return number(value) .. " " .. gui_translations.per_second_suffix
end
local function object(obj, _, player_data, options)
local obj_data = database[obj.class][obj.name]
local obj_options = options and table.shallow_copy(options) or {}
obj_options.amount_ident = obj.amount_ident
local info = formatter(obj_data, player_data, obj_options)
if info then
return info.caption
end
end
local function get_amount_string(amount_ident, player_data, options)
local cache_key = build_cache_key(
"amount_string",
amount_ident.amount,
amount_ident.amount_min,
amount_ident.amount_max,
amount_ident.catalyst_amount,
amount_ident.probability,
amount_ident.format,
options.amount_only,
options.rocket_parts_required
)
local cache = caches[player_data.player_index]
local cached = cache[cache_key]
if cached then
return cached
end
local amount = amount_ident.amount
local output
if options.amount_only then
output = amount_ident.amount and tostring(math.round(amount, 0.1))
or "~" .. math.round((amount_ident.amount_min + amount_ident.amount_max) / 2, 0.1)
else
local gui_translations = player_data.translations.gui
-- Amount
local format_string = gui_translations[amount_ident.format]
if amount then
output = expand_string(format_string, number(amount))
else
output = expand_string(format_string, number(amount_ident.amount_min) .. " - " .. number(amount_ident.amount_max))
end
-- Catalyst amount
local catalyst = amount_ident.catalyst_amount
if catalyst then
output = gui_translations.catalyst_abbrev .. " " .. output
end
-- Probability
local probability = amount_ident.probability
if probability and probability < 1 then
output = math.round(probability * 100, 0.01) .. "% " .. output
end
-- Rocket parts required
-- Hardcoded to always use the `amount` formatter
if options.rocket_parts_required then
output = expand_string(gui_translations.format_amount, options.rocket_parts_required) .. " " .. output
end
end
cache[cache_key] = output
return output
end
local function get_caption(obj_data, obj_properties, player_data, options)
local settings = player_data.settings
local gui_translations = player_data.translations.gui
local prototype_name = obj_data.prototype_name
local name = obj_data.name or prototype_name
local cache = caches[player_data.player_index]
local cache_key =
build_cache_key("caption", obj_data.class, name, obj_properties.enabled, obj_properties.hidden, options.hide_glyph)
local cached = cache[cache_key]
if cached then
return cached
end
local class = obj_data.class
local before = ""
if settings.general.captions.show_glyphs and not options.hide_glyph then
before = rich_text(
"font",
"RecipeBook",
constants.class_to_font_glyph[class] or constants.class_to_font_glyph[class]
) .. " "
end
if obj_properties.hidden then
before = before .. rich_text("font", "default-semibold", gui_translations.hidden_abbrev) .. " "
end
if not obj_properties.enabled then
before = before .. rich_text("font", "default-semibold", gui_translations.disabled_abbrev) .. " "
end
local type = constants.class_to_type[class]
if type then
before = before .. sprite(type, prototype_name) .. " "
end
local after
if settings.general.captions.show_internal_names then
after = name
else
after = player_data.translations[class][name] or name
end
local output = { before = before, after = after }
cache[cache_key] = output
return output
end
local function get_base_tooltip(obj_data, obj_properties, player_data, options)
options = options or {}
local settings = player_data.settings
local gui_translations = player_data.translations.gui
local show_internal_names = settings.general.captions.show_internal_names
local prototype_name = obj_data.prototype_name
local name = obj_data.name or prototype_name
local class = obj_data.class
local type = constants.class_to_type[class]
local catalyst_amount = options.amount_ident and options.amount_ident.catalyst_amount or false
local cache = caches[player_data.player_index]
local cache_key = build_cache_key(
"base_tooltip",
obj_data.class,
name,
obj_properties.enabled,
obj_properties.hidden,
obj_properties.researched,
catalyst_amount
)
local cached = cache[cache_key]
if cached then
return cached
end
local before
if type then
before = sprite(type, prototype_name) .. " "
else
before = ""
end
local name_str
if show_internal_names then
name_str = name
else
name_str = player_data.translations[class][name]
end
local after = rich_text("font", "default-semibold", rich_text("color", "heading", name_str)) .. "\n"
if settings.general.tooltips.show_alternate_name then
local alternate_name
if show_internal_names then
alternate_name = player_data.translations[class][name]
else
alternate_name = name
end
after = after .. rich_text("color", "green", alternate_name) .. "\n"
end
if catalyst_amount then
after = after
.. rich_text("font", "default-semibold", gui_translations.catalyst_amount .. ":")
.. " "
.. number(catalyst_amount)
.. "\n"
end
if settings.general.tooltips.show_descriptions then
local description = player_data.translations[class .. "_description"][name]
if description then
after = after .. description .. "\n"
end
end
after = after .. rich_text("color", "info", gui_translations[class])
if not obj_properties.researched then
after = after .. " | " .. rich_text("color", "unresearched", gui_translations.unresearched)
end
if not obj_properties.enabled then
after = after .. " | " .. gui_translations.disabled
end
if obj_properties.hidden then
after = after .. " | " .. gui_translations.hidden
end
local output = { before = before, after = after }
cache[cache_key] = output
return output
end
local function get_tooltip_deets(obj_data, player_data)
local gui_translations = player_data.translations.gui
local cache = caches[player_data.player_index]
local cache_key = build_cache_key("tooltip_deets", obj_data.class, obj_data.name or obj_data.prototype_name)
local cached = cache[cache_key]
if cached then
return cached
end
local deets_structure = constants.tooltips[obj_data.class]
local output = ""
for _, deet in pairs(deets_structure) do
if deet.source ~= "group" then
local values
local type = deet.type
if type == "plain" then
values = { obj_data[deet.source] }
elseif type == "list" then
values = table.array_copy(obj_data[deet.source] or {})
end
local values_output = ""
for _, value in pairs(values) do
local fmtr = deet.formatter
if fmtr then
value = formatter[fmtr](value, gui_translations, player_data, deet.options)
end
if value then
if type == "plain" then
values_output = values_output .. " " .. value
elseif type == "list" then
values_output = values_output .. "\n " .. value
end
end
end
if #values_output > 0 then
output = output
.. "\n"
.. rich_text("font", "default-semibold", gui_translations[deet.label or deet.source] .. ":")
.. values_output
end
end
end
cache[cache_key] = output
return output
end
local function get_interaction_helps(obj_data, player_data, options)
local gui_translations = player_data.translations.gui
local show_interaction_helps = player_data.settings.general.tooltips.show_interaction_helps
local cache = caches[player_data.player_index]
local cache_key = build_cache_key(
"interaction_helps",
obj_data.class,
obj_data.name or obj_data.prototype_name,
options.blueprint_result and options.blueprint_result.name .. (options.blueprint_result.recipe or "") or nil
)
local cached = cache[cache_key]
if cached then
return cached
end
local helps_output = ""
local interactions = constants.interactions[obj_data.class]
local num_interactions = 0
for _, interaction in pairs(interactions) do
local test = interaction.test
if not test or test(obj_data, options) then
local source = interaction.source
if not source or obj_data[source] then
num_interactions = num_interactions + 1
if show_interaction_helps then
local action = gui_translations[interaction.label or interaction.action]
local input_name = table.reduce(interaction.modifiers, function(acc, modifier)
return acc .. modifier .. "_"
end, "") .. "click"
local button = interaction.button
if button then
button = button .. "_"
else
button = ""
end
local label = rich_text(
"font",
"default-semibold",
rich_text("color", "info", gui_translations[button .. input_name] .. ": ")
)
helps_output = helps_output .. "\n" .. label .. action
end
end
end
end
local output = { output = helps_output, num_interactions = num_interactions }
cache[cache_key] = output
return output
end
local function get_obj_properties(obj_data, player_data, options)
-- Player data
local force = player_data.force
local player_settings = player_data.settings
local show_hidden = player_settings.general.content.show_hidden
local show_unresearched = player_settings.general.content.show_unresearched
local show_disabled = player_settings.general.content.show_disabled
-- Actually get object properties
local researched
if obj_data.enabled_at_start then
researched = true
elseif obj_data.researched_forces then
researched = obj_data.researched_forces[force.index] or false
else
researched = true
end
local enabled = true
-- We have to get the current enabled status from the object itself
-- Recipes are unlocked by "enabling" them, so only check a recipe if it's researched
if obj_data.class == "recipe" and researched then
enabled = player_data.force_recipes[obj_data.prototype_name].enabled
elseif obj_data.class == "technology" then
enabled = player_data.force_technologies[obj_data.prototype_name].enabled
elseif obj_data.enabled ~= nil then
enabled = obj_data.enabled
end
local obj_properties = { hidden = obj_data.hidden or false, researched = researched, enabled = enabled }
-- Determine if we should show this object
local should_show = false
if options.always_show then
should_show = true
elseif
(show_hidden or not obj_properties.hidden)
and (show_unresearched or obj_properties.researched)
and (show_disabled or obj_properties.enabled)
then
-- Indexing the plurals table is much faster than looping through the canonical table
if constants.category_class_plurals[obj_data.class] then
-- Check if this category is enabled
if player_settings.categories[obj_data.class][obj_data.prototype_name] then
should_show = true
end
else
-- Check categories
local good_categories = 0
local has_categories = 0
for _, category in pairs(constants.category_classes) do
local obj_category = obj_data[category]
local obj_categories = obj_data[constants.category_class_plurals[category]]
if obj_category then
has_categories = has_categories + 1
if player_settings.categories[category][obj_category.name] then
good_categories = good_categories + 1
end
elseif obj_categories and #obj_categories > 0 then -- Empty category lists pass by default
has_categories = has_categories + 1
local category_settings = player_settings.categories[category]
if constants.category_all_match[category] then
-- All categories must be enabled
local matched_all = true
for _, category_ident in pairs(obj_categories) do
if not category_settings[category_ident.name] then
matched_all = false
break
end
end
if matched_all then
good_categories = good_categories + 1
end
else
-- At least one category must be enabled
for _, category_ident in pairs(obj_categories) do
if category_settings[category_ident.name] then
good_categories = good_categories + 1
break
end
end
end
end
end
if good_categories == has_categories then
should_show = true
end
end
end
return should_show and obj_properties or false
end
--- @class FormatOptions
local available_options = {
hide_glyphs = false,
base_tooltip_only = false,
label_only = true,
is_label = false,
--- @type AmountIdent|boolean
amount_ident = false,
--- @type number|boolean
rocket_parts_required = false,
amount_only = false,
}
--- @param options FormatOptions
function formatter.format(obj_data, player_data, options)
options = table.deep_merge({ available_options, options or {} })
if options.is_label then
options.hide_glyph = true
options.base_tooltip_only = true
end
local obj_properties = get_obj_properties(obj_data, player_data, options)
if not obj_properties then
return false
end
local amount_ident = options.amount_ident
-- Caption
local caption_output
if amount_ident and options.amount_only then
caption_output = get_amount_string(amount_ident, player_data, options)
else
local caption = get_caption(obj_data, obj_properties, player_data, options)
if amount_ident then
caption_output = caption.before
.. rich_text("font", "default-semibold", get_amount_string(amount_ident, player_data, options))
.. " "
.. caption.after
else
caption_output = caption.before .. caption.after
end
end
-- Tooltip
local base_tooltip = get_base_tooltip(obj_data, obj_properties, player_data, options)
local tooltip_output
if amount_ident and options.amount_only then
tooltip_output = base_tooltip.before
.. rich_text(
"font",
"default-bold",
rich_text("color", "heading", get_amount_string(amount_ident, player_data, {}))
)
.. " "
.. base_tooltip.after
else
tooltip_output = base_tooltip.before .. base_tooltip.after
end
local settings = player_data.settings
if settings.general.tooltips.show_detailed_tooltips and not options.base_tooltip_only then
tooltip_output = tooltip_output .. get_tooltip_deets(obj_data, player_data)
end
local num_interactions = 0
if not options.base_tooltip_only then
local helps_output = get_interaction_helps(obj_data, player_data, options)
tooltip_output = tooltip_output .. helps_output.output
num_interactions = helps_output.num_interactions
end
return {
caption = caption_output,
disabled = not obj_properties.enabled,
hidden = obj_properties.hidden,
num_interactions = num_interactions,
researched = obj_properties.researched,
tooltip = tooltip_output,
}
end
function formatter.create_cache(player_index)
caches[player_index] = {}
end
function formatter.create_all_caches()
for i in pairs(global.players) do
caches[i] = {}
end
end
function formatter.build_player_data(player, player_table)
return {
force = player.force,
force_recipes = player.force.recipes,
force_technologies = player.force.technologies,
player_index = player.index,
settings = player_table.settings,
translations = player_table.translations,
}
end
formatter.area = area
formatter.build_cache_key = build_cache_key
formatter.control = control
formatter.energy = energy
formatter.energy_storage = energy_storage
formatter.expand_string = expand_string
formatter.fuel_value = fuel_value
formatter.number = number
formatter.object = object
formatter.percent = percent
formatter.per_second = per_second
formatter.rich_text = rich_text
formatter.seconds_from_ticks = seconds_from_ticks
formatter.seconds = seconds
formatter.sprite = sprite
formatter.temperature = temperature
setmetatable(formatter, {
__call = function(_, ...)
return formatter.format(...)
end,
})
return formatter

View File

@@ -0,0 +1,62 @@
local table = require("__flib__.table")
local constants = require("constants")
local global_data = {}
function global_data.init()
global.forces = {}
global.players = {}
global.prototypes = {}
end
function global_data.build_prototypes()
global.forces = table.shallow_copy(game.forces)
local prototypes = {}
for key, filters in pairs(constants.prototypes.filtered_entities) do
prototypes[key] = table.shallow_copy(game.get_filtered_entity_prototypes(filters))
end
for _, type in pairs(constants.prototypes.straight_conversions) do
prototypes[type] = table.shallow_copy(game[type .. "_prototypes"])
end
global.prototypes = prototypes
end
function global_data.update_sync_data()
global.sync_data = {
active_mods = script.active_mods,
settings = table.map(settings.startup, function(v)
return v
end),
}
end
function global_data.add_force(force)
table.insert(global.forces, force)
end
function global_data.check_should_load()
local sync_data = global.sync_data or { active_mods = {}, settings = {} }
if
global.prototypes
and table.deep_compare(sync_data.active_mods, script.active_mods)
and table.deep_compare(sync_data.settings, settings.startup)
then
for _, prototypes in pairs(global.prototypes) do
for _, prototype in pairs(prototypes) do
if not prototype.valid then
return false
end
end
end
return true
end
return false
end
return global_data

View File

@@ -0,0 +1,304 @@
local math = require("__flib__.math")
local on_tick_n = require("__flib__.on-tick-n")
local constants = require("constants")
local database = require("scripts.database")
local gui_util = require("scripts.gui.util")
local util = require("scripts.util")
local actions = {}
--- @param Gui InfoGui
function actions.set_as_active(Gui, _, _)
Gui.player_table.guis.info._active_id = Gui.id
end
--- @param Gui InfoGui
--- @param e on_gui_click
function actions.reset_location(Gui, _, e)
if e.button == defines.mouse_button_type.middle then
Gui.refs.root.force_auto_center()
end
end
--- @param Gui InfoGui
function actions.close(Gui, _, _)
Gui:destroy()
end
--- @param Gui InfoGui
function actions.bring_to_front(Gui, _, _)
if not Gui.state.docked then
Gui.refs.root.bring_to_front()
end
end
--- @param Gui InfoGui
function actions.toggle_search(Gui, _, _)
local state = Gui.state
local refs = Gui.refs
local opened = state.search_opened
state.search_opened = not opened
local search_button = refs.titlebar.search_button
local search_textfield = refs.titlebar.search_textfield
if opened then
search_button.sprite = "utility/search_white"
search_button.style = "frame_action_button"
search_textfield.visible = false
if state.search_query ~= "" then
-- Reset query
search_textfield.text = ""
state.search_query = ""
-- Refresh page
Gui:update_contents()
end
else
-- Show search textfield
search_button.sprite = "utility/search_black"
search_button.style = "flib_selected_frame_action_button"
search_textfield.visible = true
search_textfield.focus()
end
end
--- @param Gui InfoGui
--- @param msg table
--- @param e on_gui_click
function actions.navigate(Gui, msg, e)
-- Update position in history
local delta = msg.delta
local history = Gui.state.history
if e.shift then
if delta < 0 then
history._index = 1
else
history._index = #history
end
else
history._index = math.clamp(history._index + delta, 1, #history)
end
Gui:update_contents()
end
--- @param Gui InfoGui
--- @param msg table
--- @param e on_gui_text_changed
function actions.update_search_query(Gui, msg, e)
local state = Gui.state
local id = msg.id
local query = string.lower(e.element.text)
-- Fuzzy search
if Gui.player_table.settings.general.search.fuzzy_search then
query = string.gsub(query, ".", "%1.*")
end
-- Input sanitization
for pattern, replacement in pairs(constants.input_sanitizers) do
query = string.gsub(query, pattern, replacement)
end
-- Save query
state.search_query = query
-- Remove scheduled update if one exists
if state.update_results_ident then
on_tick_n.remove(state.update_results_ident)
state.update_results_ident = nil
end
if query == "" then
-- Update now
Gui:update_contents({ refresh = true })
else
-- Update in a while
state.update_results_ident = on_tick_n.add(
game.tick + constants.search_timeout,
{ gui = "info", id = id, action = "update_search_results", player_index = e.player_index }
)
end
end
--- @param Gui InfoGui
function actions.update_search_results(Gui, _, _)
-- Update based on query
Gui:update_contents({ refresh = true })
end
--- @param Gui InfoGui
--- @param e on_gui_click
function actions.navigate_to(Gui, _, e)
local context = gui_util.navigate_to(e)
if context then
if e.button == defines.mouse_button_type.middle then
INFO_GUI.build(Gui.player, Gui.player_table, context)
else
Gui:update_contents({ new_context = context })
end
end
end
--- @param Gui InfoGui
--- @param msg table
function actions.navigate_to_plain(Gui, msg, _)
Gui:update_contents({ new_context = msg.context })
end
--- @param Gui InfoGui
function actions.open_in_tech_window(Gui, _, _)
Gui.player_table.flags.technology_gui_open = true
Gui.player.open_technology_gui(Gui:get_context().name)
end
--- @param Gui InfoGui
function actions.go_to_base_fluid(Gui, _, _)
local base_fluid = database.fluid[Gui:get_context().name].prototype_name
Gui:update_contents({ new_context = { class = "fluid", name = base_fluid } })
end
--- @param Gui InfoGui
function actions.toggle_quick_ref(Gui, _, _)
local player = Gui.player
-- Toggle quick ref GUI
local name = Gui:get_context().name
--- @type QuickRefGui?
local QuickRefGui = util.get_gui(player.index, "quick_ref", name)
local to_state = false
if QuickRefGui then
QuickRefGui:destroy()
else
to_state = true
QUICK_REF_GUI.build(player, Gui.player_table, name)
end
-- Update all quick ref buttons
for _, InfoGui in pairs(INFO_GUI.find_open_context(Gui.player_table, Gui:get_context())) do
InfoGui:dispatch({
action = "update_header_button",
button = "quick_ref_button",
to_state = to_state,
})
end
end
--- @param Gui InfoGui
function actions.toggle_favorite(Gui, _, _)
local player_table = Gui.player_table
local favorites = player_table.favorites
local context = Gui:get_context()
local combined_name = context.class .. "." .. context.name
local to_state
if favorites[combined_name] then
to_state = false
favorites[combined_name] = nil
else
-- Copy the table instead of passing a reference
favorites[combined_name] = { class = context.class, name = context.name }
to_state = true
end
for _, InfoGui in pairs(INFO_GUI.find_open_context(Gui.player_table, context)) do
InfoGui:dispatch({ action = "update_header_button", button = "favorite_button", to_state = to_state })
end
local SearchGui = util.get_gui(Gui.player.index, "search")
if SearchGui and SearchGui.refs.window.visible then
SearchGui:dispatch("update_favorites")
end
end
--- @param Gui InfoGui
--- @param msg table
function actions.update_header_button(Gui, msg, _)
local button = Gui.refs.header[msg.button]
if msg.to_state then
button.style = "flib_selected_tool_button"
button.tooltip = constants.header_button_tooltips[msg.button].selected
else
button.style = "tool_button"
button.tooltip = constants.header_button_tooltips[msg.button].unselected
end
end
--- @param Gui InfoGui
--- @param msg table
function actions.open_list(Gui, msg, _)
local list_context = msg.context
local source = msg.source
local list = database[list_context.class][list_context.name][source]
if list and #list > 0 then
local first_obj = list[1]
OPEN_PAGE(Gui.player, Gui.player_table, {
class = first_obj.class,
name = first_obj.name,
list = {
context = list_context,
index = 1,
source = source,
},
})
end
end
--- @param Gui InfoGui
--- @param msg table
function actions.toggle_collapsed(Gui, msg, _)
local context = msg.context
local component_index = msg.component_index
local component_ident = constants.pages[context.class][component_index]
if component_ident then
local state = Gui.state.components[component_index]
if state then
state.collapsed = not state.collapsed
Gui:update_contents({ refresh = true })
end
end
end
--- @param Gui InfoGui
--- @param msg table
function actions.change_tech_level(Gui, msg, _)
local context = Gui:get_context()
local state = Gui.state
local context_data = database[context.class][context.name]
local min = context_data.min_level
local max = context_data.max_level
local new_level = math.clamp(state.selected_tech_level + msg.delta, min, max)
if new_level ~= state.selected_tech_level then
state.selected_tech_level = new_level
Gui:update_contents({ refresh = true })
end
end
--- @param Gui InfoGui
function actions.detach_window(Gui, _, _)
local state = Gui.state
-- Just in case
if not state.docked then
return
end
local context = Gui:get_context()
-- Close this GUI and create a detached one
Gui:destroy()
OPEN_PAGE(Gui.player, Gui.player_table, context)
end
--- @param Gui InfoGui
function actions.print_object(Gui, _, _)
local context = Gui:get_context()
local obj_data = database[context.class][context.name]
if obj_data then
if __DebugAdapter then
__DebugAdapter.print(obj_data)
Gui.player.print("Object data has been printed to the debug console.")
else
log(serpent.block(obj_data))
Gui.player.print("Object data has been printed to the log file.")
end
end
end
return actions

View File

@@ -0,0 +1,653 @@
local gui = require("__flib__.gui")
local table = require("__flib__.table")
local constants = require("constants")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local player_data = require("scripts.player-data")
local util = require("scripts.util")
local components = {
list_box = require("scripts.gui.info.list-box"),
table = require("scripts.gui.info.table"),
}
local function tool_button(sprite, tooltip, ref, action, style_mods)
return {
type = "sprite-button",
style = "tool_button",
style_mods = style_mods,
sprite = sprite,
tooltip = tooltip,
mouse_button_filter = { "left" },
ref = ref,
actions = {
on_click = action,
},
}
end
--- @class InfoGui
local Gui = {}
local actions = require("scripts.gui.info.actions")
function Gui:dispatch(msg, e)
-- Mark this GUI as the active one whenever we do anything
self.player_table.guis.info._active_id = self.id
if type(msg) == "string" then
actions[msg](self, msg, e)
else
actions[msg.action](self, msg, e)
end
end
function Gui:destroy()
self.refs.window.destroy()
self.player_table.guis.info[self.id] = nil
if self.state.docked and not self.state.search_info then
self.player_table.guis.info._relative_id = nil
end
end
function Gui:get_context()
local history = self.state.history
return history[history._index]
end
function Gui:update_contents(options)
options = options or {}
local new_context = options.new_context
local refresh = options.refresh
local state = self.state
local refs = self.refs
-- HISTORY
-- Add new history if needed
local history = state.history
if new_context then
-- Remove all entries after this
for i = history._index + 1, #history do
history[i] = nil
end
-- Insert new entry
local new_index = #history + 1
history[new_index] = new_context
history._index = new_index
-- Limit the length
local max_size = constants.session_history_size
if new_index > max_size then
history._index = max_size
for _ = max_size + 1, new_index do
table.remove(history, 1)
end
end
end
local context = new_context or history[history._index]
if not refresh then
player_data.update_global_history(self.player_table.global_history, context)
local SearchGui = util.get_gui(self.player.index, "search")
if SearchGui then
SearchGui:dispatch("update_history")
end
end
-- COMMON DATA
local obj_data = database[context.class][context.name]
local player_data = formatter.build_player_data(self.player, self.player_table)
local gui_translations = player_data.translations.gui
-- TECH LEVEL
if not refresh and obj_data.research_unit_count_formula then
state.selected_tech_level = player_data.force.technologies[context.name].level
end
-- TITLEBAR
-- Nav buttons
-- Generate tooltips
local history_index = history._index
local history_len = #history
local entries = {}
for i, history_context in ipairs(history) do
local obj_data = database[history_context.class][history_context.name]
local info = formatter(obj_data, player_data, { always_show = true, label_only = true })
local caption = info.caption
if not info.researched then
caption = formatter.rich_text("color", "unresearched", caption)
end
entries[history_len - (i - 1)] = formatter.rich_text(
"font",
"default-semibold",
formatter.rich_text("color", history_index == i and "green" or "invisible", ">")
) .. " " .. caption
end
local entries = table.concat(entries, "\n")
local base_tooltip = formatter.rich_text(
"font",
"default-bold",
formatter.rich_text("color", "heading", gui_translations.session_history)
) .. "\n" .. entries
-- Apply button properties
local nav_backward_button = refs.titlebar.nav_backward_button
if history._index == 1 then
nav_backward_button.enabled = false
nav_backward_button.sprite = "rb_nav_backward_disabled"
else
nav_backward_button.enabled = true
nav_backward_button.sprite = "rb_nav_backward_white"
end
nav_backward_button.tooltip = base_tooltip
.. formatter.control(gui_translations.click, gui_translations.go_backward)
.. formatter.control(gui_translations.shift_click, gui_translations.go_to_the_back)
local nav_forward_button = refs.titlebar.nav_forward_button
if history._index == #history then
nav_forward_button.enabled = false
nav_forward_button.sprite = "rb_nav_forward_disabled"
else
nav_forward_button.enabled = true
nav_forward_button.sprite = "rb_nav_forward_white"
end
nav_forward_button.tooltip = base_tooltip
.. formatter.control(gui_translations.click, gui_translations.go_forward)
.. formatter.control(gui_translations.shift_click, gui_translations.go_to_the_front)
-- Label
local label = refs.titlebar.label
label.caption = gui_translations[context.class]
-- Reset search when moving pages
if not options.refresh and state.search_opened then
state.search_opened = false
local search_button = refs.titlebar.search_button
local search_textfield = refs.titlebar.search_textfield
search_button.sprite = "utility/search_white"
search_button.style = "frame_action_button"
search_textfield.visible = false
if state.search_query ~= "" then
-- Reset query
search_textfield.text = ""
state.search_query = ""
end
end
-- HEADER
-- List navigation
-- List nav is kind of weird because it doesn't respect your settings, but making it respect the settings would be
-- too much work
local list_context = context.list
if list_context then
local source = list_context.context
local source_data = database[source.class][source.name]
local list = source_data[list_context.source]
local list_len = #list
local index = list_context.index
local list_refs = refs.header.list_nav
list_refs.flow.visible = true
-- Labels
local source_info = formatter(source_data, player_data, { always_show = true })
local source_label = list_refs.source_label
source_label.caption = formatter.rich_text("color", "heading", source_info.caption)
.. " - "
.. gui_translations[list_context.source]
local position_label = list_refs.position_label
position_label.caption = " (" .. index .. " / " .. list_len .. ")"
-- Buttons
for delta, button in pairs({ [-1] = list_refs.back_button, [1] = list_refs.forward_button }) do
local new_index = index + delta
if new_index < 1 then
new_index = list_len
elseif new_index > list_len then
new_index = 1
end
local ident = list[new_index]
gui.set_action(button, "on_click", {
gui = "info",
id = self.id,
action = "navigate_to_plain",
context = {
class = ident.class,
name = ident.name,
list = {
context = source,
index = new_index,
source = list_context.source,
},
},
})
end
refs.header.line.visible = true
else
refs.header.list_nav.flow.visible = false
refs.header.line.visible = false
end
-- Label
local title_info = formatter(obj_data, player_data, { always_show = true, is_label = true })
local label = refs.header.label
label.caption = title_info.caption
label.tooltip = title_info.tooltip
label.style = title_info.researched and "rb_toolbar_label" or "rb_unresearched_toolbar_label"
-- Buttons
if context.class == "technology" then
refs.header.open_in_tech_window_button.visible = true
else
refs.header.open_in_tech_window_button.visible = false
end
if context.class == "fluid" and obj_data.temperature_ident then
local base_fluid_button = refs.header.go_to_base_fluid_button
base_fluid_button.visible = true
gui.set_action(base_fluid_button, "on_click", {
gui = "info",
id = self.id,
action = "navigate_to_plain",
context = obj_data.base_fluid,
})
else
refs.header.go_to_base_fluid_button.visible = false
end
if context.class == "recipe" then
local button = refs.header.quick_ref_button
button.visible = true
local is_selected = self.player_table.guis.quick_ref[context.name]
button.style = is_selected and "flib_selected_tool_button" or "tool_button"
button.tooltip = { "gui.rb-" .. (is_selected and "close" or "open") .. "-quick-ref-window" }
else
refs.header.quick_ref_button.visible = false
end
local favorite_button = refs.header.favorite_button
if self.player_table.favorites[context.class .. "." .. context.name] then
favorite_button.style = "flib_selected_tool_button"
favorite_button.tooltip = { "gui.rb-remove-from-favorites" }
else
favorite_button.style = "tool_button"
favorite_button.tooltip = { "gui.rb-add-to-favorites" }
end
-- PAGE
local pane = refs.page_scroll_pane
local page_refs = refs.page_components
local page_settings = self.player_table.settings.pages[context.class]
local i = 0
local visible = false
local component_variables = {
context = context,
gui_id = self.id,
search_query = state.search_query,
selected_tech_level = state.selected_tech_level,
}
-- Add or update relevant components
for _, component_ident in pairs(constants.pages[context.class]) do
i = i + 1
local component = components[component_ident.type]
local component_refs = page_refs[i]
if not component_refs or component_refs.type ~= component_ident.type then
-- Destroy old elements
if component_refs then
component_refs.root.destroy()
end
-- Create new elements
component_refs = component.build(pane, i, component_ident, component_variables)
component_refs.type = component_ident.type
page_refs[i] = component_refs
end
local component_settings = page_settings[component_ident.label or component_ident.source]
if not refresh then
state.components[i] = component.default_state(component_settings)
end
component_variables.component_index = i
component_variables.component_state = state.components[i]
local comp_visible =
component.update(component_ident, component_refs, obj_data, player_data, component_settings, component_variables)
visible = visible or comp_visible
end
-- Destroy extraneous components
for j = i + 1, #page_refs do
page_refs[j].root.destroy()
page_refs[j] = nil
end
-- Show error frame if nothing is visible
if not visible and not state.warning_shown then
state.warning_shown = true
pane.visible = false
refs.page_frame.style = "rb_inside_warning_frame"
refs.page_frame.style.vertically_stretchable = state.docked and state.search_info
refs.warning_flow.visible = true
if state.search_query == "" then
refs.warning_text.caption = { "gui.rb-no-content-warning" }
else
refs.warning_text.caption = { "gui.rb-no-results" }
end
elseif visible and state.warning_shown then
state.warning_shown = false
pane.visible = true
refs.page_frame.style = "inside_shallow_frame"
refs.page_frame.style.vertically_stretchable = state.docked and state.search_info
refs.warning_flow.visible = false
end
end
local index = {}
--- @param player LuaPlayer
--- @param player_table PlayerTable
--- @param context Context
--- @param options table?
function index.build(player, player_table, context, options)
options = options or {}
local id = player_table.guis.info._next_id
player_table.guis.info._next_id = id + 1
local root_elem = options.parent or player.gui.screen
local search_info = root_elem.name == "rb_search_window" or root_elem.name == "rb_visual_search_window"
local relative = options.parent and not search_info
local refs = gui.build(root_elem, {
{
type = "frame",
style_mods = { minimal_width = 430, maximal_width = 600 },
direction = "vertical",
ref = { "window" },
anchor = options.anchor,
actions = {
on_click = { gui = "info", id = id, action = "set_as_active" },
on_closed = { gui = "info", id = id, action = "close" },
},
{
type = "flow",
style = "flib_titlebar_flow",
ref = { "titlebar", "flow" },
actions = {
on_click = not relative and {
gui = search_info and "search" or "info",
id = not search_info and id or nil,
action = "reset_location",
} or nil,
},
util.frame_action_button(
"rb_nav_backward",
nil,
{ "titlebar", "nav_backward_button" },
{ gui = "info", id = id, action = "navigate", delta = -1 }
),
util.frame_action_button(
"rb_nav_forward",
nil,
{ "titlebar", "nav_forward_button" },
{ gui = "info", id = id, action = "navigate", delta = 1 }
),
{
type = "label",
style = "frame_title",
style_mods = { left_margin = 4 },
ignored_by_interaction = true,
ref = { "titlebar", "label" },
},
{
type = "empty-widget",
style = relative and "flib_horizontal_pusher" or "flib_titlebar_drag_handle",
ignored_by_interaction = true,
},
{
type = "textfield",
style_mods = {
top_margin = -3,
right_padding = 3,
width = 120,
},
clear_and_focus_on_right_click = true,
visible = false,
ref = { "titlebar", "search_textfield" },
actions = {
on_text_changed = { gui = "info", id = id, action = "update_search_query" },
},
},
util.frame_action_button(
"utility/search",
{ "gui.rb-search-instruction" },
{ "titlebar", "search_button" },
{ gui = "info", id = id, action = "toggle_search" }
),
options.parent and util.frame_action_button(
"rb_detach",
{ "gui.rb-detach-instruction" },
nil,
{ gui = "info", id = id, action = "detach_window" }
) or {},
util.frame_action_button(
"utility/close",
{ "gui.close" },
{ "titlebar", "close_button" },
{ gui = "info", id = id, action = "close" }
),
},
{
type = "frame",
style = "inside_shallow_frame",
style_mods = { vertically_stretchable = search_info },
direction = "vertical",
ref = { "page_frame" },
action = {
on_click = { gui = "info", id = id, action = "set_as_active" },
},
{
type = "frame",
style = "rb_subheader_frame",
direction = "vertical",
{
type = "flow",
style_mods = { vertical_align = "center" },
visible = false,
ref = { "header", "list_nav", "flow" },
action = {
on_click = { gui = "info", id = id, action = "set_as_active" },
},
tool_button(
"rb_list_nav_backward_black",
{ "gui.rb-go-backward" },
{ "header", "list_nav", "back_button" },
nil,
{ padding = 3 }
),
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "label",
style = "bold_label",
style_mods = { horizontally_squashable = true },
ref = { "header", "list_nav", "source_label" },
},
{
type = "label",
style = "bold_label",
style_mods = { font_color = constants.colors.info.tbl },
ref = { "header", "list_nav", "position_label" },
},
{ type = "empty-widget", style = "flib_horizontal_pusher" },
tool_button(
"rb_list_nav_forward_black",
{ "gui.rb-go-forward" },
{ "header", "list_nav", "forward_button" },
nil,
{ padding = 3 }
),
},
{
type = "line",
style = "rb_dark_line",
direction = "horizontal",
visible = false,
ref = { "header", "line" },
},
{
type = "flow",
style_mods = { vertical_align = "center" },
{ type = "label", style = "rb_toolbar_label", ref = { "header", "label" } },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
__DebugAdapter and tool_button(nil, "Print", nil, { gui = "info", id = id, action = "print_object" }) or {},
tool_button(
"rb_technology_gui_black",
{ "gui.rb-open-in-technology-window" },
{ "header", "open_in_tech_window_button" },
{ gui = "info", id = id, action = "open_in_tech_window" }
),
tool_button(
"rb_fluid_black",
{ "gui.rb-view-base-fluid" },
{ "header", "go_to_base_fluid_button" },
{ gui = "info", id = id, action = "go_to_base_fluid" }
),
tool_button(
"rb_clipboard_black",
{ "gui.rb-toggle-quick-ref-window" },
{ "header", "quick_ref_button" },
{ gui = "info", id = id, action = "toggle_quick_ref" }
),
tool_button(
"rb_favorite_black",
{ "gui.rb-add-to-favorites" },
{ "header", "favorite_button" },
{ gui = "info", id = id, action = "toggle_favorite" }
),
},
},
{
type = "scroll-pane",
style = "rb_page_scroll_pane",
style_mods = { maximal_height = 900 },
ref = { "page_scroll_pane" },
action = {
on_click = { gui = "info", id = id, action = "set_as_active" },
},
},
{
type = "flow",
style = "rb_warning_flow",
direction = "vertical",
visible = false,
ref = { "warning_flow" },
{
type = "label",
style = "bold_label",
caption = { "gui.rb-no-content-warning" },
ref = { "warning_text" },
},
},
},
},
})
if options.parent then
refs.root = root_elem
else
refs.root = refs.window
refs.root.force_auto_center()
end
if not options.parent or search_info then
refs.titlebar.flow.drag_target = refs.root
end
refs.page_components = {}
--- @class InfoGui
local self = {
id = id,
player = player,
player_table = player_table,
refs = refs,
state = {
components = {},
docked = options.parent and true or false,
history = { _index = 0 },
search_info = search_info,
search_opened = false,
search_query = "",
selected_tech_level = 0,
warning_shown = false,
},
}
index.load(self)
player_table.guis.info[id] = self
player_table.guis.info._active_id = id
if options.anchor then
player_table.guis.info._relative_id = id
end
self:update_contents({ new_context = context })
end
function index.load(self)
setmetatable(self, { __index = Gui })
end
--- Find all info GUIs that are viewing the given context.
--- @param player_table PlayerTable
--- @param context Context
--- @return table<number, InfoGui>
function index.find_open_context(player_table, context)
local open = {}
for id, Gui in pairs(player_table.guis.info) do
if not constants.ignored_info_ids[id] then
local state = Gui.state
local opened_context = state.history[state.history._index]
if opened_context and opened_context.class == context.class and opened_context.name == context.name then
open[id] = Gui
end
end
end
return open
end
-- function root.update_all(player, player_table)
-- for id in pairs(player_table.guis.info) do
-- if not constants.ignored_info_ids[id] then
-- root.update_contents(player, player_table, id, { refresh = true })
-- end
-- end
-- end
-- function root.bring_all_to_front(player_table)
-- for id, gui_data in pairs(player_table.guis.info) do
-- if not constants.ignored_info_ids[id] then
-- if gui_data.state.docked then
-- if gui_data.state.search_info then
-- gui_data.refs.root.bring_to_front()
-- end
-- else
-- gui_data.refs.window.bring_to_front()
-- end
-- end
-- end
-- end
return index

View File

@@ -0,0 +1,190 @@
local gui = require("__flib__.gui")
local constants = require("constants")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local list_box = {}
function list_box.build(parent, index, component, variables)
return gui.build(parent, {
{
type = "flow",
direction = "vertical",
index = index,
ref = { "root" },
action = {
on_click = { gui = "info", id = variables.gui_id, action = "set_as_active" },
},
{
type = "flow",
style_mods = { vertical_align = "center" },
action = {
on_click = { gui = "info", id = variables.gui_id, action = "set_as_active" },
},
{ type = "label", style = "rb_list_box_label", ref = { "label" } },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "sprite-button",
style = "mini_button_aligned_to_text_vertically_when_centered",
tooltip = { "gui.rb-open-list-in-new-window" },
sprite = "rb_export_black",
ref = { "open_list_button" },
-- NOTE: Actions are set in the update function
},
{
type = "sprite-button",
style = "mini_button_aligned_to_text_vertically_when_centered",
ref = { "expand_collapse_button" },
-- NOTE: Sprite, tooltip, and action are set in the update function
},
},
{
type = "frame",
style = "deep_frame_in_shallow_frame",
{
type = "scroll-pane",
style = "rb_list_box_scroll_pane",
ref = { "scroll_pane" },
},
},
},
})
end
function list_box.default_state(settings)
return { collapsed = settings.default_state == "collapsed" }
end
function list_box.update(component, refs, context_data, player_data, settings, variables)
-- Scroll pane
local scroll = refs.scroll_pane
local children = scroll.children
-- Settings and variables
local always_show = component.always_show
local context = variables.context
local query = variables.search_query
local search_type = player_data.settings.general.search.search_type
-- Add items
local i = 0 -- The "added" index
local iterator = component.use_pairs and pairs or ipairs
local objects = settings.default_state ~= "hidden" and context_data[component.source] or {}
for _, obj in iterator(objects) do
local translation = player_data.translations[obj.class][obj.name]
-- Match against search string
local matched
if search_type == "both" then
matched = string.find(string.lower(obj.name), query) or string.find(string.lower(translation), query)
elseif search_type == "internal" then
matched = string.find(string.lower(obj.name), query)
elseif search_type == "localised" then
matched = string.find(string.lower(translation), query)
end
if matched then
local obj_data = database[obj.class][obj.name]
local blueprint_result
if context.class == "recipe" and component.source == "made_in" and obj_data.blueprintable then
blueprint_result = { name = obj.name, recipe = context.name }
elseif context.class == "entity" and component.source == "can_craft" and context_data.blueprintable then
blueprint_result = { name = context.name, recipe = obj.name }
end
local info = formatter(obj_data, player_data, {
always_show = always_show,
amount_ident = obj.amount_ident,
blueprint_result = blueprint_result,
rocket_parts_required = obj_data.rocket_parts_required,
})
if info then
i = i + 1
local style = info.researched and "rb_list_box_item" or "rb_unresearched_list_box_item"
local item = children[i]
if item then
item.style = style
item.caption = info.caption
item.tooltip = info.tooltip
item.enabled = info.num_interactions > 0
gui.update_tags(
item,
{ blueprint_result = blueprint_result, context = { class = obj.class, name = obj.name } }
)
else
gui.add(scroll, {
type = "button",
style = style,
caption = info.caption,
tooltip = info.tooltip,
enabled = info.num_interactions > 0,
mouse_button_filter = { "left", "middle" },
tags = {
blueprint_result = blueprint_result,
context = { class = obj.class, name = obj.name },
},
actions = {
on_click = { gui = "info", id = variables.gui_id, action = "navigate_to" },
},
})
end
end
end
end
-- Destroy extraneous items
for j = i + 1, #children do
children[j].destroy()
end
-- Set listbox properties
if i > 0 then
refs.root.visible = true
local translations = player_data.translations.gui
-- Update label caption
refs.label.caption = formatter.expand_string(
translations.list_box_label,
translations[component.source] or component.source,
i
)
-- Update open list button
if i > 1 then
refs.open_list_button.visible = true
gui.set_action(refs.open_list_button, "on_click", {
gui = "info",
id = variables.gui_id,
action = "open_list",
context = variables.context,
source = component.source,
})
else
refs.open_list_button.visible = false
end
-- Update expand/collapse button and height
gui.set_action(refs.expand_collapse_button, "on_click", {
gui = "info",
id = variables.gui_id,
action = "toggle_collapsed",
context = variables.context,
component_index = variables.component_index,
})
if variables.component_state.collapsed then
refs.expand_collapse_button.sprite = "rb_collapsed"
scroll.style.maximal_height = 1
refs.expand_collapse_button.tooltip = { "gui.rb-expand" }
else
refs.expand_collapse_button.sprite = "rb_expanded"
scroll.style.maximal_height = (settings.max_rows or constants.default_max_rows) * 28
refs.expand_collapse_button.tooltip = { "gui.rb-collapse" }
end
else
refs.root.visible = false
end
return i > 0
end
return list_box

View File

@@ -0,0 +1,248 @@
local gui = require("__flib__.gui")
local table = require("__flib__.table")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local table_comp = {}
function table_comp.build(parent, index, component, variables)
local has_label = (component.label or component.source) and true or false
return gui.build(parent, {
{
type = "flow",
style_mods = not has_label and { top_margin = 4 } or nil,
direction = "vertical",
index = index,
ref = { "root" },
action = {
on_click = { gui = "info", id = variables.gui_id, action = "set_as_active" },
},
{
type = "flow",
style_mods = { vertical_align = "center" },
action = {
on_click = { gui = "info", id = variables.gui_id, action = "set_as_active" },
},
visible = has_label,
{ type = "label", style = "rb_list_box_label", ref = { "label" } },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "sprite-button",
style = "mini_button_aligned_to_text_vertically_when_centered",
ref = { "expand_collapse_button" },
-- NOTE: Sprite, tooltip, and action are set in the update function
},
},
{
type = "frame",
style = "deep_frame_in_shallow_frame",
action = {
on_click = { gui = "info", id = variables.gui_id, action = "set_as_active" },
},
ref = { "deep_frame" },
{
type = "table",
style = "rb_info_table",
column_count = 2,
ref = { "table" },
-- Dummy elements so the first row doesn't get used
{ type = "empty-widget" },
{ type = "empty-widget" },
},
},
},
})
end
function table_comp.default_state(settings)
return { collapsed = settings.default_state == "collapsed" }
end
function table_comp.update(component, refs, object_data, player_data, settings, variables)
local tbl = refs.table
local children = tbl.children
local gui_translations = player_data.translations.gui
local search_query = variables.search_query
local i = 2
local is_shown = settings.default_state ~= "hidden"
local row_settings = settings.rows
local source_tbl = is_shown and (component.source and object_data[component.source] or component.rows) or {}
for _, row in ipairs(source_tbl) do
local row_name = row.label or row.source
local value = row.value or object_data[row.source]
if value and (not row_settings or row_settings[row_name]) then
local caption = gui_translations[row_name] or row_name
if string.find(string.lower(caption), search_query) then
-- Label
i = i + 1
local label_label = children[i]
if not label_label or not label_label.valid then
label_label = tbl.add({
type = "label",
style = "rb_table_label",
index = i,
})
end
local tooltip = row.label_tooltip
if tooltip then
caption = caption .. " [img=info]"
tooltip = gui_translations[row.label_tooltip]
else
tooltip = ""
end
label_label.caption = caption
label_label.tooltip = tooltip
-- Value
if row.type == "plain" then
local fmt = row.formatter
if fmt then
value = formatter[fmt](value, gui_translations)
end
i = i + 1
local value_label = children[i]
if not value_label or not value_label.valid or value_label.type ~= "label" then
if value_label then
value_label.destroy()
end
value_label = tbl.add({ type = "label", index = i })
end
value_label.caption = value
elseif row.type == "goto" then
i = i + 1
local button = children[i]
if not button or button.type ~= "button" then
if button then
button.destroy()
end
button = tbl.add({
type = "button",
style = "rb_table_button",
mouse_button_filter = { "left", "middle" },
index = i,
})
end
local source_data = database[value.class][value.name]
local options = table.shallow_copy(row.options or {})
options.label_only = true
options.amount_ident = value.amount_ident
options.blueprint_result = value.class == "entity" and source_data.blueprintable and { name = value.name }
or nil
local info = formatter(source_data, player_data, options)
if info then
button.caption = info.caption
button.tooltip = info.tooltip
gui.set_action(button, "on_click", { gui = "info", id = variables.gui_id, action = "navigate_to" })
gui.update_tags(
button,
{ context = { class = value.class, name = value.name }, blueprint_result = options.blueprint_result }
)
else
-- Don't actually show this row
-- This is an ugly way to do it, but whatever
button.destroy()
label_label.destroy()
i = i - 2
end
elseif row.type == "tech_level_selector" then
i = i + 1
local flow = children[i]
if not flow or flow.type ~= "flow" then
if flow then
flow.destroy()
end
flow = gui.build(tbl, {
{
type = "flow",
style_mods = { vertical_align = "center" },
index = i,
ref = { "flow" },
{
type = "sprite-button",
style = "mini_button_aligned_to_text_vertically_when_centered",
sprite = "rb_minus_black",
mouse_button_filter = { "left" },
actions = {
on_click = { gui = "info", id = variables.gui_id, action = "change_tech_level", delta = -1 },
},
},
{ type = "label", name = "tech_level_label" },
{
type = "sprite-button",
style = "mini_button_aligned_to_text_vertically_when_centered",
sprite = "rb_plus_black",
mouse_button_filter = { "left" },
actions = {
on_click = { gui = "info", id = variables.gui_id, action = "change_tech_level", delta = 1 },
},
},
},
}).flow
end
flow.tech_level_label.caption = formatter.number(variables.selected_tech_level)
elseif row.type == "tech_level_research_unit_count" then
i = i + 1
local value_label = children[i]
if not value_label or value_label.type ~= "label" then
if value_label then
value_label.destroy()
end
value_label = tbl.add({ type = "label", index = i })
end
local tech_level = variables.selected_tech_level
value_label.caption = formatter[row.formatter](
game.evaluate_expression(value, { L = tech_level, l = tech_level })
)
end
end
end
end
for j = i + 1, #children do
children[j].destroy()
end
if i > 3 then
refs.root.visible = true
local label_source = component.source or component.label
if label_source then
if component.hide_count then
refs.label.caption = gui_translations[label_source] or label_source
else
refs.label.caption = formatter.expand_string(
gui_translations.list_box_label,
gui_translations[label_source] or label_source,
i / 2 - 1
)
end
end
-- Update expand/collapse button and height
gui.set_action(refs.expand_collapse_button, "on_click", {
gui = "info",
id = variables.gui_id,
action = "toggle_collapsed",
context = variables.context,
component_index = variables.component_index,
})
if variables.component_state.collapsed then
refs.deep_frame.style.maximal_height = 1
refs.expand_collapse_button.sprite = "rb_collapsed"
refs.expand_collapse_button.tooltip = { "gui.rb-expand" }
else
refs.deep_frame.style.maximal_height = 0
refs.expand_collapse_button.sprite = "rb_expanded"
refs.expand_collapse_button.tooltip = { "gui.rb-collapse" }
end
else
refs.root.visible = false
end
return i > 3
end
return table_comp

View File

@@ -0,0 +1,50 @@
local gui = require("__flib__.gui")
local gui_util = require("scripts.gui.util")
local actions = {}
--- @param Gui QuickRefGui
function actions.close(Gui, _, _)
Gui:destroy()
end
--- @param Gui QuickRefGui
--- @param e on_gui_click
function actions.reset_location(Gui, _, e)
if e.button == defines.mouse_button_type.middle then
Gui.refs.window.location = { x = 0, y = 0 }
end
end
--- @param Gui QuickRefGui
function actions.bring_to_front(Gui, _, _)
Gui.refs.window.bring_to_front()
end
--- @param Gui QuickRefGui
--- @param e on_gui_click
function actions.handle_button_click(Gui, _, e)
if e.alt then
local button = e.element
local style = button.style.name
if style == "flib_slot_button_green" then
button.style = gui.get_tags(button).previous_style
else
gui.update_tags(button, { previous_style = style })
button.style = "flib_slot_button_green"
end
else
local context = gui_util.navigate_to(e)
if context then
OPEN_PAGE(Gui.player, Gui.player_table, context)
end
end
end
--- @param Gui QuickRefGui
function actions.view_details(Gui, _, _)
OPEN_PAGE(Gui.player, Gui.player_table, { class = "recipe", name = Gui.recipe_name })
end
return actions

View File

@@ -0,0 +1,231 @@
local gui = require("__flib__.gui")
local constants = require("constants")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local util = require("scripts.util")
local function quick_ref_panel(ref)
return {
type = "flow",
direction = "vertical",
ref = { ref, "flow" },
{ type = "label", style = "rb_list_box_label", ref = { ref, "label" } },
{
type = "frame",
style = "rb_slot_table_frame",
ref = { ref, "frame" },
{ type = "table", style = "slot_table", column_count = 5, ref = { ref, "table" } },
},
}
end
--- @class QuickRefGuiRefs
--- @field window LuaGuiElement
--- @field titlebar_flow LuaGuiElement
--- @field label LuaGuiElement
--- @class QuickRefGui
local Gui = {}
local actions = require("scripts.gui.quick-ref.actions")
function Gui:dispatch(msg, e)
if type(msg) == "string" then
actions[msg](self, msg, e)
else
actions[msg.action](self, msg, e)
end
end
function Gui:destroy()
self.refs.window.destroy()
self.player_table.guis.quick_ref[self.recipe_name] = nil
local context = { class = "recipe", name = self.recipe_name }
for _, InfoGui in pairs(INFO_GUI.find_open_context(self.player_table, context)) do
InfoGui:dispatch({
action = "update_header_button",
button = "quick_ref_button",
to_state = false,
})
end
end
function Gui:update_contents()
local refs = self.refs
local show_made_in = self.player_table.settings.general.content.show_made_in_in_quick_ref
local recipe_data = database.recipe[self.recipe_name]
local player_data = formatter.build_player_data(self.player, self.player_table)
-- Label
local recipe_info = formatter(recipe_data, player_data, { always_show = true, is_label = true })
local label = refs.label
label.caption = recipe_info.caption
label.tooltip = recipe_info.tooltip
label.style = recipe_info.researched and "rb_toolbar_label" or "rb_unresearched_toolbar_label"
-- Slot boxes
for _, source in ipairs({ "ingredients", "products", "made_in" }) do
local box = refs[source]
if source == "made_in" and not show_made_in then
box.flow.visible = false
break
else
box.flow.visible = true
end
local table = box.table
local buttons = table.children
local i = 0
for _, object in pairs(recipe_data[source]) do
local object_data = database[object.class][object.name]
local blueprint_result = source == "made_in" and { name = object.name, self.recipe_name } or nil
local object_info = formatter(object_data, player_data, {
always_show = source ~= "made_in",
amount_ident = object.amount_ident,
amount_only = true,
blueprint_result = blueprint_result,
})
if object_info then
i = i + 1
local button_style = object_info.researched and "flib_slot_button_default" or "flib_slot_button_red"
local button = buttons[i]
if button and button.valid then
button.style = button_style
button.sprite = constants.class_to_type[object.class] .. "/" .. object_data.prototype_name
button.tooltip = object_info.tooltip
gui.update_tags(button, {
blueprint_result = blueprint_result,
context = object,
researched = object_data.researched,
})
else
local probability = object.amount_ident.probability
if probability == 1 then
probability = false
end
gui.build(table, {
{
type = "sprite-button",
style = button_style,
sprite = constants.class_to_type[object.class] .. "/" .. object_data.prototype_name,
tooltip = object_info.tooltip,
tags = {
blueprint_result = blueprint_result,
context = object,
researched = object_data.researched,
},
actions = {
on_click = {
gui = "quick_ref",
id = self.recipe_name,
action = "handle_button_click",
source = source,
},
},
{
type = "label",
style = "rb_slot_label",
caption = object_info.caption,
ignored_by_interaction = true,
},
{
type = "label",
style = "rb_slot_label_top",
caption = probability and "%" or "",
ignored_by_interaction = true,
},
},
})
end
end
for j = i + 1, #buttons do
buttons[j].destroy()
end
-- Label
box.label.caption = { "gui.rb-list-box-label", { "gui.rb-" .. string.gsub(source, "_", "-") }, i }
end
end
end
local index = {}
function index.build(player, player_table, recipe_name)
--- @type QuickRefGuiRefs
local refs = gui.build(player.gui.screen, {
{
type = "frame",
direction = "vertical",
ref = { "window" },
{
type = "flow",
style = "flib_titlebar_flow",
ref = { "titlebar_flow" },
actions = {
on_click = { gui = "quick_ref", id = recipe_name, action = "reset_location" },
},
{ type = "label", style = "frame_title", caption = { "gui.rb-recipe" }, ignored_by_interaction = true },
{ type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true },
util.frame_action_button(
"rb_expand",
{ "gui.rb-view-details" },
nil,
{ gui = "quick_ref", id = recipe_name, action = "view_details" }
),
util.frame_action_button(
"utility/close",
{ "gui.close" },
nil,
{ gui = "quick_ref", id = recipe_name, action = "close" }
),
},
{
type = "frame",
style = "rb_quick_ref_content_frame",
direction = "vertical",
{
type = "frame",
style = "subheader_frame",
{ type = "label", style = "rb_toolbar_label", ref = { "label" } },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
},
{
type = "flow",
style = "rb_quick_ref_content_flow",
direction = "vertical",
quick_ref_panel("ingredients"),
quick_ref_panel("products"),
quick_ref_panel("made_in"),
},
},
},
})
refs.titlebar_flow.drag_target = refs.window
--- @class QuickRefGui
local self = {
player = player,
player_table = player_table,
recipe_name = recipe_name,
refs = refs,
}
index.load(self)
player_table.guis.quick_ref[recipe_name] = self
self:update_contents()
end
function index.load(self)
setmetatable(self, { __index = Gui })
end
return index

View File

@@ -0,0 +1,427 @@
local gui = require("__flib__.gui")
local on_tick_n = require("__flib__.on-tick-n")
local table = require("__flib__.table")
local constants = require("constants")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local gui_util = require("scripts.gui.util")
local util = require("scripts.util")
local actions = {}
--- @param Gui SearchGui
--- @param e on_gui_click
function actions.reset_location(Gui, _, e)
if e.button ~= defines.mouse_button_type.middle then
return
end
if Gui.player_table.settings.general.interface.search_gui_location == "top_left" then
local scale = Gui.player.display_scale
Gui.refs.window.location = table.map(constants.search_gui_top_left_location, function(pos)
return pos * scale
end)
Gui.refs.window.auto_center = false
else
Gui.refs.window.force_auto_center()
end
end
--- @param Gui SearchGui
function actions.close(Gui, _, _)
if not Gui.state.ignore_closed and not Gui.player_table.flags.technology_gui_open then
Gui:close()
end
end
--- @param Gui SearchGui
function actions.toggle_pinned(Gui, _, _)
local player = Gui.player
local refs = Gui.refs
local state = Gui.state
local pin_button = refs.titlebar.pin_button
state.pinned = not state.pinned
if state.pinned then
pin_button.style = "flib_selected_frame_action_button"
pin_button.sprite = "rb_pin_black"
if player.opened == refs.window then
state.ignore_closed = true
player.opened = nil
state.ignore_closed = false
end
else
pin_button.style = "frame_action_button"
pin_button.sprite = "rb_pin_white"
player.opened = refs.window
end
end
--- @param Gui SearchGui
function actions.toggle_settings(Gui, _, _)
local state = Gui.state
local player = Gui.player
state.ignore_closed = true
local SettingsGui = util.get_gui(Gui.player.index, "settings")
if SettingsGui then
SettingsGui:destroy()
else
SETTINGS_GUI.build(player, Gui.player_table)
end
state.ignore_closed = false
local settings_button = Gui.refs.titlebar.settings_button
if Gui.player_table.guis.settings then
settings_button.style = "flib_selected_frame_action_button"
settings_button.sprite = "rb_settings_black"
else
settings_button.style = "frame_action_button"
settings_button.sprite = "rb_settings_white"
if not state.pinned then
player.opened = Gui.refs.window
end
end
end
--- @param Gui SearchGui
function actions.deselect_settings_button(Gui, _, _)
local settings_button = Gui.refs.titlebar.settings_button
settings_button.style = "frame_action_button"
settings_button.sprite = "rb_settings_white"
if not Gui.state.pinned and Gui.refs.window.visible then
Gui.player.opened = Gui.refs.window
end
end
--- @param Gui SearchGui
--- @param e on_gui_text_changed
function actions.update_search_query(Gui, _, e)
local player_table = Gui.player_table
local state = Gui.state
local refs = Gui.refs
local class_filter
local query = string.lower(e.element.text)
if string.find(query, "/") then
-- The `_`s here are technically globals, but whatever
_, _, class_filter, query = string.find(query, "^/(.-)/(.-)$")
if class_filter then
class_filter = string.lower(class_filter)
end
-- Check translations of each class filter
local matched = false
if class_filter then
local gui_translations = player_table.translations.gui
for _, class in pairs(constants.classes) do
if class_filter == string.lower(gui_translations[class]) then
matched = true
class_filter = class
break
end
end
end
-- Invalidate textfield
if not class_filter or not query or not matched then
class_filter = false
query = nil
end
end
-- Remove results update action if there is one
if state.update_results_ident then
on_tick_n.remove(state.update_results_ident)
state.update_results_ident = nil
end
if query then
-- Fuzzy search
if player_table.settings.general.search.fuzzy_search then
query = string.gsub(query, ".", "%1.*")
end
-- Input sanitization
for pattern, replacement in pairs(constants.input_sanitizers) do
query = string.gsub(query, pattern, replacement)
end
-- Save query
state.search_query = query
state.class_filter = class_filter
-- Reset textfield style
refs.search_textfield.style = "rb_search_textfield"
if #query == 0 and not class_filter then
-- Update immediately
actions.update_search_results(Gui)
else
-- Update in a while
state.update_results_ident = on_tick_n.add(
game.tick + constants.search_timeout,
{ gui = "search", action = "update_search_results", player_index = e.player_index }
)
end
else
state.search_query = ""
refs.search_textfield.style = "rb_search_invalid_textfield"
end
end
--- @param Gui SearchGui
function actions.update_search_results(Gui, _, _)
local player = Gui.player
local player_table = Gui.player_table
local state = Gui.state
local refs = Gui.refs
-- Data
local player_data = formatter.build_player_data(player, player_table)
local show_fluid_temperatures = player_table.settings.general.search.show_fluid_temperatures
local search_type = player_table.settings.general.search.search_type
local class_filter = state.class_filter
local query = state.search_query
if state.search_type == "textual" then
-- Update results based on query
local i = 0
local pane = refs.textual_results_pane
local children = pane.children
local max = constants.search_results_limit
if class_filter ~= false and (class_filter or #query >= 2) then
for class in pairs(constants.pages) do
if not class_filter or class_filter == class then
for internal, translation in pairs(player_table.translations[class]) do
-- Match against search string
local matched
if search_type == "both" then
matched = string.find(string.lower(internal), query) or string.find(string.lower(translation), query)
elseif search_type == "internal" then
matched = string.find(string.lower(internal), query)
elseif search_type == "localised" then
matched = string.find(string.lower(translation), query)
end
if matched then
local obj_data = database[class][internal]
-- Check temperature settings
local passed = true
if obj_data.class == "fluid" then
local temperature_ident = obj_data.temperature_ident
if temperature_ident then
local is_range = temperature_ident.min ~= temperature_ident.max
if is_range then
if show_fluid_temperatures ~= "all" then
passed = false
end
else
if show_fluid_temperatures == "off" then
passed = false
end
end
end
end
if passed then
local blueprint_result = class == "entity" and obj_data.blueprintable and { name = internal } or nil
local info = formatter(obj_data, player_data, { blueprint_result = blueprint_result })
if info then
i = i + 1
local style = info.researched and "rb_list_box_item" or "rb_unresearched_list_box_item"
local item = children[i]
if item then
item.style = style
item.caption = info.caption
item.tooltip = info.tooltip
item.enabled = info.num_interactions > 0
gui.update_tags(
item,
{ blueprint_result = blueprint_result, context = { class = class, name = internal } }
)
else
gui.add(pane, {
type = "button",
style = style,
caption = info.caption,
tooltip = info.tooltip,
enabled = info.num_interactions > 0,
mouse_button_filter = { "left", "middle" },
tags = {
blueprint_result = blueprint_result,
context = { class = class, name = internal },
},
actions = {
on_click = { gui = "search", action = "open_object" },
},
})
if i >= max then
break
end
end
end
end
end
end
end
if i >= max then
break
end
end
end
-- Destroy extraneous items
for j = i + 1, #children do
children[j].destroy()
end
elseif state.search_type == "visual" then
refs.objects_frame.visible = true
refs.warning_frame.visible = false
--- @type LuaGuiElement
local group_table = refs.group_table
for _, group_scroll in pairs(refs.objects_frame.children) do
local group_has_results = false
for _, subgroup_table in pairs(group_scroll.children) do
local visible_count = 0
for _, obj_button in pairs(subgroup_table.children) do
local context = gui.get_tags(obj_button).context
local matched
-- Match against class filter
if not class_filter or class_filter == context.class then
local translation = player_data.translations[context.class][context.name]
-- Match against search string
if search_type == "both" then
matched = string.find(string.lower(context.name), query) or string.find(string.lower(translation), query)
elseif search_type == "internal" then
matched = string.find(string.lower(context.name), query)
elseif search_type == "localised" then
matched = string.find(string.lower(translation), query)
end
end
if matched then
obj_button.visible = true
visible_count = visible_count + 1
else
obj_button.visible = false
end
end
if visible_count > 0 then
group_has_results = true
subgroup_table.visible = true
else
subgroup_table.visible = false
end
end
local group_name = group_scroll.name
local group_button = group_table[group_name]
if group_has_results then
group_button.style = "rb_filter_group_button_tab"
group_button.enabled = state.active_group ~= group_scroll.name
if state.active_group == group_name then
group_scroll.visible = true
else
group_scroll.visible = false
end
else
group_scroll.visible = false
group_button.style = "rb_disabled_filter_group_button_tab"
group_button.enabled = false
if state.active_group == group_name then
local matched = false
for _, group_button in pairs(group_table.children) do
if group_button.enabled then
matched = true
actions.change_group(Gui, { group = group_button.name, ignore_last_button = true })
break
end
end
if not matched then
refs.objects_frame.visible = false
refs.warning_frame.visible = true
end
end
end
end
end
end
--- @param Gui SearchGui
--- @param e on_gui_click
function actions.open_object(Gui, _, e)
local context = gui_util.navigate_to(e)
if context then
local attach = Gui.player_table.settings.general.interface.attach_search_results
local sticky = attach and e.button == defines.mouse_button_type.left
local id = sticky and Gui.state.id and Gui.player_table.guis.info[Gui.state.id] and Gui.state.id or nil
local parent = sticky and Gui.refs.window or nil
OPEN_PAGE(Gui.player, Gui.player_table, context, { id = id, parent = parent })
if sticky and not id then
Gui.state.id = Gui.player_table.guis.info._active_id
end
if not sticky and Gui.player_table.settings.general.interface.close_search_gui_after_selection then
actions.close(Gui)
end
end
end
--- @param Gui SearchGui
function actions.change_search_type(Gui)
local state = Gui.state
local refs = Gui.refs
if state.search_type == "textual" then
state.search_type = "visual"
refs.textual_results_pane.visible = false
refs.visual_results_flow.visible = true
if state.needs_visual_update then
state.needs_visual_update = false
Gui:update_visual_contents()
end
elseif state.search_type == "visual" then
state.search_type = "textual"
refs.textual_results_pane.visible = true
refs.visual_results_flow.visible = false
end
actions.update_search_results(Gui)
end
--- @param Gui SearchGui
--- @param msg table
function actions.change_group(Gui, msg)
local last_group = Gui.state.active_group
if not msg.ignore_last_button then
Gui.refs.group_table[last_group].enabled = true
end
Gui.refs.objects_frame[last_group].visible = false
local new_group = msg.group
Gui.refs.group_table[new_group].enabled = false
Gui.refs.objects_frame[new_group].visible = true
Gui.state.active_group = msg.group
end
--- @param Gui SearchGui
function actions.update_favorites(Gui, _, _)
Gui:update_favorites()
end
--- @param Gui SearchGui
function actions.update_history(Gui, _, _)
Gui:update_history()
end
--- @param Gui SearchGui
function actions.delete_favorites(Gui, _, _)
Gui.player_table.favorites = {}
Gui:update_favorites()
end
--- @param Gui SearchGui
function actions.delete_history(Gui, _, _)
Gui.player_table.global_history = {}
Gui:update_history()
end
return actions

View File

@@ -0,0 +1,512 @@
local gui = require("__flib__.gui")
local table = require("__flib__.table")
local constants = require("constants")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local gui_util = require("scripts.gui.util")
local util = require("scripts.util")
--- @class SearchGuiRefs
--- @field window LuaGuiElement
--- @field titlebar SearchGuiTitlebarRefs
--- @field tabbed_pane LuaGuiElement
--- @field search_textfield LuaGuiElement
--- @field textual_results_pane LuaGuiElement
--- @field visual_results_flow LuaGuiElement
--- @field group_table LuaGuiElement
--- @field objects_frame LuaGuiElement
--- @field warning_frame LuaGuiElement
--- @field delete_favorites_button LuaGuiElement
--- @field delete_history_button LuaGuiElement
--- @field favorites_pane LuaGuiElement
--- @field history_pane LuaGuiElement
--- @class SearchGuiTitlebarRefs
--- @field flow LuaGuiElement
--- @field drag_handle LuaGuiElement
--- @field pin_button LuaGuiElement
--- @field settings_button LuaGuiElement
--- @class SearchGui
local Gui = {}
local actions = require("scripts.gui.search.actions")
function Gui:dispatch(msg, e)
if type(msg) == "string" then
actions[msg](self, msg, e)
else
actions[msg.action](self, msg, e)
end
end
function Gui:destroy()
if self.refs.window.valid then
self.refs.window.destroy()
end
self.player_table.guis.search = nil
self.player.set_shortcut_toggled("rb-search", false)
end
function Gui:open()
local refs = self.refs
refs.window.visible = true
refs.window.bring_to_front()
refs.tabbed_pane.selected_tab_index = 1
refs.search_textfield.select_all()
refs.search_textfield.focus()
if not self.state.pinned then
self.player.opened = refs.window
end
-- Workaround to prevent the search GUI from centering itself if the player doesn't manually recenter
if self.player_table.settings.general.interface.search_gui_location ~= "center" then
refs.window.auto_center = false
end
self.player.set_shortcut_toggled("rb-search", true)
if self.state.search_type == "visual" and self.state.needs_visual_update then
self:update_visual_contents()
end
end
function Gui:close()
local window = self.player_table.guis.search.refs.window
window.visible = false
local player = self.player
player.set_shortcut_toggled("rb-search", false)
if player.opened == window then
player.opened = nil
end
end
function Gui:toggle()
if self.refs.window.visible then
self:close()
else
self:open()
end
end
function Gui:update_visual_contents()
self.state.needs_visual_update = false
local player_data = formatter.build_player_data(self.player, self.player_table)
local show_fluid_temperatures = player_data.settings.general.search.show_fluid_temperatures
local groups = {}
for _, objects in pairs(
{ database.item, database.fluid }
-- { database.recipe }
) do
for name, object in pairs(objects) do
-- Create / retrieve group and subgroup
local group = object.group
local group_table = groups[group.name]
if not group_table then
group_table = {
button = {
type = "sprite-button",
name = group.name,
style = "rb_filter_group_button_tab",
sprite = "item-group/" .. group.name,
tooltip = { "item-group-name." .. group.name },
actions = {
on_click = { gui = "search", action = "change_group", group = group.name },
},
},
members = 0,
scroll_pane = {
type = "scroll-pane",
name = group.name,
style = "rb_filter_scroll_pane",
vertical_scroll_policy = "always",
visible = false,
},
subgroups = {},
}
groups[group.name] = group_table
end
local subgroup = object.subgroup
local subgroup_table = group_table.subgroups[subgroup.name]
if not subgroup_table then
subgroup_table = { type = "table", style = "slot_table", column_count = 10 }
group_table.subgroups[subgroup.name] = subgroup_table
table.insert(group_table.scroll_pane, subgroup_table)
end
-- Check fluid temperature
local matched = true
local temperature_ident = object.temperature_ident
if temperature_ident then
local is_range = temperature_ident.min ~= temperature_ident.max
if is_range then
if show_fluid_temperatures ~= "all" then
matched = false
end
else
if show_fluid_temperatures == "off" then
matched = false
end
end
end
if matched then
local blueprint_result = object.place_result and { name = object.place_result.name } or nil
local formatted = formatter(object, player_data, { blueprint_result = blueprint_result })
if formatted then
group_table.members = group_table.members + 1
local style = "default"
if formatted.disabled or formatted.hidden then
style = "grey"
elseif not formatted.researched then
style = "red"
end
-- Create the button
table.insert(subgroup_table, {
type = "sprite-button",
style = "flib_slot_button_" .. style,
sprite = object.class .. "/" .. object.prototype_name,
tooltip = formatted.tooltip,
mouse_button_filter = { "left", "middle", "right" },
tags = {
blueprint_result = blueprint_result,
context = { class = object.class, name = name },
},
actions = {
on_click = { gui = "search", action = "open_object" },
},
temperature_ident and {
type = "label",
style = "rb_slot_label",
caption = temperature_ident.short_string,
ignored_by_interaction = true,
} or nil,
temperature_ident and temperature_ident.short_top_string and {
type = "label",
style = "rb_slot_label_top",
caption = temperature_ident.short_top_string,
ignored_by_interaction = true,
} or nil,
})
end
end
end
end
local group_buttons = {}
local group_scroll_panes = {}
local first_group
for group_name, group in pairs(groups) do
if group.members > 0 then
table.insert(group_buttons, group.button)
table.insert(group_scroll_panes, group.scroll_pane)
if not first_group then
first_group = group_name
end
end
end
if
#self.state.active_group == 0
or not table.for_each(group_buttons, function(button)
return button.name == self.state.active_group
end)
then
self.state.active_group = first_group
end
local refs = self.refs
refs.group_table.clear()
gui.build(refs.group_table, group_buttons)
refs.objects_frame.clear()
gui.build(refs.objects_frame, group_scroll_panes)
self:dispatch({ action = "change_group", group = self.state.active_group, ignore_last_button = true })
self:dispatch("update_search_results")
end
function Gui:update_favorites()
local favorites = self.player_table.favorites
local refs = self.refs
gui_util.update_list_box(
refs.favorites_pane,
favorites,
formatter.build_player_data(self.player, self.player_table),
pairs,
{ always_show = true }
)
refs.delete_favorites_button.enabled = next(favorites) and true or false
for id, InfoGui in pairs(self.player_table.guis.info) do
if not constants.ignored_info_ids[id] then
local context = InfoGui:get_context()
local to_state = favorites[context.class .. "." .. context.name]
InfoGui:dispatch({ action = "update_header_button", button = "favorite_button", to_state = to_state })
end
end
end
function Gui:update_history()
local refs = self.refs
gui_util.update_list_box(
refs.history_pane,
self.player_table.global_history,
formatter.build_player_data(self.player, self.player_table),
ipairs,
{ always_show = true }
)
refs.delete_history_button.enabled = next(self.player_table.global_history) and true or false
end
function Gui:bring_to_front()
self.refs.window.bring_to_front()
end
local index = {}
--- @param player LuaPlayer
--- @param player_table PlayerTable
function index.build(player, player_table)
--- @type SearchGuiRefs
local gui_type = player_table.settings.general.search.default_gui_type
local refs = gui.build(player.gui.screen, {
{
type = "frame",
name = "rb_search_window",
style = "invisible_frame",
visible = false,
ref = { "window" },
actions = {
on_closed = { gui = "search", action = "close" },
},
-- Search frame
{
type = "frame",
direction = "vertical",
{
type = "flow",
style = "flib_titlebar_flow",
ref = { "titlebar", "flow" },
actions = {
on_click = { gui = "search", action = "reset_location" },
},
{
type = "label",
style = "frame_title",
caption = { "gui.rb-search-title" },
ignored_by_interaction = true,
},
{ type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true },
util.frame_action_button(
"rb_pin",
{ "gui.rb-pin-instruction" },
{ "titlebar", "pin_button" },
{ gui = "search", action = "toggle_pinned" }
),
util.frame_action_button(
"rb_settings",
{ "gui.rb-settings-instruction" },
{ "titlebar", "settings_button" },
{ gui = "search", action = "toggle_settings" }
),
util.frame_action_button(
"utility/close",
{ "gui.close" },
{ "titlebar", "close_button" },
{ gui = "search", action = "close" }
),
},
{
type = "frame",
style = "inside_deep_frame_for_tabs",
direction = "vertical",
ref = { "tab_frame" },
{
type = "tabbed-pane",
style = "tabbed_pane_with_no_side_padding",
style_mods = { maximal_width = 426 },
ref = { "tabbed_pane" },
{
tab = { type = "tab", caption = { "gui.search" } },
content = {
type = "frame",
style = "rb_inside_deep_frame_under_tabs",
direction = "vertical",
{
type = "frame",
style = "rb_subheader_frame",
{
type = "textfield",
style = "flib_widthless_textfield",
style_mods = { horizontally_stretchable = true },
clear_and_focus_on_right_click = true,
ref = { "search_textfield" },
actions = {
on_text_changed = { gui = "search", action = "update_search_query" },
},
},
-- {
-- type = "sprite-button",
-- style = "tool_button",
-- tooltip = { "gui.rb-search-filters" },
-- sprite = "rb_filter",
-- actions = {
-- on_click = { gui = "search", action = "toggle_filters" },
-- },
-- },
{
type = "sprite-button",
style = "tool_button",
tooltip = { "gui.rb-change-search-type" },
sprite = "rb_swap",
actions = {
on_click = { gui = "search", action = "change_search_type" },
},
},
},
{
type = "scroll-pane",
style = "rb_search_results_scroll_pane",
ref = { "textual_results_pane" },
visible = gui_type == "textual",
},
{
type = "flow",
style_mods = { padding = 0, margin = 0, vertical_spacing = 0 },
direction = "vertical",
visible = gui_type == "visual",
ref = { "visual_results_flow" },
{
type = "table",
style = "filter_group_table",
style_mods = { width = 426 },
column_count = 6,
ref = { "group_table" },
},
{
type = "frame",
style = "rb_filter_frame",
{
type = "frame",
style = "deep_frame_in_shallow_frame",
style_mods = { natural_height = 40 * 15, natural_width = 40 * 10 },
ref = { "objects_frame" },
},
{
type = "frame",
style = "rb_warning_frame_in_shallow_frame",
style_mods = { height = 40 * 15, width = 40 * 10 },
ref = { "warning_frame" },
visible = false,
{
type = "flow",
style = "rb_warning_flow",
direction = "vertical",
{ type = "label", style = "bold_label", caption = { "gui.rb-no-results" } },
},
},
},
},
},
},
{
tab = { type = "tab", caption = { "gui.rb-favorites" } },
content = {
type = "frame",
style = "rb_inside_deep_frame_under_tabs",
direction = "vertical",
{
type = "frame",
style = "subheader_frame",
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "sprite-button",
style = "tool_button_red",
sprite = "utility/trash",
tooltip = { "gui.rb-delete-favorites" },
ref = { "delete_favorites_button" },
actions = {
on_click = { gui = "search", action = "delete_favorites" },
},
},
},
{ type = "scroll-pane", style = "rb_search_results_scroll_pane", ref = { "favorites_pane" } },
},
},
{
tab = { type = "tab", caption = { "gui.rb-history" } },
content = {
type = "frame",
style = "rb_inside_deep_frame_under_tabs",
direction = "vertical",
{
type = "frame",
style = "subheader_frame",
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "sprite-button",
style = "tool_button_red",
sprite = "utility/trash",
tooltip = { "gui.rb-delete-history" },
ref = { "delete_history_button" },
actions = {
on_click = { gui = "search", action = "delete_history" },
},
},
},
{ type = "scroll-pane", style = "rb_search_results_scroll_pane", ref = { "history_pane" } },
},
},
},
},
},
},
})
refs.titlebar.flow.drag_target = refs.window
if player_table.settings.general.interface.search_gui_location == "top_left" then
refs.window.location = table.map(constants.search_gui_top_left_location, function(pos)
return pos * player.display_scale
end)
else
refs.window.force_auto_center()
end
--- @class SearchGui
local self = {
player = player,
player_table = player_table,
state = {
active_group = "",
ignore_closed = false,
needs_visual_update = true,
search_query = "",
search_type = gui_type,
pinned = false,
},
refs = refs,
}
index.load(self)
player_table.guis.search = self
self:update_favorites()
self:update_history()
if gui_type == "visual" then
self:update_visual_contents()
end
end
function index.load(self)
setmetatable(self, { __index = Gui })
end
return index

View File

@@ -0,0 +1,200 @@
local on_tick_n = require("__flib__.on-tick-n")
local constants = require("constants")
local util = require("scripts.util")
local actions = {}
--- @param Gui SettingsGui
function actions.close(Gui, _, _)
Gui:destroy()
local SearchGui = util.get_gui(Gui.player.index, "search")
if SearchGui then
SearchGui:dispatch("deselect_settings_button")
end
end
--- @param Gui SettingsGui
--- @param e on_gui_click
function actions.reset_location(Gui, _, e)
if e.button == defines.mouse_button_type.middle then
Gui.refs.window.force_auto_center()
end
end
--- @param Gui SettingsGui
function actions.toggle_search(Gui, _, _)
local state = Gui.state
local refs = Gui.refs
local opened = state.search_opened
state.search_opened = not opened
local search_button = refs.titlebar.search_button
local search_textfield = refs.titlebar.search_textfield
if opened then
search_button.style = "frame_action_button"
search_button.sprite = "utility/search_white"
search_textfield.visible = false
if state.search_query ~= "" then
-- Reset query
search_textfield.text = ""
state.search_query = ""
-- Immediately refresh page
Gui:update_contents()
end
else
-- Show search textfield
search_button.style = "flib_selected_frame_action_button"
search_button.sprite = "utility/search_black"
search_textfield.visible = true
search_textfield.focus()
end
end
--- @param Gui SettingsGui
--- @param e on_gui_text_changed
function actions.update_search_query(Gui, _, e)
local player_table = Gui.player_table
local state = Gui.state
local query = string.lower(e.element.text)
-- Fuzzy search
if player_table.settings.general.search.fuzzy_search then
query = string.gsub(query, ".", "%1.*")
end
-- Input sanitization
for pattern, replacement in pairs(constants.input_sanitizers) do
query = string.gsub(query, pattern, replacement)
end
-- Save query
state.search_query = query
-- Remove scheduled update if one exists
if state.update_results_ident then
on_tick_n.remove(state.update_results_ident)
state.update_results_ident = nil
end
if query == "" then
-- Update now
actions.update_search_results(Gui)
else
-- Update in a while
state.update_results_ident = on_tick_n.add(
game.tick + constants.search_timeout,
{ gui = "settings", action = "update_search_results", player_index = e.player_index }
)
end
end
--- @param Gui SettingsGui
function actions.update_search_results(Gui, _, _)
Gui:update_contents()
end
--- @param Gui SettingsGui
--- @param msg table
--- @param e on_gui_checked_state_changed|on_gui_selection_state_changed
function actions.change_general_setting(Gui, msg, e)
local type = msg.type
local category = msg.category
local name = msg.name
local setting_ident = constants.general_settings[category][name]
local settings = Gui.player_table.settings.general[category]
local new_value
local element = e.element
-- NOTE: This shouldn't ever happen, but we will avoid a crash just in case!
if not element.valid then
return
end
if type == "bool" then
new_value = element.state
elseif type == "enum" then
local selected_index = element.selected_index
new_value = setting_ident.options[selected_index]
end
-- NOTE: This _also_ shouldn't ever happen, but you can't be too safe!
if new_value ~= nil then
settings[name] = new_value
REFRESH_CONTENTS(Gui.player, Gui.player_table)
-- Update enabled statuses
Gui:update_contents("general")
end
end
--- @param Gui SettingsGui
--- @param e on_gui_selection_state_changed
function actions.change_category(Gui, _, e)
Gui.state.selected_category = e.element.selected_index
Gui:update_contents("categories")
end
--- @param Gui SettingsGui
--- @param msg table
--- @param e on_gui_checked_state_changed
function actions.change_category_setting(Gui, msg, e)
local class = msg.class
local name = msg.name
local category_settings = Gui.player_table.settings.categories[class]
category_settings[name] = e.element.state
REFRESH_CONTENTS(Gui.player, Gui.player_table)
end
--- @param Gui SettingsGui
--- @param e on_gui_selected_tab_changed
function actions.change_page(Gui, _, e)
Gui.state.selected_page = e.element.selected_index
Gui:update_contents("pages")
end
--- @param Gui SettingsGui
--- @param msg table
--- @param e on_gui_selection_state_changed
function actions.change_default_state(Gui, msg, e)
local class = msg.class
local component = msg.component
local component_settings = Gui.player_table.settings.pages[class][component]
if component_settings then
component_settings.default_state = constants.component_states[e.element.selected_index]
end
REFRESH_CONTENTS(Gui.player, Gui.player_table)
end
--- @param Gui SettingsGui
--- @param msg table
--- @param e on_gui_text_changed
function actions.change_max_rows(Gui, msg, e)
local class = msg.class
local component = msg.component
local component_settings = Gui.player_table.settings.pages[class][component]
if component_settings then
component_settings.max_rows = tonumber(e.element.text)
end
REFRESH_CONTENTS(Gui.player, Gui.player_table)
end
--- @param Gui SettingsGui
--- @param msg table
--- @param e on_gui_checked_state_changed
function actions.change_row_visible(Gui, msg, e)
local class = msg.class
local component = msg.component
local row = msg.row
local component_settings = Gui.player_table.settings.pages[class][component]
if component_settings then
component_settings.rows[row] = e.element.state
end
REFRESH_CONTENTS(Gui.player, Gui.player_table)
end
return actions

View File

@@ -0,0 +1,414 @@
local gui = require("__flib__.gui")
local table = require("__flib__.table")
local constants = require("constants")
local database = require("scripts.database")
local util = require("scripts.util")
--- @class SettingsGui
local Gui = {}
local actions = require("scripts.gui.settings.actions")
function Gui:dispatch(msg, e)
if type(msg) == "string" then
actions[msg](self, msg, e)
else
actions[msg.action](self, msg, e)
end
end
function Gui:destroy()
self.player_table.guis.settings.refs.window.destroy()
self.player_table.guis.settings = nil
end
function Gui:update_contents(tab)
local refs = self.refs
local state = self.state
local query = state.search_query
local translations = self.player_table.translations
local gui_translations = translations.gui
local actual_settings = self.player_table.settings
-- For simplicity's sake, since there's not _that much_ going on here, we will just destroy and recreate things
-- instead of updating them.
-- GENERAL
if not tab or tab == "general" then
local general_pane = refs.general.pane
general_pane.clear()
for category, settings in pairs(constants.general_settings) do
local actual_category_settings = actual_settings.general[category]
local children = {}
for setting_name, setting_ident in pairs(settings) do
local caption = gui_translations[setting_name] or setting_name
if string.find(string.lower(caption), query) then
local converted_setting_name = string.gsub(setting_name, "_", "-")
local tooltip = ""
if setting_ident.has_tooltip then
tooltip = { "gui.rb-" .. converted_setting_name .. "-description" }
caption = caption .. " [img=info]"
end
local enabled = true
if setting_ident.dependencies then
for _, dependency in pairs(setting_ident.dependencies) do
if actual_settings.general[dependency.category][dependency.name] ~= dependency.value then
enabled = false
break
end
end
end
if setting_ident.type == "bool" then
children[#children + 1] = {
type = "checkbox",
caption = caption,
tooltip = tooltip,
state = actual_category_settings[setting_name],
enabled = enabled,
actions = enabled and {
on_click = {
gui = "settings",
action = "change_general_setting",
type = setting_ident.type,
category = category,
name = setting_name,
},
} or nil,
}
elseif setting_ident.type == "enum" then
children[#children + 1] = {
type = "flow",
style_mods = { vertical_align = "center" },
{ type = "label", caption = caption, tooltip = tooltip },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "drop-down",
items = table.map(setting_ident.options, function(option_name)
return { "gui.rb-" .. converted_setting_name .. "-" .. string.gsub(option_name, "_", "-") }
end),
selected_index = table.find(setting_ident.options, actual_category_settings[setting_name]),
enabled = enabled,
actions = enabled and {
on_selection_state_changed = {
gui = "settings",
action = "change_general_setting",
type = setting_ident.type,
category = category,
name = setting_name,
},
} or nil,
},
}
end
end
end
if #children > 0 then
gui.build(general_pane, {
{
type = "frame",
style = "bordered_frame",
direction = "vertical",
caption = gui_translations[category] or category,
children = children,
},
})
end
end
end
-- CATEGORIES
if not tab or tab == "categories" then
local categories_frame = refs.categories.frame
categories_frame.clear()
local selected_class = constants.category_classes[state.selected_category]
local class_settings = actual_settings.categories[selected_class]
local class_translations = translations[selected_class]
local children = {}
for category_name in pairs(database[selected_class]) do
local category_translation = class_translations[category_name] or category_name
if string.find(string.lower(category_translation), query) then
local img_type = constants.class_to_type[selected_class]
if img_type then
category_translation = "[img=" .. img_type .. "/" .. category_name .. "] " .. category_translation
end
children[#children + 1] = {
type = "checkbox",
caption = category_translation,
state = class_settings[category_name],
actions = {
on_checked_state_changed = {
gui = "settings",
action = "change_category_setting",
class = selected_class,
name = category_name,
},
},
}
end
end
if #children > 0 then
gui.build(categories_frame, children)
end
end
-- PAGES
if not tab or tab == "pages" then
local pages_pane = refs.pages.pane
pages_pane.clear()
local selected_page = constants.pages_arr[state.selected_page]
local page_settings = actual_settings.pages[selected_page]
local children = {}
for component_name, component_settings in pairs(page_settings) do
local component_children = {}
component_children[1] = {
type = "flow",
style_mods = { vertical_align = "center" },
{ type = "label", caption = gui_translations.default_state },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "drop-down",
items = table.map(constants.component_states, function(option_name)
return { "gui.rb-" .. string.gsub(option_name, "_", "-") }
end),
selected_index = table.find(constants.component_states, component_settings.default_state),
actions = {
on_selection_state_changed = {
gui = "settings",
action = "change_default_state",
class = selected_page,
component = component_name,
},
},
},
}
if component_settings.max_rows then
component_children[#component_children + 1] = {
type = "flow",
style_mods = { vertical_align = "center" },
{ type = "label", caption = gui_translations.max_rows },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "textfield",
style_mods = { width = 50, horizontal_align = "center" },
numeric = true,
lose_focus_on_confirm = true,
clear_and_focus_on_right_click = true,
text = tostring(component_settings.max_rows),
actions = {
on_confirmed = {
gui = "settings",
action = "change_max_rows",
class = selected_page,
component = component_name,
},
},
},
}
end
if component_settings.rows then
for row_name, row_state in pairs(component_settings.rows) do
component_children[#component_children + 1] = {
type = "checkbox",
caption = gui_translations[row_name],
state = row_state,
actions = {
on_checked_state_changed = {
gui = "settings",
action = "change_row_visible",
class = selected_page,
component = component_name,
row = row_name,
},
},
}
end
end
children[#children + 1] = {
type = "frame",
style = "bordered_frame",
style_mods = { minimal_width = 300, horizontally_stretchable = true },
direction = "vertical",
caption = gui_translations[component_name] or component_name,
children = component_children,
}
end
gui.build(pages_pane, children)
end
end
local index = {}
local function subpage_set(name, action, include_tooltip, include_bordered_frame, initial_items)
return {
tab = {
type = "tab",
style_mods = { padding = { 7, 10, 8, 10 } },
caption = { "", { "gui.rb-" .. name }, include_tooltip and " [img=info]" or nil },
tooltip = include_tooltip and { "gui.rb-" .. name .. "-description" } or nil,
},
content = {
type = "flow",
style_mods = { horizontal_spacing = 12, padding = { 8, 0, 12, 12 } },
{
type = "list-box",
style = "list_box_in_shallow_frame",
style_mods = { height = 28 * constants.settings_gui_rows, width = 150 },
items = initial_items,
selected_index = 1,
actions = {
on_selection_state_changed = { gui = "settings", action = action },
},
},
{
type = "frame",
style = "flib_shallow_frame_in_shallow_frame",
style_mods = { height = 28 * constants.settings_gui_rows },
{
type = "scroll-pane",
style = "flib_naked_scroll_pane",
style_mods = { padding = 4, vertically_stretchable = true },
vertical_scroll_policy = "always",
ref = { name, "pane" },
include_bordered_frame and {
type = "frame",
style = "bordered_frame",
style_mods = { minimal_width = 300, horizontally_stretchable = true, vertically_stretchable = true },
direction = "vertical",
ref = { name, "frame" },
} or nil,
},
},
},
}
end
--- @param player LuaPlayer
--- @param player_table PlayerTable
function index.build(player, player_table)
local gui_translations = player_table.translations.gui
local refs = gui.build(player.gui.screen, {
{
type = "frame",
name = "rb_settings_window",
direction = "vertical",
ref = { "window" },
actions = {
on_closed = { gui = "settings", action = "close" },
},
{
type = "flow",
style = "flib_titlebar_flow",
ref = { "titlebar", "flow" },
actions = {
on_click = { gui = "settings", action = "reset_location" },
},
{ type = "label", style = "frame_title", caption = { "gui.rb-settings" }, ignored_by_interaction = true },
{ type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true },
{
type = "textfield",
style_mods = {
top_margin = -3,
right_padding = 3,
width = 120,
},
clear_and_focus_on_right_click = true,
visible = false,
ref = { "titlebar", "search_textfield" },
actions = {
on_text_changed = { gui = "settings", action = "update_search_query" },
},
},
util.frame_action_button(
"utility/search",
{ "gui.rb-search-instruction" },
{ "titlebar", "search_button" },
{ gui = "settings", action = "toggle_search" }
),
util.frame_action_button(
"utility/close",
{ "gui.close" },
{ "titlebar", "close_button" },
{ gui = "settings", action = "close" }
),
},
{
type = "frame",
style = "inside_deep_frame_for_tabs",
direction = "vertical",
{
type = "tabbed-pane",
style = "flib_tabbed_pane_with_no_padding",
{
tab = { type = "tab", caption = { "gui.rb-general" } },
content = {
type = "flow",
style_mods = { padding = 4 },
direction = "vertical",
ref = { "general", "pane" },
},
},
subpage_set(
"categories",
"change_category",
true,
true,
table.map(constants.category_classes, function(class)
return gui_translations[class] or class
end)
),
subpage_set(
"pages",
"change_page",
false,
false,
table.map(constants.pages_arr, function(class)
return gui_translations[class] or class
end)
),
},
},
},
})
refs.window.force_auto_center()
refs.titlebar.flow.drag_target = refs.window
player.opened = refs.window
--- @type SettingsGui
local self = {
player = player,
player_table = player_table,
refs = refs,
state = {
search_opened = false,
search_query = "",
selected_category = 1,
selected_page = 1,
},
}
index.load(self)
player_table.guis.settings = self
self:update_contents()
end
function index.load(self)
setmetatable(self, { __index = Gui })
end
return index

View File

@@ -0,0 +1,125 @@
local bounding_box = require("__flib__.bounding-box")
local gui = require("__flib__.gui")
local math = require("__flib__.math")
local table = require("__flib__.table")
local constants = require("constants")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local gui_util = {}
-- The calling GUI will navigate to the context that is returned, if any
-- Actions that do not open a page will not return a context
function gui_util.navigate_to(e)
local tags = gui.get_tags(e.element)
local context = tags.context
local modifiers = {}
for name, modifier in pairs({ control = e.control, shift = e.shift, alt = e.alt }) do
if modifier then
modifiers[#modifiers + 1] = name
end
end
for _, interaction in pairs(constants.interactions[context.class]) do
if table.deep_compare(interaction.modifiers, modifiers) then
local action = interaction.action
local context_data = database[context.class][context.name]
local player = game.get_player(e.player_index) --[[@as LuaPlayer]]
if action == "view_details" then
return context
elseif action == "view_product_details" and #context_data.products == 1 then
return context_data.products[1]
elseif action == "get_blueprint" then
local blueprint_result = tags.blueprint_result
if blueprint_result then
local cursor_stack = player.cursor_stack
player.clear_cursor()
if cursor_stack and cursor_stack.valid then
local collision_box = game.entity_prototypes[blueprint_result.name].collision_box
local height = bounding_box.height(collision_box)
local width = bounding_box.width(collision_box)
cursor_stack.set_stack({ name = "blueprint", count = 1 })
cursor_stack.set_blueprint_entities({
{
entity_number = 1,
name = blueprint_result.name,
position = {
-- Entities with an even number of tiles to a side need to be set at -0.5 instead of 0
math.ceil(width) % 2 == 0 and -0.5 or 0,
math.ceil(height) % 2 == 0 and -0.5 or 0,
},
recipe = blueprint_result.recipe,
},
})
player.add_to_clipboard(cursor_stack)
player.activate_paste()
end
else
player.create_local_flying_text({
text = { "message.rb-cannot-create-blueprint" },
create_at_cursor = true,
})
player.play_sound({ path = "utility/cannot_build" })
end
elseif action == "open_in_technology_window" then
local player_table = global.players[e.player_index]
player_table.flags.technology_gui_open = true
player.open_technology_gui(context.name)
elseif action == "view_source" then
local source = context_data[interaction.source]
if source then
return source
end
end
end
end
end
function gui_util.update_list_box(pane, source_tbl, player_data, iterator, options)
local i = 0
local children = pane.children
local add = pane.add
for _, obj_ident in iterator(source_tbl) do
local obj_data = database[obj_ident.class][obj_ident.name]
local info = formatter(obj_data, player_data, options)
if info then
i = i + 1
local style = info.researched and "rb_list_box_item" or "rb_unresearched_list_box_item"
local item = children[i]
if item then
item.style = style
item.caption = info.caption
item.tooltip = info.tooltip
item.enabled = info.num_interactions > 0
gui.update_tags(item, { context = { class = obj_ident.class, name = obj_ident.name } })
else
add({
type = "button",
style = style,
caption = info.caption,
tooltip = info.tooltip,
enabled = info.num_interactions > 0,
mouse_button_filter = { "left", "middle" },
tags = {
[script.mod_name] = {
context = { class = obj_ident.class, name = obj_ident.name },
flib = {
on_click = { gui = "search", action = "open_object" },
},
},
},
})
end
end
end
-- Destroy extraneous items
for j = i + 1, #children do
children[j].destroy()
end
end
return gui_util

View File

@@ -0,0 +1,98 @@
local on_tick_n = require("__flib__.on-tick-n")
local database = require("scripts.database")
local global_data = require("scripts.global-data")
local player_data = require("scripts.player-data")
return {
-- Migrations from before 3.0 are no longer required
["3.0.0"] = function()
-- NUKE EVERYTHING
global = {}
global_data.init()
global_data.build_prototypes()
database.build()
database.check_forces()
on_tick_n.init()
for i, player in pairs(game.players) do
-- Destroy all old Recipe Book GUIs
for _, window in pairs(player.gui.screen.children) do
if window.get_mod() == "RecipeBook" then
window.destroy()
end
end
-- Re-init player
player_data.init(i)
player_data.refresh(player, global.players[i])
end
end,
["3.0.2"] = function()
global.flags = nil
for _, player_table in pairs(global.players) do
player_table.flags.gui_open = nil
player_table.flags.technology_gui_open = nil
end
end,
["3.2.0"] = function()
for _, player_table in pairs(global.players) do
player_table.guis.info._sticky_id = nil
end
end,
["3.2.2"] = function()
-- Migrate header names
local changes = {
compatible_equipment = "accepted_equipment",
compatible_fuels = "can_burn",
compatible_mining_drills = "mined_by",
compatible_modules = "accepted_modules",
compatible_recipes = "can_craft",
compatible_resources = "can_mine",
}
for _, player_table in pairs(global.players) do
local page_settings = player_table.settings.pages
if page_settings then
for page_name, components in pairs(page_settings) do
local new_components = {}
for name, data in pairs(components) do
new_components[changes[name] or name] = data
end
page_settings[page_name] = new_components
end
end
end
end,
["3.3.0"] = function()
-- Add player and player_table to all GUIs
for i, player_table in pairs(global.players) do
local player = game.get_player(i)
if player and player.valid then
local guis = player_table.guis
if guis.search then
guis.search.player = player
guis.search.player_table = player_table
end
if guis.settings then
guis.settings.player = player
guis.settings.player_table = player_table
end
for recipe_name, quick_ref_gui in pairs(guis.quick_ref) do
quick_ref_gui.player = player
quick_ref_gui.player_table = player_table
quick_ref_gui.recipe_name = recipe_name
end
for id, info_gui in pairs(guis.info) do
if type(info_gui) == "table" then
info_gui.player = player
info_gui.player_table = player_table
info_gui.id = id
end
end
end
end
end,
}

View File

@@ -0,0 +1,235 @@
local dictionary = require("__flib__.dictionary-lite")
local table = require("__flib__.table")
local constants = require("constants")
local database = require("scripts.database")
local formatter = require("scripts.formatter")
local util = require("scripts.util")
local player_data = {}
function player_data.init(player_index)
--- @class PlayerTable
local data = {
favorites = {},
flags = {
can_open_gui = false,
show_message_after_translation = false,
technology_gui_open = false,
},
global_history = {},
guis = {
--- @type table<number, InfoGui>
info = { _next_id = 1 },
--- @type table<string, QuickRefGui>
quick_ref = {},
},
--- @type string?
language = nil,
settings = {
general = {},
categories = {},
},
translations = nil, -- Assigned its initial value in player_data.refresh
}
global.players[player_index] = data
end
function player_data.update_settings(player, player_table)
local former_settings = player_table.settings
local settings = {
general = {},
categories = {},
pages = {},
}
-- General settings
for category_name, settings_data in pairs(constants.general_settings) do
local former_category_settings = former_settings.general[category_name] or {}
local category_settings = {}
settings.general[category_name] = category_settings
for setting_name, setting_ident in pairs(settings_data) do
if setting_ident.type == "bool" then
local former_setting = former_category_settings[setting_name]
if former_setting ~= nil then
category_settings[setting_name] = former_setting
else
category_settings[setting_name] = setting_ident.default_value
end
elseif setting_ident.type == "enum" then
local former_setting = former_category_settings[setting_name]
if former_setting ~= nil and table.find(setting_ident.options, former_setting) then
category_settings[setting_name] = former_setting
else
category_settings[setting_name] = setting_ident.default_value
end
end
end
end
-- Categories
for _, category_class_name in pairs(constants.category_classes) do
local former_category_settings = former_settings.categories[category_class_name] or {}
local category_settings = {}
settings.categories[category_class_name] = category_settings
for category_name in pairs(database[category_class_name]) do
local disabled_by_default = constants.disabled_categories[category_class_name][category_name]
local former_setting = former_category_settings[category_name]
if former_setting ~= nil then
category_settings[category_name] = former_setting
else
category_settings[category_name] = not disabled_by_default
end
end
end
-- Pages
-- Default state (normal / collapsed / hidden)
-- Max rows
for class, page_ident in pairs(constants.pages) do
local former_page_settings = (former_settings.pages or {})[class] or {}
local page_settings = {}
settings.pages[class] = page_settings
for i, component_ident in pairs(page_ident) do
local component_name = component_ident.label or component_ident.source or i
local former_component_settings = former_page_settings[component_name] or {}
local component_settings = {
default_state = former_component_settings.default_state or component_ident.default_state or "normal",
}
page_settings[component_name] = component_settings
if component_ident.type == "list_box" then
component_settings.max_rows = former_component_settings.max_rows
or component_ident.max_rows
or constants.default_max_rows
end
-- Default state
-- Row settings for fixed tables
if component_ident.rows then
local former_row_settings = component_settings.rows or {}
local row_settings = {}
component_settings.rows = row_settings
for _, row_ident in pairs(component_ident.rows) do
local row_name = row_ident.label or row_ident.source
local state = former_row_settings[row_name]
if state == nil then
if row_ident.default_state ~= nil then
state = row_ident.default_state
else
state = true
end
end
row_settings[row_name] = state
end
end
end
end
-- Save to `global`
player_table.settings = settings
-- Create or purge memoizer cache
formatter.create_cache(player.index)
end
function player_data.validate_favorites(favorites)
local to_remove = {}
for key, obj in pairs(favorites) do
if not database[obj.class] or not database[obj.class][obj.name] then
table.insert(to_remove, key)
end
end
for _, key in pairs(to_remove) do
favorites[key] = nil
end
end
function player_data.validate_global_history(global_history)
for i = #global_history, 1, -1 do
local entry = global_history[i]
if not (database[entry.class] and database[entry.class][entry.name]) then
table.remove(global_history, i)
global_history[entry.class .. "." .. entry.name] = nil
end
end
end
function player_data.refresh(player, player_table)
-- Destroy GUIs
util.dispatch_all(player.index, "info", "close")
util.dispatch_all(player.index, "quick_ref", "close")
--- @type SearchGui?
local SearchGui = util.get_gui(player.index, "search")
if SearchGui then
SearchGui:destroy()
end
--- @type SettingsGui?
local SettingsGui = util.get_gui(player.index, "settings")
if SettingsGui then
SettingsGui:destroy()
end
-- Set flag
player_table.flags.can_open_gui = false
-- Set shortcut state
player.set_shortcut_toggled("rb-search", false)
player.set_shortcut_available("rb-search", false)
-- Validate favorites
player_data.validate_favorites(player_table.favorites)
-- Validate global history
player_data.validate_global_history(player_table.global_history)
-- Update settings
player_data.update_settings(player, player_table)
-- Run translations
player_table.translations = nil
end
function player_data.remove(player_index)
global.players[player_index] = nil
end
function player_data.check_cursor_stack(player)
local cursor_stack = player.cursor_stack
if cursor_stack and cursor_stack.valid and cursor_stack.valid_for_read and database.item[cursor_stack.name] then
return cursor_stack.name
end
return false
end
function player_data.update_global_history(global_history, new_context)
new_context = table.shallow_copy(new_context)
local ident = new_context.class .. "." .. new_context.name
if global_history[ident] then
for i, context in ipairs(global_history) do
if context.class == new_context.class and context.name == new_context.name then
-- Custom implementation of table.insert and table.remove that does the minimal amount of work needed
global_history[i] = nil
local prev = new_context
local current
for j = 1, i do
current = global_history[j]
global_history[j] = prev
prev = current
end
break
end
end
else
table.insert(global_history, 1, new_context)
global_history[ident] = true
end
for i = constants.global_history_size + 1, #global_history do
local context = global_history[i]
local ident = context.class .. "." .. context.name
global_history[ident] = nil
global_history[i] = nil
end
end
return player_data

View File

@@ -0,0 +1,67 @@
local constants = require("constants")
local database = require("scripts.database")
local remote_interface = {}
--- Returns a copy of the given object's information in the Recipe Book database.
--- @param class string One of `crafter`, `entity`, `equipment_category`, `equipment`, `fluid`, `fuel_category`, `group`, `item`, `lab`, `mining_drill`, `offshore_pump`, `recipe_category`, `recipe`, `resource_category`, `resource`, or `technology`.
--- @param name string The name of the object to get data for.
--- @return table? The object's data, or `nil` if the object was not found.
function remote_interface.get_object_data(class, name)
if not class then
error("Remote interface caller did not provide an object class.")
end
if not constants.pages[class] then
error("Remote interface caller provided an invalid class: `" .. class .. "`")
end
if not name then
error("Remote interface caller did not provide an object name.")
end
return database[class][name]
end
--- Opens the given info page in a Recipe Book window.
--- @param player_index uint
--- @param class string One of `crafter`, `entity`, `equipment_category`, `equipment`, `fluid`, `fuel_category`, `group`, `item`, `lab`, `mining_drill`, `offshore_pump`, `recipe_category`, `recipe`, `resource_category`, `resource`, or `technology`.
--- @param name string The name of the object to open.
--- @return boolean did_open Whether or not the page was opened.
function remote_interface.open_page(player_index, class, name)
if not class then
error("Remote interface caller did not provide an object class.")
end
if not constants.pages[class] then
error("Remote interface caller provided an invalid class: `" .. class .. "`")
end
if not name then
error("Remote interface caller did not provide an object name.")
end
local data = database[class][name]
if not data then
return false
end
local player = game.get_player(player_index)
local player_table = global.players[player_index]
if player_table.flags.can_open_gui then
OPEN_PAGE(player, player_table, { class = class, name = name })
return true
else
return false
end
end
--- Returns the current interface version.
---
--- This version will be incremented if breaking changes are made to the interface. Check against this version before calling interface functions to avoid crashing.
---
--- The current interface version is `4`.
--- @return number
function remote_interface.version()
return constants.interface_version
end
return remote_interface

View File

@@ -0,0 +1,264 @@
local bounding_box = require("__flib__.bounding-box")
local dictionary = require("__flib__.dictionary-lite")
local format = require("__flib__.format")
local math = require("__flib__.math")
local table = require("__flib__.table")
local constants = require("constants")
local core_util = require("__core__.lualib.util")
local util = {}
--- @return AmountIdent
function util.build_amount_ident(input)
--- @class AmountIdent
return {
amount = input.amount or false,
amount_min = input.amount_min or false,
amount_max = input.amount_max or false,
catalyst_amount = input.catalyst_amount or false,
probability = input.probability or false,
format = input.format or "format_amount",
}
end
-- HACK: Requiring `formatter` in this file causes a dependency loop
local function format_number(value)
return format.number(math.round(value, 0.01))
end
--- @class TemperatureIdent
--- @field string string
--- @field short_string string
--- @field min double
--- @field max double
--- Builds a `TemperatureIdent` based on the fluid input/output parameters.
function util.build_temperature_ident(fluid)
local temperature = fluid.temperature
local temperature_min = fluid.minimum_temperature
local temperature_max = fluid.maximum_temperature
local temperature_string
local short_temperature_string
local short_top_string
if temperature then
temperature_string = format_number(temperature)
short_temperature_string = core_util.format_number(temperature, true)
temperature_min = temperature
temperature_max = temperature
elseif temperature_min and temperature_max then
if temperature_min == math.min_double then
temperature_string = "" .. format_number(temperature_max)
short_temperature_string = "" .. core_util.format_number(temperature_max, true)
elseif temperature_max == math.max_double then
temperature_string = "" .. format_number(temperature_min)
short_temperature_string = "" .. core_util.format_number(temperature_min, true)
else
temperature_string = "" .. format_number(temperature_min) .. "-" .. format_number(temperature_max)
short_temperature_string = core_util.format_number(temperature_min, true)
short_top_string = core_util.format_number(temperature_max, true)
end
end
if temperature_string then
return {
string = temperature_string,
short_string = short_temperature_string,
short_top_string = short_top_string,
min = temperature_min,
max = temperature_max,
}
end
end
--- Get the "sorting number" of a temperature. Will sort in ascending order, with absolute, then min range, then max range.
--- @param temperature_ident TemperatureIdent
function util.get_sorting_number(temperature_ident)
if temperature_ident.min == math.min_double then
return temperature_ident.max + 0.001
elseif temperature_ident.max == math.max_double then
return temperature_ident.min + 0.003
elseif temperature_ident.min ~= temperature_ident.max then
return temperature_ident.min + 0.002
else
return temperature_ident.min
end
end
function util.convert_and_sort(tbl)
for key in pairs(tbl) do
tbl[#tbl + 1] = key
end
table.sort(tbl)
return tbl
end
function util.unique_string_array(initial_tbl)
initial_tbl = initial_tbl or {}
local hash = {}
for _, value in pairs(initial_tbl) do
hash[value] = true
end
return setmetatable(initial_tbl, {
__newindex = function(tbl, key, value)
if not hash[value] then
hash[value] = true
rawset(tbl, key, value)
end
end,
})
end
function util.unique_obj_array(initial_tbl)
local hash = {}
return setmetatable(initial_tbl or {}, {
__newindex = function(tbl, key, value)
if not hash[value.name] then
hash[value.name] = true
rawset(tbl, key, value)
end
end,
})
end
function util.frame_action_button(sprite, tooltip, ref, action)
return {
type = "sprite-button",
style = "frame_action_button",
sprite = sprite .. "_white",
hovered_sprite = sprite .. "_black",
clicked_sprite = sprite .. "_black",
tooltip = tooltip,
mouse_button_filter = { "left" },
ref = ref,
actions = {
on_click = action,
},
}
end
function util.process_placed_by(prototype)
local placed_by = prototype.items_to_place_this
if placed_by then
return table.map(placed_by, function(item_stack)
return {
class = "item",
name = item_stack.name,
amount_ident = util.build_amount_ident({ amount = item_stack.count }),
}
end)
end
end
function util.convert_categories(source_tbl, class)
local categories = {}
for category in pairs(source_tbl) do
categories[#categories + 1] = { class = class, name = category }
end
return categories
end
function util.convert_to_ident(class, source)
if source then
return { class = class, name = source }
end
end
--- @param prototype LuaEntityPrototype
--- @return DisplayResolution?
function util.get_size(prototype)
if prototype.selection_box then
local box = prototype.selection_box
return { height = math.ceil(bounding_box.height(box)), width = math.ceil(bounding_box.width(box)) }
end
end
--- @param prototype LuaEntityPrototype
function util.process_energy_source(prototype)
local burner = prototype.burner_prototype
local fluid_energy_source = prototype.fluid_energy_source_prototype
if burner then
return util.convert_categories(burner.fuel_categories, "fuel_category")
elseif fluid_energy_source then
local filter = fluid_energy_source.fluid_box.filter
if filter then
return {}, { class = "fluid", name = filter.name }
end
return { { class = "fuel_category", name = "burnable-fluid" } }
end
return {}
end
--- Safely retrive the given GUI, checking for validity.
--- @param player_index number
--- @param gui_name string
--- @param gui_key number|string?
function util.get_gui(player_index, gui_name, gui_key)
local player_table = global.players[player_index]
if not player_table then
return
end
local tbl = player_table.guis[gui_name]
if not tbl then
return
end
if gui_key then
tbl = tbl[gui_key]
end
if tbl and tbl.refs.window and tbl.refs.window.valid then
return tbl
end
end
--- Dispatch the given action on all GUIs of the given name.
--- @param player_index number
--- @param gui_name string
--- @param msg string|table
function util.dispatch_all(player_index, gui_name, msg)
local player_table = global.players[player_index]
if not player_table then
return
end
local ignored = gui_name == "info" and constants.ignored_info_ids or {}
for key, Gui in pairs(player_table.guis[gui_name]) do
if not ignored[key] then
Gui:dispatch(msg)
end
end
end
--- Determine if the given prototype is blueprintable
--- @param prototype LuaEntityPrototype
--- @return boolean
function util.is_blueprintable(prototype)
return prototype.has_flag("player-creation")
and not prototype.has_flag("not-selectable-in-game")
and not prototype.has_flag("not-blueprintable")
and not prototype.has_flag("hidden")
end
--- Create a new dictionary only if not in on_load.
--- @param name string
--- @param initial_contents Dictionary?
function util.new_dictionary(name, initial_contents)
if game then
dictionary.new(name, initial_contents)
end
end
--- Add to the dictionary only if not in on_load.
--- @param dict string
--- @param key string
--- @param localised LocalisedString
function util.add_to_dictionary(dict, key, localised)
if game then
-- Fall back to internal key in non-description dictionaries
if not string.find(dict, "description") then
localised = { "?", localised, key }
end
dictionary.add(dict, key, localised)
end
end
return util