mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-15 00:24:52 +09:00
The plugin is now exp_player_statistics (@expcluster/player-statistics), with the module, global, permission, route and database directory renamed to match. The exp-statistics locale section keeps its name. Every statistic is tracked unconditionally, so the enable flags are gone from config.lua. Playtime and JoinCount are no longer counted in game: clusterio already keeps online time and joins for each user, so the controller fills them in from the user when a player's totals are requested, and the web page does the same from the user list. They are not stored or adjusted by the plugin. The web table has a column picker, a multiple select with check boxes, Select all and Reset buttons, showing "N statistics selected". The choice is kept in local storage. The user page shows the statistics of the player in a grid like the core player stats. The readme statistics table drops its second title and uses the short time format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--[[-- Gui Module - Readme Adds a main gui that contains important information about the server. ]] local ExpUtil = require("modules/exp_util") local Gui = require("modules/exp_gui") local Roles = require("modules/exp_roles") local Commands = require("modules/exp_commands") local ExpPlayerStatistics = require("modules/exp_player_statistics") local ExpServerList = require("modules/exp_server_list") local community = require("modules/exp_scenario/config/community_details").config local format_time = ExpUtil.format_time_factory_locale{ format = "long", days = true, hours = true, minutes = true } local frame_width = 595 local title_width = 270 local scroll_height = 275 local description_length = 120 --- Cut a string to a number of bytes without splitting a utf8 character --- @param str string --- @param length number --- @return string local function shorten(str, length) if #str <= length then return str end local next_byte = str:byte(length + 1) while length > 0 and next_byte >= 0x80 and next_byte < 0xC0 do length = length - 1 next_byte = str:byte(length + 1) end return str:sub(1, length) .. "..." end --- @class ExpGui_Readme.elements local Elements = {} --- @type table<number, { caption: LocalisedString, tooltip: LocalisedString | (fun(): LocalisedString), element: ExpElement }> local tabs = {} --- Register a readme tab --- @param caption LocalisedString --- @param tooltip LocalisedString | fun(): LocalisedString Functions are called when the readme opens --- @param element ExpElement local function define_tab(caption, tooltip, element) tabs[#tabs + 1] = { caption = caption, tooltip = tooltip, element = element } end --- Create a title section table --- @class ExpGui_Readme.elements.title_table: ExpElement --- @overload fun(parent: LuaGuiElement, bar_size: number, caption: LocalisedString, column_count: number): LuaGuiElement Elements.title_table = Gui.define("readme/title_table") :draw(function(_, parent, bar_size, caption, column_count) Gui.elements.title_label(parent, bar_size, caption) return parent.add{ type = "table", column_count = column_count, style = "bordered_table", } end) :style{ padding = 0, cell_padding = 0, vertical_align = "center", horizontally_stretchable = true, } --[[@as any]] --- Scroll pane used for title tables --- @class ExpGui_Readme.elements.title_table_scroll: ExpElement --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.title_table_scroll = Gui.define("readme/title_table_scroll") :draw{ type = "scroll-pane", direction = "vertical", horizontal_scroll_policy = "never", vertical_scroll_policy = "auto", style = "scroll_pane_under_subheader", } :style{ padding = { 1, 3 }, maximal_height = scroll_height, horizontally_stretchable = true, } --[[@as any]] --- Sub content frame --- @class ExpGui_Readme.elements.sub_content: ExpElement --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.sub_content = Gui.define("readme/sub_content") :draw{ type = "frame", direction = "vertical", style = "inside_deep_frame", } :style{ horizontally_stretchable = true, horizontal_align = "center", padding = { 2, 2 }, top_margin = 2, } --[[@as any]] --- @class ExpGui_Readme.elements.join_server.elements --- @field server_id number --- Join server button --- @class ExpGui_Readme.elements.join_server: ExpElement --- @field data table<LuaGuiElement, ExpGui_Readme.elements.join_server.elements> --- @overload fun(parent: LuaGuiElement, server: ExpServerList.Server): LuaGuiElement Elements.join_server = Gui.define("readme/join_server") :track_all_elements() :draw(function(def, parent, server) --- @cast def ExpGui_Readme.elements.join_server --- @cast server ExpServerList.Server local flow = parent.add{ type = "flow", } local button = flow.add{ type = "sprite-button", sprite = "utility/circuit_network_panel", hovered_sprite = "utility/circuit_network_panel", style = "frame_action_button", } def.data[button] = { server_id = server.id, } Elements.join_server.refresh(button, server) return button end) :style{ size = 20, padding = -1, } :on_click(function(def, player, button) --- @cast def ExpGui_Readme.elements.join_server local server = ExpServerList.get_server(def.data[button].server_id) if server and ExpServerList.is_online(server) then ExpServerList.request_connection(player, server, true) end end) --[[@as any]] --- Refresh join server button --- @param button LuaGuiElement --- @param server ExpServerList.Server? function Elements.join_server.refresh(button, server) local current = ExpServerList.get_current() local status, detail = "Online", nil if not server or not ExpServerList.is_online(server) then status = "Offline" elseif current and server.id == current.id then status = "Current" elseif current and server.factorio_version ~= current.factorio_version then status, detail = "Version", server.factorio_version elseif server.has_password then status = "Password" elseif current and server.mod_pack_name ~= current.mod_pack_name then status, detail = "Modded", server.mod_pack_name end -- Factorio drops nil parameters, which would shift the player count into __1__ local player_count = server and server.player_count or 0 button.tooltip = { "exp-gui_readme.servers-connect-" .. status, detail or "", player_count } if status == "Offline" or status == "Current" then button.enabled = false button.sprite = "utility/circuit_network_panel" button.hovered_sprite = "utility/circuit_network_panel" elseif status == "Version" then button.enabled = false button.sprite = "utility/shuffle" button.hovered_sprite = "utility/shuffle" elseif status == "Password" then button.enabled = true button.sprite = "utility/warning_white" button.hovered_sprite = "utility/warning" elseif status == "Modded" then button.enabled = true button.sprite = "utility/downloading_white" button.hovered_sprite = "utility/downloading" else button.enabled = true button.sprite = "utility/circuit_network_panel" button.hovered_sprite = "utility/circuit_network_panel" end end --- Refresh all join buttons function Elements.join_server.refresh_all() for _, button in Elements.join_server:tracked_elements() do local server_id = Elements.join_server.data[button].server_id Elements.join_server.refresh(button, ExpServerList.get_server(server_id)) end end --- Welcome tab define_tab( { "exp-gui_readme.welcome-tab" }, function() return { "exp-gui_readme.welcome-tooltip", community.community_name } end, Gui.define("readme/welcome") :draw(function(_, parent) local player = Gui.get_player(parent) local server_details = { name = community.community_name .. " S0 - Local", welcome = "Failed to load description: disconnected from the controller.", reset_time = "Not Set", } local current = ExpServerList.get_current() if current then server_details = { name = current.name, welcome = current.welcome ~= "" and current.welcome or { "exp-gui_readme.welcome-default" }, reset_time = current.reset_time ~= "" and current.reset_time or "Not Set", } end local container = parent.add{ type = "flow", direction = "vertical" } local top_flow = container.add{ type = "flow" } top_flow.add{ type = "sprite", sprite = "file/modules/exp_scenario/gui/logo.png" } local center_flow = top_flow.add{ type = "flow", direction = "vertical" } center_flow.style.horizontal_align = "center" Gui.elements.centered_label(center_flow, 380, { "exp-gui_readme.welcome-title", server_details.name }) Gui.elements.centered_label(center_flow, 380, server_details.welcome) top_flow.add{ type = "sprite", sprite = "file/modules/exp_scenario/gui/logo.png" } Gui.elements.bar(container) container.add{ type = "flow" }.style.height = 4 local role_names = {} for i, role in ipairs(Roles.sort_roles(Roles.get_player_roles(player))) do role_names[i] = role.name end Gui.elements.centered_label( Elements.sub_content(container), frame_width, { "exp-gui_readme.welcome-general", community.community_name, format_time(game.tick), server_details.reset_time, } ) Gui.elements.centered_label( Elements.sub_content(container), frame_width, { "exp-gui_readme.welcome-roles", table.concat(role_names, ", "), } ) Gui.elements.centered_label( Elements.sub_content(container), frame_width, { "exp-gui_readme.welcome-chat" } ) return container end) --[[@as any]] ) --- Rules tab define_tab( { "exp-gui_readme.rules-tab" }, { "exp-gui_readme.rules-tooltip" }, Gui.define("readme/rules") :draw(function(_, parent) local container = parent.add{ type = "flow", direction = "vertical" } Gui.elements.title_label(container, title_width - 3, { "exp-gui_readme.rules-tab" }) Gui.elements.centered_label(container, frame_width, { "exp-gui_readme.rules-general" }) Gui.elements.bar(container) container.add{ type = "flow" } local rules = Gui.elements.scroll_table(container, scroll_height, 1) rules.style = "bordered_table" local rules_style = rules.style --[[@as LuaStyle]] rules_style.cell_padding = 4 for i = 1, 15 do Gui.elements.centered_label(rules, frame_width - 30, { "exp-gui_readme.rules-" .. i }) end return container end) --[[@as any]] ) --- Commands tab define_tab( { "exp-gui_readme.commands-tab" }, { "exp-gui_readme.commands-tooltip" }, Gui.define("readme/commands") :draw(function(_, parent) local player = Gui.get_player(parent) local container = parent.add{ type = "flow", direction = "vertical" } Gui.elements.title_label(container, title_width - 20, { "exp-gui_readme.commands-tab" }) Gui.elements.centered_label(container, frame_width, { "exp-gui_readme.commands-general" }) Gui.elements.bar(container) container.add{ type = "flow" } local commands = Gui.elements.scroll_table(container, scroll_height, 2) commands.style = "bordered_table" local commands_style = commands.style --[[@as LuaStyle]] commands_style.cell_padding = 0 for name, command in pairs(Commands.list_for_player(player)) do Gui.elements.centered_label(commands, 120, name) Gui.elements.centered_label(commands, 450, command.description) end return container end) --[[@as any]] ) --- Servers tab define_tab( { "exp-gui_readme.servers-tab" }, { "exp-gui_readme.servers-tooltip" }, Gui.define("readme/servers") :draw(function(_, parent) local container = parent.add{ type = "flow", direction = "vertical" } Gui.elements.title_label(container, title_width - 10, { "exp-gui_readme.servers-tab" }) Gui.elements.centered_label(container, frame_width, { "exp-gui_readme.servers-general" }) Gui.elements.bar(container) container.add{ type = "flow" } local scroll_pane = Elements.title_table_scroll(container) scroll_pane.style.maximal_height = scroll_height + 20 local factorio_servers = Elements.title_table(scroll_pane, 225, { "exp-gui_readme.servers-factorio" }, 3) for _, server in ipairs(ExpServerList.get_servers()) do local description = shorten(server.description, description_length) local tooltip = description ~= server.description and server.description or nil Gui.elements.centered_label(factorio_servers, 110, server.display_name) Gui.elements.centered_label(factorio_servers, 436, description, tooltip) Elements.join_server(factorio_servers, server) end local external_links = Elements.title_table(scroll_pane, 235, { "exp-gui_readme.servers-external" }, 2) for _, key in ipairs{ "discord", "website", "patreon", "status", "github" } do Gui.elements.centered_label(external_links, 110, key:gsub("^%l", string.upper)) Gui.elements.centered_label(external_links, 460, community[key], { "exp-gui_readme.servers-open-in-browser" }) end return container end) --[[@as any]] ) --- Backers tab --- Content area for the backers tab define_tab( { "exp-gui_readme.backers-tab" }, { "exp-gui_readme.backers-tooltip" }, Gui.define("readme/backers") :draw(function(_, parent) local container = parent.add{ type = "flow", direction = "vertical" } Gui.elements.title_label(container, title_width - 10, { "exp-gui_readme.backers-tab" }) Gui.elements.centered_label(container, frame_width, { "exp-gui_readme.backers-general" }) Gui.elements.bar(container) container.add{ type = "flow" } local groups = { { roles = { "Senior Administrator", "Administrator" }, title = { "exp-gui_readme.backers-management" }, width = 230, players = {}, --- @type string[] }, { roles = { "Board Member", "Senior Backer" }, title = { "exp-gui_readme.backers-board" }, width = 145, players = {}, --- @type string[] }, { roles = { "Sponsor", "Supporter" }, title = { "exp-gui_readme.backers-backers" }, width = 196, players = {}, --- @type string[] }, { roles = { "Moderator", "Trainee" }, title = { "exp-gui_readme.backers-staff" }, width = 235, players = {}, --- @type string[] }, { roles = {}, time = 3 * 3600 * 60, title = { "exp-gui_readme.backers-active" }, width = 235, players = {}, --- @type string[] }, } local done = {} -- Fill groups from the players who hold the roles, a player can be in more than one group for _, group in ipairs(groups) do local seen = {} for _, role_name in ipairs(group.roles) do local role = Roles.get_role_by_name(role_name) for _, player_name in ipairs(role and role:get_player_names() or {}) do if not seen[player_name] then seen[player_name] = true done[player_name] = true group.players[#group.players + 1] = player_name end end end end -- Fill active player group for _, player in pairs(game.players) do if not done[player.name] then for _, group in ipairs(groups) do if group.time and player.online_time > group.time then group.players[#group.players + 1] = player.name end end end end local scroll_pane = Elements.title_table_scroll(container) for _, group in ipairs(groups) do if #group.players > 0 then local backers_table = Elements.title_table(scroll_pane, group.width, group.title, 4) for _, player_name in ipairs(group.players) do Gui.elements.centered_label(backers_table, 140, player_name) end if #group.players < 4 then for i = 1, 4 - #group.players do Gui.elements.centered_label(backers_table, 140) end end end end return container end) --[[@as any]] ) --- Content area for the statistics tab define_tab( { "exp-gui_readme.data-tab" }, { "exp-gui_readme.data-tooltip" }, Gui.define("readme/data") :draw(function(_, parent) local container = parent.add{ type = "flow", direction = "vertical" } local player_name = Gui.get_player(parent).name Gui.elements.title_label(container, title_width, { "exp-gui_readme.data-tab" }) Gui.elements.centered_label(container, frame_width, { "exp-gui_readme.data-general" }) Gui.elements.bar(container) container.add{ type = "flow" } local scroll_pane = Elements.title_table_scroll(container) local statistics = scroll_pane.add{ type = "table", column_count = 4, style = "bordered_table" } statistics.style.cell_padding = 0 statistics.style.horizontally_stretchable = true for _, name in ipairs(ExpPlayerStatistics.display_order) do local value = ExpPlayerStatistics.format(name, ExpPlayerStatistics.get(player_name, name), true) local tooltip = { "exp-statistics." .. name .. "-tooltip" } Gui.elements.centered_label(statistics, 150, { "exp-statistics." .. name }, tooltip) Gui.elements.centered_label(statistics, 130, { "exp-gui_readme.data-format", value, ExpPlayerStatistics.units[name] or "" }, tooltip) end return container end) --[[@as any]] ) --- @class ExpGui_Readme.elements.container.elements --- @field pane LuaGuiElement --- Main readme container --- @class ExpGui_Readme.elements.container: ExpElement --- @field data table<LuaGuiElement, ExpGui_Readme.elements.container.elements> --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.container = Gui.define("readme/container") :track_all_elements() :draw(function(def, parent) --- @cast def ExpGui_Readme.elements.container local container = parent.add{ name = def.name, type = "frame", style = "invisible_frame" } local left_alignment = Gui.elements.aligned_flow(container, { vertical_align = "bottom" }) left_alignment.style.padding = { 32, 0, 0, 0 } local left_side = left_alignment.add{ type = "frame", style = "character_gui_left_side" } left_side.style.vertically_stretchable = true left_side.style.padding = 0 left_side.style.width = 5 local pane = container.add{ name = "pane", type = "tabbed-pane", style = "frame_tabbed_pane", } for _, tab in ipairs(tabs) do local tooltip = tab.tooltip if type(tooltip) == "function" then tooltip = tooltip() end local gui_tab = pane.add{ type = "tab", style = "frame_tab", caption = tab.caption, tooltip = tooltip, } pane.add_tab(gui_tab, tab.element(pane)) end def.data[container] = { pane = pane, } return container end) :on_opened(function(def, player) Gui.toolbar.set_button_toggled_state(Elements.toggle_button, player, true) end) :on_closed(function(def, player, element) Gui.toolbar.set_button_toggled_state(Elements.toggle_button, player, false) Gui.destroy_if_valid(element) end) --- Toggle button Elements.toggle_button = Gui.toolbar.create_button{ name = "readme_toggle", auto_toggle = true, sprite = "virtual-signal/signal-info", tooltip = { "exp-gui_readme.main-tooltip" }, visible = function(player) return Roles.player_has_permission(player, "exp_scenario.gui.readme") end, } :on_click(function(_, player) local center = player.gui.center local readme = center[Elements.container.name] if readme then player.opened = nil else player.opened = Elements.container(center) end end) --- Open readme for new players --- @param event EventData.on_player_created local function open_readme(event) local player = assert(game.get_player(event.player_index)) local element = Elements.container(player.gui.center) element.pane.selected_tab_index = 1 player.opened = element end --- Clear stale readme --- @param event EventData.on_player_joined_game | EventData.on_player_respawned local function clear_readme(event) local player = game.players[event.player_index] if not player.opened then Gui.destroy_if_valid(player.gui.center[Elements.container.name]) end end local e = defines.events return { elements = Elements, events = { [e.on_player_created] = open_readme, [e.on_player_joined_game] = clear_readme, [e.on_player_respawned] = clear_readme, [ExpServerList.on_servers_updated] = Elements.join_server.refresh_all, }, }