Aleksei-bird 7c9c708c92 Первый фикс
Пачки некоторых позиций увеличены
2024-03-01 20:54:33 +03:00

339 lines
15 KiB
Lua

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