Files
factorio-scenario-ExpCluster/exp_legacy/module/modules/gui/vlayer.lua
T
bbassieandClaude Opus 5 ab89b9feca Type vlayer forward declarations and config entries
The gui element locals are declared at the top and assigned far below,
so emmylua inferred them as nil at every use site.

circuit_oc was reassigned from a control behaviour to a logistic
section, which hid every section method behind the wrong type.

allowed_items entries carry heterogeneous optional properties, now
described by a class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:18 +00:00

547 lines
20 KiB
Lua

--[[-- Gui Module - Virtual Layer
- Adds a virtual layer to store power to save space.
@gui Virtual Layer
@alias vlayer_container
]]
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 format_number = require("util").format_number --- @dep util
local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer
local vlayer = require("modules.exp_legacy.modules.control.vlayer")
local Selection = require("modules/exp_util/selection")
local SelectArea = Selection.connect("ExpGui_VLayer")
--- Align an aabb to the grid by expanding it
local function aabb_align_expand(aabb)
return {
left_top = { x = math.floor(aabb.left_top.x), y = math.floor(aabb.left_top.y) },
right_bottom = { x = math.ceil(aabb.right_bottom.x), y = math.ceil(aabb.right_bottom.y) },
}
end
local vlayer_container --- @type ExpElement
local vlayer_gui_control_type --- @type ExpElement
local vlayer_gui_control_list --- @type ExpElement
local vlayer_control_type_list = {
[1] = "energy",
[2] = "circuit",
[3] = "storage_input",
[4] = "storage_output",
}
local function pos_to_gps_string(pos, surface_name)
return "[gps=" .. string.format("%.1f", pos.x) .. "," .. string.format("%.1f", pos.y) .. "," .. surface_name .. "]"
end
local function format_energy(amount, unit)
if amount < 1 then
return "0 " .. unit
end
local suffix = ""
local suffix_list = {
["P"] = 1000000000000000,
["T"] = 1000000000000,
["G"] = 1000000000,
["M"] = 1000000,
["k"] = 1000,
}
for letter, limit in pairs(suffix_list) do
if math.abs(amount) >= limit then
amount = string.format("%.1f", amount / limit)
suffix = letter
break
end
end
local k
local formatted = amount
while true do
formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", "%1,%2")
if (k == 0) then
break
end
end
return formatted .. " " .. suffix .. unit
end
local ExpUtil = require("modules/exp_util")
--- When an area is selected to add protection to the area
SelectArea:on_selection(function(event)
log(ExpUtil.format_any(event))
local area = aabb_align_expand(event.area)
local player = game.players[event.player_index]
if not player then
return
end
local container = Gui.get_left_element(vlayer_container, player)
local disp = container.frame["vlayer_st_2"].disp.table
local target = vlayer_control_type_list[disp[vlayer_gui_control_type.name].selected_index]
local entities --- @type LuaEntity[]
if config.power_on_space and event.surface and event.surface.platform and target == "energy" then
if player.force.technologies[config.power_on_space_research.name].level >= config.power_on_space_research.level then
entities = event.surface.find_entities_filtered{ area = area, name = "constant-combinator", force = player.force }
else
player.print{ "vlayer.power-on-space-research", config.power_on_space_research.name, config.power_on_space_research.level }
return
end
else
entities = event.surface.find_entities_filtered{ area = area, name = "steel-chest", force = player.force }
end
if #entities == 0 then
player.print{ "vlayer.steel-chest-detect" }
return
elseif #entities > 1 then
player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-multiple" } }
return
end
if not entities[1] then
return
end
local e = entities[1]
local e_pos = { x = string.format("%.1f", e.position.x), y = string.format("%.1f", e.position.y) }
local e_circ = nil -- e.get_wire_connectors{ or_create = false }
if e.name and e.name == "steel-chest" and (not e.get_inventory(defines.inventory.chest).is_empty()) then
player.print{ "vlayer.steel-chest-empty" }
return
end
if (vlayer.get_interface_counts()[target] >= config.interface_limit[target]) then
player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-limit" } }
return
end
e.destroy()
if target == "energy" then
if not vlayer.create_energy_interface(event.surface, e_pos, player) then
player.print{ "vlayer.result-unable", { "vlayer.control-type-energy" }, { "vlayer.result-space" } }
return
end
elseif target == "circuit" then
vlayer.create_circuit_interface(event.surface, e_pos, e_circ, player)
elseif target == "storage_input" then
vlayer.create_input_interface(event.surface, e_pos, e_circ, player)
elseif target == "storage_output" then
vlayer.create_output_interface(event.surface, e_pos, e_circ, player)
end
game.print{ "vlayer.interface-result", player.name, pos_to_gps_string(e_pos, event.surface.name), { "vlayer.result-build" }, { "vlayer.control-type-" .. target:gsub("_", "-") } }
end)
--- Display label for the number of solar panels
-- @element vlayer_gui_display_item_solar_name
local vlayer_gui_display_item_solar_name = Gui.define("vlayer_gui_display_item_solar_name")
:draw{
type = "label",
name = Gui.from_name,
caption = { "vlayer.display-item-solar" },
style = "heading_2_label",
}:style{
width = 200,
}
local vlayer_gui_display_item_solar_count = Gui.define("vlayer_gui_display_item_solar_count")
:draw{
type = "progressbar",
name = Gui.from_name,
caption = "",
value = 0,
style = "electric_satisfaction_statistics_progressbar",
}:style{
width = 200,
font = "heading-2",
}
--- Display label for the number of accumulators
-- @element vlayer_gui_display_item_accumulator_name
local vlayer_gui_display_item_accumulator_name = Gui.define("vlayer_gui_display_item_accumulator_name")
:draw{
type = "label",
name = Gui.from_name,
caption = { "vlayer.display-item-accumulator" },
style = "heading_2_label",
}:style{
width = 200,
}
local vlayer_gui_display_item_accumulator_count = Gui.define("vlayer_gui_display_item_accumulator_count")
:draw{
type = "progressbar",
name = Gui.from_name,
caption = "",
value = 0,
style = "electric_satisfaction_statistics_progressbar",
}:style{
width = 200,
font = "heading-2",
}
--- Display label for the surface area
-- @element vlayer_gui_display_signal_surface_area_name
local vlayer_gui_display_signal_surface_area_name = Gui.define("vlayer_gui_display_signal_surface_area_name")
:draw{
type = "label",
name = Gui.from_name,
caption = { "vlayer.display-remaining-surface-area" },
tooltip = { "vlayer.display-remaining-surface-area-tooltip" },
style = "heading_2_label",
}:style{
width = 200,
}
local vlayer_gui_display_signal_surface_area_count = Gui.define("vlayer_gui_display_signal_surface_area_count")
:draw{
type = "progressbar",
name = Gui.from_name,
caption = "",
value = 0,
style = "electric_satisfaction_statistics_progressbar",
}:style{
width = 200,
font = "heading-2",
}
--- Display label for the sustained energy production
-- @element vlayer_gui_display_signal_sustained_name
local vlayer_gui_display_signal_sustained_name = Gui.define("vlayer_gui_display_signal_sustained_name")
:draw{
type = "label",
name = Gui.from_name,
caption = { "vlayer.display-sustained-production" },
tooltip = { "vlayer.display-sustained-production-tooltip" },
style = "heading_2_label",
}:style{
width = 200,
}
local vlayer_gui_display_signal_sustained_count = Gui.define("vlayer_gui_display_signal_sustained_count")
:draw{
type = "progressbar",
name = Gui.from_name,
caption = "",
value = 0,
style = "electric_satisfaction_statistics_progressbar",
}:style{
width = 200,
font = "heading-2",
}
--- Display label for the current energy production
-- @element vlayer_gui_display_signal_production_name
local vlayer_gui_display_signal_production_name = Gui.define("vlayer_gui_display_signal_production_name")
:draw{
type = "label",
name = Gui.from_name,
caption = { "vlayer.display-current-production" },
tooltip = { "vlayer.display-current-production-tooltip" },
style = "heading_2_label",
}:style{
width = 200,
}
local vlayer_gui_display_signal_production_count = Gui.define("vlayer_gui_display_signal_production_count")
:draw{
type = "progressbar",
name = Gui.from_name,
caption = "",
value = 0,
style = "electric_satisfaction_statistics_progressbar",
}:style{
width = 200,
font = "heading-2",
}
--- Display label for the sustained energy capacity
-- @element vlayer_gui_display_signal_capacity_name
local vlayer_gui_display_signal_capacity_name = Gui.define("vlayer_gui_display_signal_capacity_name")
:draw{
type = "label",
name = Gui.from_name,
caption = { "vlayer.display-current-capacity" },
tooltip = { "vlayer.display-current-capacity-tooltip" },
style = "heading_2_label",
}:style{
width = 200,
}
local vlayer_gui_display_signal_capacity_count = Gui.define("vlayer_gui_display_signal_capacity_count")
:draw{
type = "progressbar",
name = Gui.from_name,
caption = "",
value = 0,
style = "electric_satisfaction_statistics_progressbar",
}:style{
width = 200,
font = "heading-2",
}
--- A vertical flow containing all the displays labels and their counts
-- @element vlayer_display_set
local vlayer_display_set = Gui.define("vlayer_display_set")
:draw(function(_, parent, name)
local vlayer_set = parent.add{ type = "flow", direction = "vertical", name = name }
local disp = Gui.elements.scroll_table(vlayer_set, 400, 2, "disp")
vlayer_gui_display_item_solar_name(disp)
vlayer_gui_display_item_solar_count(disp)
vlayer_gui_display_item_accumulator_name(disp)
vlayer_gui_display_item_accumulator_count(disp)
vlayer_gui_display_signal_surface_area_name(disp)
vlayer_gui_display_signal_surface_area_count(disp)
vlayer_gui_display_signal_sustained_name(disp)
vlayer_gui_display_signal_sustained_count(disp)
vlayer_gui_display_signal_production_name(disp)
vlayer_gui_display_signal_production_count(disp)
vlayer_gui_display_signal_capacity_name(disp)
vlayer_gui_display_signal_capacity_count(disp)
return vlayer_set
end)
local function vlayer_gui_list_refresh(player)
local container = Gui.get_left_element(vlayer_container, player)
local disp = container.frame["vlayer_st_2"].disp.table
local target = disp[vlayer_gui_control_type.name].selected_index
local full_list = {}
if target then
local interface = vlayer.get_interfaces()[vlayer_control_type_list[target]]
for i = 1, vlayer.get_interface_counts()[vlayer_control_type_list[target]], 1 do
table.insert(full_list, i .. " X " .. interface[i].position.x .. " Y " .. interface[i].position.y)
end
disp[vlayer_gui_control_list.name].items = full_list
end
end
--- A drop down list filter by this type
-- @element vlayer_gui_control_type
vlayer_gui_control_type = Gui.define("vlayer_gui_control_type")
:draw{
type = "drop-down",
name = Gui.from_name,
items = { { "vlayer.control-type-energy" }, { "vlayer.control-type-circuit" }, { "vlayer.control-type-storage-input" }, { "vlayer.control-type-storage-output" } },
selected_index = 1,
}:style{
width = 200,
}:on_selection_state_changed(function(def, player, element)
vlayer_gui_list_refresh(player)
end)
--- A drop down list to see the exact item to remove
-- @element vlayer_gui_control_list
vlayer_gui_control_list = Gui.define("vlayer_gui_control_list")
:draw{
type = "drop-down",
name = Gui.from_name,
}:style{
width = 200,
}
--- A button to refresh the remove list
-- @element vlayer_gui_control_refresh
local vlayer_gui_control_refresh = Gui.define("vlayer_gui_control_refresh")
:draw{
type = "button",
name = Gui.from_name,
caption = { "vlayer.control-refresh" },
}:style{
width = 200,
}:on_click(function(def, player, element)
vlayer_gui_list_refresh(player)
end)
--- A button to check if the item is the one wanted to remove
-- @element vlayer_gui_control_see
local vlayer_gui_control_see = Gui.define("vlayer_gui_control_see")
:draw{
type = "button",
name = Gui.from_name,
caption = { "vlayer.control-see" },
}:style{
width = 200,
}:on_click(function(def, player, element, event)
local target = element.parent[vlayer_gui_control_type.name].selected_index
local n = element.parent[vlayer_gui_control_list.name].selected_index
if target and vlayer_control_type_list[target] and n > 0 then
local i = vlayer.get_interfaces()
local entity = i[vlayer_control_type_list[target]][n]
if entity and entity.valid then
local player = Gui.get_player(event)
player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface }
player.print{ "vlayer.result-interface-location", { "vlayer.control-type-" .. vlayer_control_type_list[target]:gsub("_", "-") }, pos_to_gps_string(entity.position, entity.surface.name) }
end
end
end)
--- A button used to build the vlayer interface
-- @element vlayer_gui_control_build
local vlayer_gui_control_build = Gui.define("vlayer_gui_control_build")
:draw{
type = "button",
name = Gui.from_name,
caption = { "vlayer.control-build" },
}:style{
width = 200,
}:on_click(function(def, player, element)
if SelectArea:stop(player) then
player.print{ "vlayer.exit" }
else
SelectArea:start(player)
player.print{ "vlayer.enter" }
end
vlayer_gui_list_refresh(player)
end)
--- A button used to remove the vlayer interface
-- @element vlayer_gui_control_remove
local vlayer_gui_control_remove = Gui.define("vlayer_gui_control_remove")
:draw{
type = "button",
name = Gui.from_name,
caption = { "vlayer.control-remove" },
}:style{
width = 200,
}:on_click(function(def, player, element)
local target = element.parent[vlayer_gui_control_type.name].selected_index
local n = element.parent[vlayer_gui_control_list.name].selected_index
if target and vlayer_control_type_list[target] and n > 0 then
local i = vlayer.get_interfaces()
if i and i[vlayer_control_type_list[target]] then
local interface_type, interface_surface, interface_position = vlayer.remove_interface(i[vlayer_control_type_list[target]][n].surface, i[vlayer_control_type_list[target]][n].position)
if interface_type then
game.print{ "vlayer.interface-result", player.name, pos_to_gps_string(interface_position, interface_surface.name), { "vlayer.result-remove" }, { "vlayer.control-type-" .. interface_type } }
end
end
end
vlayer_gui_list_refresh(player)
end)
--- A vertical flow containing all the control buttons
-- @element vlayer_control_set
local vlayer_control_set = Gui.define("vlayer_control_set")
:draw(function(_, parent, name)
local player = Gui.get_player(parent)
local vlayer_set = parent.add{ type = "flow", direction = "vertical", name = name }
local disp = Gui.elements.scroll_table(vlayer_set, 400, 2, "disp")
vlayer_gui_control_type(disp)
vlayer_gui_control_list(disp)
vlayer_gui_control_refresh(disp)
vlayer_gui_control_see(disp)
local b = vlayer_gui_control_build(disp)
local r = vlayer_gui_control_remove(disp)
local v = Roles.player_allowed(player, "gui/vlayer-edit")
b.visible = v
r.visible = v
return vlayer_set
end)
--- The main container for the vlayer gui
-- @element vlayer_container
vlayer_container = Gui.define("vlayer_container")
:draw(function(definition, parent)
local container = Gui.elements.container(parent, 400)
vlayer_display_set(container, "vlayer_st_1")
vlayer_control_set(container, "vlayer_st_2")
return container.parent
end)
--- Add the element to the left flow with a toolbar button
Gui.add_left_element(vlayer_container, false)
Gui.toolbar.create_button{
name = "vlayer_toggle",
left_element = vlayer_container,
sprite = "entity/solar-panel",
tooltip = { "vlayer.main-tooltip" },
visible = function(player, element)
return Roles.player_allowed(player, "gui/vlayer")
end
}
--- Update the visibly of the buttons based on a players roles
local function role_update_event(event)
local player = game.players[event.player_index]
local visible = Roles.player_allowed(player, "gui/vlayer-edit")
local container = Gui.get_left_element(vlayer_container, player)
local disp = container.frame["vlayer_st_2"].disp.table
disp[vlayer_gui_control_build.name].visible = visible
disp[vlayer_gui_control_remove.name].visible = visible
end
Event.add(Roles.events.on_role_assigned, role_update_event)
Event.add(Roles.events.on_role_unassigned, role_update_event)
Event.on_nth_tick(config.update_tick_gui, function(_)
local stats = vlayer.get_statistics()
local items = vlayer.get_items()
local items_alloc = vlayer.get_allocated_items()
local vlayer_display = {
[vlayer_gui_display_item_solar_count.name] = {
val = (items_alloc["solar-panel"] / math.max(items["solar-panel"], 1)),
cap = format_number(items_alloc["solar-panel"], false) .. " / " .. format_number(items["solar-panel"], false),
},
[vlayer_gui_display_item_accumulator_count.name] = {
val = (items_alloc["accumulator"] / math.max(items["accumulator"], 1)),
cap = format_number(items_alloc["accumulator"], false) .. " / " .. format_number(items["accumulator"], false),
},
[vlayer_gui_display_signal_surface_area_count.name] = {
val = (stats.total_surface_area / math.max(stats.surface_area, 1)),
cap = format_number(stats.remaining_surface_area)
},
[vlayer_gui_display_signal_sustained_count.name] = {
val = (stats.energy_sustained / math.max(stats.energy_total_production, 1)),
cap = format_energy(stats.energy_sustained, "W") .. " / " .. format_energy(stats.energy_total_production, "W")
},
[vlayer_gui_display_signal_production_count.name] = {
val = (stats.energy_production / math.max(stats.energy_max, 1)),
cap = format_energy(stats.energy_production, "W") .. " / " .. format_energy(stats.energy_max, "W"),
},
[vlayer_gui_display_signal_capacity_count.name] = {
val = (stats.energy_storage / math.max(stats.energy_capacity, 1)),
cap = format_energy(stats.energy_storage, "J") .. " / " .. format_energy(stats.energy_capacity, "J"),
},
}
for _, player in pairs(game.connected_players) do
local container = Gui.get_left_element(vlayer_container, player)
local disp = container.frame["vlayer_st_1"].disp.table
for k, v in pairs(vlayer_display) do
disp[k].caption = v.cap
if v.val then
disp[k].value = v.val
end
end
end
end)