From 1fcf351566e7f94df8be4f689d2039e7dd9e4560 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:18:37 +0000 Subject: [PATCH 1/6] Migrate rocket info gui to scenario and fix for Space Age Port modules/gui/rocket-info into exp_scenario/gui/rocket_info using the new exp_gui element API, matching the other scenario guis. The per-force stats, launch times and silo tracking from the legacy control module are folded into the gui module. - Drop the removed auto/remote launch controls and count launches via on_cargo_pod_finished_ascending instead of the old launch event - Add the exp-gui_rocket-info locale section - Disable the legacy module in the file loader and re-enable the gui/rocket-info role permission Co-Authored-By: Claude Opus 4.8 (1M context) --- exp_legacy/module/config/_file_loader.lua | 2 +- exp_legacy/module/config/expcore/roles.lua | 2 +- exp_scenario/module/control.lua | 1 + exp_scenario/module/gui/rocket_info.lua | 510 +++++++++++++++++++++ exp_scenario/module/locale/en.cfg | 36 ++ 5 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 exp_scenario/module/gui/rocket_info.lua diff --git a/exp_legacy/module/config/_file_loader.lua b/exp_legacy/module/config/_file_loader.lua index 91be3f15..8a4c7530 100644 --- a/exp_legacy/module/config/_file_loader.lua +++ b/exp_legacy/module/config/_file_loader.lua @@ -22,7 +22,7 @@ return { --"modules.data.toolbar", --- GUI - "modules.gui.rocket-info", + -- "modules.gui.rocket-info", -- migrated to exp_scenario/gui/rocket_info "modules.gui.warp-list", "modules.gui.player-list", "modules.gui.vlayer", diff --git a/exp_legacy/module/config/expcore/roles.lua b/exp_legacy/module/config/expcore/roles.lua index 618b57f9..46e26d73 100644 --- a/exp_legacy/module/config/expcore/roles.lua +++ b/exp_legacy/module/config/expcore/roles.lua @@ -283,7 +283,7 @@ local default = Roles.new_role("Guest", "") "command/data-preference", "command/connect", "gui/player-list", - --"gui/rocket-info", + "gui/rocket-info", "gui/science-info", "gui/task-list", "gui/warp-list", diff --git a/exp_scenario/module/control.lua b/exp_scenario/module/control.lua index 65e35d78..b031e221 100644 --- a/exp_scenario/module/control.lua +++ b/exp_scenario/module/control.lua @@ -78,6 +78,7 @@ add(require("modules/exp_scenario/gui/production_stats")) add(require("modules/exp_scenario/gui/quick_actions")) add(require("modules/exp_scenario/gui/readme")) add(require("modules/exp_scenario/gui/research_milestones")) +add(require("modules/exp_scenario/gui/rocket_info")) add(require("modules/exp_scenario/gui/science_production")) add(require("modules/exp_scenario/gui/surveillance")) add(require("modules/exp_scenario/gui/task_list")) diff --git a/exp_scenario/module/gui/rocket_info.lua b/exp_scenario/module/gui/rocket_info.lua new file mode 100644 index 00000000..cacf897a --- /dev/null +++ b/exp_scenario/module/gui/rocket_info.lua @@ -0,0 +1,510 @@ +--[[-- Gui - Rocket Info +Adds a rocket information gui which shows general stats, milestones and build progress of rockets. +The auto launch and remote launch controls were removed because the api no longer exposes auto launch in space age. +]] + +local ExpUtil = require("modules/exp_util") +local Storage = require("modules/exp_util/storage") +local Gui = require("modules/exp_gui") +local Roles = require("modules/exp_legacy/expcore/roles") +local config = require("modules/exp_legacy/config/gui/rockets") + +--- @class ExpGui_RocketInfo.elements +local Elements = {} + +local time_formats = { + caption = ExpUtil.format_time_factory_locale{ format = "short", minutes = true, seconds = true }, + caption_hours = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true }, + tooltip = ExpUtil.format_time_factory_locale{ format = "long", minutes = true, seconds = true }, + tooltip_hours = ExpUtil.format_time_factory_locale{ format = "long", hours = true, minutes = true, seconds = true }, +} + +--- The font colours used for the build progress label +local font_color = { + neutral = { r = 1, g = 1, b = 1 }, + waiting = { r = 0.3, g = 1, b = 1 }, + launched = { r = 0.3, g = 1, b = 0.3 }, +} + +--[[ +Below here is the rocket data tracking, it stores per force stats and the times each rocket was launched. +This used to live in modules/control/rockets but it is only used by this gui so it has been folded in. +]] + +--- @class ExpGui_RocketInfo.silo_data +--- @field entity LuaEntity The rocket silo entity +--- @field force string The name of the force that owns the silo +--- @field launched number The number of rockets launched from this silo +--- @field awaiting_reset boolean True when a launch is ordered but the silo has not reset yet + +--- @type table> Force name to an array of launch ticks indexed by rocket number +local rocket_times = {} +--- @type table Force name to launch stats +local rocket_stats = {} +--- @type table Silo unit number to its data +local rocket_silos = {} + +Storage.register({ + rocket_times = rocket_times, + rocket_stats = rocket_stats, + rocket_silos = rocket_silos, +}, function(tbl) + rocket_times = tbl.rocket_times + rocket_stats = tbl.rocket_stats + rocket_silos = tbl.rocket_silos +end) + +-- The largest rolling average is used to know when an old launch time can be discarded +local largest_rolling_avg = 0 +for _, avg_over in pairs(config.stats.rolling_avg) do + if avg_over > largest_rolling_avg then + largest_rolling_avg = avg_over + end +end + +--- Get all the valid rocket silos that belong to a force, pruning any that are no longer valid +--- @param force_name string Name of the force to get the silos for +--- @return ExpGui_RocketInfo.silo_data[] +local function get_silos(force_name) + local rtn = {} + for unit_number, silo_data in pairs(rocket_silos) do + if not silo_data.entity.valid then + rocket_silos[unit_number] = nil + elseif silo_data.force == force_name then + rtn[#rtn + 1] = silo_data + end + end + + return rtn +end + +--- Get the number of rockets that a force has launched +--- @param force_name string Name of the force to get the count for +--- @return number +local function get_rocket_count(force_name) + return game.forces[force_name].rockets_launched +end + +--- Get the total number of rockets launched by all forces +--- @return number +local function get_game_rocket_count() + local rtn = 0 + for _, force in pairs(game.forces) do + rtn = rtn + force.rockets_launched + end + + return rtn +end + +--- Get the rolling average time to launch a rocket based on the last count rockets +--- @param force_name string Name of the force to get the average for +--- @param count number Number of rockets to average over +--- @return number # Number of ticks required to launch one rocket +local function get_rolling_average(force_name, count) + local times = rocket_times[force_name] + local rocket_count = game.forces[force_name].rockets_launched + if rocket_count == 0 or not times then return 0 end + + local last_launch_time = times[rocket_count] or 0 + local start_rocket_time = 0 + if count < rocket_count then + start_rocket_time = times[rocket_count - count + 1] or 0 + rocket_count = count + end + + return math.floor((last_launch_time - start_rocket_time) / rocket_count) +end + +--[[ +Below here is the gui, it is split into three collapsible sections; stats, milestones, and build progress. +]] + +--- Toggle the visible state of a section, the section is stored in the element data +--- @class ExpGui_RocketInfo.elements.toggle_section_button: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, section: LuaGuiElement): LuaGuiElement +Elements.toggle_section_button = Gui.define("rocket_info/toggle_section_button") + :draw{ + type = "sprite-button", + sprite = "utility/expand", + tooltip = { "exp-gui_rocket-info.tooltip-toggle-section-expand" }, + style = "frame_action_button", + } + :style{ + size = 20, + padding = -2, + } + :element_data( + Gui.from_argument(1) + ) + :on_click(function(def, player, element) + --- @cast def ExpGui_RocketInfo.elements.toggle_section_button + local section = def.data[element] + if Gui.toggle_visible_state(section) then + element.sprite = "utility/collapse" + element.tooltip = { "exp-gui_rocket-info.tooltip-toggle-section-collapse" } + else + element.sprite = "utility/expand" + element.tooltip = { "exp-gui_rocket-info.tooltip-toggle-section-expand" } + end + end) --[[ @as any ]] + +--- A clickable coordinate label which opens the silo location on the map when pressed +--- @class ExpGui_RocketInfo.elements.position_label: ExpElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, opts: { caption: LocalisedString, tooltip: LocalisedString?, unit_number: number }): LuaGuiElement +Elements.position_label = Gui.define("rocket_info/position_label") + :draw{ + type = "label", + caption = Gui.from_argument("caption"), + tooltip = Gui.from_argument("tooltip"), + } + :style{ + padding = { 0, 2 }, + } + :element_data( + Gui.from_argument("unit_number") + ) + :on_click(function(def, player, element) + --- @cast def ExpGui_RocketInfo.elements.position_label + if not config.progress.allow_zoom_to_map then return end + local silo_data = rocket_silos[def.data[element]] + if not silo_data or not silo_data.entity.valid then return end + local entity = silo_data.entity + player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface } + end) --[[ @as any ]] + +--- Add a name and value label pair to a data table +--- @param data_table LuaGuiElement The two column table to add the pair to +--- @param name string The data name, used to select the locale key +--- @param subname number? Optional subname passed as a parameter to the locale string +--- @param value LocalisedString The value to display +--- @param value_tooltip LocalisedString? Optional tooltip for the value label +local function add_data_label(data_table, name, subname, value, value_tooltip) + local name_label = data_table.add{ + type = "label", + caption = { "exp-gui_rocket-info.data-caption-" .. name, subname }, + tooltip = { "exp-gui_rocket-info.data-tooltip-" .. name, subname }, + } + name_label.style.padding = { 0, 2 } + + local value_label = data_table.add{ + type = "label", + caption = value, + tooltip = value_tooltip, + } + value_label.style.padding = { 0, 2 } +end + +--- Data table showing the launch statistics for a force +--- @class ExpGui_RocketInfo.elements.stats_table: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.stats_table = Gui.define("rocket_info/stats_table") + :track_all_elements() + :draw(function(def, parent) + return Gui.elements.scroll_table(parent, 215, 2) + end) --[[ @as any ]] + +--- Refresh a stats table with the most recent data for a force +--- @param stats_table LuaGuiElement +--- @param force_name string +function Elements.stats_table.refresh(stats_table, force_name) + stats_table.clear() + local stats = rocket_stats[force_name] or {} + local force_rockets = get_rocket_count(force_name) + + if config.stats.show_first_rocket then + local value = stats.first_launch or 0 + add_data_label(stats_table, "first-launch", nil, time_formats.caption_hours(value), time_formats.tooltip_hours(value)) + end + + if config.stats.show_last_rocket then + local value = stats.last_launch or 0 + add_data_label(stats_table, "last-launch", nil, time_formats.caption_hours(value), time_formats.tooltip_hours(value)) + end + + if config.stats.show_fastest_rocket then + local value = stats.fastest_launch or 0 + add_data_label(stats_table, "fastest-launch", nil, time_formats.caption_hours(value), time_formats.tooltip_hours(value)) + end + + if config.stats.show_total_rockets then + local total_rockets = get_game_rocket_count() + if total_rockets == 0 then total_rockets = 1 end + local percentage = math.floor(force_rockets / total_rockets * 1000) / 10 + add_data_label(stats_table, "total-rockets", nil, tostring(force_rockets), { "exp-gui_rocket-info.value-tooltip-total-rockets", percentage }) + end + + if config.stats.show_game_avg then + local avg = force_rockets > 0 and math.floor(game.tick / force_rockets) or 0 + add_data_label(stats_table, "avg-launch", nil, time_formats.caption(avg), time_formats.tooltip(avg)) + end + + for _, avg_over in pairs(config.stats.rolling_avg) do + local avg = get_rolling_average(force_name, avg_over) + add_data_label(stats_table, "avg-launch-n", avg_over, time_formats.caption(avg), time_formats.tooltip(avg)) + end +end + +--- Data table showing the milestones for a force +--- @class ExpGui_RocketInfo.elements.milestones_table: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.milestones_table = Gui.define("rocket_info/milestones_table") + :track_all_elements() + :draw(function(def, parent) + return Gui.elements.scroll_table(parent, 215, 2) + end) --[[ @as any ]] + +--- Refresh a milestones table with the most recent data for a force +--- @param milestones_table LuaGuiElement +--- @param force_name string +function Elements.milestones_table.refresh(milestones_table, force_name) + milestones_table.clear() + local force_rockets = get_rocket_count(force_name) + local times = rocket_times[force_name] or {} + + for _, milestone in ipairs(config.milestones) do + if milestone <= force_rockets then + local time = times[milestone] or 0 + add_data_label(milestones_table, "milestone-n", milestone, time_formats.caption_hours(time), time_formats.tooltip_hours(time)) + else + -- The first unachieved milestone is shown as the next milestone then we stop + add_data_label(milestones_table, "milestone-n", milestone, { "exp-gui_rocket-info.data-caption-milestone-next" }, { "exp-gui_rocket-info.data-tooltip-milestone-next" }) + break + end + end +end + +--- Data table showing the build progress of each silo for a force +--- @class ExpGui_RocketInfo.elements.progress_table: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.progress_table = Gui.define("rocket_info/progress_table") + :track_all_elements() + :draw(function(def, parent) + return Gui.elements.scroll_table(parent, 215, 3) + end) --[[ @as any ]] + +--- Refresh a progress table with the most recent data for a force +--- @param progress_table LuaGuiElement +--- @param force_name string +function Elements.progress_table.refresh(progress_table, force_name) + progress_table.clear() + local silos = get_silos(force_name) + + if #silos == 0 then + progress_table.add{ + type = "label", + caption = { "exp-gui_rocket-info.progress-no-silos" }, + }.style.padding = { 1, 2 } + return + end + + local zoom_tooltip = config.progress.allow_zoom_to_map and { "exp-gui_rocket-info.progress-label-tooltip" } or nil + for _, silo_data in pairs(silos) do + local entity = silo_data.entity + local position = entity.position + + -- Work out the progress caption, tooltip and colour + local waiting = entity.status == defines.entity_status.waiting_to_launch_rocket + local progress_caption = { "exp-gui_rocket-info.progress-caption", entity.rocket_parts } + local progress_tooltip = { "exp-gui_rocket-info.progress-tooltip", silo_data.launched } + local progress_color = font_color.neutral + if waiting and silo_data.awaiting_reset then + progress_caption = { "exp-gui_rocket-info.progress-launched" } + progress_color = font_color.launched + elseif waiting then + progress_caption = { "exp-gui_rocket-info.progress-caption", 100 } + progress_color = font_color.waiting + else + silo_data.awaiting_reset = false + end + + -- Add the clickable coordinates and the progress label + Elements.position_label(progress_table, { caption = { "exp-gui_rocket-info.progress-x-pos", position.x }, tooltip = zoom_tooltip, unit_number = entity.unit_number }) + Elements.position_label(progress_table, { caption = { "exp-gui_rocket-info.progress-y-pos", position.y }, tooltip = zoom_tooltip, unit_number = entity.unit_number }) + local progress_label = progress_table.add{ + type = "label", + caption = progress_caption, + tooltip = progress_tooltip, + } + progress_label.style.padding = { 0, 2 } + progress_label.style.font_color = progress_color + end +end + +--- Add a collapsible section to a container, returns the populated data table +--- @param container LuaGuiElement The container frame to add the section to +--- @param section_name string Used to select the locale keys for the header +--- @param table_define ExpElement The data table element define to add +--- @param force_name string The force to populate the table with +--- @return LuaGuiElement +local function add_section(container, section_name, table_define, force_name) + local header = Gui.elements.header(container, { + caption = { "exp-gui_rocket-info.section-caption-" .. section_name }, + tooltip = { "exp-gui_rocket-info.section-tooltip-" .. section_name }, + }) + + local data_table = table_define(container) + table_define.refresh(data_table, force_name) + + -- The scroll pane is the parent of the table, it is what gets collapsed + local scroll_pane = assert(data_table.parent) + scroll_pane.visible = false + Elements.toggle_section_button(header, scroll_pane) + + return data_table +end + +--- Container added to the left gui flow +--- @class ExpGui_RocketInfo.elements.container: ExpElement +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.container = Gui.define("rocket_info/container") + :draw(function(def, parent) + local container = Gui.elements.container(parent, 200) + container.style.padding = 0 + + local force_name = Gui.get_player(parent).force.name --[[ @as string ]] + + if config.stats.show_stats then + add_section(container, "stats", Elements.stats_table, force_name) + end + + if config.milestones.show_milestones then + add_section(container, "milestones", Elements.milestones_table, force_name) + end + + if config.progress.show_progress then + add_section(container, "progress", Elements.progress_table, force_name) + end + + return Gui.elements.container.get_root_element(container) + end) --[[ @as any ]] + +--- Add the element to the left flow with a toolbar button +Gui.add_left_element(Elements.container, false) +Gui.toolbar.create_button{ + name = "toggle_rocket_info", + left_element = Elements.container, + sprite = "item/rocket-silo", + tooltip = { "exp-gui_rocket-info.tooltip-main" }, + visible = function(player, element) + return Roles.player_allowed(player, "gui/rocket-info") + end +} + +--[[ +Below here is the event handling, the data tracking and gui refreshing are wired up to the same events. +]] + +--- Refresh the stats and milestones tables for all online players on a force +--- @param force_name string +local function refresh_force_stats(force_name) + local force = game.forces[force_name] + for _, stats_table in Elements.stats_table:online_elements(force) do + Elements.stats_table.refresh(stats_table, force_name) + end + for _, milestones_table in Elements.milestones_table:online_elements(force) do + Elements.milestones_table.refresh(milestones_table, force_name) + end +end + +--- Refresh the progress table for all online players on a force +--- @param force_name string +local function refresh_force_progress(force_name) + local force = game.forces[force_name] + for _, progress_table in Elements.progress_table:online_elements(force) do + Elements.progress_table.refresh(progress_table, force_name) + end +end + +--- Record the launch and update the stats when a cargo pod finishes ascending +--- @param event EventData.on_cargo_pod_finished_ascending +local function on_cargo_pod_finished_ascending(event) + local force = event.cargo_pod.force --[[ @as LuaForce ]] + local force_name = force.name + local rockets_launched = force.rockets_launched + + -- Update the launch stats for the force + local stats = rocket_stats[force_name] + if not stats then + stats = {} + rocket_stats[force_name] = stats + end + + if rockets_launched == 1 then + stats.first_launch = event.tick + stats.fastest_launch = event.tick + elseif stats.last_launch and event.tick - stats.last_launch < (stats.fastest_launch or math.huge) then + stats.fastest_launch = event.tick - stats.last_launch + end + + stats.last_launch = event.tick + + -- Append the launch tick into the times array + local times = rocket_times[force_name] + if not times then + times = {} + rocket_times[force_name] = times + end + + times[rockets_launched] = event.tick + + -- Discard the launch time that is no longer needed by any rolling average unless it is a milestone + local remove_rocket = rockets_launched - largest_rolling_avg + if remove_rocket > 0 and not table.array_contains(config.milestones, remove_rocket) then + times[remove_rocket] = nil + end + + refresh_force_stats(force_name) + refresh_force_progress(force_name) +end + +--- Mark a silo as awaiting reset when a launch is ordered +--- @param event EventData.on_rocket_launch_ordered +local function on_rocket_launch_ordered(event) + local silo_data = rocket_silos[event.rocket_silo.unit_number] + if not silo_data then return end + silo_data.launched = silo_data.launched + 1 + silo_data.awaiting_reset = true + refresh_force_progress(event.rocket_silo.force.name) +end + +--- Add a silo to the list when it is built +--- @param event EventData.on_built_entity | EventData.on_robot_built_entity | EventData.script_raised_built | EventData.script_raised_revive +local function on_built(event) + local entity = event.entity + if not entity.valid or entity.name ~= "rocket-silo" then return end + rocket_silos[entity.unit_number] = { + entity = entity, + force = entity.force.name, + launched = 0, + awaiting_reset = false, + } + refresh_force_progress(entity.force.name) +end + +--- Refresh the progress for all forces that own at least one silo +local function refresh_all_progress() + for _, force in pairs(game.forces) do + if #get_silos(force.name) > 0 then + refresh_force_progress(force.name) + end + end +end + +local e = defines.events + +return { + elements = Elements, + events = { + [e.on_cargo_pod_finished_ascending] = on_cargo_pod_finished_ascending, + [e.on_rocket_launch_ordered] = on_rocket_launch_ordered, + [e.on_built_entity] = on_built, + [e.on_robot_built_entity] = on_built, + [e.script_raised_built] = on_built, + [e.script_raised_revive] = on_built, + }, + on_nth_tick = { + [150] = refresh_all_progress, + } +} diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index d2c5cf99..6c6071fa 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -431,6 +431,42 @@ caption-research-name=[technology=__1__] __2__ notice-inf=[color=255, 255, 255] Research completed at __1__ - [technology=__2__] - __3__[/color] notice=[color=255, 255, 255] Research completed at __1__ - [technology=__2__][/color] +[exp-gui_rocket-info] +tooltip-main=Rocket Info +caption-main=Rocket Info +tooltip-toggle-section-expand=Expand Section +tooltip-toggle-section-collapse=Collapse Section +section-caption-stats=Statistics +section-tooltip-stats=Statistics about how rockets are launched +section-caption-milestones=Milestones +section-tooltip-milestones=The times when milestones were met +section-caption-progress=Build Progress +section-tooltip-progress=The progress for your rocket silos +progress-no-silos=You have no rocket silos +data-caption-first-launch=First Launch +data-tooltip-first-launch=The time of the first launch +data-caption-last-launch=Last Launch +data-tooltip-last-launch=The time of the last launch +data-caption-fastest-launch=Fastest Launch +data-tooltip-fastest-launch=The time taken for the fastest launch +data-caption-total-rockets=Total Launched +data-tooltip-total-rockets=Total number of rockets launched by your force +value-tooltip-total-rockets=__1__% of all rockets on this map +data-caption-avg-launch=Average Time +data-tooltip-avg-launch=The average amount of time taken to launch a rocket +data-caption-avg-launch-n=Average Time __1__ +data-tooltip-avg-launch-n=The average amount of time taken to launch the last __1__ rockets +data-caption-milestone-n=Milestone __1__ +data-tooltip-milestone-n=Time taken to reach __1__ rockets +data-caption-milestone-next=N/A +data-tooltip-milestone-next=Not yet achieved +progress-x-pos=X __1__ +progress-y-pos=Y __1__ +progress-label-tooltip=View on map +progress-launched=Launched +progress-caption=__1__% +progress-tooltip=This silo has launched __1__ rockets + [exp-gui_science-production] tooltip-main=Science Production caption-main=Science From 0ead1a51f9b0a6c28ee1eec60e67ad8e8bfd564f Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:53:23 +0000 Subject: [PATCH 2/6] Use gui force data and add_row pattern for rocket info Address review feedback: store the per-force rocket stats, launch times and silos on the container's force data instead of a separate storage registration, and rebuild the stats, milestone and progress tables with add_row/refresh_row helpers that update label references in place. Co-Authored-By: Claude Opus 4.8 (1M context) --- exp_scenario/module/gui/rocket_info.lua | 583 +++++++++++++++--------- 1 file changed, 357 insertions(+), 226 deletions(-) diff --git a/exp_scenario/module/gui/rocket_info.lua b/exp_scenario/module/gui/rocket_info.lua index cacf897a..c6be45a4 100644 --- a/exp_scenario/module/gui/rocket_info.lua +++ b/exp_scenario/module/gui/rocket_info.lua @@ -4,7 +4,6 @@ The auto launch and remote launch controls were removed because the api no longe ]] local ExpUtil = require("modules/exp_util") -local Storage = require("modules/exp_util/storage") local Gui = require("modules/exp_gui") local Roles = require("modules/exp_legacy/expcore/roles") local config = require("modules/exp_legacy/config/gui/rockets") @@ -26,34 +25,6 @@ local font_color = { launched = { r = 0.3, g = 1, b = 0.3 }, } ---[[ -Below here is the rocket data tracking, it stores per force stats and the times each rocket was launched. -This used to live in modules/control/rockets but it is only used by this gui so it has been folded in. -]] - ---- @class ExpGui_RocketInfo.silo_data ---- @field entity LuaEntity The rocket silo entity ---- @field force string The name of the force that owns the silo ---- @field launched number The number of rockets launched from this silo ---- @field awaiting_reset boolean True when a launch is ordered but the silo has not reset yet - ---- @type table> Force name to an array of launch ticks indexed by rocket number -local rocket_times = {} ---- @type table Force name to launch stats -local rocket_stats = {} ---- @type table Silo unit number to its data -local rocket_silos = {} - -Storage.register({ - rocket_times = rocket_times, - rocket_stats = rocket_stats, - rocket_silos = rocket_silos, -}, function(tbl) - rocket_times = tbl.rocket_times - rocket_stats = tbl.rocket_stats - rocket_silos = tbl.rocket_silos -end) - -- The largest rolling average is used to know when an old launch time can be discarded local largest_rolling_avg = 0 for _, avg_over in pairs(config.stats.rolling_avg) do @@ -62,27 +33,32 @@ for _, avg_over in pairs(config.stats.rolling_avg) do end end ---- Get all the valid rocket silos that belong to a force, pruning any that are no longer valid ---- @param force_name string Name of the force to get the silos for ---- @return ExpGui_RocketInfo.silo_data[] -local function get_silos(force_name) - local rtn = {} - for unit_number, silo_data in pairs(rocket_silos) do - if not silo_data.entity.valid then - rocket_silos[unit_number] = nil - elseif silo_data.force == force_name then - rtn[#rtn + 1] = silo_data - end +--[[ +The per force rocket data is stored on the container element data using the force scope, this means it +persists with the rest of the gui data and is cleaned up when forces are merged. It holds the launch +stats, the tick each rocket was launched on, and the silos that belong to the force. +]] + +--- @class ExpGui_RocketInfo.silo_data +--- @field entity LuaEntity The rocket silo entity +--- @field launched number The number of rockets launched from this silo +--- @field awaiting_reset boolean True when a launch is ordered but the silo has not reset yet + +--- @class ExpGui_RocketInfo.force_data +--- @field stats { first_launch: number?, last_launch: number?, fastest_launch: number? } Launch stats for the force +--- @field times table Launch tick indexed by rocket number +--- @field silos table Silo data indexed by unit number + +--- Get the rocket data for a force, creating it if it does not exist +--- @param force LuaForce +--- @return ExpGui_RocketInfo.force_data +local function get_force_data(force) + local data = Elements.container.data[force] + if not data then + data = { stats = {}, times = {}, silos = {} } + Elements.container.data[force] = data end - - return rtn -end - ---- Get the number of rockets that a force has launched ---- @param force_name string Name of the force to get the count for ---- @return number -local function get_rocket_count(force_name) - return game.forces[force_name].rockets_launched + return data end --- Get the total number of rockets launched by all forces @@ -97,14 +73,14 @@ local function get_game_rocket_count() end --- Get the rolling average time to launch a rocket based on the last count rockets ---- @param force_name string Name of the force to get the average for +--- @param force LuaForce --- @param count number Number of rockets to average over --- @return number # Number of ticks required to launch one rocket -local function get_rolling_average(force_name, count) - local times = rocket_times[force_name] - local rocket_count = game.forces[force_name].rockets_launched - if rocket_count == 0 or not times then return 0 end +local function get_rolling_average(force, count) + local rocket_count = force.rockets_launched + if rocket_count == 0 then return 0 end + local times = get_force_data(force).times local last_launch_time = times[rocket_count] or 0 local start_rocket_time = 0 if count < rocket_count then @@ -115,6 +91,119 @@ local function get_rolling_average(force_name, count) return math.floor((last_launch_time - start_rocket_time) / rocket_count) end +--- @class ExpGui_RocketInfo.stat_row +--- @field key string Unique key used to look up the value label +--- @field name string Data name used to select the locale keys +--- @field subname number? Optional subname passed as a locale parameter +--- @field value LocalisedString +--- @field tooltip LocalisedString? + +--- Compute the ordered stat rows for a force +--- @param force LuaForce +--- @return ExpGui_RocketInfo.stat_row[] +local function compute_stat_rows(force) + local stats = get_force_data(force).stats + local force_rockets = force.rockets_launched + local rows = {} + + if config.stats.show_first_rocket then + local value = stats.first_launch or 0 + rows[#rows + 1] = { key = "first-launch", name = "first-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } + end + + if config.stats.show_last_rocket then + local value = stats.last_launch or 0 + rows[#rows + 1] = { key = "last-launch", name = "last-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } + end + + if config.stats.show_fastest_rocket then + local value = stats.fastest_launch or 0 + rows[#rows + 1] = { key = "fastest-launch", name = "fastest-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } + end + + if config.stats.show_total_rockets then + local total_rockets = get_game_rocket_count() + if total_rockets == 0 then total_rockets = 1 end + local percentage = math.floor(force_rockets / total_rockets * 1000) / 10 + rows[#rows + 1] = { key = "total-rockets", name = "total-rockets", value = tostring(force_rockets), tooltip = { "exp-gui_rocket-info.value-tooltip-total-rockets", percentage } } + end + + if config.stats.show_game_avg then + local avg = force_rockets > 0 and math.floor(game.tick / force_rockets) or 0 + rows[#rows + 1] = { key = "avg-launch", name = "avg-launch", value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) } + end + + for _, avg_over in pairs(config.stats.rolling_avg) do + local avg = get_rolling_average(force, avg_over) + rows[#rows + 1] = { key = "avg-launch-n-" .. avg_over, name = "avg-launch-n", subname = avg_over, value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) } + end + + return rows +end + +--- @class ExpGui_RocketInfo.milestone_row +--- @field milestone number The milestone this row represents +--- @field value LocalisedString +--- @field tooltip LocalisedString + +--- Compute the ordered milestone rows for a force, up to and including the next unachieved milestone +--- @param force LuaForce +--- @return ExpGui_RocketInfo.milestone_row[] +local function compute_milestone_rows(force) + local times = get_force_data(force).times + local force_rockets = force.rockets_launched + local rows = {} + + for _, milestone in ipairs(config.milestones) do + --- @cast milestone number + if milestone <= force_rockets then + local time = times[milestone] or 0 + rows[#rows + 1] = { milestone = milestone, value = time_formats.caption_hours(time), tooltip = time_formats.tooltip_hours(time) } + else + rows[#rows + 1] = { milestone = milestone, value = { "exp-gui_rocket-info.data-caption-milestone-next" }, tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-next" } } + break + end + end + + return rows +end + +--- @class ExpGui_RocketInfo.progress_row +--- @field x LocalisedString +--- @field y LocalisedString +--- @field caption LocalisedString +--- @field tooltip LocalisedString +--- @field color Color + +--- Compute the progress row data for a silo, clears awaiting_reset once the silo is no longer waiting +--- @param silo_data ExpGui_RocketInfo.silo_data +--- @return ExpGui_RocketInfo.progress_row +local function compute_progress_row(silo_data) + local entity = silo_data.entity + local position = entity.position + local waiting = entity.status == defines.entity_status.waiting_to_launch_rocket + + local caption = { "exp-gui_rocket-info.progress-caption", entity.rocket_parts } + local color = font_color.neutral + if waiting and silo_data.awaiting_reset then + caption = { "exp-gui_rocket-info.progress-launched" } + color = font_color.launched + elseif waiting then + caption = { "exp-gui_rocket-info.progress-caption", 100 } + color = font_color.waiting + else + silo_data.awaiting_reset = false + end + + return { + x = { "exp-gui_rocket-info.progress-x-pos", position.x }, + y = { "exp-gui_rocket-info.progress-y-pos", position.y }, + caption = caption, + tooltip = { "exp-gui_rocket-info.progress-tooltip", silo_data.launched }, + color = color, + } +end + --[[ Below here is the gui, it is split into three collapsible sections; stats, milestones, and build progress. ]] @@ -151,8 +240,8 @@ Elements.toggle_section_button = Gui.define("rocket_info/toggle_section_button") --- A clickable coordinate label which opens the silo location on the map when pressed --- @class ExpGui_RocketInfo.elements.position_label: ExpElement ---- @field data table ---- @overload fun(parent: LuaGuiElement, opts: { caption: LocalisedString, tooltip: LocalisedString?, unit_number: number }): LuaGuiElement +--- @field data table +--- @overload fun(parent: LuaGuiElement, opts: { caption: LocalisedString, tooltip: LocalisedString?, entity: LuaEntity }): LuaGuiElement Elements.position_label = Gui.define("rocket_info/position_label") :draw{ type = "label", @@ -163,189 +252,244 @@ Elements.position_label = Gui.define("rocket_info/position_label") padding = { 0, 2 }, } :element_data( - Gui.from_argument("unit_number") + Gui.from_argument("entity") ) :on_click(function(def, player, element) --- @cast def ExpGui_RocketInfo.elements.position_label if not config.progress.allow_zoom_to_map then return end - local silo_data = rocket_silos[def.data[element]] - if not silo_data or not silo_data.entity.valid then return end - local entity = silo_data.entity + local entity = def.data[element] + if not entity or not entity.valid then return end player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface } end) --[[ @as any ]] ---- Add a name and value label pair to a data table ---- @param data_table LuaGuiElement The two column table to add the pair to ---- @param name string The data name, used to select the locale key ---- @param subname number? Optional subname passed as a parameter to the locale string ---- @param value LocalisedString The value to display ---- @param value_tooltip LocalisedString? Optional tooltip for the value label -local function add_data_label(data_table, name, subname, value, value_tooltip) +--- Data table showing the launch statistics for a force +--- @class ExpGui_RocketInfo.elements.stats_table: ExpElement +--- @field data table> +--- @overload fun(parent: LuaGuiElement): LuaGuiElement +Elements.stats_table = Gui.define("rocket_info/stats_table") + :track_all_elements() + :draw(function(def, parent) + --- @cast def ExpGui_RocketInfo.elements.stats_table + local data_table = Gui.elements.scroll_table(parent, 215, 2) + def.data[data_table] = {} + return data_table + end) --[[ @as any ]] + +--- Add a stat row to the table and store its value label +--- @param data_table LuaGuiElement +--- @param row ExpGui_RocketInfo.stat_row +function Elements.stats_table.add_row(data_table, row) + local labels = Elements.stats_table.data[data_table] local name_label = data_table.add{ type = "label", - caption = { "exp-gui_rocket-info.data-caption-" .. name, subname }, - tooltip = { "exp-gui_rocket-info.data-tooltip-" .. name, subname }, + caption = { "exp-gui_rocket-info.data-caption-" .. row.name, row.subname }, + tooltip = { "exp-gui_rocket-info.data-tooltip-" .. row.name, row.subname }, } name_label.style.padding = { 0, 2 } local value_label = data_table.add{ type = "label", - caption = value, - tooltip = value_tooltip, + caption = row.value, + tooltip = row.tooltip, } value_label.style.padding = { 0, 2 } + + labels[row.key] = value_label end ---- Data table showing the launch statistics for a force ---- @class ExpGui_RocketInfo.elements.stats_table: ExpElement ---- @overload fun(parent: LuaGuiElement): LuaGuiElement -Elements.stats_table = Gui.define("rocket_info/stats_table") - :track_all_elements() - :draw(function(def, parent) - return Gui.elements.scroll_table(parent, 215, 2) - end) --[[ @as any ]] - ---- Refresh a stats table with the most recent data for a force ---- @param stats_table LuaGuiElement ---- @param force_name string -function Elements.stats_table.refresh(stats_table, force_name) - stats_table.clear() - local stats = rocket_stats[force_name] or {} - local force_rockets = get_rocket_count(force_name) - - if config.stats.show_first_rocket then - local value = stats.first_launch or 0 - add_data_label(stats_table, "first-launch", nil, time_formats.caption_hours(value), time_formats.tooltip_hours(value)) - end - - if config.stats.show_last_rocket then - local value = stats.last_launch or 0 - add_data_label(stats_table, "last-launch", nil, time_formats.caption_hours(value), time_formats.tooltip_hours(value)) - end - - if config.stats.show_fastest_rocket then - local value = stats.fastest_launch or 0 - add_data_label(stats_table, "fastest-launch", nil, time_formats.caption_hours(value), time_formats.tooltip_hours(value)) - end - - if config.stats.show_total_rockets then - local total_rockets = get_game_rocket_count() - if total_rockets == 0 then total_rockets = 1 end - local percentage = math.floor(force_rockets / total_rockets * 1000) / 10 - add_data_label(stats_table, "total-rockets", nil, tostring(force_rockets), { "exp-gui_rocket-info.value-tooltip-total-rockets", percentage }) - end - - if config.stats.show_game_avg then - local avg = force_rockets > 0 and math.floor(game.tick / force_rockets) or 0 - add_data_label(stats_table, "avg-launch", nil, time_formats.caption(avg), time_formats.tooltip(avg)) - end - - for _, avg_over in pairs(config.stats.rolling_avg) do - local avg = get_rolling_average(force_name, avg_over) - add_data_label(stats_table, "avg-launch-n", avg_over, time_formats.caption(avg), time_formats.tooltip(avg)) +--- Refresh the stats table for a force, adding any rows that do not yet exist +--- @param data_table LuaGuiElement +--- @param force LuaForce +function Elements.stats_table.refresh(data_table, force) + local labels = Elements.stats_table.data[data_table] + for _, row in ipairs(compute_stat_rows(force)) do + local value_label = labels[row.key] + if value_label then + value_label.caption = row.value + value_label.tooltip = row.tooltip + else + Elements.stats_table.add_row(data_table, row) + end end end --- Data table showing the milestones for a force --- @class ExpGui_RocketInfo.elements.milestones_table: ExpElement +--- @field data table> --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.milestones_table = Gui.define("rocket_info/milestones_table") :track_all_elements() :draw(function(def, parent) - return Gui.elements.scroll_table(parent, 215, 2) + --- @cast def ExpGui_RocketInfo.elements.milestones_table + local data_table = Gui.elements.scroll_table(parent, 215, 2) + def.data[data_table] = {} + return data_table end) --[[ @as any ]] ---- Refresh a milestones table with the most recent data for a force ---- @param milestones_table LuaGuiElement ---- @param force_name string -function Elements.milestones_table.refresh(milestones_table, force_name) - milestones_table.clear() - local force_rockets = get_rocket_count(force_name) - local times = rocket_times[force_name] or {} +--- Add a milestone row to the table and store its value label +--- @param data_table LuaGuiElement +--- @param row ExpGui_RocketInfo.milestone_row +function Elements.milestones_table.add_row(data_table, row) + local labels = Elements.milestones_table.data[data_table] + local name_label = data_table.add{ + type = "label", + caption = { "exp-gui_rocket-info.data-caption-milestone-n", row.milestone }, + tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-n", row.milestone }, + } + name_label.style.padding = { 0, 2 } - for _, milestone in ipairs(config.milestones) do - if milestone <= force_rockets then - local time = times[milestone] or 0 - add_data_label(milestones_table, "milestone-n", milestone, time_formats.caption_hours(time), time_formats.tooltip_hours(time)) + local value_label = data_table.add{ + type = "label", + caption = row.value, + tooltip = row.tooltip, + } + value_label.style.padding = { 0, 2 } + + labels[row.milestone] = value_label +end + +--- Refresh the milestones table for a force, adding rows as new milestones become visible +--- @param data_table LuaGuiElement +--- @param force LuaForce +function Elements.milestones_table.refresh(data_table, force) + local labels = Elements.milestones_table.data[data_table] + for _, row in ipairs(compute_milestone_rows(force)) do + local value_label = labels[row.milestone] + if value_label then + value_label.caption = row.value + value_label.tooltip = row.tooltip else - -- The first unachieved milestone is shown as the next milestone then we stop - add_data_label(milestones_table, "milestone-n", milestone, { "exp-gui_rocket-info.data-caption-milestone-next" }, { "exp-gui_rocket-info.data-tooltip-milestone-next" }) - break + Elements.milestones_table.add_row(data_table, row) end end end +--- @class ExpGui_RocketInfo.elements.progress_table.row +--- @field x LuaGuiElement +--- @field y LuaGuiElement +--- @field progress LuaGuiElement + +--- @class ExpGui_RocketInfo.elements.progress_table.data +--- @field rows table +--- @field no_silos LuaGuiElement + --- Data table showing the build progress of each silo for a force --- @class ExpGui_RocketInfo.elements.progress_table: ExpElement +--- @field data table --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.progress_table = Gui.define("rocket_info/progress_table") :track_all_elements() :draw(function(def, parent) - return Gui.elements.scroll_table(parent, 215, 3) - end) --[[ @as any ]] + --- @cast def ExpGui_RocketInfo.elements.progress_table + local data_table = Gui.elements.scroll_table(parent, 215, 3) ---- Refresh a progress table with the most recent data for a force ---- @param progress_table LuaGuiElement ---- @param force_name string -function Elements.progress_table.refresh(progress_table, force_name) - progress_table.clear() - local silos = get_silos(force_name) - - if #silos == 0 then - progress_table.add{ + -- The no silos label lives next to the table inside the same collapsible scroll pane + local no_silos = assert(data_table.parent).add{ type = "label", caption = { "exp-gui_rocket-info.progress-no-silos" }, - }.style.padding = { 1, 2 } - return - end - - local zoom_tooltip = config.progress.allow_zoom_to_map and { "exp-gui_rocket-info.progress-label-tooltip" } or nil - for _, silo_data in pairs(silos) do - local entity = silo_data.entity - local position = entity.position - - -- Work out the progress caption, tooltip and colour - local waiting = entity.status == defines.entity_status.waiting_to_launch_rocket - local progress_caption = { "exp-gui_rocket-info.progress-caption", entity.rocket_parts } - local progress_tooltip = { "exp-gui_rocket-info.progress-tooltip", silo_data.launched } - local progress_color = font_color.neutral - if waiting and silo_data.awaiting_reset then - progress_caption = { "exp-gui_rocket-info.progress-launched" } - progress_color = font_color.launched - elseif waiting then - progress_caption = { "exp-gui_rocket-info.progress-caption", 100 } - progress_color = font_color.waiting - else - silo_data.awaiting_reset = false - end - - -- Add the clickable coordinates and the progress label - Elements.position_label(progress_table, { caption = { "exp-gui_rocket-info.progress-x-pos", position.x }, tooltip = zoom_tooltip, unit_number = entity.unit_number }) - Elements.position_label(progress_table, { caption = { "exp-gui_rocket-info.progress-y-pos", position.y }, tooltip = zoom_tooltip, unit_number = entity.unit_number }) - local progress_label = progress_table.add{ - type = "label", - caption = progress_caption, - tooltip = progress_tooltip, } - progress_label.style.padding = { 0, 2 } - progress_label.style.font_color = progress_color - end + no_silos.style.padding = { 1, 2 } + + def.data[data_table] = { rows = {}, no_silos = no_silos } + return data_table + end) --[[ @as any ]] + +--- Add a silo row to the progress table and store its labels +--- @param data_table LuaGuiElement +--- @param silo_data ExpGui_RocketInfo.silo_data +function Elements.progress_table.add_row(data_table, silo_data) + local rows = Elements.progress_table.data[data_table].rows + local entity = silo_data.entity + local row_data = compute_progress_row(silo_data) + local zoom_tooltip = config.progress.allow_zoom_to_map and { "exp-gui_rocket-info.progress-label-tooltip" } or nil + + local x = Elements.position_label(data_table, { caption = row_data.x, tooltip = zoom_tooltip, entity = entity }) + local y = Elements.position_label(data_table, { caption = row_data.y, tooltip = zoom_tooltip, entity = entity }) + local progress = data_table.add{ + type = "label", + caption = row_data.caption, + tooltip = row_data.tooltip, + } + progress.style.padding = { 0, 2 } + progress.style.font_color = row_data.color + + rows[entity.unit_number] = { x = x, y = y, progress = progress } end ---- Add a collapsible section to a container, returns the populated data table +--- Update an existing silo row to match the latest data +--- @param data_table LuaGuiElement +--- @param silo_data ExpGui_RocketInfo.silo_data +function Elements.progress_table.refresh_row(data_table, silo_data) + local row = Elements.progress_table.data[data_table].rows[silo_data.entity.unit_number] + local row_data = compute_progress_row(silo_data) + row.x.caption = row_data.x + row.y.caption = row_data.y + row.progress.caption = row_data.caption + row.progress.tooltip = row_data.tooltip + row.progress.style.font_color = row_data.color +end + +--- Remove a silo row from the progress table +--- @param data_table LuaGuiElement +--- @param unit_number number +function Elements.progress_table.remove_row(data_table, unit_number) + local rows = Elements.progress_table.data[data_table].rows + local row = rows[unit_number] + if not row then return end + rows[unit_number] = nil + Gui.destroy_if_valid(row.x) + Gui.destroy_if_valid(row.y) + Gui.destroy_if_valid(row.progress) +end + +--- Refresh the progress table for a force, reconciling rows with the current set of silos +--- @param data_table LuaGuiElement +--- @param force LuaForce +function Elements.progress_table.refresh(data_table, force) + local silos = get_force_data(force).silos + local data = Elements.progress_table.data[data_table] + local rows = data.rows + local seen = {} + local has_silos = false + + for unit_number, silo_data in pairs(silos) do + if not silo_data.entity.valid then + -- Prune silos that are no longer valid + silos[unit_number] = nil + else + has_silos = true + seen[unit_number] = true + if rows[unit_number] then + Elements.progress_table.refresh_row(data_table, silo_data) + else + Elements.progress_table.add_row(data_table, silo_data) + end + end + end + + -- Remove rows for silos that are no longer present + for unit_number in pairs(rows) do + if not seen[unit_number] then + Elements.progress_table.remove_row(data_table, unit_number) + end + end + + data.no_silos.visible = not has_silos + data_table.visible = has_silos +end + +--- Add a collapsible section to a container and return its data table --- @param container LuaGuiElement The container frame to add the section to --- @param section_name string Used to select the locale keys for the header ---- @param table_define ExpElement The data table element define to add ---- @param force_name string The force to populate the table with +--- @param table_define any The data table element define to add --- @return LuaGuiElement -local function add_section(container, section_name, table_define, force_name) +local function add_section(container, section_name, table_define) local header = Gui.elements.header(container, { caption = { "exp-gui_rocket-info.section-caption-" .. section_name }, tooltip = { "exp-gui_rocket-info.section-tooltip-" .. section_name }, }) local data_table = table_define(container) - table_define.refresh(data_table, force_name) -- The scroll pane is the parent of the table, it is what gets collapsed local scroll_pane = assert(data_table.parent) @@ -357,28 +501,28 @@ end --- Container added to the left gui flow --- @class ExpGui_RocketInfo.elements.container: ExpElement ---- @overload fun(parent: LuaGuiElement): LuaGuiElement +--- @field data table Elements.container = Gui.define("rocket_info/container") :draw(function(def, parent) local container = Gui.elements.container(parent, 200) container.style.padding = 0 - local force_name = Gui.get_player(parent).force.name --[[ @as string ]] + local force = Gui.get_player(parent).force --[[ @as LuaForce ]] if config.stats.show_stats then - add_section(container, "stats", Elements.stats_table, force_name) + Elements.stats_table.refresh(add_section(container, "stats", Elements.stats_table), force) end if config.milestones.show_milestones then - add_section(container, "milestones", Elements.milestones_table, force_name) + Elements.milestones_table.refresh(add_section(container, "milestones", Elements.milestones_table), force) end if config.progress.show_progress then - add_section(container, "progress", Elements.progress_table, force_name) + Elements.progress_table.refresh(add_section(container, "progress", Elements.progress_table), force) end return Gui.elements.container.get_root_element(container) - end) --[[ @as any ]] + end) --- Add the element to the left flow with a toolbar button Gui.add_left_element(Elements.container, false) @@ -397,23 +541,21 @@ Below here is the event handling, the data tracking and gui refreshing are wired ]] --- Refresh the stats and milestones tables for all online players on a force ---- @param force_name string -local function refresh_force_stats(force_name) - local force = game.forces[force_name] - for _, stats_table in Elements.stats_table:online_elements(force) do - Elements.stats_table.refresh(stats_table, force_name) +--- @param force LuaForce +local function refresh_force_stats(force) + for _, data_table in Elements.stats_table:online_elements(force) do + Elements.stats_table.refresh(data_table, force) end - for _, milestones_table in Elements.milestones_table:online_elements(force) do - Elements.milestones_table.refresh(milestones_table, force_name) + for _, data_table in Elements.milestones_table:online_elements(force) do + Elements.milestones_table.refresh(data_table, force) end end --- Refresh the progress table for all online players on a force ---- @param force_name string -local function refresh_force_progress(force_name) - local force = game.forces[force_name] - for _, progress_table in Elements.progress_table:online_elements(force) do - Elements.progress_table.refresh(progress_table, force_name) +--- @param force LuaForce +local function refresh_force_progress(force) + for _, data_table in Elements.progress_table:online_elements(force) do + Elements.progress_table.refresh(data_table, force) end end @@ -421,16 +563,11 @@ end --- @param event EventData.on_cargo_pod_finished_ascending local function on_cargo_pod_finished_ascending(event) local force = event.cargo_pod.force --[[ @as LuaForce ]] - local force_name = force.name local rockets_launched = force.rockets_launched + local force_data = get_force_data(force) -- Update the launch stats for the force - local stats = rocket_stats[force_name] - if not stats then - stats = {} - rocket_stats[force_name] = stats - end - + local stats = force_data.stats if rockets_launched == 1 then stats.first_launch = event.tick stats.fastest_launch = event.tick @@ -441,53 +578,47 @@ local function on_cargo_pod_finished_ascending(event) stats.last_launch = event.tick -- Append the launch tick into the times array - local times = rocket_times[force_name] - if not times then - times = {} - rocket_times[force_name] = times - end - - times[rockets_launched] = event.tick + force_data.times[rockets_launched] = event.tick -- Discard the launch time that is no longer needed by any rolling average unless it is a milestone local remove_rocket = rockets_launched - largest_rolling_avg if remove_rocket > 0 and not table.array_contains(config.milestones, remove_rocket) then - times[remove_rocket] = nil + force_data.times[remove_rocket] = nil end - refresh_force_stats(force_name) - refresh_force_progress(force_name) + refresh_force_stats(force) + refresh_force_progress(force) end --- Mark a silo as awaiting reset when a launch is ordered --- @param event EventData.on_rocket_launch_ordered local function on_rocket_launch_ordered(event) - local silo_data = rocket_silos[event.rocket_silo.unit_number] + local silo = event.rocket_silo + local silo_data = get_force_data(silo.force --[[ @as LuaForce ]]).silos[silo.unit_number] if not silo_data then return end silo_data.launched = silo_data.launched + 1 silo_data.awaiting_reset = true - refresh_force_progress(event.rocket_silo.force.name) + refresh_force_progress(silo.force --[[ @as LuaForce ]]) end ---- Add a silo to the list when it is built +--- Add a silo to the force data when it is built --- @param event EventData.on_built_entity | EventData.on_robot_built_entity | EventData.script_raised_built | EventData.script_raised_revive local function on_built(event) local entity = event.entity if not entity.valid or entity.name ~= "rocket-silo" then return end - rocket_silos[entity.unit_number] = { + get_force_data(entity.force --[[ @as LuaForce ]]).silos[entity.unit_number] = { entity = entity, - force = entity.force.name, launched = 0, awaiting_reset = false, } - refresh_force_progress(entity.force.name) + refresh_force_progress(entity.force --[[ @as LuaForce ]]) end --- Refresh the progress for all forces that own at least one silo local function refresh_all_progress() for _, force in pairs(game.forces) do - if #get_silos(force.name) > 0 then - refresh_force_progress(force.name) + if next(get_force_data(force).silos) then + refresh_force_progress(force) end end end From 8f6d6e1caa6649867f5854f086eabf78e2a072f3 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:05:48 +0000 Subject: [PATCH 3/6] Fix milestone lint warning in rocket info gui Narrow the milestone loop variable with a type check instead of an @cast, which tripped cast-type-mismatch because the milestones config mixes the show_milestones flag in with the numbers. Co-Authored-By: Claude Opus 4.8 (1M context) --- exp_scenario/module/gui/rocket_info.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/exp_scenario/module/gui/rocket_info.lua b/exp_scenario/module/gui/rocket_info.lua index c6be45a4..10774ae4 100644 --- a/exp_scenario/module/gui/rocket_info.lua +++ b/exp_scenario/module/gui/rocket_info.lua @@ -155,13 +155,15 @@ local function compute_milestone_rows(force) local rows = {} for _, milestone in ipairs(config.milestones) do - --- @cast milestone number - if milestone <= force_rockets then - local time = times[milestone] or 0 - rows[#rows + 1] = { milestone = milestone, value = time_formats.caption_hours(time), tooltip = time_formats.tooltip_hours(time) } - else - rows[#rows + 1] = { milestone = milestone, value = { "exp-gui_rocket-info.data-caption-milestone-next" }, tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-next" } } - break + -- The milestones config mixes the show_milestones flag with the milestone numbers + if type(milestone) == "number" then + if milestone <= force_rockets then + local time = times[milestone] or 0 + rows[#rows + 1] = { milestone = milestone, value = time_formats.caption_hours(time), tooltip = time_formats.tooltip_hours(time) } + else + rows[#rows + 1] = { milestone = milestone, value = { "exp-gui_rocket-info.data-caption-milestone-next" }, tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-next" } } + break + end end end From c669e36bec06ab0d2c15df4293b368f356e9b2e3 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:19:29 +0000 Subject: [PATCH 4/6] Remove migrated rocket-info from exp_legacy The rocket info gui and its control module now live in exp_scenario/gui/rocket_info, so delete the orphaned legacy files and drop the disabled file loader entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- exp_legacy/module/config/_file_loader.lua | 1 - exp_legacy/module/modules/control/rockets.lua | 204 ------ exp_legacy/module/modules/gui/rocket-info.lua | 587 ------------------ 3 files changed, 792 deletions(-) delete mode 100644 exp_legacy/module/modules/control/rockets.lua delete mode 100644 exp_legacy/module/modules/gui/rocket-info.lua diff --git a/exp_legacy/module/config/_file_loader.lua b/exp_legacy/module/config/_file_loader.lua index 8a4c7530..4562a7d5 100644 --- a/exp_legacy/module/config/_file_loader.lua +++ b/exp_legacy/module/config/_file_loader.lua @@ -22,7 +22,6 @@ return { --"modules.data.toolbar", --- GUI - -- "modules.gui.rocket-info", -- migrated to exp_scenario/gui/rocket_info "modules.gui.warp-list", "modules.gui.player-list", "modules.gui.vlayer", diff --git a/exp_legacy/module/modules/control/rockets.lua b/exp_legacy/module/modules/control/rockets.lua deleted file mode 100644 index a46d66c2..00000000 --- a/exp_legacy/module/modules/control/rockets.lua +++ /dev/null @@ -1,204 +0,0 @@ ---[[-- Control Module - Rockets - - Stores rocket stats for each force. - @control Rockets - @alias Rockets - - @usage - -- import the module from the control modules - local Rockets = require("modules.exp_legacy.modules.control.rockets") --- @dep modules.control.rockets - - -- Some basic information is stored for each silo that has been built - -- the data includes: the tick it was built, the rockets launched from it and more - Rockets.get_silo_data(rocket_silo_entity) - - -- Some information is also stored for each force - Rockets.get_stats('player') - - -- You can get the rocket data for all silos for a force by using get_silos - Rockets.get_silos('player') - - -- You can get the launch time for a rocket, meaning what game tick the 50th rocket was launched - Rockets.get_rocket_time('player', 50) - - -- The rolling average will work out the time to launch one rocket based on the last X rockets - Rockets.get_rolling_average('player', 10) - -]] - -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local Storage = require("modules/exp_util/storage") -local config = require("modules.exp_legacy.config.gui.rockets") --- @dep config.rockets - -local largest_rolling_avg = 0 -for _, avg_over in pairs(config.stats.rolling_avg) do - if avg_over > largest_rolling_avg then - largest_rolling_avg = avg_over - end -end - -local Rockets = { - times = {}, - stats = {}, - silos = {}, -} - -local rocket_times = Rockets.times -local rocket_stats = Rockets.stats -local rocket_silos = Rockets.silos -Storage.register({ - rocket_times = rocket_times, - rocket_stats = rocket_stats, - rocket_silos = rocket_silos, -}, function(tbl) - Rockets.times = tbl.rocket_times - Rockets.stats = tbl.rocket_stats - Rockets.silos = tbl.rocket_silos - rocket_times = Rockets.times - rocket_stats = Rockets.stats - rocket_silos = Rockets.silos -end) - ---- Gets the silo data for a given silo entity ---- @param silo LuaEntity Rocket silo entity ---- @return table # Data table for this silo, contains rockets launch, silo status, and its force -function Rockets.get_silo_data(silo) - return rocket_silos[silo.unit_number] -end - ---- Gets the silo entity from its silo name, reverse to get_silo_data ---- @param silo_name string Silo name that is stored in its data ---- @return LuaEntity # Rocket silo entity -function Rockets.get_silo_entity(silo_name) - return rocket_silos[tonumber(silo_name)].entity -end - ---- Gets the rocket stats for a force ---- @param force_name string Name of the force to get the stats for ---- @return table # Stats for the force -function Rockets.get_stats(force_name) - return rocket_stats[force_name] or {} -end - ---- Gets all the rocket silos that belong to a force ---- @param force_name string Name of the force to get the silos for ---- @return table # Array of silo data that all belong to this force -function Rockets.get_silos(force_name) - local rtn = {} - for _, silo_data in pairs(rocket_silos) do - if silo_data.force == force_name then - table.insert(rtn, silo_data) - end - end - - return rtn -end - ---- Gets the launch time of a given rocket, due to cleaning not all counts are valid ---- @param force_name string Name of the force to get the count for ---- @param rocket_number number Number of the rocket to get the launch time for ---- @return number? # Game tick that the rocket was launched on -function Rockets.get_rocket_time(force_name, rocket_number) - return rocket_times[force_name] and rocket_times[force_name][rocket_number] or nil -end - ---- Gets the number of rockets that a force has launched ---- @param force_name string the name of the force to get the count for ---- @return number # Number of rockets that the force has launched -function Rockets.get_rocket_count(force_name) - local force = game.forces[force_name] - return force.rockets_launched -end - ---- Gets the total number of rockets launched by all forces ---- @return number # Total number of rockets launched this game -function Rockets.get_game_rocket_count() - local rtn = 0 - for _, force in pairs(game.forces) do - rtn = rtn + force.rockets_launched - end - - return rtn -end - ---- Gets the rolling average time to launch a rocket ---- @param force_name string Name of the force to get the average for ---- @param count number Distance to get the rolling average over ---- @return number # Number of ticks required to launch one rocket -function Rockets.get_rolling_average(force_name, count) - local force = game.forces[force_name] - local rocket_count = force.rockets_launched - if rocket_count == 0 then return 0 end - local last_launch_time = rocket_times[force_name][rocket_count] - local start_rocket_time = 0 - if count < rocket_count then - start_rocket_time = rocket_times[force_name][rocket_count - count + 1] - rocket_count = count - end - return math.floor((last_launch_time - start_rocket_time) / rocket_count) -end - ---- When a launch is trigger it will wait for the silo to reset ---- @param event EventData.on_rocket_launch_ordered -Event.add(defines.events.on_rocket_launch_ordered, function(event) - local silo_data = Rockets.get_silo_data(event.rocket_silo) - assert(silo_data, "Rocket silo missing data: " .. tostring(event.rocket_silo)) - silo_data.launched = silo_data.launched + 1 - silo_data.awaiting_reset = true -end) - ---- Event used to update the stats and the hui when a rocket is launched ---- @param event EventData.on_cargo_pod_finished_ascending -Event.add(defines.events.on_cargo_pod_finished_ascending, function(event) - local force = event.cargo_pod.force - local force_name = force.name - local rockets_launched = force.rockets_launched - - --- Handles updates to the rocket stats - local stats = rocket_stats[force_name] - if not stats then - rocket_stats[force_name] = {} - stats = rocket_stats[force_name] - end - - if rockets_launched == 1 then - stats.first_launch = event.tick - stats.fastest_launch = event.tick - elseif event.tick - stats.last_launch < stats.fastest_launch then - stats.fastest_launch = event.tick - stats.last_launch - end - - stats.last_launch = event.tick - - --- Appends the new rocket into the array - if not rocket_times[force_name] then - rocket_times[force_name] = {} - end - - rocket_times[force_name][rockets_launched] = event.tick - - local remove_rocket = rockets_launched - largest_rolling_avg - if remove_rocket > 0 and not table.contains(config.milestones, remove_rocket) then - rocket_times[force_name][remove_rocket] = nil - end -end) - ---- Adds a silo to the list when it is built ---- @param event EventData.on_built_entity | EventData.on_robot_built_entity -local function on_built(event) - local entity = event.entity - if entity.valid and entity.name == "rocket-silo" then - rocket_silos[entity.unit_number] = { - name = tostring(entity.unit_number), - force = entity.force.name, - entity = entity, - launched = 0, - awaiting_reset = false, - built = game.tick, - } - end -end - -Event.add(defines.events.on_built_entity, on_built) -Event.add(defines.events.on_robot_built_entity, on_built) - -return Rockets diff --git a/exp_legacy/module/modules/gui/rocket-info.lua b/exp_legacy/module/modules/gui/rocket-info.lua deleted file mode 100644 index e31efea9..00000000 --- a/exp_legacy/module/modules/gui/rocket-info.lua +++ /dev/null @@ -1,587 +0,0 @@ ---[[-- Gui Module - Rocket Info - - Adds a rocket infomation gui which shows general stats, milestones and build progress of rockets - @gui Rocket-Info - @alias rocket_info -]] - -local ExpUtil = require("modules/exp_util") -local Gui = require("modules/exp_gui") -local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local config = require("modules.exp_legacy.config.gui.rockets") --- @dep config.gui.rockets -local Colors = require("modules/exp_util/include/color") -local Rockets = require("modules.exp_legacy.modules.control.rockets") --- @dep modules.control.rockets - -local time_formats = { - caption = ExpUtil.format_time_factory_locale{ format = "short", minutes = true, seconds = true }, - caption_hours = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true }, - tooltip = ExpUtil.format_time_factory_locale{ format = "long", minutes = true, seconds = true }, - tooltip_hours = ExpUtil.format_time_factory_locale{ format = "long", hours = true, minutes = true, seconds = true }, -} - ---- Check if a player is allowed to use certain interactions -local function check_player_permissions(player, action) - if not config.progress["allow_" .. action] then - return false - end - - if config.progress[action .. "_admins_only"] and not player.admin then - return false - end - - if config.progress[action .. "_role_permission"] - and not Roles.player_allowed(player, config.progress[action .. "_role_permission"]) then - return false - end - - return true -end - ---- Button to toggle the auto launch on a rocket silo --- @element toggle_launch -local toggle_launch = Gui.define("toggle_launch") - :draw{ - type = "sprite-button", - sprite = "utility/play", - tooltip = { "rocket-info.toggle-rocket-tooltip" }, - name = Gui.from_name, - } - :style(Gui.styles.sprite{ - size = 16, - }) - :on_click(function(def, player, element) - local rocket_silo_name = element.parent.name:sub(8) - local rocket_silo = Rockets.get_silo_entity(rocket_silo_name) - if rocket_silo.auto_launch then - element.sprite = "utility/play" - element.tooltip = { "rocket-info.toggle-rocket-tooltip" } - rocket_silo.auto_launch = false - else - element.sprite = "utility/stop" - element.tooltip = { "rocket-info.toggle-rocket-tooltip-disabled" } - rocket_silo.auto_launch = true - end - end) - ---- XY cords that allow zoom to map when pressed --- @element silo_cords -local silo_cords = Gui.define("silo_cords") - :draw(function(definition, parent, silo_data) - local silo_name = silo_data.silo_name - local pos = silo_data.position - local tooltip = config.progress.allow_zoom_to_map and { "rocket-info.progress-label-tooltip" } or nil - - -- Add the x cord flow - local flow_x = parent.add{ - type = "flow", - name = "label-x-" .. silo_name, - caption = silo_name, - } - flow_x.style.padding = { 0, 2, 0, 1 } - - -- Add the x cord label - local label_x = flow_x.add{ - type = "label", - caption = { "rocket-info.progress-x-pos", pos.x }, - tooltip = tooltip, - } - - -- Add the y cord flow - local flow_y = parent.add{ - type = "flow", - name = "label-y-" .. silo_name, - caption = silo_name, - } - flow_y.style.padding = { 0, 2, 0, 1 } - - -- Add the y cord label - local label_y = flow_y.add{ - type = "label", - caption = { "rocket-info.progress-y-pos", pos.y }, - tooltip = tooltip, - } - - if config.progress.allow_zoom_to_map then - definition:link_element(label_x) - definition:link_element(label_y) - end - - return Gui.no_return() - end) - :on_click(function(def, player, element) - local rocket_silo_name = element.parent.caption - local rocket_silo = Rockets.get_silo_entity(rocket_silo_name) - player.set_controller{ type = defines.controllers.remote, position = rocket_silo.position, surface = rocket_silo.surface } - end) - ---- Base element for each rocket in the progress list --- @element rocket_entry -local rocket_entry = Gui.define("rocket_entry") - :draw(function(_, parent, silo_data) - local silo_name = silo_data.silo_name - local player = Gui.get_player(parent) - - -- Add the toggle auto launch if the player is allowed it - -- Auto launch was removed from the api and no 2.0 equivalent was added - -- https://forums.factorio.com/viewtopic.php?f=28&t=118065&p=656502 - if check_player_permissions(player, "toggle_active") then - parent.add{ type = "flow" } - --[[local flow = parent.add{ type = "flow", name = "toggle-" .. silo_name } - local button = toggle_launch(flow) - button.tooltip = silo_data.toggle_tooltip - button.sprite = silo_data.toggle_sprite]] - end - - -- Draw the silo cords element - silo_cords(parent, silo_data) - - -- Add a progress label - local alignment = Gui.elements.aligned_flow(parent, { name = silo_name }) - local element = - alignment.add{ - type = "label", - name = "label", - caption = silo_data.progress_caption, - tooltip = silo_data.progress_tooltip, - } - - -- Return the progress label - return element - end) - ---- Data label which contains a name and a value label pair --- @element data_label -local data_label = Gui.define("data_label") - :draw(function(_, parent, label_data) - local data_name = label_data.name - local data_subname = label_data.subname - local data_fullname = data_subname and data_name .. data_subname or data_name - - -- Add the name label - local name_label = parent.add{ - type = "label", - name = data_fullname .. "-label", - caption = { "rocket-info.data-caption-" .. data_name, data_subname }, - tooltip = { "rocket-info.data-tooltip-" .. data_name, data_subname }, - } - name_label.style.padding = { 0, 2 } - - --- Right aligned label to store the data - local alignment = Gui.elements.aligned_flow(parent, { name = data_fullname }) - local element = - alignment.add{ - type = "label", - name = "label", - caption = label_data.value, - tooltip = label_data.tooltip, - } - element.style.padding = { 0, 2 } - - return element - end) - --- Used to update the captions and tooltips on the data labels -local function update_data_labels(parent, data_label_data) - for _, label_data in ipairs(data_label_data) do - local data_name = label_data.subname and label_data.name .. label_data.subname or label_data.name - if not parent[data_name] then - data_label(parent, label_data) - else - local data_label_element = parent[data_name].label - data_label_element.tooltip = label_data.tooltip - data_label_element.caption = label_data.value - end - end -end - -local function get_progress_data(force_name) - local force_silos = Rockets.get_silos(force_name) - local progress_data = {} - - for _, silo_data in pairs(force_silos) do - local rocket_silo = silo_data.entity - if not rocket_silo or not rocket_silo.valid then - -- Remove from list if not valid - force_silos[silo_data.name] = nil - table.insert(progress_data, { - silo_name = silo_data.name, - remove = true, - }) - else - -- Get the progress caption and tooltip - local progress_color = Colors.white - local progress_caption = { "rocket-info.progress-caption", rocket_silo.rocket_parts } - local progress_tooltip = { "rocket-info.progress-tooltip", silo_data.launched or 0 } - local status = rocket_silo.status == defines.entity_status.waiting_to_launch_rocket - if status and silo_data.awaiting_reset then - progress_caption = { "rocket-info.progress-launched" } - progress_color = Colors.green - elseif status then - progress_caption = { "rocket-info.progress-caption", 100 } - progress_color = Colors.cyan - else - silo_data.awaiting_reset = false - end - - -- Get the toggle button data - local toggle_tooltip = { "rocket-info.toggle-rocket-tooltip-disabled" } - local toggle_sprite = "utility/play" - if false --[[rocket_silo.auto_launch]] then - toggle_tooltip = { "rocket-info.toggle-rocket-tooltip" } - toggle_sprite = "utility/stop" - end - - -- Insert the gui data - table.insert(progress_data, { - silo_name = silo_data.name, - position = rocket_silo.position, - allow_launch = not silo_data.awaiting_reset and status or false, - progress_color = progress_color, - progress_caption = progress_caption, - progress_tooltip = progress_tooltip, - toggle_tooltip = toggle_tooltip, - toggle_sprite = toggle_sprite, - }) - end - end - - return progress_data -end - ---- Update the build progress section -local function update_build_progress(parent, progress_data) - local show_message = true - for _, silo_data in ipairs(progress_data) do - parent.parent.no_silos.visible = false - parent.visible = true - local silo_name = silo_data.silo_name - local progress_label = parent[silo_name] - if silo_data.remove then - -- Remove the rocket from the list - Gui.destroy_if_valid(parent["toggle-" .. silo_name]) - Gui.destroy_if_valid(parent["launch-" .. silo_name]) - Gui.destroy_if_valid(parent["label-x-" .. silo_name]) - Gui.destroy_if_valid(parent["label-y-" .. silo_name]) - Gui.destroy_if_valid(parent[silo_name]) - elseif not progress_label then - -- Add the rocket to the list - show_message = false - rocket_entry(parent, silo_data) - else - show_message = false - -- Update the existing labels - progress_label = progress_label.label - progress_label.caption = silo_data.progress_caption - progress_label.tooltip = silo_data.progress_tooltip - progress_label.style.font_color = silo_data.progress_color - - -- Update the toggle button - local toggle_button = parent["toggle-" .. silo_name] - if toggle_button then - toggle_button = toggle_button[toggle_launch.name] - toggle_button.tooltip = silo_data.toggle_tooltip - toggle_button.sprite = silo_data.toggle_sprite - end - end - end - - if show_message then - parent.parent.no_silos.visible = true - parent.visible = false - end -end - ---- Gets the label data for all the different stats -local function get_stats_data(force_name) - local force_rockets = Rockets.get_rocket_count(force_name) - local stats = Rockets.get_stats(force_name) - local stats_data = {} - - -- Format the first launch data - if config.stats.show_first_rocket then - local value = stats.first_launch or 0 - table.insert(stats_data, { - name = "first-launch", - value = time_formats.caption_hours(value), - tooltip = time_formats.tooltip_hours(value), - }) - end - - -- Format the last launch data - if config.stats.show_last_rocket then - local value = stats.last_launch or 0 - table.insert(stats_data, { - name = "last-launch", - value = time_formats.caption_hours(value), - tooltip = time_formats.tooltip_hours(value), - }) - end - - -- Format fastest launch data - if config.stats.show_fastest_rocket then - local value = stats.fastest_launch or 0 - table.insert(stats_data, { - name = "fastest-launch", - value = time_formats.caption_hours(value), - tooltip = time_formats.tooltip_hours(value), - }) - end - - -- Format total rocket data - if config.stats.show_total_rockets then - local total_rockets = Rockets.get_game_rocket_count() - total_rockets = total_rockets == 0 and 1 or total_rockets - local percentage = math.round(force_rockets / total_rockets, 3) * 100 - table.insert(stats_data, { - name = "total-rockets", - value = force_rockets, - tooltip = { "rocket-info.value-tooltip-total-rockets", percentage }, - }) - end - - -- Format game avg data - if config.stats.show_game_avg then - local avg = force_rockets > 0 and math.floor(game.tick / force_rockets) or 0 - table.insert(stats_data, { - name = "avg-launch", - value = time_formats.caption(avg), - tooltip = time_formats.tooltip(avg), - }) - end - - -- Format rolling avg data - for _, avg_over in pairs(config.stats.rolling_avg) do - local avg = Rockets.get_rolling_average(force_name, avg_over) - table.insert(stats_data, { - name = "avg-launch-n", - subname = avg_over, - value = time_formats.caption(avg), - tooltip = time_formats.tooltip(avg), - }) - end - - -- Return formated data - return stats_data -end - ---- Gets the label data for the milestones -local function get_milestone_data(force_name) - local force_rockets = Rockets.get_rocket_count(force_name) - local milestone_data = {} - - for _, milestone in ipairs(config.milestones) do - if milestone <= force_rockets then - local time = Rockets.get_rocket_time(force_name, milestone) - table.insert(milestone_data, { - name = "milestone-n", - subname = milestone, - value = time_formats.caption_hours(time), - tooltip = time_formats.tooltip_hours(time), - }) - else - table.insert(milestone_data, { - name = "milestone-n", - subname = milestone, - value = { "rocket-info.data-caption-milestone-next" }, - tooltip = { "rocket-info.data-tooltip-milestone-next" }, - }) - break - end - end - - return milestone_data -end - --- Button to toggle a section dropdown --- @element toggle_section -local toggle_section = Gui.define("rocket_info_toggle_section") - :draw{ - type = "sprite-button", - sprite = "utility/expand", - hovered_sprite = "utility/expand", - tooltip = { "rocket-info.toggle-section-tooltip" }, - style = "frame_action_button", - name = Gui.from_name, - } - :style(Gui.styles.sprite{ - size = 20, - }) - :on_click(function(def, player, element) - local header_flow = assert(element.parent) - local flow_name = header_flow.caption - local flow = header_flow.parent.parent[flow_name] - if Gui.toggle_visible_state(flow) then - element.sprite = "utility/collapse" - element.tooltip = { "rocket-info.toggle-section-collapse-tooltip" } - else - element.sprite = "utility/expand" - element.tooltip = { "rocket-info.toggle-section-tooltip" } - end - end) - --- Draw a section header and main scroll --- @element rocket_list_container -local section = Gui.define("rocket_info_section") - :draw(function(definition, parent, section_name, table_size) - -- Draw the header for the section - local header = Gui.elements.header(parent, { - name = section_name .. "-header", - caption = { "rocket-info.section-caption-" .. section_name }, - tooltip = { "rocket-info.section-tooltip-" .. section_name }, - }) - - definition:link_element(header.label) - - -- Right aligned button to toggle the section - header.caption = section_name - toggle_section(header) - - -- Table used to store the data - local scroll_table = Gui.elements.scroll_table(parent, 215, table_size, section_name) - scroll_table.parent.visible = false - - -- Return the flow table - return definition:unlink_element(scroll_table) - end) - :on_click(function(def, player, element, event) - event.element = element.parent.flow[toggle_section.name] - toggle_section:raise_event(event) - end) - ---- Main gui container for the left flow --- @element rocket_list_container -local rocket_list_container = Gui.define("rocket_list_container") - :draw(function(definition, parent) - -- Draw the internal container - local container = Gui.elements.container(parent, 200) - - -- Set the container style - local style = container.style - style.padding = 0 - - local player = Gui.get_player(parent) - local force_name = player.force.name - -- Draw stats section - if config.stats.show_stats then - update_data_labels(section(container, "stats", 2), get_stats_data(force_name)) - end - - -- Draw milestones section - if config.milestones.show_milestones then - update_data_labels(section(container, "milestones", 2), get_milestone_data(force_name)) - end - - -- Draw build progress list - if config.progress.show_progress then - local col_count = 3 - if check_player_permissions(player, "toggle_active") then col_count = col_count + 1 end - local progress = section(container, "progress", col_count) - -- Label used when there are no active silos - local no_silos = progress.parent.add{ - type = "label", - name = "no_silos", - caption = { "rocket-info.progress-no-silos" }, - } - no_silos.style.padding = { 1, 2 } - update_build_progress(progress, get_progress_data(force_name)) - end - - -- Return the external container - return container.parent - end) - ---- Add the element to the left flow with a toolbar button -Gui.add_left_element(rocket_list_container, function(player, element) - return player.force.rockets_launched > 0 and Roles.player_allowed(player, "gui/rocket-info") -end) -Gui.toolbar.create_button{ - name = "rocket_list_toggle", - left_element = rocket_list_container, - sprite = "item/rocket-silo", - tooltip = { "rocket-info.main-tooltip" }, - visible = function(player, element) - return Roles.player_allowed(player, "gui/rocket-info") - end -} - ---- Update the gui for all players on a force -local function update_rocket_gui_all(force_name) - local stats = get_stats_data(force_name) - local milestones = get_milestone_data(force_name) - local progress = get_progress_data(force_name) - for _, player in pairs(game.forces[force_name].players) do - local container = Gui.get_left_element(rocket_list_container, player) - local frame = container.frame - update_data_labels(frame.stats.table, stats) - update_data_labels(frame.milestones.table, milestones) - update_build_progress(frame.progress.table, progress) - end -end - ---- Event used to update the stats when a rocket is launched ---- @param event EventData.on_cargo_pod_finished_ascending -Event.add(defines.events.on_cargo_pod_finished_ascending, function(event) - update_rocket_gui_all(event.cargo_pod.force.name) -end) - ---- Update only the progress gui for a force -local function update_rocket_gui_progress(force_name) - local progress = get_progress_data(force_name) - for _, player in pairs(game.forces[force_name].connected_players) do - local container = Gui.get_left_element(rocket_list_container, player) - local frame = container.frame - update_build_progress(frame.progress.table, progress) - end -end - ---- Event used to set a rocket silo to be awaiting reset ---- @param event EventData.on_rocket_launch_ordered -Event.add(defines.events.on_rocket_launch_ordered, function(event) - update_rocket_gui_progress(event.rocket_silo.force.name) -end) - -Event.on_nth_tick(150, function() - for _, force in pairs(game.forces) do - if #Rockets.get_silos(force.name) > 0 then - update_rocket_gui_progress(force.name) - end - end -end) - ---- Adds a silo to the list when it is built ---- @param event EventData.on_built_entity | EventData.on_robot_built_entity | EventData.script_raised_built | EventData.script_raised_revive -local function on_built(event) - local entity = event.entity - if entity.valid and entity.name == "rocket-silo" then - update_rocket_gui_progress(entity.force.name) - end -end - -Event.add(defines.events.on_built_entity, on_built) -Event.add(defines.events.on_robot_built_entity, on_built) -Event.add(defines.events.script_raised_built, on_built) -Event.add(defines.events.script_raised_revive, on_built) - ---- Redraw the progress section on role change -local function role_update_event(event) - if not config.progress.show_progress then return end - local player = game.players[event.player_index] - local container = Gui.get_left_element(rocket_list_container, player) - local progress_scroll = container.frame.progress - Gui.destroy_if_valid(progress_scroll.table) - - local col_count = 3 - if check_player_permissions(player, "toggle_active") then col_count = col_count + 1 end - local progress = progress_scroll.add{ - type = "table", - name = "table", - column_count = col_count, - } - - update_build_progress(progress, get_progress_data(player.force.name)) -end - -Event.add(Roles.events.on_role_assigned, role_update_event) -Event.add(Roles.events.on_role_unassigned, role_update_event) - -return rocket_list_container From 05b586fb8cfdc059428c4867667cacec559b945c Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:35:44 +0100 Subject: [PATCH 5/6] Align names and call patterns to best practice --- exp_scenario/module/gui/rocket_info.lua | 712 +++++++++++++----------- 1 file changed, 401 insertions(+), 311 deletions(-) diff --git a/exp_scenario/module/gui/rocket_info.lua b/exp_scenario/module/gui/rocket_info.lua index 10774ae4..53b5ea5e 100644 --- a/exp_scenario/module/gui/rocket_info.lua +++ b/exp_scenario/module/gui/rocket_info.lua @@ -1,6 +1,5 @@ --[[-- Gui - Rocket Info Adds a rocket information gui which shows general stats, milestones and build progress of rockets. -The auto launch and remote launch controls were removed because the api no longer exposes auto launch in space age. ]] local ExpUtil = require("modules/exp_util") @@ -33,34 +32,6 @@ for _, avg_over in pairs(config.stats.rolling_avg) do end end ---[[ -The per force rocket data is stored on the container element data using the force scope, this means it -persists with the rest of the gui data and is cleaned up when forces are merged. It holds the launch -stats, the tick each rocket was launched on, and the silos that belong to the force. -]] - ---- @class ExpGui_RocketInfo.silo_data ---- @field entity LuaEntity The rocket silo entity ---- @field launched number The number of rockets launched from this silo ---- @field awaiting_reset boolean True when a launch is ordered but the silo has not reset yet - ---- @class ExpGui_RocketInfo.force_data ---- @field stats { first_launch: number?, last_launch: number?, fastest_launch: number? } Launch stats for the force ---- @field times table Launch tick indexed by rocket number ---- @field silos table Silo data indexed by unit number - ---- Get the rocket data for a force, creating it if it does not exist ---- @param force LuaForce ---- @return ExpGui_RocketInfo.force_data -local function get_force_data(force) - local data = Elements.container.data[force] - if not data then - data = { stats = {}, times = {}, silos = {} } - Elements.container.data[force] = data - end - return data -end - --- Get the total number of rockets launched by all forces --- @return number local function get_game_rocket_count() @@ -80,7 +51,7 @@ local function get_rolling_average(force, count) local rocket_count = force.rockets_launched if rocket_count == 0 then return 0 end - local times = get_force_data(force).times + local times = Elements.container.get_launch_times(force) local last_launch_time = times[rocket_count] or 0 local start_rocket_time = 0 if count < rocket_count then @@ -91,125 +62,6 @@ local function get_rolling_average(force, count) return math.floor((last_launch_time - start_rocket_time) / rocket_count) end ---- @class ExpGui_RocketInfo.stat_row ---- @field key string Unique key used to look up the value label ---- @field name string Data name used to select the locale keys ---- @field subname number? Optional subname passed as a locale parameter ---- @field value LocalisedString ---- @field tooltip LocalisedString? - ---- Compute the ordered stat rows for a force ---- @param force LuaForce ---- @return ExpGui_RocketInfo.stat_row[] -local function compute_stat_rows(force) - local stats = get_force_data(force).stats - local force_rockets = force.rockets_launched - local rows = {} - - if config.stats.show_first_rocket then - local value = stats.first_launch or 0 - rows[#rows + 1] = { key = "first-launch", name = "first-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } - end - - if config.stats.show_last_rocket then - local value = stats.last_launch or 0 - rows[#rows + 1] = { key = "last-launch", name = "last-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } - end - - if config.stats.show_fastest_rocket then - local value = stats.fastest_launch or 0 - rows[#rows + 1] = { key = "fastest-launch", name = "fastest-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } - end - - if config.stats.show_total_rockets then - local total_rockets = get_game_rocket_count() - if total_rockets == 0 then total_rockets = 1 end - local percentage = math.floor(force_rockets / total_rockets * 1000) / 10 - rows[#rows + 1] = { key = "total-rockets", name = "total-rockets", value = tostring(force_rockets), tooltip = { "exp-gui_rocket-info.value-tooltip-total-rockets", percentage } } - end - - if config.stats.show_game_avg then - local avg = force_rockets > 0 and math.floor(game.tick / force_rockets) or 0 - rows[#rows + 1] = { key = "avg-launch", name = "avg-launch", value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) } - end - - for _, avg_over in pairs(config.stats.rolling_avg) do - local avg = get_rolling_average(force, avg_over) - rows[#rows + 1] = { key = "avg-launch-n-" .. avg_over, name = "avg-launch-n", subname = avg_over, value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) } - end - - return rows -end - ---- @class ExpGui_RocketInfo.milestone_row ---- @field milestone number The milestone this row represents ---- @field value LocalisedString ---- @field tooltip LocalisedString - ---- Compute the ordered milestone rows for a force, up to and including the next unachieved milestone ---- @param force LuaForce ---- @return ExpGui_RocketInfo.milestone_row[] -local function compute_milestone_rows(force) - local times = get_force_data(force).times - local force_rockets = force.rockets_launched - local rows = {} - - for _, milestone in ipairs(config.milestones) do - -- The milestones config mixes the show_milestones flag with the milestone numbers - if type(milestone) == "number" then - if milestone <= force_rockets then - local time = times[milestone] or 0 - rows[#rows + 1] = { milestone = milestone, value = time_formats.caption_hours(time), tooltip = time_formats.tooltip_hours(time) } - else - rows[#rows + 1] = { milestone = milestone, value = { "exp-gui_rocket-info.data-caption-milestone-next" }, tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-next" } } - break - end - end - end - - return rows -end - ---- @class ExpGui_RocketInfo.progress_row ---- @field x LocalisedString ---- @field y LocalisedString ---- @field caption LocalisedString ---- @field tooltip LocalisedString ---- @field color Color - ---- Compute the progress row data for a silo, clears awaiting_reset once the silo is no longer waiting ---- @param silo_data ExpGui_RocketInfo.silo_data ---- @return ExpGui_RocketInfo.progress_row -local function compute_progress_row(silo_data) - local entity = silo_data.entity - local position = entity.position - local waiting = entity.status == defines.entity_status.waiting_to_launch_rocket - - local caption = { "exp-gui_rocket-info.progress-caption", entity.rocket_parts } - local color = font_color.neutral - if waiting and silo_data.awaiting_reset then - caption = { "exp-gui_rocket-info.progress-launched" } - color = font_color.launched - elseif waiting then - caption = { "exp-gui_rocket-info.progress-caption", 100 } - color = font_color.waiting - else - silo_data.awaiting_reset = false - end - - return { - x = { "exp-gui_rocket-info.progress-x-pos", position.x }, - y = { "exp-gui_rocket-info.progress-y-pos", position.y }, - caption = caption, - tooltip = { "exp-gui_rocket-info.progress-tooltip", silo_data.launched }, - color = color, - } -end - ---[[ -Below here is the gui, it is split into three collapsible sections; stats, milestones, and build progress. -]] - --- Toggle the visible state of a section, the section is stored in the element data --- @class ExpGui_RocketInfo.elements.toggle_section_button: ExpElement --- @field data table @@ -270,102 +122,215 @@ Elements.position_label = Gui.define("rocket_info/position_label") --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.stats_table = Gui.define("rocket_info/stats_table") :track_all_elements() + :element_data{} :draw(function(def, parent) - --- @cast def ExpGui_RocketInfo.elements.stats_table - local data_table = Gui.elements.scroll_table(parent, 215, 2) - def.data[data_table] = {} - return data_table + return Gui.elements.scroll_table(parent, 215, 2) end) --[[ @as any ]] +--- @class ExpGui_RocketInfo.elements.stats_table.row_data +--- @field key string Unique key used to look up the value label +--- @field name string Data name used to select the locale keys +--- @field subname number? Optional subname passed as a locale parameter +--- @field value LocalisedString +--- @field tooltip LocalisedString? + +--- Compute the ordered stat rows for a force +--- @param force LuaForce +--- @return ExpGui_RocketInfo.elements.stats_table.row_data[] +function Elements.stats_table.calculate_row_data(force) + local stats = Elements.container.get_stats(force) + local force_rockets = force.rockets_launched + local rows = {} + + if config.stats.show_first_rocket then + local value = stats.first_launch or 0 + rows[#rows + 1] = { key = "first-launch", name = "first-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } + end + + if config.stats.show_last_rocket then + local value = stats.last_launch or 0 + rows[#rows + 1] = { key = "last-launch", name = "last-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } + end + + if config.stats.show_fastest_rocket then + local value = stats.fastest_launch or 0 + rows[#rows + 1] = { key = "fastest-launch", name = "fastest-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) } + end + + if config.stats.show_total_rockets then + local total_rockets = get_game_rocket_count() + if total_rockets == 0 then total_rockets = 1 end + local percentage = math.floor(force_rockets / total_rockets * 1000) / 10 + rows[#rows + 1] = { key = "total-rockets", name = "total-rockets", value = tostring(force_rockets), tooltip = { "exp-gui_rocket-info.value-tooltip-total-rockets", percentage } } + end + + if config.stats.show_game_avg then + local avg = force_rockets > 0 and math.floor(game.tick / force_rockets) or 0 + rows[#rows + 1] = { key = "avg-launch", name = "avg-launch", value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) } + end + + for _, avg_over in pairs(config.stats.rolling_avg) do + local avg = get_rolling_average(force, avg_over) + rows[#rows + 1] = { key = "avg-launch-n-" .. avg_over, name = "avg-launch-n", subname = avg_over, value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) } + end + + return rows +end + --- Add a stat row to the table and store its value label ---- @param data_table LuaGuiElement ---- @param row ExpGui_RocketInfo.stat_row -function Elements.stats_table.add_row(data_table, row) - local labels = Elements.stats_table.data[data_table] - local name_label = data_table.add{ +--- @param stats_table LuaGuiElement +--- @param row_data ExpGui_RocketInfo.elements.stats_table.row_data +function Elements.stats_table.add_row(stats_table, row_data) + local labels = Elements.stats_table.data[stats_table] + local name_label = stats_table.add{ type = "label", - caption = { "exp-gui_rocket-info.data-caption-" .. row.name, row.subname }, - tooltip = { "exp-gui_rocket-info.data-tooltip-" .. row.name, row.subname }, + caption = { "exp-gui_rocket-info.data-caption-" .. row_data.name, row_data.subname }, + tooltip = { "exp-gui_rocket-info.data-tooltip-" .. row_data.name, row_data.subname }, } name_label.style.padding = { 0, 2 } - local value_label = data_table.add{ + local value_label = stats_table.add{ type = "label", - caption = row.value, - tooltip = row.tooltip, + caption = row_data.value, + tooltip = row_data.tooltip, } value_label.style.padding = { 0, 2 } - labels[row.key] = value_label + labels[row_data.key] = value_label end ---- Refresh the stats table for a force, adding any rows that do not yet exist ---- @param data_table LuaGuiElement ---- @param force LuaForce -function Elements.stats_table.refresh(data_table, force) - local labels = Elements.stats_table.data[data_table] - for _, row in ipairs(compute_stat_rows(force)) do +--- Refresh the stats table, adding any rows that do not yet exist +--- @param stats_table LuaGuiElement +--- @param row_data ExpGui_RocketInfo.elements.stats_table.row_data[] +function Elements.stats_table.refresh(stats_table, row_data) + local labels = Elements.stats_table.data[stats_table] + for _, row in ipairs(row_data) do local value_label = labels[row.key] if value_label then value_label.caption = row.value value_label.tooltip = row.tooltip else - Elements.stats_table.add_row(data_table, row) + Elements.stats_table.add_row(stats_table, row) end end end +--- Refresh the stats table for a player, adding any rows that do not yet exist +--- @param player LuaPlayer +function Elements.stats_table.refresh_player(player) + local force = player.force --[[ @as LuaForce ]] + local row_data = Elements.stats_table.calculate_row_data(force) + for _, stats_table in Elements.stats_table:online_elements(player) do + Elements.stats_table.refresh(stats_table, row_data) + end +end + +--- Refresh the stats table for a force, adding any rows that do not yet exist +--- @param force LuaForce +function Elements.stats_table.refresh_force(force) + local row_data = Elements.stats_table.calculate_row_data(force) + for _, stats_table in Elements.stats_table:online_elements(force) do + Elements.stats_table.refresh(stats_table, row_data) + end +end + --- Data table showing the milestones for a force --- @class ExpGui_RocketInfo.elements.milestones_table: ExpElement --- @field data table> --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.milestones_table = Gui.define("rocket_info/milestones_table") :track_all_elements() + :element_data{} :draw(function(def, parent) - --- @cast def ExpGui_RocketInfo.elements.milestones_table - local data_table = Gui.elements.scroll_table(parent, 215, 2) - def.data[data_table] = {} - return data_table + return Gui.elements.scroll_table(parent, 215, 2) end) --[[ @as any ]] +--- @class ExpGui_RocketInfo.elements.milestones_table.row_data +--- @field milestone number The milestone this row represents +--- @field value LocalisedString +--- @field tooltip LocalisedString + +--- Compute the ordered milestone rows for a force, up to and including the next unachieved milestone +--- @param force LuaForce +--- @return ExpGui_RocketInfo.elements.milestones_table.row_data[] +function Elements.milestones_table.calculate_row_data(force) + local times = Elements.container.get_launch_times(force) + local force_rockets = force.rockets_launched + local rows = {} + + for _, milestone in ipairs(config.milestones) do + -- The milestones config mixes the show_milestones flag with the milestone numbers + if type(milestone) == "number" then + if milestone <= force_rockets then + local time = times[milestone] or 0 + rows[#rows + 1] = { milestone = milestone, value = time_formats.caption_hours(time), tooltip = time_formats.tooltip_hours(time) } + else + rows[#rows + 1] = { milestone = milestone, value = { "exp-gui_rocket-info.data-caption-milestone-next" }, tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-next" } } + break + end + end + end + + return rows +end + --- Add a milestone row to the table and store its value label ---- @param data_table LuaGuiElement ---- @param row ExpGui_RocketInfo.milestone_row -function Elements.milestones_table.add_row(data_table, row) - local labels = Elements.milestones_table.data[data_table] - local name_label = data_table.add{ +--- @param milestones_table LuaGuiElement +--- @param row_data ExpGui_RocketInfo.elements.milestones_table.row_data +function Elements.milestones_table.add_row(milestones_table, row_data) + local labels = Elements.milestones_table.data[milestones_table] + local name_label = milestones_table.add{ type = "label", - caption = { "exp-gui_rocket-info.data-caption-milestone-n", row.milestone }, - tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-n", row.milestone }, + caption = { "exp-gui_rocket-info.data-caption-milestone-n", row_data.milestone }, + tooltip = { "exp-gui_rocket-info.data-tooltip-milestone-n", row_data.milestone }, } name_label.style.padding = { 0, 2 } - local value_label = data_table.add{ + local value_label = milestones_table.add{ type = "label", - caption = row.value, - tooltip = row.tooltip, + caption = row_data.value, + tooltip = row_data.tooltip, } value_label.style.padding = { 0, 2 } - labels[row.milestone] = value_label + labels[row_data.milestone] = value_label end ---- Refresh the milestones table for a force, adding rows as new milestones become visible ---- @param data_table LuaGuiElement ---- @param force LuaForce -function Elements.milestones_table.refresh(data_table, force) - local labels = Elements.milestones_table.data[data_table] - for _, row in ipairs(compute_milestone_rows(force)) do +--- Refresh the milestones table, adding rows as new milestones become visible +--- @param milestones_table LuaGuiElement +--- @param row_data ExpGui_RocketInfo.elements.milestones_table.row_data[] +function Elements.milestones_table.refresh(milestones_table, row_data) + local labels = Elements.milestones_table.data[milestones_table] + for _, row in ipairs(row_data) do local value_label = labels[row.milestone] if value_label then value_label.caption = row.value value_label.tooltip = row.tooltip else - Elements.milestones_table.add_row(data_table, row) + Elements.milestones_table.add_row(milestones_table, row) end end end +--- Refresh the milestones table for a player, adding rows as new milestones become visible +--- @param player LuaPlayer +function Elements.milestones_table.refresh_player(player) + local force = player.force --[[ @as LuaForce ]] + local row_data = Elements.stats_table.calculate_row_data(force) + for _, stats_table in Elements.stats_table:online_elements(player) do + Elements.stats_table.refresh(stats_table, row_data) + end +end + +--- Refresh the milestones table for a force, adding rows as new milestones become visible +--- @param force LuaForce +function Elements.milestones_table.refresh_force(force) + local row_data = Elements.stats_table.calculate_row_data(force) + for _, stats_table in Elements.stats_table:online_elements(force) do + Elements.stats_table.refresh(stats_table, row_data) + end +end + --- @class ExpGui_RocketInfo.elements.progress_table.row --- @field x LuaGuiElement --- @field y LuaGuiElement @@ -383,31 +348,84 @@ Elements.progress_table = Gui.define("rocket_info/progress_table") :track_all_elements() :draw(function(def, parent) --- @cast def ExpGui_RocketInfo.elements.progress_table - local data_table = Gui.elements.scroll_table(parent, 215, 3) + local progress_table = Gui.elements.scroll_table(parent, 215, 3) -- The no silos label lives next to the table inside the same collapsible scroll pane - local no_silos = assert(data_table.parent).add{ + local no_silos = assert(progress_table.parent).add{ type = "label", caption = { "exp-gui_rocket-info.progress-no-silos" }, } no_silos.style.padding = { 1, 2 } - def.data[data_table] = { rows = {}, no_silos = no_silos } - return data_table + def.data[progress_table] = { rows = {}, no_silos = no_silos } + return progress_table end) --[[ @as any ]] ---- Add a silo row to the progress table and store its labels ---- @param data_table LuaGuiElement ---- @param silo_data ExpGui_RocketInfo.silo_data -function Elements.progress_table.add_row(data_table, silo_data) - local rows = Elements.progress_table.data[data_table].rows +--- @class ExpGui_RocketInfo.elements.progress_table.row_data +--- @field entity LuaEntity +--- @field x LocalisedString +--- @field y LocalisedString +--- @field caption LocalisedString +--- @field tooltip LocalisedString +--- @field color Color + +--- Compute the progress row data for a silo, clears awaiting_reset once the silo is no longer waiting +--- @param silo_data ExpGui_RocketInfo.elements.container.silo_data +--- @return ExpGui_RocketInfo.elements.progress_table.row_data +function Elements.progress_table.calculate_row_data(silo_data) local entity = silo_data.entity - local row_data = compute_progress_row(silo_data) + local position = entity.position + local waiting = entity.status == defines.entity_status.waiting_to_launch_rocket + + local caption = { "exp-gui_rocket-info.progress-caption", entity.rocket_parts } + local color = font_color.neutral + if waiting and silo_data.awaiting_reset then + caption = { "exp-gui_rocket-info.progress-launched" } + color = font_color.launched + elseif waiting then + caption = { "exp-gui_rocket-info.progress-caption", 100 } + color = font_color.waiting + else + silo_data.awaiting_reset = false + end + + return { + entity = entity, + x = { "exp-gui_rocket-info.progress-x-pos", position.x }, + y = { "exp-gui_rocket-info.progress-y-pos", position.y }, + caption = caption, + tooltip = { "exp-gui_rocket-info.progress-tooltip", silo_data.launched }, + color = color, + } +end + +--- Calculate the row data for all rows, pruning the silo data as required +--- @param silo_data table +--- @return ExpGui_RocketInfo.elements.progress_table.row_data[] +function Elements.progress_table.calculate_row_data_all(silo_data) + local row_data = {} + for unit_number, silo_data in pairs(silo_data) do + if silo_data.entity.valid then + row_data[unit_number] = Elements.progress_table.calculate_row_data(silo_data) + else + -- Prune silos that are no longer valid + silo_data[unit_number] = nil + end + end + + return row_data +end + +--- Add a silo row to the progress table and store its labels +--- @param progress_table LuaGuiElement +--- @param row_data ExpGui_RocketInfo.elements.progress_table.row_data +function Elements.progress_table.add_row(progress_table, row_data) + local rows = Elements.progress_table.data[progress_table].rows local zoom_tooltip = config.progress.allow_zoom_to_map and { "exp-gui_rocket-info.progress-label-tooltip" } or nil - local x = Elements.position_label(data_table, { caption = row_data.x, tooltip = zoom_tooltip, entity = entity }) - local y = Elements.position_label(data_table, { caption = row_data.y, tooltip = zoom_tooltip, entity = entity }) - local progress = data_table.add{ + local x = Elements.position_label(progress_table, { caption = row_data.x, tooltip = zoom_tooltip, entity = row_data.entity }) + local y = Elements.position_label(progress_table, { caption = row_data.y, tooltip = zoom_tooltip, entity = row_data.entity }) + local progress = progress_table.add{ type = "label", caption = row_data.caption, tooltip = row_data.tooltip, @@ -415,15 +433,29 @@ function Elements.progress_table.add_row(data_table, silo_data) progress.style.padding = { 0, 2 } progress.style.font_color = row_data.color - rows[entity.unit_number] = { x = x, y = y, progress = progress } + rows[row_data.entity.unit_number] = { x = x, y = y, progress = progress } +end + +--- Remove a silo row from the progress table +--- @param progress_table LuaGuiElement +--- @param unit_number number +function Elements.progress_table.remove_row(progress_table, unit_number) + local element_data = Elements.progress_table.data[progress_table] + local row = element_data.rows[unit_number] + + if not row then return end + element_data.rows[unit_number] = nil + Gui.destroy_if_valid(row.x) + Gui.destroy_if_valid(row.y) + Gui.destroy_if_valid(row.progress) end --- Update an existing silo row to match the latest data ---- @param data_table LuaGuiElement ---- @param silo_data ExpGui_RocketInfo.silo_data -function Elements.progress_table.refresh_row(data_table, silo_data) - local row = Elements.progress_table.data[data_table].rows[silo_data.entity.unit_number] - local row_data = compute_progress_row(silo_data) +--- @param progress_table LuaGuiElement +--- @param row_data ExpGui_RocketInfo.elements.progress_table.row_data +function Elements.progress_table.refresh_row(progress_table, row_data) + local element_data = Elements.progress_table.data[progress_table] + local row = element_data.rows[row_data.entity.unit_number] row.x.caption = row_data.x row.y.caption = row_data.y row.progress.caption = row_data.caption @@ -431,61 +463,109 @@ function Elements.progress_table.refresh_row(data_table, silo_data) row.progress.style.font_color = row_data.color end ---- Remove a silo row from the progress table ---- @param data_table LuaGuiElement ---- @param unit_number number -function Elements.progress_table.remove_row(data_table, unit_number) - local rows = Elements.progress_table.data[data_table].rows - local row = rows[unit_number] - if not row then return end - rows[unit_number] = nil - Gui.destroy_if_valid(row.x) - Gui.destroy_if_valid(row.y) - Gui.destroy_if_valid(row.progress) -end - ---- Refresh the progress table for a force, reconciling rows with the current set of silos ---- @param data_table LuaGuiElement ---- @param force LuaForce -function Elements.progress_table.refresh(data_table, force) - local silos = get_force_data(force).silos - local data = Elements.progress_table.data[data_table] - local rows = data.rows - local seen = {} +--- Refresh the progress table, reconciling rows with the current set of silos +--- @param progress_table LuaGuiElement +--- @param row_data ExpGui_RocketInfo.elements.progress_table.row_data[] +function Elements.progress_table.refresh(progress_table, row_data) + local element_data = Elements.progress_table.data[progress_table] + local rows = element_data.rows local has_silos = false + local seen = {} - for unit_number, silo_data in pairs(silos) do - if not silo_data.entity.valid then - -- Prune silos that are no longer valid - silos[unit_number] = nil + for _, row in pairs(row_data) do + has_silos = true + seen[row.entity.unit_number] = true + if rows[row.entity.unit_number] then + Elements.progress_table.refresh_row(progress_table, row) else - has_silos = true - seen[unit_number] = true - if rows[unit_number] then - Elements.progress_table.refresh_row(data_table, silo_data) - else - Elements.progress_table.add_row(data_table, silo_data) - end + Elements.progress_table.add_row(progress_table, row) end end -- Remove rows for silos that are no longer present for unit_number in pairs(rows) do if not seen[unit_number] then - Elements.progress_table.remove_row(data_table, unit_number) + Elements.progress_table.remove_row(progress_table, unit_number) end end - data.no_silos.visible = not has_silos - data_table.visible = has_silos + element_data.no_silos.visible = not has_silos + progress_table.visible = has_silos end +--- Refresh the progress table for a player, reconciling rows with the current set of silos +--- @param player LuaPlayer +function Elements.progress_table.refresh_player(player) + local force = player.force --[[ @as LuaForce ]] + local silos = Elements.container.get_silos(force) + local row_data = Elements.progress_table.calculate_row_data_all(silos) + for _, progress_table in Elements.progress_table:online_elements(player) do + Elements.progress_table.refresh(progress_table, row_data) + end +end + +--- Refresh the progress table for a force, reconciling rows with the current set of silos +--- @param force LuaForce +function Elements.progress_table.refresh_force(force) + local silos = Elements.container.get_silos(force) + local row_data = Elements.progress_table.calculate_row_data_all(silos) + for _, progress_table in Elements.progress_table:online_elements(force) do + Elements.progress_table.refresh(progress_table, row_data) + end +end + +--- @class ExpGui_RocketInfo.elements.container.silo_data +--- @field entity LuaEntity The rocket silo entity +--- @field launched number The number of rockets launched from this silo +--- @field awaiting_reset boolean True when a launch is ordered but the silo has not reset yet + +--- @class ExpGui_RocketInfo.elements.container.force_stats +--- @field first_launch number? The tick the first rocket was launched +--- @field last_launch number? The tick the last rocket was launched +--- @field fastest_launch number? The tick duration between the two closest launches + +--- @class ExpGui_RocketInfo.elements.container.force_data +--- @field stats ExpGui_RocketInfo.elements.container.force_stats Launch stats for the force +--- @field times table Launch tick indexed by rocket number +--- @field silos table Silo data indexed by unit number + +--- Container added to the left gui flow +--- @class ExpGui_RocketInfo.elements.container: ExpElement +--- @field data table +Elements.container = Gui.define("rocket_info/container") + :draw(function(def, parent) + --- @cast def ExpGui_RocketInfo.elements.container + local container = Gui.elements.container(parent, 200) + container.style.padding = 0 + + local player = Gui.get_player(parent) + local force = player.force --[[ @as LuaForce ]] + local force_data = def._get_force_data(force) + + if config.stats.show_stats then + local row_data = Elements.stats_table.calculate_row_data(force) + Elements.stats_table.refresh(def.add_section(container, "stats", Elements.stats_table), row_data) + end + + if config.milestones.show_milestones then + local row_data = Elements.milestones_table.calculate_row_data(force) + Elements.milestones_table.refresh(def.add_section(container, "milestones", Elements.milestones_table), row_data) + end + + if config.progress.show_progress then + local row_data = Elements.progress_table.calculate_row_data_all(force_data.silos) + Elements.progress_table.refresh(def.add_section(container, "progress", Elements.progress_table), row_data) + end + + return Gui.elements.container.get_root_element(container) + end) + --- Add a collapsible section to a container and return its data table --- @param container LuaGuiElement The container frame to add the section to --- @param section_name string Used to select the locale keys for the header --- @param table_define any The data table element define to add --- @return LuaGuiElement -local function add_section(container, section_name, table_define) +function Elements.container.add_section(container, section_name, table_define) local header = Gui.elements.header(container, { caption = { "exp-gui_rocket-info.section-caption-" .. section_name }, tooltip = { "exp-gui_rocket-info.section-tooltip-" .. section_name }, @@ -501,30 +581,62 @@ local function add_section(container, section_name, table_define) return data_table end ---- Container added to the left gui flow ---- @class ExpGui_RocketInfo.elements.container: ExpElement ---- @field data table -Elements.container = Gui.define("rocket_info/container") - :draw(function(def, parent) - local container = Gui.elements.container(parent, 200) - container.style.padding = 0 +--- Get or create the data for a force +--- @param force LuaForce +--- @return ExpGui_RocketInfo.elements.container.force_data +function Elements.container._get_force_data(force) + local force_data = Elements.container.data[force] + if not force_data then + force_data = { stats = {}, times = {}, silos = {} } + Elements.container.data[force] = force_data + end - local force = Gui.get_player(parent).force --[[ @as LuaForce ]] + return force_data +end - if config.stats.show_stats then - Elements.stats_table.refresh(add_section(container, "stats", Elements.stats_table), force) - end +--- Gets the stats for a force +--- @param force LuaForce +--- @return { first_launch: number?, last_launch: number?, fastest_launch: number? } +function Elements.container.get_stats(force) + return Elements.container._get_force_data(force).stats +end - if config.milestones.show_milestones then - Elements.milestones_table.refresh(add_section(container, "milestones", Elements.milestones_table), force) - end +--- Gets the list of rocket launch times +--- @param force LuaForce +--- @return table +function Elements.container.get_launch_times(force) + return Elements.container._get_force_data(force).times +end - if config.progress.show_progress then - Elements.progress_table.refresh(add_section(container, "progress", Elements.progress_table), force) - end +--- Gets the list of current silos for a force +--- @param force LuaForce +--- @return table +function Elements.container.get_silos(force) + return Elements.container._get_force_data(force).silos +end - return Gui.elements.container.get_root_element(container) - end) +--- Add a silo to the list of current silos for a force +--- @param entity LuaEntity +function Elements.container.add_silo(entity) + local force = entity.force --[[ @as LuaForce ]] + local silos = Elements.container._get_force_data(force).silos + silos[entity.unit_number] = { + entity = entity, + launched = 0, + awaiting_reset = false, + } +end + +--- Increment the launch count for a silo +--- @param entity LuaEntity +function Elements.container.increment_silo(entity) + local force = entity.force --[[ @as LuaForce ]] + local silos = Elements.container._get_force_data(force).silos + local silo_data = silos[entity.unit_number] + if not silo_data then return end + silo_data.launched = silo_data.launched + 1 + silo_data.awaiting_reset = true +end --- Add the element to the left flow with a toolbar button Gui.add_left_element(Elements.container, false) @@ -538,38 +650,14 @@ Gui.toolbar.create_button{ end } ---[[ -Below here is the event handling, the data tracking and gui refreshing are wired up to the same events. -]] - ---- Refresh the stats and milestones tables for all online players on a force ---- @param force LuaForce -local function refresh_force_stats(force) - for _, data_table in Elements.stats_table:online_elements(force) do - Elements.stats_table.refresh(data_table, force) - end - for _, data_table in Elements.milestones_table:online_elements(force) do - Elements.milestones_table.refresh(data_table, force) - end -end - ---- Refresh the progress table for all online players on a force ---- @param force LuaForce -local function refresh_force_progress(force) - for _, data_table in Elements.progress_table:online_elements(force) do - Elements.progress_table.refresh(data_table, force) - end -end - --- Record the launch and update the stats when a cargo pod finishes ascending --- @param event EventData.on_cargo_pod_finished_ascending local function on_cargo_pod_finished_ascending(event) local force = event.cargo_pod.force --[[ @as LuaForce ]] local rockets_launched = force.rockets_launched - local force_data = get_force_data(force) -- Update the launch stats for the force - local stats = force_data.stats + local stats = Elements.container.get_stats(force) if rockets_launched == 1 then stats.first_launch = event.tick stats.fastest_launch = event.tick @@ -580,27 +668,34 @@ local function on_cargo_pod_finished_ascending(event) stats.last_launch = event.tick -- Append the launch tick into the times array - force_data.times[rockets_launched] = event.tick + local times = Elements.container.get_launch_times(force) + times[rockets_launched] = event.tick -- Discard the launch time that is no longer needed by any rolling average unless it is a milestone local remove_rocket = rockets_launched - largest_rolling_avg if remove_rocket > 0 and not table.array_contains(config.milestones, remove_rocket) then - force_data.times[remove_rocket] = nil + times[remove_rocket] = nil end - refresh_force_stats(force) - refresh_force_progress(force) + Elements.stats_table.refresh_force(force) + Elements.milestones_table.refresh_force(force) + Elements.progress_table.refresh_force(force) end --- Mark a silo as awaiting reset when a launch is ordered --- @param event EventData.on_rocket_launch_ordered local function on_rocket_launch_ordered(event) - local silo = event.rocket_silo - local silo_data = get_force_data(silo.force --[[ @as LuaForce ]]).silos[silo.unit_number] - if not silo_data then return end - silo_data.launched = silo_data.launched + 1 - silo_data.awaiting_reset = true - refresh_force_progress(silo.force --[[ @as LuaForce ]]) + Elements.container.increment_silo(event.rocket_silo) + Elements.progress_table.refresh_force(event.rocket_silo.force --[[ @as LuaForce ]]) +end + +--- Refresh the gui when a player joins the game as it may be outdated +--- @param event EventData.on_player_joined_game +local function on_player_joined_game(event) + local player = Gui.get_player(event) + Elements.stats_table.refresh_player(player) + Elements.milestones_table.refresh_player(player) + Elements.progress_table.refresh_player(player) end --- Add a silo to the force data when it is built @@ -608,20 +703,14 @@ end local function on_built(event) local entity = event.entity if not entity.valid or entity.name ~= "rocket-silo" then return end - get_force_data(entity.force --[[ @as LuaForce ]]).silos[entity.unit_number] = { - entity = entity, - launched = 0, - awaiting_reset = false, - } - refresh_force_progress(entity.force --[[ @as LuaForce ]]) + Elements.container.add_silo(entity) + Elements.progress_table.refresh_force(entity.force --[[ @as LuaForce ]]) end --- Refresh the progress for all forces that own at least one silo local function refresh_all_progress() for _, force in pairs(game.forces) do - if next(get_force_data(force).silos) then - refresh_force_progress(force) - end + Elements.progress_table.refresh_force(force) end end @@ -632,6 +721,7 @@ return { events = { [e.on_cargo_pod_finished_ascending] = on_cargo_pod_finished_ascending, [e.on_rocket_launch_ordered] = on_rocket_launch_ordered, + [e.on_player_joined_game] = on_player_joined_game, [e.on_built_entity] = on_built, [e.on_robot_built_entity] = on_built, [e.script_raised_built] = on_built, From 05cb831b30a9c2588b9e2f64a8833097e5ffd6cb Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:43:26 +0100 Subject: [PATCH 6/6] Update Locales --- exp_legacy/module/locale/en/gui.cfg | 40 ------------------------- exp_legacy/module/locale/zh-CN/gui.cfg | 40 ------------------------- exp_legacy/module/locale/zh-TW/gui.cfg | 40 ------------------------- exp_scenario/module/gui/rocket_info.lua | 2 +- exp_scenario/module/locale/en.cfg | 2 +- exp_scenario/module/locale/zh-CN.cfg | 36 ++++++++++++++++++++++ exp_scenario/module/locale/zh-TW.cfg | 36 ++++++++++++++++++++++ 7 files changed, 74 insertions(+), 122 deletions(-) diff --git a/exp_legacy/module/locale/en/gui.cfg b/exp_legacy/module/locale/en/gui.cfg index 317e23fe..c83514fc 100644 --- a/exp_legacy/module/locale/en/gui.cfg +++ b/exp_legacy/module/locale/en/gui.cfg @@ -14,46 +14,6 @@ ban-player=Ban player afk-time=__1__% of total map time\nLast moved __2__ ago open-map=__1__ __2__\n__3__\nClick to open map -[rocket-info] -main-tooltip=Rocket Info -launch-tooltip=Launch rocket -launch-tooltip-disabled=Launch rocket (not ready) -toggle-rocket-tooltip=Disable auto launch -toggle-rocket-tooltip-disabled=Enable auto launch -toggle-section-tooltip=Expand Section -toggle-section-collapse-tooltip=Collapse Section -section-caption-stats=Statistics -section-tooltip-stats=Statistics about how rockets are launched -section-caption-milestones=Milestones -section-tooltip-milestones=The times when milestones were met -section-caption-progress=Build Progress -section-tooltip-progress=The progress for your rocket silos -progress-no-silos=You have no rocket silos -data-caption-first-launch=First Launch -data-tooltip-first-launch=The time of the first launch -data-caption-last-launch=Last Launch -data-tooltip-last-launch=The time of the last launch -data-caption-fastest-launch=Fastest Launch -data-tooltip-fastest-launch=The time taken for the fastest launch -data-caption-total-rockets=Total Launched -data-tooltip-total-rockets=Total number of rockets launched by your force -value-tooltip-total-rockets=__1__% of all rockets on this map -data-caption-avg-launch=Average Time -data-tooltip-avg-launch=The average amount of time taken to launch a rocket -data-caption-avg-launch-n=Average Time __1__ -data-tooltip-avg-launch-n=The average amount of time taken to launch the last __1__ rockets -data-caption-milestone-n=Milestone __1__ -data-tooltip-milestone-n=Time taken to each __1__ rockets -data-caption-milestone-next=N/A -data-tooltip-milestone-next=Not yet achieved -progress-x-pos=X __1__ -progress-y-pos=Y __1__ -progress-label-tooltip=View on map -progress-launched=Launched -progress-caption=__1__% -progress-tooltip=This silo has launched __1__ rockets -launch-failed=Failed to launch rocket, please wait a few seconds and try again. - [warp-list] main-caption=Warp List [img=info] main-tooltip=Warp List diff --git a/exp_legacy/module/locale/zh-CN/gui.cfg b/exp_legacy/module/locale/zh-CN/gui.cfg index 7e2f5954..fbadf609 100644 --- a/exp_legacy/module/locale/zh-CN/gui.cfg +++ b/exp_legacy/module/locale/zh-CN/gui.cfg @@ -14,46 +14,6 @@ ban-player=封禁用戶 afk-time=__1__% 地圖時間\n上次移動: __2__ open-map=__1__ __2__\n__3__\n按下打開地圖 -[rocket-info] -main-tooltip=火箭資訊 -launch-tooltip=發射火箭 -launch-tooltip-disabled=發射火箭 (未準備) -toggle-rocket-tooltip=取消自動發射 -toggle-rocket-tooltip-disabled=啟用自動發射 -toggle-section-tooltip=擴張欄 -toggle-section-collapse-tooltip=收縮欄 -section-caption-stats=數據 -section-tooltip-stats=火箭發射數據 -section-caption-milestones=里程碑 -section-tooltip-milestones=里程碑達成次數 -section-caption-progress=建造程度 -section-tooltip-progress=火箭建造程度 -progress-no-silos=你沒有火箭發射井 -data-caption-first-launch=首次發射 -data-tooltip-first-launch=首次發射時間 -data-caption-last-launch=最後發射 -data-tooltip-last-launch=最後發射時間 -data-caption-fastest-launch=最快發射 -data-tooltip-fastest-launch=最快發射時間 -data-caption-total-rockets=總發射次數 -data-tooltip-total-rockets=我方總發射次數 -value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。 -data-caption-avg-launch=平均時間 -data-tooltip-avg-launch=火箭發射平均時間 -data-caption-avg-launch-n=平均時間 __1__ -data-tooltip-avg-launch-n=__1__ 次火箭發射的平均時間 -data-caption-milestone-n=里程碑 __1__ -data-tooltip-milestone-n=__1__ 次火箭發射的時間 -data-caption-milestone-next=N/A -data-tooltip-milestone-next=未達成 -progress-x-pos=X __1__ -progress-y-pos=Y __1__ -progress-label-tooltip=在地圖上看 -progress-launched=已發射 -progress-caption=__1__ % -progress-tooltip=該火箭發射井發射了 __1__ 次 -launch-failed=火箭發射失敗, 請過一會再試。 - [warp-list] main-caption=傳送陣清單 [img=info] main-tooltip=傳送陣清單 diff --git a/exp_legacy/module/locale/zh-TW/gui.cfg b/exp_legacy/module/locale/zh-TW/gui.cfg index 7e2f5954..fbadf609 100644 --- a/exp_legacy/module/locale/zh-TW/gui.cfg +++ b/exp_legacy/module/locale/zh-TW/gui.cfg @@ -14,46 +14,6 @@ ban-player=封禁用戶 afk-time=__1__% 地圖時間\n上次移動: __2__ open-map=__1__ __2__\n__3__\n按下打開地圖 -[rocket-info] -main-tooltip=火箭資訊 -launch-tooltip=發射火箭 -launch-tooltip-disabled=發射火箭 (未準備) -toggle-rocket-tooltip=取消自動發射 -toggle-rocket-tooltip-disabled=啟用自動發射 -toggle-section-tooltip=擴張欄 -toggle-section-collapse-tooltip=收縮欄 -section-caption-stats=數據 -section-tooltip-stats=火箭發射數據 -section-caption-milestones=里程碑 -section-tooltip-milestones=里程碑達成次數 -section-caption-progress=建造程度 -section-tooltip-progress=火箭建造程度 -progress-no-silos=你沒有火箭發射井 -data-caption-first-launch=首次發射 -data-tooltip-first-launch=首次發射時間 -data-caption-last-launch=最後發射 -data-tooltip-last-launch=最後發射時間 -data-caption-fastest-launch=最快發射 -data-tooltip-fastest-launch=最快發射時間 -data-caption-total-rockets=總發射次數 -data-tooltip-total-rockets=我方總發射次數 -value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。 -data-caption-avg-launch=平均時間 -data-tooltip-avg-launch=火箭發射平均時間 -data-caption-avg-launch-n=平均時間 __1__ -data-tooltip-avg-launch-n=__1__ 次火箭發射的平均時間 -data-caption-milestone-n=里程碑 __1__ -data-tooltip-milestone-n=__1__ 次火箭發射的時間 -data-caption-milestone-next=N/A -data-tooltip-milestone-next=未達成 -progress-x-pos=X __1__ -progress-y-pos=Y __1__ -progress-label-tooltip=在地圖上看 -progress-launched=已發射 -progress-caption=__1__ % -progress-tooltip=該火箭發射井發射了 __1__ 次 -launch-failed=火箭發射失敗, 請過一會再試。 - [warp-list] main-caption=傳送陣清單 [img=info] main-tooltip=傳送陣清單 diff --git a/exp_scenario/module/gui/rocket_info.lua b/exp_scenario/module/gui/rocket_info.lua index 53b5ea5e..c377155a 100644 --- a/exp_scenario/module/gui/rocket_info.lua +++ b/exp_scenario/module/gui/rocket_info.lua @@ -353,7 +353,7 @@ Elements.progress_table = Gui.define("rocket_info/progress_table") -- The no silos label lives next to the table inside the same collapsible scroll pane local no_silos = assert(progress_table.parent).add{ type = "label", - caption = { "exp-gui_rocket-info.progress-no-silos" }, + caption = { "exp-gui_rocket-info.progress-caption-no-silos" }, } no_silos.style.padding = { 1, 2 } diff --git a/exp_scenario/module/locale/en.cfg b/exp_scenario/module/locale/en.cfg index 6c6071fa..ac5ec9cb 100644 --- a/exp_scenario/module/locale/en.cfg +++ b/exp_scenario/module/locale/en.cfg @@ -442,7 +442,6 @@ section-caption-milestones=Milestones section-tooltip-milestones=The times when milestones were met section-caption-progress=Build Progress section-tooltip-progress=The progress for your rocket silos -progress-no-silos=You have no rocket silos data-caption-first-launch=First Launch data-tooltip-first-launch=The time of the first launch data-caption-last-launch=Last Launch @@ -466,6 +465,7 @@ progress-label-tooltip=View on map progress-launched=Launched progress-caption=__1__% progress-tooltip=This silo has launched __1__ rockets +progress-caption-no-silos=You have no rocket silos [exp-gui_science-production] tooltip-main=Science Production diff --git a/exp_scenario/module/locale/zh-CN.cfg b/exp_scenario/module/locale/zh-CN.cfg index eea134ae..da26d0fa 100644 --- a/exp_scenario/module/locale/zh-CN.cfg +++ b/exp_scenario/module/locale/zh-CN.cfg @@ -430,6 +430,42 @@ caption-research-name=[technology=__1__] __2__ notice-inf=[color=255, 255, 255] 研究完成在 __1__ - [technology=__2__] - __3__[/color] notice=[color=255, 255, 255] 研究完成在 __1__ - [technology=__2__][/color] +[exp-gui_rocket-info] +tooltip-main=火箭資訊 +caption-main=發射火箭 +tooltip-toggle-section-expand=擴張欄 +tooltip-toggle-section-collapse=收縮欄 +section-caption-stats=數據 +section-tooltip-stats=火箭發射數據 +section-caption-milestones=里程碑 +section-tooltip-milestones=里程碑達成次數 +section-caption-progress=建造程度 +section-tooltip-progress=火箭建造程度 +data-caption-first-launch=首次發射 +data-tooltip-first-launch=首次發射時間 +data-caption-last-launch=最後發射 +data-tooltip-last-launch=最後發射時間 +data-caption-fastest-launch=最快發射 +data-tooltip-fastest-launch=最快發射時間 +data-caption-total-rockets=總發射次數 +data-tooltip-total-rockets=我方總發射次數 +value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。 +data-caption-avg-launch=平均時間 +data-tooltip-avg-launch=火箭發射平均時間 +data-caption-avg-launch-n=平均時間 __1__ +data-tooltip-avg-launch-n=__1__ 次火箭發射的平均時間 +data-caption-milestone-n=里程碑 __1__ +data-tooltip-milestone-n=__1__ 次火箭發射的時間 +data-caption-milestone-next=N/A +data-tooltip-milestone-next=未達成 +progress-x-pos=X __1__ +progress-y-pos=Y __1__ +progress-label-tooltip=在地圖上看 +progress-launched=已發射 +progress-caption=__1__ % +progress-tooltip=該火箭發射井發射了 __1__ 次 +progress-caption-no-silos=你沒有火箭發射井 + [exp-gui_science-production] tooltip-main=研究資訊 caption-main=研究瓶 diff --git a/exp_scenario/module/locale/zh-TW.cfg b/exp_scenario/module/locale/zh-TW.cfg index eea134ae..da26d0fa 100644 --- a/exp_scenario/module/locale/zh-TW.cfg +++ b/exp_scenario/module/locale/zh-TW.cfg @@ -430,6 +430,42 @@ caption-research-name=[technology=__1__] __2__ notice-inf=[color=255, 255, 255] 研究完成在 __1__ - [technology=__2__] - __3__[/color] notice=[color=255, 255, 255] 研究完成在 __1__ - [technology=__2__][/color] +[exp-gui_rocket-info] +tooltip-main=火箭資訊 +caption-main=發射火箭 +tooltip-toggle-section-expand=擴張欄 +tooltip-toggle-section-collapse=收縮欄 +section-caption-stats=數據 +section-tooltip-stats=火箭發射數據 +section-caption-milestones=里程碑 +section-tooltip-milestones=里程碑達成次數 +section-caption-progress=建造程度 +section-tooltip-progress=火箭建造程度 +data-caption-first-launch=首次發射 +data-tooltip-first-launch=首次發射時間 +data-caption-last-launch=最後發射 +data-tooltip-last-launch=最後發射時間 +data-caption-fastest-launch=最快發射 +data-tooltip-fastest-launch=最快發射時間 +data-caption-total-rockets=總發射次數 +data-tooltip-total-rockets=我方總發射次數 +value-tooltip-total-rockets=__1__% 的火箭發射都在本地圖上。 +data-caption-avg-launch=平均時間 +data-tooltip-avg-launch=火箭發射平均時間 +data-caption-avg-launch-n=平均時間 __1__ +data-tooltip-avg-launch-n=__1__ 次火箭發射的平均時間 +data-caption-milestone-n=里程碑 __1__ +data-tooltip-milestone-n=__1__ 次火箭發射的時間 +data-caption-milestone-next=N/A +data-tooltip-milestone-next=未達成 +progress-x-pos=X __1__ +progress-y-pos=Y __1__ +progress-label-tooltip=在地圖上看 +progress-launched=已發射 +progress-caption=__1__ % +progress-tooltip=該火箭發射井發射了 __1__ 次 +progress-caption-no-silos=你沒有火箭發射井 + [exp-gui_science-production] tooltip-main=研究資訊 caption-main=研究瓶