392 lines
16 KiB
Lua
392 lines
16 KiB
Lua
require("backend.classes.Collection")
|
|
require("backend.classes.Factory")
|
|
require("backend.classes.Subfactory")
|
|
require("backend.classes.Floor")
|
|
require("backend.classes.Line")
|
|
require("backend.classes.Recipe")
|
|
require("backend.classes.Machine")
|
|
require("backend.classes.Beacon")
|
|
require("backend.classes.ModuleSet")
|
|
require("backend.classes.Module")
|
|
require("backend.classes.Item")
|
|
require("backend.classes.Fuel")
|
|
|
|
local loader = require("backend.handlers.loader")
|
|
local migrator = require("backend.handlers.migrator")
|
|
require("backend.handlers.prototyper")
|
|
require("backend.handlers.screenshotter")
|
|
|
|
require("backend.calculation.solver")
|
|
|
|
---@class PlayerTable
|
|
---@field preferences PreferencesTable
|
|
---@field settings SettingsTable
|
|
---@field ui_state UIStateTable
|
|
---@field mod_version VersionString
|
|
---@field index PlayerIndex
|
|
---@field factory FPFactory
|
|
---@field archive FPFactory
|
|
---@field translation_tables { [string]: TranslatedDictionary }?
|
|
---@field clipboard ClipboardEntry?
|
|
|
|
---@class PreferencesTable
|
|
---@field pause_on_interface boolean
|
|
---@field tutorial_mode boolean
|
|
---@field utility_scopes { components: "Subfactory" | "Floor" }
|
|
---@field recipe_filters { disabled: boolean, hidden: boolean }
|
|
---@field attach_subfactory_products boolean
|
|
---@field show_floor_items boolean
|
|
---@field fold_out_subfloors boolean
|
|
---@field ingredient_satisfaction boolean
|
|
---@field round_button_numbers boolean
|
|
---@field ignore_barreling_recipes boolean
|
|
---@field ignore_recycling_recipes boolean
|
|
---@field done_column boolean
|
|
---@field pollution_column boolean
|
|
---@field line_comment_column boolean
|
|
---@field mb_defaults MBDefaults
|
|
---@field default_prototypes DefaultPrototypes
|
|
|
|
---@class MBDefaults
|
|
---@field machine FPModulePrototype
|
|
---@field machine_secondary FPModulePrototype
|
|
---@field beacon FPBeaconPrototype
|
|
---@field beacon_count number
|
|
|
|
---@class DefaultPrototypes
|
|
---@field machines PrototypeWithCategoryDefault
|
|
---@field fuels PrototypeWithCategoryDefault
|
|
---@field belts PrototypeDefault
|
|
---@field wagons PrototypeWithCategoryDefault
|
|
---@field beacons PrototypeDefault
|
|
|
|
|
|
-- ** LOCAL UTIL **
|
|
---@param player LuaPlayer
|
|
local function reload_preferences(player)
|
|
-- Reloads the user preferences, incorporating previous preferences if possible
|
|
local preferences = global.players[player.index].preferences
|
|
|
|
preferences.pause_on_interface = preferences.pause_on_interface or false
|
|
if preferences.tutorial_mode == nil then preferences.tutorial_mode = true end
|
|
preferences.utility_scopes = preferences.utility_scopes or {components = "Subfactory"}
|
|
preferences.recipe_filters = preferences.recipe_filters or {disabled = false, hidden = false}
|
|
|
|
preferences.attach_subfactory_products = preferences.attach_subfactory_products or false
|
|
preferences.show_floor_items = preferences.show_floor_items or false
|
|
preferences.fold_out_subfloors = preferences.fold_out_subfloors or false
|
|
preferences.ingredient_satisfaction = preferences.ingredient_satisfaction or false
|
|
preferences.round_button_numbers = preferences.round_button_numbers or false
|
|
preferences.ignore_barreling_recipes = preferences.ignore_barreling_recipes or false
|
|
preferences.ignore_recycling_recipes = preferences.ignore_recycling_recipes or false
|
|
|
|
preferences.done_column = preferences.done_column or false
|
|
preferences.pollution_column = preferences.pollution_column or false
|
|
preferences.line_comment_column = preferences.line_comment_column or false
|
|
|
|
preferences.mb_defaults = preferences.mb_defaults
|
|
or {machine = nil, machine_secondary = nil, beacon = nil, beacon_count = nil}
|
|
|
|
preferences.default_prototypes = preferences.default_prototypes or {}
|
|
preferences.default_prototypes = {
|
|
machines = preferences.default_prototypes.machines or prototyper.defaults.get_fallback("machines"),
|
|
fuels = preferences.default_prototypes.fuels or prototyper.defaults.get_fallback("fuels"),
|
|
belts = preferences.default_prototypes.belts or prototyper.defaults.get_fallback("belts"),
|
|
wagons = preferences.default_prototypes.wagons or prototyper.defaults.get_fallback("wagons"),
|
|
beacons = preferences.default_prototypes.beacons or prototyper.defaults.get_fallback("beacons")
|
|
}
|
|
end
|
|
|
|
---@class SettingsTable
|
|
---@field show_gui_button boolean
|
|
---@field products_per_row integer
|
|
---@field subfactory_list_rows integer
|
|
---@field default_timescale integer
|
|
---@field belts_or_lanes string
|
|
---@field prefer_product_picker boolean
|
|
---@field prefer_matrix_solver boolean
|
|
|
|
---@param player LuaPlayer
|
|
local function reload_settings(player)
|
|
-- Writes the current user mod settings to their player_table, for read-performance
|
|
local settings = settings.get_player_settings(player)
|
|
local settings_table = {}
|
|
|
|
local timescale_to_number = {one_second = 1, one_minute = 60, one_hour = 3600}
|
|
|
|
settings_table.show_gui_button = settings["fp_display_gui_button"].value
|
|
settings_table.products_per_row = tonumber(settings["fp_products_per_row"].value)
|
|
settings_table.subfactory_list_rows = tonumber(settings["fp_subfactory_list_rows"].value)
|
|
settings_table.default_timescale = timescale_to_number[settings["fp_default_timescale"].value] ---@type integer
|
|
settings_table.belts_or_lanes = settings["fp_view_belts_or_lanes"].value
|
|
settings_table.prefer_product_picker = settings["fp_prefer_product_picker"].value
|
|
settings_table.prefer_matrix_solver = settings["fp_prefer_matrix_solver"].value
|
|
|
|
global.players[player.index].settings = settings_table
|
|
end
|
|
|
|
---@class UIStateTable
|
|
---@field main_dialog_dimensions DisplayResolution
|
|
---@field last_action string
|
|
---@field view_states ViewStates
|
|
---@field messages PlayerMessage[]
|
|
---@field main_elements { [string]: LuaGuiElement }
|
|
---@field compact_elements { [string]: LuaGuiElement }
|
|
---@field context Context
|
|
---@field last_selected_picker_group integer?
|
|
---@field modal_dialog_type ModalDialogType?
|
|
---@field modal_data ModalData?
|
|
---@field queued_dialog_metadata ModalData?
|
|
---@field flags UIStateFlags
|
|
|
|
---@class UIStateFlags
|
|
---@field archive_open boolean
|
|
---@field selection_mode boolean
|
|
---@field compact_view boolean
|
|
---@field recalculate_on_subfactory_change boolean
|
|
|
|
---@param player LuaPlayer
|
|
local function reset_ui_state(player)
|
|
local ui_state_table = {}
|
|
|
|
ui_state_table.main_dialog_dimensions = nil ---@type DisplayResolution Can only be calculated after on_init
|
|
ui_state_table.last_action = nil ---@type string The last user action (used for rate limiting)
|
|
ui_state_table.view_states = nil ---@type ViewStates The state of the production views
|
|
ui_state_table.messages = {} ---@type PlayerMessage[] The general message/warning list
|
|
ui_state_table.main_elements = {} -- References to UI elements in the main interface
|
|
ui_state_table.compact_elements = {} -- References to UI elements in the compact interface
|
|
ui_state_table.context = util.context.create(player) -- The currently displayed set of data
|
|
ui_state_table.last_selected_picker_group = nil ---@type integer The item picker category that was last selected
|
|
|
|
ui_state_table.modal_dialog_type = nil ---@type ModalDialogType The internal modal dialog type
|
|
ui_state_table.modal_data = nil ---@type ModalData Data that can be set for a modal dialog to use
|
|
ui_state_table.queued_dialog_metadata = nil ---@type ModalData Info on dialog to open after the current one closes
|
|
|
|
ui_state_table.flags = {
|
|
archive_open = false, -- Wether the players subfactory archive is currently open
|
|
selection_mode = false, -- Whether the player is currently using a selector
|
|
compact_view = false, -- Whether the user has switched to the compact main view
|
|
recalculate_on_subfactory_change = false -- Whether calculations should re-run
|
|
}
|
|
|
|
-- The UI table gets replaced because the whole interface is reset
|
|
global.players[player.index].ui_state = ui_state_table
|
|
end
|
|
|
|
|
|
---@param player LuaPlayer
|
|
local function create_player_table(player)
|
|
global.players[player.index] = {}
|
|
local player_table = global.players[player.index]
|
|
|
|
player_table.mod_version = global.mod_version
|
|
player_table.index = player.index
|
|
|
|
player_table.factory = Factory.init()
|
|
player_table.archive = Factory.init()
|
|
|
|
player_table.preferences = {}
|
|
reload_preferences(player)
|
|
|
|
reload_settings(player)
|
|
reset_ui_state(player)
|
|
|
|
util.messages.raise(player, "hint", {"fp.hint_tutorial"}, 12)
|
|
end
|
|
|
|
---@param player LuaPlayer
|
|
local function refresh_player_table(player)
|
|
local player_table = global.players[player.index]
|
|
|
|
reload_preferences(player)
|
|
reload_settings(player)
|
|
reset_ui_state(player)
|
|
|
|
-- This whole reset thing will be moved ...
|
|
local archive_subfactories = Factory.get_in_order(player_table.archive, "Subfactory")
|
|
player_table.archive.selected_subfactory = archive_subfactories[1] -- can be nil
|
|
|
|
local factory = player_table.factory
|
|
local subfactories = Factory.get_in_order(factory, "Subfactory")
|
|
local subfactory_to_select = subfactories[1] -- can be nil
|
|
if factory.selected_subfactory ~= nil then
|
|
-- Get the selected subfactory from the factory to make sure it still exists
|
|
local selected_subfactory = Factory.get(factory, "Subfactory", factory.selected_subfactory.id)
|
|
if selected_subfactory ~= nil then subfactory_to_select = selected_subfactory end
|
|
end
|
|
util.context.set_subfactory(player, subfactory_to_select)
|
|
|
|
player_table.translation_tables = nil
|
|
player_table.clipboard = nil
|
|
end
|
|
|
|
---@return FPSubfactory?
|
|
local function import_tutorial_subfactory()
|
|
local imported_tutorial_factory, error = util.porter.process_export_string(TUTORIAL_EXPORT_STRING)
|
|
if error then return nil end
|
|
return Factory.get(imported_tutorial_factory --[[@as FPSubfactory]], "Subfactory", 1)
|
|
end
|
|
|
|
|
|
local function global_init()
|
|
-- Set up a new save for development if necessary
|
|
local freeplay = remote.interfaces["freeplay"]
|
|
if DEV_ACTIVE and freeplay then -- Disable freeplay popup-message
|
|
if freeplay["set_skip_intro"] then remote.call("freeplay", "set_skip_intro", true) end
|
|
if freeplay["set_disable_crashsite"] then remote.call("freeplay", "set_disable_crashsite", true) end
|
|
end
|
|
|
|
-- Initiates all factorio-global variables
|
|
global.mod_version = script.active_mods["factoryplanner"] ---@type VersionString
|
|
global.players = {} ---@type { [PlayerIndex]: PlayerTable }
|
|
|
|
-- Save metadata about currently registered on_nth_tick events
|
|
global.nth_tick_events = {} ---@type { [Tick]: NthTickEvent }
|
|
|
|
prototyper.build() -- Generate all relevant prototypes and save them in global
|
|
loader.run(true) -- Run loader which creates useful caches of prototype data
|
|
|
|
-- Retain current modset to detect mod changes for subfactories that became invalid
|
|
global.installed_mods = script.active_mods ---@type ModToVersion
|
|
-- Import the tutorial subfactory so it's 'cached'
|
|
global.tutorial_subfactory = import_tutorial_subfactory()
|
|
|
|
-- Initialize flib's translation module
|
|
translator.on_init()
|
|
prototyper.util.build_translation_dictionaries()
|
|
|
|
-- Create player tables for all existing players
|
|
for _, player in pairs(game.players) do create_player_table(player) end
|
|
end
|
|
|
|
-- Prompts migrations, a GUI and prototype reload, and a validity check on all subfactories
|
|
local function handle_configuration_change()
|
|
prototyper.build() -- Run prototyper
|
|
loader.run(true) -- Re-run the loader to update with the new prototypes
|
|
|
|
migrator.migrate_global() -- Migrate global
|
|
|
|
-- Runs through all players, even new ones without player_table
|
|
for _, player in pairs(game.players) do
|
|
-- Migrate player_table data if it exists
|
|
migrator.migrate_player_table(player)
|
|
|
|
-- Create or update player_table
|
|
refresh_player_table(player)
|
|
local player_table = global.players[player.index]
|
|
|
|
-- Migrate the prototypes used in the player's preferences
|
|
prototyper.defaults.migrate(player_table)
|
|
prototyper.util.migrate_mb_defaults(player_table)
|
|
|
|
-- Update the validity of the entire factory and archive
|
|
Factory.validate(player_table.factory)
|
|
Factory.validate(player_table.archive)
|
|
end
|
|
|
|
global.installed_mods = script.active_mods
|
|
global.tutorial_subfactory = import_tutorial_subfactory()
|
|
|
|
translator.on_configuration_changed()
|
|
prototyper.util.build_translation_dictionaries()
|
|
|
|
for index, player in pairs(game.players) do
|
|
util.gui.reset_player(player) -- Destroys all existing GUI's
|
|
util.gui.toggle_mod_gui(player) -- Recreates the mod-GUI if necessary
|
|
|
|
-- Update factory and archive calculations in case prototypes changed in a relevant way
|
|
local player_table = global.players[index] ---@type PlayerTable
|
|
Factory.update_calculations(player_table.factory, player)
|
|
Factory.update_calculations(player_table.archive, player)
|
|
end
|
|
end
|
|
|
|
|
|
-- ** TOP LEVEL **
|
|
script.on_init(global_init)
|
|
|
|
script.on_configuration_changed(handle_configuration_change)
|
|
|
|
script.on_load(loader.run)
|
|
|
|
|
|
-- ** PLAYER DATA **
|
|
script.on_event(defines.events.on_player_created, function(event)
|
|
local player = game.get_player(event.player_index) ---@cast player -nil
|
|
|
|
-- Sets up the player_table for the new player
|
|
create_player_table(player)
|
|
|
|
-- Sets up the mod-GUI for the new player if necessary
|
|
util.gui.toggle_mod_gui(player)
|
|
|
|
-- Add the subfactories that are handy for development
|
|
if DEV_ACTIVE then util.porter.add_by_string(player, DEV_EXPORT_STRING) end
|
|
end)
|
|
|
|
script.on_event(defines.events.on_player_removed, function(event)
|
|
global.players[event.player_index] = nil
|
|
end)
|
|
|
|
|
|
script.on_event(defines.events.on_player_joined_game, translator.on_player_joined_game)
|
|
|
|
script.on_event(defines.events.on_runtime_mod_setting_changed, function(event)
|
|
if event.setting_type == "runtime-per-user" then -- this mod only has per-user settings
|
|
local player = game.get_player(event.player_index) ---@cast player -nil
|
|
reload_settings(player)
|
|
|
|
if event.setting == "fp_display_gui_button" then
|
|
util.gui.toggle_mod_gui(player)
|
|
|
|
elseif event.setting == "fp_products_per_row"
|
|
or event.setting == "fp_subfactory_list_rows"
|
|
or event.setting == "fp_prefer_product_picker" then
|
|
main_dialog.rebuild(player, false)
|
|
|
|
elseif event.setting == "fp_view_belts_or_lanes" then
|
|
local player_table = util.globals.player_table(player)
|
|
|
|
-- Goes through every subfactory's top level products and updates their defined_by
|
|
local defined_by = player_table.settings.belts_or_lanes
|
|
Factory.update_product_definitions(player_table.factory, defined_by)
|
|
Factory.update_product_definitions(player_table.archive, defined_by)
|
|
local subfactory = player_table.ui_state.context.subfactory
|
|
|
|
solver.update(player, subfactory)
|
|
main_dialog.rebuild(player, false)
|
|
end
|
|
end
|
|
end)
|
|
|
|
|
|
-- ** TRANSLATION **
|
|
-- Required by flib's translation module
|
|
script.on_event(defines.events.on_tick, translator.on_tick)
|
|
|
|
-- Keep translation going
|
|
script.on_event(defines.events.on_string_translated, translator.on_string_translated)
|
|
|
|
---@param event GuiEvent
|
|
local function dictionaries_ready(event)
|
|
local player = game.get_player(event.player_index) ---@cast player -nil
|
|
local player_table = util.globals.player_table(player)
|
|
|
|
player_table.translation_tables = translator.get_all(event.player_index)
|
|
modal_dialog.set_searchfield_state(player) -- enables searchfields if possible
|
|
end
|
|
|
|
-- Save translations once they are complete
|
|
script.on_event(translator.on_player_dictionaries_ready, dictionaries_ready)
|
|
|
|
|
|
-- ** COMMANDS **
|
|
commands.add_command("fp-reset-prototypes", {"command-help.fp_reset_prototypes"}, handle_configuration_change)
|
|
commands.add_command("fp-restart-translation", {"command-help.fp_restart_translation"}, function()
|
|
translator.on_init()
|
|
prototyper.util.build_translation_dictionaries()
|
|
end)
|
|
commands.add_command("fp-shrinkwrap-interface", {"command-help.fp_shrinkwrap_interface"}, function(command)
|
|
if command.player_index then main_dialog.shrinkwrap_interface(game.get_player(command.player_index)) end
|
|
end)
|