local dictionary = require("__flib__.dictionary-lite") local gui = require("__flib__.gui") local migration = require("__flib__.migration") local on_tick_n = require("__flib__.on-tick-n") local table = require("__flib__.table") local constants = require("constants") local database = require("scripts.database") local formatter = require("scripts.formatter") local global_data = require("scripts.global-data") local migrations = require("scripts.migrations") local player_data = require("scripts.player-data") local remote_interface = require("scripts.remote-interface") local util = require("scripts.util") -- ----------------------------------------------------------------------------- -- GLOBALS INFO_GUI = require("scripts.gui.info.index") QUICK_REF_GUI = require("scripts.gui.quick-ref.index") SEARCH_GUI = require("scripts.gui.search.index") SETTINGS_GUI = require("scripts.gui.settings.index") --- Open the given page. --- @param player LuaPlayer --- @param player_table PlayerTable --- @param context Context --- @param options table? function OPEN_PAGE(player, player_table, context, options) options = options or {} --- @type InfoGui? local Gui if options.id then Gui = util.get_gui(player.index, "info", options.id) else _, Gui = next(INFO_GUI.find_open_context(player_table, context)) end if Gui and Gui.refs.root.visible then Gui:update_contents({ new_context = context }) else INFO_GUI.build(player, player_table, context, options) end end --- Refresh the contents of all Recipe Book GUIs. --- @param player LuaPlayer --- @param player_table PlayerTable --- @param skip_memoizer_purge boolean? function REFRESH_CONTENTS(player, player_table, skip_memoizer_purge) if not skip_memoizer_purge then formatter.create_cache(player.index) end --- @type table local info_guis = player_table.guis.info for id, InfoGui in pairs(info_guis) do if not constants.ignored_info_ids[id] and InfoGui.refs.window.valid then InfoGui:update_contents({ refresh = true }) end end --- @type table local quick_ref_guis = player_table.guis.quick_ref for _, QuickRefGui in pairs(quick_ref_guis) do if QuickRefGui.refs.window.valid then QuickRefGui:update_contents() end end --- @type SearchGui? local SearchGui = util.get_gui(player.index, "search") if SearchGui then SearchGui:dispatch("update_favorites") SearchGui:dispatch("update_history") if SearchGui.state.search_type == "textual" then SearchGui:dispatch("update_search_results") elseif SearchGui.state.search_type == "visual" then if SearchGui.refs.window.visible then SearchGui:update_visual_contents() else SearchGui.state.needs_visual_update = true end end end end -- ----------------------------------------------------------------------------- -- COMMANDS -- User commands commands.add_command("rb-refresh-all", { "command-help.rb-refresh-all" }, function(e) local player = game.get_player(e.player_index) --[[@as LuaPlayer]] if not player.admin then player.print({ "cant-run-command-not-admin", "rb-refresh-all" }) return end game.print("[color=red]REFRESHING RECIPE BOOK[/color]") game.print("Get comfortable, this could take a while!") on_tick_n.add(game.tick + 1, { action = "refresh_all" }) end) -- Debug commands commands.add_command("rb-print-object", nil, function(e) if not e.parameter then return end local player = game.get_player(e.player_index) --[[@as LuaPlayer]] if not player.admin then player.print({ "cant-run-command-not-admin", "rb-dump-data" }) return end local class, name = string.match(e.parameter, "^(.+) (.+)$") if not class or not name then player.print("Invalid arguments format") return end local obj = database[class] and database[class][name] if not obj then player.print("Not a valid object") return end if __DebugAdapter then __DebugAdapter.print(obj) player.print("Object data has been printed to the debug console.") else log(game.table_to_json(obj)) player.print("Object data has been printed to the log file.") end end) commands.add_command("rb-count-objects", nil, function(e) local player = game.get_player(e.player_index) --[[@as LuaPlayer]] if not player.admin then player.print({ "cant-run-command-not-admin", "rb-dump-data" }) return end for name, tbl in pairs(database) do if type(tbl) == "table" then local output = name .. ": " .. table_size(tbl) player.print(output) log(output) end end end) commands.add_command("rb-dump-database", nil, function(e) local player = game.get_player(e.player_index) --[[@as LuaPlayer]] if not player.admin then player.print({ "cant-run-command-not-admin", "rb-dump-data" }) return end if __DebugAdapter and (not e.parameter or #e.parameter == 0) then __DebugAdapter.print(database) game.print("Database has been dumped to the debug console.") else game.print("[color=red]DUMPING RECIPE BOOK DATABASE[/color]") game.print("Get comfortable, this could take a while!") on_tick_n.add( game.tick + 1, { action = "dump_database", player_index = e.player_index, raw = e.parameter == "raw" } ) end end) -- ----------------------------------------------------------------------------- -- EVENT HANDLERS -- BOOTSTRAP script.on_init(function() dictionary.on_init() on_tick_n.init() global_data.init() global_data.update_sync_data() global_data.build_prototypes() database.build() database.check_forces() for i, player in pairs(game.players) do player_data.init(i) player_data.refresh(player, global.players[i]) end end) script.on_load(function() formatter.create_all_caches() -- When mod configuration changes, don't bother to build anything because it'll have to be built again anyway if global_data.check_should_load() then database.build() database.check_forces() end -- Load GUIs for _, player_table in pairs(global.players) do local guis = player_table.guis if guis then for _, Gui in pairs(guis.quick_ref or {}) do QUICK_REF_GUI.load(Gui) end for id, Gui in pairs(guis.info or {}) do if not constants.ignored_info_ids[id] then INFO_GUI.load(Gui) end end if guis.search then SEARCH_GUI.load(guis.search) end if guis.settings then SETTINGS_GUI.load(guis.settings) end end end end) migration.handle_on_configuration_changed(migrations, function() dictionary.on_configuration_changed() global_data.update_sync_data() global_data.build_prototypes() database.build() database.check_forces() for i, player in pairs(game.players) do player_data.refresh(player, global.players[i]) end end) -- DICTIONARIES dictionary.handle_events() script.on_event(dictionary.on_player_dictionaries_ready, function(e) local player = game.get_player(e.player_index) --[[@as LuaPlayer]] --[[@as LuaPlayer]] local player_table = global.players[e.player_index] player_table.translations = dictionary.get_all(e.player_index) if player_table.flags.can_open_gui then REFRESH_CONTENTS(player, player_table) else -- Show message if needed if player_table.flags.show_message_after_translation then player.print({ "message.rb-can-open-gui" }) player_table.flags.show_message_after_translation = false end -- Create GUI SEARCH_GUI.build(player, player_table) -- Update flags player_table.flags.can_open_gui = true -- Enable shortcut player.set_shortcut_available("rb-search", true) end end) -- FORCE script.on_event(defines.events.on_force_created, function(e) if not global.forces or not database.generated then return end global_data.add_force(e.force) database.check_force(e.force) end) script.on_event({ defines.events.on_research_finished, defines.events.on_research_reversed }, function(e) -- This can be called by other mods before we get a chance to load if not global.players or not database.generated then return end if not database[constants.classes[1]] then return end database.handle_research_updated(e.research, e.name == defines.events.on_research_finished and true or nil) -- Refresh all GUIs to reflect finished research for _, player in pairs(e.research.force.players) do local player_table = global.players[player.index] if player_table and player_table.flags.can_open_gui then REFRESH_CONTENTS(player, player_table, true) end end end) -- GUI local function handle_gui_action(msg, e) local Gui = util.get_gui(e.player_index, msg.gui, msg.id) if Gui then Gui:dispatch(msg, e) end end local function read_gui_action(e) local msg = gui.read_action(e) if msg then handle_gui_action(msg, e) return true end return false end gui.hook_events(read_gui_action) script.on_event(defines.events.on_gui_click, function(e) -- If clicking on the Factory Planner dimmer frame if not read_gui_action(e) and e.element.style.name == "fp_frame_semitransparent" then -- Bring all GUIs to the front local player_table = global.players[e.player_index] if player_table.flags.can_open_gui then util.dispatch_all(e.player_index, "info", "bring_to_front") util.dispatch_all(e.player_index, "quick_ref", "bring_to_front") --- @type SearchGui? local SearchGui = util.get_gui(e.player_index, "search") if SearchGui and SearchGui.refs.window.visible then SearchGui:bring_to_front() end end end end) script.on_event(defines.events.on_gui_closed, function(e) if not read_gui_action(e) then local player = game.get_player(e.player_index) --[[@as LuaPlayer]] local player_table = global.players[e.player_index] if player_table.flags.technology_gui_open then player_table.flags.technology_gui_open = false local gui_data = player_table.guis.search if not gui_data.state.pinned then player.opened = gui_data.refs.window end elseif player_table.guis.info._relative_id then --- @type InfoGui? local InfoGui = util.get_gui(e.player_index, "info", player_table.guis.info._relative_id) if InfoGui then InfoGui:dispatch("close") end end end end) -- INTERACTION script.on_event(defines.events.on_lua_shortcut, function(e) if e.prototype_name == "rb-search" then local player = game.get_player(e.player_index) --[[@as LuaPlayer]] local player_table = global.players[e.player_index] local cursor_stack = player.cursor_stack if cursor_stack and cursor_stack.valid_for_read then local data = database.item[cursor_stack.name] if data then OPEN_PAGE(player, player_table, { class = "item", name = cursor_stack.name }) else -- If we're here, the selected object has no page in RB player.create_local_flying_text({ text = { "message.rb-object-has-no-page" }, create_at_cursor = true, }) player.play_sound({ path = "utility/cannot_build" }) end return end -- Open search GUI --- @type SearchGui? local SearchGui = util.get_gui(e.player_index, "search") if SearchGui then SearchGui:toggle() end end end) local entity_type_to_gui_type = { ["infinity-container"] = defines.relative_gui_type.container_gui, ["linked-container"] = defines.relative_gui_type.container_gui, ["logistic-container"] = defines.relative_gui_type.container_gui, } local function get_opened_relative_gui_type(player) local gui_type = player.opened_gui_type local opened = player.opened -- Attempt 1: Some GUIs can be converted straight from their gui_type local straight_conversion = defines.relative_gui_type[table.find(defines.gui_type, gui_type) .. "_gui"] if straight_conversion then return { gui = straight_conversion } end -- Attempt 2: Specific logic if gui_type == defines.gui_type.entity and opened.valid then local gui = defines.relative_gui_type[string.gsub(opened.type or "", "%-", "_") .. "_gui"] or entity_type_to_gui_type[opened.type] if gui then return { gui = gui, type = opened.type, name = opened.name } end end if gui_type == defines.gui_type.item and opened and opened.valid then -- Sometimes items don't show up!? if opened.object_name == "LuaEquipmentGrid" then return { gui = defines.relative_gui_type.equipment_grid_gui } else local gui = defines.relative_gui_type[string.gsub(opened.type, "%-", "_") .. "_gui"] or defines.relative_gui_type.item_with_inventory_gui if gui then return { gui = gui, type = opened.type, name = opened.name } end end end end script.on_event({ "rb-search", "rb-open-selected" }, function(e) local player = game.get_player(e.player_index) --[[@as LuaPlayer]] local player_table = global.players[e.player_index] if player_table.flags.can_open_gui then if e.input_name == "rb-open-selected" then -- Open the selected prototype local selected_prototype = e.selected_prototype if selected_prototype then -- Special case: Don't open selection tools if we're holding them if constants.ignored_cursor_inspection_types[selected_prototype.derived_type] and player.cursor_stack and player.cursor_stack.valid_for_read and player.cursor_stack.name == selected_prototype.name then return end local class = constants.type_to_class[selected_prototype.derived_type] or constants.type_to_class[selected_prototype.base_type] -- Not everything will have a Recipe Book entry if class then local name = selected_prototype.name local obj_data = database[class][name] if obj_data then local options if player_table.settings.general.interface.open_info_relative_to_gui then local id = player_table.guis.info._relative_id if id then options = { id = id } else -- Get the context of the current opened GUI local anchor = get_opened_relative_gui_type(player) if anchor then anchor.position = defines.relative_gui_position.right options = { parent = player.gui.relative, anchor = anchor } end end end local context = { class = class, name = name } OPEN_PAGE(player, player_table, context, options) return end end -- If we're here, the selected object has no page in RB player.create_local_flying_text({ text = { "message.rb-object-has-no-page" }, create_at_cursor = true, }) player.play_sound({ path = "utility/cannot_build" }) return end else --- @type SearchGui? local SearchGui = util.get_gui(e.player_index, "search") if SearchGui then SearchGui:toggle() end end else player.print({ "message.rb-cannot-open-gui" }) player_table.flags.show_message_after_translation = true end end) script.on_event( { "rb-navigate-backward", "rb-navigate-forward", "rb-return-to-home", "rb-jump-to-front", "rb-linked-focus-search" }, function(e) local player = game.get_player(e.player_index) --[[@as LuaPlayer]] local player_table = global.players[e.player_index] local opened = player.opened if player_table.flags.can_open_gui and player.opened_gui_type == defines.gui_type.custom and (not opened or (opened.valid and player.opened.name == "rb_search_window")) then local active_id = player_table.guis.info._active_id if active_id then --- @type InfoGui? local InfoGui = util.get_gui(e.player_index, "info", active_id) if InfoGui then if e.input_name == "rb-linked-focus-search" then InfoGui:dispatch({ action = "toggle_search" }) else local event_properties = constants.nav_event_properties[e.input_name] InfoGui:dispatch( { action = "navigate", delta = event_properties.delta }, { player_index = e.player_index, shift = event_properties.shift } ) end end end end end ) -- PLAYER script.on_event(defines.events.on_player_created, function(e) player_data.init(e.player_index) local player = game.get_player(e.player_index) --[[@as LuaPlayer]] local player_table = global.players[e.player_index] player_data.refresh(player, player_table) formatter.create_cache(e.player_index) end) script.on_event(defines.events.on_player_removed, function(e) player_data.remove(e.player_index) end) -- TICK script.on_event(defines.events.on_tick, function(e) dictionary.on_tick() local actions = on_tick_n.retrieve(e.tick) if actions then for _, msg in pairs(actions) do if msg.gui then handle_gui_action(msg, { player_index = msg.player_index }) elseif msg.action == "dump_database" then -- game.table_to_json() does not like functions local output = {} for key, value in pairs(database) do if type(value) ~= "function" then output[key] = value end end local func = msg.raw and serpent.dump or game.table_to_json game.write_file("rb-dump", func(output), false, msg.player_index) game.print("[color=green]Dumped database to script-output/rb-dump[/color]") elseif msg.action == "refresh_all" then dictionary.on_init() database.build() database.check_forces() for player_index, player in pairs(game.players) do local player_table = global.players[player_index] player_data.refresh(player, player_table) player_table.flags.show_message_after_translation = true end game.print("[color=green]Database refresh complete, retranslating dictionaries...[/color]") end end end end) -- ----------------------------------------------------------------------------- -- REMOTE INTERFACE remote.add_interface("RecipeBook", remote_interface)