Первый фикс

Пачки некоторых позиций увеличены
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,372 @@
-- ** LOCAL UTIL **
local function add_recipe(player, context, type, item_proto)
if context.floor.level > 1 then
local message = {"fp.error_recipe_wrong_floor", {"fp.pu_" .. type, 1}}
util.messages.raise(player, "error", message, 1)
else
local production_type = (type == "byproduct") and "consume" or "produce"
util.raise.open_dialog(player, {dialog="recipe",
modal_data={category_id=item_proto.category_id, product_id=item_proto.id,
floor_id=context.floor.id, production_type=production_type}})
end
end
local function build_item_box(player, category, column_count)
local item_boxes_elements = util.globals.main_elements(player).item_boxes
local window_frame = item_boxes_elements.horizontal_flow.add{type="frame", direction="vertical",
style="inside_shallow_frame"}
window_frame.style.top_padding = 6
window_frame.style.bottom_padding = MAGIC_NUMBERS.frame_spacing
local title_flow = window_frame.add{type="flow", direction="horizontal"}
title_flow.style.vertical_align = "center"
local label = title_flow.add{type="label", caption={"fp.pu_" .. category, 2}, style="caption_label"}
label.style.left_padding = MAGIC_NUMBERS.frame_spacing
label.style.bottom_margin = 4
if category == "ingredient" then
local button_combinator = title_flow.add{type="sprite-button", sprite="item/constant-combinator",
tooltip={"fp.ingredients_to_combinator_tt"}, tags={mod="fp", on_gui_click="ingredients_to_combinator"},
visible=false, mouse_button_filter={"left"}}
button_combinator.style.size = 24
button_combinator.style.padding = -2
button_combinator.style.left_margin = 4
item_boxes_elements["ingredient_combinator_button"] = button_combinator
end
local scroll_pane = window_frame.add{type="scroll-pane", style="fp_scroll-pane_slot_table"}
scroll_pane.style.maximal_height = MAGIC_NUMBERS.item_box_max_rows * MAGIC_NUMBERS.item_button_size
scroll_pane.style.horizontally_stretchable = false
scroll_pane.style.vertically_stretchable = false
local item_frame = scroll_pane.add{type="frame", style="slot_button_deep_frame"}
item_frame.style.width = column_count * MAGIC_NUMBERS.item_button_size
local table_items = item_frame.add{type="table", column_count=column_count, style="filter_slot_table"}
item_boxes_elements[category .. "_item_table"] = table_items
end
local function refresh_item_box(player, items, category, subfactory, shows_floor_items)
local ui_state = util.globals.ui_state(player)
local item_boxes_elements = ui_state.main_elements.item_boxes
local table_items = item_boxes_elements[category .. "_item_table"]
table_items.clear()
if not subfactory or not subfactory.valid then
item_boxes_elements["ingredient_combinator_button"].visible = false
return 0
end
local table_item_count = 0
local metadata = view_state.generate_metadata(player, subfactory)
local default_style = (category == "byproduct") and "flib_slot_button_red" or "flib_slot_button_default"
local action = (shows_floor_items) and ("act_on_floor_item") or ("act_on_top_level_" .. category)
local tutorial_tt = (util.globals.preferences(player).tutorial_mode)
and util.actions.tutorial_tooltip(action, nil, player) or nil
for _, item in ipairs(items) do
local required_amount = (not shows_floor_items and category == "product") and Item.required_amount(item) or nil
local amount, number_tooltip = view_state.process_item(metadata, item, required_amount, nil)
if amount == -1 then goto skip_item end -- an amount of -1 means it was below the margin of error
local style = default_style
local satisfaction_line = "" ---@type LocalisedString
if not shows_floor_items and category == "product" and amount ~= nil and amount ~= "0" then
local satisfied_percentage = (item.amount / required_amount) * 100
local percentage_string = util.format.number(satisfied_percentage, 3)
satisfaction_line = {"", "\n", {"fp.bold_label", (percentage_string .. "%")}, " ", {"fp.satisfied"}}
if satisfied_percentage <= 0 then style = "flib_slot_button_red"
elseif satisfied_percentage < 100 then style = "flib_slot_button_yellow"
else style = "flib_slot_button_green" end
end
local number_line = (number_tooltip) and {"", "\n", number_tooltip} or ""
local name_line, tooltip, enabled = nil, nil, true
if item.proto.type == "entity" then -- only relevant to ingredients
name_line = {"fp.tt_title_with_note", item.proto.localised_name, {"fp.raw_ore"}}
tooltip = {"", name_line, number_line, satisfaction_line}
style = "flib_slot_button_transparent"
enabled = false
else
name_line = {"fp.tt_title", item.proto.localised_name}
tooltip = {"", name_line, number_line, satisfaction_line, tutorial_tt}
end
table_items.add{type="sprite-button", tooltip=tooltip, number=amount, style=style, sprite=item.proto.sprite,
tags={mod="fp", on_gui_click=action, category=category, item_id=item.id}, enabled=enabled,
mouse_button_filter={"left-and-right"}}
table_item_count = table_item_count + 1
::skip_item:: -- goto for fun, wooohoo
end
if category == "product" and not shows_floor_items then -- meaning allow the user to add items of this type
table_items.add{type="sprite-button", enabled=(not ui_state.flags.archive_open),
tags={mod="fp", on_gui_click="add_top_level_item", category=category}, sprite="utility/add",
tooltip={"", {"fp.add"}, " ", {"fp.pl_" .. category, 1}, "\n", {"fp.shift_to_paste"}},
style="fp_sprite-button_inset_add_slot", mouse_button_filter={"left"}}
table_item_count = table_item_count + 1
end
if category == "ingredient" then
item_boxes_elements["ingredient_combinator_button"].visible = (table_item_count > 0)
end
local table_rows_required = math.ceil(table_item_count / table_items.column_count)
return table_rows_required
end
local function handle_item_add(player, tags, event)
local context = util.globals.context(player)
if event.shift then -- paste
-- Use a fake item to paste on top of
local class = tags.category:gsub("^%l", string.upper)
local fake_item = {proto={name=""}, parent=context.subfactory, class=class}
util.clipboard.paste(player, fake_item)
else
util.raise.open_dialog(player, {dialog="picker", modal_data={item_id=nil, item_category=tags.category}})
end
end
local function handle_item_button_click(player, tags, action)
local player_table = util.globals.player_table(player)
local context = player_table.ui_state.context
local floor_items_active = (player_table.preferences.show_floor_items and context.floor.level > 1)
local class = (tags.category:gsub("^%l", string.upper))
local item = (floor_items_active) and Line.get(context.floor.origin_line, class, tags.item_id)
or Subfactory.get(context.subfactory, class, tags.item_id)
if action == "add_recipe" then
add_recipe(player, context, tags.category, item.proto)
elseif action == "edit" then
util.raise.open_dialog(player, {dialog="picker", modal_data={item_id=item.id, item_category="product"}})
elseif action == "copy" then
util.clipboard.copy(player, item)
elseif action == "paste" then
util.clipboard.paste(player, item)
elseif action == "delete" then
Subfactory.remove(context.subfactory, item)
solver.update(player, context.subfactory)
util.raise.refresh(player, "all", nil) -- make sure product icons are updated
elseif action == "specify_amount" then
-- Set the view state so that the amount shown in the dialog makes sense
view_state.select(player, "items_per_timescale")
util.raise.refresh(player, "subfactory", nil)
local modal_data = {
title = {"fp.options_item_title", {"fp.pl_ingredient", 1}},
text = {"fp.options_item_text", item.proto.localised_name},
submission_handler_name = "scale_subfactory_by_ingredient_amount",
item_id = item.id,
fields = {
{
type = "numeric_textfield",
name = "item_amount",
caption = {"fp.options_item_amount"},
tooltip = {"fp.options_subfactory_ingredient_amount_tt"},
text = item.amount,
width = 140,
focus = true
}
}
}
util.raise.open_dialog(player, {dialog="options", modal_data=modal_data})
elseif action == "put_into_cursor" then
local amount = (not floor_items_active and tags.category == "product")
and Item.required_amount(item) or item.amount
util.cursor.add_to_item_combinator(player, item.proto, amount)
elseif action == "recipebook" then
util.open_in_recipebook(player, item.proto.type, item.proto.name)
end
end
local function put_ingredients_into_cursor(player, _, _)
local context = util.globals.context(player)
local floor = context.floor
local show_floor_items = util.globals.preferences(player).show_floor_items
local container = (show_floor_items and floor.level > 1) and floor.origin_line or context.subfactory
local ingredients = {}
for _, ingredient in pairs(_G[container.class].get_all(container, "Ingredient")) do
if ingredient.proto.type == "item" then ingredients[ingredient.proto.name] = ingredient.amount end
end
local success = util.cursor.set_item_combinator(player, ingredients)
if success then main_dialog.toggle(player) end
end
local function scale_subfactory_by_ingredient_amount(player, options, action)
if action == "submit" then
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
local item = Subfactory.get(subfactory, "Ingredient", ui_state.modal_data.item_id)
if options.item_amount then
-- The division is not pre-calculated to avoid precision errors in some cases
local current_amount, target_amount = item.amount, options.item_amount
for _, product in pairs(Subfactory.get_all(subfactory, "Product")) do
local requirement = product.required_amount
requirement.amount = requirement.amount * target_amount / current_amount
end
end
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
end
end
local function refresh_item_boxes(player)
local player_table = util.globals.player_table(player)
local main_elements = player_table.ui_state.main_elements
if main_elements.main_frame == nil then return end
local context = player_table.ui_state.context
local subfactory = context.subfactory
local floor = context.floor
-- This is all kinds of stupid, but the mob wishes the feature to exist
local function refresh(parent, class, shows_floor_items)
local items = (parent) and _G[parent.class].get_in_order(parent, class) or {}
return refresh_item_box(player, items, class:lower(), subfactory, shows_floor_items)
end
local prow_count, brow_count, irow_count = 0, 0, 0
if player_table.preferences.show_floor_items and floor and floor.level > 1 then
local line = floor.origin_line
prow_count = refresh(line, "Product", true)
brow_count = refresh(line, "Byproduct", true)
irow_count = refresh(line, "Ingredient", true)
else
prow_count = refresh(subfactory, "Product", false)
brow_count = refresh(subfactory, "Byproduct", false)
irow_count = refresh(subfactory, "Ingredient", false)
end
local maxrow_count = math.max(prow_count, math.max(brow_count, irow_count))
local actual_row_count = math.min(math.max(maxrow_count, 1), MAGIC_NUMBERS.item_box_max_rows)
local item_table_height = actual_row_count * MAGIC_NUMBERS.item_button_size
-- set the heights for both the visible frame and the scroll pane containing it
local item_boxes_elements = player_table.ui_state.main_elements.item_boxes
item_boxes_elements.product_item_table.parent.style.minimal_height = item_table_height
item_boxes_elements.product_item_table.parent.parent.style.minimal_height = item_table_height
item_boxes_elements.byproduct_item_table.parent.style.minimal_height = item_table_height
item_boxes_elements.byproduct_item_table.parent.parent.style.minimal_height = item_table_height
item_boxes_elements.ingredient_item_table.parent.style.minimal_height = item_table_height
item_boxes_elements.ingredient_item_table.parent.parent.style.minimal_height = item_table_height
end
local function build_item_boxes(player)
local main_elements = util.globals.main_elements(player)
main_elements.item_boxes = {}
local parent_flow = main_elements.flows.right_vertical
local flow_horizontal = parent_flow.add{type="flow", direction="horizontal"}
flow_horizontal.style.horizontal_spacing = MAGIC_NUMBERS.frame_spacing
main_elements.item_boxes["horizontal_flow"] = flow_horizontal
local products_per_row = util.globals.settings(player).products_per_row
build_item_box(player, "product", products_per_row)
build_item_box(player, "byproduct", products_per_row)
build_item_box(player, "ingredient", products_per_row*2)
refresh_item_boxes(player)
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "add_top_level_item",
handler = handle_item_add
},
{
name = "act_on_top_level_product",
modifier_actions = {
add_recipe = {"left", {archive_open=false}},
edit = {"right", {archive_open=false}},
copy = {"shift-right"},
paste = {"shift-left", {archive_open=false}},
delete = {"control-right", {archive_open=false}},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_button_click
},
{
name = "act_on_top_level_byproduct",
modifier_actions = {
add_recipe = {"left", {archive_open=false, matrix_active=true}},
copy = {"shift-right"},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_button_click
},
{
name = "act_on_top_level_ingredient",
modifier_actions = {
add_recipe = {"left", {archive_open=false}},
specify_amount = {"right", {archive_open=false}},
copy = {"shift-right"},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_button_click
},
{
name = "act_on_floor_item",
modifier_actions = {
copy = {"shift-right"},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_button_click
},
{
name = "ingredients_to_combinator",
timeout = 20,
handler = put_ingredients_into_cursor
}
}
}
listeners.misc = {
build_gui_element = (function(player, event)
if event.trigger == "main_dialog" then
build_item_boxes(player)
end
end),
refresh_gui_element = (function(player, event)
local triggers = {item_boxes=true, production=true, subfactory=true, all=true}
if triggers[event.trigger] then refresh_item_boxes(player) end
end)
}
listeners.global = {
scale_subfactory_by_ingredient_amount = scale_subfactory_by_ingredient_amount
}
return { listeners }

View File

@@ -0,0 +1,197 @@
-- ** LOCAL UTIL **
local function refresh_production(player, _, _)
local subfactory = util.globals.context(player).subfactory
if subfactory and subfactory.valid then
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
end
end
local function paste_line(player, _, event)
if event.button == defines.mouse_button_type.left and event.shift then
local context = util.globals.context(player)
local line_count = context.floor.Line.count
local last_line = Floor.get_by_gui_position(context.floor, "Line", line_count)
-- Use a fake first line to paste below if no actual line exists
if not last_line then last_line = {parent=context.floor, class="Line", gui_position=0} end
if util.clipboard.paste(player, last_line) then
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
end
end
end
-- Changes the floor to either be the top one or the one above the current one
local function change_floor(player, destination)
if util.context.change_floor(player, destination) then
-- Only refresh if the floor was indeed changed
util.raise.refresh(player, "production", nil)
end
end
local function refresh_production_box(player)
local player_table = util.globals.player_table(player)
local ui_state = player_table.ui_state
if ui_state.main_elements.main_frame == nil then return end
local production_box_elements = ui_state.main_elements.production_box
local subfactory = ui_state.context.subfactory
local subfactory_valid = subfactory and subfactory.valid
local current_level = (subfactory_valid) and subfactory.selected_floor.level or 1
local any_lines_present = (subfactory_valid) and (subfactory.selected_floor.Line.count > 0) or false
local archive_open = (ui_state.flags.archive_open)
production_box_elements.refresh_button.enabled = (not archive_open and subfactory_valid and any_lines_present)
production_box_elements.level_label.caption = (not subfactory_valid) and ""
or {"fp.bold_label", {"", {"fp.level"}, " ", current_level}}
production_box_elements.floor_up_button.visible = (subfactory_valid)
production_box_elements.floor_up_button.enabled = (current_level > 1)
production_box_elements.floor_top_button.visible = (subfactory_valid)
production_box_elements.floor_top_button.enabled = (current_level > 1)
production_box_elements.separator_line.visible = (subfactory_valid)
production_box_elements.utility_dialog_button.visible = (subfactory_valid)
util.raise.refresh(player, "view_state", production_box_elements.view_state_table)
production_box_elements.view_state_table.visible = (subfactory_valid)
-- This structure is stupid and huge, but not sure how to do it more elegantly
production_box_elements.instruction_label.visible = false
if not archive_open then
if subfactory == nil then
production_box_elements.instruction_label.caption = {"fp.production_instruction_subfactory"}
production_box_elements.instruction_label.visible = true
elseif subfactory_valid then
if subfactory.Product.count == 0 then
production_box_elements.instruction_label.caption = {"fp.production_instruction_product"}
production_box_elements.instruction_label.visible = true
elseif not any_lines_present then
production_box_elements.instruction_label.caption = {"fp.production_instruction_recipe"}
production_box_elements.instruction_label.visible = true
end
end
end
end
local function build_production_box(player)
local main_elements = util.globals.main_elements(player)
main_elements.production_box = {}
local parent_flow = main_elements.flows.right_vertical
local frame_vertical = parent_flow.add{type="frame", direction="vertical", style="inside_deep_frame"}
-- Insert a 'superfluous' flow for the sole purpose of detecting clicks on it
local click_flow = frame_vertical.add{type="flow", direction="vertical",
tags={mod="fp", on_gui_click="paste_line"}}
click_flow.style.vertically_stretchable = true
click_flow.style.horizontally_stretchable = true
main_elements.production_box["vertical_frame"] = click_flow
local subheader = click_flow.add{type="frame", direction="horizontal", style="subheader_frame"}
subheader.style.maximal_height = 100 -- large value to nullify maximal_height
subheader.style.padding = {8, 8, 6, 8}
local button_refresh = subheader.add{type="sprite-button", tags={mod="fp", on_gui_click="refresh_production"},
sprite="utility/refresh", style="tool_button", tooltip={"fp.refresh_production"}, mouse_button_filter={"left"}}
main_elements.production_box["refresh_button"] = button_refresh
local label_title = subheader.add{type="label", caption={"fp.production"}, style="frame_title"}
label_title.style.padding = {0, 8}
local label_level = subheader.add{type="label"}
label_level.style.right_margin = 8
main_elements.production_box["level_label"] = label_level
local button_floor_up = subheader.add{type="sprite-button", sprite="fp_sprite_arrow_line_up",
tooltip={"fp.floor_up_tt"}, tags={mod="fp", on_gui_click="change_floor", destination="up"},
style="fp_sprite-button_rounded_mini", mouse_button_filter={"left"}}
main_elements.production_box["floor_up_button"] = button_floor_up
local button_floor_top = subheader.add{type="sprite-button", sprite="fp_sprite_arrow_line_bar_up",
tooltip={"fp.floor_top_tt"}, tags={mod="fp", on_gui_click="change_floor", destination="top"},
style="fp_sprite-button_rounded_mini", mouse_button_filter={"left"}}
main_elements.production_box["floor_top_button"] = button_floor_top
local separator = subheader.add{type="line", direction="vertical"}
separator.style.margin = {0, 8}
main_elements.production_box["separator_line"] = separator
local button_utility_dialog = subheader.add{type="button", caption={"fp.utilities"},
tooltip={"fp.utility_dialog_tt"}, tags={mod="fp", on_gui_click="open_utility_dialog"},
style="fp_button_rounded_mini", mouse_button_filter={"left"}}
main_elements.production_box["utility_dialog_button"] = button_utility_dialog
subheader.add{type="empty-widget", style="flib_horizontal_pusher"}
util.raise.build(player, "view_state", subheader)
main_elements.production_box["view_state_table"] = subheader["table_view_state"]
local label_instruction = click_flow.add{type="label", style="bold_label"}
label_instruction.style.margin = 20
main_elements.production_box["instruction_label"] = label_instruction
local frame_messages = frame_vertical.add{type="frame", direction="vertical",
visible=false, style="fp_frame_messages"}
main_elements["messages_frame"] = frame_messages
refresh_production_box(player)
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "refresh_production",
timeout = 20,
handler = refresh_production
},
{
name = "change_floor",
handler = (function(player, tags, _)
change_floor(player, tags.destination)
end)
},
{
name = "open_utility_dialog",
handler = (function(player, _, _)
util.raise.open_dialog(player, {dialog="utility"})
end)
},
{
name = "paste_line",
handler = paste_line
}
}
}
listeners.misc = {
fp_refresh_production = (function(player, _, _)
if main_dialog.is_in_focus(player) then refresh_production(player, nil, nil) end
end),
fp_up_floor = (function(player, _, _)
if main_dialog.is_in_focus(player) then change_floor(player, "up") end
end),
fp_top_floor = (function(player, _, _)
if main_dialog.is_in_focus(player) then change_floor(player, "top") end
end),
build_gui_element = (function(player, event)
if event.trigger == "main_dialog" then
build_production_box(player)
end
end),
refresh_gui_element = (function(player, event)
local triggers = {production_box=true, production_detail=true, production=true, subfactory=true, all=true}
if triggers[event.trigger] then refresh_production_box(player) end
end)
}
return { listeners }

View File

@@ -0,0 +1,500 @@
-- ** LOCAL UTIL **
local function handle_line_move_click(player, tags, event)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
local spots_to_shift = (event.control) and 5 or ((not event.shift) and 1 or nil)
local translated_direction = (tags.direction == "up") and "negative" or "positive"
local first_position = (floor.level > 1) and 2 or 1
Floor.shift(floor, line, first_position, translated_direction, spots_to_shift)
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
end
local function handle_recipe_click(player, tags, action)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
if action == "open_subfloor" then
if relevant_line.recipe.production_type == "consume" then
util.messages.raise(player, "error", {"fp.error_no_subfloor_on_byproduct_recipes"}, 1)
return
end
local subfloor = line.subfloor
if subfloor == nil then
if util.globals.flags(player).archive_open then
util.messages.raise(player, "error", {"fp.error_no_new_subfloors_in_archive"}, 1)
return
end
subfloor = Floor.init(line) -- attaches itself to the given line automatically
Subfactory.add(context.subfactory, subfloor)
solver.update(player, context.subfactory)
end
util.context.set_floor(player, subfloor)
util.raise.refresh(player, "production", nil)
elseif action == "copy" then
util.clipboard.copy(player, line) -- use actual line
elseif action == "paste" then
util.clipboard.paste(player, line) -- use actual line
elseif action == "toggle" then
relevant_line.active = not relevant_line.active
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
elseif action == "delete" then
Floor.remove(floor, line)
-- If this recipe is deleted from a lower level floor (folded out subfloor), reset if necessary
if context.floor.level < floor.level and Floor.count(floor, "Line") < 2 then Floor.reset(floor) end
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
elseif action == "recipebook" then
util.open_in_recipebook(player, "recipe", relevant_line.recipe.proto.name)
end
end
local function handle_percentage_change(player, tags, event)
local ui_state = util.globals.ui_state(player)
local floor = Subfactory.get(ui_state.context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
relevant_line.percentage = tonumber(event.element.text) or 100
ui_state.flags.recalculate_on_subfactory_change = true -- set flag to recalculate if necessary
end
local function handle_percentage_confirmation(player, _, _)
local ui_state = util.globals.ui_state(player)
ui_state.flags.recalculate_on_subfactory_change = false -- reset this flag as we refresh below
solver.update(player, ui_state.context.subfactory)
util.raise.refresh(player, "subfactory", nil)
end
local function handle_machine_click(player, tags, action)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
-- I don't need to care about relevant lines here because this only gets called on lines without subfloor
if action == "put_into_cursor" then
local success = util.cursor.set_entity(player, line, line.machine)
if success then main_dialog.toggle(player) end
elseif action == "edit" then
util.raise.open_dialog(player, {dialog="machine", modal_data={floor_id=floor.id, line_id=line.id,
recipe_name=line.recipe.proto.localised_name}})
elseif action == "copy" then
util.clipboard.copy(player, line.machine)
elseif action == "paste" then
util.clipboard.paste(player, line.machine)
elseif action == "reset_to_default" then
Line.change_machine_to_default(line, player) -- guaranteed to find something
line.machine.limit = nil
line.machine.force_limit = true
local message = Line.apply_mb_defaults(line, player)
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
if message ~= nil then util.messages.raise(player, message.category, message.text, 1) end
elseif action == "recipebook" then
util.open_in_recipebook(player, "entity", line.machine.proto.name)
end
end
local function handle_machine_module_add(player, tags, event)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
if event.shift then -- paste
util.clipboard.paste(player, line.machine)
else
util.raise.open_dialog(player, {dialog="machine", modal_data={floor_id=floor.id, line_id=line.id,
recipe_name=line.recipe.proto.localised_name}})
end
end
local function handle_beacon_click(player, tags, action)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
-- I don't need to care about relevant lines here because this only gets called on lines without subfloor
if action == "put_into_cursor" then
local success = util.cursor.set_entity(player, line, line.beacon)
if success then main_dialog.toggle(player) end
elseif action == "edit" then
util.raise.open_dialog(player, {dialog="beacon", modal_data={floor_id=floor.id, line_id=line.id,
machine_name=line.machine.proto.localised_name, edit=true}})
elseif action == "copy" then
util.clipboard.copy(player, line.beacon)
elseif action == "paste" then
util.clipboard.paste(player, line.beacon)
elseif action == "delete" then
Line.set_beacon(line, nil)
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
elseif action == "recipebook" then
util.open_in_recipebook(player, "entity", line.beacon.proto.name)
end
end
local function handle_beacon_add(player, tags, event)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
if event.shift then -- paste
-- Use a fake beacon to paste on top of
local fake_beacon = {parent=line, class="Beacon"}
util.clipboard.paste(player, fake_beacon)
else
util.raise.open_dialog(player, {dialog="beacon", modal_data={floor_id=floor.id, line_id=line.id,
machine_name=line.machine.proto.localised_name, edit=false}})
end
end
local function handle_module_click(player, tags, action)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
-- I don't need to care about relevant lines here because this only gets called on lines without subfloor
local parent_entity = line[tags.parent_type]
local module = ModuleSet.get(parent_entity.module_set, tags.module_id)
if action == "edit" then
util.raise.open_dialog(player, {dialog=tags.parent_type, modal_data={floor_id=floor.id, line_id=line.id,
recipe_name=line.recipe.proto.localised_name, machine_name=line.machine.proto.localised_name, edit=true}})
elseif action == "copy" then
util.clipboard.copy(player, module)
elseif action == "paste" then
util.clipboard.paste(player, module)
elseif action == "delete" then
local module_set = parent_entity.module_set
ModuleSet.remove(module_set, module)
if parent_entity.class == "Beacon" and module_set.module_count == 0 then
Line.set_beacon(line, nil)
end
ModuleSet.normalize(module_set, {effects=true})
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
elseif action == "recipebook" then
util.open_in_recipebook(player, "item", module.proto.name)
end
end
local function apply_item_options(player, options, action)
if action == "submit" then
local ui_state = util.globals.ui_state(player)
local modal_data = ui_state.modal_data
local subfactory = ui_state.context.subfactory
local floor = Subfactory.get(subfactory, "Floor", modal_data.floor_id)
local line = Floor.get(floor, "Line", modal_data.line_id)
local item = Line.get(line, modal_data.item_class, modal_data.item_id)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
local current_amount, item_amount = item.amount, options.item_amount or item.amount
if item.class ~= "Ingredient" then
local other_class = (item.class == "Product") and "Byproduct" or "Product"
local corresponding_item = Line.get_by_type_and_name(relevant_line, other_class,
item.proto.type, item.proto.name)
if corresponding_item then -- Further adjustments if item is both product and byproduct
-- In either case, we need to consider the sum of both types as the current amount
current_amount = current_amount + corresponding_item.amount
-- If it's a byproduct, we want to set its amount to the exact number entered, which this does
if item.class == "Byproduct" then item_amount = item_amount + corresponding_item.amount end
end
end
relevant_line.percentage = (current_amount == 0) and 100
or (relevant_line.percentage * item_amount) / current_amount
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
end
end
local function handle_item_click(player, tags, action)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
local item = Line.get(line, tags.class, tags.item_id)
if action == "prioritize" then
if line.Product.count < 2 then
util.messages.raise(player, "warning", {"fp.warning_no_prioritizing_single_product"}, 1)
else
-- Remove the priority_product if the already selected one is clicked
line.priority_product_proto = (line.priority_product_proto ~= item.proto) and item.proto or nil
solver.update(player, context.subfactory)
util.raise.refresh(player, "subfactory", nil)
end
elseif action == "add_recipe_to_end" or action == "add_recipe_below" then
local production_type = (tags.class == "Byproduct") and "consume" or "produce"
local add_after_position = (action == "add_recipe_below") and line.gui_position or nil
util.raise.open_dialog(player, {dialog="recipe", modal_data={category_id=item.proto.category_id,
product_id=item.proto.id, floor_id=floor.id, production_type=production_type,
add_after_position=add_after_position}})
elseif action == "specify_amount" then
-- Set the view state so that the amount shown in the dialog makes sense
view_state.select(player, "items_per_timescale")
util.raise.refresh(player, "subfactory", nil)
local type_localised_string = {"fp.pl_" .. tags.class:lower(), 1}
local produce_consume = (tags.class == "Ingredient") and {"fp.consume"} or {"fp.produce"}
local modal_data = {
title = {"fp.options_item_title", type_localised_string},
text = {"fp.options_item_text", item.proto.localised_name},
submission_handler_name = "apply_item_options",
item_class = item.class, item_id = item.id,
floor_id = floor.id, line_id = line.id,
fields = {
{
type = "numeric_textfield",
name = "item_amount",
caption = {"fp.options_item_amount"},
tooltip = {"fp.options_item_amount_tt", type_localised_string, produce_consume},
text = item.amount,
width = 140,
focus = true
}
}
}
util.raise.open_dialog(player, {dialog="options", modal_data=modal_data})
elseif action == "copy" then
util.clipboard.copy(player, item)
elseif action == "put_into_cursor" then
util.cursor.add_to_item_combinator(player, item.proto, item.amount)
elseif action == "recipebook" then
util.open_in_recipebook(player, item.proto.type, item.proto.name)
end
end
local function handle_fuel_click(player, tags, action)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
local fuel = line.machine.fuel -- must exist to be able to get here
if action == "add_recipe_to_end" or action == "add_recipe_below" then
local add_after_position = (action == "add_recipe_below") and line.gui_position or nil
local category = PROTOTYPE_MAPS.items[fuel.proto.type]
local proto_id = category.members[fuel.proto.name].id
util.raise.open_dialog(player, {dialog="recipe", modal_data={category_id=category.id,
product_id=proto_id, floor_id=floor.id, production_type="produce",
add_after_position=add_after_position}})
elseif action == "edit" then -- fuel is changed through the machine dialog
util.raise.open_dialog(player, {dialog="machine", modal_data={floor_id=floor.id, line_id=line.id,
recipe_name=line.recipe.proto.localised_name}})
elseif action == "copy" then
util.clipboard.copy(player, fuel)
elseif action == "paste" then
util.clipboard.paste(player, fuel)
elseif action == "put_into_cursor" then
util.cursor.add_to_item_combinator(player, fuel.proto, fuel.amount)
elseif action == "recipebook" then
util.open_in_recipebook(player, fuel.proto.type, fuel.proto.name)
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "move_line",
handler = handle_line_move_click
},
{
name = "act_on_line_recipe",
modifier_actions = {
open_subfloor = {"left"}, -- does its own archive check
copy = {"shift-right"},
paste = {"shift-left", {archive_open=false}},
toggle = {"control-left", {archive_open=false}},
delete = {"control-right", {archive_open=false}},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_recipe_click
},
{
name = "act_on_line_machine",
modifier_actions = {
edit = {"right", {archive_open=false}},
copy = {"shift-right"},
paste = {"shift-left", {archive_open=false}},
reset_to_default = {"control-right", {archive_open=false}},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_machine_click
},
{
name = "add_machine_module",
handler = handle_machine_module_add
},
{
name = "act_on_line_beacon",
modifier_actions = {
edit = {"right", {archive_open=false}},
copy = {"shift-right"},
paste = {"shift-left", {archive_open=false}},
delete = {"control-right", {archive_open=false}},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_beacon_click
},
{
name = "add_line_beacon",
handler = handle_beacon_add
},
{
name = "act_on_line_module",
modifier_actions = {
edit = {"right", {archive_open=false}},
copy = {"shift-right"},
paste = {"shift-left", {archive_open=false}},
delete = {"control-right", {archive_open=false}},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_module_click
},
{
name = "act_on_line_product",
modifier_actions = {
prioritize = {"left", {archive_open=false, matrix_active=false}},
specify_amount = {"right", {archive_open=false, matrix_active=false}},
copy = {"shift-right"},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_click
},
{
name = "act_on_line_byproduct",
modifier_actions = {
add_recipe_to_end = {"left", {archive_open=false, matrix_active=true}},
add_recipe_below = {"control-left", {archive_open=false, matrix_active=true}},
specify_amount = {"right", {archive_open=false, matrix_active=false}},
copy = {"shift-right"},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_click
},
{
name = "act_on_line_ingredient",
modifier_actions = {
add_recipe_to_end = {"left", {archive_open=false}},
add_recipe_below = {"control-left", {archive_open=false}},
specify_amount = {"right", {archive_open=false, matrix_active=false}},
copy = {"shift-right"},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_click
},
{
name = "act_on_line_fuel",
modifier_actions = {
add_recipe_to_end = {"left", {archive_open=false}},
add_recipe_below = {"control-left", {archive_open=false}},
edit = {"right", {archive_open=false}},
copy = {"shift-right"},
paste = {"shift-left", {archive_open=false}},
put_into_cursor = {"alt-left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_fuel_click
}
},
on_gui_checked_state_changed = {
{
name = "checkmark_line",
handler = (function(player, tags, _)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
local line = Floor.get(floor, "Line", tags.line_id)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
relevant_line.done = not relevant_line.done
end)
}
},
on_gui_text_changed = {
{
name = "line_percentage",
handler = handle_percentage_change
},
{
name = "line_comment",
handler = (function(player, tags, event)
local context = util.globals.context(player)
local floor = Subfactory.get(context.subfactory, "Floor", tags.floor_id)
Floor.get(floor, "Line", tags.line_id).comment = event.element.text
end)
}
},
on_gui_confirmed = {
{
name = "line_percentage",
handler = handle_percentage_confirmation
}
}
}
listeners.global = {
apply_item_options = apply_item_options
}
return { listeners }

View File

@@ -0,0 +1,465 @@
-- ** LOCAL UTIL **
local function generate_metadata(player)
local ui_state = util.globals.ui_state(player)
local preferences = util.globals.preferences(player)
local subfactory = ui_state.context.subfactory
local metadata = {
archive_open = ui_state.flags.archive_open,
matrix_solver_active = (subfactory.matrix_free_items ~= nil),
fold_out_subfloors = preferences.fold_out_subfloors,
round_button_numbers = preferences.round_button_numbers,
pollution_column = preferences.pollution_column,
ingredient_satisfaction = preferences.ingredient_satisfaction,
view_state_metadata = view_state.generate_metadata(player, subfactory),
any_beacons_available = (next(global.prototypes.beacons) ~= nil),
line_count = nil, level = nil -- set dynamically per floor
}
if preferences.tutorial_mode then
util.actions.tutorial_tooltip_list(metadata, player, {
recipe_tutorial_tt = "act_on_line_recipe",
machine_tutorial_tt = "act_on_line_machine",
beacon_tutorial_tt = "act_on_line_beacon",
module_tutorial_tt = "act_on_line_module",
product_tutorial_tt = "act_on_line_product",
byproduct_tutorial_tt = "act_on_line_byproduct",
ingredient_tutorial_tt = "act_on_line_ingredient",
fuel_tutorial_tt = "act_on_line_fuel"
})
end
return metadata
end
-- ** BUILDERS **
local builders = {}
function builders.done(line, parent_flow, _)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
parent_flow.add{type="checkbox", state=relevant_line.done, mouse_button_filter={"left"},
tags={mod="fp", on_gui_checked_state_changed="checkmark_line", floor_id=line.parent.id, line_id=line.id}}
end
function builders.recipe(line, parent_flow, metadata, indent)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
local recipe_proto = relevant_line.recipe.proto
parent_flow.style.vertical_align = "center"
parent_flow.style.horizontal_spacing = 3
if indent > 0 then parent_flow.style.left_margin = indent * 18 end
local function create_move_button(flow, direction, disable)
local direction_enabled = (direction == "up" and line.gui_position ~= ((metadata.level > 1) and 2 or 1))
or (direction == "down" and line.gui_position < metadata.line_count)
local enabled = direction_enabled and (not metadata.archive_open and not disable)
local endpoint = (direction == "up") and {"fp.top"} or {"fp.bottom"}
local move_tooltip = (enabled) and {"fp.move_row_tt", {"fp.pl_recipe", 1}, {"fp." .. direction}, endpoint} or ""
flow.add{type="sprite-button", style="fp_button_move_row", sprite="fp_sprite_arrow_" .. direction,
tags={mod="fp", on_gui_click="move_line", direction=direction, floor_id=line.parent.id, line_id=line.id},
tooltip=move_tooltip, enabled=enabled, mouse_button_filter={"left"}}
end
local move_flow = parent_flow.add{type="flow", direction="vertical"}
move_flow.style.vertical_spacing = 0
move_flow.style.top_padding = 2
local first_subfloor_line = (line.parent.level > 1 and line.gui_position == 1)
create_move_button(move_flow, "up", first_subfloor_line)
create_move_button(move_flow, "down", first_subfloor_line)
local style, enabled, tutorial_tooltip = nil, true, ""
local note = "" ---@type LocalisedString
if first_subfloor_line then
style = "flib_slot_button_grey_small"
enabled = false -- first subfloor line is static
else
style = (relevant_line.active) and "flib_slot_button_default_small" or "flib_slot_button_red_small"
note = (relevant_line.active) and "" or {"fp.recipe_inactive"}
tutorial_tooltip = metadata.recipe_tutorial_tt
if line.subfloor then
style = (relevant_line.active) and "flib_slot_button_blue_small" or "flib_slot_button_purple_small"
note = {"fp.recipe_subfloor_attached"}
elseif line.recipe.production_type == "consume" then
style = (relevant_line.active) and "flib_slot_button_yellow_small" or "flib_slot_button_orange_small"
note = {"fp.recipe_consumes_byproduct"}
end
end
local first_line = (note == "") and {"fp.tt_title", recipe_proto.localised_name}
or {"fp.tt_title_with_note", recipe_proto.localised_name, note}
local tooltip = {"", first_line, line.effects_tooltip, tutorial_tooltip}
parent_flow.add{type="sprite-button", tags={mod="fp", on_gui_click="act_on_line_recipe", floor_id=line.parent.id,
line_id=line.id}, enabled=enabled, sprite=recipe_proto.sprite, tooltip=tooltip, style=style,
mouse_button_filter={"left-and-right"}}
end
function builders.percentage(line, parent_flow, metadata)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
local enabled = (not metadata.archive_open and not metadata.matrix_solver_active)
local textfield_percentage = parent_flow.add{type="textfield", text=tostring(relevant_line.percentage),
tags={mod="fp", on_gui_text_changed="line_percentage", on_gui_confirmed="line_percentage",
floor_id=line.parent.id, line_id=line.id}, enabled=enabled}
util.gui.setup_numeric_textfield(textfield_percentage, true, false)
textfield_percentage.style.horizontal_align = "center"
textfield_percentage.style.width = 55
end
local function add_module_flow(parent_flow, line, parent_type, metadata)
for _, module in ipairs(ModuleSet.get_in_order(line[parent_type].module_set)) do
local number_line = {"", "\n", module.amount, " ", {"fp.pl_module", module.amount}}
local tooltip = {"", {"fp.tt_title", module.proto.localised_name}, number_line, module.effects_tooltip,
metadata.module_tutorial_tt}
parent_flow.add{type="sprite-button", sprite=module.proto.sprite, tooltip=tooltip,
tags={mod="fp", on_gui_click="act_on_line_module", floor_id=line.parent.id, line_id=line.id,
parent_type=parent_type, module_id=module.id}, number=module.amount, style="flib_slot_button_default_small",
mouse_button_filter={"left-and-right"}}
end
end
function builders.machine(line, parent_flow, metadata)
local machine_count = line.machine.count
parent_flow.style.horizontal_spacing = 2
if line.subfloor then -- add a button that shows the total of all machines on the subfloor
-- Machine count doesn't need any special formatting in this case because it'll always be an integer
local tooltip = {"fp.subfloor_machine_count", machine_count, {"fp.pl_machine", machine_count}}
parent_flow.add{type="sprite-button", sprite="fp_generic_assembler", style="flib_slot_button_default_small",
enabled=false, number=machine_count, tooltip=tooltip}
else
local active, round_number = (line.production_ratio > 0), metadata.round_button_numbers
local count, tooltip_line = util.format.machine_count(machine_count, active, round_number)
local machine_limit = line.machine.limit
local style, note = "flib_slot_button_default_small", nil
if not metadata.matrix_solver_active and machine_limit ~= nil then
if line.machine.force_limit then
style = "flib_slot_button_pink_small"
note = {"fp.machine_limit_force", machine_limit}
elseif line.production_ratio < line.uncapped_production_ratio then
style = "flib_slot_button_orange_small"
note = {"fp.machine_limit_enforced", machine_limit}
else
style = "flib_slot_button_green_small"
note = {"fp.machine_limit_set", machine_limit}
end
end
if note ~= nil then table.insert(tooltip_line, {"", " - ", note}) end
local tooltip = {"", {"fp.tt_title", line.machine.proto.localised_name}, "\n", tooltip_line,
line.machine.effects_tooltip, metadata.machine_tutorial_tt}
parent_flow.add{type="sprite-button", style=style, sprite=line.machine.proto.sprite, number=count,
tags={mod="fp", on_gui_click="act_on_line_machine", floor_id=line.parent.id, line_id=line.id,
type="machine"}, tooltip=tooltip, mouse_button_filter={"left-and-right"}}
add_module_flow(parent_flow, line, "machine", metadata)
local module_set = line.machine.module_set
if module_set.module_limit > module_set.module_count then
local module_tooltip = {"", {"fp.add_machine_module"}, "\n", {"fp.shift_to_paste"}}
local button = parent_flow.add{type="sprite-button", sprite="utility/add", tooltip=module_tooltip,
tags={mod="fp", on_gui_click="add_machine_module", floor_id=line.parent.id, line_id=line.id},
style="fp_sprite-button_inset_add", mouse_button_filter={"left"}, enabled=(not metadata.archive_open)}
button.style.margin = 2
end
end
end
function builders.beacon(line, parent_flow, metadata)
-- Some mods might remove all beacons, in which case no beacon buttons should be added
if not metadata.any_beacons_available then return end
-- Beacons only work on machines that have some allowed_effects
if line.subfloor ~= nil or line.machine.proto.allowed_effects == nil then return end
local beacon = line.beacon
if beacon == nil then
local tooltip = {"", {"fp.add_beacon"}, "\n", {"fp.shift_to_paste"}}
local button = parent_flow.add{type="sprite-button", sprite="utility/add", tooltip=tooltip,
tags={mod="fp", on_gui_click="add_line_beacon", floor_id=line.parent.id, line_id=line.id},
style="fp_sprite-button_inset_add", mouse_button_filter={"left"}, enabled=(not metadata.archive_open)}
button.style.margin = 2
else
local plural_parameter = (beacon.amount == 1) and 1 or 2 -- needed because the amount can be decimal
local number_line = {"", "\n", beacon.amount, " ", {"fp.pl_beacon", plural_parameter}}
if beacon.total_amount then table.insert(number_line, {"", " - ", {"fp.in_total", beacon.total_amount}}) end
local tooltip = {"", {"fp.tt_title", beacon.proto.localised_name}, number_line, beacon.effects_tooltip,
metadata.beacon_tutorial_tt}
local button_beacon = parent_flow.add{type="sprite-button", sprite=beacon.proto.sprite, number=beacon.amount,
tags={mod="fp", on_gui_click="act_on_line_beacon", floor_id=line.parent.id, line_id=line.id, type="beacon"},
style="flib_slot_button_default_small", tooltip=tooltip, mouse_button_filter={"left-and-right"}}
if beacon.total_amount ~= nil then -- add a graphical hint that a beacon total is set
local sprite_overlay = button_beacon.add{type="sprite", sprite="fp_sprite_white_square"}
sprite_overlay.ignored_by_interaction = true
end
add_module_flow(parent_flow, line, "beacon", metadata)
end
end
function builders.power(line, parent_flow, metadata)
local pollution_line = (metadata.pollution_column) and ""
or {"", "\n", {"fp.pollution"}, ": ", util.format.SI_value(line.pollution, "P/m", 5)}
parent_flow.add{type="label", caption=util.format.SI_value(line.energy_consumption, "W", 3),
tooltip={"", util.format.SI_value(line.energy_consumption, "W", 5), pollution_line}}
end
function builders.pollution(line, parent_flow, _)
parent_flow.add{type="label", caption=util.format.SI_value(line.pollution, "P/m", 3),
tooltip=util.format.SI_value(line.pollution, "P/m", 5)}
end
function builders.products(line, parent_flow, metadata)
for _, product in ipairs(Line.get_in_order(line, "Product")) do
-- items/s/machine does not make sense for lines with subfloors, show items/s instead
local machine_count = (not line.subfloor) and line.machine.count or nil
local amount, number_tooltip = view_state.process_item(metadata.view_state_metadata,
product, nil, machine_count)
if amount == -1 then goto skip_product end -- an amount of -1 means it was below the margin of error
local style, note = "flib_slot_button_default_small", nil
if not line.subfloor and not metadata.matrix_solver_active then
-- We can check for identity because they reference the same table
if line.Product.count > 1 and line.priority_product_proto == product.proto then
style = "flib_slot_button_pink_small"
note = {"fp.priority_product"}
end
end
local name_line = (note == nil) and {"fp.tt_title", product.proto.localised_name}
or {"fp.tt_title_with_note", product.proto.localised_name, note}
local number_line = (number_tooltip) and {"", "\n", number_tooltip} or ""
local tooltip = {"", name_line, number_line, metadata.product_tutorial_tt}
parent_flow.add{type="sprite-button", tags={mod="fp", on_gui_click="act_on_line_product",
floor_id=line.parent.id, line_id=line.id, class="Product", item_id=product.id},
sprite=product.proto.sprite, style=style, number=amount,
tooltip=tooltip, mouse_button_filter={"left-and-right"}}
::skip_product::
end
end
function builders.byproducts(line, parent_flow, metadata)
for _, byproduct in ipairs(Line.get_in_order(line, "Byproduct")) do
-- items/s/machine does not make sense for lines with subfloors, show items/s instead
local machine_count = (not line.subfloor) and line.machine.count or nil
local amount, number_tooltip = view_state.process_item(metadata.view_state_metadata,
byproduct, nil, machine_count)
if amount == -1 then goto skip_byproduct end -- an amount of -1 means it was below the margin of error
local number_line = (number_tooltip) and {"", "\n", number_tooltip} or ""
local tooltip = {"", {"fp.tt_title", byproduct.proto.localised_name}, number_line,
metadata.byproduct_tutorial_tt}
parent_flow.add{type="sprite-button", tags={mod="fp", on_gui_click="act_on_line_byproduct",
floor_id=line.parent.id, line_id=line.id, class="Byproduct", item_id=byproduct.id},
sprite=byproduct.proto.sprite, style="flib_slot_button_red_small", number=amount,
tooltip=tooltip, mouse_button_filter={"left-and-right"}}
::skip_byproduct::
end
end
function builders.ingredients(line, parent_flow, metadata)
for _, ingredient in ipairs(Line.get_in_order(line, "Ingredient")) do
-- items/s/machine does not make sense for lines with subfloors, show items/s instead
local machine_count = (not line.subfloor) and line.machine.count or nil
local amount, number_tooltip = view_state.process_item(metadata.view_state_metadata,
ingredient, nil, machine_count)
if amount == -1 then goto skip_ingredient end -- an amount of -1 means it was below the margin of error
local style, enabled, note = "flib_slot_button_green_small", true, nil
local satisfaction_line = "" ---@type LocalisedString
if ingredient.proto.type == "entity" then
style = "flib_slot_button_transparent_small"
enabled = false
note = {"fp.raw_ore"}
elseif metadata.ingredient_satisfaction and ingredient.amount > 0 then
local satisfaction_percentage = (ingredient.satisfied_amount / ingredient.amount) * 100
local formatted_percentage = util.format.number(satisfaction_percentage, 3)
-- We use the formatted percentage here because it smooths out the number to 3 places
local satisfaction = tonumber(formatted_percentage)
if satisfaction <= 0 then
style = "flib_slot_button_red_small"
elseif satisfaction < 100 then
style = "flib_slot_button_yellow_small"
end -- else, it stays green
satisfaction_line = {"", "\n", (formatted_percentage .. "%"), " ", {"fp.satisfied"}}
end
local name_line = (note == nil) and {"fp.tt_title", ingredient.proto.localised_name}
or {"fp.tt_title_with_note", ingredient.proto.localised_name, note}
local number_line = (number_tooltip) and {"", "\n", number_tooltip} or ""
local tutorial_tt = (enabled) and metadata.ingredient_tutorial_tt or ""
local tooltip = {"", name_line, number_line, satisfaction_line, tutorial_tt}
parent_flow.add{type="sprite-button", tags={mod="fp", on_gui_click="act_on_line_ingredient",
floor_id=line.parent.id, line_id=line.id, class="Ingredient", item_id=ingredient.id},
sprite=ingredient.proto.sprite, style=style, number=amount, tooltip=tooltip,
enabled=enabled, mouse_button_filter={"left-and-right"}}
::skip_ingredient::
end
if not line.subfloor and line.machine.fuel then builders.fuel(line, parent_flow, metadata) end
end
-- This is not a standard builder function, as it gets called indirectly by the ingredient builder
function builders.fuel(line, parent_flow, metadata)
local fuel = line.machine.fuel
local amount, number_tooltip = view_state.process_item(metadata.view_state_metadata, fuel, nil, line.machine.count)
if amount == -1 then return end -- an amount of -1 means it was below the margin of error
local satisfaction_line = "" ---@type LocalisedString
if metadata.ingredient_satisfaction and fuel.amount > 0 then
local satisfaction_percentage = (fuel.satisfied_amount / fuel.amount) * 100
local formatted_percentage = util.format.number(satisfaction_percentage, 3)
satisfaction_line = {"", "\n", (formatted_percentage .. "%"), " ", {"fp.satisfied"}}
end
local name_line = {"fp.tt_title_with_note", fuel.proto.localised_name, {"fp.pl_fuel", 1}}
local number_line = (number_tooltip) and {"", "\n", number_tooltip} or ""
local tooltip = {"", name_line, number_line, satisfaction_line, metadata.fuel_tutorial_tt}
parent_flow.add{type="sprite-button", sprite=fuel.proto.sprite, style="flib_slot_button_cyan_small",
tags={mod="fp", on_gui_click="act_on_line_fuel", floor_id=line.parent.id, line_id=line.id},
number=amount, tooltip=tooltip, mouse_button_filter={"left-and-right"}}
end
function builders.line_comment(line, parent_flow, _)
local textfield_comment = parent_flow.add{type="textfield", tags={mod="fp", on_gui_text_changed="line_comment",
floor_id=line.parent.id, line_id=line.id}, text=(line.comment or "")}
textfield_comment.style.width = 250
util.gui.setup_textfield(textfield_comment)
end
local all_production_columns = {
-- name, caption, tooltip, alignment
{name="done", caption="", tooltip={"fp.column_done_tt"}, alignment="center"},
{name="recipe", caption={"fp.pu_recipe", 1}, alignment="left"},
{name="percentage", caption="%", tooltip={"fp.column_percentage_tt"}, alignment="center"},
{name="machine", caption={"fp.pu_machine", 1}, alignment="left"},
{name="beacon", caption={"fp.pu_beacon", 1}, alignment="left"},
{name="power", caption={"fp.u_power"}, alignment="center"},
{name="pollution", caption={"fp.pollution"}, alignment="center"},
{name="products", caption={"fp.pu_product", 2}, alignment="left"},
{name="byproducts", caption={"fp.pu_byproduct", 2}, alignment="left"},
{name="ingredients", caption={"fp.pu_ingredient", 2}, alignment="left"},
{name="line_comment", caption={"fp.column_comment"}, alignment="left"}
}
local function refresh_production_table(player)
local ui_state = util.globals.ui_state(player)
if ui_state.main_elements.main_frame == nil then return end
-- Determine the column_count first, because not all columns are nessecarily shown
local preferences = util.globals.preferences(player)
local subfactory = ui_state.context.subfactory
local production_table_elements = ui_state.main_elements.production_table
local subfactory_valid = (subfactory and subfactory.valid)
local any_lines_present = (subfactory_valid) and (subfactory.selected_floor.Line.count > 0) or false
production_table_elements.production_scroll_pane.visible = (subfactory_valid and any_lines_present)
if not subfactory_valid then return end
local production_columns = {}
for _, column_data in ipairs(all_production_columns) do
-- Explicit comparison needed here, as both true and nil columns should be shown
if preferences[column_data.name .. "_column"] ~= false then
table.insert(production_columns, column_data)
end
end
local scroll_pane_production = production_table_elements.production_scroll_pane
scroll_pane_production.clear()
local table_production = scroll_pane_production.add{type="table", column_count=(#production_columns+1),
style="fp_table_production"}
table_production.style.horizontal_spacing = 16
table_production.style.padding = {6, 0, 0, 12}
-- Column headers
for index, column_data in ipairs(production_columns) do
local caption = (column_data.tooltip) and {"fp.info_label", column_data.caption} or column_data.caption
local label_column = table_production.add{type="label", caption=caption, tooltip=column_data.tooltip,
style="bold_label"}
label_column.style.bottom_margin = 6
table_production.style.column_alignments[index] = column_data.alignment
end
-- Add pushers in both directions to make sure the table takes all available space
local flow_pusher = table_production.add{type="flow"}
flow_pusher.add{type="empty-widget", style="flib_vertical_pusher"}
flow_pusher.add{type="empty-widget", style="flib_horizontal_pusher"}
-- Generates some data that is relevant to several different builders
local metadata = generate_metadata(player)
-- Production lines
local function render_lines(floor, indent)
for _, line in ipairs(Floor.get_in_order(floor, "Line")) do
metadata.line_count = Floor.count(floor, "Line")
metadata.level = floor.level
for _, column_data in ipairs(production_columns) do
local flow = table_production.add{type="flow", direction="horizontal"}
builders[column_data.name](line, flow, metadata, indent)
end
table_production.add{type="empty-widget"}
if line.subfloor and metadata.fold_out_subfloors then render_lines(line.subfloor, indent + 1) end
end
end
render_lines(ui_state.context.floor, 0)
end
local function build_production_table(player)
local main_elements = util.globals.main_elements(player)
main_elements.production_table = {}
-- Can't do much here since the table needs to be destroyed on refresh anyways
local frame_vertical = main_elements.production_box.vertical_frame
local scroll_pane_production = frame_vertical.add{type="scroll-pane", direction="vertical",
style="flib_naked_scroll_pane_no_padding"}
scroll_pane_production.style.horizontally_stretchable = true
main_elements.production_table["production_scroll_pane"] = scroll_pane_production
refresh_production_table(player)
end
-- ** EVENTS **
local listeners = {}
listeners.misc = {
build_gui_element = (function(player, event)
if event.trigger == "main_dialog" then
build_production_table(player)
end
end),
refresh_gui_element = (function(player, event)
local triggers = {production_table=true, production_detail=true, production=true, subfactory=true, all=true}
if triggers[event.trigger] then refresh_production_table(player) end
end)
}
return { listeners }

View File

@@ -0,0 +1,322 @@
-- ** LOCAL UTIL **
local function repair_subfactory(player, _, _)
-- This function can only run is a subfactory is selected and invalid
local subfactory = util.globals.context(player).subfactory
Subfactory.repair(subfactory, player)
solver.update(player, subfactory)
util.raise.refresh(player, "all", nil) -- needs the full refresh to reset subfactory list buttons
end
local function change_timescale(player, new_timescale)
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
local old_timescale = subfactory.timescale
subfactory.timescale = new_timescale
-- Adjust the required_amount according to the new timescale
local timescale_ratio = (new_timescale / old_timescale)
for _, top_level_product in pairs(Subfactory.get_in_order(subfactory, "Product")) do
local required_amount = top_level_product.required_amount
-- No need to change amounts for belts/lanes, as timescale change does that implicitly
if required_amount.defined_by == "amount" then
required_amount.amount = required_amount.amount * timescale_ratio
end
end
solver.update(player, subfactory)
-- View state updates itself automatically if it detects a timescale change
util.raise.refresh(player, "subfactory", nil)
end
local function handle_solver_change(player, _, event)
local subfactory = util.globals.context(player).subfactory
local new_solver = (event.element.switch_state == "left") and "traditional" or "matrix"
if new_solver == "matrix" then
subfactory.matrix_free_items = {} -- 'activate' the matrix solver
else
subfactory.matrix_free_items = nil -- disable the matrix solver
subfactory.linearly_dependant = false
-- This function works its way through subfloors. Consuming recipes can't have subfloors though.
local any_lines_removed = false
local function remove_consuming_recipes(floor)
for _, line in pairs(Floor.get_in_order(floor, "Line")) do
if line.subfloor then
remove_consuming_recipes(line.subfloor)
elseif line.recipe.production_type == "consume" then
Floor.remove(floor, line)
any_lines_removed = true
end
end
end
-- The sequential solver doesn't like byproducts yet, so remove those lines
local top_floor = Subfactory.get(subfactory, "Floor", 1)
remove_consuming_recipes(top_floor)
if any_lines_removed then -- inform the user if any byproduct recipes are being removed
util.messages.raise(player, "hint", {"fp.hint_byproducts_removed"}, 1)
end
end
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
end
local function refresh_subfactory_info(player)
local ui_state = util.globals.ui_state(player)
if ui_state.main_elements.main_frame == nil then return end
local subfactory_info_elements = ui_state.main_elements.subfactory_info
local subfactory = ui_state.context.subfactory
local invalid_subfactory_selected = (subfactory and not subfactory.valid)
subfactory_info_elements.repair_flow.visible = invalid_subfactory_selected
local valid_subfactory_selected = (subfactory and subfactory.valid)
subfactory_info_elements.power_pollution_flow.visible = valid_subfactory_selected
subfactory_info_elements.info_flow.visible = valid_subfactory_selected
if invalid_subfactory_selected then
subfactory_info_elements.repair_label.tooltip = util.porter.format_modset_diff(subfactory.last_valid_modset)
elseif valid_subfactory_selected then -- we need to refresh some stuff in this case
local archive_open = ui_state.flags.archive_open
local matrix_solver_active = (subfactory.matrix_free_items ~= nil)
-- Power + Pollution
local label_power = subfactory_info_elements.power_label
label_power.caption = {"fp.bold_label", util.format.SI_value(subfactory.energy_consumption, "W", 3)}
label_power.tooltip = util.format.SI_value(subfactory.energy_consumption, "W", 5)
local label_pollution = subfactory_info_elements.pollution_label
label_pollution.caption = {"fp.bold_label", util.format.SI_value(subfactory.pollution, "P/m", 3)}
label_pollution.tooltip = util.format.SI_value(subfactory.pollution, "P/m", 5)
-- Timescale
for _, button in pairs(subfactory_info_elements.timescales_table.children) do
button.toggled = (subfactory.timescale == button.tags.timescale)
end
-- Mining Productivity
local custom_prod_set = subfactory.mining_productivity
if not custom_prod_set then -- only do this calculation when it'll actually be shown
local prod_bonus = util.format.number((player.force.mining_drill_productivity_bonus * 100), 4)
subfactory_info_elements.prod_bonus_label.caption = {"fp.bold_label", prod_bonus .. "%"}
end
subfactory_info_elements.prod_bonus_label.visible = not custom_prod_set
subfactory_info_elements.override_prod_bonus_button.enabled = (not archive_open)
subfactory_info_elements.override_prod_bonus_button.visible = not custom_prod_set
if custom_prod_set then -- only change the text when the textfield will actually be shown
subfactory_info_elements.prod_bonus_override_textfield.text = tostring(subfactory.mining_productivity)
end
subfactory_info_elements.prod_bonus_override_textfield.enabled = (not archive_open)
subfactory_info_elements.prod_bonus_override_textfield.visible = custom_prod_set
subfactory_info_elements.percentage_label.visible = custom_prod_set
-- Solver Choice
local switch_state = (matrix_solver_active) and "right" or "left"
subfactory_info_elements.solver_choice_switch.switch_state = switch_state
subfactory_info_elements.solver_choice_switch.enabled = (not archive_open)
subfactory_info_elements.configure_solver_button.enabled = (not archive_open and matrix_solver_active)
end
end
local function build_subfactory_info(player)
local main_elements = util.globals.main_elements(player)
main_elements.subfactory_info = {}
local parent_flow = main_elements.flows.left_vertical
local frame_vertical = parent_flow.add{type="frame", direction="vertical",
style="inside_shallow_frame_with_padding"}
frame_vertical.style.size = {MAGIC_NUMBERS.list_width, MAGIC_NUMBERS.info_height}
local flow_title = frame_vertical.add{type="flow", direction="horizontal"}
flow_title.style.margin = {-4, 0, 8, 0}
flow_title.add{type="label", caption={"fp.subfactory_info"}, style="caption_label"}
flow_title.add{type="empty-widget", style="flib_horizontal_pusher"}
-- Power + Pollution
local flow_power_pollution = flow_title.add{type="flow", direction="horizontal"}
main_elements.subfactory_info["power_pollution_flow"] = flow_power_pollution
local label_power_value = flow_power_pollution.add{type="label"}
main_elements.subfactory_info["power_label"] = label_power_value
flow_power_pollution.add{type="label", caption="|"}
local label_pollution_value = flow_power_pollution.add{type="label"}
main_elements.subfactory_info["pollution_label"] = label_pollution_value
-- Repair flow
local flow_repair = frame_vertical.add{type="flow", direction="vertical"}
flow_repair.style.top_margin = -2
main_elements.subfactory_info["repair_flow"] = flow_repair
local label_repair = flow_repair.add{type="label", caption={"fp.warning_with_icon", {"fp.subfactory_needs_repair"}}}
label_repair.style.single_line = false
main_elements.subfactory_info["repair_label"] = label_repair
local button_repair = flow_repair.add{type="button", tags={mod="fp", on_gui_click="repair_subfactory"},
caption={"fp.repair_subfactory"}, style="fp_button_rounded_mini", mouse_button_filter={"left"}}
button_repair.style.top_margin = 2
-- Subfactory info
local flow_info = frame_vertical.add{type="flow", direction="vertical"}
flow_info.style.vertical_spacing = 8
main_elements.subfactory_info["info_flow"] = flow_info
-- Timescale
local flow_timescale = flow_info.add{type="flow", direction="horizontal"}
flow_timescale.style.horizontal_spacing = 10
flow_timescale.style.vertical_align = "center"
flow_timescale.add{type="label", caption={"fp.info_label", {"fp.timescale"}}, tooltip={"fp.timescale_tt"}}
flow_timescale.add{type="empty-widget", style="flib_horizontal_pusher"}
local timescale_map = {[1] = "second", [60] = "minute", [3600] = "hour"}
local table_timescales = flow_timescale.add{type="table", column_count=table_size(timescale_map)}
table_timescales.style.horizontal_spacing = 0
main_elements.subfactory_info["timescales_table"] = table_timescales
for scale, name in pairs(timescale_map) do
local button = table_timescales.add{type="button", caption={"", "1", {"fp.unit_" .. name}},
tags={mod="fp", on_gui_click="change_timescale", timescale=scale},
style="fp_button_push", mouse_button_filter={"left"}}
button.style.width = 42
end
-- Mining productivity
local flow_mining_prod = flow_info.add{type="flow", direction="horizontal"}
flow_mining_prod.style.horizontal_spacing = 10
flow_mining_prod.style.vertical_align = "center"
flow_mining_prod.add{type="label", caption={"fp.info_label", {"fp.mining_productivity"}},
tooltip={"fp.mining_productivity_tt"}}
flow_mining_prod.add{type="empty-widget", style="flib_horizontal_pusher"}
local label_prod_bonus = flow_mining_prod.add{type="label"}
main_elements.subfactory_info["prod_bonus_label"] = label_prod_bonus
local button_override_prod_bonus = flow_mining_prod.add{type="button", caption={"fp.override"},
tags={mod="fp", on_gui_click="override_mining_prod"}, style="fp_button_rounded_mini",
mouse_button_filter={"left"}}
button_override_prod_bonus.style.disabled_font_color = {}
main_elements.subfactory_info["override_prod_bonus_button"] = button_override_prod_bonus
local textfield_prod_bonus = flow_mining_prod.add{type="textfield",
tags={mod="fp", on_gui_text_changed="mining_prod_override", on_gui_confirmed="mining_prod_override"}}
textfield_prod_bonus.style.size = {60, 26}
util.gui.setup_numeric_textfield(textfield_prod_bonus, true, true)
main_elements.subfactory_info["prod_bonus_override_textfield"] = textfield_prod_bonus
local label_percentage = flow_mining_prod.add{type="label", caption={"fp.bold_label", "%"}}
main_elements.subfactory_info["percentage_label"] = label_percentage
-- Solver Choice
local flow_solver_choice = flow_info.add{type="flow", direction="horizontal"}
flow_solver_choice.style.horizontal_spacing = 10
flow_solver_choice.style.vertical_align = "center"
flow_solver_choice.add{type="label", caption={"fp.info_label", {"fp.solver_choice"}},
tooltip={"fp.solver_choice_tt"}}
flow_solver_choice.add{type="empty-widget", style="flib_horizontal_pusher"}
local switch_solver_choice = flow_solver_choice.add{type="switch", right_label_caption={"fp.solver_choice_matrix"},
left_label_caption={"fp.solver_choice_traditional"},
tags={mod="fp", on_gui_switch_state_changed="solver_choice_changed"}}
main_elements.subfactory_info["solver_choice_switch"] = switch_solver_choice
local button_configure_solver = flow_solver_choice.add{type="sprite-button", sprite="utility/change_recipe",
tooltip={"fp.solver_choice_configure"}, tags={mod="fp", on_gui_click="configure_matrix_solver"},
style="fp_sprite-button_rounded_mini", mouse_button_filter={"left"}}
button_configure_solver.style.size = 26
button_configure_solver.style.padding = 0
main_elements.subfactory_info["configure_solver_button"] = button_configure_solver
refresh_subfactory_info(player)
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "repair_subfactory",
timeout = 20,
handler = repair_subfactory
},
{
name = "change_timescale",
handler = (function(player, tags, _)
change_timescale(player, tags.timescale)
end)
},
{
name = "override_mining_prod",
handler = (function(player, _, _)
local subfactory = util.globals.context(player).subfactory
subfactory.mining_productivity = 0
solver.update(player, subfactory)
util.raise.refresh(player, "subfactory", nil)
end)
},
{
name = "configure_matrix_solver",
handler = (function(player, _, _)
util.raise.open_dialog(player, {dialog="matrix", modal_data={configuration=true}})
end)
}
},
on_gui_text_changed = {
{
name = "mining_prod_override",
handler = (function(player, _, event)
local ui_state = util.globals.ui_state(player)
ui_state.context.subfactory.mining_productivity = tonumber(event.element.text)
ui_state.flags.recalculate_on_subfactory_change = true -- set flag to recalculate if necessary
end)
}
},
on_gui_switch_state_changed = {
{
name = "solver_choice_changed",
handler = handle_solver_change
}
},
on_gui_confirmed = {
{
name = "mining_prod_override",
handler = (function(player, _, _)
local ui_state = util.globals.ui_state(player)
ui_state.flags.recalculate_on_subfactory_change = false -- reset this flag as we refresh below
solver.update(player, ui_state.context.subfactory)
util.raise.refresh(player, "subfactory", nil)
end)
}
}
}
listeners.misc = {
build_gui_element = (function(player, event)
if event.trigger == "main_dialog" then
build_subfactory_info(player)
end
end),
refresh_gui_element = (function(player, event)
local triggers = {subfactory_info=true, subfactory=true, all=true}
if triggers[event.trigger] then refresh_subfactory_info(player) end
end)
}
return { listeners }

View File

@@ -0,0 +1,401 @@
-- ** LOCAL UTIL **
local function toggle_archive(player, _, _)
local player_table = util.globals.player_table(player)
local flags = player_table.ui_state.flags
flags.archive_open = not flags.archive_open
local factory = flags.archive_open and player_table.archive or player_table.factory
util.context.set_factory(player, factory)
util.raise.refresh(player, "all", nil)
end
-- Refresh the dialog, quitting archive view if it has become empty
local function refresh_after_subfactory_deletion(player, factory, removed_gui_position)
if removed_gui_position > factory.Subfactory.count then removed_gui_position = removed_gui_position - 1 end
local subfactory = Factory.get_by_gui_position(factory, "Subfactory", removed_gui_position)
util.context.set_subfactory(player, subfactory)
local archive_open = util.globals.flags(player).archive_open
if archive_open and Factory.count(factory, "Subfactory") == 0 then
-- Make sure the just-unarchived subfactory is the selected one in factory; It'll always be the last one
local main_factory = util.globals.player_table(player).factory
local last_position = Factory.count(main_factory, "Subfactory")
-- It's okay to set selected_subfactory directly here, as toggle_archive calls the proper context util function
main_factory.selected_subfactory = Factory.get_by_gui_position(main_factory, "Subfactory", last_position)
toggle_archive(player) -- does refreshing on its own
else
util.raise.refresh(player, "all", nil)
end
end
-- Delete subfactory for good and refresh interface if necessary
local function delete_subfactory_for_good(metadata)
local archive = metadata.subfactory.parent
local removed_gui_position = Factory.remove(archive, metadata.subfactory)
local player = game.get_player(metadata.player_index) ---@cast player -nil
if util.globals.main_elements(player).main_frame == nil then return end
if util.globals.flags(player).archive_open then
refresh_after_subfactory_deletion(player, archive, removed_gui_position)
else -- only need to refresh the archive button enabled state really
util.raise.refresh(player, "subfactory_list", nil)
end
end
local function archive_subfactory(player, _, _)
local player_table = util.globals.player_table(player)
local ui_state = player_table.ui_state
local subfactory = ui_state.context.subfactory
local archive_open = ui_state.flags.archive_open
local origin = archive_open and player_table.archive or player_table.factory
local destination = archive_open and player_table.factory or player_table.archive
-- Reset deletion if a deleted subfactory is un-archived
if archive_open and subfactory.tick_of_deletion then
util.nth_tick.cancel(subfactory.tick_of_deletion)
subfactory.tick_of_deletion = nil
end
local removed_gui_position = Factory.remove(origin, subfactory)
Factory.add(destination, subfactory) -- needs to be added after the removal else shit breaks
refresh_after_subfactory_deletion(player, origin, removed_gui_position)
end
local function add_subfactory(player, _, event)
local prefer_product_picker = util.globals.settings(player).prefer_product_picker
local function xor(a, b) return not a ~= not b end -- fancy, first time I ever needed this
if xor(event.shift, prefer_product_picker) then -- go right to the item picker with automatic subfactory naming
util.raise.open_dialog(player, {dialog="picker", modal_data={item_id=nil, item_category="product",
create_subfactory=true}})
else -- otherwise, have the user pick a subfactory name first
util.raise.open_dialog(player, {dialog="subfactory", modal_data={subfactory_id=nil}})
end
end
local function duplicate_subfactory(player, _, _)
local player_table = util.globals.player_table(player)
local context = player_table.ui_state.context
local archive_open = player_table.ui_state.flags.archive_open
local factory = player_table.factory
local clone = Subfactory.clone(context.subfactory)
local inserted_clone = nil
if archive_open then
inserted_clone = Factory.add(factory, clone)
toggle_archive(player, _, _)
else
inserted_clone = Factory.insert_at(factory, context.subfactory.gui_position+1, clone)
end
util.context.set_subfactory(player, inserted_clone)
solver.update(player, inserted_clone)
util.raise.refresh(player, "all", nil)
end
local function handle_move_subfactory_click(player, tags, event)
local context = util.globals.context(player)
local subfactory = Factory.get(context.factory, "Subfactory", tags.subfactory_id)
local spots_to_shift = (event.control) and 5 or ((not event.shift) and 1 or nil)
local translated_direction = (tags.direction == "up") and "negative" or "positive"
Factory.shift(context.factory, subfactory, 1, translated_direction, spots_to_shift)
util.raise.refresh(player, "subfactory_list", nil)
end
local function handle_subfactory_click(player, tags, action)
local ui_state = util.globals.ui_state(player)
local previous_subfactory = ui_state.context.subfactory
local selected_subfactory = Factory.get(ui_state.context.factory, "Subfactory", tags.subfactory_id)
util.context.set_subfactory(player, selected_subfactory)
if action == "select" then
if ui_state.flags.recalculate_on_subfactory_change then
-- This flag is set when a textfield is changed but not confirmed
ui_state.flags.recalculate_on_subfactory_change = false
solver.update(player, previous_subfactory)
end
util.raise.refresh(player, "all", nil)
elseif action == "edit" then
util.raise.refresh(player, "all", nil) -- refresh to update the selected subfactory
util.raise.open_dialog(player, {dialog="subfactory",
modal_data={subfactory_id=selected_subfactory.id}})
elseif action == "delete" then
subfactory_list.delete_subfactory(player)
end
end
local function refresh_subfactory_list(player)
local player_table = util.globals.player_table(player)
local flags, context = player_table.ui_state.flags, player_table.ui_state.context
local main_elements = player_table.ui_state.main_elements
if main_elements.main_frame == nil then return end
local selected_subfactory = context.subfactory
local subfactory_list_elements = main_elements.subfactory_list
local listbox = subfactory_list_elements.subfactory_listbox
listbox.clear()
if selected_subfactory ~= nil then -- only need to run this if any subfactory exists
local attach_subfactory_products = player_table.preferences.attach_subfactory_products
local subfactory_count = Factory.count(context.factory, "Subfactory")
local tutorial_tt = (player_table.preferences.tutorial_mode)
and util.actions.tutorial_tooltip("act_on_subfactory", nil, player) or nil
for _, subfactory in pairs(Factory.get_in_order(context.factory, "Subfactory")) do
local selected = (selected_subfactory.id == subfactory.id)
local caption, info_tooltip = Subfactory.tostring(subfactory, attach_subfactory_products, false)
local padded_caption = {"", " ", caption}
local tooltip = {"", info_tooltip, tutorial_tt}
-- Pretty sure this needs the 'using-spaces-to-shift-the-label'-hack, padding doesn't work
local subfactory_button = listbox.add{type="button", caption=padded_caption, tooltip=tooltip,
tags={mod="fp", on_gui_click="act_on_subfactory", subfactory_id=subfactory.id},
style="fp_button_fake_listbox_item", toggled=selected, mouse_button_filter={"left-and-right"}}
local function create_move_button(flow, direction)
local enabled = (direction == "up" and subfactory.gui_position ~= 1)
or (direction == "down" and subfactory.gui_position < subfactory_count)
local endpoint = (direction == "up") and {"fp.top"} or {"fp.bottom"}
local move_tooltip = (enabled) and {"fp.move_row_tt", {"fp.pl_subfactory", 1}, {"fp." .. direction}, endpoint} or ""
flow.add{type="sprite-button", style="fp_button_move_row", sprite="fp_sprite_arrow_" .. direction,
tags={mod="fp", on_gui_click="move_subfactory", direction=direction, subfactory_id=subfactory.id},
tooltip=move_tooltip, enabled=enabled, mouse_button_filter={"left"}}
end
local move_flow = subfactory_button.add{type="flow", direction="horizontal"}
move_flow.style.top_padding = 3
move_flow.style.horizontal_spacing = 0
create_move_button(move_flow, "up")
create_move_button(move_flow, "down")
end
end
-- Set all the button states and styles appropriately
local subfactory_exists = (selected_subfactory ~= nil)
local archive_open = (flags.archive_open)
local archived_subfactory_count = Factory.count(player_table.archive, "Subfactory")
subfactory_list_elements.toggle_archive_button.enabled = (archived_subfactory_count > 0)
subfactory_list_elements.toggle_archive_button.style = (archive_open)
and "flib_selected_tool_button" or "tool_button"
if not archive_open then
local subfactory_plural = {"fp.pl_subfactory", archived_subfactory_count}
local archive_tooltip = {"fp.action_open_archive_tt", (archived_subfactory_count > 0)
and {"fp.archive_filled", archived_subfactory_count, subfactory_plural} or {"fp.archive_empty"}}
subfactory_list_elements.toggle_archive_button.tooltip = archive_tooltip
else
subfactory_list_elements.toggle_archive_button.tooltip = {"fp.action_close_archive_tt"}
end
subfactory_list_elements.archive_button.enabled = (subfactory_exists)
subfactory_list_elements.archive_button.sprite = (archive_open)
and "utility/export_slot" or "utility/import_slot"
subfactory_list_elements.archive_button.tooltip = (archive_open)
and {"fp.action_unarchive_subfactory"} or {"fp.action_archive_subfactory"}
subfactory_list_elements.import_button.enabled = (not archive_open)
subfactory_list_elements.export_button.enabled = (subfactory_exists)
local prefer_product_picker = util.globals.settings(player).prefer_product_picker
subfactory_list_elements.add_button.enabled = (not archive_open)
subfactory_list_elements.add_button.tooltip = (prefer_product_picker)
and {"fp.action_add_subfactory_by_product"} or {"fp.action_add_subfactory_by_name"}
subfactory_list_elements.edit_button.enabled = (subfactory_exists)
subfactory_list_elements.duplicate_button.enabled = (selected_subfactory ~= nil and selected_subfactory.valid)
subfactory_list_elements.delete_button.enabled = (subfactory_exists)
local delay_in_minutes = math.floor(MAGIC_NUMBERS.subfactory_deletion_delay / 3600)
subfactory_list_elements.delete_button.tooltip = (archive_open)
and {"fp.action_delete_subfactory"} or {"fp.action_trash_subfactory", delay_in_minutes}
end
local function build_subfactory_list(player)
local main_elements = util.globals.main_elements(player)
main_elements.subfactory_list = {}
local parent_flow = main_elements.flows.left_vertical
local frame_vertical = parent_flow.add{type="frame", direction="vertical", style="inside_deep_frame"}
local row_count = util.globals.settings(player).subfactory_list_rows
frame_vertical.style.height = MAGIC_NUMBERS.subheader_height + (row_count * MAGIC_NUMBERS.list_element_height)
local subheader = frame_vertical.add{type="frame", direction="horizontal", style="subheader_frame"}
local button_toggle_archive = subheader.add{type="sprite-button", tags={mod="fp", on_gui_click="toggle_archive"},
sprite="fp_sprite_archive_dark", mouse_button_filter={"left"}}
main_elements.subfactory_list["toggle_archive_button"] = button_toggle_archive
local button_archive = subheader.add{type="sprite-button", tags={mod="fp", on_gui_click="archive_subfactory"},
style="tool_button", mouse_button_filter={"left"}}
main_elements.subfactory_list["archive_button"] = button_archive
subheader.add{type="empty-widget", style="flib_horizontal_pusher"}
local button_import = subheader.add{type="sprite-button", sprite="utility/import",
tooltip={"fp.action_import_subfactory"}, style="tool_button", mouse_button_filter={"left"},
tags={mod="fp", on_gui_click="subfactory_list_open_dialog", type="import"}}
main_elements.subfactory_list["import_button"] = button_import
local button_export = subheader.add{type="sprite-button", sprite="utility/export",
tooltip={"fp.action_export_subfactory"}, style="tool_button", mouse_button_filter={"left"},
tags={mod="fp", on_gui_click="subfactory_list_open_dialog", type="export"}}
main_elements.subfactory_list["export_button"] = button_export
subheader.add{type="empty-widget", style="flib_horizontal_pusher"}
local button_add = subheader.add{type="sprite-button", tags={mod="fp", on_gui_click="add_subfactory"},
sprite="utility/add", style="flib_tool_button_light_green", mouse_button_filter={"left"}}
main_elements.subfactory_list["add_button"] = button_add
local button_edit = subheader.add{type="sprite-button", tags={mod="fp", on_gui_click="edit_subfactory"},
sprite="utility/rename_icon_normal", tooltip={"fp.action_edit_subfactory"}, style="tool_button",
mouse_button_filter={"left"}}
main_elements.subfactory_list["edit_button"] = button_edit
local button_duplicate = subheader.add{type="sprite-button", tags={mod="fp", on_gui_click="duplicate_subfactory"},
sprite="utility/clone", tooltip={"fp.action_duplicate_subfactory"}, style="tool_button",
mouse_button_filter={"left"}}
main_elements.subfactory_list["duplicate_button"] = button_duplicate
local button_delete = subheader.add{type="sprite-button", tags={mod="fp", on_gui_click="delete_subfactory"},
sprite="utility/trash", style="tool_button_red", mouse_button_filter={"left"}}
main_elements.subfactory_list["delete_button"] = button_delete
-- This is not really a list-box, but it imitates one and allows additional features
local listbox_subfactories = frame_vertical.add{type="scroll-pane", style="fp_scroll-pane_fake_listbox"}
listbox_subfactories.style.width = MAGIC_NUMBERS.list_width
main_elements.subfactory_list["subfactory_listbox"] = listbox_subfactories
refresh_subfactory_list(player)
end
-- ** TOP LEVEL **
subfactory_list = {}
-- Utility function to centralize subfactory creation behavior
function subfactory_list.add_subfactory(player, name)
local subfactory = Subfactory.init(name)
local settings = util.globals.settings(player)
subfactory.timescale = settings.default_timescale
if settings.prefer_matrix_solver then subfactory.matrix_free_items = {} end
local context = util.globals.context(player)
Factory.add(context.factory, subfactory)
util.context.set_subfactory(player, subfactory)
return subfactory
end
-- Utility function to centralize subfactory deletion behavior
function subfactory_list.delete_subfactory(player)
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
if subfactory == nil then return end -- prevent crashes due to multiplayer latency
if ui_state.flags.archive_open then
if subfactory.tick_of_deletion then util.nth_tick.cancel(subfactory.tick_of_deletion) end
local factory = ui_state.context.factory
local removed_gui_position = Factory.remove(factory, subfactory)
refresh_after_subfactory_deletion(player, factory, removed_gui_position)
else
local desired_tick_of_deletion = game.tick + MAGIC_NUMBERS.subfactory_deletion_delay
local actual_tick_of_deletion = util.nth_tick.register(desired_tick_of_deletion,
"delete_subfactory_for_good", {player_index=player.index, subfactory=subfactory})
subfactory.tick_of_deletion = actual_tick_of_deletion
archive_subfactory(player)
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "toggle_archive",
handler = toggle_archive
},
{
name = "archive_subfactory",
handler = archive_subfactory
},
{ -- import/export buttons
name = "subfactory_list_open_dialog",
handler = (function(player, tags, _)
util.raise.open_dialog(player, {dialog=tags.type})
end)
},
{
name = "add_subfactory",
handler = add_subfactory
},
{
name = "edit_subfactory",
handler = (function(player, _, _)
local subfactory = util.globals.context(player).subfactory
util.raise.open_dialog(player, {dialog="subfactory",
modal_data={subfactory_id=subfactory.id}})
end)
},
{
name = "duplicate_subfactory",
handler = duplicate_subfactory
},
{
name = "delete_subfactory",
handler = subfactory_list.delete_subfactory
},
{
name = "move_subfactory",
handler = handle_move_subfactory_click
},
{
name = "act_on_subfactory",
modifier_actions = {
select = {"left"},
edit = {"right"},
delete = {"control-right"}
},
handler = handle_subfactory_click
}
}
}
listeners.misc = {
build_gui_element = (function(player, event)
if event.trigger == "main_dialog" then
build_subfactory_list(player)
end
end),
refresh_gui_element = (function(player, event)
local triggers = {subfactory_list=true, all=true}
if triggers[event.trigger] then refresh_subfactory_list(player) end
end)
}
listeners.global = {
delete_subfactory_for_good = delete_subfactory_for_good
}
return { listeners }

View File

@@ -0,0 +1,135 @@
-- ** LOCAL UTIL **
local function toggle_paused_state(player, _, _)
if not game.is_multiplayer() then
local preferences = util.globals.preferences(player)
preferences.pause_on_interface = not preferences.pause_on_interface
local main_elements = util.globals.main_elements(player)
local button_pause = main_elements.title_bar.pause_button
button_pause.toggled = (preferences.pause_on_interface)
main_dialog.set_pause_state(player, main_elements.main_frame)
end
end
local function refresh_title_bar(player)
local ui_state = util.globals.ui_state(player)
if ui_state.main_elements.main_frame == nil then return end
local subfactory = ui_state.context.subfactory
local title_bar_elements = ui_state.main_elements.title_bar
-- Disallow switching to compact view if the selected subfactory is nil or invalid
title_bar_elements.switch_button.enabled = (subfactory and subfactory.valid)
title_bar_elements.pause_button.enabled = (not game.is_multiplayer())
end
local function build_title_bar(player)
local main_elements = util.globals.main_elements(player)
main_elements.title_bar = {}
local parent_flow = main_elements.flows.top_horizontal
local flow_title_bar = parent_flow.add{type="flow", direction="horizontal",
tags={mod="fp", on_gui_click="re-center_main_dialog"}}
flow_title_bar.style.horizontal_spacing = 8
flow_title_bar.drag_target = main_elements.main_frame
-- The separator line causes the height to increase for some inexplicable reason, so we must hardcode it here
flow_title_bar.style.height = MAGIC_NUMBERS.title_bar_height
local button_switch = flow_title_bar.add{type="sprite-button", style="frame_action_button",
tags={mod="fp", on_gui_click="switch_to_compact_view"}, tooltip={"fp.switch_to_compact_view"},
sprite="fp_sprite_arrow_left_light", hovered_sprite="fp_sprite_arrow_left_dark",
clicked_sprite="fp_sprite_arrow_left_dark", mouse_button_filter={"left"}}
button_switch.style.padding = 2
main_elements.title_bar["switch_button"] = button_switch
flow_title_bar.add{type="label", caption={"mod-name.factoryplanner"}, style="frame_title",
ignored_by_interaction=true}
local drag_handle = flow_title_bar.add{type="empty-widget", style="flib_titlebar_drag_handle",
ignored_by_interaction=true}
drag_handle.style.minimal_width = 80
flow_title_bar.add{type="button", caption={"fp.tutorial"}, style="fp_button_frame_tool",
tags={mod="fp", on_gui_click="title_bar_open_dialog", type="tutorial"}, mouse_button_filter={"left"}}
flow_title_bar.add{type="button", caption={"fp.preferences"}, style="fp_button_frame_tool",
tags={mod="fp", on_gui_click="title_bar_open_dialog", type="preferences"}, mouse_button_filter={"left"}}
local separation = flow_title_bar.add{type="line", direction="vertical"}
separation.style.height = 24
local button_pause = flow_title_bar.add{type="button", caption={"fp.pause"}, tooltip={"fp.pause_on_interface"},
tags={mod="fp", on_gui_click="toggle_pause_game"}, style="fp_button_frame_tool", mouse_button_filter={"left"}}
main_elements.title_bar["pause_button"] = button_pause
local preferences = util.globals.preferences(player)
button_pause.toggled = (preferences.pause_on_interface)
local button_close = flow_title_bar.add{type="sprite-button", tags={mod="fp", on_gui_click="close_main_dialog"},
sprite="utility/close_white", hovered_sprite="utility/close_black", clicked_sprite="utility/close_black",
tooltip={"fp.close_interface"}, style="frame_action_button", mouse_button_filter={"left"}}
button_close.style.padding = 1
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "re-center_main_dialog",
handler = (function(player, _, event)
if event.button == defines.mouse_button_type.middle then
local ui_state = util.globals.ui_state(player)
local main_frame = ui_state.main_elements.main_frame
util.gui.properly_center_frame(player, main_frame, ui_state.main_dialog_dimensions)
end
end)
},
{
name = "switch_to_compact_view",
handler = (function(player, _, _)
main_dialog.toggle(player)
util.globals.flags(player).compact_view = true
compact_dialog.toggle(player)
end)
},
{
name = "close_main_dialog",
handler = (function(player, _, _)
main_dialog.toggle(player)
end)
},
{
name = "toggle_pause_game",
handler = toggle_paused_state
},
{
name = "title_bar_open_dialog",
handler = (function(player, tags, _)
util.raise.open_dialog(player, {dialog=tags.type})
end)
}
}
}
listeners.misc = {
fp_toggle_pause = (function(player, _)
if main_dialog.is_in_focus(player) then toggle_paused_state(player) end
end),
build_gui_element = (function(player, event)
if event.trigger == "main_dialog" then
build_title_bar(player)
end
end),
refresh_gui_element = (function(player, event)
local triggers = {title_bar=true, subfactory=true, all=true}
if triggers[event.trigger] then refresh_title_bar(player) end
end)
}
return { listeners }