Files
factorio-expcluster/exp_scenario/module/gui/autofill.lua
T
bbassieandClaude Opus 5.5 628e4df514 Rework features into a Feature class with per field events
Review points from Cooldude:

- Feature.register(name[, config]) is required again and returns a
  feature object. Each feature has its config, its fields, and its
  overrides, so one registry holds all the data. Only the overrides are
  stored. On load each feature points at its own stored table again.
- The methods are feature:guard(lib), feature:update_config(enabled,
  values), and feature:is_enabled(). Feature.get(name) looks up a
  feature, and commands flag the feature object.
- update_config raises on_config_updated once per changed value, with
  the feature name, path, old value, and new value. The gui refresh only
  reacts to the "enabled" path.
- apply only sets values. on_apply is gone. Lookup sets built from
  lists use Feature.to_set, which is cached by the list itself. A
  replaced list gets a new set, so the sets are also correct after a
  load. Protection exposes getters for its protected names and types.

Web: settings use small inputs, the same size as the config pages.
Fields are shaped like clusterio config field definitions. A field
with an inputComponent renders the component registered with the web
interface, so the role setting now uses core's role input, and other
plugin inputs can be used later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:03:11 +00:00

408 lines
15 KiB
Lua

--[[-- Gui - Autofill
Adds a config menu for setting autofill of placed entities
]]
local Feature = require("modules/exp_scenario/features")
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.gui.autofill")
local feature = Feature.register("autofill")
local FlyingText = require("modules/exp_util/flying_text")
local min = math.min
local string_format = string.format
--- @class ExpGui_Autofill.elements
local Elements = {}
--- Format a type and name to a rich text image
--- @param type string
--- @param name string
--- @return string
local function rich_img(type, name)
return string_format("[img=%s/%s]", type, name)
end
--- Toggle the visible state of a section
--- @class ExpGui_Autofill.elements.toggle_section_button: ExpElement
--- @field data table<LuaGuiElement, LuaGuiElement>
--- @overload fun(parent: LuaGuiElement, section: LuaGuiElement): LuaGuiElement
Elements.toggle_section_button = Gui.define("autofill/toggle_section_button")
:draw{
type = "sprite-button",
sprite = "utility/expand",
tooltip = { "exp-gui_autofill.tooltip-toggle-section-expand" },
style = "frame_action_button",
}
:style{
size = 20,
padding = -2,
}
:element_data(
Gui.from_argument(1)
)
:on_click(function(def, player, element)
--- @cast def ExpGui_Autofill.elements.toggle_section_button
local section = def.data[element]
if Gui.toggle_visible_state(section) then
element.sprite = "utility/collapse"
element.tooltip = { "exp-gui_autofill.tooltip-toggle-section-collapse" }
else
element.sprite = "utility/expand"
element.tooltip = { "exp-gui_autofill.tooltip-toggle-section-expand" }
end
end) --[[@as any]]
--- Toggle if an entity will be autofilled when played
--- @class ExpGui_Autofill.elements.toggle_entity_button: ExpElement
--- @field data table<LuaGuiElement, ExpGui_Autofill.entity_settings>
--- @overload fun(parent: LuaGuiElement, entity_settings: ExpGui_Autofill.entity_settings): LuaGuiElement
Elements.toggle_entity_button = Gui.define("autofill/toggle_entity_button")
:draw(function(_, parent, entity_settings)
--- @cast entity_settings ExpGui_Autofill.entity_settings
local enabled = entity_settings.enabled
return parent.add{
type = "sprite-button",
tooltip = { "exp-gui_autofill.tooltip-toggle-entity", rich_img("item", entity_settings.entity) },
sprite = enabled and "utility/confirm_slot" or "utility/close_black",
style = enabled and "shortcut_bar_button_green" or "shortcut_bar_button_red",
}
end)
:style{
size = 22,
padding = -2,
}
:element_data(
Gui.from_argument(1)
)
:on_click(function(def, player, element)
--- @cast def ExpGui_Autofill.elements.toggle_entity_button
local entity_settings = def.data[element]
local enabled = not entity_settings.enabled
entity_settings.enabled = enabled
-- Update the sprite and style
element.sprite = enabled and "utility/confirm_slot" or "utility/close_black"
element.style = enabled and "shortcut_bar_button_green" or "shortcut_bar_button_red"
-- Correct the button size
local style = element.style
style.padding = 0
style.height = 22
style.width = 22
end) --[[@as any]]
--- Toggle if an item will be inserted into an entity
--- @class ExpGui_Autofill.elements.toggle_item_button: ExpElement
--- @field data table<LuaGuiElement, ExpGui_Autofill.item_settings>
--- @overload fun(parent: LuaGuiElement, item_settings: ExpGui_Autofill.item_settings): LuaGuiElement
Elements.toggle_item_button = Gui.define("autofill/toggle_item_button")
:draw(function(_, parent, item_settings)
--- @cast item_settings ExpGui_Autofill.item_settings
return parent.add{
type = "sprite-button",
sprite = "item/" .. item_settings.name,
tooltip = { "exp-gui_autofill.tooltip-toggle-item", rich_img("item", item_settings.name), item_settings.category },
style = item_settings.enabled and "shortcut_bar_button_green" or "shortcut_bar_button_red",
}
end)
:style{
size = 32,
right_margin = -3,
padding = -1,
}
:element_data(
Gui.from_argument(1)
)
:on_click(function(def, player, element)
--- @cast def ExpGui_Autofill.elements.toggle_item_button
local item_settings = def.data[element]
local enabled = not item_settings.enabled
item_settings.enabled = enabled
-- Update the style
element.style = enabled and "shortcut_bar_button_green" or "shortcut_bar_button_red"
-- Correct the button size
local style = element.style
style.right_margin = -3
style.padding = -2
style.height = 32
style.width = 32
end) --[[@as any]]
--- The amount of an item to insert
--- @class ExpGui_Autofill.elements.amount_textfield: ExpElement
--- @field data table<LuaGuiElement, ExpGui_Autofill.item_settings>
--- @overload fun(parent: LuaGuiElement, item_settings: ExpGui_Autofill.item_settings): LuaGuiElement
Elements.amount_textfield = Gui.define("autofill/amount_textfield")
:draw(function(_, parent, item_settings)
--- @cast item_settings ExpGui_Autofill.item_settings
return parent.add{
type = "textfield",
tooltip = { "exp-gui_autofill.tooltip-amount", item_settings.category },
text = tostring(item_settings.amount) or "",
clear_and_focus_on_right_click = true,
numeric = true,
allow_decimal = false,
allow_negative = false,
}
end)
:style{
horizontally_stretchable = true,
minimal_width = 40,
height = 31,
padding = -2,
}
:element_data(
Gui.from_argument(1)
)
:on_text_changed(function(def, player, element, event)
--- @cast def ExpGui_Autofill.elements.amount_textfield
local value = tonumber(element.text) or 0
local clamped = math.clamp(value, 0, 999)
local item_settings = def.data[element]
item_settings.amount = clamped
if clamped ~= value then
element.text = tostring(clamped)
player.print{ "exp-gui_autofill.invalid", clamped, rich_img("item", item_settings.name), rich_img("entity", item_settings.entity) }
end
end) --[[@as any]]
--- A disabled version of the autofill settings used as a filler
Elements.disabled_autofill_setting = Gui.define("autofill/empty_autofill_setting")
:draw(function(_, parent)
local toggle_element_style = parent.add{
type = "sprite-button",
enabled = false,
}.style
toggle_element_style.right_margin = -3
toggle_element_style.width = 32
toggle_element_style.height = 32
local amount_element_style = parent.add{
type = "textfield",
enabled = false,
}.style
amount_element_style.horizontally_stretchable = true
amount_element_style.minimal_width = 40
amount_element_style.height = 31
amount_element_style.padding = -2
return Gui.no_return()
end)
--- Section representing an entity
--- @class ExpGui_Autofill.elements.section: ExpElement
--- @field data table<LuaGuiElement, LuaGuiElement|ExpGui_Autofill.entity_settings>
--- @overload fun(parent: LuaGuiElement, entity_settings: ExpGui_Autofill.entity_settings): LuaGuiElement
Elements.section = Gui.define("autofill/section")
:draw(function(def, parent, entity_settings)
--- @cast def ExpGui_Autofill.elements.section
--- @cast entity_settings ExpGui_Autofill.entity_settings
local header = Gui.elements.header(parent, {
caption = { "exp-gui_autofill.caption-section-header", rich_img("item", entity_settings.entity), { "entity-name." .. entity_settings.entity } },
tooltip = { "exp-gui_autofill.tooltip-toggle-section" },
})
local section_table = parent.add{
type = "table",
column_count = 3,
visible = false,
}
section_table.style.padding = 3
local header_label = header.label
Elements.toggle_entity_button(header, entity_settings)
def.data[header_label] = Elements.toggle_section_button(header, section_table)
def.data[section_table] = entity_settings
def:link_element(header_label)
return def:unlink_element(section_table)
end)
:on_click(function(def, player, element, event)
--- @cast def ExpGui_Autofill.elements.section
event.element = def.data[element] --[[@as LuaGuiElement]]
Elements.toggle_section_button:raise_event(event)
end) --[[@as any]]
--- Add an item category to a section, at most three can exist
--- @param section LuaGuiElement
--- @param category_name string
--- @return LuaGuiElement, number
function Elements.section.add_category(section, category_name)
local category = section.add{
type = "table",
column_count = 2,
}
category.style.vertical_spacing = 1
local ctn = 0
local entity_settings = Elements.section.data[section] --[[@as ExpGui_Autofill.entity_settings]]
for _, item_data in pairs(entity_settings.items) do
if item_data.category == category_name then
Elements.toggle_item_button(category, item_data)
Elements.amount_textfield(category, item_data)
ctn = ctn + 1
end
end
return category, ctn
end
--- @class ExpGui_Autofill.item_settings
--- @field entity string
--- @field category string
--- @field inv defines.inventory
--- @field name string
--- @field amount number
--- @field enabled boolean
--- @class ExpGui_Autofill.entity_settings
--- @field entity string
--- @field enabled boolean
--- @field items ExpGui_Autofill.item_settings[]
--- Container added to the left gui flow
--- @class ExpGui_Autofill.elements.container: ExpElement
--- @field data table<LuaPlayer, table<string, ExpGui_Autofill.entity_settings>>
Elements.container = Gui.define("autofill/container")
:draw(function(def, parent)
--- @cast def ExpGui_Autofill.elements.container
local container = Gui.elements.container(parent)
local scroll_pane = Gui.elements.scroll_pane(container, 524)
scroll_pane.style.padding = 0
-- Cant modify vertical spacing on scroll pane style so need a sub flow
scroll_pane = scroll_pane.add{ type = "flow", direction = "vertical" }
scroll_pane.style.vertical_spacing = 0
scroll_pane.style.padding = 0
-- Add a header
Gui.elements.header(scroll_pane, {
caption = { "exp-gui_autofill.caption-main" },
})
-- Setup the player data, this is used by section and item category so needs to be done here
local player = Gui.get_player(parent)
--- @type table<string, ExpGui_Autofill.entity_settings>
local player_data = def.data[player] or table.deep_copy(config.default_entities)
def.data[player] = player_data
-- Add sections for each entity
for _, entity_settings in pairs(player_data) do
local section = Elements.section(scroll_pane, entity_settings)
-- Add the categories
local categories, largest = {}, 0
for _, category_name in pairs(config.categories) do
local category, size = Elements.section.add_category(section, category_name)
if largest < size then
largest = size
end
categories[category] = size
end
-- Fill in blanks for smaller categories
for category, size in pairs(categories) do
for i = size, largest - 1 do
Elements.disabled_autofill_setting(category)
end
end
end
return container.parent
end) --[[@as any]]
--- Get the autofill settings for a player
--- @param player LuaPlayer
--- @param entity_name string
--- @return ExpGui_Autofill.entity_settings
function Elements.container.get_autofill_settings(player, entity_name)
return Elements.container.data[player][entity_name]
end
--- Add the element to the left flow with a toolbar button
Gui.add_left_element(Elements.container, false)
Gui.toolbar.create_button{
name = "toggle_autofill",
left_element = Elements.container,
sprite = config.icon,
tooltip = { "exp-gui_autofill.tooltip-main" },
visible = function(player, element)
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.autofill")
end
}
--- @param event EventData.on_built_entity
local function on_built_entity(event)
local player = Gui.get_player(event)
-- Check if the entity is in the config and enabled
local entity = event.entity
local entity_settings = Elements.container.get_autofill_settings(player, entity.name)
if not entity_settings or not entity_settings.enabled then
return
end
-- Get the inventory of the player
local player_inventory = player.get_main_inventory() --- @cast player_inventory -nil
local player_get_item_count = player_inventory.get_item_count
local player_remove = player_inventory.remove
-- Setup the tables being used
local offset = { x = 0, y = 0 }
local item = { name = "", count = 0 }
local color = { r = 0, g = 255, b = 0, a = 255 }
local flyingText = {
target_entity = entity,
text = "",
offset = offset,
player = player,
color = color,
}
for _, item_settings in pairs(entity_settings.items) do
-- Check if the item is enabled or goto next item
if not item_settings.enabled then goto continue end
-- Get the inventory of the entity or goto next item
local entity_inventory = entity.get_inventory(item_settings.inv)
if not entity_inventory then goto continue end
local preferred_amount = item_settings.amount
local item_amount = player_get_item_count(item_settings.name)
if item_amount ~= 0 then
item.name = item_settings.name
item.count = min(preferred_amount, item_amount)
if not entity_inventory.can_insert(item) then goto continue end
local inserted = entity_inventory.insert(item)
local ran_out = item_amount < preferred_amount
color.r = ran_out and 255 or 0
color.g = ran_out and 165 or 255
item.count = inserted
player_remove(item)
flyingText.text = { "exp-gui_autofill.inserted", inserted, rich_img("item", item_settings.name), rich_img("entity", entity.name) }
FlyingText.create_above_entity(flyingText)
offset.y = offset.y - 0.33
end
::continue::
end
end
local e = defines.events
return feature:guard{
elements = Elements,
events = {
[e.on_built_entity] = on_built_entity,
},
}