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) <noreply@anthropic.com>
This commit is contained in:
bbassie
2026-06-18 18:53:23 +00:00
co-authored by Claude Opus 4.8
parent 1fcf351566
commit 0ead1a51f9
+357 -226
View File
@@ -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 ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage")
local Gui = require("modules/exp_gui") local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_legacy/expcore/roles") local Roles = require("modules/exp_legacy/expcore/roles")
local config = require("modules/exp_legacy/config/gui/rockets") 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 }, 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<string, table<number, number>> Force name to an array of launch ticks indexed by rocket number
local rocket_times = {}
--- @type table<string, { first_launch: number?, last_launch: number?, fastest_launch: number? }> Force name to launch stats
local rocket_stats = {}
--- @type table<number, ExpGui_RocketInfo.silo_data> 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 -- The largest rolling average is used to know when an old launch time can be discarded
local largest_rolling_avg = 0 local largest_rolling_avg = 0
for _, avg_over in pairs(config.stats.rolling_avg) do 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
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 The per force rocket data is stored on the container element data using the force scope, this means it
--- @return ExpGui_RocketInfo.silo_data[] persists with the rest of the gui data and is cleaned up when forces are merged. It holds the launch
local function get_silos(force_name) stats, the tick each rocket was launched on, and the silos that belong to the force.
local rtn = {} ]]
for unit_number, silo_data in pairs(rocket_silos) do
if not silo_data.entity.valid then --- @class ExpGui_RocketInfo.silo_data
rocket_silos[unit_number] = nil --- @field entity LuaEntity The rocket silo entity
elseif silo_data.force == force_name then --- @field launched number The number of rockets launched from this silo
rtn[#rtn + 1] = silo_data --- @field awaiting_reset boolean True when a launch is ordered but the silo has not reset yet
end
--- @class ExpGui_RocketInfo.force_data
--- @field stats { first_launch: number?, last_launch: number?, fastest_launch: number? } Launch stats for the force
--- @field times table<number, number> Launch tick indexed by rocket number
--- @field silos table<number, ExpGui_RocketInfo.silo_data> 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 end
return data
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 end
--- Get the total number of rockets launched by all forces --- Get the total number of rockets launched by all forces
@@ -97,14 +73,14 @@ local function get_game_rocket_count()
end end
--- Get the rolling average time to launch a rocket based on the last count rockets --- 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 --- @param count number Number of rockets to average over
--- @return number # Number of ticks required to launch one rocket --- @return number # Number of ticks required to launch one rocket
local function get_rolling_average(force_name, count) local function get_rolling_average(force, count)
local times = rocket_times[force_name] local rocket_count = force.rockets_launched
local rocket_count = game.forces[force_name].rockets_launched if rocket_count == 0 then return 0 end
if rocket_count == 0 or not times then return 0 end
local times = get_force_data(force).times
local last_launch_time = times[rocket_count] or 0 local last_launch_time = times[rocket_count] or 0
local start_rocket_time = 0 local start_rocket_time = 0
if count < rocket_count then 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) return math.floor((last_launch_time - start_rocket_time) / rocket_count)
end 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. 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 --- A clickable coordinate label which opens the silo location on the map when pressed
--- @class ExpGui_RocketInfo.elements.position_label: ExpElement --- @class ExpGui_RocketInfo.elements.position_label: ExpElement
--- @field data table<LuaGuiElement, number> --- @field data table<LuaGuiElement, LuaEntity>
--- @overload fun(parent: LuaGuiElement, opts: { caption: LocalisedString, tooltip: LocalisedString?, unit_number: number }): LuaGuiElement --- @overload fun(parent: LuaGuiElement, opts: { caption: LocalisedString, tooltip: LocalisedString?, entity: LuaEntity }): LuaGuiElement
Elements.position_label = Gui.define("rocket_info/position_label") Elements.position_label = Gui.define("rocket_info/position_label")
:draw{ :draw{
type = "label", type = "label",
@@ -163,189 +252,244 @@ Elements.position_label = Gui.define("rocket_info/position_label")
padding = { 0, 2 }, padding = { 0, 2 },
} }
:element_data( :element_data(
Gui.from_argument("unit_number") Gui.from_argument("entity")
) )
:on_click(function(def, player, element) :on_click(function(def, player, element)
--- @cast def ExpGui_RocketInfo.elements.position_label --- @cast def ExpGui_RocketInfo.elements.position_label
if not config.progress.allow_zoom_to_map then return end if not config.progress.allow_zoom_to_map then return end
local silo_data = rocket_silos[def.data[element]] local entity = def.data[element]
if not silo_data or not silo_data.entity.valid then return end if not entity or not entity.valid then return end
local entity = silo_data.entity
player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface } player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface }
end) --[[ @as any ]] end) --[[ @as any ]]
--- Add a name and value label pair to a data table --- Data table showing the launch statistics for a force
--- @param data_table LuaGuiElement The two column table to add the pair to --- @class ExpGui_RocketInfo.elements.stats_table: ExpElement
--- @param name string The data name, used to select the locale key --- @field data table<LuaGuiElement, table<string, LuaGuiElement>>
--- @param subname number? Optional subname passed as a parameter to the locale string --- @overload fun(parent: LuaGuiElement): LuaGuiElement
--- @param value LocalisedString The value to display Elements.stats_table = Gui.define("rocket_info/stats_table")
--- @param value_tooltip LocalisedString? Optional tooltip for the value label :track_all_elements()
local function add_data_label(data_table, name, subname, value, value_tooltip) :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{ local name_label = data_table.add{
type = "label", type = "label",
caption = { "exp-gui_rocket-info.data-caption-" .. name, subname }, caption = { "exp-gui_rocket-info.data-caption-" .. row.name, row.subname },
tooltip = { "exp-gui_rocket-info.data-tooltip-" .. name, subname }, tooltip = { "exp-gui_rocket-info.data-tooltip-" .. row.name, row.subname },
} }
name_label.style.padding = { 0, 2 } name_label.style.padding = { 0, 2 }
local value_label = data_table.add{ local value_label = data_table.add{
type = "label", type = "label",
caption = value, caption = row.value,
tooltip = value_tooltip, tooltip = row.tooltip,
} }
value_label.style.padding = { 0, 2 } value_label.style.padding = { 0, 2 }
labels[row.key] = value_label
end end
--- Data table showing the launch statistics for a force --- Refresh the stats table for a force, adding any rows that do not yet exist
--- @class ExpGui_RocketInfo.elements.stats_table: ExpElement --- @param data_table LuaGuiElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement --- @param force LuaForce
Elements.stats_table = Gui.define("rocket_info/stats_table") function Elements.stats_table.refresh(data_table, force)
:track_all_elements() local labels = Elements.stats_table.data[data_table]
:draw(function(def, parent) for _, row in ipairs(compute_stat_rows(force)) do
return Gui.elements.scroll_table(parent, 215, 2) local value_label = labels[row.key]
end) --[[ @as any ]] if value_label then
value_label.caption = row.value
--- Refresh a stats table with the most recent data for a force value_label.tooltip = row.tooltip
--- @param stats_table LuaGuiElement else
--- @param force_name string Elements.stats_table.add_row(data_table, row)
function Elements.stats_table.refresh(stats_table, force_name) end
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
end end
--- Data table showing the milestones for a force --- Data table showing the milestones for a force
--- @class ExpGui_RocketInfo.elements.milestones_table: ExpElement --- @class ExpGui_RocketInfo.elements.milestones_table: ExpElement
--- @field data table<LuaGuiElement, table<number, LuaGuiElement>>
--- @overload fun(parent: LuaGuiElement): LuaGuiElement --- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.milestones_table = Gui.define("rocket_info/milestones_table") Elements.milestones_table = Gui.define("rocket_info/milestones_table")
:track_all_elements() :track_all_elements()
:draw(function(def, parent) :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 ]] end) --[[ @as any ]]
--- Refresh a milestones table with the most recent data for a force --- Add a milestone row to the table and store its value label
--- @param milestones_table LuaGuiElement --- @param data_table LuaGuiElement
--- @param force_name string --- @param row ExpGui_RocketInfo.milestone_row
function Elements.milestones_table.refresh(milestones_table, force_name) function Elements.milestones_table.add_row(data_table, row)
milestones_table.clear() local labels = Elements.milestones_table.data[data_table]
local force_rockets = get_rocket_count(force_name) local name_label = data_table.add{
local times = rocket_times[force_name] or {} 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 local value_label = data_table.add{
if milestone <= force_rockets then type = "label",
local time = times[milestone] or 0 caption = row.value,
add_data_label(milestones_table, "milestone-n", milestone, time_formats.caption_hours(time), time_formats.tooltip_hours(time)) 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 else
-- The first unachieved milestone is shown as the next milestone then we stop Elements.milestones_table.add_row(data_table, row)
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 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<number, ExpGui_RocketInfo.elements.progress_table.row>
--- @field no_silos LuaGuiElement
--- Data table showing the build progress of each silo for a force --- Data table showing the build progress of each silo for a force
--- @class ExpGui_RocketInfo.elements.progress_table: ExpElement --- @class ExpGui_RocketInfo.elements.progress_table: ExpElement
--- @field data table<LuaGuiElement, ExpGui_RocketInfo.elements.progress_table.data>
--- @overload fun(parent: LuaGuiElement): LuaGuiElement --- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.progress_table = Gui.define("rocket_info/progress_table") Elements.progress_table = Gui.define("rocket_info/progress_table")
:track_all_elements() :track_all_elements()
:draw(function(def, parent) :draw(function(def, parent)
return Gui.elements.scroll_table(parent, 215, 3) --- @cast def ExpGui_RocketInfo.elements.progress_table
end) --[[ @as any ]] local data_table = Gui.elements.scroll_table(parent, 215, 3)
--- Refresh a progress table with the most recent data for a force -- The no silos label lives next to the table inside the same collapsible scroll pane
--- @param progress_table LuaGuiElement local no_silos = assert(data_table.parent).add{
--- @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", type = "label",
caption = { "exp-gui_rocket-info.progress-no-silos" }, 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 } no_silos.style.padding = { 1, 2 }
progress_label.style.font_color = progress_color
end 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 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 container LuaGuiElement The container frame to add the section to
--- @param section_name string Used to select the locale keys for the header --- @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 table_define any The data table element define to add
--- @param force_name string The force to populate the table with
--- @return LuaGuiElement --- @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, { local header = Gui.elements.header(container, {
caption = { "exp-gui_rocket-info.section-caption-" .. section_name }, caption = { "exp-gui_rocket-info.section-caption-" .. section_name },
tooltip = { "exp-gui_rocket-info.section-tooltip-" .. section_name }, tooltip = { "exp-gui_rocket-info.section-tooltip-" .. section_name },
}) })
local data_table = table_define(container) 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 -- The scroll pane is the parent of the table, it is what gets collapsed
local scroll_pane = assert(data_table.parent) local scroll_pane = assert(data_table.parent)
@@ -357,28 +501,28 @@ end
--- Container added to the left gui flow --- Container added to the left gui flow
--- @class ExpGui_RocketInfo.elements.container: ExpElement --- @class ExpGui_RocketInfo.elements.container: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement --- @field data table<LuaForce, ExpGui_RocketInfo.force_data>
Elements.container = Gui.define("rocket_info/container") Elements.container = Gui.define("rocket_info/container")
:draw(function(def, parent) :draw(function(def, parent)
local container = Gui.elements.container(parent, 200) local container = Gui.elements.container(parent, 200)
container.style.padding = 0 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 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 end
if config.milestones.show_milestones then 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 end
if config.progress.show_progress then 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 end
return Gui.elements.container.get_root_element(container) return Gui.elements.container.get_root_element(container)
end) --[[ @as any ]] end)
--- Add the element to the left flow with a toolbar button --- Add the element to the left flow with a toolbar button
Gui.add_left_element(Elements.container, false) 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 --- Refresh the stats and milestones tables for all online players on a force
--- @param force_name string --- @param force LuaForce
local function refresh_force_stats(force_name) local function refresh_force_stats(force)
local force = game.forces[force_name] for _, data_table in Elements.stats_table:online_elements(force) do
for _, stats_table in Elements.stats_table:online_elements(force) do Elements.stats_table.refresh(data_table, force)
Elements.stats_table.refresh(stats_table, force_name)
end end
for _, milestones_table in Elements.milestones_table:online_elements(force) do for _, data_table in Elements.milestones_table:online_elements(force) do
Elements.milestones_table.refresh(milestones_table, force_name) Elements.milestones_table.refresh(data_table, force)
end end
end end
--- Refresh the progress table for all online players on a force --- Refresh the progress table for all online players on a force
--- @param force_name string --- @param force LuaForce
local function refresh_force_progress(force_name) local function refresh_force_progress(force)
local force = game.forces[force_name] for _, data_table in Elements.progress_table:online_elements(force) do
for _, progress_table in Elements.progress_table:online_elements(force) do Elements.progress_table.refresh(data_table, force)
Elements.progress_table.refresh(progress_table, force_name)
end end
end end
@@ -421,16 +563,11 @@ end
--- @param event EventData.on_cargo_pod_finished_ascending --- @param event EventData.on_cargo_pod_finished_ascending
local function on_cargo_pod_finished_ascending(event) local function on_cargo_pod_finished_ascending(event)
local force = event.cargo_pod.force --[[ @as LuaForce ]] local force = event.cargo_pod.force --[[ @as LuaForce ]]
local force_name = force.name
local rockets_launched = force.rockets_launched local rockets_launched = force.rockets_launched
local force_data = get_force_data(force)
-- Update the launch stats for the force -- Update the launch stats for the force
local stats = rocket_stats[force_name] local stats = force_data.stats
if not stats then
stats = {}
rocket_stats[force_name] = stats
end
if rockets_launched == 1 then if rockets_launched == 1 then
stats.first_launch = event.tick stats.first_launch = event.tick
stats.fastest_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 stats.last_launch = event.tick
-- Append the launch tick into the times array -- Append the launch tick into the times array
local times = rocket_times[force_name] force_data.times[rockets_launched] = event.tick
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 -- 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 local remove_rocket = rockets_launched - largest_rolling_avg
if remove_rocket > 0 and not table.array_contains(config.milestones, remove_rocket) then 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 end
refresh_force_stats(force_name) refresh_force_stats(force)
refresh_force_progress(force_name) refresh_force_progress(force)
end end
--- Mark a silo as awaiting reset when a launch is ordered --- Mark a silo as awaiting reset when a launch is ordered
--- @param event EventData.on_rocket_launch_ordered --- @param event EventData.on_rocket_launch_ordered
local function on_rocket_launch_ordered(event) 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 if not silo_data then return end
silo_data.launched = silo_data.launched + 1 silo_data.launched = silo_data.launched + 1
silo_data.awaiting_reset = true silo_data.awaiting_reset = true
refresh_force_progress(event.rocket_silo.force.name) refresh_force_progress(silo.force --[[ @as LuaForce ]])
end 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 --- @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 function on_built(event)
local entity = event.entity local entity = event.entity
if not entity.valid or entity.name ~= "rocket-silo" then return end 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, entity = entity,
force = entity.force.name,
launched = 0, launched = 0,
awaiting_reset = false, awaiting_reset = false,
} }
refresh_force_progress(entity.force.name) refresh_force_progress(entity.force --[[ @as LuaForce ]])
end end
--- Refresh the progress for all forces that own at least one silo --- Refresh the progress for all forces that own at least one silo
local function refresh_all_progress() local function refresh_all_progress()
for _, force in pairs(game.forces) do for _, force in pairs(game.forces) do
if #get_silos(force.name) > 0 then if next(get_force_data(force).silos) then
refresh_force_progress(force.name) refresh_force_progress(force)
end end
end end
end end