Files
factorio-scenario-ExpCluster/exp_scenario/module/gui

--[[-- 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_legacy.expcore.roles")
local Commands = require("modules/exp_commands")
local PlayerData = require("modules.exp_legacy.expcore.player_data")
local External = require("modules.exp_legacy.expcore.external")

local format_number = require("util").format_number
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

--- @class ExpGui_Readme.elements
local Elements = {}

--- @type table<number, { caption: LocalisedString, tooltip: LocalisedString, element: ExpElement }>
local tabs = {}

--- Register a readme tab
--- @param caption LocalisedString
--- @param tooltip LocalisedString
--- @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 string

--- 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_id: string, wrong_version: string?): LuaGuiElement
Elements.join_server = Gui.define("readme/join_server")
    :track_all_elements()
    :draw(function(def, parent, server_id, wrong_version)
        --- @cast def ExpGui_Readme.elements.join_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, wrong_version)
        return button
    end)
    :style{
        size = 20,
        padding = -1,
    }
    :on_click(function(def, player, button)
        --- @cast def ExpGui_Readme.elements.join_server
        local server_id = def.data[button].server_id
        External.request_connection(player, server_id, true)
    end) --[[ @as any ]]

--- Refresh join server button
--- @param button LuaGuiElement
--- @param wrong_version string?
function Elements.join_server.refresh(button, wrong_version)
    local server_id = Elements.join_server.data[button].server_id
    local status = External.get_server_status(server_id) or "Offline"

    if wrong_version then
        status = "Version"
    end

    button.tooltip = { "exp-gui_readme.servers-connect-" .. status, wrong_version }

    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 online join buttons
function Elements.join_server.refresh_all()
    if not External.valid() then
        return
    end

    local current_version = External.get_current_server().version

    for _, button in Elements.join_server:tracked_elements() do
        local server_id = Elements.join_server.data[button].server_id
        local server = External.get_servers()[server_id]

        if server then
            Elements.join_server.refresh(button, current_version ~= server.version and server.version or nil)
        end
    end
end

--- Welcome tab
define_tab(
    { "exp-gui_readme.welcome-tab" },
    { "exp-gui_readme.welcome-tooltip" },
    Gui.define("readme/welcome")
    :draw(function(_, parent)
        local player = Gui.get_player(parent)

        local server_details = {
            name = "ExpGaming S0 - Local",
            welcome = "Failed to load description: disconnected from external api.",
            reset_time = "Not Set",
        }

        if External.valid() then
            server_details = External.get_current_server()
        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_legacy/modules/gui/logo.png" }

        local center_flow = top_flow.add{ type = "flow", direction = "vertical" }
        center_flow.style.horizontal_align = "center"

        Gui.elements.title_label(center_flow, 62, { "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_legacy/modules/gui/logo.png" }

        Gui.elements.bar(container)
        container.add{ type = "flow" }.style.height = 4

        local role_names = {}
        for i, role in ipairs(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",
                server_details.reset_time,
                format_time(game.tick),
            }
        )

        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"
        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"
        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

        if External.valid() then
            local current_version = External.get_current_server().version

            local factorio_servers = Elements.title_table(scroll_pane, 225, { "exp-gui_readme.servers-factorio" }, 3)

            for server_id, server in pairs(External.get_servers()) do
                Gui.elements.centered_label(factorio_servers, 110, server.short_name)
                Gui.elements.centered_label(factorio_servers, 436, server.description)
                Elements.join_server(factorio_servers, server_id, current_version ~= server.version and server.version or nil)
            end
        else
            local factorio_servers = Elements.title_table(scroll_pane, 225, { "exp-gui_readme.servers-factorio" }, 2)
            for i = 1, 8 do
                Gui.elements.centered_label(factorio_servers, 110, { "exp-gui_readme.servers-" .. i })
                Gui.elements.centered_label(factorio_servers, 460, { "exp-gui_readme.servers-d" .. i })
            end
        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, { "links." .. 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 = {},
            },
            {
                roles = { "Board Member", "Senior Backer" },
                title = { "exp-gui_readme.backers-board" },
                width = 145,
                players = {},
            },
            {
                roles = { "Sponsor", "Supporter" },
                title = { "exp-gui_readme.backers-backers" },
                width = 196,
                players = {},
            },
            {
                roles = { "Moderator", "Trainee" },
                title = { "exp-gui_readme.backers-staff" },
                width = 235,
                players = {},
            },
            {
                roles = {},
                time = 3 * 3600 * 60,
                title = { "exp-gui_readme.backers-active" },
                width = 235,
                players = {},
            },
        }

        local done = {}

        -- Fill groups from configured roles
        for player_name, player_roles in pairs(Roles.config.players) do
            for _, group in ipairs(groups) do
                for _, role_name in ipairs(group.roles) do
                    if table.contains(player_roles, role_name) then
                        done[player_name] = true
                        group.players[#group.players + 1] = player_name
                        break
                    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 ]]
)

--- @class (exact) ExpGui_Readme.elements.readme_data.param table
--- @field scroll_pane LuaGuiElement
--- @field player LuaPlayer
--- @field player_name string
--- @field children table
--- @field title LocalisedString
--- @field locale_prefix string
--- @field default_stringify fun(value: any): string
--- @field columns number
--- @field title_width number
--- @field column_width number
--- @field extra_rows fun(data_table: LuaGuiElement)?

--- Render a player data category
--- @param opts ExpGui_Readme.elements.readme_data.param
local function render_data_category(opts)
    local data_table = Elements.title_table(opts.scroll_pane, opts.title_width, opts.title, opts.columns)

    if opts.extra_rows then
        opts.extra_rows(data_table)
    end

    for name, child in pairs(opts.children) do
        local metadata = child.metadata

        if not metadata.permission or Roles.player_allowed(opts.player, metadata.permission) then
            local value = child:get(opts.player_name)

            if value ~= nil or metadata.show_always then
                if metadata.stringify_short then
                    value = metadata.stringify_short(value)
                elseif metadata.stringify then
                    value = metadata.stringify(value)
                else
                    value = opts.default_stringify(value)
                end

                local tooltip = metadata.tooltip or { opts.locale_prefix .. name .. "-tooltip" }
                Gui.elements.centered_label(
                    data_table, 150,
                    metadata.name or { opts.locale_prefix .. name },
                    tooltip
                )

                Gui.elements.centered_label(
                    data_table, opts.column_width,
                    { "exp-gui_readme.data-format", value, metadata.unit or "" },
                    metadata.value_tooltip or { "?", { opts.locale_prefix .. name .. "-value-tooltip" }, tooltip }
                )
            end
        end
    end
end

--- Content area for the player data 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 = Gui.get_player(parent)
        local player_name = player.name

        local enum = PlayerData.PreferenceEnum
        local preference = PlayerData.DataSavingPreference:get(player_name)
        local preference_meta = PlayerData.DataSavingPreference.metadata
        preference = enum[preference]

        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)

        render_data_category{
            scroll_pane = scroll_pane,
            player = player,
            player_name = player_name,
            children = PlayerData.Required.children,
            title = { "exp-gui_readme.data-required" },
            locale_prefix = "exp-required.",
            columns = 2,
            title_width = 250,
            column_width = 420,
            default_stringify = tostring,
            extra_rows = function(required)
                Gui.elements.centered_label(required, 150, preference_meta.name, preference_meta.tooltip)
                Gui.elements.centered_label(required, 420, { "expcore-data.preference-" .. enum[preference] }, preference_meta.value_tooltip)
            end,
        }

        if preference <= enum.Settings then
            render_data_category{
                scroll_pane = scroll_pane,
                player = player,
                player_name = player_name,
                children = PlayerData.Settings.children,
                title = { "exp-gui_readme.data-settings" },
                locale_prefix = "exp-settings.",
                columns = 2,
                title_width = 255,
                column_width = 420,
                default_stringify = function(value)
                    return tostring(value or "None set")
                end,
            }
        end

        if preference <= enum.Statistics then
            render_data_category{
                scroll_pane = scroll_pane,
                player = player,
                player_name = player_name,
                children = PlayerData.Statistics.children,
                title = { "exp-gui_readme.data-statistics" },
                locale_prefix = "exp-statistics.",
                columns = 4,
                title_width = 250,
                column_width = 130,
                default_stringify = function(value)
                    return format_number(value or 0, false)
                end,
            }
        end

        local skip = {
            DataSavingPreference = true,
            Settings = true,
            Statistics = true,
            Required = true,
        }

        local count = 0
        for _ in pairs(PlayerData.All.children) do
            count = count + 1
        end

        if preference <= enum.All and count > 4 then
            local misc = {}
            for name, child in pairs(PlayerData.All.children) do
                if not skip[name] then
                    misc[name] = child
                end
            end

            render_data_category{
                scroll_pane = scroll_pane,
                player = player,
                player_name = player_name,
                children = misc,
                title = { "exp-gui_readme.data-misc" },
                locale_prefix = "",
                columns = 2,
                title_width = 232,
                column_width = 420,
                default_stringify = tostring,
            }
        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>
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 gui_tab = pane.add{
                type = "tab",
                style = "frame_tab",
                caption = tab.caption,
                tooltip = tab.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_allowed(player, "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,
    },
    on_nth_tick = {
        [60 * 60] = Elements.join_server.refresh_all,
    }
}