Files
factorio-expcluster/exp_scenario/module/gui/warp_list.lua
T
Bastiaan 757a36fc03 Merge pull request #486 from bbassie/feature/convert-configs
Convert the remaining scenario configs into features
2026-10-05 00:33:36 +01:00

1252 lines
50 KiB
Lua

--[[-- Gui - Warp List
Adds a list of warp points which players can travel between, add, and edit
]]
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local Colors = require("modules/exp_util/include/color")
local Feature = require("modules/exp_scenario/features")
local ExpUtil = require("modules/exp_util")
local format_time = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true }
local floor = math.floor
local ceil = math.ceil
--- @class ExpGui_WarpList.elements
local Elements = {}
local Styles = {
sprite22 = { height = 22, width = 22, padding = -2 },
sprite32 = { height = 32, width = 32, left_margin = 1 },
}
--- Rich text icons shown next to a warp name to describe why it can or can not be used
local status_icons = {
cooldown = "[img=utility/multiplayer_waiting_icon]",
not_available = "[img=utility/set_bar_slot]",
bypass = "[img=utility/side_menu_bonus_icon]",
current = "[img=utility/side_menu_map_icon]",
connected = "[img=utility/logistic_network_panel_white]",
different = "[img=utility/warning_white]",
}
local feature, config = Feature.register("warp_list", {
minimum_distance = 100, -- the minimum distance that is allowed between warps on the same force
cooldown_duration = 60, -- the duration of the warp cooldown in seconds
standard_proximity_radius = 4, -- the minimum distance a player is allowed to be to a warp in order to use it
spawn_proximity_radius = 20, -- the minimum distance a player is allowed to be from their spawn point to use warps
user_can_edit_own_warps = false, -- the player who made a warp can edit it without the edit permission
})
--- Ticks between updates of the cooldown timers
local update_interval = 6
--- The default icon of new warps
local default_icon = { type = "item", name = "discharge-defense-equipment" }
--- The entities which are created for warp areas { name, x, y }
local area_entities = {
{ "small-lamp", -4, -2 }, { "small-lamp", -2, -4 }, { "medium-electric-pole", -3, -3 }, -- Top left corner
{ "small-lamp", 3, -2 }, { "small-lamp", 1, -4 }, { "medium-electric-pole", 2, -3 }, -- Top right corner
{ "small-lamp", 3, 1 }, { "small-lamp", 1, 3 }, { "medium-electric-pole", 2, 2 }, -- Bottom right corner
{ "small-lamp", -4, 1 }, { "small-lamp", -2, 3 }, { "medium-electric-pole", -3, 2 }, -- Bottom left corner
}
--- The tiles which are created for warp areas { name, x, y }
local area_tiles = {
{ "black-refined-concrete", -4, -2 }, { "black-refined-concrete", -4, -1 }, { "black-refined-concrete", -4, 0 }, { "black-refined-concrete", -4, 1 },
{ "black-refined-concrete", -3, -3 }, { "purple-refined-concrete", -3, -2 }, { "purple-refined-concrete", -3, -1 }, { "purple-refined-concrete", -3, 0 },
{ "purple-refined-concrete", -3, 1 }, { "black-refined-concrete", -3, 2 }, { "black-refined-concrete", -2, -4 }, { "purple-refined-concrete", -2, -3 },
{ "purple-refined-concrete", -2, -2 }, { "purple-refined-concrete", -2, -1 }, { "purple-refined-concrete", -2, 0 }, { "purple-refined-concrete", -2, 1 },
{ "purple-refined-concrete", -2, 2 }, { "black-refined-concrete", -2, 3 }, { "black-refined-concrete", -1, -4 }, { "purple-refined-concrete", -1, -3 },
{ "purple-refined-concrete", -1, -2 }, { "purple-refined-concrete", -1, -1 }, { "purple-refined-concrete", -1, 0 }, { "purple-refined-concrete", -1, 1 },
{ "purple-refined-concrete", -1, 2 }, { "black-refined-concrete", -1, 3 }, { "black-refined-concrete", 0, -4 }, { "purple-refined-concrete", 0, -3 },
{ "purple-refined-concrete", 0, -2 }, { "purple-refined-concrete", 0, -1 }, { "purple-refined-concrete", 0, 0 }, { "purple-refined-concrete", 0, 1 },
{ "purple-refined-concrete", 0, 2 }, { "black-refined-concrete", 0, 3 }, { "black-refined-concrete", 1, -4 }, { "purple-refined-concrete", 1, -3 },
{ "purple-refined-concrete", 1, -2 }, { "purple-refined-concrete", 1, -1 }, { "purple-refined-concrete", 1, 0 }, { "purple-refined-concrete", 1, 1 },
{ "purple-refined-concrete", 1, 2 }, { "black-refined-concrete", 1, 3 }, { "black-refined-concrete", 2, -3 }, { "purple-refined-concrete", 2, -2 },
{ "purple-refined-concrete", 2, -1 }, { "purple-refined-concrete", 2, 0 }, { "purple-refined-concrete", 2, 1 }, { "black-refined-concrete", 2, 2 },
{ "black-refined-concrete", 3, -2 }, { "black-refined-concrete", 3, -1 }, { "black-refined-concrete", 3, 0 }, { "black-refined-concrete", 3, 1 },
}
--- Names of the entities which make up a warp area, used to find them when the area is removed
local area_entity_names = {} --- @type string[]
for _, entity in ipairs(area_entities) do
area_entity_names[#area_entity_names + 1] = entity[1]
end
--- The role permission each action needs
local permissions = {
bypass_warp_cooldown = "exp_scenario.gui.warp_list.bypass_cooldown",
bypass_warp_proximity = "exp_scenario.gui.warp_list.bypass_proximity",
allow_add_warp = "exp_scenario.gui.warp_list.add",
allow_edit_warp = "exp_scenario.gui.warp_list.edit",
}
--- @class ExpGui_WarpList.Warp
--- @field id number
--- @field force LuaForce
--- @field name string
--- @field icon SignalID
--- @field surface LuaSurface
--- @field position MapPosition.struct The tile the warp is centred on
--- @field last_user LuaPlayer? Nil for warps made by the scenario
--- @field last_edit_tick number
--- @field editing table<number, string> Names of the players editing the warp keyed by their index
--- @field tag LuaCustomChartTag?
--- @field electric_pole LuaEntity? Pole within the warp area, warps on the same network can be travelled between
--- @field old_tile string? The tile the warp area replaced, nil when no area was made
--- @field new boolean? True until the first edit is confirmed, discarding that edit removes the warp
--- @class ExpGui_WarpList.PlayerStatus
--- @field in_range ExpGui_WarpList.Warp? The warp the player is standing on
--- @field on_cooldown boolean
--- @field bypass_proximity boolean
--- @param player LuaPlayer
--- @param action "bypass_warp_cooldown" | "bypass_warp_proximity" | "allow_add_warp" | "allow_edit_warp"
--- @return boolean
local function has_config_permission(player, action)
return Roles.player_has_permission(player, permissions[action])
end
--- Check if a player can edit a warp, the spawn warp can never be edited
--- @param player LuaPlayer
--- @param warp ExpGui_WarpList.Warp
--- @return boolean
local function has_permission_edit_warp(player, warp)
if Elements.container.get_spawn_warp(warp.force) == warp then
return false
end
if config.user_can_edit_own_warps and warp.last_user == player then
return true
end
return has_config_permission(player, "allow_edit_warp")
end
--- The sprite path for a warp icon, signal ids and sprite paths name virtual signals differently
--- @param icon SignalID
--- @return SpritePath
local function icon_sprite(icon)
if icon.type == "virtual" then
return "virtual-signal/" .. icon.name
end
return (icon.type or "item") .. "/" .. icon.name
end
--- Draw a cross over something that stops a warp being added
--- @param surface LuaSurface
--- @param player LuaPlayer
--- @param target LuaEntity | MapPosition
local function draw_blocker(surface, player, target)
rendering.draw_sprite{
sprite = "utility/rail_path_not_possible",
x_scale = 0.5,
y_scale = 0.5,
target = target,
surface = surface,
players = { player },
time_to_live = 60,
}
end
--- Button in the header which adds a warp at the player
--- @class ExpGui_WarpList.elements.add_warp_button: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.add_warp_button = Gui.define("warp_list/add_warp_button")
:track_all_elements()
:draw{
type = "sprite-button",
sprite = "utility/add",
tooltip = { "exp-gui_warp-list.tooltip-add" },
style = "shortcut_bar_button",
}
:style(Styles.sprite22)
:on_click(function(_, player)
if player.controller_type ~= defines.controllers.character then return end
local surface = player.physical_surface
local position = player.physical_position
local water_tiles = surface.find_tiles_filtered{
collision_mask = "water_tile",
radius = config.standard_proximity_radius + 1,
position = position,
}
if #water_tiles > 0 then
player.print({ "exp-gui_warp-list.add-warning-water", config.standard_proximity_radius + 1 }, Colors.orange_red)
player.play_sound{ path = "utility/wire_pickup" }
for _, tile in pairs(water_tiles) do
draw_blocker(surface, player, tile.position)
end
return
end
-- A larger radius than the area because the entities a player can place are larger than tiles
local entities = surface.find_entities_filtered{
radius = config.standard_proximity_radius + 2.5,
position = position,
collision_mask = { "item", "object", "player", "water_tile" },
}
if #entities > 1 then -- The player's own character is always found
player.print({ "exp-gui_warp-list.add-warning-entities", config.standard_proximity_radius + 2.5 }, Colors.orange_red)
player.play_sound{ path = "utility/wire_pickup" }
local character = player.character
for _, entity in pairs(entities) do
if entity ~= character then
draw_blocker(surface, player, entity)
end
end
return
end
local force = player.force --[[@as LuaForce]]
local warp = Elements.container.add_warp(force, surface, position, player)
Elements.container.set_editing(warp, player, true)
end) --[[@as any]]
--- Refresh whether the add button is enabled based on the closest warp
--- @param player LuaPlayer
--- @param blocking ExpGui_WarpList.Warp? The warp too close to add another, nil when a warp can be added
function Elements.add_warp_button.refresh_player_blocking(player, blocking)
local tooltip = blocking and { "exp-gui_warp-list.tooltip-add-too-close", blocking.name } or { "exp-gui_warp-list.tooltip-add" }
for _, add_warp_button in Elements.add_warp_button:tracked_elements(player) do
add_warp_button.enabled = blocking == nil
add_warp_button.tooltip = tooltip
end
end
--- @class ExpGui_WarpList.elements.warp_button.element_data
--- @field warp ExpGui_WarpList.Warp
--- Button which travels to the warp, enabled when the player is allowed to use it
--- @class ExpGui_WarpList.elements.warp_button: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.warp_button.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement
Elements.warp_button = Gui.define("warp_list/warp_button")
:draw(function(_, parent, warp)
--- @cast warp ExpGui_WarpList.Warp
return parent.add{
type = "sprite-button",
sprite = icon_sprite(warp.icon),
style = "slot_button",
}
end)
:style(Styles.sprite32)
:element_data{
warp = Gui.from_argument(1),
}
:on_click(function(def, player, warp_button)
--- @cast def ExpGui_WarpList.elements.warp_button
Elements.container.teleport_player(player, def.data[warp_button].warp)
end) --[[@as any]]
--- @class ExpGui_WarpList.elements.icon_picker.element_data
--- @field warp ExpGui_WarpList.Warp
--- Signal picker shown in place of the warp button while editing
--- @class ExpGui_WarpList.elements.icon_picker: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.icon_picker.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement
Elements.icon_picker = Gui.define("warp_list/icon_picker")
:draw(function(_, parent, warp)
--- @cast warp ExpGui_WarpList.Warp
return parent.add{
type = "choose-elem-button",
elem_type = "signal",
signal = warp.icon,
tooltip = { "exp-gui_warp-list.tooltip-icon" },
}
end)
:style(Styles.sprite32)
:element_data{
warp = Gui.from_argument(1),
} --[[@as any]]
--- Get the icon chosen in a picker, the game omits the type for items
--- @param icon_picker LuaGuiElement
--- @return SignalID
function Elements.icon_picker.get_icon(icon_picker)
local icon = icon_picker.elem_value --[[@as SignalID?]]
if not icon then
return Elements.icon_picker.data[icon_picker].warp.icon
end
icon.type = icon.type or "item"
return icon
end
--- Icon describing whether the warp can be used
--- @class ExpGui_WarpList.elements.status_label: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.status_label = Gui.define("warp_list/status_label")
:draw{
type = "label",
caption = status_icons.not_available,
}
:style{
top_padding = 1, -- Keeps the icon in place when the taller textfield is shown
single_line = false,
} --[[@as any]]
--- @class ExpGui_WarpList.elements.name_label.element_data
--- @field warp ExpGui_WarpList.Warp
--- Warp name, clicking it opens the map at the warp
--- @class ExpGui_WarpList.elements.name_label: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.name_label.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement
Elements.name_label = Gui.define("warp_list/name_label")
:draw(function(_, parent, warp)
--- @cast warp ExpGui_WarpList.Warp
return parent.add{
type = "label",
caption = warp.name,
}
end)
:style{
single_line = true,
left_padding = 2,
right_padding = 2,
horizontally_stretchable = true,
}
:element_data{
warp = Gui.from_argument(1),
}
:on_click(function(def, player, name_label)
--- @cast def ExpGui_WarpList.elements.name_label
local warp = def.data[name_label].warp
player.set_controller{ type = defines.controllers.remote, position = warp.position, surface = warp.surface }
end) --[[@as any]]
--- Refresh the name and last edit of the label
--- @param name_label LuaGuiElement
--- @param warp ExpGui_WarpList.Warp
function Elements.name_label.refresh(name_label, warp)
local last_user = warp.last_user and warp.last_user.name or "<server>"
name_label.caption = warp.name
name_label.tooltip = { "exp-gui_warp-list.tooltip-last-edit", last_user, format_time(warp.last_edit_tick) }
end
--- @class ExpGui_WarpList.elements.name_field.element_data
--- @field warp ExpGui_WarpList.Warp
--- @field icon_picker LuaGuiElement
--- Textfield shown in place of the name label while editing, confirming it saves the edit
--- @class ExpGui_WarpList.elements.name_field: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.name_field.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp, icon_picker: LuaGuiElement): LuaGuiElement
Elements.name_field = Gui.define("warp_list/name_field")
:draw(function(_, parent, warp)
--- @cast warp ExpGui_WarpList.Warp
return parent.add{
type = "textfield",
text = warp.name,
clear_and_focus_on_right_click = true,
}
end)
:style{
minimal_width = 10,
maximal_width = 300,
horizontally_squashable = true,
horizontally_stretchable = true,
height = 22,
padding = -2,
left_margin = 2,
right_margin = 2,
}
:element_data{
warp = Gui.from_argument(1),
icon_picker = Gui.from_argument(2),
}
:on_confirmed(function(def, player, name_field)
--- @cast def ExpGui_WarpList.elements.name_field
local elements = def.data[name_field]
local icon = Elements.icon_picker.get_icon(elements.icon_picker)
Elements.container.update_warp(elements.warp, name_field.text, icon, player)
end) --[[@as any]]
--- @class ExpGui_WarpList.elements.confirm_edit_button.element_data
--- @field warp ExpGui_WarpList.Warp
--- @field name_field LuaGuiElement
--- @field icon_picker LuaGuiElement
--- Button which saves the name and icon being edited
--- @class ExpGui_WarpList.elements.confirm_edit_button: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.confirm_edit_button.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp, name_field: LuaGuiElement, icon_picker: LuaGuiElement): LuaGuiElement
Elements.confirm_edit_button = Gui.define("warp_list/confirm_edit_button")
:draw{
type = "sprite-button",
sprite = "utility/confirm_slot",
tooltip = { "exp-gui_warp-list.tooltip-confirm" },
style = "shortcut_bar_button_green",
}
:style(Styles.sprite22)
:element_data{
warp = Gui.from_argument(1),
name_field = Gui.from_argument(2),
icon_picker = Gui.from_argument(3),
}
:on_click(function(def, player, confirm_edit_button)
--- @cast def ExpGui_WarpList.elements.confirm_edit_button
local elements = def.data[confirm_edit_button]
local icon = Elements.icon_picker.get_icon(elements.icon_picker)
Elements.container.update_warp(elements.warp, elements.name_field.text, icon, player)
end) --[[@as any]]
--- @class ExpGui_WarpList.elements.discard_edit_button.element_data
--- @field warp ExpGui_WarpList.Warp
--- Button which discards an edit, a warp which was never confirmed is removed
--- @class ExpGui_WarpList.elements.discard_edit_button: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.discard_edit_button.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement
Elements.discard_edit_button = Gui.define("warp_list/discard_edit_button")
:draw{
type = "sprite-button",
sprite = "utility/close_black",
tooltip = { "exp-gui_warp-list.tooltip-discard" },
style = "shortcut_bar_button_red",
}
:style(Styles.sprite22)
:element_data{
warp = Gui.from_argument(1),
}
:on_click(function(def, player, discard_edit_button)
--- @cast def ExpGui_WarpList.elements.discard_edit_button
local warp = def.data[discard_edit_button].warp
if warp.new then
Elements.container.remove_warp(warp)
else
Elements.container.set_editing(warp, player, false)
end
end) --[[@as any]]
--- @class ExpGui_WarpList.elements.edit_warp_button.element_data
--- @field warp ExpGui_WarpList.Warp
--- Button which starts editing a warp
--- @class ExpGui_WarpList.elements.edit_warp_button: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.edit_warp_button.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement
Elements.edit_warp_button = Gui.define("warp_list/edit_warp_button")
:draw{
type = "sprite-button",
sprite = "utility/rename_icon",
tooltip = { "exp-gui_warp-list.tooltip-edit-none" },
style = "shortcut_bar_button",
}
:style(Styles.sprite22)
:element_data{
warp = Gui.from_argument(1),
}
:on_click(function(def, player, edit_warp_button)
--- @cast def ExpGui_WarpList.elements.edit_warp_button
Elements.container.set_editing(def.data[edit_warp_button].warp, player, true)
end) --[[@as any]]
--- Refresh the tooltip which lists who is editing the warp
--- @param edit_warp_button LuaGuiElement
--- @param warp ExpGui_WarpList.Warp
function Elements.edit_warp_button.refresh(edit_warp_button, warp)
if next(warp.editing) then
local player_names = table.get_values(warp.editing)
edit_warp_button.hovered_sprite = "utility/warning_icon"
edit_warp_button.tooltip = { "exp-gui_warp-list.tooltip-edit", table.concat(player_names, ", ") }
else
edit_warp_button.hovered_sprite = "utility/rename_icon"
edit_warp_button.tooltip = { "exp-gui_warp-list.tooltip-edit-none" }
end
end
--- @class ExpGui_WarpList.elements.remove_warp_button.element_data
--- @field warp ExpGui_WarpList.Warp
--- Button which removes a warp along with its area and map tag
--- @class ExpGui_WarpList.elements.remove_warp_button: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.remove_warp_button.element_data>
--- @overload fun(parent: LuaGuiElement, warp: ExpGui_WarpList.Warp): LuaGuiElement
Elements.remove_warp_button = Gui.define("warp_list/remove_warp_button")
:draw{
type = "sprite-button",
sprite = "utility/trash",
tooltip = { "exp-gui_warp-list.tooltip-remove" },
style = "shortcut_bar_button_red",
}
:style(Styles.sprite22)
:element_data{
warp = Gui.from_argument(1),
}
:on_click(function(def, _, remove_warp_button)
--- @cast def ExpGui_WarpList.elements.remove_warp_button
Elements.container.remove_warp(def.data[remove_warp_button].warp)
end) --[[@as any]]
--- @class ExpGui_WarpList.elements.warp_table.row
--- @field warp ExpGui_WarpList.Warp
--- @field icon_flow LuaGuiElement
--- @field name_flow LuaGuiElement
--- @field action_flow LuaGuiElement
--- @field warp_button LuaGuiElement
--- @field icon_picker LuaGuiElement
--- @field status_label LuaGuiElement
--- @field name_label LuaGuiElement
--- @field name_field LuaGuiElement
--- @field confirm_edit_button LuaGuiElement
--- @field discard_edit_button LuaGuiElement
--- @field edit_warp_button LuaGuiElement
--- @field remove_warp_button LuaGuiElement
--- @class ExpGui_WarpList.elements.warp_table.element_data
--- @field rows table<number, ExpGui_WarpList.elements.warp_table.row> Keyed by warp id
--- Table with a row of icon, name, and actions for each warp of the player's force
--- @class ExpGui_WarpList.elements.warp_table: ExpElement
--- @field data table<LuaGuiElement, ExpGui_WarpList.elements.warp_table.element_data>
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.warp_table = Gui.define("warp_list/warp_table")
:track_all_elements()
:element_data{ rows = {} }
:draw(function(_, parent)
local warp_table = Gui.elements.scroll_table(parent, 250, 3, "scroll")
-- Always showing the scrollbar stops the gui changing width as warps are added
assert(warp_table.parent).vertical_scroll_policy = "always"
return warp_table
end)
:style{
top_cell_padding = 3,
bottom_cell_padding = 3,
} --[[@as any]]
--- Add a row for a warp
--- @param warp_table LuaGuiElement
--- @param warp ExpGui_WarpList.Warp
--- @return ExpGui_WarpList.elements.warp_table.row
function Elements.warp_table.add_row(warp_table, warp)
local icon_flow = warp_table.add{ type = "flow" }
icon_flow.style.padding = 0
local warp_button = Elements.warp_button(icon_flow, warp)
local icon_picker = Elements.icon_picker(icon_flow, warp)
local name_flow = warp_table.add{ type = "flow" }
name_flow.style.padding = 0
local status_label = Elements.status_label(name_flow)
local name_label = Elements.name_label(name_flow, warp)
local name_field = Elements.name_field(name_flow, warp, icon_picker)
local action_flow = warp_table.add{ type = "flow" }
action_flow.style.padding = 0
local confirm_edit_button = Elements.confirm_edit_button(action_flow, warp, name_field, icon_picker)
local discard_edit_button = Elements.discard_edit_button(action_flow, warp)
local edit_warp_button = Elements.edit_warp_button(action_flow, warp)
local remove_warp_button = Elements.remove_warp_button(action_flow, warp)
local row = {
warp = warp,
icon_flow = icon_flow,
name_flow = name_flow,
action_flow = action_flow,
warp_button = warp_button,
icon_picker = icon_picker,
status_label = status_label,
name_label = name_label,
name_field = name_field,
confirm_edit_button = confirm_edit_button,
discard_edit_button = discard_edit_button,
edit_warp_button = edit_warp_button,
remove_warp_button = remove_warp_button,
}
Elements.warp_table.data[warp_table].rows[warp.id] = row
return row
end
--- Remove the row for a warp
--- @param warp_table LuaGuiElement
--- @param warp ExpGui_WarpList.Warp
function Elements.warp_table.remove_row(warp_table, warp)
local rows = Elements.warp_table.data[warp_table].rows
local row = rows[warp.id]
if not row then return end
rows[warp.id] = nil
row.icon_flow.destroy()
row.name_flow.destroy()
row.action_flow.destroy()
end
--- Refresh whether a warp can be used from where the player is
--- @param row ExpGui_WarpList.elements.warp_table.row
--- @param status ExpGui_WarpList.PlayerStatus
function Elements.warp_table._refresh_row_status(row, status)
local warp = row.warp
local in_range = status.in_range
local position = warp.position
local tooltip, icon, enabled
if not in_range then
if status.bypass_proximity then
tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-bypass", position.x, position.y }, status_icons.bypass, true
else
tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-disabled" }, status_icons.not_available, false
end
elseif in_range == warp then
tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-same-warp" }, status_icons.current, false
elseif status.on_cooldown then
tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-cooldown" }, status_icons.cooldown, false
else
-- Warps are connected when their poles share an electric network, missing poles never match
local network_id = warp.electric_pole and warp.electric_pole.valid and warp.electric_pole.electric_network_id or -1
local in_range_network_id = in_range.electric_pole and in_range.electric_pole.valid and in_range.electric_pole.electric_network_id or -2
if network_id == in_range_network_id then
tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto", position.x, position.y }, status_icons.connected, true
elseif status.bypass_proximity then
tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-bypass-different-network", position.x, position.y }, status_icons.bypass, true
else
tooltip, icon, enabled = { "exp-gui_warp-list.tooltip-goto-different-network" }, status_icons.different, false
end
end
row.warp_button.tooltip = tooltip
row.warp_button.enabled = enabled
row.status_label.tooltip = tooltip
row.status_label.caption = icon
row.name_label.style.font = enabled and "default-semibold" or "default"
end
--- Refresh a row to match its warp, the player's permissions, and where the player is
--- @param warp_table LuaGuiElement
--- @param row ExpGui_WarpList.elements.warp_table.row
--- @param status ExpGui_WarpList.PlayerStatus
function Elements.warp_table.refresh_row(warp_table, row, status)
local player = Gui.get_player(warp_table)
local warp = row.warp
local can_edit = has_permission_edit_warp(player, warp)
local editing = warp.editing[player.index] ~= nil
Elements.edit_warp_button.refresh(row.edit_warp_button, warp)
Elements.name_label.refresh(row.name_label, warp)
row.warp_button.visible = not editing
row.icon_picker.visible = editing
row.name_label.visible = not editing
row.name_field.visible = editing
row.confirm_edit_button.visible = editing
row.discard_edit_button.visible = editing
row.edit_warp_button.visible = can_edit and not editing
row.remove_warp_button.visible = can_edit and not editing
if editing then
row.name_field.focus()
assert(warp_table.parent).scroll_to_element(row.name_field, "top-third")
else
row.warp_button.sprite = icon_sprite(warp.icon)
row.icon_picker.elem_value = warp.icon
row.name_field.text = warp.name
end
Elements.warp_table._refresh_row_status(row, status)
end
--- Rebuild every row in the order of the warps
--- @param warp_table LuaGuiElement
--- @param warps ExpGui_WarpList.Warp[]
--- @param status ExpGui_WarpList.PlayerStatus
function Elements.warp_table.rebuild(warp_table, warps, status)
warp_table.clear()
Elements.warp_table.data[warp_table].rows = {}
for _, warp in ipairs(warps) do
local row = Elements.warp_table.add_row(warp_table, warp)
Elements.warp_table.refresh_row(warp_table, row, status)
end
end
--- Refresh the row of one warp for the online players of its force
--- @param warp ExpGui_WarpList.Warp
function Elements.warp_table.refresh_row_force(warp)
for player, warp_table in Elements.warp_table:online_elements(warp.force) do
--- @cast player -nil
local row = Elements.warp_table.data[warp_table].rows[warp.id]
if row then
Elements.warp_table.refresh_row(warp_table, row, Elements.container.get_status(player))
end
end
end
--- Refresh whether each warp can be used from where the player is
--- @param player LuaPlayer
function Elements.warp_table.refresh_status_player(player)
local status = Elements.container.get_status(player)
for _, warp_table in Elements.warp_table:tracked_elements(player) do
for _, row in pairs(Elements.warp_table.data[warp_table].rows) do
Elements.warp_table._refresh_row_status(row, status)
end
end
end
--- Rebuild the rows for a player, used when their force or permissions change
--- @param player LuaPlayer
function Elements.warp_table.rebuild_player(player)
local warps = Elements.container.get_warps(player.force --[[@as LuaForce]])
local status = Elements.container.get_status(player)
for _, warp_table in Elements.warp_table:tracked_elements(player) do
Elements.warp_table.rebuild(warp_table, warps, status)
end
end
--- Rebuild the rows for the online players of a force, used when the warps change
--- @param force LuaForce
function Elements.warp_table.rebuild_force(force)
local warps = Elements.container.get_warps(force)
for player, warp_table in Elements.warp_table:online_elements(force) do
--- @cast player -nil
Elements.warp_table.rebuild(warp_table, warps, Elements.container.get_status(player))
end
end
--- Progress bar which fills as the warp cooldown runs out
--- @class ExpGui_WarpList.elements.cooldown_bar: ExpElement
--- @overload fun(parent: LuaGuiElement): LuaGuiElement
Elements.cooldown_bar = Gui.define("warp_list/cooldown_bar")
:track_all_elements()
:draw{
type = "progressbar",
value = 1,
tooltip = { "exp-gui_warp-list.tooltip-cooldown-none", config.cooldown_duration },
}
:style{
horizontally_stretchable = true,
color = Colors.light_blue,
} --[[@as any]]
--- Refresh the bar to show the ticks left on the cooldown
--- @param cooldown_bar LuaGuiElement
--- @param cooldown number
function Elements.cooldown_bar.refresh(cooldown_bar, cooldown)
if cooldown > 0 then
cooldown_bar.value = 1 - cooldown / (config.cooldown_duration * 60)
cooldown_bar.tooltip = { "exp-gui_warp-list.tooltip-cooldown", ceil(cooldown / 60) }
else
cooldown_bar.value = 1
cooldown_bar.tooltip = { "exp-gui_warp-list.tooltip-cooldown-none", config.cooldown_duration }
end
end
--- Refresh the bars of a player
--- @param player LuaPlayer
--- @param cooldown number
function Elements.cooldown_bar.refresh_player(player, cooldown)
for _, cooldown_bar in Elements.cooldown_bar:tracked_elements(player) do
Elements.cooldown_bar.refresh(cooldown_bar, cooldown)
end
end
--- @class ExpGui_WarpList.elements.container.elements
--- @field frame LuaGuiElement
--- @field add_warp_button LuaGuiElement
--- @field warp_table LuaGuiElement
--- @field cooldown_bar LuaGuiElement
--- @class ExpGui_WarpList.elements.container.force_data
--- @field warps ExpGui_WarpList.Warp[] The spawn warp first, then by name
--- @field spawn ExpGui_WarpList.Warp?
--- @field next_warp_id number
--- @class ExpGui_WarpList.elements.container.player_data
--- @field cooldown number Ticks until the player can warp again
--- @field in_range ExpGui_WarpList.Warp? The warp the player is standing on
--- @field blocking ExpGui_WarpList.Warp? The warp too close for another to be added
--- @field keep_open boolean True once the player toggles the list themselves, which stops it hiding when they leave a warp
--- @class ExpGui_WarpList.elements.container.data
--- @field [LuaGuiElement] ExpGui_WarpList.elements.container.elements
--- @field [LuaForce] ExpGui_WarpList.elements.container.force_data
--- @field [LuaPlayer] ExpGui_WarpList.elements.container.player_data
--- Container added to the left gui flow
--- @class ExpGui_WarpList.elements.container: ExpElement
--- @field data ExpGui_WarpList.elements.container.data
Elements.container = Gui.define("warp_list/container")
:draw(function(def, parent)
--- @cast def ExpGui_WarpList.elements.container
local player = Gui.get_player(parent)
local allow_add_warp = has_config_permission(player, "allow_add_warp")
local container = Gui.elements.container(parent, allow_add_warp and 268 or 220)
local header = Gui.elements.header(container, {
caption = { "exp-gui_warp-list.caption-main" },
tooltip = {
"exp-gui_warp-list.tooltip-sub",
config.cooldown_duration,
config.standard_proximity_radius,
{ "exp-gui_warp-list.tooltip-sub-current", status_icons.current },
{ "exp-gui_warp-list.tooltip-sub-connected", status_icons.connected },
{ "exp-gui_warp-list.tooltip-sub-different", status_icons.different },
{ "exp-gui_warp-list.tooltip-sub-cooldown", status_icons.cooldown },
{ "exp-gui_warp-list.tooltip-sub-not-available", status_icons.not_available },
{ "exp-gui_warp-list.tooltip-sub-bypass", status_icons.bypass },
},
})
local add_warp_button = Elements.add_warp_button(header)
add_warp_button.visible = allow_add_warp
local warp_table = Elements.warp_table(container)
local force = player.force --[[@as LuaForce]]
Elements.warp_table.rebuild(warp_table, def.get_warps(force), def.get_status(player))
local cooldown_bar = Elements.cooldown_bar(container)
Elements.cooldown_bar.refresh(cooldown_bar, def._get_player_data(player).cooldown)
local root = Gui.elements.container.get_root_element(container)
def.data[root] = {
frame = container,
add_warp_button = add_warp_button,
warp_table = warp_table,
cooldown_bar = cooldown_bar,
}
return root
end) --[[@as any]]
--- Get or create the data for a force
--- @param force LuaForce
--- @return ExpGui_WarpList.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 = { warps = {}, next_warp_id = 1 }
Elements.container.data[force] = force_data
end
return force_data
end
--- Get or create the data for a player
--- @param player LuaPlayer
--- @return ExpGui_WarpList.elements.container.player_data
function Elements.container._get_player_data(player)
local player_data = Elements.container.data[player]
if not player_data then
player_data = { cooldown = 0, keep_open = false }
Elements.container.data[player] = player_data
end
return player_data
end
--- Get the warps of a force, the spawn warp first and then by name
--- @param force LuaForce
--- @return ExpGui_WarpList.Warp[]
function Elements.container.get_warps(force)
return Elements.container._get_force_data(force).warps
end
--- Get the spawn warp of a force
--- @param force LuaForce
--- @return ExpGui_WarpList.Warp?
function Elements.container.get_spawn_warp(force)
return Elements.container._get_force_data(force).spawn
end
--- Get what decides which warps a player can use
--- @param player LuaPlayer
--- @return ExpGui_WarpList.PlayerStatus
function Elements.container.get_status(player)
local player_data = Elements.container._get_player_data(player)
return {
in_range = player_data.in_range,
on_cooldown = player_data.cooldown > 0,
bypass_proximity = has_config_permission(player, "bypass_warp_proximity"),
}
end
--- Keep the warps of a force in display order
--- @param force_data ExpGui_WarpList.elements.container.force_data
function Elements.container._sort_warps(force_data)
local spawn = force_data.spawn
table.sort(force_data.warps, function(a, b)
if a == spawn then return true end
if b == spawn then return false end
if a.name == b.name then return a.id < b.id end
return a.name < b.name
end)
end
--- Add or update the map tag of a warp
--- @param warp ExpGui_WarpList.Warp
function Elements.container._make_tag(warp)
local tag = warp.tag
if tag and tag.valid then
tag.text = "Warp: " .. warp.name
tag.icon = warp.icon
return
end
local position = warp.position
warp.tag = warp.force.add_chart_tag(warp.surface, {
position = { x = position.x + 0.5, y = position.y + 0.5 },
text = "Warp: " .. warp.name,
icon = warp.icon,
})
end
--- Remove the map tag of a warp
--- @param warp ExpGui_WarpList.Warp
function Elements.container._remove_tag(warp)
local tag = warp.tag
if tag and tag.valid then
tag.destroy()
end
warp.tag = nil
end
--- Place the tiles and entities which mark a warp on the ground
--- @param warp ExpGui_WarpList.Warp
function Elements.container._make_area(warp)
local surface = warp.surface
local position = warp.position
warp.old_tile = surface.get_tile(position.x, position.y).name
local tiles = {}
for index, tile in ipairs(area_tiles) do
tiles[index] = { name = tile[1], position = { x = tile[2] + position.x, y = tile[3] + position.y } }
end
surface.set_tiles(tiles)
for _, entity_config in ipairs(area_entities) do
local entity = assert(surface.create_entity{
name = entity_config[1],
position = { x = entity_config[2] + position.x, y = entity_config[3] + position.y },
force = "neutral",
})
entity.destructible = false
entity.health = 0
entity.minable_flag = false
entity.rotatable = false
if entity.type == "electric-pole" then
warp.electric_pole = entity
end
end
end
--- Restore the ground under a warp and remove its entities
--- @param warp ExpGui_WarpList.Warp
function Elements.container._remove_area(warp)
local old_tile = warp.old_tile
if not old_tile then return end
local surface = warp.surface
local position = warp.position
local tiles = {}
for index, tile in ipairs(area_tiles) do
tiles[index] = { name = old_tile, position = { x = tile[2] + position.x, y = tile[3] + position.y } }
end
surface.set_tiles(tiles)
local radius = config.standard_proximity_radius
local area = {
left_top = { x = position.x - radius, y = position.y - radius },
right_bottom = { x = position.x + radius, y = position.y + radius },
}
local entities = surface.find_entities_filtered{ force = "neutral", area = area, name = area_entity_names }
for _, entity in pairs(entities) do
if entity.destructible == false then
entity.destructible = true
entity.die(entity.force)
end
end
-- The area may not be covered by a radar
warp.force.chart(surface, area)
end
--- Add a warp for a force, when added by a player it is new until they confirm their first edit
--- @param force LuaForce
--- @param surface LuaSurface
--- @param position MapPosition.struct
--- @param player LuaPlayer?
--- @param name string?
--- @return ExpGui_WarpList.Warp
function Elements.container.add_warp(force, surface, position, player, name)
local force_data = Elements.container._get_force_data(force)
local id = force_data.next_warp_id
force_data.next_warp_id = id + 1
--- @type ExpGui_WarpList.Warp
local warp = {
id = id,
force = force,
name = name or "New warp",
icon = { type = default_icon.type, name = default_icon.name },
surface = surface,
position = { x = floor(position.x), y = floor(position.y) },
last_user = player,
last_edit_tick = game.tick,
editing = {},
}
-- Warps added by a player start in edit mode, discarding that edit removes them again
if player then
warp.new = true
Elements.container._make_area(warp)
end
force_data.warps[#force_data.warps + 1] = warp
Elements.container._sort_warps(force_data)
Elements.container._make_tag(warp)
Elements.warp_table.rebuild_force(force)
return warp
end
--- Remove a warp along with its area and map tag
--- @param warp ExpGui_WarpList.Warp
function Elements.container.remove_warp(warp)
local force_data = Elements.container._get_force_data(warp.force)
table.remove_element(force_data.warps, warp)
if force_data.spawn == warp then
force_data.spawn = nil
end
Elements.container._remove_tag(warp)
Elements.container._remove_area(warp)
-- Offline players are corrected by their next proximity update
for _, player in pairs(warp.force.connected_players) do
local player_data = Elements.container._get_player_data(player)
if player_data.in_range == warp then player_data.in_range = nil end
if player_data.blocking == warp then player_data.blocking = nil end
end
for _, warp_table in Elements.warp_table:online_elements(warp.force) do
Elements.warp_table.remove_row(warp_table, warp)
end
end
--- Save a new name and icon for a warp, which also ends the player's edit
--- @param warp ExpGui_WarpList.Warp
--- @param name string
--- @param icon SignalID
--- @param player LuaPlayer
function Elements.container.update_warp(warp, name, icon, player)
warp.name = name
warp.icon = icon
warp.last_user = player
warp.last_edit_tick = game.tick
warp.editing[player.index] = nil
warp.new = nil
Elements.container._sort_warps(Elements.container._get_force_data(warp.force))
Elements.container._make_tag(warp)
Elements.warp_table.rebuild_force(warp.force)
end
--- Start or stop a player editing a warp
--- @param warp ExpGui_WarpList.Warp
--- @param player LuaPlayer
--- @param editing boolean
function Elements.container.set_editing(warp, player, editing)
warp.editing[player.index] = editing and player.name or nil
Elements.warp_table.refresh_row_force(warp)
end
--- Make a warp the spawn of its force, the force respawns there
--- @param warp ExpGui_WarpList.Warp
function Elements.container.set_spawn_warp(warp)
local force_data = Elements.container._get_force_data(warp.force)
force_data.spawn = warp
Elements.container._sort_warps(force_data)
warp.force.set_spawn_position(warp.position, warp.surface)
end
--- Move a player to a warp and start their cooldown
--- @param player LuaPlayer
--- @param warp ExpGui_WarpList.Warp
function Elements.container.teleport_player(player, warp)
local surface = warp.surface
local position = { x = warp.position.x + 0.5, y = warp.position.y + 0.5 }
local vehicle = player.vehicle
if vehicle then
local vehicle_position = surface.find_non_colliding_position(vehicle.name, position, 32, 1)
-- Only cars can be teleported between surfaces
if vehicle.type == "car" then
vehicle.teleport(vehicle_position, surface)
elseif surface == vehicle.surface then
if not vehicle.teleport(vehicle_position) then
player.driving = false
player.teleport(surface.find_non_colliding_position("character", position, 32, 1), surface)
end
end
else
player.teleport(surface.find_non_colliding_position("character", position, 32, 1), surface)
end
local player_data = Elements.container._get_player_data(player)
if not has_config_permission(player, "bypass_warp_cooldown") then
player_data.cooldown = config.cooldown_duration * 60
Elements.cooldown_bar.refresh_player(player, player_data.cooldown)
end
player_data.in_range = warp
Elements.warp_table.refresh_status_player(player)
end
--- Refresh the parts of a player's gui which depend on their permissions
--- @param player LuaPlayer
function Elements.container.refresh_player(player)
local allow_add_warp = has_config_permission(player, "allow_add_warp")
for _, container in Elements.container:tracked_elements(player) do
local elements = Elements.container.data[container]
elements.frame.style.minimal_width = allow_add_warp and 268 or 220
elements.add_warp_button.visible = allow_add_warp
end
Elements.warp_table.rebuild_player(player)
end
--- Count down the cooldown of a player
--- @param player LuaPlayer
--- @param player_data ExpGui_WarpList.elements.container.player_data
function Elements.container._update_cooldown(player, player_data)
if player_data.cooldown == 0 then return end
local cooldown = player_data.cooldown - update_interval
if cooldown < 0 then cooldown = 0 end
player_data.cooldown = cooldown
Elements.cooldown_bar.refresh_player(player, cooldown)
if cooldown == 0 then
Elements.warp_table.refresh_status_player(player)
end
end
--- Find which warp a player is standing on and whether one can be added where they are
--- @param player LuaPlayer
--- @param player_data ExpGui_WarpList.elements.container.player_data
function Elements.container._update_proximity(player, player_data)
local force_data = Elements.container._get_force_data(player.force --[[@as LuaForce]])
local surface = player.physical_surface
local position = player.physical_position
local px, py = position.x, position.y
local closest, closest_distance = nil, nil --- @type ExpGui_WarpList.Warp?, number?
for _, warp in ipairs(force_data.warps) do
if warp.surface == surface then
local dx, dy = px - warp.position.x, py - warp.position.y
local distance = dx * dx + dy * dy
if not closest_distance or distance < closest_distance then
closest, closest_distance = warp, distance
end
end
end
local in_range = nil --- @type ExpGui_WarpList.Warp?
if closest and closest_distance < (closest == force_data.spawn and config.spawn_proximity_radius or config.standard_proximity_radius) ^ 2 then
in_range = closest
end
if in_range ~= player_data.in_range then
player_data.in_range = in_range
if not player_data.keep_open then
Gui.toolbar.set_left_element_visible_state(Elements.container, player, in_range ~= nil)
end
Elements.warp_table.refresh_status_player(player)
end
local blocking = nil --- @type ExpGui_WarpList.Warp?
if closest and closest_distance <= config.minimum_distance ^ 2 then
blocking = closest
end
if blocking ~= player_data.blocking then
player_data.blocking = blocking
Elements.add_warp_button.refresh_player_blocking(player, blocking)
end
end
--- Add the element to the left flow with a toolbar button
Gui.add_left_element(Elements.container, false)
Gui.toolbar.create_button{
name = "toggle_warp_list",
left_element = Elements.container,
sprite = default_icon.type .. "/" .. default_icon.name,
tooltip = { "exp-gui_warp-list.tooltip-main" },
visible = function(player, element)
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.warp_list")
end
}:on_click(function(def, player)
-- A list the player opened themselves stays open when they leave a warp
Elements.container._get_player_data(player).keep_open = Gui.toolbar.get_button_toggled_state(def, player)
end)
--- Give a force a spawn warp when its first player is created
--- @param event EventData.on_player_created
local function on_player_created(event)
local player = Gui.get_player(event)
local force = player.force --[[@as LuaForce]]
if Elements.container.get_spawn_warp(force) then return end
local surface = player.physical_surface
local position = force.get_spawn_position(surface)
local warp = Elements.container.add_warp(force, surface, position, nil, "Spawn")
Elements.container.set_spawn_warp(warp)
-- Spawn has no warp area, so any pole nearby connects it to the network
local poles = surface.find_entities_filtered{ type = "electric-pole", position = position, radius = 20, limit = 1 }
warp.electric_pole = poles[1]
end
--- Rebuild the gui for a player as it may be outdated
--- @param event EventData.on_player_joined_game | EventData.on_player_changed_force
local function refresh_player_warps(event)
local player = Gui.get_player(event)
Elements.container.refresh_player(player)
Elements.cooldown_bar.refresh_player(player, Elements.container._get_player_data(player).cooldown)
end
--- Rebuild the gui for a player as their permissions may have changed
--- @param event EventData.ExpRoles.on_player_roles_changed
local function refresh_player_permissions(event)
Elements.container.refresh_player(Gui.get_player(event))
end
--- Restore the map tag of a warp when a player edits or removes it
--- @param event EventData.on_chart_tag_modified | EventData.on_chart_tag_removed
local function on_chart_tag_changed(event)
if not event.player_index then return end
for _, warp in ipairs(Elements.container.get_warps(event.force)) do
local tag = warp.tag
if not tag or not tag.valid or tag == event.tag then
if event.name == defines.events.on_chart_tag_removed then
warp.tag = nil
end
Elements.container._make_tag(warp)
end
end
end
--- Count down cooldowns and check where each player is
local function update_players()
for _, player in pairs(game.connected_players) do
local player_data = Elements.container._get_player_data(player)
Elements.container._update_cooldown(player, player_data)
Elements.container._update_proximity(player, player_data)
end
end
local e = defines.events
return feature:guard{
elements = Elements,
events = {
[e.on_player_created] = on_player_created,
[e.on_player_joined_game] = refresh_player_warps,
[e.on_player_changed_force] = refresh_player_warps,
[e.on_chart_tag_modified] = on_chart_tag_changed,
[e.on_chart_tag_removed] = on_chart_tag_changed,
[Roles.events.on_player_roles_changed] = refresh_player_permissions,
},
on_nth_tick = {
[update_interval] = update_players,
}
}