From b1c0f2535bc2ac743821996eeea42e44a33e36bf Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:07:03 +0000 Subject: [PATCH 1/6] Migrate player list gui to scenario Move modules/gui/player-list into exp_scenario/gui/player_list using the new exp_gui element API, matching the other scenario guis. - Store the selected player and action on the container's player data instead of a datastore, folding the datastore on_update reactivity into explicit setter functions - Switch the actions config to injected accessor functions and the exp-gui_player-list locale namespace - Add the locale section, register in control.lua, and disable the legacy module in the file loader Co-Authored-By: Claude Opus 4.8 (1M context) --- exp_legacy/module/config/_file_loader.lua | 2 +- .../module/config/gui/player_list_actions.lua | 35 +- exp_scenario/module/control.lua | 1 + exp_scenario/module/gui/player_list.lua | 518 ++++++++++++++++++ exp_scenario/module/locale/en.cfg | 16 + 5 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 exp_scenario/module/gui/player_list.lua diff --git a/exp_legacy/module/config/_file_loader.lua b/exp_legacy/module/config/_file_loader.lua index 91be3f15..45461057 100644 --- a/exp_legacy/module/config/_file_loader.lua +++ b/exp_legacy/module/config/_file_loader.lua @@ -24,7 +24,7 @@ return { --- GUI "modules.gui.rocket-info", "modules.gui.warp-list", - "modules.gui.player-list", + -- "modules.gui.player-list", -- migrated to exp_scenario/gui/player_list "modules.gui.vlayer", "modules.gui._role_updates", diff --git a/exp_legacy/module/config/gui/player_list_actions.lua b/exp_legacy/module/config/gui/player_list_actions.lua index 3c1d675c..2e5e2094 100644 --- a/exp_legacy/module/config/gui/player_list_actions.lua +++ b/exp_legacy/module/config/gui/player_list_actions.lua @@ -14,9 +14,10 @@ local Jail = require("modules.exp_legacy.modules.control.jail") --- @dep modules local Colors = require("modules/exp_util/include/color") local format_player_name = ExpUtil.format_player_name_locale -local SelectedPlayer, SelectedAction -local function set_datastores(player, action) - SelectedPlayer, SelectedAction = player, action +--- Accessors injected by the gui so the actions can read the selected player and set the selected action +local get_selected_player, set_selected_action +local function set_accessors(player_getter, action_setter) + get_selected_player, set_selected_action = player_getter, action_setter end -- auth that will only allow when on player's of lower roles @@ -30,7 +31,7 @@ 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 = SelectedPlayer:get(player) + local selected_player_name = get_selected_player(player) local selected_player = game.players[selected_player_name] local selected_player_color = format_player_name(selected_player) return selected_player_name, selected_player_color @@ -63,7 +64,7 @@ end --- Teleports the user to the action player -- @element goto_player -local goto_player = new_button("utility/export", { "player-list.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] @@ -76,7 +77,7 @@ local goto_player = new_button("utility/export", { "player-list.goto-player" }) --- Teleports the action player to the user -- @element bring_player -local bring_player = new_button("utility/import", { "player-list.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] @@ -89,13 +90,13 @@ local bring_player = new_button("utility/import", { "player-list.bring-player" } --- Reports the action player, requires a reason to be given -- @element report_player -local report_player = new_button("utility/spawn_flag", { "player-list.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 player.print({ "exp-commands_report.already-reported" }, Colors.orange_red) else - SelectedAction:set(player, "command/report") + set_selected_action(player, "command/report") end end) @@ -109,9 +110,9 @@ end --- Gives the action player a warning, requires a reason -- @element warn_player -local warn_player = new_button("utility/spawn_flag", { "player-list.warn-player" }) +local warn_player = new_button("utility/spawn_flag", { "exp-gui_player-list.warn-player" }) :on_click(function(def, player, element) - SelectedAction:set(player, "command/give-warning") + set_selected_action(player, "command/give-warning") end) local function warn_player_callback(player, reason) @@ -123,13 +124,13 @@ end --- Jails the action player, requires a reason -- @element jail_player -local jail_player = new_button("utility/multiplayer_waiting_icon", { "player-list.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 player.print({ "exp-commands_jail.already-jailed", selected_player_color }, Colors.orange_red) else - SelectedAction:set(player, "command/jail") + set_selected_action(player, "command/jail") end end) @@ -142,9 +143,9 @@ end --- Kicks the action player, requires a reason -- @element kick_player -local kick_player = new_button("utility/warning_icon", { "player-list.kick-player" }) +local kick_player = new_button("utility/warning_icon", { "exp-gui_player-list.kick-player" }) :on_click(function(def, player, element) - SelectedAction:set(player, "command/kick") + set_selected_action(player, "command/kick") end) local function kick_player_callback(player, reason) @@ -154,9 +155,9 @@ end --- Bans the action player, requires a reason -- @element ban_player -local ban_player = new_button("utility/danger_icon", { "player-list.ban-player" }) +local ban_player = new_button("utility/danger_icon", { "exp-gui_player-list.ban-player" }) :on_click(function(def, player, element) - SelectedAction:set(player, "command/ban") + set_selected_action(player, "command/ban") end) local function ban_player_callback(player, reason) @@ -165,7 +166,7 @@ local function ban_player_callback(player, reason) end return { - set_datastores = set_datastores, + set_accessors = set_accessors, buttons = { ["command/teleport"] = { auth = function(player, selected_player) diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 65e35d78..83fe6e25 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -73,6 +73,7 @@ add(require("modules/exp_scenario/gui/elements")) add(require("modules/exp_scenario/gui/landfill_blueprint")) add(require("modules/exp_scenario/gui/module_inserter")) add(require("modules/exp_scenario/gui/player_bonus")) +add(require("modules/exp_scenario/gui/player_list")) add(require("modules/exp_scenario/gui/player_stats")) add(require("modules/exp_scenario/gui/production_stats")) add(require("modules/exp_scenario/gui/quick_actions")) diff --git a/exp_scenario/module/gui/player_list.lua b/exp_scenario/module/gui/player_list.lua new file mode 100644 index 00000000..0dc4583d --- /dev/null +++ b/exp_scenario/module/gui/player_list.lua @@ -0,0 +1,518 @@ +--[[-- 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") +local Gui = require("modules/exp_gui") +local Roles = require("modules/exp_legacy/expcore/roles") +local config = require("modules/exp_legacy/config/gui/player_list_actions") + +--- @class ExpGui_PlayerList.elements +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 + 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 +--- @overload fun(parent: LuaGuiElement, player_name: string): LuaGuiElement +Elements.open_action_bar = Gui.define("player_list/open_action_bar") + :draw{ + type = "sprite-button", + sprite = "utility/expand_dots", + tooltip = { "exp-gui_player-list.open-action-bar" }, + style = "frame_button", + } + :style{ + padding = -2, + width = 8, + height = 14, + } + :element_data( + Gui.from_argument(1) + ) + :on_click(function(def, player, element) + --- @cast def ExpGui_PlayerList.elements.open_action_bar + toggle_selected_player(player, def.data[element]) + end) --[[ @as any ]] + +--- Button used to close the action bar +--- @class ExpGui_PlayerList.elements.close_action_bar: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.close_action_bar = Gui.define("player_list/close_action_bar") + :draw{ + type = "sprite-button", + sprite = "utility/close_black", + tooltip = { "exp-gui_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) + set_selected_player(player, nil) + end) --[[ @as any ]] + +--- Button used to confirm a reason +--- @class ExpGui_PlayerList.elements.reason_confirm: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.reason_confirm = Gui.define("player_list/reason_confirm") + :draw{ + type = "sprite-button", + sprite = "utility/confirm_slot", + tooltip = { "exp-gui_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 action_name = 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 + if reason == nil or not reason:find("%S") then reason = "no reason given" end + button_data.reason_callback(player, reason) + end + element.parent.entry.text = "" + set_selected_player(player, nil) + 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 +--- @overload fun(parent: LuaGuiElement, opts: { name: string, tooltip: LocalisedString }): LuaGuiElement +Elements.player_name_label = Gui.define("player_list/player_name_label") + :draw{ + type = "label", + caption = Gui.from_argument("name"), + tooltip = Gui.from_argument("tooltip"), + } + :style{ + padding = { 0, 2, 0, 0 }, + } + :element_data( + Gui.from_argument("name") + ) + :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] + if event.button == defines.mouse_button_type.left then + -- Left click opens remote view at the player + player.set_controller{ + type = defines.controllers.remote, + position = selected_player.physical_position, + surface = selected_player.physical_surface, + } + else + -- Right click toggles the action bar + toggle_selected_player(player, selected_player_name) + 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 +--- @field time_label LuaGuiElement + +--- @class ExpGui_PlayerList.elements.player_table.data +--- @field rows table +--- @field action_bar LuaGuiElement +--- @field reason_bar LuaGuiElement + +--- Scroll table containing a row for each online player +--- @class ExpGui_PlayerList.elements.player_table: ExpElement +--- @field data table +--- @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 ]] + +--- 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) + local rows = Elements.player_table.data[player_table].rows + + -- 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) + + -- 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_label.style.font_color = player_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, + } + time_label.style.padding = 0 + + rows[player_data.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) + 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) + end +end + +--- Remove a single player row from the table +--- @param player_table LuaGuiElement +--- @param player_name string +function Elements.player_table.remove_row(player_table, player_name) + local rows = Elements.player_table.data[player_table].rows + local row = rows[player_name] + if not row then return end + rows[player_name] = nil + Gui.destroy_if_valid(row.open_button.parent) + Gui.destroy_if_valid(row.name_label) + Gui.destroy_if_valid(row.time_label.parent) +end + +--- Refresh the time labels for all rows of a player table +--- @param player_table LuaGuiElement +function Elements.player_table.refresh_times(player_table) + 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 + 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 } + action_bar.visible = false + + Elements.close_action_bar(action_bar) + for action_name, button_data in pairs(config.buttons) do + local permission_flow = action_bar.add{ type = "flow", name = action_name } + permission_flow.visible = false + for _, button in ipairs(button_data) do + button(permission_flow) + end + end + + return action_bar +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.visible = false + + local reason_field = reason_bar.add{ + name = "entry", + type = "textfield", + style = "stretchable_textfield", + tooltip = { "exp-gui_player-list.reason-entry" }, + } + local entry_style = reason_field.style + entry_style.padding = 0 + entry_style.height = 28 + entry_style.minimal_width = 160 + + Elements.reason_confirm(reason_bar) + + return reason_bar +end + +--- Container added to the left gui flow +--- @class ExpGui_PlayerList.elements.container: ExpElement +--- @field data table +Elements.container = Gui.define("player_list/container") + :draw(function(def, 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) + + Elements.player_table.data[player_table] = { + rows = {}, + action_bar = action_bar, + reason_bar = reason_bar, + } + Elements.player_table.rebuild(player_table) + + return Gui.elements.container.get_root_element(container) + end) --[[ @as any ]] + +--- Add the element to the left flow with a toolbar button +Gui.add_left_element(Elements.container, true) +Gui.toolbar.create_button{ + name = "toggle_player_list", + left_element = Elements.container, + sprite = "entity/character", + tooltip = { "exp-gui_player-list.main-tooltip" }, + visible = function(player, element) + return Roles.player_allowed(player, "gui/player-list") + end +} + +--- Refresh the play times for all online players +local function refresh_times() + for _, player_table in Elements.player_table:online_elements() do + Elements.player_table.refresh_times(player_table) + end +end + +--- Rebuild the player list for all online players, used when the sort order changes +local function redraw_player_list() + for _, player_table in Elements.player_table:online_elements() do + Elements.player_table.rebuild(player_table) + update_player_ui(Gui.get_player(player_table)) + end +end + +--- Remove a player from the list when they leave and clear any selection pointing at them +--- @param event EventData.on_player_left_game +local function on_player_left_game(event) + local left_player = game.players[event.player_index] + 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) + end + end +end + +local e = defines.events + +return { + elements = Elements, + events = { + [e.on_player_joined_game] = redraw_player_list, + [e.on_player_left_game] = on_player_left_game, + [Roles.events.on_role_assigned] = redraw_player_list, + [Roles.events.on_role_unassigned] = redraw_player_list, + }, + on_nth_tick = { + [1800] = refresh_times, + } +} diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index d2c5cf99..6a02d27a 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -324,6 +324,22 @@ tooltip-character_reach_distance_bonus=Character reach distance bonus caption-personal_battery_recharge=Battery tooltip-personal_battery_recharge=Armor battery recharge +[exp-gui_player-list] +main-tooltip=Player List +open-action-bar=Options +close-action-bar=Close Options +reason-confirm=Confirm Reason +reason-entry=Enter Reason +goto-player=Goto player +bring-player=Bring player +report-player=Report player +warn-player=Warn player +jail-player=Jail player +kick-player=Kick player +ban-player=Ban player +afk-time=__1__% of total map time\nLast moved __2__ ago +open-map=__1__ __2__\n__3__\nClick to open map + [exp-gui_player-stats] tooltip-main=Player Stats caption-main=Player Stats From 4bd539426853703c13b90d43eb63886627023a48 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:10:20 +0000 Subject: [PATCH 2/6] 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) --- exp_legacy/module/config/_file_loader.lua | 1 - .../module/config/gui/player_list_actions.lua | 37 +- exp_legacy/module/modules/gui/player-list.lua | 448 ------------- exp_scenario/module/gui/player_list.lua | 587 +++++++++--------- 4 files changed, 322 insertions(+), 751 deletions(-) delete mode 100644 exp_legacy/module/modules/gui/player-list.lua diff --git a/exp_legacy/module/config/_file_loader.lua b/exp_legacy/module/config/_file_loader.lua index 45461057..fba1c006 100644 --- a/exp_legacy/module/config/_file_loader.lua +++ b/exp_legacy/module/config/_file_loader.lua @@ -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", diff --git a/exp_legacy/module/config/gui/player_list_actions.lua b/exp_legacy/module/config/gui/player_list_actions.lua index 2e5e2094..15b03f33 100644 --- a/exp_legacy/module/config/gui/player_list_actions.lua +++ b/exp_legacy/module/config/gui/player_list_actions.lua @@ -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 diff --git a/exp_legacy/module/modules/gui/player-list.lua b/exp_legacy/module/modules/gui/player-list.lua deleted file mode 100644 index 56183c8f..00000000 --- a/exp_legacy/module/modules/gui/player-list.lua +++ /dev/null @@ -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) diff --git a/exp_scenario/module/gui/player_list.lua b/exp_scenario/module/gui/player_list.lua index 0dc4583d..6dd02289 100644 --- a/exp_scenario/module/gui/player_list.lua +++ b/exp_scenario/module/gui/player_list.lua @@ -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 - end - return 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 ---- 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 ---- @overload fun(parent: LuaGuiElement, player_name: string): LuaGuiElement +--- @field data table +--- @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 ---- @overload fun(parent: LuaGuiElement, opts: { name: string, tooltip: LocalisedString }): LuaGuiElement +--- @field data table +--- @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 +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,91 +289,199 @@ 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 +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 } - action_bar.visible = false +--- 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) - Elements.close_action_bar(action_bar) - for action_name, button_data in pairs(config.buttons) do - local permission_flow = action_bar.add{ type = "flow", name = action_name } - permission_flow.visible = false - for _, button in ipairs(button_data) do - button(permission_flow) + 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 - - return action_bar 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.visible = false +--- 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 - local reason_field = reason_bar.add{ - name = "entry", - type = "textfield", - style = "stretchable_textfield", - tooltip = { "exp-gui_player-list.reason-entry" }, - } - local entry_style = reason_field.style - entry_style.padding = 0 - entry_style.height = 28 - entry_style.minimal_width = 160 + Elements.close_action_bar(action_bar) + for action_name, button_data in pairs(config.buttons) do + local permission_flow = action_bar.add{ type = "flow", name = action_name } + permission_flow.visible = false + for _, button in ipairs(button_data) do + button(permission_flow) + end + end - Elements.reason_confirm(reason_bar) + return action_bar + end) + :style{ + height = 35, + padding = { 1, 3 }, + } --[[ @as any ]] - return reason_bar +--- 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 +--- 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{ + name = "entry", + type = "textfield", + style = "stretchable_textfield", + tooltip = { "exp-gui_player-list.reason-entry" }, + } + local entry_style = reason_field.style + entry_style.padding = 0 + entry_style.height = 28 + entry_style.minimal_width = 160 + + Elements.reason_confirm(reason_bar) + return reason_bar + 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 +--- @field data table 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, } } From df129b5e9acaf7ea1df141ed304c65d669a1138a Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:22:37 +0000 Subject: [PATCH 3/6] Address player list review feedback - Rename the player table element data class to element_data - Remove the redundant offline selection clear; on_player_left_game already clears any selection pointing at a player who disconnects - Move the action bar and reason bar visibility into their own refresh methods so refresh_player just delegates to them Co-Authored-By: Claude Opus 4.8 (1M context) --- exp_scenario/module/gui/player_list.lua | 36 +++++++++++++------------ 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/exp_scenario/module/gui/player_list.lua b/exp_scenario/module/gui/player_list.lua index 6dd02289..52509ebc 100644 --- a/exp_scenario/module/gui/player_list.lua +++ b/exp_scenario/module/gui/player_list.lua @@ -149,14 +149,14 @@ Elements.player_name_label = Gui.define("player_list/player_name_label") --- @field name_label LuaGuiElement --- @field time_label LuaGuiElement ---- @class ExpGui_PlayerList.elements.player_table.data +--- @class ExpGui_PlayerList.elements.player_table.element_data --- @field rows table --- @field action_bar LuaGuiElement --- @field reason_bar LuaGuiElement --- Scroll table containing a row for each online player --- @class ExpGui_PlayerList.elements.player_table: ExpElement ---- @field data table +--- @field data table --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.player_table = Gui.define("player_list/player_table") :track_all_elements() @@ -305,24 +305,13 @@ end --- 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 + Elements.action_bar.refresh(element_data.action_bar, player, selected_player) + Elements.reason_bar.refresh(element_data.reason_bar, selected_player, selected_action) -- Highlight the open button of the selected player for player_name, row in pairs(element_data.rows) do @@ -355,11 +344,16 @@ Elements.action_bar = Gui.define("player_list/action_bar") padding = { 1, 3 }, } --[[ @as any ]] ---- Update the action bar buttons to match the selection for a player +--- Update the action bar buttons to match the selection for a player, hidden when nothing is selected --- @param action_bar LuaGuiElement --- @param player LuaPlayer ---- @param selected_player LuaPlayer +--- @param selected_player LuaPlayer? function Elements.action_bar.refresh(action_bar, player, selected_player) + if not selected_player then + action_bar.visible = false + return + end + action_bar.visible = true for action_name, buttons in pairs(config.buttons) do local flow = action_bar[action_name] @@ -398,6 +392,14 @@ Elements.reason_bar = Gui.define("player_list/reason_bar") padding = { -1, 3 }, } --[[ @as any ]] +--- Update the reason bar visibility to match the selection, shown only while an action awaits a reason +--- @param reason_bar LuaGuiElement +--- @param selected_player LuaPlayer? +--- @param selected_action string? +function Elements.reason_bar.refresh(reason_bar, selected_player, selected_action) + reason_bar.visible = selected_player ~= nil and selected_action ~= nil +end + --- @class ExpGui_PlayerList.elements.container.selection --- @field selected_player LuaPlayer? --- @field selected_action string? From 31c18c7851ff52b05a05918250a8446b27c56189 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:41:49 +0100 Subject: [PATCH 4/6] Style and locale fixes --- exp_legacy/module/config/gui/player_list_actions.lua | 4 ++-- exp_scenario/module/commands/clear_inventory.lua | 2 +- exp_scenario/module/gui/player_list.lua | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/exp_legacy/module/config/gui/player_list_actions.lua b/exp_legacy/module/config/gui/player_list_actions.lua index 15b03f33..f692b47f 100644 --- a/exp_legacy/module/config/gui/player_list_actions.lua +++ b/exp_legacy/module/config/gui/player_list_actions.lua @@ -67,7 +67,7 @@ local goto_player = new_button("utility/export", { "exp-gui_player-list.goto-pla :on_click(function(def, player, element) 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) + player.print({ "exp-commands-parse.player-alive" }, Colors.orange_red) else teleport(player, selected_player) end @@ -79,7 +79,7 @@ local bring_player = new_button("utility/import", { "exp-gui_player-list.bring-p :on_click(function(def, player, element) 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) + player.print({ "exp-commands-parse.player-alive" }, Colors.orange_red) else teleport(selected_player, player) end diff --git a/exp_scenario/module/commands/clear_inventory.lua b/exp_scenario/module/commands/clear_inventory.lua index 9a37fc84..ed17db7a 100644 --- a/exp_scenario/module/commands/clear_inventory.lua +++ b/exp_scenario/module/commands/clear_inventory.lua @@ -14,7 +14,7 @@ Commands.new("clear-inventory", { "exp-commands_clear-inventory.description" }) :register(function(player, other_player) local inventory = other_player.get_main_inventory() if not inventory then - return Commands.status.error{ "expcore-commands.reject-player-alive" } + return Commands.status.error{ "exp-commands-parse.player-alive" } end transfer_inventory{ diff --git a/exp_scenario/module/gui/player_list.lua b/exp_scenario/module/gui/player_list.lua index 52509ebc..3b5379b3 100644 --- a/exp_scenario/module/gui/player_list.lua +++ b/exp_scenario/module/gui/player_list.lua @@ -71,12 +71,12 @@ Elements.close_action_bar = Gui.define("player_list/close_action_bar") tooltip = { "exp-gui_player-list.close-action-bar" }, style = "slot_sized_button_red", } - :style(Gui.styles.sprite{ - size = 20, + :style{ + size = 30, padding = -1, top_margin = -1, right_margin = -1, - }) + } :on_click(function(_, player) Elements.container.set_selected_player(player, nil) Elements.player_table.refresh_player(player) @@ -92,12 +92,12 @@ Elements.reason_confirm = Gui.define("player_list/reason_confirm") tooltip = { "exp-gui_player-list.reason-confirm" }, style = "slot_sized_button_green", } - :style(Gui.styles.sprite{ + :style{ size = 30, padding = -1, left_margin = -2, right_margin = -1, - }) + } :on_click(function(_, player, element) local action_name = Elements.container.get_selected_action(player) local button_data = action_name and config.buttons[action_name] @@ -382,7 +382,7 @@ Elements.reason_bar = Gui.define("player_list/reason_bar") local entry_style = reason_field.style entry_style.padding = 0 entry_style.height = 28 - entry_style.minimal_width = 160 + entry_style.minimal_width = 158 Elements.reason_confirm(reason_bar) return reason_bar From cb89c08c9f8974c31ba7a9ae235c9d0ebb1926cd Mon Sep 17 00:00:00 2001 From: PHIDIAS Date: Fri, 26 Jun 2026 20:43:00 +0900 Subject: [PATCH 5/6] Update vlayer.lua (#440) --- exp_legacy/module/modules/control/vlayer.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exp_legacy/module/modules/control/vlayer.lua b/exp_legacy/module/modules/control/vlayer.lua index ed9703c4..39346063 100644 --- a/exp_legacy/module/modules/control/vlayer.lua +++ b/exp_legacy/module/modules/control/vlayer.lua @@ -349,7 +349,7 @@ function vlayer.create_input_interface(surface, position, circuit, last_user) end interface.destructible = false - interface.minable = false + interface.minable_flag = false interface.operable = true return interface @@ -427,7 +427,7 @@ function vlayer.create_output_interface(surface, position, circuit, last_user) end interface.destructible = false - interface.minable = false + interface.minable_flag = false interface.operable = true return interface @@ -568,7 +568,7 @@ function vlayer.create_circuit_interface(surface, position, circuit, last_user) end interface.destructible = false - interface.minable = false + interface.minable_flag = false interface.operable = true return interface @@ -647,7 +647,7 @@ function vlayer.create_energy_interface(surface, position, last_user) end interface.destructible = false - interface.minable = false + interface.minable_flag = false interface.operable = false interface.electric_buffer_size = 0 interface.power_production = 0 From 71ab3e7be485b147622f44d3a30b25ec080dddb3 Mon Sep 17 00:00:00 2001 From: PHIDIAS Date: Fri, 26 Jun 2026 21:15:49 +0900 Subject: [PATCH 6/6] clear blueprint changed to selection (#435) * Update surface.lua * Update en.cfg * Update surface.lua * Update en.cfg * Fix description keys for ground items and blueprints * Update roles.lua * Update surface.lua * Update quick_actions.lua * Update en.cfg * Update en.cfg * Update surface.lua * Update zh-CN.cfg * Update zh-TW.cfg * . * . * . * Many fixes and reverts --------- Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> --- exp_legacy/module/config/expcore/roles.lua | 4 +- exp_scenario/module/commands/surface.lua | 131 +++++++++++---------- exp_scenario/module/gui/quick_actions.lua | 50 +++++--- exp_scenario/module/locale/en.cfg | 19 ++- exp_scenario/module/locale/zh-CN.cfg | 14 +-- exp_scenario/module/locale/zh-TW.cfg | 14 +-- 6 files changed, 124 insertions(+), 108 deletions(-) diff --git a/exp_legacy/module/config/expcore/roles.lua b/exp_legacy/module/config/expcore/roles.lua index 3b1b8023..df346afb 100644 --- a/exp_legacy/module/config/expcore/roles.lua +++ b/exp_legacy/module/config/expcore/roles.lua @@ -125,7 +125,7 @@ Roles.new_role("Trainee", "TrMod") "command/search-online", "command/search-amount", "command/search-recent", - "command/clear-blueprints", + "command/clear-blueprints-surface", "gui/playerdata", } @@ -204,7 +204,7 @@ Roles.new_role("Veteran", "Vet") :allow{ "command/chat-commands", "command/clear-ground-items", - "command/clear-blueprints-radius", + "command/clear-blueprints", "command/set-trains-to-automatic", } :set_auto_assign_condition(function(player) diff --git a/exp_scenario/module/commands/surface.lua b/exp_scenario/module/commands/surface.lua index 5bcdeab9..1d98e96a 100644 --- a/exp_scenario/module/commands/surface.lua +++ b/exp_scenario/module/commands/surface.lua @@ -2,12 +2,17 @@ Adds a command that clear item on ground so blueprint can deploy safely ]] +local AABB = require("modules/exp_util/aabb") +local Commands = require("modules/exp_commands") local ExpUtil = require("modules/exp_util") local move_items = ExpUtil.move_items_to_surface - -local Commands = require("modules/exp_commands") +local Selection = require("modules/exp_util/selection") +local SelectArea = Selection.connect("ExpCommand_ClearBlueprint") local format_player_name = Commands.format_player_name_locale +--- @class ExpCommand_ClearBlueprint.commands +local commands = {} + --- @param surface LuaSurface --- @return LuaItemStack[] local function get_ground_items(surface) @@ -19,71 +24,73 @@ local function get_ground_items(surface) return items end ---- Clear all items on the ground, optional to select a single surface -Commands.new("clear-ground-items", { "exp-commands_surface.description-items" }) +--- Clear all items on the ground on a surface +commands.clear_ground_items = Commands.new("clear-ground-items", { "exp-commands_surface.description-items" }) :optional("surface", { "exp-commands_surface.arg-surface" }, Commands.types.surface) + :defaults{ + surface = function(player) return player.surface end + } :register(function(player, surface) - --- @cast surface LuaSurface? - local player_name = format_player_name(player) - if surface then - move_items{ - surface = surface, - items = get_ground_items(surface), - allow_creation = true, - name = "iron-chest", - } - game.print{ "exp-commands_surface.items-surface", player_name, surface.localised_name } - else - for _, surface in pairs(game.surfaces) do - move_items{ - surface = surface, - items = get_ground_items(surface), - allow_creation = true, - name = "iron-chest", - } - end - game.print{ "exp-commands_surface.items-all", player_name } - end - end) - ---- Clear all blueprints, optional to select a single surface -Commands.new("clear-blueprints", { "exp-commands_surface.description-blueprints" }) - :optional("surface", { "exp-commands_surface.arg-surface" }, Commands.types.surface) - :register(function(player, surface) - --- @cast surface LuaSurface? - local player_name = format_player_name(player) - if surface then - local entities = surface.find_entities_filtered{ type = "entity-ghost" } - for _, entity in ipairs(entities) do - entity.destroy() - end - game.print{ "exp-commands_surface.blueprint-surface", player_name, surface.localised_name } - else - for _, surface in pairs(game.surfaces) do - local entities = surface.find_entities_filtered{ type = "entity-ghost" } - for _, entity in ipairs(entities) do - entity.destroy() - end - end - game.print{ "exp-commands_surface.blueprint-all", player_name } - end - end) - ---- Clear all blueprints in a radius around you -Commands.new("clear-blueprints-radius", { "exp-commands_surface.description-radius" }) - :argument("radius", { "exp-commands_surface.arg-radius" }, Commands.types.number_range(1, 100)) - :register(function(player, radius) - --- @cast radius number - local player_name = format_player_name(player) - local entities = player.surface.find_entities_filtered{ - type = "entity-ghost", - position = player.position, - radius = radius, + --- @cast surface LuaSurface + move_items{ + surface = surface, + items = get_ground_items(surface), + allow_creation = true, + name = "iron-chest", } + local player_name = format_player_name(player) + game.print{ "exp-commands_surface.items", player_name, surface.localised_name } + end) +--- Clear all blueprints on a surface +commands.clear_blueprints_surface = Commands.new("clear-blueprints-surface", { "exp-commands_surface.description-blueprints-surface" }) + :optional("surface", { "exp-commands_surface.arg-surface" }, Commands.types.surface) + :defaults{ + surface = function(player) return player.surface end + } + :register(function(player, surface) + --- @cast surface LuaSurface + local entities = surface.find_entities_filtered{ type = "entity-ghost" } for _, entity in ipairs(entities) do entity.destroy() end - - game.print{ "exp-commands_surface.blueprint-radius", player_name, radius, player.surface.localised_name } + local player_name = format_player_name(player) + game.print{ "exp-commands_surface.blueprints", player_name, surface.localised_name } end) + +--- Clear all blueprint in the area, selected by toggle player selection mode +--- @class ExpCommands_ClearBlueprint.commands.clear_blueprint: ExpCommand +--- @overload fun(player: LuaPlayer) +commands.clear_blueprints = Commands.new("clear-blueprints", { "exp-commands_surface.description-blueprints" }) + :register(function(player) + if SelectArea:stop(player) then + return Commands.status.success{ "exp-commands_surface.exit" } + end + + SelectArea:start(player) + return Commands.status.success{ "exp-commands_surface.enter" } + end) --[[ @as any ]] + +--- When an area is selected +SelectArea:on_selection(function(event) + local player = assert(game.get_player(event.player_index)) + local area = AABB.expand(event.area) + local surface = event.surface + local area_size = (area.right_bottom.x - area.left_top.x) * (area.right_bottom.y - area.left_top.y) + + if area_size > 1000 then + player.print({ "exp-commands_surface.area-too-large", 1000, area_size }, Commands.print_settings.error) + return + end + + local entities = surface.find_entities_filtered{ type = "entity-ghost", area = area } + for _, entity in ipairs(entities) do + entity.destroy() + end + + game.print({ "exp-commands_surface.complete", #entities }, Commands.print_settings.default) +end) + +return { + commands = commands, +} diff --git a/exp_scenario/module/gui/quick_actions.lua b/exp_scenario/module/gui/quick_actions.lua index 3bead1e8..056a213a 100644 --- a/exp_scenario/module/gui/quick_actions.lua +++ b/exp_scenario/module/gui/quick_actions.lua @@ -10,6 +10,7 @@ local addon_artillery = require("modules/exp_scenario/commands/artillery") local addon_trains = require("modules/exp_scenario/commands/trains") local addon_teleport = require("modules/exp_scenario/commands/teleport") local addon_waterfill = require("modules/exp_scenario/commands/waterfill") +local addon_surface = require("modules/exp_scenario/commands/surface") local addon_lawnmower = require("modules/exp_scenario/commands/lawnmower") local addon_home = require("modules/exp_scenario/commands/home") local addon_vlayer = require("modules/exp_scenario/commands/vlayer") @@ -21,15 +22,23 @@ local Elements = {} --- @type table local Actions = {} ---- @param name string --- @param command ExpCommand | function (this is needed because of the overload on commands) --- @param on_click? ExpElement.EventHandler -local function new_quick_action(name, command, on_click) - local element = Gui.define("quick_actions/" .. name) +local function new_quick_action(command, on_click) + local command_name = command.name + + local element = Gui.define("quick_actions/" .. command_name) :draw{ type = "button", - caption = { "exp-gui_quick-actions.caption-" .. name }, - tooltip = { "exp-gui_quick-actions.tooltip-" .. name }, + caption = { "?", + { "exp-gui_quick-actions.caption-" .. command_name }, + command_name, + }, + tooltip = { "?", + { "exp-gui_quick-actions.tooltip-" .. command_name }, + command.description, + "" + }, } :style{ width = 160, @@ -38,26 +47,31 @@ local function new_quick_action(name, command, on_click) command(player) end) - Elements[name] = element - Actions[name] = { + Elements[command_name] = element + Actions[command_name] = { command = command --[[ @as ExpCommand ]], element = element, } end -new_quick_action("artillery", addon_artillery.commands.artillery) -new_quick_action("trains", addon_trains.commands.set_trains_to_automatic) -new_quick_action("spawn", addon_teleport.commands.spawn, function(_def, player, _element, _event) +new_quick_action(addon_artillery.commands.artillery) +new_quick_action(addon_trains.commands.set_trains_to_automatic) + +new_quick_action(addon_teleport.commands.spawn, function(def, player, element, event) addon_teleport.commands.spawn(player, player) end) -new_quick_action("waterfill", addon_waterfill.commands.waterfill) -new_quick_action("lawnmower", addon_lawnmower.commands.lawnmower) -new_quick_action("home", addon_home.commands.home) -new_quick_action("return", addon_home.commands._return) -new_quick_action("set-home", addon_home.commands.set_home) -new_quick_action("get-home", addon_home.commands.get_home) -new_quick_action("vlayer", addon_vlayer.commands.vlayer) -new_quick_action("repair", addon_repair.commands.repair) + +new_quick_action(addon_waterfill.commands.waterfill) +new_quick_action(addon_lawnmower.commands.lawnmower) +new_quick_action(addon_surface.commands.clear_ground_items) +new_quick_action(addon_surface.commands.clear_blueprints_surface) +new_quick_action(addon_surface.commands.clear_blueprints) +new_quick_action(addon_home.commands.home) +new_quick_action(addon_home.commands._return) +new_quick_action(addon_home.commands.set_home) +new_quick_action(addon_home.commands.get_home) +new_quick_action(addon_vlayer.commands.vlayer) +new_quick_action(addon_repair.commands.repair) --- Container added to the left gui flow --- @class ExpGui_QuickActions.elements.container: ExpElement diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index cec48c84..ce446356 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -220,14 +220,14 @@ follow-self=You can not follow yourself. [exp-commands_surface] description-items=Clear all items on the ground. description-blueprints=Clear all blueprints. -description-radius=Clear all blueprints in an radius around you. +description-blueprints-surface=Clear all blueprints on the current surface. arg-surface=Surface to clear on, default all. -arg-radius=Radius to clear. -items-surface=__1__ cleared all items on the ground of __2__. -items-all=__1__ cleared all items on the ground for all surfaces. -blueprints-surface=__1__ cleared all blueprints on __2__. -blueprints-all=__1__ cleared all blueprints for all surfaces. -blueprints-radius=__1__ cleared all blueprints in a __2__ radius around them on __3__. +items=__1__ cleared all items on the ground of __2__. +blueprints=__1__ cleared all blueprints on __2__. +enter=Entered selection mode, select the area. +exit=Exited selection mode. +area-too-large=Selected area is too large, must be less than __1__, selected __2__. +complete=__1__ blueprint ghost were removed. [exp-commands_trains] description=Set All Trains to Automatic, does not effect trains with passengers. @@ -347,15 +347,10 @@ caption-net=Net [exp-gui_quick-actions] tooltip-main=Quick Actions caption-artillery=Artillery -tooltip-artillery=Select artillery targets caption-research=Auto Research -tooltip-research=Toggle auto research queue caption-spawn=Teleport Spawn -tooltip-spawn-tooltip=Teleport to spawn caption-trains=Set Auto Train -tooltip-trains=Set all trains to automatic caption-waterfill=Waterfill -tooltip-waterfill=Change tiles to water [exp-gui_readme] main-tooltip=Information diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index 625f2394..bf65db42 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -215,14 +215,14 @@ follow-self=你不能追蹤自己 [exp-commands_surface] description-items=清除地面上的物品 description-blueprints=清除藍圖 -description-radius=清除周邊的藍圖 +description-blueprints-surface=清除藍圖 arg-surface=位面 -arg-radius=周邊半徑 -items-surface=__1__ 清除了所有在 __2__ 中掉地上的東西。 -items-all=__1__ 清除了所有掉地上的東西。 -blueprints-surface=__1__ 清除了所有在 __2__ 中在地上的藍圖。 -blueprints-all=__1__ 清除了所有在地上的藍圖。 -blueprints-radius=__1__ 清除了在 __3__ 上,周邊 __2__ 格的地上的藍圖。 +items=__1__ 清除了所有在 __2__ 中掉地上的東西。 +blueprints=__1__ 清除了所有在 __2__ 中在地上的藍圖。 +enter=現在進入區域選擇 +exit=已進入區域選擇 +area-too-large=區域太大了,需少過 __1__ 格,你選了 __2__ 格。 +complete=__1__ 個地上的藍圖已被清除。 [exp-commands_trains] description=把火車設置為自動模式 diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index 625f2394..bf65db42 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -215,14 +215,14 @@ follow-self=你不能追蹤自己 [exp-commands_surface] description-items=清除地面上的物品 description-blueprints=清除藍圖 -description-radius=清除周邊的藍圖 +description-blueprints-surface=清除藍圖 arg-surface=位面 -arg-radius=周邊半徑 -items-surface=__1__ 清除了所有在 __2__ 中掉地上的東西。 -items-all=__1__ 清除了所有掉地上的東西。 -blueprints-surface=__1__ 清除了所有在 __2__ 中在地上的藍圖。 -blueprints-all=__1__ 清除了所有在地上的藍圖。 -blueprints-radius=__1__ 清除了在 __3__ 上,周邊 __2__ 格的地上的藍圖。 +items=__1__ 清除了所有在 __2__ 中掉地上的東西。 +blueprints=__1__ 清除了所有在 __2__ 中在地上的藍圖。 +enter=現在進入區域選擇 +exit=已進入區域選擇 +area-too-large=區域太大了,需少過 __1__ 格,你選了 __2__ 格。 +complete=__1__ 個地上的藍圖已被清除。 [exp-commands_trains] description=把火車設置為自動模式