-- This contains both the UI handling for view states, as well as the amount conversions local timescale_map = {[1] = "second", [60] = "minute", [3600] = "hour"} ---@class ViewStates: table -- ** LOCAL UTIL ** local function cycle_views(player, direction) local ui_state = util.globals.ui_state(player) if ui_state.view_states and main_dialog.is_in_focus(player) or compact_dialog.is_in_focus(player) then local selected_view_id, view_state_count = ui_state.view_states.selected_view_id, #ui_state.view_states local new_view_id = nil -- need to make sure this is wrapped properly in either direction if direction == "standard" then new_view_id = (selected_view_id == view_state_count) and 1 or (selected_view_id + 1) else -- direction == "reverse" new_view_id = (selected_view_id == 1) and view_state_count or (selected_view_id - 1) end view_state.select(player, new_view_id) local compact_view = util.globals.flags(player).compact_view local refresh = (compact_view) and "compact_subfactory" or "production" util.raise.refresh(player, refresh, nil) -- This avoids the game focusing a random textfield when pressing Tab to change states local main_frame = ui_state.main_elements.main_frame if main_frame ~= nil then main_frame.focus() end end end local processors = {} -- individual functions for each kind of view state function processors.items_per_timescale(metadata, raw_amount, item_proto, _) local number = util.format.number(raw_amount, metadata.formatting_precision) local plural_parameter = (number == "1") and 1 or 2 local type_string = (item_proto.type == "fluid") and {"fp.l_fluid"} or {"fp.pl_item", plural_parameter} local tooltip = {"", number, " ", type_string, "/", metadata.timescale_string} return number, tooltip end function processors.belts_or_lanes(metadata, raw_amount, item_proto, _) if item_proto.type == "entity" then return nil, nil end -- raw ores don't make sense here local divisor = (item_proto.type == "fluid") and 50 or 1 local raw_number = raw_amount * metadata.throughput_multiplier * metadata.timescale_inverse / divisor local number = util.format.number(raw_number, metadata.formatting_precision) local plural_parameter = (number == "1") and 1 or 2 local tooltip = {"", number, " ", {"fp.pl_" .. metadata.belt_or_lane, plural_parameter}} local return_number = (metadata.round_button_numbers) and math.ceil(raw_number - 0.001) or number return return_number, tooltip end function processors.wagons_per_timescale(metadata, raw_amount, item_proto, _) if item_proto.type == "entity" then return nil, nil end -- raw ores don't make sense here local wagon_capacity = (item_proto.type == "fluid") and metadata.fluid_wagon_capacity or metadata.cargo_wagon_capactiy * item_proto.stack_size local wagon_count = raw_amount / wagon_capacity local number = util.format.number(wagon_count, metadata.formatting_precision) local plural_parameter = (number == "1") and 1 or 2 local tooltip = {"", number, " ", {"fp.pl_wagon", plural_parameter}, "/", metadata.timescale_string} return number, tooltip end function processors.items_per_second_per_machine(metadata, raw_amount, item_proto, machine_count) if machine_count == 0 then return 0, "" end -- avoid division by zero if item_proto.type == "entity" then return nil, nil end -- raw ores don't make sense here local raw_number = raw_amount * metadata.timescale_inverse / (math.ceil((machine_count or 1) - 0.001)) local number = util.format.number(raw_number, metadata.formatting_precision) local plural_parameter = (number == "1") and 1 or 2 local type_string = (item_proto.type == "fluid") and {"fp.l_fluid"} or {"fp.pl_item", plural_parameter} -- If machine_count is nil, this shouldn't show /machine local per_machine = (machine_count ~= nil) and {"", "/", {"fp.pl_machine", 1}} or "" local tooltip = {"", number, " ", type_string, "/", {"fp.second"}, per_machine} return number, tooltip end local function refresh_view_state(player, table_view_state) local ui_state = util.globals.ui_state(player) -- Automatically detects a timescale change and refreshes the state if necessary local subfactory = ui_state.context.subfactory if not subfactory then return elseif subfactory.current_timescale ~= ui_state.view_states.timescale then view_state.rebuild_state(player) end for _, view_button in ipairs(table_view_state.children) do local view_state = ui_state.view_states[view_button.tags.view_id] view_button.caption = view_state.caption view_button.tooltip = view_state.tooltip view_button.toggled = (view_state.selected) end end local function build_view_state(player, parent_element) local view_states = util.globals.ui_state(player).view_states local table_view_state = parent_element.add{type="table", name="table_view_state", column_count=#view_states} table_view_state.style.horizontal_spacing = 0 -- Using ipairs is important as we only want to iterate the array-part for view_id, _ in ipairs(view_states) do local button = table_view_state.add{type="button", style="fp_button_push", mouse_button_filter={"left"}, tags={mod="fp", on_gui_click="change_view_state", view_id=view_id}} button.style.padding = {0, 12} end end -- ** TOP LEVEL ** view_state = {} -- Creates metadata relevant for a whole batch of items function view_state.generate_metadata(player, subfactory) local player_table = util.globals.player_table(player) local view_states = player_table.ui_state.view_states local current_view_name = view_states[view_states.selected_view_id].name local belts_or_lanes = player_table.settings.belts_or_lanes local round_button_numbers = player_table.preferences.round_button_numbers local throughput = prototyper.defaults.get(player, "belts").throughput local throughput_divisor = (belts_or_lanes == "belts") and throughput or (throughput / 2) local default_cargo_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["cargo-wagon"].id) local default_fluid_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["fluid-wagon"].id) return { processor = processors[current_view_name], timescale_inverse = 1 / subfactory.timescale, timescale_string = {"fp." .. timescale_map[subfactory.timescale]}, adjusted_margin_of_error = MAGIC_NUMBERS.margin_of_error * subfactory.timescale, belt_or_lane = belts_or_lanes:sub(1, -2), round_button_numbers = round_button_numbers, throughput_multiplier = 1 / throughput_divisor, formatting_precision = 4, cargo_wagon_capactiy = default_cargo_wagon.storage, fluid_wagon_capacity = default_fluid_wagon.storage } end function view_state.process_item(metadata, item, item_amount, machine_count) local raw_amount = item_amount or item.amount if raw_amount == nil or (raw_amount ~= 0 and raw_amount < metadata.adjusted_margin_of_error) then return -1, nil end return metadata.processor(metadata, raw_amount, item.proto, machine_count) end function view_state.rebuild_state(player) local ui_state = util.globals.ui_state(player) local subfactory = ui_state.context.subfactory -- If no subfactory exists yet, choose a default timescale so the UI can build properly local timescale = (subfactory) and timescale_map[subfactory.timescale] or "second" local singular_bol = util.globals.settings(player).belts_or_lanes:sub(1, -2) local belt_proto = prototyper.defaults.get(player, "belts") local default_cargo_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["cargo-wagon"].id) local default_fluid_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["fluid-wagon"].id) local new_view_states = { [1] = { name = "items_per_timescale", caption = {"", {"fp.pu_item", 2}, "/", {"fp.unit_" .. timescale}}, tooltip = {"fp.view_state_tt", {"fp.items_per_timescale", {"fp." .. timescale}}} }, [2] = { name = "belts_or_lanes", caption = {"", belt_proto.rich_text, " ", {"fp.pu_" .. singular_bol, 2}}, tooltip = {"fp.view_state_tt", {"fp.belts_or_lanes", {"fp.pl_" .. singular_bol, 2}, belt_proto.rich_text, belt_proto.localised_name}} }, [3] = { name = "wagons_per_timescale", caption = {"", {"fp.pu_wagon", 2}, "/", {"fp.unit_" .. timescale}}, tooltip = {"fp.view_state_tt", {"fp.wagons_per_timescale", {"fp." .. timescale}, default_cargo_wagon.rich_text, default_cargo_wagon.localised_name, default_fluid_wagon.rich_text, default_fluid_wagon.localised_name}} }, [4] = { name = "items_per_second_per_machine", caption = {"", {"fp.pu_item", 2}, "/", {"fp.unit_second"}, "/[img=fp_generic_assembler]"}, tooltip = {"fp.view_state_tt", {"fp.items_per_second_per_machine"}} }, selected_view_id = nil, -- set below timescale = timescale -- conserve the timescale to rebuild the state } -- Conserve the previous view selection if possible local old_view_states = ui_state.view_states local selected_view_id = (old_view_states) and old_view_states.selected_view_id or "items_per_timescale" ui_state.view_states = new_view_states view_state.select(player, selected_view_id) end function view_state.select(player, selected_view) local view_states = util.globals.ui_state(player).view_states -- Selected view can be either an id or a name, so we might need to match an id to a name local selected_view_id = selected_view if type(selected_view) == "string" then for view_id, view_state in ipairs(view_states) do if view_state.name == selected_view then selected_view_id = view_id break end end end -- Only run any code if the selected view did indeed change if view_states.selected_view_id ~= selected_view_id then for view_id, view_state in ipairs(view_states) do if view_id == selected_view_id then view_states.selected_view_id = selected_view_id view_state.selected = true else view_state.selected = false end end end end -- ** EVENTS ** local listeners = {} listeners.gui = { on_gui_click = { { name = "change_view_state", handler = (function(player, tags, _) view_state.select(player, tags.view_id) local compact_view = util.globals.flags(player).compact_view local refresh = (compact_view) and "compact_subfactory" or "production" util.raise.refresh(player, refresh, nil) end) } } } listeners.misc = { fp_cycle_production_views = (function(player, _) cycle_views(player, "standard") end), fp_reverse_cycle_production_views = (function(player, _) cycle_views(player, "reverse") end), build_gui_element = (function(player, event) if event.trigger == "view_state" then build_view_state(player, event.parent) end end), refresh_gui_element = (function(player, event) if event.trigger == "view_state" then refresh_view_state(player, event.element) end end) } return { listeners }