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