Первый фикс

Пачки некоторых позиций увеличены
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,206 @@
require("ui.elements.module_configurator")
-- ** LOCAL UTIL **
local function add_beacon_frame(parent_flow, modal_data)
local modal_elements = modal_data.modal_elements
local beacon = modal_data.object
local flow_beacon = parent_flow.add{type="frame", style="fp_frame_module", direction="horizontal"}
flow_beacon.add{type="label", caption={"fp.pu_beacon", 1}, style="heading_3_label"}
local beacon_filter = {{filter="type", type="beacon"}, {filter="flag", flag="hidden", invert=true, mode="and"}}
local button_beacon = flow_beacon.add{type="choose-elem-button", elem_type="entity", entity=beacon.proto.name,
tags={mod="fp", on_gui_elem_changed="select_beacon"}, elem_filters=beacon_filter,
style="fp_sprite-button_inset_tiny"}
button_beacon.style.right_margin = 12
modal_elements["beacon_button"] = button_beacon
flow_beacon.add{type="label", caption={"fp.info_label", {"fp.amount"}}, tooltip={"fp.beacon_amount_tt"},
style="heading_3_label"}
local beacon_amount = (beacon.amount ~= 0) and tostring(beacon.amount) or ""
local textfield_amount = flow_beacon.add{type="textfield", text=beacon_amount, enabled=(not BEACON_OVERLOAD_ACTIVE),
tags={mod="fp", on_gui_text_changed="beacon_amount"}}
util.gui.setup_numeric_textfield(textfield_amount, true, false)
if not BEACON_OVERLOAD_ACTIVE then util.gui.select_all(textfield_amount) end
textfield_amount.style.width = 40
textfield_amount.style.right_margin = 12
modal_elements["beacon_amount"] = textfield_amount
flow_beacon.add{type="label", caption={"fp.info_label", {"fp.beacon_total"}}, tooltip={"fp.beacon_total_tt"},
style="heading_3_label"}
local textfield_total = flow_beacon.add{type="textfield", name="fp_textfield_beacon_total_amount",
text=tostring(beacon.total_amount or "")}
util.gui.setup_numeric_textfield(textfield_total, true, false)
textfield_total.style.width = 40
modal_elements["beacon_total"] = textfield_total
local button_total = flow_beacon.add{type="sprite-button", tags={mod="fp", on_gui_click="use_beacon_selector"},
tooltip={"fp.beacon_selector_tt"}, sprite="fp_zone_selection", style="button", mouse_button_filter={"left"}}
button_total.style.padding = 2
button_total.style.size = 26
button_total.style.top_margin = 1
end
local function update_dialog_submit_button(modal_data)
local beacon_amount = tonumber(modal_data.modal_elements.beacon_amount.text)
local message = nil
if not beacon_amount or beacon_amount < 1 then
message = {"fp.beacon_issue_set_amount"}
elseif modal_data.module_set.module_count == 0 then
message = {"fp.beacon_issue_no_modules"}
end
modal_dialog.set_submit_button_state(modal_data.modal_elements, (message == nil), message)
end
local function handle_beacon_change(player, _, _)
local modal_data = util.globals.modal_data(player)
local beacon_button = modal_data.modal_elements.beacon_button
local beacon = modal_data.object
local previous_beacon_name = beacon.proto.name
if not beacon_button.elem_value then
beacon_button.elem_value = previous_beacon_name -- reset the beacon so it can't be nil
return -- nothing changed
elseif beacon_button.elem_value == previous_beacon_name then
return -- nothing changed
end
-- Change the beacon to the new type
beacon.proto = prototyper.util.find_prototype("beacons", beacon_button.elem_value, nil)
ModuleSet.normalize(beacon.module_set, {compatibility=true, trim=true, effects=true})
module_configurator.refresh_modules_flow(player, false)
end
local function handle_beacon_selection(player, entities)
local modal_elements = util.globals.modal_elements(player)
modal_elements.beacon_total.text = tostring(table_size(entities))
modal_elements.beacon_total.focus()
modal_dialog.leave_selection_mode(player)
end
local function open_beacon_dialog(player, modal_data)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", modal_data.floor_id)
local line = Floor.get(floor, "Line", modal_data.line_id)
modal_data.line = line
if line.beacon ~= nil then
modal_data.backup_beacon = Beacon.clone(line.beacon)
modal_data.object = line.beacon
else
local beacon_proto = prototyper.defaults.get(player, "beacons")
local beacon_count = util.globals.preferences(player).mb_defaults.beacon_count
modal_data.object = Beacon.init(beacon_proto, beacon_count, nil, line)
Line.set_beacon(line, modal_data.object)
end
if BEACON_OVERLOAD_ACTIVE then modal_data.object.amount = 1 end
modal_data.module_set = modal_data.object.module_set
local content_frame = modal_data.modal_elements.content_frame
content_frame.style.minimal_width = 460
-- Beacon
add_beacon_frame(content_frame, modal_data)
update_dialog_submit_button(modal_data)
-- Modules
modal_data.submit_checker = update_dialog_submit_button
module_configurator.add_modules_flow(content_frame, modal_data)
module_configurator.refresh_modules_flow(player, false)
end
local function close_beacon_dialog(player, action)
local modal_data = util.globals.modal_data(player)
local subfactory = util.globals.context(player).subfactory
if action == "submit" then
local beacon = modal_data.object
local total_amount = tonumber(modal_data.modal_elements.beacon_total.text) or 0
beacon.total_amount = (total_amount > 0) and total_amount or nil
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
elseif action == "delete" then
Line.set_beacon(modal_data.line, nil)
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
else -- action == "cancel"
Line.set_beacon(modal_data.line, modal_data.backup_beacon) -- can write nil
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_elem_changed = {
{
name = "select_beacon",
handler = handle_beacon_change
}
},
on_gui_text_changed = {
{
name = "beacon_amount",
handler = (function(player, _, _)
local modal_data = util.globals.modal_data(player)
modal_data.object.amount = tonumber(modal_data.modal_elements.beacon_amount.text) or 0
ModuleSet.normalize(modal_data.object.module_set, {effects=true})
module_configurator.refresh_effects_flow(modal_data)
update_dialog_submit_button(modal_data)
end)
}
},
on_gui_click = {
{
name = "use_beacon_selector",
timeout = 20,
handler = (function(player, _, _)
modal_dialog.enter_selection_mode(player, "fp_beacon_selector")
end)
}
}
}
listeners.dialog = {
dialog = "beacon",
metadata = (function(modal_data)
local action = (modal_data.edit) and "edit" or "add"
return {
caption = {"", {"fp." .. action}, " ", {"fp.pl_beacon", 1}},
subheader_text = {("fp.beacon_dialog_description_" .. action), modal_data.machine_name},
create_content_frame = true,
show_submit_button = true,
show_delete_button = (modal_data.edit)
}
end),
open = open_beacon_dialog,
close = close_beacon_dialog
}
listeners.misc = {
on_player_cursor_stack_changed = (function(player, _)
-- If the cursor stack is not valid_for_read, it's empty, thus the selector has been put away
if util.globals.flags(player).selection_mode and not player.cursor_stack.valid_for_read then
modal_dialog.leave_selection_mode(player)
end
end),
on_player_selected_area = (function(player, event)
if event.item == "fp_beacon_selector" and util.globals.flags(player).selection_mode then
handle_beacon_selection(player, event.entities)
end
end)
}
return { listeners }

View File

@@ -0,0 +1,227 @@
-- This file contains general-purpose dialogs that are generic and used in several places
-- Note: This system seems to have a problem, as references to functions stored in global
-- break when reloading the game. In that case, just close the dialog without changes
-- ** CHOOSER **
local function add_chooser_button(modal_elements, definition)
local style, note = "flib_slot_button_default", nil
if definition.selected then
style = "flib_slot_button_green"
note = {"fp.selected"}
elseif definition.preferred then
style = "flib_slot_button_pink"
note = {"fp.preferred"}
end
local first_line = (note == nil) and {"fp.tt_title", definition.localised_name}
or {"fp.tt_title_with_note", definition.localised_name, note}
local tooltip = {"", first_line, "\n", definition.amount_line, "\n\n", definition.tooltip_appendage}
modal_elements.choices_table.add{type="sprite-button", style=style, tooltip=tooltip,
tags={mod="fp", on_gui_click="make_chooser_choice", element_id=definition.element_id},
sprite=definition.sprite, number=definition.button_number, mouse_button_filter={"left"}}
end
local function handler_chooser_button_click(player, tags, event)
local handler_name = util.globals.modal_data(player).click_handler_name
GLOBAL_HANDLERS[handler_name](player, tags.element_id, event)
util.raise.close_dialog(player, "cancel")
end
-- Handles populating the chooser dialog
local function open_chooser_dialog(_, modal_data)
local modal_elements = modal_data.modal_elements
local content_frame = modal_elements.content_frame
local frame_choices = content_frame.add{type="frame", direction="horizontal", style="slot_button_deep_frame"}
modal_elements.choices_table = frame_choices.add{type="table", column_count=8, style="filter_slot_table"}
for _, definition in ipairs(modal_data.button_definitions) do
add_chooser_button(modal_elements, definition)
end
end
-- ** EVENTS **
local chooser_listeners = {}
chooser_listeners.gui = {
on_gui_click = {
{
name = "make_chooser_choice", -- great naming right there
timeout = 20,
handler = handler_chooser_button_click
}
}
}
chooser_listeners.dialog = {
dialog = "chooser",
metadata = (function(modal_data)
local info_tag = (modal_data.text_tooltip) and "[img=info]" or ""
return {
caption = {"", {"fp.choose"}, " ", modal_data.title},
subheader_text = {"", modal_data.text, " ", info_tag},
subheader_tooltip = (modal_data.text_tooltip or ""),
create_content_frame = true
}
end),
open = open_chooser_dialog
}
-- ** OPTIONS **
local options_listeners = {}
-- ** LOCAL UTIL **
local function call_change_handler(player, tags, event)
local modal_data = util.globals.modal_data(player)
local handler_name = modal_data.field_handlers[tags.field_name]
if handler_name then GLOBAL_HANDLERS[handler_name](modal_data, event) end
end
-- ** ELEMENTS **
options_listeners.gui = {}
local elements = {}
-- ** TEXTFIELD **
elements.textfield = {}
function elements.textfield.create(table, field, modal_elements)
local textfield = table.add{type="textfield", text=field.text,
tags={mod="fp", on_gui_text_changed="change_option", field_name=field.name}}
textfield.style.width = (field.width or 180)
if field.focus then util.gui.select_all(textfield) end
modal_elements[field.name] = textfield
end
function elements.textfield.read(textfield)
return textfield.text
end
-- ** NUMERIC TEXTFIELD **
elements.numeric_textfield = {}
function elements.numeric_textfield.create(table, field, modal_elements)
local textfield = table.add{type="textfield", text=tostring(field.text or ""),
tags={mod="fp", on_gui_text_changed="change_option", field_name=field.name}}
textfield.style.width = (field.width or 75)
util.gui.setup_numeric_textfield(textfield, true, false)
if field.focus then util.gui.select_all(textfield) end
modal_elements[field.name] = textfield
end
function elements.numeric_textfield.read(textfield)
return tonumber(textfield.text)
end
-- ** TEXTFIELD EVENT **
options_listeners.gui.on_gui_text_changed = {
{
name = "change_option",
handler = call_change_handler
}
}
-- ** ON OFF SWITCH **
elements.on_off_switch = {}
options_listeners.gui.on_gui_switch_state_changed = {
{
name = "change_option",
handler = call_change_handler
}
}
function elements.on_off_switch.create(table, field, modal_elements)
local state = util.gui.switch.convert_to_state(field.state)
local switch = table.add{type="switch", switch_state=state,
tags={mod="fp", on_gui_switch_state_changed="change_option", field_name=field.name},
left_label_caption={"fp.on"}, right_label_caption={"fp.off"}}
modal_elements[field.name] = switch
end
function elements.on_off_switch.read(switch)
return util.gui.switch.convert_to_boolean(switch.switch_state)
end
-- ** CHOOSE ELEM BUTTON **
elements.choose_elem_button = {}
options_listeners.gui.on_gui_elem_changed = {
{
name = "change_option",
handler = call_change_handler
}
}
function elements.choose_elem_button.create(table, field, modal_elements)
local choose_elem_button = table.add{type="choose-elem-button",
tags={mod="fp", on_gui_elem_changed="change_option", field_name=field.name},
elem_type=field.elem_type, style="fp_sprite-button_inset"}
choose_elem_button.elem_value = field.elem_value
modal_elements[field.name] = choose_elem_button
end
function elements.choose_elem_button.read(choose_elem_button)
return choose_elem_button.elem_value
end
local function open_options_dialog(_, modal_data)
local modal_elements = modal_data.modal_elements
local content_frame = modal_elements.content_frame
local table_options = content_frame.add{type="table", column_count=2}
table_options.style.margin = {0, 12, 8, 2}
table_options.style.horizontal_spacing = 24
table_options.style.vertical_spacing = 16
modal_data.field_handlers = {}
for _, field in ipairs(modal_data.fields) do
local caption = (field.tooltip) and {"fp.info_label", field.caption} or field.caption
table_options.add{type="label", caption=caption, tooltip=field.tooltip, style="heading_3_label"}
elements[field.type].create(table_options, field, modal_elements)
modal_data.field_handlers[field.name] = field.change_handler_name
end
-- Call all the change handlers once to set the initial state correctly
for field_name, handler_name in pairs(modal_data.field_handlers) do
GLOBAL_HANDLERS[handler_name](modal_data, modal_elements[field_name])
end
end
local function close_options_dialog(player, action)
local modal_data = util.globals.modal_data(player)
local options_data = {}
for _, field in pairs(modal_data.fields) do
local element = modal_data.modal_elements[field.name]
options_data[field.name] = elements[field.type].read(element)
end
local handler_name = modal_data.submission_handler_name
GLOBAL_HANDLERS[handler_name](player, options_data, action)
end
options_listeners.dialog = {
dialog = "options",
metadata = (function(modal_data) return {
caption = modal_data.title,
subheader_text = modal_data.text,
create_content_frame = true,
show_submit_button = true,
show_delete_button = modal_data.allow_deletion
} end),
open = open_options_dialog,
close = close_options_dialog
}
return { chooser_listeners, options_listeners }

View File

@@ -0,0 +1,252 @@
require("ui.elements.module_configurator")
-- ** LOCAL UTIL **
local function refresh_machine_frame(player)
local modal_data = util.globals.modal_data(player)
local table_machine = modal_data.modal_elements.machine_table
table_machine.clear()
local current_proto = modal_data.object.proto
for _, machine_proto in pairs(PROTOTYPE_MAPS.machines[current_proto.category].members) do
if Line.is_machine_applicable(modal_data.line, machine_proto) then
local attributes = prototyper.util.get_attributes(machine_proto)
local tooltip = {"", {"fp.tt_title", machine_proto.localised_name}, "\n", attributes}
local selected = (machine_proto.id == current_proto.id)
local button_style = (selected) and "flib_slot_button_green" or "flib_slot_button_default"
table_machine.add{type="sprite-button", sprite=machine_proto.sprite, tooltip=tooltip,
tags={mod="fp", on_gui_click="choose_machine", proto_id=machine_proto.id},
style=button_style, mouse_button_filter={"left"}}
end
end
end
local function refresh_fuel_frame(player)
local modal_data = util.globals.modal_data(player)
local machine = modal_data.object
local modal_elements = modal_data.modal_elements
modal_elements.fuel_table.clear()
local machine_burner = machine.proto.burner
modal_elements.fuel_table.visible = (machine_burner ~= nil)
modal_elements.fuel_info_label.visible = (machine_burner == nil)
if machine_burner == nil then return end
local current_proto = machine.fuel.proto
-- Applicable fuels come from all categories that this burner supports
for category_name, _ in pairs(machine_burner.categories) do
local category = PROTOTYPE_MAPS.fuels[category_name]
if category ~= nil then
for _, fuel_proto in pairs(category.members) do
local attributes = prototyper.util.get_attributes(fuel_proto)
local tooltip = {"", {"fp.tt_title", fuel_proto.localised_name}, "\n", attributes}
local selected = (current_proto.category == fuel_proto.category and current_proto.id == fuel_proto.id)
local button_style = (selected) and "flib_slot_button_green" or "flib_slot_button_default"
modal_elements.fuel_table.add{type="sprite-button", sprite=fuel_proto.sprite,
tags={mod="fp", on_gui_click="choose_fuel", proto_id=(category.id .. "_" .. fuel_proto.id)},
tooltip=tooltip, style=button_style, mouse_button_filter={"left"}}
end
end
end
end
local function refresh_limit_elements(player)
local modal_data = util.globals.modal_data(player)
local textfield = modal_data.modal_elements.limit_textfield
local switch = modal_data.modal_elements.force_limit_switch
local machine = modal_data.object
textfield.text = tostring(machine.limit or "")
switch.switch_state = util.gui.switch.convert_to_state(machine.force_limit)
switch.enabled = (machine.limit ~= nil)
end
local function add_choices_frame(parent_frame, modal_elements, type)
local frame_choices = parent_frame.add{type="frame", direction="vertical", style="fp_frame_bordered_stretch"}
local table_choices = frame_choices.add{type="table", column_count=3}
table_choices.style.horizontal_spacing = 20
table_choices.style.padding = {0, 0, -4, 0}
table_choices.add{type="label", caption={"fp.pu_" .. type, 1}, style="heading_3_label"}
local flow = table_choices.add{type="flow", direction="horizontal"}
local frame = flow.add{type="frame", direction="horizontal", style="slot_button_deep_frame"}
local table = frame.add{type="table", column_count=7, style="filter_slot_table"}
modal_elements[type .. "_table"] = table
if type == "fuel" then
local label_info = flow.add{type="label", caption={"fp.machine_does_not_use_fuel"}}
label_info.style.padding = {10, -8} -- make sure spacing stays the same when no fuel button is shown
modal_elements["fuel_info_label"] = label_info
end
end
local function add_limit_frame(parent_frame, modal_elements)
local frame_limit = parent_frame.add{type="frame", direction="horizontal", style="fp_frame_bordered_stretch"}
local table_limit = frame_limit.add{type="table", column_count=2}
table_limit.style.horizontal_spacing = 20
table_limit.style.padding = {6, 0, 2, 0}
local flow_limit = table_limit.add{type="flow", direction="horizontal", style="fp_flow_horizontal_centered"}
flow_limit.add{type="label", caption={"fp.info_label", {"fp.machine_limit"}},
tooltip={"fp.machine_limit_tt"}, style="heading_3_label"}
local textfield_limit = flow_limit.add{type="textfield", tags={mod="fp", on_gui_text_changed="machine_limit"}}
textfield_limit.style.width = 45
util.gui.setup_numeric_textfield(textfield_limit, true, false)
modal_elements["limit_textfield"] = textfield_limit
local flow_force_limit = table_limit.add{type="flow", direction="horizontal", style="fp_flow_horizontal_centered"}
flow_force_limit.add{type="label", caption={"fp.info_label", {"fp.machine_force_limit"}},
tooltip={"fp.machine_force_limit_tt"}, style="heading_3_label"}
local switch_force_limit = util.gui.switch.add_on_off(flow_force_limit, "machine_force_limit", {}, "left")
modal_elements["force_limit_switch"] = switch_force_limit
end
local function handle_machine_choice(player, tags, _)
local modal_data = util.globals.modal_data(player)
local machine = modal_data.object
local machine_category_id = PROTOTYPE_MAPS.machines[machine.proto.category].id
local machine_proto = global.prototypes.machines[machine_category_id].members[tags.proto_id]
-- This can't use Line.change_machine_to_proto() as that modifies the line, which we can't do
machine.proto = machine_proto
Machine.normalize_fuel(machine, player)
ModuleSet.normalize(machine.module_set, {compatibility=true, trim=true, effects=true})
-- Make sure the line's beacon is removed if this machine no longer supports it
if machine.proto.allowed_effects == nil then Line.set_beacon(machine.parent, nil) end
refresh_machine_frame(player)
refresh_fuel_frame(player)
module_configurator.refresh_modules_flow(player, false)
end
local function handle_fuel_choice(player, tags, _)
local modal_data = util.globals.modal_data(player)
local split_id = util.split_string(tags.proto_id, "_")
modal_data.object.fuel.proto = global.prototypes.fuels[split_id[1]].members[split_id[2]]
refresh_fuel_frame(player)
end
local function change_machine_limit(player, _, event)
local modal_data = util.globals.modal_data(player)
local machine = modal_data.object
machine.limit = tonumber(event.element.text)
if machine.limit == nil then machine.force_limit = true end
refresh_limit_elements(player)
end
local function change_machine_force_limit(player, _, event)
local modal_data = util.globals.modal_data(player)
local switch_state = util.gui.switch.convert_to_boolean(event.element.switch_state)
modal_data.object.force_limit = switch_state
refresh_limit_elements(player)
end
local function open_machine_dialog(player, modal_data)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", modal_data.floor_id)
modal_data.line = Floor.get(floor, "Line", modal_data.line_id)
modal_data.object = modal_data.line.machine
local modal_elements = modal_data.modal_elements
local content_frame = modal_elements.content_frame
content_frame.style.minimal_width = 460
modal_data.machine_backup = Machine.clone(modal_data.object)
modal_data.beacon_backup = Beacon.clone(modal_data.line.beacon)
modal_data.module_set = modal_data.object.module_set
-- Choices
add_choices_frame(content_frame, modal_elements, "machine")
refresh_machine_frame(player)
add_choices_frame(content_frame, modal_elements, "fuel")
refresh_fuel_frame(player)
-- Limit
if modal_data.line.parent.parent.matrix_free_items == nil then
add_limit_frame(content_frame, modal_elements)
refresh_limit_elements(player)
end
-- Modules
module_configurator.add_modules_flow(content_frame, modal_data)
module_configurator.refresh_modules_flow(player, false)
end
local function close_machine_dialog(player, action)
local modal_data = util.globals.modal_data(player)
local machine, line = modal_data.object, modal_data.line
if action == "submit" then
ModuleSet.normalize(machine.module_set, {sort=true})
local subfactory = util.globals.context(player).subfactory
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
else -- action == "cancel"
line.machine = modal_data.machine_backup
ModuleSet.normalize(line.machine.module_set, {effects=true})
Line.set_beacon(modal_data.line, modal_data.beacon_backup)
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "choose_machine",
handler = handle_machine_choice
},
{
name = "choose_fuel",
handler = handle_fuel_choice
}
},
on_gui_text_changed = {
{
name = "machine_limit",
handler = change_machine_limit
}
},
on_gui_switch_state_changed = {
{
name = "machine_force_limit",
handler = change_machine_force_limit
}
}
}
listeners.dialog = {
dialog = "machine",
metadata = (function(modal_data) return {
caption = {"", {"fp.edit"}, " ", {"fp.pl_machine", 1}},
subheader_text = {"fp.machine_dialog_description", modal_data.recipe_name},
create_content_frame = true,
show_submit_button = true
} end),
open = open_machine_dialog,
close = close_machine_dialog
}
return { listeners }

View File

@@ -0,0 +1,196 @@
local matrix_engine = require("backend.calculation.matrix_engine")
-- ** LOCAL UTIL **
local function show_linearly_dependent_recipes(modal_data, recipe_protos)
local flow_recipes = modal_data.modal_elements.content_frame.add{type="flow", direction="vertical"}
local label_title = flow_recipes.add{type="label", caption={"fp.matrix_linearly_dependent_recipes"}}
label_title.style.font = "heading-2"
local frame_recipes = flow_recipes.add{type="frame", direction="horizontal", style="slot_button_deep_frame"}
local table_recipes = frame_recipes.add{type="table", column_count=8, style="filter_slot_table"}
for _, recipe_proto in ipairs(recipe_protos) do
table_recipes.add{type="sprite", sprite=recipe_proto.sprite, tooltip=recipe_proto.localised_name}
end
end
local function update_dialog_submit_button(modal_data, matrix_metadata)
local num_needed_free_items = matrix_metadata.num_rows - matrix_metadata.num_cols + #matrix_metadata.free_items
local curr_free_items = #modal_data["free_items"]
local message = nil
if num_needed_free_items > curr_free_items then
message = {"fp.matrix_constrained_items", num_needed_free_items, {"fp.pl_item", num_needed_free_items}}
end
modal_dialog.set_submit_button_state(modal_data.modal_elements, (message == nil), message)
end
local function refresh_item_category(modal_data, type)
local table_items = modal_data.modal_elements[type .. "_table"]
table_items.clear()
-- order items by the natural Factorio order
local display_order = {}
for index, proto in ipairs(modal_data[type .. "_items"]) do
display_order[index] = {
key = { proto.group.order, proto.subgroup.order, proto.order, proto.name, index },
index = index, proto = proto }
end
table.sort(display_order, function (item_1, item_2)
local key_1 = item_1.key
local key_2 = item_2.key
assert(#key_1 == #key_2)
for i = 1, #key_1 do
if key_1[i] ~= key_2[i] then
return key_1[i] < key_2[i]
end
end
return false -- identical items
end)
for _, item in pairs(display_order) do
local index = item.index
local proto = item.proto
local button = table_items.add{type="sprite-button", sprite=proto.sprite, tooltip=proto.localised_name,
tags={mod="fp", on_gui_click="swap_item_category", type=type, index=index}, style="flib_slot_button_default",
mouse_button_filter={"left"}}
button.style.size = 48
end
end
local function create_item_category(modal_data, type, label_arg)
local flow_category = modal_data.modal_elements.content_frame.add{type="flow", direction="vertical"}
local title_string = (type == "free") and {"fp.matrix_free_items"}
or {"fp.matrix_constrained_items", label_arg, {"fp.pl_item", label_arg}}
local label_title = flow_category.add{type="label", caption=title_string}
label_title.style.single_line = false
local frame_items = flow_category.add{type="frame", direction="horizontal", style="slot_button_deep_frame"}
local table_items = frame_items.add{type="table", column_count=8, style="filter_slot_table"}
modal_data.modal_elements[type .. "_table"] = table_items
refresh_item_category(modal_data, type)
end
local function swap_item_category(player, tags, _)
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
local modal_data = ui_state.modal_data
-- update the free items here, set the constrained items based on linear dependence data
if tags.type == "free" then
-- note this assumes the gui's list has the same order as the subfactory
table.remove(subfactory.matrix_free_items, tags.index)
else -- "constrained"
local item_proto = modal_data["constrained_items"][tags.index]
table.insert(subfactory.matrix_free_items, item_proto)
end
local matrix_metadata = matrix_engine.get_matrix_solver_metadata(modal_data.subfactory_data)
local linear_dependence_data = matrix_engine.get_linear_dependence_data(modal_data.subfactory_data, matrix_metadata)
modal_data.constrained_items = linear_dependence_data.allowed_free_items
modal_data.free_items = matrix_metadata.free_items
refresh_item_category(modal_data, "constrained")
refresh_item_category(modal_data, "free")
update_dialog_submit_button(modal_data, matrix_metadata)
end
local function matrix_early_abort_check(player, modal_data)
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
if subfactory.selected_floor.Line.count == 0 then return true end
local subfactory_data = solver.generate_subfactory_data(player, subfactory)
local matrix_metadata = matrix_engine.get_matrix_solver_metadata(subfactory_data)
modal_data.subfactory_data = subfactory_data
local linear_dependence_data = matrix_engine.get_linear_dependence_data(subfactory_data, matrix_metadata)
if next(linear_dependence_data.linearly_dependent_recipes) then -- too many ways to create the products
modal_data.linearly_dependent_recipes = linear_dependence_data.linearly_dependent_recipes
subfactory.linearly_dependant = true
return false
end
subfactory.linearly_dependant = false -- TODO not the proper way to signal this, but it works
modal_data.constrained_items = linear_dependence_data.allowed_free_items
modal_data.free_items = matrix_metadata.free_items
local num_needed_free_items = matrix_metadata.num_rows - matrix_metadata.num_cols + #matrix_metadata.free_items
if num_needed_free_items == 0 then -- User doesn't need to select any free items, just run the matrix solver
if modal_data.configuration then
util.messages.raise(player, "warning", {"fp.warning_no_matrix_configuration_needed"}, 1)
end
return true
end
-- If it gets to here, the dialog should open normally
modal_data.num_needed_free_items = num_needed_free_items
modal_data.matrix_metadata = matrix_metadata
return false
end
local function open_matrix_dialog(player, modal_data)
if util.globals.context(player).subfactory.linearly_dependant then
show_linearly_dependent_recipes(modal_data, modal_data.linearly_dependent_recipes)
modal_dialog.set_submit_button_state(modal_data.modal_elements, false, {"fp.matrix_linearly_dependent_recipes"})
-- Dispose of the temporary GUI-opening variables
modal_data.linearly_dependent_recipes = nil
else
create_item_category(modal_data, "constrained", modal_data.num_needed_free_items)
create_item_category(modal_data, "free")
update_dialog_submit_button(modal_data, modal_data.matrix_metadata)
-- Dispose of the temporary GUI-opening variables
modal_data.num_needed_free_items = nil
modal_data.matrix_metadata = nil
end
end
local function close_matrix_dialog(player, action)
if action == "submit" then
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
subfactory.matrix_free_items = ui_state.modal_data.free_items
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
elseif action == "cancel" then
util.raise.refresh(player, "production_detail", nil)
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "swap_item_category",
handler = swap_item_category
}
}
}
listeners.dialog = {
dialog = "matrix",
metadata = (function(_) return {
caption = {"fp.matrix_solver"},
create_content_frame = true,
show_submit_button = true
} end),
early_abort_check = matrix_early_abort_check,
open = open_matrix_dialog,
close = close_matrix_dialog
}
return { listeners }

View File

@@ -0,0 +1,485 @@
-- This dialog works as the product picker currently, but could also work as an ingredient picker down the line
-- ** ITEM PICKER **
local function select_item_group(modal_data, new_group_id)
modal_data.selected_group_id = new_group_id
for group_id, group_elements in pairs(modal_data.modal_elements.groups) do
local selected_group = (group_id == new_group_id)
group_elements.button.enabled = not selected_group
group_elements.scroll_pane.visible = selected_group
end
end
local function search_picker_items(player, search_term)
local modal_data = util.globals.modal_data(player)
local modal_elements = modal_data.modal_elements
-- Groups are indexed continuously, so using ipairs here is fine
local first_visible_group_id = nil
for group_id, group in ipairs(modal_elements.groups) do
local any_item_visible = false
for _, subgroup_table in pairs(group.subgroup_tables) do
for item_data, element in pairs(subgroup_table) do
-- Can only get to this if translations are complete, as the textfield is disabled otherwise
local visible = (search_term == item_data.name)
or (string.find(item_data.translated_name, search_term, 1, true) ~= nil)
element.visible = visible
any_item_visible = any_item_visible or visible
end
end
group.button.visible = any_item_visible
first_visible_group_id = first_visible_group_id or ((any_item_visible) and group_id or nil)
end
local any_result_found = (first_visible_group_id ~= nil)
modal_elements.warning_label.visible = not any_result_found
modal_elements.filter_frame.visible = any_result_found
if first_visible_group_id ~= nil then
local selected_group_id = modal_data.selected_group_id
local is_selected_group_visible = modal_elements.groups[selected_group_id].button.visible
local group_id_to_select = is_selected_group_visible and selected_group_id or first_visible_group_id
select_item_group(modal_data, group_id_to_select)
end
end
local function add_item_picker(parent_flow, player)
local player_table = util.globals.player_table(player)
local ui_state = player_table.ui_state
local modal_elements = ui_state.modal_data.modal_elements
local translations = player_table.translation_tables
local label_warning = parent_flow.add{type="label", caption={"fp.error_message", {"fp.no_item_found"}}}
label_warning.style.font = "heading-2"
label_warning.style.margin = 12
label_warning.visible = false -- There can't be a warning upon first opening of the dialog
modal_elements["warning_label"] = label_warning
-- Item picker (optimized for performance, so not everything is done in the obvious way)
local groups_per_row = MAGIC_NUMBERS.groups_per_row
local table_item_groups = parent_flow.add{type="table", style="filter_group_table", column_count=groups_per_row}
table_item_groups.style.width = 71 * groups_per_row
table_item_groups.style.horizontal_spacing = 0
table_item_groups.style.vertical_spacing = 0
local frame_filters = parent_flow.add{type="frame", style="fp_frame_slot_table"}
modal_elements["filter_frame"] = frame_filters
local group_id_cache, group_flow_cache, subgroup_table_cache = {}, {}, {}
modal_elements.groups = {}
local existing_products = {}
if not ui_state.modal_data.create_subfactory then -- check if this is for a new subfactory or not
for _, product in pairs(Subfactory.get_in_order(ui_state.context.subfactory, "Product")) do
existing_products[product.proto.name] = true
end
end
local items_per_row = MAGIC_NUMBERS.items_per_row
local current_item_rows, max_item_rows = 0, 0
local current_items_in_table_count = 0
for _, item_proto in ipairs(SORTED_ITEMS) do
if not item_proto.hidden and not item_proto.ingredient_only then
local group_name = item_proto.group.name
local group_id = group_id_cache[group_name]
local flow_subgroups, subgroup_tables = nil, nil
if group_id == nil then
local cache_count = table_size(group_id_cache) + 1
group_id_cache[group_name] = cache_count
group_id = cache_count
local button_group = table_item_groups.add{type="sprite-button", sprite=("item-group/" .. group_name),
tags={mod="fp", on_gui_click="select_picker_item_group", group_id=group_id},
style="fp_sprite-button_group_tab", tooltip=item_proto.group.localised_name,
mouse_button_filter={"left"}}
-- This only exists when button_group also exists
local scroll_pane_subgroups = frame_filters.add{type="scroll-pane",
style="fp_scroll-pane_slot_table"}
scroll_pane_subgroups.style.vertically_stretchable = true
local frame_subgroups = scroll_pane_subgroups.add{type="frame", style="slot_button_deep_frame"}
frame_subgroups.style.vertically_stretchable = true
-- This flow is only really needed to set the correct vertical spacing
flow_subgroups = frame_subgroups.add{type="flow", name="flow_group", direction="vertical"}
flow_subgroups.style.vertical_spacing = 0
group_flow_cache[group_id] = flow_subgroups
modal_elements.groups[group_id] = {
button = button_group,
frame = frame_subgroups,
scroll_pane = scroll_pane_subgroups,
subgroup_tables = {}
}
subgroup_tables = modal_elements.groups[group_id].subgroup_tables
-- Catch up on adding the last item flow's row count
current_item_rows = current_item_rows + math.ceil(current_items_in_table_count / items_per_row)
current_items_in_table_count = 0
max_item_rows = math.max(current_item_rows, max_item_rows)
current_item_rows = 0
else
flow_subgroups = group_flow_cache[group_id]
subgroup_tables = modal_elements.groups[group_id].subgroup_tables
end
local subgroup_name = item_proto.subgroup.name
local table_subgroup = subgroup_table_cache[subgroup_name]
local subgroup_table = nil
if table_subgroup == nil then
table_subgroup = flow_subgroups.add{type="table", column_count=items_per_row,
style="filter_slot_table"}
table_subgroup.style.horizontally_stretchable = true
subgroup_table_cache[subgroup_name] = table_subgroup
subgroup_tables[subgroup_name] = {}
subgroup_table = subgroup_tables[subgroup_name]
current_item_rows = current_item_rows + math.ceil(current_items_in_table_count / items_per_row)
current_items_in_table_count = 0
else
subgroup_table = subgroup_tables[subgroup_name]
end
current_items_in_table_count = current_items_in_table_count + 1
local item_name = item_proto.name
local existing_product = existing_products[item_name]
local button_style = (existing_product) and "flib_slot_button_red" or "flib_slot_button_default"
local button_item = table_subgroup.add{type="sprite-button", sprite=item_proto.sprite, style=button_style,
tags={mod="fp", on_gui_click="select_picker_item", item_id=item_proto.id,
category_id=item_proto.category_id}, enabled=(existing_product == nil),
tooltip=item_proto.localised_name, mouse_button_filter={"left"}}
-- Figure out the translated name here so search doesn't have to repeat the work for every character
local translated_name = (translations) and translations[item_proto.type][item_name] or nil
translated_name = (translated_name) and translated_name:lower() or item_name
subgroup_table[{name=item_name, translated_name=translated_name}] = button_item
end
end
-- Catch up on addding the last item flow and groups row counts
current_item_rows = current_item_rows + math.ceil(current_items_in_table_count / items_per_row)
max_item_rows = math.max(current_item_rows, max_item_rows)
frame_filters.style.natural_height = max_item_rows * 40 + (2*12)
-- Select the previously selected item group if possible
local group_to_select, previous_selection = 1, ui_state.last_selected_picker_group
if previous_selection ~= nil and modal_elements.groups[previous_selection] ~= nil then
group_to_select = previous_selection
end
select_item_group(ui_state.modal_data, group_to_select)
end
-- ** PICKER DIALOG **
local function set_appropriate_focus(modal_data)
if modal_data.amount_defined_by == "amount" then
util.gui.select_all(modal_data.modal_elements["item_amount_textfield"])
else -- "belts"/"lanes"
util.gui.select_all(modal_data.modal_elements["belt_amount_textfield"])
end
end
-- Is only called when defined_by ~= "amount"
local function sync_amounts(modal_data)
local modal_elements = modal_data.modal_elements
local belt_amount = tonumber(modal_elements.belt_amount_textfield.text)
if belt_amount == nil then
modal_elements.item_amount_textfield.text = ""
else
local belt_proto = modal_data.belt_proto
local throughput = belt_proto.throughput * ((modal_data.lob == "belts") and 1 or 0.5)
local item_amount = belt_amount * throughput * modal_data.timescale
modal_elements.item_amount_textfield.text = util.format.number(item_amount, 6)
end
end
local function set_belt_proto(modal_data, belt_proto)
modal_data.belt_proto = belt_proto
local modal_elements = modal_data.modal_elements
modal_elements.item_amount_textfield.enabled = (belt_proto == nil)
modal_elements.belt_amount_textfield.enabled = (belt_proto ~= nil)
if belt_proto == nil then
modal_elements.belt_choice_button.elem_value = nil
modal_elements.belt_amount_textfield.text = ""
modal_data.amount_defined_by = "amount"
else
-- Might double set the choice button, but it doesn't matter
modal_elements.belt_choice_button.elem_value = belt_proto.name
modal_data.amount_defined_by = modal_data.lob
local item_amount = tonumber(modal_elements.item_amount_textfield.text)
if item_amount ~= nil then
local throughput = belt_proto.throughput * ((modal_data.lob == "belts") and 1 or 0.5)
local belt_amount = item_amount / throughput / modal_data.timescale
modal_elements.belt_amount_textfield.text = util.format.number(belt_amount, 6)
end
sync_amounts(modal_data)
end
end
local function set_item_proto(modal_data, item_proto)
local modal_elements = modal_data.modal_elements
modal_data.item_proto = item_proto
local item_choice_button = modal_elements.item_choice_button
item_choice_button.sprite = (item_proto) and item_proto.sprite or nil
item_choice_button.tooltip = (item_proto) and item_proto.tooltip or ""
-- Disable definition by belt for fluids
local is_fluid = item_proto and item_proto.type == "fluid"
modal_elements.belt_choice_button.enabled = (not is_fluid)
-- Clear the belt-related fields if needed
if is_fluid then set_belt_proto(modal_data, nil) end
end
local function update_dialog_submit_button(modal_elements)
local item_choice_button = modal_elements.item_choice_button
local item_amount_textfield = modal_elements.item_amount_textfield
local message = nil
if item_choice_button.sprite == "" then
message = {"fp.picker_issue_select_item"}
-- The item amount will be filled even if the item is defined_by ~= "amount"
elseif tonumber(item_amount_textfield.text) == nil then
message = {"fp.picker_issue_enter_amount"}
end
modal_dialog.set_submit_button_state(modal_elements, (message == nil), message)
end
local function add_item_pane(parent_flow, modal_data, item_category, item)
local function create_flow()
local flow = parent_flow.add{type="flow", direction="horizontal"}
flow.style.vertical_align = "center"
flow.style.horizontal_spacing = 8
flow.style.bottom_margin = 6
return flow
end
local modal_elements = modal_data.modal_elements
local defined_by = (item) and item.required_amount.defined_by or "amount"
modal_data.amount_defined_by = defined_by
local flow_amount = create_flow()
flow_amount.add{type="label", caption={"fp.pu_" .. item_category, 1}}
local item_choice_button = flow_amount.add{type="sprite-button", style="fp_sprite-button_inset_tiny"}
item_choice_button.style.right_margin = 12
modal_elements["item_choice_button"] = item_choice_button
flow_amount.add{type="label", caption={"fp.amount"}}
local item_amount = (item and defined_by == "amount") and tostring(item.required_amount.amount) or ""
local textfield_amount = flow_amount.add{type="textfield", text=item_amount,
tags={mod="fp", on_gui_text_changed="picker_item_amount"}}
util.gui.setup_numeric_textfield(textfield_amount, true, false)
textfield_amount.style.width = 90
modal_elements["item_amount_textfield"] = textfield_amount
local flow_belts = create_flow()
flow_belts.add{type="label", caption={"fp.amount_by", {"fp.pl_" .. modal_data.lob:sub(1, -2), 2}}}
local belt_amount = (item and defined_by ~= "amount") and tostring(item.required_amount.amount) or ""
local textfield_belts = flow_belts.add{type="textfield", text=belt_amount,
tags={mod="fp", on_gui_text_changed="picker_belt_amount"}}
util.gui.setup_numeric_textfield(textfield_belts, true, false)
textfield_belts.style.width = 85
textfield_belts.style.left_margin = 4
modal_elements["belt_amount_textfield"] = textfield_belts
flow_belts.add{type="label", caption="x"}
local belt_filter = {{filter="type", type="transport-belt"}, {filter="flag", flag="hidden",
invert=true, mode="and"}}
local choose_belt_button = flow_belts.add{type="choose-elem-button", elem_type="entity",
tags={mod="fp", on_gui_elem_changed="picker_choose_belt"}, elem_filters=belt_filter,
style="fp_sprite-button_inset_tiny"}
modal_elements["belt_choice_button"] = choose_belt_button
local item_proto = (item) and item.proto or nil
set_item_proto(modal_data, item_proto)
local belt_proto = (defined_by ~= "amount") and item.required_amount.belt_proto or nil
set_belt_proto(modal_data, belt_proto)
if (item) then set_appropriate_focus(modal_data)
else modal_elements.search_textfield.focus() end
update_dialog_submit_button(modal_elements)
end
local function handle_item_pick(player, tags, _)
local modal_data = util.globals.modal_data(player)
local item_proto = global.prototypes.items[tags.category_id].members[tags.item_id]
set_item_proto(modal_data, item_proto) -- no need for sync in this case
set_appropriate_focus(modal_data)
update_dialog_submit_button(modal_data.modal_elements)
end
local function handle_belt_pick(player, _, event)
local belt_name = event.element.elem_value
local belt_proto = prototyper.util.find_prototype("belts", belt_name, nil)
local modal_data = util.globals.modal_data(player)
set_belt_proto(modal_data, belt_proto) -- syncs amounts itself
set_appropriate_focus(modal_data)
update_dialog_submit_button(modal_data.modal_elements)
end
local function open_picker_dialog(player, modal_data)
-- Create a blank subfactory if requested
local settings = util.globals.settings(player)
modal_data.timescale = settings.default_timescale
modal_data.lob = settings.belts_or_lanes
local subfactory = util.globals.context(player).subfactory
if subfactory then
local class_name = modal_data.item_category:gsub("^%l", string.upper)
modal_data.item = Subfactory.get(subfactory, class_name, modal_data.item_id)
end
local dialog_flow = modal_data.modal_elements.dialog_flow
dialog_flow.style.vertical_spacing = 12
local item_content_frame = dialog_flow.add{type="frame", direction="vertical", style="inside_shallow_frame"}
item_content_frame.style.minimal_width = 325
item_content_frame.style.padding = {12, 12, 6, 12}
add_item_pane(item_content_frame, modal_data, modal_data.item_category, modal_data.item)
-- The item picker only needs to show when adding a new item
if modal_data.item == nil then
local picker_content_frame = dialog_flow.add{type="frame", direction="vertical", style="inside_deep_frame"}
add_item_picker(picker_content_frame, player)
end
end
local function close_picker_dialog(player, action)
local player_table = util.globals.player_table(player)
local ui_state = player_table.ui_state
local modal_data = ui_state.modal_data
local subfactory = ui_state.context.subfactory
if action == "submit" then
local defined_by = modal_data.amount_defined_by
local relevant_textfield_name = ((defined_by == "amount") and "item" or "belt") .. "_amount_textfield"
local relevant_amount = tonumber(modal_data.modal_elements[relevant_textfield_name].text)
local req_amount = {defined_by=defined_by, amount=relevant_amount, belt_proto=modal_data.belt_proto}
local refresh_scope = "subfactory"
if modal_data.item ~= nil then -- ie. this is an edit
modal_data.item.required_amount = req_amount
else
local class_name = modal_data.item_category:gsub("^%l", string.upper)
local item_proto = modal_data.item_proto
local top_level_item = Item.init(item_proto, class_name, 0, req_amount)
if modal_data.create_subfactory then -- if this flag is set, create a subfactory to put the item into
local translations = player_table.translation_tables
local translated_name = (translations) and translations[item_proto.type][item_proto.name] or ""
local icon = (not player_table.preferences.attach_subfactory_products)
and "[img=" .. top_level_item.proto.sprite .. "] " or ""
subfactory = subfactory_list.add_subfactory(player, (icon .. translated_name))
end
Subfactory.add(subfactory, top_level_item)
refresh_scope = "all" -- need to refresh subfactory list too
end
solver.update(player, subfactory)
util.raise.refresh(player, refresh_scope, nil)
elseif action == "delete" then
Subfactory.remove(subfactory, modal_data.item)
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
end
-- Remember selected group so it can be re-applied when the dialog is re-opened
ui_state.last_selected_picker_group = modal_data.selected_group_id
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "select_picker_item_group",
handler = (function(player, tags, _)
local modal_data = util.globals.modal_data(player)
select_item_group(modal_data, tags.group_id)
end)
},
{
name = "select_picker_item",
handler = handle_item_pick
}
},
on_gui_elem_changed = {
{
name = "picker_choose_belt",
handler = handle_belt_pick
}
},
on_gui_text_changed = {
{
name = "picker_item_amount",
handler = (function(player, _, _)
local modal_data = util.globals.modal_data(player)
update_dialog_submit_button(modal_data.modal_elements)
end)
},
{
name = "picker_belt_amount",
handler = (function(player, _, _)
local modal_data = util.globals.modal_data(player)
sync_amounts(modal_data) -- defined_by ~= "amount"
update_dialog_submit_button(modal_data.modal_elements)
end)
}
}
}
listeners.dialog = {
dialog = "picker",
metadata = (function(modal_data)
local action = (modal_data.item_id) and {"fp.edit"} or {"fp.add"}
return {
caption = {"", action, " ", {"fp.pl_" .. modal_data.item_category, 1}},
search_handler_name = (not modal_data.item_id) and "search_picker_items" or nil,
show_submit_button = true,
show_delete_button = (modal_data.item_id ~= nil)
}
end),
open = open_picker_dialog,
close = close_picker_dialog
}
listeners.global = {
search_picker_items = search_picker_items
}
return { listeners }

View File

@@ -0,0 +1,369 @@
-- ** LOCAL UTIL **
local function set_tool_button_state(button, dialog_type, enabled)
button.enabled = enabled
button.sprite = (enabled) and ("utility/" .. dialog_type) or ("fp_sprite_" .. dialog_type .. "_light")
end
local function set_dialog_submit_button(modal_elements, enabled, action_to_take)
local message = (not enabled) and {"fp.importer_issue_" .. action_to_take} or nil
modal_dialog.set_submit_button_state(modal_elements, enabled, message)
end
-- Sets the state of either the export_subfactories- or dialog_submit-button
local function set_relevant_submit_button(modal_elements, dialog_type, enabled)
if dialog_type == "export" then
set_tool_button_state(modal_elements.export_button, dialog_type, enabled)
else -- dialog_type == "import"
set_dialog_submit_button(modal_elements, enabled, "select_subfactory")
end
end
-- Sets the slave checkboxes after the master one has been clicked
local function set_all_checkboxes(player, checkbox_state)
local ui_state = util.globals.ui_state(player)
local modal_elements = ui_state.modal_data.modal_elements
for _, checkbox in pairs(modal_elements.subfactory_checkboxes) do
if checkbox.enabled then checkbox.state = checkbox_state end
end
set_relevant_submit_button(modal_elements, ui_state.modal_dialog_type, checkbox_state)
end
-- Sets the master checkbox to the appropriate state after a slave one is changed
local function adjust_after_checkbox_click(player, _, _)
local ui_state = util.globals.ui_state(player)
local modal_elements = ui_state.modal_data.modal_elements
local checked_element_count, unchecked_element_count = 0, 0
for _, checkbox in pairs(modal_elements.subfactory_checkboxes) do
if checkbox.state == true then checked_element_count = checked_element_count + 1
elseif checkbox.enabled then unchecked_element_count = unchecked_element_count + 1 end
end
modal_elements.master_checkbox.state = (unchecked_element_count == 0)
set_relevant_submit_button(modal_elements, ui_state.modal_dialog_type, (checked_element_count > 0))
end
-- Adds a flow containing a textfield and a button
local function add_textfield_and_button(modal_elements, dialog_type, button_first, button_enabled)
local flow = modal_elements.content_frame.add{type="flow", direction="horizontal"}
flow.style.vertical_align = "center"
local function add_button()
local button = flow.add{type="sprite-button", tags={mod="fp", on_gui_click=(dialog_type .. "_subfactories")},
style="flib_tool_button_light_green", tooltip={"fp." .. dialog_type .. "_button_tooltip"},
mouse_button_filter={"left"}}
set_tool_button_state(button, dialog_type, button_enabled)
modal_elements[dialog_type .. "_button"] = button
end
local function add_textfield()
local tags = (dialog_type == "import")
and {mod="fp", on_gui_text_changed="import_string", on_gui_confirmed="import_string"} or nil
local textfield = flow.add{type="textfield", tags=tags}
util.gui.setup_textfield(textfield)
textfield.style.width = 0 -- needs to be set to 0 so stretching works
textfield.style.minimal_width = 280
textfield.style.horizontally_stretchable = true
if button_first then textfield.style.left_margin = 6
else textfield.style.right_margin = 6 end
modal_elements[dialog_type .. "_textfield"] = textfield
end
if button_first then add_button(); add_textfield()
else add_textfield(); add_button() end
end
-- Initializes the subfactories table by adding it and its header
local function setup_subfactories_table(modal_elements, add_location)
modal_elements.subfactory_checkboxes = {} -- setup for later use in add_to_subfactories_table
local scroll_pane = modal_elements.content_frame.add{type="scroll-pane", style="flib_naked_scroll_pane_no_padding"}
scroll_pane.style.maximal_height = 450 -- I hate that I have to set this, seemingly
modal_elements.subfactories_scroll_pane = scroll_pane
local frame_subfactories = scroll_pane.add{type="frame", style="deep_frame_in_shallow_frame"}
frame_subfactories.style.padding = {-2, 2, 3, 2}
local table_columns = {
[2] = {caption={"fp.pu_subfactory", 2}, alignment="left", margin={6, 130, 6, 4}},
[3] = {caption={"fp.validity"}}
}
if add_location then table_columns[4] = {caption={"fp.location"}} end
local table_subfactories = frame_subfactories.add{type="table", style="mods_table",
column_count=(table_size(table_columns) + 1)}
modal_elements.subfactories_table = table_subfactories
-- Add master checkbox in any case
local checkbox_master = table_subfactories.add{type="checkbox", state=false,
tags={mod="fp", on_gui_checked_state_changed="toggle_porter_master_checkbox"}}
modal_elements.master_checkbox = checkbox_master
for column_nr, table_column in pairs(table_columns) do
table_subfactories.style.column_alignments[column_nr] = table_column.alignment or "center"
local label_column = table_subfactories.add{type="label", caption=table_column.caption, style="heading_3_label"}
label_column.style.margin = table_column.margin or {0, 4}
end
end
-- Adds a row to the subfactories table
local function add_to_subfactories_table(modal_elements, subfactory, location_name, enable_checkbox, attach_products)
local table_subfactories = modal_elements.subfactories_table
local checkbox = table_subfactories.add{type="checkbox", state=false, enabled=(enable_checkbox or subfactory.valid),
tags={mod="fp", on_gui_checked_state_changed="toggle_porter_checkbox"}}
local label = table_subfactories.add{type="label", caption=Subfactory.tostring(subfactory, attach_products, true)}
label.style.maximal_width = 350
label.style.right_margin = 4
local validity_caption = (subfactory.valid) and {"fp.valid"} or {"fp.error_message", {"fp.invalid"}}
table_subfactories.add{type="label", caption=validity_caption}
if location_name then table_subfactories.add{type="label", caption={"fp." .. location_name}} end
local identifier = (location_name or "tmp") .. "_" .. subfactory.id
modal_elements.subfactory_checkboxes[identifier] = checkbox
end
-- Tries importing the given string, showing the resulting subfactories-table, if possible
local function import_subfactories(player, _, _)
local player_table = util.globals.player_table(player)
local attach_subfactory_products = player_table.preferences.attach_subfactory_products
local modal_data = player_table.ui_state.modal_data
local modal_elements = modal_data.modal_elements
local content_frame = modal_elements.content_frame
local textfield_export_string = modal_elements.import_textfield
-- The imported subfactories will be temporarily contained in a factory object
local import_factory, error = util.porter.process_export_string(textfield_export_string.text)
local function add_info_label(caption)
local label_info = content_frame.add{type="label", caption=caption}
label_info.style.single_line = false
label_info.style.bottom_margin = 4
label_info.style.width = 330
modal_elements.info_label = label_info
end
if not modal_elements.porter_line then
local line = content_frame.add{type="line", direction="horizontal"}
line.style.margin = {6, 0, 6, 0}
modal_elements.porter_line = line
end
if modal_elements.info_label then modal_elements.info_label.destroy() end
if modal_elements.subfactories_scroll_pane then modal_elements.subfactories_scroll_pane.destroy() end
if error ~= nil then
add_info_label({"fp.error_message", {"fp.importer_" .. error}})
util.gui.select_all(textfield_export_string)
else
---@cast import_factory -nil
add_info_label({"fp.import_instruction_2"})
setup_subfactories_table(modal_elements, false)
modal_data.subfactories = {}
local any_invalid_subfactories = true
for _, subfactory in ipairs(Factory.get_in_order(import_factory, "Subfactory")) do
add_to_subfactories_table(modal_elements, subfactory, nil, true, attach_subfactory_products)
modal_data.subfactories["tmp_" .. subfactory.id] = subfactory
any_invalid_subfactories = any_invalid_subfactories or (not subfactory.valid)
end
if any_invalid_subfactories then
modal_data.export_modset = import_factory.export_modset
local diff_tooltip = util.porter.format_modset_diff(import_factory.export_modset)
if diff_tooltip ~= "" then
modal_elements.info_label.caption = {"fp.info_label", {"fp.import_instruction_2"}}
modal_elements.info_label.tooltip = diff_tooltip
end
end
modal_elements.master_checkbox.state = true
set_all_checkboxes(player, true)
end
set_dialog_submit_button(modal_elements, (error == nil), "import_string")
modal_elements.modal_frame.force_auto_center()
end
-- Exports the currently selected subfactories and puts the resulting string into the textbox
local function export_subfactories(player, _, _)
local modal_data = util.globals.modal_data(player)
local modal_elements = modal_data.modal_elements
local subfactories_to_export = {}
for subfactory_identifier, checkbox in pairs(modal_elements.subfactory_checkboxes) do
if checkbox.state == true then
local subfactory = modal_data.subfactories[subfactory_identifier]
table.insert(subfactories_to_export, subfactory)
end
end
local export_string = util.porter.generate_export_string(subfactories_to_export)
modal_elements.export_textfield.text = export_string
util.gui.select_all(modal_elements.export_textfield)
end
local function open_import_dialog(_, modal_data)
local modal_elements = modal_data.modal_elements
set_dialog_submit_button(modal_elements, false, "import_string")
add_textfield_and_button(modal_elements, "import", false, false)
util.gui.select_all(modal_elements.import_textfield)
end
-- Imports the selected subfactories into the player's main factory
local function close_import_dialog(player, action)
if action == "submit" then
local ui_state = util.globals.ui_state(player)
local modal_data = ui_state.modal_data
local factory = ui_state.context.factory
local first_subfactory = nil
for subfactory_identifier, checkbox in pairs(modal_data.modal_elements.subfactory_checkboxes) do
if checkbox.state == true then
local subfactory = modal_data.subfactories[subfactory_identifier]
local imported_subfactory = Factory.add(factory, subfactory)
if not imported_subfactory.valid then -- carry over modset if need be
imported_subfactory.last_valid_modset = modal_data.export_modset
end
solver.update(player, imported_subfactory)
first_subfactory = first_subfactory or imported_subfactory
end
end
util.context.set_subfactory(player, first_subfactory)
util.raise.refresh(player, "all", nil)
end
end
-- ** EVENTS **
local import_listeners = {}
import_listeners.gui = {
on_gui_click = {
{
name = "import_subfactories",
timeout = 20,
handler = import_subfactories
}
},
on_gui_text_changed = {
{
name = "import_string",
handler = (function(player, _, event)
local button_import = util.globals.modal_elements(player).import_button
set_tool_button_state(button_import, "import", (string.len(event.element.text) > 0))
end)
}
},
on_gui_confirmed = {
{
name = "import_string",
handler = (function(player, _, event)
if event.element.text ~= "" then import_subfactories(player) end
end)
}
}
}
import_listeners.dialog = {
dialog = "import",
metadata = (function(_) return {
caption = {"", {"fp.import"}, " ", {"fp.pl_subfactory", 1}},
subheader_text = {"fp.import_instruction_1"},
create_content_frame = true,
disable_scroll_pane = true,
show_submit_button = true
} end),
open = open_import_dialog,
close = close_import_dialog
}
local function open_export_dialog(player, modal_data)
local player_table = util.globals.player_table(player)
local attach_subfactory_products = player_table.preferences.attach_subfactory_products
local modal_elements = modal_data.modal_elements
setup_subfactories_table(modal_elements, true)
modal_data.subfactories = {}
local valid_subfactory_found = false
for _, factory_name in ipairs{"factory", "archive"} do
for _, subfactory in ipairs(Factory.get_in_order(player_table[factory_name], "Subfactory")) do
add_to_subfactories_table(modal_elements, subfactory, factory_name, false, attach_subfactory_products)
modal_data.subfactories[factory_name .. "_" .. subfactory.id] = subfactory
valid_subfactory_found = valid_subfactory_found or subfactory.valid
end
end
modal_elements.master_checkbox.enabled = valid_subfactory_found
add_textfield_and_button(modal_elements, "export", true, false)
modal_elements.export_textfield.parent.style.top_margin = 6
end
-- ** EVENTS **
local export_listeners = {}
export_listeners.gui = {
on_gui_click = {
{
name = "export_subfactories",
timeout = 20,
handler = export_subfactories
}
}
}
export_listeners.dialog = {
dialog = "export",
metadata = (function(_) return {
caption = {"", {"fp.export"}, " ", {"fp.pl_subfactory", 1}},
subheader_text = {"fp.info_label", {"fp.export_instruction"}},
subheader_tooltip = {"fp.export_instruction_tt"},
create_content_frame = true,
disable_scroll_pane = true
} end),
open = open_export_dialog
}
-- ** SHARED **
local porter_listeners = {}
porter_listeners.gui = {
on_gui_checked_state_changed = {
{
name = "toggle_porter_master_checkbox",
handler = (function(player, _, event)
set_all_checkboxes(player, event.element.state)
end)
},
{
name = "toggle_porter_checkbox",
handler = adjust_after_checkbox_click
}
}
}
return { import_listeners, export_listeners, porter_listeners }

View File

@@ -0,0 +1,332 @@
-- ** LOCAL UTIL **
local function add_preference_box(content_frame, type)
local bordered_frame = content_frame.add{type="frame", direction="vertical", style="fp_frame_bordered_stretch"}
local caption = {"fp.info_label", {"fp.preference_".. type .. "_title"}}
local tooltip = {"fp.preference_".. type .. "_title_tt"}
bordered_frame.add{type="label", caption=caption, tooltip=tooltip, style="caption_label"}
return bordered_frame
end
local function refresh_defaults_table(player, modal_elements, type, category_id)
local table_prototypes, prototypes
if not category_id then
table_prototypes = modal_elements[type]
prototypes = global.prototypes[type]
else
table_prototypes = modal_elements[type][category_id]
prototypes = global.prototypes[type][category_id].members
end
table_prototypes.clear()
local default_proto = prototyper.defaults.get(player, type, category_id)
for prototype_id, prototype in ipairs(prototypes) do
local selected = (default_proto.id == prototype_id)
local style = (selected) and "flib_slot_button_green_small" or "flib_slot_button_default_small"
local first_line = (selected) and {"fp.tt_title_with_note", prototype.localised_name, {"fp.selected"}}
or {"fp.tt_title", prototype.localised_name}
local tooltip = {"", first_line, "\n", prototyper.util.get_attributes(prototype)}
table_prototypes.add{type="sprite-button", sprite=prototype.sprite, tooltip=tooltip, style=style,
tags={mod="fp", on_gui_click="select_preference_default", type=type, prototype_id=prototype_id,
category_id=category_id}, mouse_button_filter={"left"}}
end
end
local preference_structures = {}
function preference_structures.checkboxes(preferences, content_frame, type, preference_names)
local preference_box = add_preference_box(content_frame, type)
local flow_checkboxes = preference_box.add{type="flow", direction="vertical"}
for _, pref_name in ipairs(preference_names) do
local identifier = type .. "_" .. pref_name
local caption = {"fp.info_label", {"fp.preference_" .. identifier}}
local tooltip ={"fp.preference_" .. identifier .. "_tt"}
flow_checkboxes.add{type="checkbox", state=preferences[pref_name], caption=caption, tooltip=tooltip,
tags={mod="fp", on_gui_checked_state_changed="toggle_preference", type=type, name=pref_name}}
end
end
function preference_structures.mb_defaults(preferences, content_frame)
local mb_defaults = preferences.mb_defaults
local preference_box = add_preference_box(content_frame, "mb_defaults")
local function add_mb_default_button(parent_flow, type)
local flow = parent_flow.add{type="flow", direction="horizontal"}
flow.style.vertical_align = "center"
flow.style.horizontal_spacing = 8
flow.add{type="label", caption={"fp.info_label", {"fp.preference_mb_default_" .. type}},
tooltip={"fp.preference_mb_default_" .. type .. "_tt"}}
local item = (mb_defaults[type] ~= nil) and mb_defaults[type].name or nil
flow.add{type="choose-elem-button", elem_type="item", item=item, style="fp_sprite-button_inset_tiny",
elem_filters={{filter="type", type="module"}, {filter="flag", flag="hidden", mode="and", invert=true}},
tags={mod="fp", on_gui_elem_changed="change_mb_default", type=type}}
end
local table_mb_defaults = preference_box.add{type="table", column_count=3}
table_mb_defaults.style.horizontal_spacing = 18
-- Table alignment is so stupid
table_mb_defaults.style.column_alignments[1] = "left"
table_mb_defaults.style.column_alignments[2] = "right"
table_mb_defaults.style.column_alignments[3] = "right"
table_mb_defaults.add{type="label", caption={"", {"fp.pu_machine", 1}, ":"}}
add_mb_default_button(table_mb_defaults, "machine")
add_mb_default_button(table_mb_defaults, "machine_secondary")
table_mb_defaults.add{type="label", caption={"", {"fp.pu_beacon", 1}, ":"}}
add_mb_default_button(table_mb_defaults, "beacon")
local beacon_amount_flow = table_mb_defaults.add{type="flow", direction="horizontal"}
beacon_amount_flow.style.vertical_align = "center"
beacon_amount_flow.style.horizontal_spacing = 8
beacon_amount_flow.add{type="label", caption={"fp.info_label", {"fp.preference_mb_default_beacon_amount"}},
tooltip={"fp.preference_mb_default_beacon_amount_tt"}}
local beacon_amount = (BEACON_OVERLOAD_ACTIVE) and "1" or tostring(mb_defaults.beacon_count or "")
local textfield_amount = beacon_amount_flow.add{type="textfield", text=beacon_amount,
enabled=(not BEACON_OVERLOAD_ACTIVE), tags={mod="fp", on_gui_text_changed="mb_default_beacon_amount"}}
util.gui.setup_numeric_textfield(textfield_amount, true, false)
textfield_amount.style.width = 42
end
function preference_structures.prototypes(player, content_frame, modal_elements, type)
local preference_box = add_preference_box(content_frame, ("default_" .. type))
local table_prototypes = preference_box.add{type="table", column_count=3}
table_prototypes.style.horizontal_spacing = 20
table_prototypes.style.vertical_spacing = 8
table_prototypes.style.top_margin = 4
local function add_defaults_table(column_count, category_id)
local frame = table_prototypes.add{type="frame", direction="horizontal", style="fp_frame_deep_slots_small"}
local table = frame.add{type="table", column_count=column_count, style="filter_slot_table"}
if category_id then
modal_elements[type] = modal_elements[type] or {}
modal_elements[type][category_id] = table
else
modal_elements[type] = table
end
end
if not prototyper.data_types[type] then
local prototypes = global.prototypes[type]
if #prototypes < 2 then preference_box.visible = false; return end
table_prototypes.add{type="empty-widget", style="flib_horizontal_pusher"}
add_defaults_table(8, nil)
refresh_defaults_table(player, modal_elements, type, nil)
else
local categories = global.prototypes[type]
if not next(categories) then preference_box.visible = false; return end
local any_category_visible = false
for category_id, category in ipairs(categories) do
local prototypes = category.members
if #prototypes > 1 then
any_category_visible = true
table_prototypes.add{type="label", caption=("'" .. category.name .. "'")}
table_prototypes.add{type="empty-widget", style="flib_horizontal_pusher"}
add_defaults_table(8, category_id)
refresh_defaults_table(player, modal_elements, type, category_id)
end
end
if not any_category_visible then preference_box.visible = false end
end
end
local function handle_checkbox_preference_change(player, tags, event)
local preference_name = tags.name
util.globals.preferences(player)[preference_name] = event.element.state
local refresh = util.globals.modal_data(player).refresh
if tags.type == "production" or preference_name == "round_button_numbers"
or preference_name == "show_floor_items" or preference_name == "fold_out_subfloors" then
refresh.production = true
end
if preference_name == "ingredient_satisfaction" then
-- Only recalculate if the satisfaction data will actually be shown now
refresh.update_ingredient_satisfaction = (event.element.state)
refresh.production = true -- always refresh production
elseif preference_name == "attach_subfactory_products" then
refresh.subfactory_list = true
end
end
local function handle_mb_default_change(player, tags, event)
local mb_defaults = util.globals.preferences(player).mb_defaults
local module_name = event.element.elem_value
mb_defaults[tags.type] = (module_name ~= nil) and MODULE_NAME_MAP[module_name] or nil
end
local function handle_default_prototype_change(player, tags, event)
local type = tags.type
local category_id = tags.category_id
local modal_data = util.globals.modal_data(player)
if type == "belts" then modal_data.refresh.view_state = true end
if type == "wagons" then modal_data.refresh.production = true end
prototyper.defaults.set(player, type, tags.prototype_id, category_id)
refresh_defaults_table(player, modal_data.modal_elements, type, category_id)
-- If this was an shift-click, set this prototype on every category that also has it
if event.shift and type == "machines" then
local new_default_prototype = prototyper.defaults.get(player, type, category_id)
for _, secondary_category in pairs(PROTOTYPE_MAPS[type]) do
if table_size(secondary_category.members) > 1 then -- don't attempt to change categories with only one machine
local secondary_prototype = secondary_category.members[new_default_prototype.name]
if secondary_prototype ~= nil then
prototyper.defaults.set(player, type, secondary_prototype.id, secondary_category.id)
refresh_defaults_table(player, modal_data.modal_elements, type, secondary_category.id)
end
end
end
end
end
local function open_preferences_dialog(player, modal_data)
local preferences = util.globals.preferences(player)
local modal_elements = modal_data.modal_elements
modal_data.refresh = {}
local flow_content = modal_elements.dialog_flow.add{type="flow", direction="horizontal"}
flow_content.style.horizontal_spacing = 12
local function add_content_frame()
local content_frame = flow_content.add{type="frame", direction="vertical", style="inside_shallow_frame"}
content_frame.style.vertically_stretchable = true
return content_frame.add{type="scroll-pane", style="flib_naked_scroll_pane"}
end
local left_content_frame = add_content_frame()
local info_frame = left_content_frame.add{type="frame", direction="vertical", style="fp_frame_bordered_stretch"}
local label_preferences_info = info_frame.add{type="label", caption={"fp.preferences_info"}}
label_preferences_info.style.single_line = false
label_preferences_info.style.width = 335
local support_frame = left_content_frame.add{type="frame", direction="vertical", style="fp_frame_bordered_stretch"}
support_frame.add{type="label", caption={"fp.preferences_support"}}
local general_preference_names = {"attach_subfactory_products", "show_floor_items", "fold_out_subfloors",
"ingredient_satisfaction", "round_button_numbers", "ignore_barreling_recipes", "ignore_recycling_recipes"}
preference_structures.checkboxes(preferences, left_content_frame, "general", general_preference_names)
local production_preference_names = {"done_column", "pollution_column", "line_comment_column"}
preference_structures.checkboxes(preferences, left_content_frame, "production", production_preference_names)
preference_structures.mb_defaults(preferences, left_content_frame)
local right_content_frame = add_content_frame()
preference_structures.prototypes(player, right_content_frame, modal_elements, "belts")
preference_structures.prototypes(player, right_content_frame, modal_elements, "beacons")
preference_structures.prototypes(player, right_content_frame, modal_elements, "wagons")
preference_structures.prototypes(player, right_content_frame, modal_elements, "fuels")
preference_structures.prototypes(player, right_content_frame, modal_elements, "machines")
end
local function close_preferences_dialog(player, _)
-- We refresh all these things only when closing to avoid duplicate refreshes
local refresh = util.globals.modal_data(player).refresh
if refresh.update_ingredient_satisfaction then
local player_table = util.globals.player_table(player)
Factory.update_ingredient_satisfactions(player_table.factory)
Factory.update_ingredient_satisfactions(player_table.archive)
end
if refresh.subfactory_list then
util.raise.refresh(player, "subfactory_list", nil)
end
local context_to_refresh = nil -- don't refresh by default
-- The order of these matters, they go from smallest context to biggest
if refresh.production then
context_to_refresh = "production"
end
if refresh.view_state then
-- Rebuilding state requires every button that shows item amounts to refresh
view_state.rebuild_state(player)
context_to_refresh = "production"
end
if refresh.calculations then
local context = util.globals.context(player)
solver.update(player, context.subfactory)
context_to_refresh = "subfactory"
end
if context_to_refresh then
util.raise.refresh(player, context_to_refresh, nil)
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "select_preference_default",
handler = handle_default_prototype_change
}
},
on_gui_text_changed = {
{
name = "mb_default_beacon_amount",
handler = (function(player, _, event)
local mb_defaults = util.globals.preferences(player).mb_defaults
mb_defaults.beacon_count = tonumber(event.element.text)
end)
}
},
on_gui_checked_state_changed = {
{
name = "toggle_preference",
handler = handle_checkbox_preference_change
}
},
on_gui_elem_changed = {
{
name = "change_mb_default",
handler = handle_mb_default_change
}
}
}
listeners.dialog = {
dialog = "preferences",
metadata = (function(_) return {
caption = {"fp.preferences"},
create_content_frame = false
} end),
open = open_preferences_dialog,
close = close_preferences_dialog
}
return { listeners }

View File

@@ -0,0 +1,338 @@
-- ** LOCAL UTIL **
-- Serves the dual-purpose of determining the appropriate settings for the recipe picker filter and, if there
-- is only one that matches, to return a recipe name that can be added directly without the modal dialog
local function run_preliminary_checks(player, modal_data)
local force_recipes, force_technologies = player.force.recipes, player.force.technologies
local preferences = util.globals.preferences(player)
local relevant_recipes = {}
local user_disabled_recipe = false
local counts = {disabled = 0, hidden = 0, disabled_hidden = 0}
local map = RECIPE_MAPS[modal_data.production_type][modal_data.category_id][modal_data.product_id]
if map ~= nil then -- this being nil means that the item has no recipes
for recipe_id, _ in pairs(map) do
local recipe = global.prototypes.recipes[recipe_id]
local force_recipe = force_recipes[recipe.name]
if recipe.custom then -- Add custom recipes by default
table.insert(relevant_recipes, {proto=recipe, enabled=true})
-- These are always enabled and non-hidden, so no need to tally them
-- They can also not be disabled by user preference
elseif force_recipe ~= nil then -- only add recipes that exist on the current force
local user_disabled = (preferences.ignore_barreling_recipes and recipe.barreling)
or (preferences.ignore_recycling_recipes and recipe.recycling)
user_disabled_recipe = user_disabled_recipe or user_disabled
if not user_disabled then -- only add recipes that are not disabled by the user
local recipe_enabled, recipe_hidden = force_recipe.enabled, recipe.hidden
local recipe_should_show = recipe.enabled_from_the_start or recipe_enabled
-- If the recipe is not enabled, it has to be made sure that there is at
-- least one enabled technology that could potentially enable it
if not recipe_should_show and recipe.enabling_technologies ~= nil then
for _, technology_name in pairs(recipe.enabling_technologies) do
local force_tech = force_technologies[technology_name]
if force_tech and (force_tech.enabled or force_tech.visible_when_disabled) then
recipe_should_show = true
break
end
end
end
if recipe_should_show then
table.insert(relevant_recipes, {proto=recipe, enabled=recipe_enabled})
if not recipe_enabled and recipe_hidden then counts.disabled_hidden = counts.disabled_hidden + 1
elseif not recipe_enabled then counts.disabled = counts.disabled + 1
elseif recipe_hidden then counts.hidden = counts.hidden + 1 end
end
end
end
end
end
-- Set filters to try and show at least one recipe, should one exist, incorporating user preferences
local filters = {}
local user_prefs = preferences.recipe_filters
local relevant_recipes_count = #relevant_recipes
if relevant_recipes_count - counts.disabled - counts.hidden - counts.disabled_hidden > 0 then
filters.disabled = user_prefs.disabled or false
filters.hidden = user_prefs.hidden or false
elseif relevant_recipes_count - counts.hidden - counts.disabled_hidden > 0 then
filters.disabled = true
filters.hidden = user_prefs.hidden or false
else
filters.disabled = true
filters.hidden = true
end
-- Return result, format: return recipe, error-message, filters
if relevant_recipes_count == 0 then
local error = (user_disabled_recipe) and {"fp.error_no_enabled_recipe"} or {"fp.error_no_relevant_recipe"}
return nil, error, nil
elseif relevant_recipes_count == 1 then
local chosen_recipe = relevant_recipes[1]
return chosen_recipe.proto.id, nil, nil
else -- 2+ relevant recipes
return relevant_recipes, nil, filters
end
end
-- Tries to add the given recipe to the current floor, then exiting the modal dialog
local function attempt_adding_line(player, recipe_id)
local ui_state = util.globals.ui_state(player)
local modal_data = ui_state.modal_data
local recipe = Recipe.init_by_id(recipe_id, modal_data.production_type)
local line = Line.init(recipe)
-- If finding a machine fails, this line is invalid
if Line.change_machine_to_default(line, player) == false then
util.messages.raise(player, "error", {"fp.error_no_compatible_machine"}, 1)
else
local floor = Subfactory.get(ui_state.context.subfactory, "Floor", modal_data.floor_id)
-- If add_after_position is given, insert it below that one, add it to the end otherwise
if modal_data.add_after_position == nil then
Floor.add(floor, line)
else
Floor.insert_at(floor, (modal_data.add_after_position + 1), line)
end
local message = nil
if not (recipe.proto.custom or player.force.recipes[recipe.proto.name].enabled) then
message = {text={"fp.warning_recipe_disabled"}, category="warning"}
end
local defaults_message = Line.apply_mb_defaults(line, player)
if not message then message = defaults_message end -- a bit silly
solver.update(player, ui_state.context.subfactory)
util.raise.refresh(player, "subfactory", nil)
if message ~= nil then util.messages.raise(player, message.category, message.text, 1) end
end
end
local function create_filter_box(modal_data)
local bordered_frame = modal_data.modal_elements.content_frame.add{type="frame", style="fp_frame_bordered_stretch"}
local table_filters = bordered_frame.add{type="table", column_count=2}
table_filters.style.horizontal_spacing = 16
local label_filters = table_filters.add{type="label", caption={"fp.show"}}
label_filters.style.top_margin = 2
label_filters.style.left_margin = 4
local flow_filter_switches = table_filters.add{type="flow", direction="vertical"}
util.gui.switch.add_on_off(flow_filter_switches, "toggle_recipe_filter", {filter_name="disabled"},
modal_data.filters.disabled, {"fp.unresearched_recipes"}, nil, false)
util.gui.switch.add_on_off(flow_filter_switches, "toggle_recipe_filter", {filter_name="hidden"},
modal_data.filters.hidden, {"fp.hidden_recipes"}, nil, false)
end
local function create_recipe_group_box(modal_data, relevant_group, translations)
local modal_elements = modal_data.modal_elements
local bordered_frame = modal_elements.content_frame.add{type="frame", style="fp_frame_bordered_stretch"}
bordered_frame.style.padding = 8
local next_index = #modal_elements.groups + 1
modal_elements.groups[next_index] = {name=relevant_group.proto.name, frame=bordered_frame, recipe_buttons={}}
local recipe_buttons = modal_elements.groups[next_index].recipe_buttons
local flow_group = bordered_frame.add{type="flow", direction="horizontal"}
flow_group.style.vertical_align = "center"
local group_sprite = flow_group.add{type="sprite-button", sprite=("item-group/" .. relevant_group.proto.name),
tooltip=relevant_group.proto.localised_name, style="transparent_slot"}
group_sprite.style.size = 64
group_sprite.style.right_margin = 12
local frame_recipes = flow_group.add{type="frame", direction="horizontal", style="fp_frame_deep_slots_small"}
local table_recipes = frame_recipes.add{type="table", column_count=MAGIC_NUMBERS.recipes_per_row,
style="filter_slot_table"}
for _, recipe in pairs(relevant_group.recipes) do
local recipe_proto = recipe.proto
local recipe_name = recipe_proto.name
local style = "flib_slot_button_green_small"
if not recipe.enabled then style = "flib_slot_button_yellow_small"
elseif recipe_proto.hidden then style = "flib_slot_button_default_small" end
local button_tags = {mod="fp", on_gui_click="pick_recipe", recipe_proto_id=recipe_proto.id}
local button_recipe = nil
if recipe_proto.custom then -- can't use choose-elem-buttons for custom recipes
button_recipe = table_recipes.add{type="sprite-button", tags=button_tags, style=style,
sprite=recipe_proto.sprite, tooltip=recipe_proto.tooltip, mouse_button_filter={"left"}}
else
button_recipe = table_recipes.add{type="choose-elem-button", elem_type="recipe", tags=button_tags,
style=style, recipe=recipe_name, mouse_button_filter={"left"}}
button_recipe.locked = true
end
-- Figure out the translated name here so search doesn't have to repeat the work for every character
local translated_name = (translations) and translations["recipe"][recipe_name] or nil
translated_name = (translated_name) and translated_name:lower() or recipe_name
recipe_buttons[{name=recipe_name, translated_name=translated_name, hidden=recipe_proto.hidden}] = button_recipe
end
end
local function create_dialog_structure(modal_data, translations)
local modal_elements = modal_data.modal_elements
local content_frame = modal_elements.content_frame
content_frame.style.width = 380
create_filter_box(modal_data)
local label_warning = content_frame.add{type="label", caption={"fp.error_message", {"fp.no_recipe_found"}}}
label_warning.style.font = "heading-2"
label_warning.style.margin = {8, 0, 0, 8}
modal_elements.warning_label = label_warning
modal_elements.groups = {}
for _, group in ipairs(ORDERED_RECIPE_GROUPS) do
local relevant_group = modal_data.recipe_groups[group.name]
-- Only actually create this group if it contains any relevant recipes
if relevant_group ~= nil then create_recipe_group_box(modal_data, relevant_group, translations) end
end
end
local function apply_recipe_filter(player, search_term)
local modal_data = util.globals.modal_data(player)
local disabled, hidden = modal_data.filters.disabled, modal_data.filters.hidden
local any_recipe_visible, desired_scroll_pane_height = false, 64+24
for _, group in ipairs(modal_data.modal_elements.groups) do
local group_data = modal_data.recipe_groups[group.name]
local any_group_recipe_visible = false
for recipe_data, button in pairs(group.recipe_buttons) do
local recipe_name = recipe_data.name
local recipe_enabled = group_data.recipes[recipe_name].enabled
-- Can only get to this if translations are complete, as the textfield is disabled otherwise
local found = (search_term == recipe_name) or string.find(recipe_data.translated_name, search_term, 1, true)
local visible = found and (disabled or recipe_enabled) and (hidden or not recipe_data.hidden)
button.visible = visible
any_group_recipe_visible = any_group_recipe_visible or visible
end
group.frame.visible = any_group_recipe_visible
any_recipe_visible = any_recipe_visible or any_group_recipe_visible
local button_table_height = math.ceil(table_size(group.recipe_buttons) / MAGIC_NUMBERS.recipes_per_row) * 36
local additional_height = math.max(88, button_table_height + 24) + 4
desired_scroll_pane_height = desired_scroll_pane_height + additional_height
end
modal_data.modal_elements.warning_label.visible = not any_recipe_visible
local scroll_pane_height = math.min(desired_scroll_pane_height, modal_data.dialog_maximal_height)
modal_data.modal_elements.content_frame.style.height = scroll_pane_height
end
local function handle_filter_change(player, tags, event)
local boolean_state = util.gui.switch.convert_to_boolean(event.element.switch_state)
util.globals.modal_data(player).filters[tags.filter_name] = boolean_state
util.globals.preferences(player).recipe_filters[tags.filter_name] = boolean_state
apply_recipe_filter(player, "")
end
-- Checks whether the dialog needs to be created at all
local function recipe_early_abort_check(player, modal_data)
-- Result is either the single possible recipe_id, or a table of relevant recipes
local result, error, filters = run_preliminary_checks(player, modal_data)
if error ~= nil then
util.messages.raise(player, "error", error, 1)
return true -- signal that the dialog does not need to actually be opened
else
-- If 1 relevant recipe is found, try it straight away
if type(result) == "number" then -- the given number being the recipe_id
attempt_adding_line(player, result)
return true -- idem. above
else -- Otherwise, save the relevant data for the dialog opener
modal_data.result = result
modal_data.filters = filters
return false -- signal that the dialog should be opened
end
end
end
-- Handles populating the recipe dialog
local function open_recipe_dialog(player, modal_data)
-- At this point, we're sure the dialog should be opened
local recipe_groups = {}
for _, recipe in pairs(modal_data.result) do
local group_name = recipe.proto.group.name
recipe_groups[group_name] = recipe_groups[group_name] or {proto=recipe.proto.group, recipes={}}
recipe_groups[group_name].recipes[recipe.proto.name] = recipe
end
modal_data.recipe_groups = recipe_groups
local translations = util.globals.player_table(player).translation_tables
create_dialog_structure(modal_data, translations)
apply_recipe_filter(player, "")
modal_data.modal_elements.search_textfield.focus()
-- Dispose of the temporary GUI-opening variables
modal_data.result = nil
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "pick_recipe",
timeout = 20,
handler = (function(player, tags, _)
attempt_adding_line(player, tags.recipe_proto_id)
util.raise.close_dialog(player, "cancel")
end)
}
},
on_gui_switch_state_changed = {
{
name = "toggle_recipe_filter",
handler = handle_filter_change
}
}
}
listeners.dialog = {
dialog = "recipe",
metadata = (function(modal_data)
local product_proto = global.prototypes.items[modal_data.category_id].members[modal_data.product_id]
return {
caption = {"", {"fp.add"}, " ", {"fp.pl_recipe", 1}},
subheader_text = {"fp.recipe_instruction", {"fp." .. modal_data.production_type},
product_proto.localised_name},
search_handler_name = "apply_recipe_filter",
create_content_frame = true
}
end),
early_abort_check = recipe_early_abort_check,
open = open_recipe_dialog
}
listeners.global = {
apply_recipe_filter = apply_recipe_filter
}
return { listeners }

View File

@@ -0,0 +1,122 @@
-- ** LOCAL UTIL **
local function update_submit_button(player, _, _)
local modal_elements = util.globals.modal_elements(player)
local name_length = string.len(modal_elements["subfactory_name"].text:gsub("^%s*(.-)%s*$", "%1"))
local issue_message = {"fp.subfactory_dialog_name_empty"}
modal_dialog.set_submit_button_state(modal_elements, (name_length > 0), issue_message)
end
local function add_rich_text(player, tags, event)
local modal_elements = util.globals.modal_elements(player)
local subfactory_name = modal_elements.subfactory_name.text
local type, elem_value = tags.type, event.element.elem_value
if elem_value == nil then return end -- no need to do anything here
if type == "signal" then
-- Signal types are insanely stupid
if not elem_value.name then event.element.elem_value = nil; return end
if elem_value.type == "virtual" then type = "virtual-signal"
else type = elem_value.type end
elem_value = elem_value.name
end
local rich_text = "[" .. type .. "=" .. elem_value .. "]"
modal_elements.subfactory_name.text = subfactory_name .. rich_text
event.element.elem_value = nil
update_submit_button(player)
end
local function open_subfactory_dialog(player, modal_data)
local factory = util.globals.context(player).factory
modal_data.subfactory = Factory.get(factory, "Subfactory", modal_data.subfactory_id)
local modal_elements = modal_data.modal_elements
local content_frame = modal_elements.content_frame
local flow_name = content_frame.add{type="flow", direction="horizontal", style="fp_flow_horizontal_centered"}
flow_name.add{type="label", caption={"fp.info_label", {"fp.subfactory_dialog_name"}},
tooltip={"fp.subfactory_dialog_name_tt"}}
local subfactory_name = (modal_data.subfactory ~= nil) and modal_data.subfactory.name or ""
local textfield_name = flow_name.add{type="textfield", text=subfactory_name,
tags={mod="fp", on_gui_text_changed="subfactory_name"}}
textfield_name.style.rich_text_setting = defines.rich_text_setting.enabled
textfield_name.style.width = 250
textfield_name.focus()
modal_elements["subfactory_name"] = textfield_name
local flow_rich_text = content_frame.add{type="flow", direction="horizontal", style="fp_flow_horizontal_centered"}
flow_rich_text.style.top_margin = 8
flow_rich_text.add{type="label", caption={"fp.info_label", {"fp.subfactory_dialog_rich_text"}},
tooltip={"fp.subfactory_dialog_rich_text_tt"}}
local signal_flow = flow_rich_text.add{type="flow", direction="horizontal", style="fp_flow_horizontal_centered"}
signal_flow.style.horizontal_spacing = 6
signal_flow.add{type="label", caption={"fp.subfactory_dialog_signals"}}
signal_flow.add{type="choose-elem-button", elem_type="signal", style="fp_sprite-button_inset_tiny",
tags={mod="fp", on_gui_elem_changed="add_rich_text", type="signal"}}
local recipe_flow = flow_rich_text.add{type="flow", direction="horizontal", style="fp_flow_horizontal_centered"}
recipe_flow.style.horizontal_spacing = 6
recipe_flow.add{type="label", caption={"fp.subfactory_dialog_recipes"}}
recipe_flow.add{type="choose-elem-button", elem_type="recipe", style="fp_sprite-button_inset_tiny",
tags={mod="fp", on_gui_elem_changed="add_rich_text", type="recipe"}}
update_submit_button(player)
end
local function close_subfactory_dialog(player, action)
local modal_data = util.globals.modal_data(player)
if action == "submit" then
local name_textfield = modal_data.modal_elements.subfactory_name
local subfactory_name = name_textfield.text:gsub("^%s*(.-)%s*$", "%1")
if modal_data.subfactory ~= nil then modal_data.subfactory.name = subfactory_name
else subfactory_list.add_subfactory(player, subfactory_name) end
util.raise.refresh(player, "all", nil)
elseif action == "delete" then
subfactory_list.delete_subfactory(player) -- handles archiving if necessary
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_text_changed = {
{
name = "subfactory_name",
handler = update_submit_button
}
},
on_gui_elem_changed = {
{
name = "add_rich_text",
handler = add_rich_text
}
}
}
listeners.dialog = {
dialog = "subfactory",
metadata = (function(modal_data)
local action = (modal_data.subfactory_id) and {"fp.edit"} or {"fp.add"}
return {
caption = {"", action, " ", {"fp.pl_subfactory", 1}},
subheader_text = {"fp.subfactory_dialog_description"},
create_content_frame = true,
show_submit_button = true,
show_delete_button = (modal_data.subfactory_id ~= nil)
}
end),
open = open_subfactory_dialog,
close = close_subfactory_dialog
}
return { listeners }

View File

@@ -0,0 +1,124 @@
-- ** LOCAL UTIL **
local tab_definitions = {"interface", "usage", "matrix_solver"}
function tab_definitions.interface(player, tab, tab_pane)
tab.caption = {"fp.interface"}
local function add_base_frame(name)
local frame = tab_pane.add{type="frame", style="fp_frame_bordered_stretch", direction="vertical"}
frame.style.horizontally_stretchable = true
frame.add{type="label", caption={"fp." .. name .. "_tutorial_title"}, style="caption_label"}
local label_text = frame.add{type="label", caption={"fp." .. name .. "_tutorial_text"}}
label_text.style.single_line = false
return frame
end
-- Interactive tutorial
local frame_interactive = add_base_frame("interactive")
local flow_interactive = frame_interactive.add{type="flow", direction="horizontal"}
flow_interactive.style.margin = {12, 20, 8, 20}
flow_interactive.add{type="empty-widget", style="flib_horizontal_pusher"}
-- If the tutorial subfactory is valid, it can be imported regardless of the current modset
local subfactory_valid = (global.tutorial_subfactory ~= nil and global.tutorial_subfactory.valid)
local button_tooltip = (not subfactory_valid) and {"fp.warning_message", {"fp.create_example_error"}} or nil
flow_interactive.add{type="button", tags={mod="fp", on_gui_click="add_example_subfactory"},
caption={"fp.create_example"}, tooltip=button_tooltip, enabled=subfactory_valid, mouse_button_filter={"left"}}
flow_interactive.add{type="empty-widget", style="flib_horizontal_pusher"}
local tutorial_mode = util.globals.preferences(player).tutorial_mode
util.gui.switch.add_on_off(flow_interactive, "toggle_tutorial_mode", {}, tutorial_mode,
{"fp.tutorial_mode"}, nil, true)
flow_interactive.add{type="empty-widget", style="flib_horizontal_pusher"}
-- Interface tutorial
local frame_interface = add_base_frame("interface")
local recipebook_string = (RECIPEBOOK_ACTIVE) and {"fp.interface_controls_recipebook"} or ""
local label_controls = frame_interface.add{type="label", caption={"", {"fp.interface_controls"}, recipebook_string}}
label_controls.style.single_line = false
label_controls.style.margin = {6, 0, 0, 6}
end
function tab_definitions.usage(_, tab, tab_pane)
tab.caption = {"fp.usage"}
local bordered_frame = tab_pane.add{type="frame", style="fp_frame_bordered_stretch"}
local label_text = bordered_frame.add{type="label", caption={"fp.tutorial_usage_text"}}
label_text.style.single_line = false
label_text.style.padding = 2
end
function tab_definitions.matrix_solver(_, tab, tab_pane)
tab.caption = {"fp.matrix_solver"}
local bordered_frame = tab_pane.add{type="frame", style="fp_frame_bordered_stretch"}
local label_text = bordered_frame.add{type="label", caption={"fp.tutorial_matrix_solver_text"}}
label_text.style.single_line = false
label_text.style.padding = 2
end
local function open_tutorial_dialog(player, modal_data)
local frame_tabs = modal_data.modal_elements.dialog_flow.add{type="frame", style="inside_deep_frame_for_tabs"}
local tabbed_pane = frame_tabs.add{type="tabbed-pane", style="tabbed_pane_with_no_side_padding"}
tabbed_pane.style.height = 600
for _, tab_name in ipairs(tab_definitions) do
local tab = tabbed_pane.add{type="tab"}
local tab_pane = tabbed_pane.add{type="scroll-pane", style="flib_naked_scroll_pane_under_tabs"}
tab_pane.style.width = 555
tab_definitions[tab_name](player, tab, tab_pane)
tabbed_pane.add_tab(tab, tab_pane)
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "add_example_subfactory",
timeout = 20,
handler = (function(player, _, _)
-- If this button can be pressed, the tutorial subfactory is valid implicitly
local player_table = util.globals.player_table(player)
local subfactory = Factory.add(player_table.factory, global.tutorial_subfactory)
solver.update(player, subfactory)
util.context.set_subfactory(player, subfactory)
util.raise.refresh(player, "all", nil)
util.raise.close_dialog(player, "cancel")
end)
}
},
on_gui_switch_state_changed = {
{
name = "toggle_tutorial_mode",
handler = (function(player, _, event)
local preferences = util.globals.preferences(player)
preferences.tutorial_mode = util.gui.switch.convert_to_boolean(event.element.switch_state)
util.raise.refresh(player, "all", nil)
end)
}
}
}
listeners.dialog = {
dialog = "tutorial",
metadata = (function(_) return {
caption = {"fp.tutorial"},
create_content_frame = false
} end),
open = open_tutorial_dialog
}
return { listeners }

View File

@@ -0,0 +1,449 @@
-- ** LOCAL UTIL **
-- Adds a box with title and optional scope switch for the given type of utility
local function add_utility_box(player, modal_elements, type, show_tooltip, show_switch)
local bordered_frame = modal_elements.content_frame.add{type="frame", direction="vertical",
style="fp_frame_bordered_stretch"}
modal_elements[type .. "_box"] = bordered_frame
local flow_title_bar = bordered_frame.add{type="flow", direction="horizontal"}
flow_title_bar.style.vertical_align = "center"
flow_title_bar.style.margin = {2, 0, 4, 0}
-- Title
local caption = (show_tooltip) and {"fp.info_label", {"fp.utility_title_".. type}} or {"fp.utility_title_".. type}
local tooltip = (show_tooltip) and {"fp.utility_title_" .. type .. "_tt"}
local label_title = flow_title_bar.add{type="label", caption=caption, tooltip=tooltip, style="caption_label"}
label_title.style.top_margin = -2
-- Empty flow for custom controls
flow_title_bar.add{type="empty-widget", style="flib_horizontal_pusher"}
local flow_custom = flow_title_bar.add{type="flow"}
flow_custom.style.right_margin = 12
-- Scope switch
local scope_switch = nil
if show_switch then
local utility_scope = util.globals.preferences(player).utility_scopes[type]
local switch_state = (utility_scope == "Subfactory") and "left" or "right"
scope_switch = flow_title_bar.add{type="switch", switch_state=switch_state,
tags={mod="fp", on_gui_switch_state_changed="utility_change_scope", utility_type=type},
left_label_caption={"fp.pu_subfactory", 1}, right_label_caption={"fp.pu_floor", 1}}
end
return bordered_frame, flow_custom, scope_switch
end
local utility_structures = {}
local function update_request_button(player, modal_data, subfactory)
local modal_elements = modal_data.modal_elements
local button_enabled, switch_enabled = true, true
local caption = "" ---@type LocalisedString
local tooltip = "" ---@type LocalisedString
local font_color = {}
if subfactory.item_request_proxy ~= nil then
caption = {"fp.cancel_request"}
font_color = {0.8, 0, 0}
switch_enabled = false
else
local scope = util.globals.preferences(player).utility_scopes.components
local scope_string = {"fp.pl_" .. scope:lower(), 1}
caption, tooltip = {"fp.request_items"}, {"fp.request_items_tt", scope_string}
if not player.force.character_logistic_requests then
tooltip = {"fp.warning_with_icon", {"fp.request_logistics_not_researched"}}
button_enabled = false
elseif not next(modal_data.missing_items) then
tooltip = {"fp.warning_with_icon", {"fp.utility_no_items_necessary", scope_string}}
button_enabled = false
elseif player.character == nil then -- happens when the editor is active for example
tooltip = {"fp.warning_with_icon", {"fp.request_no_character"}}
button_enabled = false
end
end
modal_elements.request_button.caption = caption
modal_elements.request_button.tooltip = tooltip
modal_elements.request_button.style.font_color = font_color
modal_elements.request_button.enabled = button_enabled
modal_elements.scope_switch.enabled = switch_enabled
end
function utility_structures.components(player, modal_data)
local scope = util.globals.preferences(player).utility_scopes.components
local lower_scope = scope:lower()
local context = util.globals.context(player)
local modal_elements = modal_data.modal_elements
if modal_elements.components_box == nil then
local components_box, custom_flow, scope_switch = add_utility_box(player, modal_data.modal_elements,
"components", true, true)
modal_elements.components_box = components_box
modal_elements.scope_switch = scope_switch
local button_combinator = custom_flow.add{type="sprite-button", sprite="item/constant-combinator",
tooltip={"fp.ingredients_to_combinator_tt"}, tags={mod="fp", on_gui_click="utility_item_combinator"},
style="fp_sprite-button_rounded_mini", mouse_button_filter={"left"}}
button_combinator.style.size = 29
button_combinator.style.padding = 0
modal_elements.combinator_button = button_combinator
local button_request = custom_flow.add{type="button", tags={mod="fp", on_gui_click="utility_request_items"},
style="rounded_button", mouse_button_filter={"left"}}
button_request.style.minimal_width = 0
modal_elements.request_button = button_request
local table_components = components_box.add{type="table", column_count=2}
table_components.style.horizontal_spacing = 24
table_components.style.vertical_spacing = 8
local function add_component_row(type)
table_components.add{type="label", caption={"fp.pu_" .. type, 2}, style="heading_3_label"}
local flow = table_components.add{type="flow", direction="horizontal"}
modal_elements["components_" .. type .. "_flow"] = flow
end
add_component_row("machine")
add_component_row("module")
end
local function refresh_component_flow(type)
local component_row = modal_elements["components_" .. type .. "_flow"]
component_row.clear()
local inventory_contents = modal_data.inventory_contents
local component_data = _G[scope].get_component_data(context[lower_scope], nil)
local frame_components = component_row.add{type="frame", direction="horizontal", style="slot_button_deep_frame"}
local table_components = frame_components.add{type="table", column_count=10, style="filter_slot_table"}
for _, component in pairs(component_data[type .. "s"]) do
if component.amount > 0 then
local proto, required_amount = component.proto, component.amount
local amount_in_inventory = inventory_contents[proto.name] or 0
local missing_amount = required_amount - amount_in_inventory
if missing_amount > 0 then modal_data.missing_items[proto.name] = missing_amount end
local button_style = nil
if amount_in_inventory == 0 then button_style = "flib_slot_button_red"
elseif missing_amount > 0 then button_style = "flib_slot_button_yellow"
else button_style = "flib_slot_button_green" end
local tooltip = {"fp.components_needed_tt", {"fp.tt_title", proto.localised_name},
amount_in_inventory, required_amount}
local category_id = (proto.data_type == "items") and proto.category_id
or PROTOTYPE_MAPS.items["item"].id -- modules/beacons are always an 'item'
local proto_id = (proto.data_Type == "items") and proto.id or
PROTOTYPE_MAPS.items["item"].members[proto.name].id
table_components.add{type="sprite-button", sprite=proto.sprite, number=required_amount, tooltip=tooltip,
tags={mod="fp", on_gui_click="utility_craft_items", category_id=category_id, item_id=proto_id,
missing_amount=missing_amount}, style=button_style, mouse_button_filter={"left-and-right"}}
end
end
if not next(table_components.children_names) then
frame_components.visible = false
local label = component_row.add{type="label", caption={"fp.no_components_needed", {"fp.pl_" .. type, 2}}}
label.style.margin = {10, 0}
end
end
modal_data.missing_items = {} -- a flat structure works because there is no overlap between machines and modules
refresh_component_flow("machine")
refresh_component_flow("module")
local subfactory = util.globals.context(player).subfactory
Subfactory.validate_item_request_proxy(subfactory)
local any_missing_items = (next(modal_data.missing_items) ~= nil)
modal_elements.combinator_button.enabled = any_missing_items
modal_elements.combinator_button.tooltip = (any_missing_items) and {"fp.utility_combinator_tt"}
or {"fp.warning_with_icon", {"fp.utility_no_items_necessary", {"fp.pl_" .. lower_scope, 1}}}
update_request_button(player, modal_data, subfactory)
end
function utility_structures.blueprints(player, modal_data)
local modal_elements = modal_data.modal_elements
if modal_elements.blueprints_box == nil then
local blueprints_box = add_utility_box(player, modal_data.modal_elements, "blueprints", true, false)
modal_elements["blueprints_box"] = blueprints_box
local frame_blueprints = blueprints_box.add{type="frame", direction="horizontal", style="slot_button_deep_frame"}
local table_blueprints = frame_blueprints.add{type="table", column_count=MAGIC_NUMBERS.blueprint_limit,
style="filter_slot_table"}
modal_elements["blueprints_table"] = table_blueprints
end
local subfactory = util.globals.context(player).subfactory
local blueprints = subfactory.blueprints
local table_blueprints = modal_elements["blueprints_table"]
table_blueprints.clear()
local tutorial_tt = (util.globals.preferences(player).tutorial_mode)
and util.actions.tutorial_tooltip("act_on_blueprint", nil, player) or nil
local function format_signal(signal)
local type = (signal.type == "virtual") and "virtual-signal" or signal.type
return (type .. "/" .. signal.name)
end
local blueprint = modal_data.utility_inventory[1] -- re-usable inventory slot
for index, blueprint_string in pairs(blueprints) do
blueprint.import_stack(blueprint_string)
local blueprint_book = blueprint.is_blueprint_book
local tooltip = {"", (blueprint.label or "Blueprint"), tutorial_tt}
local sprite = (blueprint_book) and "item/blueprint-book" or "item/blueprint"
local button = table_blueprints.add{type="sprite-button", sprite=sprite, tooltip=tooltip,
tags={mod="fp", on_gui_click="act_on_blueprint", index=index}, mouse_button_filter={"left-and-right"}}
local icons = (not blueprint_book) and blueprint.blueprint_icons
or blueprint.get_inventory(defines.inventory.item_main)[1].blueprint_icons
if icons then -- this is jank-hell
local icon_count = #icons
local flow = button.add{type="flow", direction="horizontal", ignored_by_interaction=true}
local top_margin = (blueprint_book) and 4 or 7
if icon_count == 1 then
local sprite_icon = flow.add{type="sprite", sprite=format_signal(icons[1].signal)}
sprite_icon.style.margin = {top_margin, 0, 0, 7}
else
flow.style.padding = {4, 0, 0, 3}
local table = flow.add{type="table", column_count=2}
table.style.cell_padding = -4
if icon_count == 2 then table.style.top_margin = top_margin end
for _, icon in pairs(icons) do
table.add{type="sprite", sprite=format_signal(icon.signal)}
end
end
end
blueprint.clear()
end
if #blueprints < MAGIC_NUMBERS.blueprint_limit then
table_blueprints.add{type="sprite-button", tags={mod="fp", on_gui_click="utility_store_blueprint"},
sprite="utility/add", style="fp_sprite-button_inset_add_slot", mouse_button_filter={"left"}}
end
end
function utility_structures.notes(player, modal_data)
local utility_box = add_utility_box(player, modal_data.modal_elements, "notes", false, false)
local notes = util.globals.context(player).subfactory.notes
local text_box = utility_box.add{type="text-box", text=notes,
tags={mod="fp", on_gui_text_changed="subfactory_notes"}}
text_box.style.size = {500, 250}
text_box.word_wrap = true
text_box.style.top_margin = -2
end
local function handle_scope_change(player, tags, event)
local utility_scope = (event.element.switch_state == "left") and "Subfactory" or "Floor"
util.globals.preferences(player).utility_scopes[tags.utility_type] = utility_scope
local modal_data = util.globals.modal_data(player)
utility_structures.components(player, modal_data)
end
local function handle_item_request(player, _, _)
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
if subfactory.item_request_proxy then -- if an item_proxy is set, cancel it
Subfactory.destroy_item_request_proxy(subfactory)
else
-- This crazy way to request items actually works, and is way easier than setting logistic requests
-- The advantage that is has is that the delivery is one-time, not a constant request
-- The disadvantage is that it's weird to have construction bots bring you stuff
subfactory.item_request_proxy = player.surface.create_entity{name="item-request-proxy",
position=player.position, force=player.force, target=player.character,
modules=ui_state.modal_data.missing_items}
end
update_request_button(player, ui_state.modal_data, subfactory)
end
local function handle_item_handcraft(player, tags, event)
local fly_text = util.cursor.create_flying_text
if not player.character then fly_text(player, {"fp.utility_no_character"}); return end
local permissions = player.permission_group
local forbidden = (permissions and not permissions.allows_action(defines.input_action.craft))
if forbidden then fly_text(player, {"fp.utility_no_crafting"}); return end
local desired_amount = (event.button == defines.mouse_button_type.right) and 5 or 1
local amount_to_craft = math.min(desired_amount, tags.missing_amount)
if amount_to_craft <= 0 then fly_text(player, {"fp.utility_no_demand"}); return end
local recipes = RECIPE_MAPS["produce"][tags.category_id][tags.item_id]
if not recipes then fly_text(player, {"fp.utility_no_recipe"}); return end
local success = false
for recipe_id, _ in pairs(recipes) do
local recipe_name = global.prototypes.recipes[recipe_id].name
local craftable_amount = player.get_craftable_count(recipe_name)
if craftable_amount > 0 then
success = true
local crafted_amount = math.min(amount_to_craft, craftable_amount)
player.begin_crafting{count=crafted_amount, recipe=recipe_name, silent=true}
amount_to_craft = amount_to_craft - crafted_amount
break
end
end
if not success then fly_text(player, {"fp.utility_no_resources"}); end
end
local function handle_inventory_change(player)
local ui_state = util.globals.ui_state(player)
if ui_state.modal_dialog_type == "utility" then
ui_state.modal_data.inventory_contents = player.get_main_inventory().get_contents()
utility_structures.components(player, ui_state.modal_data)
end
end
local function store_blueprint(player, _, _)
local ui_state = util.globals.ui_state(player)
local fly_text = util.cursor.create_flying_text
if player.is_cursor_empty() then
fly_text(player, {"fp.utility_cursor_empty"}); return
end
local cursor = player.cursor_stack
if not (cursor.is_blueprint or cursor.is_blueprint_book) then
if cursor.valid_for_read then
fly_text(player, {"fp.utility_no_blueprint"}); return
else
fly_text(player, {"fp.utility_blueprint_from_library"}); return
end
end
if cursor.is_blueprint then
if not cursor.is_blueprint_setup() then fly_text(player, {"fp.utility_blueprint_not_setup"}); return end
else -- blueprint book
local inventory = cursor.get_inventory(defines.inventory.item_main)
if inventory.is_empty() then fly_text(player, {"fp.utility_blueprint_book_empty"}); return end
end
table.insert(ui_state.context.subfactory.blueprints, cursor.export_stack())
fly_text(player, {"fp.utility_blueprint_stored"});
player.clear_cursor() -- doesn't delete blueprint, but puts it back in the inventory
utility_structures.blueprints(player, ui_state.modal_data)
end
local function handle_blueprint_click(player, tags, action)
local ui_state = util.globals.ui_state(player)
local blueprints = ui_state.context.subfactory.blueprints
if action == "pick_up" then
player.cursor_stack.import_stack(blueprints[tags.index])
util.raise.close_dialog(player, "cancel")
main_dialog.toggle(player)
elseif action == "delete" then
table.remove(blueprints, tags.index)
utility_structures.blueprints(player, ui_state.modal_data)
end
end
local function open_utility_dialog(player, modal_data)
-- Add the players' relevant inventory components to modal_data
modal_data.inventory_contents = player.get_main_inventory().get_contents()
modal_data.utility_inventory = game.create_inventory(1) -- used for blueprint decoding
utility_structures.components(player, modal_data)
utility_structures.blueprints(player, modal_data)
utility_structures.notes(player, modal_data)
end
local function close_utility_dialog(player, _)
util.globals.modal_data(player).utility_inventory.destroy()
util.raise.refresh(player, "subfactory_info", nil)
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "utility_item_combinator",
timeout = 20,
handler = (function(player, _, _)
local missing_items = util.globals.modal_data(player).missing_items
local success = util.cursor.set_item_combinator(player, missing_items)
if success then util.raise.close_dialog(player, "cancel"); main_dialog.toggle(player) end
end)
},
{
name = "utility_request_items",
timeout = 20,
handler = handle_item_request
},
{
name = "utility_craft_items",
handler = handle_item_handcraft
},
{
name = "utility_store_blueprint",
handler = store_blueprint
},
{
name = "act_on_blueprint",
modifier_actions = {
pick_up = {"left"},
delete = {"control-right"}
},
handler = handle_blueprint_click
},
},
on_gui_switch_state_changed = {
{
name = "utility_change_scope",
handler = handle_scope_change
}
},
on_gui_text_changed = {
{
name = "subfactory_notes",
handler = (function(player, _, event)
util.globals.context(player).subfactory.notes = event.element.text
end)
}
}
}
listeners.dialog = {
dialog = "utility",
metadata = (function(_) return {
caption = {"fp.utilities"},
create_content_frame = true
} end),
open = open_utility_dialog,
close = close_utility_dialog
}
listeners.misc = {
on_player_main_inventory_changed = handle_inventory_change
}
return { listeners }