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, } }