Align names and call patterns to best practice

Apply the conventions from the rocket info gui review to the player list,
and remove the now migrated legacy module.

- Move row building onto the table def as calculate_row_data, and refresh
  via refresh_player which computes the row data once per player
- Store the selection on the container with private/public accessor
  methods instead of poking element data directly
- Store the selected player as a LuaPlayer rather than a string and update
  the actions config to match
- Use :style{} on the player table and make the action and reason bars
  their own element definitions
- Delete the migrated legacy gui and its file loader entry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
bbassie
2026-06-22 20:10:20 +00:00
co-authored by Claude Opus 4.8
parent b1c0f2535b
commit 4bd5394268
4 changed files with 322 additions and 751 deletions
@@ -24,7 +24,6 @@ return {
--- GUI
"modules.gui.rocket-info",
"modules.gui.warp-list",
-- "modules.gui.player-list", -- migrated to exp_scenario/gui/player_list
"modules.gui.vlayer",
"modules.gui._role_updates",
@@ -30,11 +30,10 @@ local function auth_lower_role(player, selected_player_name)
end
-- gets the action player and a coloured name for the action to be used on
local function get_action_player_name(player)
local selected_player_name = get_selected_player(player)
local selected_player = game.players[selected_player_name]
local function get_action_player(player)
local selected_player = get_selected_player(player) --[[ @as LuaPlayer ]]
local selected_player_color = format_player_name(selected_player)
return selected_player_name, selected_player_color
return selected_player, selected_player_color
end
-- teleports one player to another
@@ -66,8 +65,7 @@ end
-- @element goto_player
local goto_player = new_button("utility/export", { "exp-gui_player-list.goto-player" })
:on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player)
local selected_player = game.players[selected_player_name]
local selected_player = get_action_player(player)
if not player.character or not selected_player.character then
player.print({ "expcore-commands.reject-player-alive" }, Colors.orange_red)
else
@@ -79,8 +77,7 @@ local goto_player = new_button("utility/export", { "exp-gui_player-list.goto-pla
-- @element bring_player
local bring_player = new_button("utility/import", { "exp-gui_player-list.bring-player" })
:on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player)
local selected_player = game.players[selected_player_name]
local selected_player = get_action_player(player)
if not player.character or not selected_player.character then
player.print({ "expcore-commands.reject-player-alive" }, Colors.orange_red)
else
@@ -92,8 +89,8 @@ local bring_player = new_button("utility/import", { "exp-gui_player-list.bring-p
-- @element report_player
local report_player = new_button("utility/spawn_flag", { "exp-gui_player-list.report-player" })
:on_click(function(def, player, element)
local selected_player_name = get_action_player_name(player)
if Reports.is_reported(selected_player_name, player.name) then
local selected_player = get_action_player(player)
if Reports.is_reported(selected_player.name, player.name) then
player.print({ "exp-commands_report.already-reported" }, Colors.orange_red)
else
set_selected_action(player, "command/report")
@@ -101,11 +98,11 @@ local report_player = new_button("utility/spawn_flag", { "exp-gui_player-list.re
end)
local function report_player_callback(player, reason)
local selected_player_name, selected_player_color = get_action_player_name(player)
local selected_player, selected_player_color = get_action_player(player)
local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_reports.response", selected_player_color, reason }
Roles.print_to_roles_higher("Trainee", { "exp-commands_reports.response-admin", selected_player_color, by_player_name_color, reason })
Reports.report_player(selected_player_name, player.name, reason)
Reports.report_player(selected_player.name, player.name, reason)
end
--- Gives the action player a warning, requires a reason
@@ -116,18 +113,18 @@ local warn_player = new_button("utility/spawn_flag", { "exp-gui_player-list.warn
end)
local function warn_player_callback(player, reason)
local selected_player_name, selected_player_color = get_action_player_name(player)
local selected_player, selected_player_color = get_action_player(player)
local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_warnings.create", selected_player_color, by_player_name_color, reason }
Warnings.add_warning(selected_player_name, player.name, reason)
Warnings.add_warning(selected_player.name, player.name, reason)
end
--- Jails the action player, requires a reason
-- @element jail_player
local jail_player = new_button("utility/multiplayer_waiting_icon", { "exp-gui_player-list.jail-player" })
:on_click(function(def, player, element)
local selected_player_name, selected_player_color = get_action_player_name(player)
if Jail.is_jailed(selected_player_name) then
local selected_player, selected_player_color = get_action_player(player)
if Jail.is_jailed(selected_player.name) then
player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red)
else
set_selected_action(player, "command/jail")
@@ -135,10 +132,10 @@ local jail_player = new_button("utility/multiplayer_waiting_icon", { "exp-gui_pl
end)
local function jail_player_callback(player, reason)
local selected_player_name, selected_player_color = get_action_player_name(player)
local selected_player, selected_player_color = get_action_player(player)
local by_player_name_color = format_player_name(player)
game.print{ "exp-commands_jail.jailed", selected_player_color, by_player_name_color, reason }
Jail.jail_player(selected_player_name, player.name, reason)
Jail.jail_player(selected_player.name, player.name, reason)
end
--- Kicks the action player, requires a reason
@@ -149,7 +146,7 @@ local kick_player = new_button("utility/warning_icon", { "exp-gui_player-list.ki
end)
local function kick_player_callback(player, reason)
local selected_player = get_action_player_name(player)
local selected_player = get_action_player(player)
game.kick_player(selected_player, reason)
end
@@ -161,7 +158,7 @@ local ban_player = new_button("utility/danger_icon", { "exp-gui_player-list.ban-
end)
local function ban_player_callback(player, reason)
local selected_player = get_action_player_name(player)
local selected_player = get_action_player(player)
game.ban_player(selected_player, reason)
end
@@ -1,448 +0,0 @@
--[[-- Gui Module - Player List
- Adds a player list to show names and play time; also includes action buttons which can preform actions to players
@gui Player-List
@alias player_list
]]
-- luacheck:ignore 211/Colors
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles
local Datastore = require("modules.exp_legacy.expcore.datastore") --- @dep expcore.datastore
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local config = require("modules.exp_legacy.config.gui.player_list_actions") --- @dep config.gui.player_list_actions
--- Stores all data for the warp gui
local PlayerListData = Datastore.connect("PlayerListData")
PlayerListData:set_serializer(Datastore.name_serializer)
local SelectedPlayer = PlayerListData:combine("SelectedPlayer")
local SelectedAction = PlayerListData:combine("SelectedAction")
-- Set the config to use these stores
config.set_datastores(SelectedPlayer, SelectedAction)
--- Button used to open the action bar
-- @element open_action_bar
local open_action_bar = Gui.define("open_action_bar")
:draw{
type = "sprite-button",
sprite = "utility/expand_dots",
tooltip = { "player-list.open-action-bar" },
style = "frame_button",
name = Gui.from_name,
}
:style{
padding = -2,
width = 8,
height = 14,
}
:on_click(function(def, player, element)
local selected_player_name = element.parent.name
local old_selected_player_name = SelectedPlayer:get(player)
if selected_player_name == old_selected_player_name then
SelectedPlayer:remove(player)
else
SelectedPlayer:set(player, selected_player_name)
end
end)
--- Button used to close the action bar
-- @element close_action_bar
local close_action_bar = Gui.define("close_action_bar")
:draw{
type = "sprite-button",
sprite = "utility/close_black",
tooltip = { "player-list.close-action-bar" },
style = "slot_sized_button_red",
}
:style(Gui.styles.sprite{
size = 20,
padding = -1,
top_margin = -1,
right_margin = -1,
})
:on_click(function(def, player, element)
SelectedPlayer:remove(player)
SelectedAction:remove(player)
end)
--- Button used to confirm a reason
-- @element reason_confirm
local reason_confirm = Gui.define("reason_confirm")
:draw{
type = "sprite-button",
sprite = "utility/confirm_slot",
tooltip = { "player-list.reason-confirm" },
style = "slot_sized_button_green",
}
:style(Gui.styles.sprite{
size = 30,
padding = -1,
left_margin = -2,
right_margin = -1,
})
:on_click(function(def, player, element)
local reason = element.parent.entry.text
local action_name = SelectedAction:get(player)
local reason_callback = config.buttons[action_name].reason_callback
if reason == nil or not reason:find("%S") then reason = "no reason given" end
reason_callback(player, reason)
SelectedPlayer:remove(player)
SelectedAction:remove(player)
element.parent.entry.text = ""
end)
--- Set of elements that are used to make up a row of the player table
-- @element add_player_base
local add_player_base = Gui.define("add_player_base")
:draw(function(_, parent, player_data)
-- Add the button to open the action bar
local toggle_action_bar_flow = parent.add{ type = "flow", name = player_data.name }
open_action_bar(toggle_action_bar_flow)
-- Add the player name
local player_name = parent.add{
type = "label",
name = "player-name-" .. player_data.index,
caption = player_data.name,
tooltip = { "player-list.open-map", player_data.name, player_data.tag, player_data.role_name },
}
player_name.style.padding = { 0, 2, 0, 0 }
player_name.style.font_color = player_data.chat_color
-- Add the time played label
local alignment = Gui.elements.aligned_flow(parent, { name = "player-time-" .. player_data.index })
local time_label = alignment.add{
name = "label",
type = "label",
caption = player_data.caption,
tooltip = player_data.tooltip,
}
time_label.style.padding = 0
return player_name
end)
:on_click(function(def, player, element, event)
local selected_player_name = element.caption
local selected_player = game.players[selected_player_name]
if event.button == defines.mouse_button_type.left then
-- LMB will open the map to the selected player
player.set_controller{
type = defines.controllers.remote,
position = selected_player.physical_position,
surface = selected_player.physical_surface
}
else
-- RMB will toggle the settings
local old_selected_player_name = SelectedPlayer:get(player)
if selected_player_name == old_selected_player_name then
SelectedPlayer:remove(player)
SelectedAction:remove(player)
else
SelectedPlayer:set(player, selected_player_name)
end
end
end)
-- Removes the three elements that are added as part of the base
local function remove_player_base(parent, player)
Gui.destroy_if_valid(parent[player.name])
Gui.destroy_if_valid(parent["player-name-" .. player.index])
Gui.destroy_if_valid(parent["player-time-" .. player.index])
end
-- Update the time label for a player using there player time data
local function update_player_base(parent, player_time)
local time_element = parent[player_time.element_name]
if time_element and time_element.valid then
time_element.label.caption = player_time.caption
time_element.label.tooltip = player_time.tooltip
end
end
--- Adds all the buttons and flows that make up the action bar
-- @element add_action_bar
local add_action_bar_buttons = Gui.define("add_action_bar_buttons")
:draw(function(_, parent)
close_action_bar(parent)
-- Loop over all the buttons in the config
for action_name, button_data in pairs(config.buttons) do
-- Added the permission flow
local permission_flow = parent.add{ type = "flow", name = action_name }
permission_flow.visible = false
-- Add the buttons under that permission
for _, button in ipairs(button_data) do
button(permission_flow)
end
end
return parent
end)
--- Updates the visible state of the action bar buttons
local function update_action_bar(element)
local player = Gui.get_player(element)
local selected_player_name = SelectedPlayer:get(player)
if not selected_player_name then
-- Hide the action bar when no player is selected
element.visible = false
else
local selected_player = game.players[selected_player_name]
if not selected_player.connected then
-- If the player is offline then reest stores
element.visible = false
SelectedPlayer:remove(player)
SelectedAction:remove(player)
else
-- Otherwise check what actions the player is allowed to use
element.visible = true
for action_name, buttons in pairs(config.buttons) do
if buttons.auth and not buttons.auth(player, selected_player) then
element[action_name].visible = false
elseif Roles.player_allowed(player, action_name) then
element[action_name].visible = true
end
end
end
end
end
--- Main player list container for the left flow
-- @element player_list_container
local player_list_container = Gui.define("player_list_container")
:draw(function(definition, parent)
-- Draw the internal container
local container = Gui.elements.container(parent)
-- Draw the scroll table for the players
local scroll_table = Gui.elements.scroll_table(container, 184, 3, "scroll")
-- Change the style of the scroll table
local scroll_table_style = scroll_table.style
scroll_table_style.padding = { 1, 0, 1, 2 }
-- Add the action bar
local action_bar = Gui.elements.footer(container, { name = "action_bar", no_flow = true })
-- Change the style of the action bar
local action_bar_style = action_bar.style
action_bar_style.height = 35
action_bar_style.padding = { 1, 3 }
action_bar.visible = false
-- Add the buttons to the action bar
add_action_bar_buttons(action_bar)
-- Add the reason bar
local reason_bar = Gui.elements.footer(container, { name = "reason_bar", no_flow = true })
-- Change the style of the reason bar
local reason_bar_style = reason_bar.style
reason_bar_style.height = 35
reason_bar_style.padding = { -1, 3 }
reason_bar.visible = false
-- Add the text entry for the reason bar
local reason_field =
reason_bar.add{
name = "entry",
type = "textfield",
style = "stretchable_textfield",
tooltip = { "player-list.reason-entry" },
}
-- Change the style of the text entry
local reason_entry_style = reason_field.style
reason_entry_style.padding = 0
reason_entry_style.height = 28
reason_entry_style.minimal_width = 160
-- Add the confirm reason button
reason_confirm(reason_bar)
-- Return the exteral container
return container.parent
end)
--- Add the element to the left flow with a toolbar button
Gui.add_left_element(player_list_container, true)
Gui.toolbar.create_button{
name = "player_list_toggle",
left_element = player_list_container,
sprite = "entity/character",
tooltip = { "player-list.main-tooltip" },
visible = function(player, element)
return Roles.player_allowed(player, "gui/player-list")
end
}
local online_time_format = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true }
local afk_time_format = ExpUtil.format_time_factory_locale{ format = "long", minutes = true }
-- Get caption and tooltip format for a player
local function get_time_formats(online_time, afk_time)
local tick = game.tick > 0 and game.tick or 1
local percent = math.round(online_time / tick, 3) * 100
local caption = online_time_format(online_time)
local tooltip = { "player-list.afk-time", percent, afk_time_format(afk_time) }
return caption, tooltip
end
-- Get the player time to be used to update time label
local function get_player_times()
local ctn = 0
local player_times = {}
for _, player in pairs(game.connected_players) do
ctn = ctn + 1
-- Add the player time details to the array
local caption, tooltip = get_time_formats(player.online_time, player.afk_time)
player_times[ctn] = {
element_name = "player-time-" .. player.index,
caption = caption,
tooltip = tooltip,
}
end
return player_times
end
-- Get a sorted list of all online players
local function get_player_list_order()
-- Sort all the online players into roles
local players = {}
for _, player in pairs(game.connected_players) do
local highest_role = Roles.get_player_highest_role(player)
if not players[highest_role.name] then
players[highest_role.name] = {}
end
table.insert(players[highest_role.name], player)
end
-- Sort the players from roles into a set order
local ctn = 0
local player_list_order = {}
for _, role_name in pairs(Roles.config.order) do
if players[role_name] then
for _, player in pairs(players[role_name]) do
ctn = ctn + 1
-- Add the player data to the array
local caption, tooltip = get_time_formats(player.online_time, player.afk_time)
player_list_order[ctn] = {
name = player.name,
index = player.index,
tag = player.tag,
role_name = role_name,
chat_color = player.chat_color,
caption = caption,
tooltip = tooltip,
}
end
end
end
--[[Adds fake players to the player list
local tick = game.tick+1
for i = 1, 10 do
local online_time = math.random(1, tick)
local afk_time = math.random(online_time-(tick/10), tick)
local caption, tooltip = get_time_formats(online_time, afk_time)
player_list_order[ctn+i] = {
name='Player '..i,
index=0-i,
tag='',
role_name = 'Fake Player',
chat_color = table.get_random(Colors),
caption = caption,
tooltip = tooltip
}
end--]]
return player_list_order
end
--- Update the play times every 30 sections
Event.on_nth_tick(1800, function()
local player_times = get_player_times()
for _, player in pairs(game.connected_players) do
local container = Gui.get_left_element(player_list_container, player)
local scroll_table = container.frame.scroll.table
for _, player_time in pairs(player_times) do
update_player_base(scroll_table, player_time)
end
end
end)
--- When a player leaves only remove they entry
Event.add(defines.events.on_player_left_game, function(event)
local remove_player = game.players[event.player_index]
for _, player in pairs(game.connected_players) do
local container = Gui.get_left_element(player_list_container, player)
local scroll_table = container.frame.scroll.table
remove_player_base(scroll_table, remove_player)
local selected_player_name = SelectedPlayer:get(player)
if selected_player_name == remove_player.name then
SelectedPlayer:remove(player)
SelectedAction:remove(player)
end
end
end)
--- All other events require a full redraw of the table
local function redraw_player_list()
local player_list_order = get_player_list_order()
for _, player in pairs(game.connected_players) do
local container = Gui.get_left_element(player_list_container, player)
local scroll_table = container.frame.scroll.table
scroll_table.clear()
for _, next_player_data in ipairs(player_list_order) do
add_player_base(scroll_table, next_player_data)
end
end
end
Event.add(defines.events.on_player_joined_game, redraw_player_list)
Event.add(Roles.events.on_role_assigned, redraw_player_list)
Event.add(Roles.events.on_role_unassigned, redraw_player_list)
--- When the action player is changed the action bar will update
SelectedPlayer:on_update(function(player_name, selected_player)
local player = game.players[player_name]
local container = Gui.get_left_element(player_list_container, player)
local scroll_table = container.frame.scroll.table
update_action_bar(container.frame.action_bar)
for _, next_player in pairs(game.connected_players) do
local element = scroll_table[next_player.name][open_action_bar.name]
local style = "frame_button"
if next_player.name == selected_player then
style = "tool_button"
end
element.style = style
local element_style = element.style --[[@as LuaStyle]]
element_style.padding = -2
element_style.width = 8
element_style.height = 14
end
end)
--- When the action name is changed the reason input will update
SelectedAction:on_update(function(player_name, selected_action)
local player = game.players[player_name]
local container = Gui.get_left_element(player_list_container, player)
local element = container.frame.reason_bar
if selected_action then
-- if there is a new value then check the player is still online
local selected_player_name = SelectedPlayer:get(player_name)
local selected_player = game.players[selected_player_name]
if selected_player.connected then
element.visible = true
else
-- Clear if the player is offline
SelectedPlayer:remove(player)
SelectedAction:remove(player)
end
else
element.visible = false
end
end)
+285 -262
View File
@@ -1,6 +1,5 @@
--[[-- Gui - Player List
Adds a player list to show names and play time; also includes action buttons which can perform actions on players.
The selected player and action used to be stored in a datastore; they are now kept on the container's player data.
]]
local ExpUtil = require("modules/exp_util")
@@ -14,138 +13,22 @@ local Elements = {}
local online_time_format = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true }
local afk_time_format = ExpUtil.format_time_factory_locale{ format = "long", minutes = true }
--[[
The selected player (the player an action will be performed on) and the selected action (which opens the reason bar)
are stored per viewing player on the container's player data. The setters drive the gui updates directly, replacing
the reactive datastore on_update callbacks used by the legacy version.
]]
--- @class ExpGui_PlayerList.selection
--- @field selected_player string? Name of the player actions will be performed on
--- @field selected_action string? Name of the action awaiting a reason
--- Get the selection state for a player, creating it if needed
--- @param player LuaPlayer
--- @return ExpGui_PlayerList.selection
local function get_selection(player)
local selection = Elements.container.data[player]
if not selection then
selection = {}
Elements.container.data[player] = selection
--- Get the caption and tooltip for a player time
--- @param online_time number
--- @param afk_time number
--- @return LocalisedString, LocalisedString
local function get_time_formats(online_time, afk_time)
local tick = game.tick > 0 and game.tick or 1
local percent = math.round(online_time / tick, 3) * 100
local caption = online_time_format(online_time)
local tooltip = { "exp-gui_player-list.afk-time", percent, afk_time_format(afk_time) }
return caption, tooltip
end
return selection
end
--- Get the name of the player that actions will be performed on
--- @param player LuaPlayer
--- @return string?
local function get_selected_player(player)
return get_selection(player).selected_player
end
--- Get the name of the action awaiting a reason
--- @param player LuaPlayer
--- @return string?
local function get_selected_action(player)
return get_selection(player).selected_action
end
--- Style an open action bar button to show if it is the selected player
--- @param button LuaGuiElement
--- @param selected boolean
local function style_open_button(button, selected)
button.style = selected and "tool_button" or "frame_button"
local style = button.style
style.padding = -2
style.width = 8
style.height = 14
end
--- Update the action bar buttons to match the current selection for a player
--- @param action_bar LuaGuiElement
--- @param player LuaPlayer
--- @param selected_player LuaPlayer
local function update_action_bar(action_bar, player, selected_player)
action_bar.visible = true
for action_name, buttons in pairs(config.buttons) do
local flow = action_bar[action_name]
if buttons.auth and not buttons.auth(player, selected_player) then
flow.visible = false
else
flow.visible = Roles.player_allowed(player, action_name)
end
end
end
--- Refresh all gui elements for a player to match their current selection
--- @param player LuaPlayer
local function update_player_ui(player)
local selection = get_selection(player)
-- Clear the selection if the selected player has gone offline
local selected_player = selection.selected_player and game.players[selection.selected_player]
if selected_player and not selected_player.connected then
selection.selected_player = nil
selection.selected_action = nil
selected_player = nil
end
for _, player_table in Elements.player_table:online_elements(player) do
local refs = Elements.player_table.data[player_table]
-- Update the action bar and reason bar visibility
if selected_player then
update_action_bar(refs.action_bar, player, selected_player)
else
refs.action_bar.visible = false
end
refs.reason_bar.visible = selected_player ~= nil and selection.selected_action ~= nil
-- Highlight the open button of the selected player
for player_name, row in pairs(refs.rows) do
style_open_button(row.open_button, player_name == selection.selected_player)
end
end
end
--- Set the player that actions will be performed on, nil to clear the selection
--- @param player LuaPlayer
--- @param selected_player_name string?
local function set_selected_player(player, selected_player_name)
local selection = get_selection(player)
selection.selected_player = selected_player_name
if not selected_player_name then
selection.selected_action = nil
end
update_player_ui(player)
end
--- Toggle the player that actions will be performed on
--- @param player LuaPlayer
--- @param selected_player_name string
local function toggle_selected_player(player, selected_player_name)
if get_selected_player(player) == selected_player_name then
set_selected_player(player, nil)
else
set_selected_player(player, selected_player_name)
end
end
--- Set the action awaiting a reason, nil to clear it
--- @param player LuaPlayer
--- @param selected_action string?
local function set_selected_action(player, selected_action)
get_selection(player).selected_action = selected_action
update_player_ui(player)
end
-- Inject the accessors the actions config needs to read the selected player and set the selected action
config.set_accessors(get_selected_player, set_selected_action)
--- Button used to open the action bar for a player
--- @class ExpGui_PlayerList.elements.open_action_bar: ExpElement
--- @field data table<LuaGuiElement, string>
--- @overload fun(parent: LuaGuiElement, player_name: string): LuaGuiElement
--- @field data table<LuaGuiElement, LuaPlayer>
--- @overload fun(parent: LuaGuiElement, selected_player: LuaPlayer): LuaGuiElement
Elements.open_action_bar = Gui.define("player_list/open_action_bar")
:draw{
type = "sprite-button",
@@ -163,9 +46,21 @@ Elements.open_action_bar = Gui.define("player_list/open_action_bar")
)
:on_click(function(def, player, element)
--- @cast def ExpGui_PlayerList.elements.open_action_bar
toggle_selected_player(player, def.data[element])
Elements.container.toggle_selected_player(player, def.data[element])
Elements.player_table.refresh_player(player)
end) --[[ @as any ]]
--- Set whether an open action bar button shows as highlighted
--- @param open_button LuaGuiElement
--- @param highlighted boolean
function Elements.open_action_bar.set_highlight(open_button, highlighted)
open_button.style = highlighted and "tool_button" or "frame_button"
local style = open_button.style
style.padding = -2
style.width = 8
style.height = 14
end
--- Button used to close the action bar
--- @class ExpGui_PlayerList.elements.close_action_bar: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
@@ -182,8 +77,9 @@ Elements.close_action_bar = Gui.define("player_list/close_action_bar")
top_margin = -1,
right_margin = -1,
})
:on_click(function(def, player, element)
set_selected_player(player, nil)
:on_click(function(_, player)
Elements.container.set_selected_player(player, nil)
Elements.player_table.refresh_player(player)
end) --[[ @as any ]]
--- Button used to confirm a reason
@@ -202,8 +98,8 @@ Elements.reason_confirm = Gui.define("player_list/reason_confirm")
left_margin = -2,
right_margin = -1,
})
:on_click(function(def, player, element)
local action_name = get_selected_action(player)
:on_click(function(_, player, element)
local action_name = Elements.container.get_selected_action(player)
local button_data = action_name and config.buttons[action_name]
if button_data and button_data.reason_callback then
local reason = element.parent.entry.text
@@ -211,13 +107,14 @@ Elements.reason_confirm = Gui.define("player_list/reason_confirm")
button_data.reason_callback(player, reason)
end
element.parent.entry.text = ""
set_selected_player(player, nil)
Elements.container.set_selected_player(player, nil)
Elements.player_table.refresh_player(player)
end) --[[ @as any ]]
--- Clickable player name label, left click opens the map and right click toggles the action bar
--- @class ExpGui_PlayerList.elements.player_name_label: ExpElement
--- @field data table<LuaGuiElement, string>
--- @overload fun(parent: LuaGuiElement, opts: { name: string, tooltip: LocalisedString }): LuaGuiElement
--- @field data table<LuaGuiElement, LuaPlayer>
--- @overload fun(parent: LuaGuiElement, opts: { name: string, player: LuaPlayer, tooltip: LocalisedString }): LuaGuiElement
Elements.player_name_label = Gui.define("player_list/player_name_label")
:draw{
type = "label",
@@ -228,12 +125,11 @@ Elements.player_name_label = Gui.define("player_list/player_name_label")
padding = { 0, 2, 0, 0 },
}
:element_data(
Gui.from_argument("name")
Gui.from_argument("player")
)
:on_click(function(def, player, element, event)
--- @cast def ExpGui_PlayerList.elements.player_name_label
local selected_player_name = def.data[element]
local selected_player = game.players[selected_player_name]
local selected_player = def.data[element]
if event.button == defines.mouse_button_type.left then
-- Left click opens remote view at the player
player.set_controller{
@@ -243,66 +139,11 @@ Elements.player_name_label = Gui.define("player_list/player_name_label")
}
else
-- Right click toggles the action bar
toggle_selected_player(player, selected_player_name)
Elements.container.toggle_selected_player(player, selected_player)
Elements.player_table.refresh_player(player)
end
end) --[[ @as any ]]
--- @class ExpGui_PlayerList.player_data
--- @field name string
--- @field tag string
--- @field role_name string
--- @field chat_color Color
--- @field caption LocalisedString
--- @field tooltip LocalisedString
--- Get the caption and tooltip for a player time
--- @param online_time number
--- @param afk_time number
--- @return LocalisedString, LocalisedString
local function get_time_formats(online_time, afk_time)
local tick = game.tick > 0 and game.tick or 1
local percent = math.round(online_time / tick, 3) * 100
local caption = online_time_format(online_time)
local tooltip = { "exp-gui_player-list.afk-time", percent, afk_time_format(afk_time) }
return caption, tooltip
end
--- Get a list of all online players sorted by their highest role
--- @return ExpGui_PlayerList.player_data[]
local function get_player_list_order()
-- Sort all the online players into roles
local players = {}
for _, player in pairs(game.connected_players) do
local highest_role = Roles.get_player_highest_role(player)
if not players[highest_role.name] then
players[highest_role.name] = {}
end
table.insert(players[highest_role.name], player)
end
-- Flatten the roles into a single ordered list
local count = 0
local player_list_order = {}
for _, role_name in pairs(Roles.config.order) do
if players[role_name] then
for _, player in pairs(players[role_name]) do
count = count + 1
local caption, tooltip = get_time_formats(player.online_time, player.afk_time)
player_list_order[count] = {
name = player.name,
tag = player.tag,
role_name = role_name,
chat_color = player.chat_color,
caption = caption,
tooltip = tooltip,
}
end
end
end
return player_list_order
end
--- @class ExpGui_PlayerList.elements.player_table.row
--- @field open_button LuaGuiElement
--- @field name_label LuaGuiElement
@@ -319,48 +160,119 @@ end
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.player_table = Gui.define("player_list/player_table")
:track_all_elements()
:draw(function(def, parent)
local scroll_table = Gui.elements.scroll_table(parent, 184, 3, "scroll")
scroll_table.style.padding = { 1, 0, 1, 2 }
return scroll_table
end) --[[ @as any ]]
:draw(function(_, parent)
return Gui.elements.scroll_table(parent, 184, 3, "scroll")
end)
:style{
padding = { 1, 0, 1, 2 },
} --[[ @as any ]]
--- Store the action and reason bars associated with a player table
--- @param player_table LuaGuiElement
--- @param action_bar LuaGuiElement
--- @param reason_bar LuaGuiElement
function Elements.player_table.set_bars(player_table, action_bar, reason_bar)
Elements.player_table.data[player_table] = {
rows = {},
action_bar = action_bar,
reason_bar = reason_bar,
}
end
--- @class ExpGui_PlayerList.elements.player_table.row_data
--- @field player LuaPlayer
--- @field tag string
--- @field role_name string
--- @field chat_color Color
--- @field caption LocalisedString
--- @field tooltip LocalisedString
--- Calculate the ordered list of online players sorted by their highest role
--- @return ExpGui_PlayerList.elements.player_table.row_data[]
function Elements.player_table.calculate_row_data()
-- Sort all the online players into roles
local players = {}
for _, player in pairs(game.connected_players) do
local highest_role = Roles.get_player_highest_role(player)
if not players[highest_role.name] then
players[highest_role.name] = {}
end
table.insert(players[highest_role.name], player)
end
-- Flatten the roles into a single ordered list
local count = 0
local row_data = {}
for _, role_name in pairs(Roles.config.order) do
if players[role_name] then
for _, player in pairs(players[role_name]) do
count = count + 1
local caption, tooltip = get_time_formats(player.online_time, player.afk_time)
row_data[count] = {
player = player,
tag = player.tag,
role_name = role_name,
chat_color = player.chat_color,
caption = caption,
tooltip = tooltip,
}
end
end
end
return row_data
end
--- Calculate the latest play time captions for each online player keyed by name
--- @return table<string, { caption: LocalisedString, tooltip: LocalisedString }>
function Elements.player_table.calculate_time_data()
local time_data = {}
for _, player in pairs(game.connected_players) do
local caption, tooltip = get_time_formats(player.online_time, player.afk_time)
time_data[player.name] = { caption = caption, tooltip = tooltip }
end
return time_data
end
--- Add a player row to the table and store its elements
--- @param player_table LuaGuiElement
--- @param player_data ExpGui_PlayerList.player_data
function Elements.player_table.add_row(player_table, player_data)
--- @param row_data ExpGui_PlayerList.elements.player_table.row_data
function Elements.player_table.add_row(player_table, row_data)
local rows = Elements.player_table.data[player_table].rows
local player = row_data.player
-- Add the button used to open the action bar
local open_flow = player_table.add{ type = "flow" }
local open_button = Elements.open_action_bar(open_flow, player_data.name)
local open_button = Elements.open_action_bar(open_flow, player)
-- Add the clickable player name
local name_label = Elements.player_name_label(player_table, {
name = player_data.name,
tooltip = { "exp-gui_player-list.open-map", player_data.name, player_data.tag, player_data.role_name },
name = player.name,
player = player,
tooltip = { "exp-gui_player-list.open-map", player.name, row_data.tag, row_data.role_name },
})
name_label.style.font_color = player_data.chat_color
name_label.style.font_color = row_data.chat_color
-- Add the time played label
local alignment = Gui.elements.aligned_flow(player_table)
local time_label = alignment.add{
type = "label",
caption = player_data.caption,
tooltip = player_data.tooltip,
caption = row_data.caption,
tooltip = row_data.tooltip,
}
time_label.style.padding = 0
rows[player_data.name] = { open_button = open_button, name_label = name_label, time_label = time_label }
rows[player.name] = { open_button = open_button, name_label = name_label, time_label = time_label }
end
--- Rebuild all rows of a player table, used when the sort order changes
--- @param player_table LuaGuiElement
function Elements.player_table.rebuild(player_table)
--- @param row_data ExpGui_PlayerList.elements.player_table.row_data[]
function Elements.player_table.rebuild(player_table, row_data)
player_table.clear()
Elements.player_table.data[player_table].rows = {}
for _, player_data in ipairs(get_player_list_order()) do
Elements.player_table.add_row(player_table, player_data)
for _, row in ipairs(row_data) do
Elements.player_table.add_row(player_table, row)
end
end
@@ -377,27 +289,54 @@ function Elements.player_table.remove_row(player_table, player_name)
Gui.destroy_if_valid(row.time_label.parent)
end
--- Refresh the time labels for all rows of a player table
--- Refresh the time labels of a player table from precomputed time data
--- @param player_table LuaGuiElement
function Elements.player_table.refresh_times(player_table)
--- @param time_data table<string, { caption: LocalisedString, tooltip: LocalisedString }>
function Elements.player_table.refresh_times(player_table, time_data)
for player_name, row in pairs(Elements.player_table.data[player_table].rows) do
local listed_player = game.players[player_name]
if listed_player and listed_player.connected then
local caption, tooltip = get_time_formats(listed_player.online_time, listed_player.afk_time)
row.time_label.caption = caption
row.time_label.tooltip = tooltip
local data = time_data[player_name]
if data then
row.time_label.caption = data.caption
row.time_label.tooltip = data.tooltip
end
end
end
--- Build the action bar footer containing the close button and the per action button flows
--- @param container LuaGuiElement
--- @return LuaGuiElement
local function build_action_bar(container)
local action_bar = Gui.elements.subframe_base(container, "subfooter_frame", "action_bar")
local style = action_bar.style
style.height = 35
style.padding = { 1, 3 }
--- Refresh the action bar, reason bar and row highlights for a player to match their selection
--- @param player LuaPlayer
function Elements.player_table.refresh_player(player)
-- Clear the selection if the selected player has gone offline
local selected_player = Elements.container.get_selected_player(player)
if selected_player and not selected_player.connected then
Elements.container.set_selected_player(player, nil)
selected_player = nil
end
local selected_action = Elements.container.get_selected_action(player)
for _, player_table in Elements.player_table:online_elements(player) do
local element_data = Elements.player_table.data[player_table]
-- Update the action bar and reason bar visibility
if selected_player then
Elements.action_bar.refresh(element_data.action_bar, player, selected_player)
else
element_data.action_bar.visible = false
end
element_data.reason_bar.visible = selected_player ~= nil and selected_action ~= nil
-- Highlight the open button of the selected player
for player_name, row in pairs(element_data.rows) do
Elements.open_action_bar.set_highlight(row.open_button, selected_player ~= nil and player_name == selected_player.name)
end
end
end
--- Action bar footer holding the close button and the per action button flows
--- @class ExpGui_PlayerList.elements.action_bar: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.action_bar = Gui.define("player_list/action_bar")
:draw(function(_, parent)
local action_bar = Gui.elements.subframe_base(parent, "subfooter_frame", "action_bar")
action_bar.visible = false
Elements.close_action_bar(action_bar)
@@ -410,16 +349,34 @@ local function build_action_bar(container)
end
return action_bar
end)
:style{
height = 35,
padding = { 1, 3 },
} --[[ @as any ]]
--- Update the action bar buttons to match the selection for a player
--- @param action_bar LuaGuiElement
--- @param player LuaPlayer
--- @param selected_player LuaPlayer
function Elements.action_bar.refresh(action_bar, player, selected_player)
action_bar.visible = true
for action_name, buttons in pairs(config.buttons) do
local flow = action_bar[action_name]
if buttons.auth and not buttons.auth(player, selected_player) then
flow.visible = false
else
flow.visible = Roles.player_allowed(player, action_name)
end
end
end
--- Build the reason bar footer containing the reason entry and confirm button
--- @param container LuaGuiElement
--- @return LuaGuiElement
local function build_reason_bar(container)
local reason_bar = Gui.elements.subframe_base(container, "subfooter_frame", "reason_bar")
local style = reason_bar.style
style.height = 35
style.padding = { -1, 3 }
--- Reason bar footer holding the reason entry and confirm button
--- @class ExpGui_PlayerList.elements.reason_bar: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.reason_bar = Gui.define("player_list/reason_bar")
:draw(function(_, parent)
local reason_bar = Gui.elements.subframe_base(parent, "subfooter_frame", "reason_bar")
reason_bar.visible = false
local reason_field = reason_bar.add{
@@ -434,34 +391,97 @@ local function build_reason_bar(container)
entry_style.minimal_width = 160
Elements.reason_confirm(reason_bar)
return reason_bar
end
end)
:style{
height = 35,
padding = { -1, 3 },
} --[[ @as any ]]
--- @class ExpGui_PlayerList.elements.container.selection
--- @field selected_player LuaPlayer?
--- @field selected_action string?
--- Container added to the left gui flow
--- @class ExpGui_PlayerList.elements.container: ExpElement
--- @field data table<LuaPlayer, ExpGui_PlayerList.selection>
--- @field data table<LuaPlayer, ExpGui_PlayerList.elements.container.selection>
Elements.container = Gui.define("player_list/container")
:draw(function(def, parent)
:draw(function(_, parent)
local container = Gui.elements.container(parent)
local player = Gui.get_player(parent)
def.data[player] = def.data[player] or {} -- Selection state for this player
local player_table = Elements.player_table(container)
local action_bar = build_action_bar(container)
local reason_bar = build_reason_bar(container)
local action_bar = Elements.action_bar(container)
local reason_bar = Elements.reason_bar(container)
Elements.player_table.set_bars(player_table, action_bar, reason_bar)
Elements.player_table.data[player_table] = {
rows = {},
action_bar = action_bar,
reason_bar = reason_bar,
}
Elements.player_table.rebuild(player_table)
local row_data = Elements.player_table.calculate_row_data()
Elements.player_table.rebuild(player_table, row_data)
return Gui.elements.container.get_root_element(container)
end) --[[ @as any ]]
--- Get or create the selection state for a player
--- @param player LuaPlayer
--- @return ExpGui_PlayerList.elements.container.selection
function Elements.container._get_selection(player)
local selection = Elements.container.data[player]
if not selection then
selection = {}
Elements.container.data[player] = selection
end
return selection
end
--- Get the player that actions will be performed on
--- @param player LuaPlayer
--- @return LuaPlayer?
function Elements.container.get_selected_player(player)
return Elements.container._get_selection(player).selected_player
end
--- Get the action awaiting a reason
--- @param player LuaPlayer
--- @return string?
function Elements.container.get_selected_action(player)
return Elements.container._get_selection(player).selected_action
end
--- Set the player that actions will be performed on, nil to clear the selection
--- @param player LuaPlayer
--- @param selected_player LuaPlayer?
function Elements.container.set_selected_player(player, selected_player)
local selection = Elements.container._get_selection(player)
selection.selected_player = selected_player
if not selected_player then
selection.selected_action = nil
end
end
--- Toggle the player that actions will be performed on
--- @param player LuaPlayer
--- @param selected_player LuaPlayer
function Elements.container.toggle_selected_player(player, selected_player)
if Elements.container.get_selected_player(player) == selected_player then
Elements.container.set_selected_player(player, nil)
else
Elements.container.set_selected_player(player, selected_player)
end
end
--- Set the action awaiting a reason, nil to clear it
--- @param player LuaPlayer
--- @param selected_action string?
function Elements.container.set_selected_action(player, selected_action)
Elements.container._get_selection(player).selected_action = selected_action
end
-- Inject the accessors the actions config needs; setting an action also refreshes the gui to show the reason bar
local function select_action(player, selected_action)
Elements.container.set_selected_action(player, selected_action)
Elements.player_table.refresh_player(player)
end
config.set_accessors(Elements.container.get_selected_player, select_action)
--- Add the element to the left flow with a toolbar button
Gui.add_left_element(Elements.container, true)
Gui.toolbar.create_button{
@@ -474,18 +494,20 @@ Gui.toolbar.create_button{
end
}
--- Refresh the play times for all online players
local function refresh_times()
--- Rebuild the player list for all online players, used when the sort order changes
local function redraw_player_list()
local row_data = Elements.player_table.calculate_row_data()
for _, player_table in Elements.player_table:online_elements() do
Elements.player_table.refresh_times(player_table)
Elements.player_table.rebuild(player_table, row_data)
Elements.player_table.refresh_player(Gui.get_player(player_table))
end
end
--- Rebuild the player list for all online players, used when the sort order changes
local function redraw_player_list()
--- Refresh the play times for all online players
local function refresh_player_times()
local time_data = Elements.player_table.calculate_time_data()
for _, player_table in Elements.player_table:online_elements() do
Elements.player_table.rebuild(player_table)
update_player_ui(Gui.get_player(player_table))
Elements.player_table.refresh_times(player_table, time_data)
end
end
@@ -496,8 +518,9 @@ local function on_player_left_game(event)
for _, player_table in Elements.player_table:online_elements() do
Elements.player_table.remove_row(player_table, left_player.name)
local viewing_player = Gui.get_player(player_table)
if get_selected_player(viewing_player) == left_player.name then
set_selected_player(viewing_player, nil)
if Elements.container.get_selected_player(viewing_player) == left_player then
Elements.container.set_selected_player(viewing_player, nil)
Elements.player_table.refresh_player(viewing_player)
end
end
end
@@ -513,6 +536,6 @@ return {
[Roles.events.on_role_unassigned] = redraw_player_list,
},
on_nth_tick = {
[1800] = refresh_times,
[1800] = refresh_player_times,
}
}