1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2025-11-25 22:32:18 +02:00
Files
ComfyFactorio/utils/gui/bottom_frame.lua
Gerkiz 38ec1a9a72 Mass refactor
This PR changes generated events by util modules to bypass the need to require each file to utilize them.

Added new module that tracks undo of a player.

The config module for GUI has been refactored to add functions/events from the caller instead of having one massive blob inside of the file.

The debug module now prints each attribute of an object instead of plain <userdata>.
2025-10-19 21:20:03 +02:00

715 lines
22 KiB
Lua

local Event = require 'utils.event'
local Global = require 'utils.global'
local Gui = require 'utils.gui'
local Task = require 'utils.task_token'
local Config = require 'utils.gui.config'
local CreatedEvents = require 'utils.created_events'
local this =
{
players = {},
storage = {},
activate_custom_buttons = false,
bottom_quickbar_button = {}
}
Global.register(
this,
function (t)
this = t
end
)
--- Events generated by the bottom frame module.
-- @table events
-- @field bottom_quickbar_respawn_raise The event triggered when the bottom quickbar is respawned or raised.
-- @field bottom_quickbar_location_changed The event triggered when the location of the bottom quickbar is changed.
local Public = {}
local set_location
local destroy_frame
local remove_player
local get_player_data
local main_frame_name = Gui.uid_name()
local sections =
{
[1] = 1,
[2] = 1,
[3] = 2,
[4] = 2,
[5] = 3,
[6] = 3,
[7] = 4,
[8] = 4,
[9] = 5,
[10] = 5,
[11] = 6,
[12] = 6
}
Config.register_scenario_module(
{
id = "bottom_frame",
admin_only = false,
gui_rows = Config.register_token(
function (player, frame)
local switch_state
local autostash = is_loaded('modules.autostash')
if autostash then
switch_state = 'right'
local bottom_frame = Public.get_player_data(player)
if bottom_frame and bottom_frame.top then
switch_state = 'left'
end
Config.add_switch(frame, switch_state, 'top_location', 'Position - top', 'Toggle to select if you want the bottom buttons at the top or the bottom.')
frame.add({ type = 'line' })
end
switch_state = 'right'
local bottom_frame = Public.get_player_data(player)
if bottom_frame and bottom_frame.bottom_state == 'bottom_left' then
switch_state = 'left'
end
Config.add_switch(frame, switch_state, 'bottom_location', 'Position - bottom', 'Toggle to select if you want the bottom button on the left side or the right side.')
frame.add({ type = 'line' })
switch_state = 'right'
if bottom_frame and bottom_frame.above then
switch_state = 'left'
end
Config.add_switch(frame, switch_state, 'middle_location', 'Position - middle', 'Toggle to select if you want the bottom button above the quickbar or the side of the quickbar.')
frame.add({ type = 'line' })
switch_state = 'right'
if bottom_frame and bottom_frame.portable then
switch_state = 'left'
end
Config.add_switch(frame, switch_state, 'portable_button', 'Position - portable', 'Toggle to select if you want the bottom button to be portable or not.')
frame.add({ type = 'line' })
end),
handlers =
{
['top_location'] = Config.register_token(
function (player, _)
local data = Public.get_player_data(player)
if data and data.state and not data.top then
Public.set_top(player, true)
else
Public.set_top(player, false)
end
end),
['bottom_location'] = Config.register_token(
function (player, event)
if event.element.switch_state == 'left' then
Public.set_location(player, 'bottom_left')
else
Public.set_location(player, 'bottom_right')
end
end),
['middle_location'] = Config.register_token(
function (player, event)
local data = Public.get_player_data(player)
if event.element.switch_state == 'left' then
data.above = true
data.portable = false
else
data.above = false
data.portable = false
end
if not data.bottom_state then
data.bottom_state = 'bottom_right'
end
Public.set_location(player, data.bottom_state)
end),
['portable_button'] = Config.register_token(
function (player, event)
local data = Public.get_player_data(player)
if event.element.switch_state == 'left' then
data.above = false
data.portable = true
else
data.portable = false
data.above = false
end
if not data.bottom_state then
data.bottom_state = 'bottom_right'
end
Public.set_location(player, data.bottom_state)
end),
}
})
local check_bottom_buttons_token =
Task.register(
function (event)
local player_index = event.player_index
local player = game.get_player(player_index)
if not player or not player.valid then
return
end
local player_data, storage_data = get_player_data(player)
if not player_data or not storage_data or not next(storage_data) then
destroy_frame(player)
remove_player(player.index)
return
end
end
)
remove_player = function (index)
this.players[index] = nil
this.storage[index] = nil
this.bottom_quickbar_button[index] = nil
end
get_player_data = function (player, remove_user_data)
if remove_user_data then
this.players[player.index] = nil
this.storage[player.index] = nil
return
end
if not this.players[player.index] then
this.players[player.index] =
{
state = 'bottom_right',
section = {},
direction = 'vertical',
row_index = 1,
row_selection = 1,
row_selection_added = 1
}
this.storage[player.index] = {}
end
return this.players[player.index], this.storage[player.index]
end
--- Refreshes all inner frames for a given player
local function refresh_inner_frames(player)
if not player or not player.valid then
return
end
local player_data, storage_data = get_player_data(player)
if not player_data or not storage_data or not player_data.frame or not player_data.frame.valid then
return
end
local main_frame = player_data.frame
local horizontal_flow = main_frame.add { type = 'flow', direction = 'horizontal' }
horizontal_flow.style.horizontal_spacing = 0
for row_index, row_index_data in pairs(storage_data) do
if row_index_data and type(row_index_data) == 'table' then
local section_row_index = player_data.section[row_index]
local vertical_flow = horizontal_flow.add { type = 'flow', direction = 'vertical' }
vertical_flow.style.vertical_spacing = 0
if not section_row_index then
player_data.section[row_index] = {}
section_row_index = player_data.section[row_index]
end
if not section_row_index.inside_frame or not section_row_index.inside_frame.valid then
section_row_index.inner_frame = vertical_flow
end
for row_selection, row_selection_data in pairs(row_index_data) do
if section_row_index[row_selection] and section_row_index[row_selection].valid then
section_row_index[row_selection].destroy()
end
section_row_index[row_selection] =
section_row_index.inner_frame.add
{
type = 'sprite-button',
sprite = row_selection_data.sprite,
name = row_selection_data.name,
tooltip = row_selection_data.tooltip or '',
style = 'quick_bar_page_button'
}
end
end
end
end
local refresh_inner_frames_token =
Task.register(
function (event)
local player_index = event.player_index
local player = game.get_player(player_index)
if not player or not player.valid then
return
end
refresh_inner_frames(player)
end
)
---Adds a new inner frame to the bottom frame
-- local BottomFrame = require 'utils.gui.bottom_frame'
-- BottomFrame.add_inner_frame({player = player, element_name = Gui.uid_name(), tooltip = 'Some tooltip', sprite = 'item/raw-fish' })
---@param data any
local function add_inner_frame(data)
if not data then
return
end
local player = data.player
local element_name = data.element_name
local tooltip = data.tooltip
local sprite = data.sprite
if not player or not player.valid then
return error('Given player was not valid', 2)
end
if not element_name then -- the element_name to pick from the row_selection
return error('Element name is missing', 2)
end
if not sprite then
return error('Sprite is missing', 2)
end
local player_data, storage_data = get_player_data(player)
if not player_data or not storage_data or not player_data.frame or not player_data.frame.valid then
return
end
if player_data.row_index > 6 then
return error('Having more than 6 rows is currently not supported.', 2)
end
local found = false
for _, row_index_data in pairs(storage_data) do
if row_index_data and type(row_index_data) == 'table' then
for _, row_selection_data in pairs(row_index_data) do
if row_selection_data and row_selection_data.name == element_name then
found = true
end
end
end
end
if found then
return
end
player_data.row_index = sections[player_data.row_selection_added]
if not storage_data[player_data.row_index] then
storage_data[player_data.row_index] = {}
end
local storage_data_section = storage_data[player_data.row_index]
storage_data_section[player_data.row_selection] =
{
name = element_name,
sprite = sprite,
tooltip = tooltip
}
player_data.row_selection = player_data.row_selection + 1
player_data.row_selection_added = player_data.row_selection_added + 1
player_data.row_selection = player_data.row_selection > 2 and 1 or player_data.row_selection
Task.priority_delay(2, refresh_inner_frames_token, { player_index = player.index })
end
local function get_frame_by_element_name(player, element_name)
local player_data, storage_data = get_player_data(player)
if not player_data or not storage_data or not player_data.frame or not player_data.frame.valid then
return
end
for _, row_index_data in pairs(storage_data) do
if row_index_data and type(row_index_data) == 'table' then
for _, row_selection_data in pairs(row_index_data) do
if row_selection_data and row_selection_data.name == element_name then
return row_selection_data
end
end
end
end
end
destroy_frame = function (player)
local gui = player.gui
local frame = gui.screen[main_frame_name]
if frame and frame.valid then
frame.destroy()
end
end
--- Creates a new frame
---@param player LuaPlayer
---@param alignment string
---@param location table
---@param data any
---@return unknown
local function create_frame(player, alignment, location, data)
local gui = player.gui
local frame = gui.screen[main_frame_name]
if frame and frame.valid then
destroy_frame(player)
end
alignment = alignment or 'vertical'
frame =
player.gui.screen.add
{
type = 'frame',
name = main_frame_name,
direction = alignment
}
if data.visible ~= nil then
if data.visible then
frame.visible = true
else
frame.visible = false
end
end
frame.style.padding = 3
frame.style.top_padding = 4
if alignment == 'vertical' then
frame.style.minimal_height = 96
end
local inner_frame =
frame.add
{
type = 'frame',
direction = alignment
}
inner_frame.style = 'quick_bar_inner_panel'
frame.location = location
if data.portable then
frame.caption = ''
end
if data.top then
frame.visible = false
else
frame.visible = true
end
data.frame = inner_frame
data.parent = frame
data.section = data.section or {}
data.section_data = data.section_data or {}
data.alignment = alignment
Task.priority_delay(5, check_bottom_buttons_token, { player_index = player.index })
return frame
end
set_location = function (player, state)
local data = get_player_data(player)
local alignment = 'vertical'
local location
local resolution = player.display_resolution
local scale = player.display_scale
state = state or data.state
if state == 'bottom_left' then
if data.above then
location =
{
x = (resolution.width / 2) - ((259) * scale),
y = (resolution.height - (-12 + (40 * 5) * scale))
}
alignment = 'horizontal'
else
location =
{
-- x = (resolution.width / 2) - ((54 + 528 - 44) * scale),
x = (resolution.width / 2) - ((455 + (data.row_index * 40)) * scale),
y = (resolution.height - (96 * scale))
}
end
data.bottom_state = 'bottom_left'
elseif state == 'bottom_right' then
if data.above then
location =
{
-- x = (resolution.width / 2) - ((-262 - (40 * t[data.row_index])) * scale),
x = (resolution.width / 2) - ((-460 + (data.row_index * 40)) * scale),
y = (resolution.height - (-12 + (40 * 5) * scale))
}
alignment = 'horizontal'
else
location =
{
x = (resolution.width / 2) - ((54 + -689) * scale),
y = (resolution.height - (96 * scale))
}
end
data.bottom_state = 'bottom_right'
else
location =
{
x = (resolution.width / 2) - ((54 + -528) * scale),
y = (resolution.height - (96 * scale))
}
end
Event.raise(CreatedEvents.events.bottom_quickbar_location_changed, { player_index = player.index, data = data })
data.state = state
create_frame(player, alignment, location, data)
refresh_inner_frames(player)
end
--- Sets then frame location of the given player
---@param player LuaPlayer?
---@param value boolean
local function set_top(player, value)
local data = get_player_data(player)
data.top = value or false
Public.set_location(player, 'bottom_right')
end
--- Returns the current frame location of the given player
---@param player LuaPlayer
---@return table|nil
local function get_location(player)
local data = get_player_data(player)
return data and data.state or nil
end
--- Activates the custom buttons
---@param value boolean
function Public.activate_custom_buttons(value)
this.activate_custom_buttons = value or false
end
--- Checks if custom buttons are enabled.
--- @return boolean: True if custom buttons are enabled, false otherwise.
function Public.is_custom_buttons_enabled()
return this.activate_custom_buttons
end
--- Toggles the player frame.
--- @param player LuaPlayer: The player entity.
--- @param state boolean: The state to set for the player frame.
function Public.toggle_player_frame(player, state)
local gui = player.gui
local frame = gui.screen[main_frame_name]
if frame and frame.valid then
local data = get_player_data(player)
if state then
data.visible = true
frame.visible = true
else
data.visible = false
frame.visible = false
end
end
end
--- Returns the current frame of the given player
---@param player LuaPlayer
---@param section_name string
---@return table|boolean|nil
function Public.get_section(player, section_name)
local data = get_player_data(player)
local section = data.section
if not section then
return false
end
for _, section_tbl in pairs(section) do
if not section_tbl or not next(section_tbl) then
break
end
for _, section_data in pairs(section_tbl) do
if section_data and section_data.valid and section_data.name == section_name then
return section_data
end
end
end
end
--- Retrieves the value associated with the specified key.
--- @param key any The key to retrieve the value for.
--- @return any The value associated with the key.
function Public.get(key)
if key then
return this[key]
else
return this
end
end
--- Sets the value of a given key.
--- @param key any The key to set.
--- @param value any The value to set for the key.
function Public.set(key, value)
if key and (value or value == false) then
this[key] = value
return this[key]
elseif key then
return this[key]
else
return this
end
end
--- Resets the bottom frame.
function Public.reset()
local players = game.players
for i = 1, #players do
local player = players[i]
if player and player.valid then
if not player.connected then
this.players[player.index] = nil
this.storage[player.index] = nil
end
end
end
end
Event.add(
defines.events.on_player_joined_game,
function (event)
if this.activate_custom_buttons then
local player = game.get_player(event.player_index)
local data = get_player_data(player)
set_location(player, data.state)
end
end
)
Event.add(
defines.events.on_player_display_resolution_changed,
function (event)
if this.activate_custom_buttons then
local player = game.get_player(event.player_index)
local data = get_player_data(player)
set_location(player, data.state)
end
end
)
Event.add(
defines.events.on_player_display_scale_changed,
function (event)
local player = game.get_player(event.player_index)
if this.activate_custom_buttons then
local data = get_player_data(player)
set_location(player, data.state)
end
end
)
Event.add(
defines.events.on_pre_player_left_game,
function (event)
local player = game.get_player(event.player_index)
destroy_frame(player)
if this.activate_custom_buttons then
get_player_data(player, true)
end
end
)
Event.add(
defines.events.on_player_left_game,
function (event)
local player = game.get_player(event.player_index)
destroy_frame(player)
if this.activate_custom_buttons then
get_player_data(player, true)
end
end
)
Event.add(
defines.events.on_pre_player_died,
function (event)
if this.activate_custom_buttons then
local player = game.get_player(event.player_index)
destroy_frame(player)
end
end
)
Event.add(
defines.events.on_player_respawned,
function (event)
if this.activate_custom_buttons then
local player = game.get_player(event.player_index)
local data = get_player_data(player)
set_location(player, data.state)
end
end
)
Event.add(
defines.events.on_player_removed,
function (event)
remove_player(event.player_index)
end
)
Event.add(
CreatedEvents.events.bottom_quickbar_respawn_raise,
function (event)
if not event or not event.player_index then
return
end
if this.activate_custom_buttons then
local player = game.get_player(event.player_index)
local data = get_player_data(player)
set_location(player, data.state)
end
end
)
Event.add(
CreatedEvents.events.bottom_quickbar_location_changed,
function (event)
if not event or not event.player_index then
return
end
if this.activate_custom_buttons then
local player = game.get_player(event.player_index)
local data = get_player_data(player)
if data.frame and data.frame.valid then
if data.top then
data.frame.visible = false
else
data.frame.visible = true
end
end
end
end
)
Public.main_frame_name = main_frame_name
Public.refresh_inner_frames = refresh_inner_frames
Public.get_player_data = get_player_data
Public.remove_player = remove_player
Public.set_location = set_location
Public.get_location = get_location
Public.set_top = set_top
Public.add_inner_frame = add_inner_frame
Public.get_frame_by_element_name = get_frame_by_element_name
Gui.screen_to_bypass(main_frame_name)
return Public