Первый фикс

Пачки некоторых позиций увеличены
This commit is contained in:
2024-03-01 20:53:32 +03:00
commit 7c9c708c92
23653 changed files with 767936 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
local _actions = {}
---@alias ActionLimitations { archive_open: boolean?, matrix_active: boolean?, recipebook: boolean? }
---@alias ActiveLimitations { archive_open: boolean, matrix_active: boolean, recipebook: boolean }
---@alias ActionList { [string]: string }
---@param player LuaPlayer
---@return ActiveLimitations
function _actions.current_limitations(player)
local ui_state = util.globals.ui_state(player)
return {
archive_open = ui_state.flags.archive_open,
matrix_active = (ui_state.context.subfactory.matrix_free_items ~= nil),
recipebook = RECIPEBOOK_ACTIVE
}
end
---@param action_limitations ActionLimitations[]
---@param active_limitations ActiveLimitations
---@return boolean
function _actions.allowed(action_limitations, active_limitations)
-- If a particular limitation is nil, it indicates that the action is allowed regardless
-- If it is non-nil, it needs to match the current state of the limitation exactly
for limitation_name, limitation in pairs(action_limitations) do
if active_limitations[limitation_name] ~= limitation then return false end
end
return true
end
---@param action_name string
---@param active_limitations ActiveLimitations
---@param player LuaPlayer?
---@return LocalisedString
function _actions.tutorial_tooltip(action_name, active_limitations, player)
active_limitations = active_limitations or util.actions.current_limitations(player)
local tooltip = {"", "\n"}
for _, action_line in pairs(TUTORIAL_TOOLTIPS[action_name]) do
if util.actions.allowed(action_line.limitations, active_limitations) then
table.insert(tooltip, action_line.string)
end
end
return tooltip
end
---@param data table
---@param player LuaPlayer
---@param action_list ActionList
function _actions.tutorial_tooltip_list(data, player, action_list)
local active_limitations = util.actions.current_limitations(player) -- done here so it's 'cached'
for reference_name, action_name in pairs(action_list) do
data[reference_name] = util.actions.tutorial_tooltip(action_name, active_limitations, nil)
end
end
function _actions.all_tutorial_tooltips(modifier_actions)
local action_lines = {}
for modifier_click, modifier_action in pairs(modifier_actions) do
local split_modifiers = util.split_string(modifier_click, "-")
local modifier_string = {""}
for _, modifier in pairs(fancytable.slice(split_modifiers, 1, -1)) do
table.insert(modifier_string, {"", {"fp.tut_" .. modifier}, " + "})
end
table.insert(modifier_string, {"fp.tut_" .. split_modifiers[#split_modifiers]})
local action_string = {"fp.tut_action_line", modifier_string, {"fp.tut_" .. modifier_action.name}}
table.insert(action_lines, {string=action_string, limitations=modifier_action.limitations})
end
return action_lines
end
-- Returns whether rate limiting is active for the given action, stopping it from proceeding
-- This is essentially to prevent duplicate commands in quick succession, enabled by lag
function _actions.rate_limited(player, tick, action_name, timeout)
local ui_state = util.globals.ui_state(player)
-- If this action has no timeout, reset the last action and allow it
if timeout == nil or game.tick_paused then
ui_state.last_action = nil
return false
end
local last_action = ui_state.last_action
-- Only disallow action under these specific circumstances
if last_action and last_action.action_name == action_name and (tick - last_action.tick) < timeout then
return true
else -- set the last action if this action will actually be carried out
ui_state.last_action = {
action_name = action_name,
tick = tick
}
return false
end
end
return _actions

View File

@@ -0,0 +1,59 @@
local _clipboard = {}
---@class ClipboardEntry
---@field class string
---@field object FPCopyableObject
---@field parent FPParentObject
-- Copies the given object into the player's clipboard as a packed object
---@param player LuaPlayer
---@param object FPCopyableObject
function _clipboard.copy(player, object)
local player_table = util.globals.player_table(player)
player_table.clipboard = {
class = object.class,
object = _G[object.class].pack(object),
parent = object.parent -- just used for unpacking, will remain a reference even if deleted elsewhere
}
util.cursor.create_flying_text(player, {"fp.copied_into_clipboard", {"fp.pu_" .. object.class:lower(), 1}})
end
-- Tries pasting the player's clipboard content onto the given target
---@param player LuaPlayer
---@param target FPCopyableObject
function _clipboard.paste(player, target)
local player_table = util.globals.player_table(player)
local clip = player_table.clipboard
if clip == nil then
util.cursor.create_flying_text(player, {"fp.clipboard_empty"})
else
local level = (clip.class == "Line") and (target.parent.level or 1) or nil
local clone = _G[clip.class].unpack(fancytable.deep_copy(clip.object), level)
clone.parent = clip.parent -- not very elegant to retain the parent here, but it's an easy solution
_G[clip.class].validate(clone)
local success, error = _G[target.class].paste(target, clone)
if success then -- objects in the clipboard are always valid since it resets on_config_changed
util.cursor.create_flying_text(player, {"fp.pasted_from_clipboard", {"fp.pu_" .. clip.class:lower(), 1}})
solver.update(player, player_table.ui_state.context.subfactory)
util.raise.refresh(player, "subfactory", nil)
else
local object_lower, target_lower = {"fp.pl_" .. clip.class:lower(), 1}, {"fp.pl_" .. target.class:lower(), 1}
if error == "incompatible_class" then
util.cursor.create_flying_text(player, {"fp.clipboard_incompatible_class", object_lower, target_lower})
elseif error == "incompatible" then
util.cursor.create_flying_text(player, {"fp.clipboard_incompatible", object_lower})
elseif error == "already_exists" then
util.cursor.create_flying_text(player, {"fp.clipboard_already_exists", target_lower})
elseif error == "no_empty_slots" then
util.cursor.create_flying_text(player, {"fp.clipboard_no_empty_slots"})
elseif error == "recipe_irrelevant" then
util.cursor.create_flying_text(player, {"fp.clipboard_recipe_irrelevant"})
end
end
end
end
return _clipboard

View File

@@ -0,0 +1,73 @@
local _context = {}
---@class Context
---@field factory FPFactory
---@field subfactory FPSubfactory?
---@field floor FPFloor?
-- Creates a blank context referencing which part of the Factory is currently displayed
---@param player LuaPlayer
---@return Context context
function _context.create(player)
return {
factory = global.players[player.index].factory,
subfactory = nil,
floor = nil
}
end
-- Updates the context to match the newly selected factory
---@param player LuaPlayer
---@param factory FPFactory
function _context.set_factory(player, factory)
local context = util.globals.context(player)
context.factory = factory
local subfactory = factory.selected_subfactory
or Factory.get_by_gui_position(factory, "Subfactory", 1) -- might be nil
util.context.set_subfactory(player, subfactory)
end
-- Updates the context to match the newly selected subfactory
---@param player LuaPlayer
---@param subfactory FPSubfactory?
function _context.set_subfactory(player, subfactory)
local context = util.globals.context(player)
context.factory.selected_subfactory = subfactory
context.subfactory = subfactory
context.floor = (subfactory ~= nil) and subfactory.selected_floor or nil
end
-- Updates the context to match the newly selected floor
---@param player LuaPlayer
---@param floor FPFloor
function _context.set_floor(player, floor)
local context = util.globals.context(player)
context.subfactory.selected_floor = floor
context.floor = floor
end
-- Changes the context to the floor indicated by the given destination
---@param player LuaPlayer
---@param destination "up" | "down"
---@return boolean success
function _context.change_floor(player, destination)
local context = util.globals.context(player)
local subfactory, floor = context.subfactory, context.floor
if subfactory == nil or floor == nil then return false end
local selected_floor = nil ---@type FPFloor
if destination == "up" and floor.level > 1 then
selected_floor = floor.origin_line.parent
elseif destination == "top" then
selected_floor = Subfactory.get(subfactory, "Floor", 1)
end
if selected_floor ~= nil then
util.context.set_floor(player, selected_floor)
-- Reset the subfloor we moved from if it doesn't have any additional recipes
if Floor.count(floor, "Line") < 2 then Floor.reset(floor) end
end
return (selected_floor ~= nil)
end
return _context

View File

@@ -0,0 +1,138 @@
local _cursor = {}
---@param player LuaPlayer
---@param blueprint_entities BlueprintEntity[]
local function set_cursor_blueprint(player, blueprint_entities)
local script_inventory = game.create_inventory(1)
local blank_slot = script_inventory[1]
blank_slot.set_stack{name="fp_cursor_blueprint"}
blank_slot.set_blueprint_entities(blueprint_entities)
player.add_to_clipboard(blank_slot)
player.activate_paste()
script_inventory.destroy()
end
---@param player LuaPlayer
---@param text LocalisedString
function _cursor.create_flying_text(player, text)
player.create_local_flying_text{text=text, create_at_cursor=true}
end
---@param player LuaPlayer
---@param line FPLine
---@param object FPMachine | FPBeacon
---@return boolean success
function _cursor.set_entity(player, line, object)
local entity_prototype = game.entity_prototypes[object.proto.name]
if entity_prototype.has_flag("not-blueprintable") or not entity_prototype.has_flag("player-creation")
or entity_prototype.items_to_place_this == nil then
util.cursor.create_flying_text(player, {"fp.put_into_cursor_failed", entity_prototype.localised_name})
return false
end
local module_list = {}
for _, module in pairs(ModuleSet.get_in_order(object.module_set)) do
module_list[module.proto.name] = module.amount
end
local blueprint_entity = {
entity_number = 1,
name = object.proto.name,
position = {0, 0},
items = module_list,
recipe = (object.class == "Machine") and line.recipe.proto.name or nil
}
set_cursor_blueprint(player, {blueprint_entity})
return true
end
---@param player LuaPlayer
---@param items { [string]: number }
---@return boolean success
function _cursor.set_item_combinator(player, items)
local combinator_proto = game.entity_prototypes["constant-combinator"]
if combinator_proto == nil then
util.cursor.create_flying_text(player, {"fp.blueprint_no_combinator_prototype"})
return false
elseif not next(items) then
util.cursor.create_flying_text(player, {"fp.impossible_to_blueprint_fluid"})
return false
end
local filter_limit = combinator_proto.item_slot_count
local blueprint_entities = {} ---@type BlueprintEntity[]
local current_combinator, current_filter_count = nil, 0
local next_entity_number, next_position = 1, {0, 0}
for proto_name, item_amount in pairs(items) do
if not current_combinator or current_filter_count == filter_limit then
current_combinator = {
entity_number = next_entity_number,
name = "constant-combinator",
tags = {fp_item_combinator = true},
position = next_position,
control_behavior = {filters = {}},
connections = {{green = {}}} -- filled in below
}
table.insert(blueprint_entities, current_combinator)
next_entity_number = next_entity_number + 1
next_position = {next_position[1] + 1, 0}
current_filter_count = 0
end
current_filter_count = current_filter_count + 1
table.insert(current_combinator.control_behavior.filters, {
signal = {type = 'item', name = proto_name},
count = math.max(item_amount, 1), -- make sure amounts < 1 are not excluded
index = current_filter_count
})
end
---@param main_entity BlueprintEntity
---@param other_entity BlueprintEntity
local function connect_if_entity_exists(main_entity, other_entity)
if other_entity ~= nil then
local entry = {entity_id = other_entity.entity_number}
table.insert(main_entity.connections[1].green, entry)
end
end
for index, entity in ipairs(blueprint_entities) do
connect_if_entity_exists(entity, blueprint_entities[index-1])
if not next(entity.connections[1].green) then entity.connections = nil end
end
set_cursor_blueprint(player, blueprint_entities)
return true
end
---@param player LuaPlayer
---@param proto FPItemPrototype | FPFuelPrototype
---@param amount number
function _cursor.add_to_item_combinator(player, proto, amount)
if proto.type ~= "item" then
util.cursor.create_flying_text(player, {"fp.impossible_to_blueprint_fluid"})
return
end
local items = {}
local blueprint_entities = player.get_blueprint_entities()
if blueprint_entities ~= nil then
for _, entity in pairs(blueprint_entities) do
if entity.tags ~= nil and entity.tags["fp_item_combinator"] then
for _, filter in pairs(entity.control_behavior.filters) do
items[filter.signal.name] = filter.count
end
end
end
end
items[proto.name] = (items[proto.name] or 0) + amount
util.cursor.set_item_combinator(player, items) -- don't care about success here
end
return _cursor

View File

@@ -0,0 +1,88 @@
local _format = {}
-- Formats given number to given number of significant digits
---@param number number
---@param precision integer
---@return string formatted_number
function _format.number(number, precision)
-- To avoid scientific notation, chop off the decimals points for big numbers
if (number / (10 ^ precision)) >= 1 then
return ("%d"):format(number)
else
-- Set very small numbers to 0
if number < (0.1 ^ precision) then
number = 0
-- Decrease significant digits for every zero after the decimal point
-- This keeps the number of digits after the decimal point constant
elseif number < 1 then
local n = number
while n < 1 do
precision = precision - 1
n = n * 10
end
end
-- Show the number in the shortest possible way
return ("%." .. precision .. "g"):format(number)
end
end
-- Returns string representing the given power
---@param value number
---@param unit string
---@param precision integer
---@return LocalisedString formatted_number
function _format.SI_value(value, unit, precision)
local prefixes = {"", "kilo", "mega", "giga", "tera", "peta", "exa", "zetta", "yotta"}
local units = {
["W"] = {"fp.unit_watt"},
["J"] = {"fp.unit_joule"},
["P/m"] = {"", {"fp.unit_pollution"}, "/", {"fp.unit_minute"}}
}
local sign = (value >= 0) and "" or "-"
value = math.abs(value) or 0
local scale_counter = 0
-- Determine unit of the energy consumption, while keeping the result above 1 (ie no 0.1kW, but 100W)
while scale_counter < #prefixes and value > (1000 ^ (scale_counter + 1)) do
scale_counter = scale_counter + 1
end
-- Round up if energy consumption is close to the next tier
if (value / (1000 ^ scale_counter)) > 999 then
scale_counter = scale_counter + 1
end
value = value / (1000 ^ scale_counter)
local prefix = (scale_counter == 0) and "" or {"fp.prefix_" .. prefixes[scale_counter + 1]}
return {"", sign .. util.format.number(value, precision) .. " ", prefix, units[unit]}
end
---@param count number
---@param active boolean
---@param round_number boolean
---@return string formatted_count
---@return LocalisedString tooltip_line
function _format.machine_count(count, active, round_number)
-- The formatting is used to 'round down' when the decimal is very small
local formatted_count = util.format.number(count, 3)
local tooltip_count = formatted_count
-- If the formatting returns 0, it is a very small number, so show it as 0.001
if formatted_count == "0" and active then
tooltip_count = "≤0.001"
formatted_count = "0.01" -- shows up as 0.0 on the button
end
if round_number then formatted_count = tostring(math.ceil(formatted_count --[[@as number]])) end
local plural_parameter = (tooltip_count == "1") and 1 or 2
local tooltip_line = {"", tooltip_count, " ", {"fp.pl_machine", plural_parameter}}
return formatted_count, tooltip_line
end
return _format

View File

@@ -0,0 +1,39 @@
local _globals = {}
---@param player LuaPlayer
---@return PlayerTable
function _globals.player_table(player) return global.players[player.index] end
---@param player LuaPlayer
---@return SettingsTable
function _globals.settings(player) return global.players[player.index].settings end
---@param player LuaPlayer
---@return PreferencesTable
function _globals.preferences(player) return global.players[player.index].preferences end
---@param player LuaPlayer
---@return UIStateTable
function _globals.ui_state(player) return global.players[player.index].ui_state end
---@param player LuaPlayer
---@return Context
function _globals.context(player) return global.players[player.index].ui_state.context end
---@param player LuaPlayer
---@return UIStateFlags
function _globals.flags(player) return global.players[player.index].ui_state.flags end
---@param player LuaPlayer
---@return ModalData
function _globals.modal_data(player) return global.players[player.index].ui_state.modal_data end
---@param player LuaPlayer
---@return LuaGuiElement[]
function _globals.main_elements(player) return global.players[player.index].ui_state.main_elements end
---@param player LuaPlayer
---@return LuaGuiElement[]
function _globals.modal_elements(player) return global.players[player.index].ui_state.modal_data.modal_elements end
return _globals

View File

@@ -0,0 +1,169 @@
local mod_gui = require("mod-gui")
local _gui = { switch = {}, mod = {} }
-- Adds an on/off-switch including a label with tooltip to the given flow
-- Automatically converts boolean state to the appropriate switch_state
---@param parent_flow LuaGuiElement
---@param action string
---@param additional_tags Tags
---@param state SwitchState
---@param caption LocalisedString?
---@param tooltip LocalisedString?
---@param label_first boolean?
---@return LuaGuiElement created_switch
function _gui.switch.add_on_off(parent_flow, action, additional_tags, state, caption, tooltip, label_first)
if type(state) == "boolean" then state = util.gui.switch.convert_to_state(state) end
local flow = parent_flow.add{type="flow", direction="horizontal"}
flow.style.vertical_align = "center"
local switch, label ---@type LuaGuiElement, LuaGuiElement
local function add_switch()
additional_tags.mod = "fp"; additional_tags.on_gui_switch_state_changed = action
switch = flow.add{type="switch", tags=additional_tags, switch_state=state,
left_label_caption={"fp.on"}, right_label_caption={"fp.off"}}
end
local function add_label()
caption = (tooltip ~= nil) and {"", caption, " [img=info]"} or caption
label = flow.add{type="label", caption=caption, tooltip=tooltip}
label.style.font = "default-semibold"
end
if label_first then add_label(); add_switch(); label.style.right_margin = 8
else add_switch(); add_label(); label.style.left_margin = 8 end
return switch
end
---@param state SwitchState
---@return boolean converted_state
function _gui.switch.convert_to_boolean(state)
return (state == "left") and true or false
end
---@param boolean boolean
---@return SwitchState converted_state
function _gui.switch.convert_to_state(boolean)
return boolean and "left" or "right"
end
-- Destroys the toggle-main-dialog-button if present
---@param player LuaPlayer
local function destroy_mod_gui(player)
local button_flow = mod_gui.get_button_flow(player)
local mod_gui_button = button_flow["fp_button_toggle_interface"]
if mod_gui_button then
-- parent.parent is to check that I'm not deleting a top level element. Now, I have no idea how that
-- could ever be a top level element, but oh well, can't know everything now can we?
if #button_flow.children_names == 1 and button_flow.parent.parent then
-- Remove whole frame if FP is the last button in there
button_flow.parent.destroy()
else
mod_gui_button.destroy()
end
end
end
-- Toggles the visibility of the toggle-main-dialog-button
---@param player LuaPlayer
function _gui.toggle_mod_gui(player)
local enable = util.globals.settings(player).show_gui_button
local frame_flow = mod_gui.get_button_flow(player)
local mod_gui_button = frame_flow["fp_button_toggle_interface"]
if enable and not mod_gui_button then
local tooltip = {"", {"shortcut-name.fp_open_interface"}, " (", {"fp.toggle_interface"}, ")"}
local button = frame_flow.add{type="sprite-button", name="fp_button_toggle_interface",
sprite="fp_mod_gui", tooltip=tooltip, tags={mod="fp", on_gui_click="mod_gui_toggle_interface"},
style=mod_gui.button_style, mouse_button_filter={"left"}}
button.style.padding = 6
elseif mod_gui_button then -- use the destroy function for possible cleanup reasons
destroy_mod_gui(player)
end
end
-- Properly centers the given frame (need width/height parameters cause no API-read exists)
---@param player LuaPlayer
---@param frame LuaGuiElement
---@param dimensions DisplayResolution
function _gui.properly_center_frame(player, frame, dimensions)
local resolution, scale = player.display_resolution, player.display_scale
local x_offset = ((resolution.width - (dimensions.width * scale)) / 2)
local y_offset = ((resolution.height - (dimensions.height * scale)) / 2)
frame.location = {x_offset, y_offset}
end
---@param textfield LuaGuiElement
function _gui.setup_textfield(textfield)
textfield.lose_focus_on_confirm = true
textfield.clear_and_focus_on_right_click = true
end
---@param textfield LuaGuiElement
---@param decimal boolean
---@param negative boolean
function _gui.setup_numeric_textfield(textfield, decimal, negative)
textfield.lose_focus_on_confirm = true
textfield.clear_and_focus_on_right_click = true
textfield.numeric = true
textfield.allow_decimal = (decimal or false)
textfield.allow_negative = (negative or false)
end
---@param textfield LuaGuiElement
function _gui.select_all(textfield)
textfield.focus()
textfield.select_all()
end
-- Destroys all GUIs so they are loaded anew the next time they are shown
---@param player LuaPlayer
function _gui.reset_player(player)
destroy_mod_gui(player) -- mod_gui button
for _, gui_element in pairs(player.gui.screen.children) do -- all mod frames
if gui_element.valid and gui_element.get_mod() == "factoryplanner" then
gui_element.destroy()
end
end
end
-- Formats the given effects for use in a tooltip
---@param effects ModuleEffects
---@param limit_effects boolean
---@return LocalisedString
function _gui.format_module_effects(effects, limit_effects)
local tooltip_lines, effect_applies = {"", "\n"}, false
local lower_bound, upper_bound = MAGIC_NUMBERS.effects_lower_bound, MAGIC_NUMBERS.effects_upper_bound
for effect_name, effect_value in pairs(effects) do
if effect_value ~= 0 then
effect_applies = true
local capped_indication = "" ---@type LocalisedString
if limit_effects then
if effect_name == "productivity" and effect_value < 0 then
effect_value, capped_indication = 0, {"fp.effect_maxed"}
elseif effect_value < lower_bound then
effect_value, capped_indication = lower_bound, {"fp.effect_maxed"}
elseif effect_value > upper_bound then
effect_value, capped_indication = upper_bound, {"fp.effect_maxed"}
end
end
-- Force display of either a '+' or '-', also round the result
local display_value = ("%+d"):format(math.floor((effect_value * 100) + 0.5))
table.insert(tooltip_lines, {"fp.effect_line", {"fp." .. effect_name}, display_value, capped_indication})
end
end
return (effect_applies) and tooltip_lines or ""
end
return _gui

View File

@@ -0,0 +1,42 @@
local _messages = {}
---@alias MessageCategory "error" | "warning" | "hint"
---@class PlayerMessage
---@field category MessageCategory
---@field text LocalisedString
---@field lifetime integer
---@param player LuaPlayer
---@param category MessageCategory
---@param message LocalisedString
---@param lifetime integer
function _messages.raise(player, category, message, lifetime)
local messages = util.globals.ui_state(player).messages
table.insert(messages, {category=category, text=message, lifetime=lifetime})
end
---@param player LuaPlayer
function _messages.refresh(player)
-- Only refresh messages if the user is actually looking at them
if not main_dialog.is_in_focus(player) then return end
local ui_state = util.globals.ui_state(player)
local message_frame = ui_state.main_elements["messages_frame"]
if not message_frame or not message_frame.valid then return end
local messages = ui_state.messages
message_frame.visible = (next(messages) ~= nil)
message_frame.clear()
for i=#messages, 1, -1 do
local message = messages[i] ---@type PlayerMessage
local caption = {"", "[img=warning-white] ", {"fp." .. message.category .. "_message", message.text}}
message_frame.add{type="label", caption=caption, style="bold_label"}
message.lifetime = message.lifetime - 1
if message.lifetime == 0 then table.remove(messages, i) end
end
end
return _messages

View File

@@ -0,0 +1,46 @@
local _nth_tick = {}
---@alias NthTickEvent { handler_name: string, metadata: table }
---@param tick Tick
local function register_nth_tick_handler(tick)
script.on_nth_tick(tick, function(nth_tick_data)
local event_data = global.nth_tick_events[nth_tick_data.nth_tick]
local handler = GLOBAL_HANDLERS[event_data.handler_name] ---@type function
handler(event_data.metadata)
util.nth_tick.cancel(tick)
end)
end
---@param desired_tick Tick
---@param handler_name string
---@param metadata table
---@return Tick
function _nth_tick.register(desired_tick, handler_name, metadata)
local actual_tick = desired_tick
-- Search until the next free nth_tick is found
while (global.nth_tick_events[actual_tick] ~= nil) do
actual_tick = actual_tick + 1
end
global.nth_tick_events[actual_tick] = {handler_name=handler_name, metadata=metadata}
register_nth_tick_handler(actual_tick)
return actual_tick -- let caller know which tick they actually got
end
---@param tick Tick
function _nth_tick.cancel(tick)
script.on_nth_tick(tick, nil)
global.nth_tick_events[tick] = nil
end
function _nth_tick.register_all()
if not global.nth_tick_events then return end
for tick, _ in pairs(global.nth_tick_events) do
register_nth_tick_handler(tick)
end
end
return _nth_tick

View File

@@ -0,0 +1,150 @@
local migrator = require("backend.handlers.migrator")
local _porter = {}
---@class ExportTable
---@field mod_version VersionString
---@field export_modset ModToVersion
---@field subfactories FPPackedSubfactory[]
---@alias ExportString string
-- Converts the given subfactories into a factory exchange string
---@param subfactories FPSubfactory[]
---@return ExportString
function _porter.generate_export_string(subfactories)
local export_table = {
-- This can use the global mod_version since it's only called for migrated, valid subfactories
mod_version = global.mod_version,
export_modset = global.installed_mods,
subfactories = {}
}
for _, subfactory in pairs(subfactories) do
table.insert(export_table.subfactories, Subfactory.pack(subfactory))
end
local export_string = game.encode_string(game.table_to_json(export_table)) ---@cast export_string -nil
return export_string
end
-- Converts the given factory exchange string into a temporary Factory
---@param export_string ExportString
---@return FPFactory?
---@return string?
function _porter.process_export_string(export_string)
local export_table = nil ---@type AnyBasic?
if not pcall(function()
export_table = game.json_to_table(game.decode_string(export_string) --[[@as string]])
assert(type(export_table) == "table")
end) then return nil, "decoding_failure" end
---@cast export_table ExportTable
if not pcall(function()
migrator.migrate_export_table(export_table)
end) then return nil, "migration_failure" end
local import_factory = Factory.init()
if not pcall(function() -- Unpacking and validating could be pcall-ed separately, but that's too many slow pcalls
for _, packed_subfactory in pairs(export_table.subfactories) do
local unpacked_subfactory = Subfactory.unpack(packed_subfactory)
-- The imported subfactories will be temporarily contained in a factory, so they
-- can be validated and moved to the appropriate 'real' factory easily
Factory.add(import_factory, unpacked_subfactory)
-- Validate the subfactory to both add the valid-attributes to all the objects
-- and potentially un-simplify the prototypes that came in packed
Subfactory.validate(unpacked_subfactory)
end
-- Include the modset at export time to be displayed to the user if a subfactory is invalid
import_factory.export_modset = export_table.export_modset
end) then return nil, "unpacking_failure" end
-- This is not strictly a decoding failure, but close enough
if Factory.count(import_factory, "Subfactory") == 0 then return nil, "decoding_failure" end
return import_factory, nil
end
---@alias UpdatedMods { [string]: { old: VersionString, current: VersionString } }
-- Creates a nice tooltip laying out which mods were added, removed and updated since the subfactory became invalid
---@param old_modset ModToVersion
---@return LocalisedString
function _porter.format_modset_diff(old_modset)
if not old_modset then return "" end
---@type { added: ModToVersion, removed: ModToVersion, updated: UpdatedMods }
local changes = {added={}, removed={}, updated={}}
local new_modset = script.active_mods
-- Determine changes by running through both sets of mods once each
for name, current_version in pairs(new_modset) do
local old_version = old_modset[name]
if not old_version then
changes.added[name] = current_version
elseif old_version ~= current_version then
changes.updated[name] = {old=old_version, current=current_version}
end
end
for name, old_version in pairs(old_modset) do
if not new_modset[name] then
changes.removed[name] = old_version
end
end
-- Compose tooltip from all three types of changes
local tooltip = {"", {"fp.subfactory_modset_changes"}} ---@type LocalisedString
local current_table, next_index = tooltip, 3
if next(changes.added) then
current_table, next_index = util.build_localised_string({
{"fp.subfactory_mod_added"}}, current_table, next_index)
for name, version in pairs(changes.added) do
current_table, next_index = util.build_localised_string({
{"fp.subfactory_mod_and_version", name, version}}, current_table, next_index)
end
end
if next(changes.removed) then
current_table, next_index = util.build_localised_string({
{"fp.subfactory_mod_removed"}}, current_table, next_index)
for name, version in pairs(changes.removed) do
current_table, next_index = util.build_localised_string({
{"fp.subfactory_mod_and_version", name, version}}, current_table, next_index)
end
end
if next(changes.updated) then
current_table, next_index = util.build_localised_string({
{"fp.subfactory_mod_updated"}}, current_table, next_index)
for name, versions in pairs(changes.updated) do
current_table, next_index = util.build_localised_string({
{"fp.subfactory_mod_and_versions", name, versions.old, versions.current}}, current_table, next_index)
end
end
-- Return an empty string if no changes were found, ie. the tooltip is still only the header
return (table_size(tooltip) == 2) and "" or tooltip
end
-- Adds given export_string-subfactories to the current factory
---@param player LuaPlayer
---@param export_string ExportString
function _porter.add_by_string(player, export_string)
local context = util.globals.context(player)
local first_subfactory = Factory.import_by_string(context.factory, export_string)
util.context.set_subfactory(player, first_subfactory)
for _, subfactory in pairs(Factory.get_in_order(context.factory, "Subfactory")) do
if not subfactory.valid then Subfactory.repair(subfactory, player) end
solver.update(player, subfactory)
end
end
return _porter

View File

@@ -0,0 +1,31 @@
local _raise = {}
---@param player LuaPlayer
---@param trigger "main_dialog" | "compact_subfactory" | "view_state"
---@param parent LuaGuiElement?
function _raise.build(player, trigger, parent)
script.raise_event(CUSTOM_EVENTS.build_gui_element, {player_index=player.index, trigger=trigger, parent=parent})
end
---@param player LuaPlayer
---@param trigger "all" | "subfactory" | "production" | "production_detail" | "title_bar" | "subfactory_list" | "subfactory_info" | "item_boxes" | "production_box" | "production_table" | "compact_subfactory" | "view_state"
---@param element LuaGuiElement?
function _raise.refresh(player, trigger, element)
script.raise_event(CUSTOM_EVENTS.refresh_gui_element, {player_index=player.index, trigger=trigger, element=element})
end
---@param player LuaPlayer
---@param metadata table
function _raise.open_dialog(player, metadata)
script.raise_event(CUSTOM_EVENTS.open_modal_dialog, {player_index=player.index, metadata=metadata})
end
---@param player LuaPlayer
---@param action "submit" | "cancel" | "delete"
---@param skip_opened boolean?
function _raise.close_dialog(player, action, skip_opened)
script.raise_event(CUSTOM_EVENTS.close_modal_dialog,
{player_index=player.index, action=action, skip_opened=skip_opened})
end
return _raise

View File

@@ -0,0 +1,73 @@
local _util = {
globals = require("util.globals"),
context = require("util.context"),
clipboard = require("util.clipboard"),
messages = require("util.messages"),
raise = require("util.raise"),
cursor = require("util.cursor"),
gui = require("util.gui"),
format = require("util.format"),
nth_tick = require("util.nth_tick"),
porter = require("util.porter"),
actions = require("util.actions")
}
-- Still can't believe this is not a thing in Lua
-- This has the added feature of turning any number strings into actual numbers
---@param str string
---@param separator string
---@return string[]
function _util.split_string(str, separator)
local result = {}
for token in string.gmatch(str, "[^" .. separator .. "]+") do
table.insert(result, (tonumber(token) or token))
end
return result
end
-- Fills up the localised table in a smart way to avoid the limit of 20 strings per level
-- To make it stateless, it needs its return values passed back as arguments
-- Uses state to avoid needing to call table_size() because that function is slow
---@param strings_to_insert LocalisedString[]
---@param current_table LocalisedString
---@param next_index integer
---@return LocalisedString, integer
function _util.build_localised_string(strings_to_insert, current_table, next_index)
current_table = current_table or {""}
next_index = next_index or 2
for _, string_to_insert in ipairs(strings_to_insert) do
if next_index == 20 then -- go a level deeper if this one is almost full
local new_table = {""}
current_table[next_index] = new_table
current_table = new_table
next_index = 2
end
current_table[next_index] = string_to_insert
next_index = next_index + 1
end
return current_table, next_index
end
-- This function is only called when Recipe Book is active, so no need to check for the mod
---@param player LuaPlayer
---@param type string
---@param name string
function _util.open_in_recipebook(player, type, name)
local message = nil ---@type LocalisedString
if remote.call("RecipeBook", "version") ~= RECIPEBOOK_API_VERSION then
message = {"fp.error_recipebook_version_incompatible"}
else
---@type boolean
local was_opened = remote.call("RecipeBook", "open_page", player.index, type, name)
if not was_opened then message = {"fp.error_recipebook_lookup_failed", {"fp.pl_" .. type, 1}} end
end
if message then util.messages.raise(player, "error", message, 1) end
end
return _util