Move files to exp_legacy

This commit is contained in:
Cooldude2606
2024-09-23 15:55:28 +01:00
parent 446e87b610
commit 65145b5d34
266 changed files with 73 additions and 0 deletions

View File

@@ -0,0 +1,364 @@
--[[-- Gui Module - Autofill
- Adds a button to enable Autofill
@gui Autofill
@alias autofill
]]
local Game = require 'utils.game' -- @dep utils.game
local Gui = require 'expcore.gui' -- @dep expcore.gui
local Roles = require 'expcore.roles' -- @dep expcore.gui
local Global = require 'utils.global' -- @dep utils.global
local config = require 'config.gui.autofill' -- @dep config.gui.autofill
local Event = require 'utils.event' -- @dep utils.event
local table = require 'overrides.table' -- @dep overrides.table
local print_text = Game.print_floating_text -- (surface, position, text, color)
--- Table that stores if autofill is enabled or not
local autofill_player_settings = {}
Global.register(autofill_player_settings, function(tbl)
autofill_player_settings = tbl
end)
local autofill_container
local function rich_img(type, value)
return '[img='..type..'/'..value..']'
end
--- Toggle entity section visibility
-- @element toggle_item_button
local toggle_section =
Gui.element{
type = 'sprite-button',
sprite = 'utility/expand_dark',
hovered_sprite = 'utility/expand',
tooltip = {'autofill.toggle-section-tooltip'},
name = Gui.unique_static_name
}
:style(Gui.sprite_style(20))
:on_click(function(_, element, _)
local header_flow = element.parent
local flow_name = header_flow.caption
local flow = header_flow.parent.parent[flow_name]
if Gui.toggle_visible_state(flow) then
element.sprite = 'utility/collapse_dark'
element.hovered_sprite = 'utility/collapse'
element.tooltip = {'autofill.toggle-section-collapse-tooltip'}
else
element.sprite = 'utility/expand_dark'
element.hovered_sprite = 'utility/expand'
element.tooltip = {'autofill.toggle-section-tooltip'}
end
end)
--- Toggle enitity button, used for toggling autofill for the specific entity
-- All entity autofill settings will be ignored if its disabled
-- @element entity_toggle
local entity_toggle =
Gui.element(function(_, parent, entity_name)
return parent.add{
type = 'sprite-button',
sprite = 'utility/confirm_slot',
tooltip = {'autofill.toggle-entity-tooltip', rich_img('item', entity_name)},
style = 'shortcut_bar_button_green'
}
end)
:style(Gui.sprite_style(22))
:on_click(function(player, element, _)
local entity_name = string.match(element.parent.parent.name,'(.*)%-header')
if not autofill_player_settings[player.name] then return end
local setting = autofill_player_settings[player.name][entity_name]
if not setting then return end
if setting.enabled then
setting.enabled = false
element.sprite = 'utility/close_black'
element.style = 'shortcut_bar_button_red'
else
setting.enabled = true
element.sprite = 'utility/confirm_slot'
element.style = 'shortcut_bar_button_green'
end
-- Correct the button size
local style = element.style
style.padding = -2
style.height = 22
style.width = 22
end)
--- Draw a section header and main scroll
-- @element autofill_section_container
local section =
Gui.element(function(definition, parent, section_name, table_size)
-- Draw the header for the section
local header = Gui.header(
parent,
{'autofill.toggle-section-caption', rich_img('item', section_name), {'entity-name.'..section_name}},
{'autofill.toggle-section-tooltip'},
true,
section_name..'-header'
)
definition:triggers_events(header.parent.header_label)
-- Right aligned button to toggle the section
header.caption = section_name
entity_toggle(header, section_name)
toggle_section(header)
local section_table = parent.add{
type = 'table',
name = section_name,
column_count = table_size
}
section_table.visible = false
return definition:no_events(section_table)
end)
:on_click(function(_, element, event)
event.element = element.parent.alignment[toggle_section.name]
toggle_section:raise_event(event)
end)
--- Toggle item button, used for toggling autofill for the specific item
-- @element toggle_item_button
local toggle_item_button =
Gui.element(function(_, parent, item)
return parent.add{
type = 'sprite-button',
sprite = 'item/'..item.name,
tooltip = {'autofill.toggle-tooltip', rich_img('item', item.name), item.category},
style = 'shortcut_bar_button_red'
}
end)
:style(Gui.sprite_style(32, nil, { right_margin = -3 }))
:on_click(function(player, element)
local item_name = element.parent.tooltip
local entity_name = element.parent.parent.parent.name
if not autofill_player_settings[player.name] then return end
local setting = autofill_player_settings[player.name][entity_name]
if not setting then return end
local item = setting.items[item_name]
if not item then return end
if item.enabled then
item.enabled = false
element.style = 'shortcut_bar_button_red'
else
item.enabled = true
element.style = 'shortcut_bar_button_green'
end
-- Correct the button size
local style = element.style
style.right_margin = -3
style.padding = -2
style.height = 32
style.width = 32
end)
--- Amount text field for a autofill item
-- @element amount_textfield
local amount_textfield =
Gui.element(function(_, parent, item)
return parent.add{
type = 'textfield',
text = item.amount,
tooltip = {'autofill.amount-tooltip', item.category },
clear_and_focus_on_right_click = true,
numeric = true,
allow_decimal = false,
allow_negative = false
}
end)
:style{
maximal_width = 40,
height = 31,
padding = -2
}
:on_text_changed(function(player, element, _)
local value = tonumber(element.text)
if not value then value = 0 end
local clamped = math.clamp(value, 0, 1000)
local item_name = element.parent.tooltip
local entity_name = element.parent.parent.parent.name
if not autofill_player_settings[player.name] then return end
local setting = autofill_player_settings[player.name][entity_name]
if not setting then return end
local item = setting.items[item_name]
if not item then return end
item.amount = clamped
if clamped ~= value then
element.text = clamped
player.print{'autofill.invalid', item.amount, rich_img('item', item.name), rich_img('entity', entity_name) }
return
end
end)
--- Autofill setting, contains a button and a textbox
-- @element add_autofill_setting
local add_autofill_setting =
Gui.element(function(_, parent, item)
local toggle_flow = parent.add{ type = 'flow', name = 'toggle-setting-'..item.name, tooltip = item.name }
local amount_flow = parent.add{ type = 'flow', name = 'amount-setting-'..item.name, tooltip = item.name }
toggle_flow.style.padding = 0
amount_flow.style.padding = 0
toggle_item_button(toggle_flow, item)
amount_textfield(amount_flow, item)
end)
--- Autofill setting empty, contains filler button and textfield gui elements
-- @element add_empty_autofill_setting
local add_empty_autofill_setting =
Gui.element(function(_, parent)
local toggle_element = parent.add{
type = 'sprite-button'
}
toggle_element.style.right_margin = -3
toggle_element.style.width = 32
toggle_element.style.height = 32
toggle_element.enabled = false
local amount_element = parent.add{
type = 'textfield'
}
amount_element.style.maximal_width = 40
amount_element.style.height = 31
amount_element.style.padding = -2
amount_element.enabled = false
end)
--- Main gui container for the left flow
-- @element autofill_container
autofill_container =
Gui.element(function(definition, parent)
-- Draw the internal container
local container = Gui.container(parent, definition.name)
-- Draw the scroll container
local scroll_table = Gui.scroll_table(container, 400, 1, 'autofill-scroll-table')
-- Set the scroll panel to always show the scrollbar (not doing this will result in a changing gui size)
scroll_table.parent.vertical_scroll_policy = 'always'
-- Scroll panel has by default padding
scroll_table.parent.style.padding = 0
-- Remove the default gap that is added in a table between elements
scroll_table.style.vertical_spacing = 0
-- Center the first collumn in the table
scroll_table.style.column_alignments[1] = 'center'
-- Loop over each default entity config
for _, setting in pairs(config.default_entities) do
local table_sizes = {}
local tables = {}
-- Draw a section for the element
local entity_table = section(scroll_table, setting.entity, 3)
-- Add some padding around the table
entity_table.style.padding = 3
-- Make sure each collumn is alignment top center
entity_table.style.column_alignments[1] = 'top-center'
entity_table.style.column_alignments[2] = 'top-center'
entity_table.style.column_alignments[3] = 'top-center'
-- Loop over each item category
for _, category in pairs(config.categories) do
if not table_sizes[category] then table_sizes[category] = 0 end
-- Draw table
local category_table = entity_table.add{
type = 'table',
name = category..'-category',
column_count = 2
}
-- Add padding between each item
category_table.style.vertical_spacing = 1
tables[category] = category_table
-- Add item autofill setting gui elements to the table
for _, item in pairs(setting.items) do
if item.category == category then
add_autofill_setting(category_table, item)
table_sizes[category] = table_sizes[category] + 1
end
end
end
-- Add empty gui elements for the categories with less items than the other categories
local t = table.get_values(table_sizes)
table.sort(t)
local biggest = t[#t]
for category, size in pairs(table_sizes) do
for i=biggest-size,1,-1 do
add_empty_autofill_setting(tables[category])
end
end
end
-- Return the external container
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
--- Button on the top flow used to toggle autofill container
-- @element autofill_toggle
Gui.left_toolbar_button(config.icon, {'autofill.main-tooltip'}, autofill_container, function(player)
return Roles.player_allowed(player, 'gui/autofill')
end)
--- When a player is created make sure they have the default autofill settings
Event.add(defines.events.on_player_created, function(event)
local player = game.players[event.player_index]
if not autofill_player_settings[player.name] then
autofill_player_settings[player.name] = table.deep_copy(config.default_entities)
end
end)
local function entity_build(event)
-- Check if player exists
local player = game.players[event.player_index]
if not player then
return
end
-- Check if the entity is in the config and enabled
local entity = event.created_entity
-- Check if player has settings
if not autofill_player_settings[player.name] then return end
local entity_settings = autofill_player_settings[player.name][entity.name]
-- Check if autofill for the entity is enabled
if not entity_settings then return end
if not entity_settings.enabled then return end
-- Get the inventory of the player
local player_inventory = player.get_main_inventory()
local text_position = { x = entity.position.x, y = entity.position.y }
-- Loop over all possible items to insert into the entity
for _, item in pairs(entity_settings.items) do
-- Check if the item is enabled or goto next item
if not item.enabled then goto end_item end
-- Get the inventory of the entity or goto next item
local entity_inventory = entity.get_inventory(item.inv)
if not entity_inventory then goto end_item end
local preferd_amount = item.amount
local item_amount = player_inventory.get_item_count(item.name)
if item_amount ~= 0 then
local inserted
text_position.y = text_position.y - 0.5
local color = { r = 0, g = 255, b = 0, a = 1}
if item_amount >= preferd_amount then
-- Can item be inserted? no, goto next item!
if not entity_inventory.can_insert{name=item.name, count=preferd_amount} then
goto end_item
end
inserted = entity_inventory.insert{name=item.name, count=preferd_amount}
else
inserted = entity_inventory.insert{name=item.name, count=item_amount}
color = { r = 255, g = 165, b = 0, a = 1}
end
player_inventory.remove{name=item.name, count=inserted}
print_text(entity.surface, text_position, {'autofill.inserted', inserted, rich_img('item', item.name), rich_img('entity', entity.name) }, color)
end
::end_item::
end
end
Event.add(defines.events.on_built_entity, entity_build)

View File

@@ -0,0 +1,392 @@
--[[-- Gui Module - Bonus
@gui Bonus
@alias bonus_container
]]
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Event = require 'utils.event' --- @dep utils.event
local config = require 'config.bonus' --- @dep config.bonus
local vlayer = require 'modules.control.vlayer'
local format_number = require('util').format_number --- @dep util
local bonus_container
local function bonus_gui_pts_needed(player)
local frame = Gui.get_left_element(player, bonus_container)
local disp = frame.container['bonus_st_2'].disp.table
local total = 0
for k, v in pairs(config.conversion) do
total = total + (disp['bonus_display_' .. k .. '_slider'].slider_value / config.player_bonus[v].cost_scale * config.player_bonus[v].cost)
end
total = total + (disp['bonus_display_personal_battery_recharge_slider'].slider_value / config.player_special_bonus['personal_battery_recharge'].cost_scale * config.player_special_bonus['personal_battery_recharge'].cost)
return total
end
local function apply_bonus(player)
if not Roles.player_allowed(player, 'gui/bonus') then
for k, v in pairs(config.player_bonus) do
player[k] = 0
if v.combined_bonus then
for i=1, #v.combined_bonus do
player[v.combined_bonus[i]] = 0
end
end
end
return
end
if not player.character then
return
end
local frame = Gui.get_left_element(player, bonus_container)
local disp = frame.container['bonus_st_2'].disp.table
for k, v in pairs(config.conversion) do
player[v] = disp['bonus_display_' .. k .. '_slider'].slider_value
if config.player_bonus[v].combined_bonus then
for i=1, #config.player_bonus[v].combined_bonus do
player[config.player_bonus[v].combined_bonus[i]] = disp['bonus_display_' .. k .. '_slider'].slider_value
end
end
end
end
local function apply_periodic_bonus(player)
if not Roles.player_allowed(player, 'gui/bonus') then
return
end
if not player.character then
return
end
local frame = Gui.get_left_element(player, bonus_container)
local disp = frame.container['bonus_st_2'].disp.table
if vlayer.get_statistics()['energy_sustained'] > 0 then
local armor = player.get_inventory(defines.inventory.character_armor)[1].grid
if armor then
local slider = disp['bonus_display_personal_battery_recharge_slider'].slider_value * 100000 * config.player_special_bonus_rate / 6
for i=1, #armor.equipment do
if armor.equipment[i].energy < armor.equipment[i].max_energy then
local energy_required = math.min(math.floor(armor.equipment[i].max_energy - armor.equipment[i].energy), vlayer.get_statistics()['energy_storage'], slider)
armor.equipment[i].energy = armor.equipment[i].energy + energy_required
vlayer.energy_changed(- energy_required)
slider = slider - energy_required
end
end
end
end
end
--- Control label for the bonus points available
-- @element bonus_gui_control_pts_a
local bonus_gui_control_pts_a =
Gui.element{
type = 'label',
name = 'bonus_control_pts_a',
caption = {'bonus.control-pts-a'},
style = 'heading_2_label'
}:style{
width = config.gui_display_width['half']
}
local bonus_gui_control_pts_a_count =
Gui.element{
type = 'label',
name = 'bonus_control_pts_a_count',
caption = config.pts.base,
style = 'heading_2_label'
}:style{
width = config.gui_display_width['half']
}
--- Control label for the bonus points needed
-- @element bonus_gui_control_pts_n
local bonus_gui_control_pts_n =
Gui.element{
type = 'label',
name = 'bonus_control_pts_n',
caption = {'bonus.control-pts-n'},
style = 'heading_2_label'
}:style{
width = config.gui_display_width['half']
}
local bonus_gui_control_pts_n_count =
Gui.element{
type = 'label',
name = 'bonus_control_pts_n_count',
caption = '0',
style = 'heading_2_label'
}:style{
width =config.gui_display_width['half']
}
--- Control label for the bonus points remaining
-- @element bonus_gui_control_pts_r
local bonus_gui_control_pts_r =
Gui.element{
type = 'label',
name = 'bonus_control_pts_r',
caption = {'bonus.control-pts-r'},
style = 'heading_2_label'
}:style{
width = config.gui_display_width['half']
}
local bonus_gui_control_pts_r_count =
Gui.element{
type = 'label',
name = 'bonus_control_pts_r_count',
caption = '0',
style = 'heading_2_label'
}:style{
width = config.gui_display_width['half']
}
--- A button used for pts calculations
-- @element bonus_gui_control_refresh
local bonus_gui_control_reset =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = {'bonus.control-reset'}
}:style{
width = config.gui_display_width['half']
}:on_click(function(player, element, _)
local frame = Gui.get_left_element(player, bonus_container)
local disp = frame.container['bonus_st_2'].disp.table
for k, v in pairs(config.conversion) do
local s = 'bonus_display_' .. k .. '_slider'
disp[s].slider_value = config.player_bonus[v].value
if config.player_bonus[v].is_percentage then
disp[disp[s].tags.counter].caption = format_number(disp[s].slider_value * 100) .. ' %'
else
disp[disp[s].tags.counter].caption = format_number(disp[s].slider_value)
end
end
local slider = disp['bonus_display_personal_battery_recharge_slider']
slider.slider_value = config.player_special_bonus['personal_battery_recharge'].value
disp[slider.tags.counter].caption = format_number(slider.slider_value)
local r = bonus_gui_pts_needed(player)
element.parent[bonus_gui_control_pts_n_count.name].caption = r
element.parent[bonus_gui_control_pts_r_count.name].caption = tonumber(element.parent[bonus_gui_control_pts_a_count.name].caption) - r
end)
--- A button used for pts apply
-- @element bonus_gui_control_apply
local bonus_gui_control_apply =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = {'bonus.control-apply'}
}:style{
width = config.gui_display_width['half']
}:on_click(function(player, element, _)
local n = bonus_gui_pts_needed(player)
element.parent[bonus_gui_control_pts_n_count.name].caption = n
local r = tonumber(element.parent[bonus_gui_control_pts_a_count.name].caption) - n
element.parent[bonus_gui_control_pts_r_count.name].caption = r
if r >= 0 then
apply_bonus(player)
end
end)
--- A vertical flow containing all the bonus control
-- @element bonus_control_set
local bonus_control_set =
Gui.element(function(_, parent, name)
local bonus_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.scroll_table(bonus_set, config.gui_display_width['half'] * 2, 2, 'disp')
bonus_gui_control_pts_a(disp)
bonus_gui_control_pts_a_count(disp)
bonus_gui_control_pts_n(disp)
bonus_gui_control_pts_n_count(disp)
bonus_gui_control_pts_r(disp)
bonus_gui_control_pts_r_count(disp)
bonus_gui_control_reset(disp)
bonus_gui_control_apply(disp)
return bonus_set
end)
--- Display group
-- @element bonus_gui_slider
local bonus_gui_slider =
Gui.element(function(_definition, parent, name, caption, tooltip, bonus)
local label = parent.add{
type = 'label',
caption = caption,
tooltip = tooltip,
style = 'heading_2_label'
}
label.style.width = config.gui_display_width['label']
local value = bonus.value
if bonus.is_percentage then
value = format_number(value * 100) .. ' %'
else
value = format_number(value)
end
local slider = parent.add{
type = 'slider',
name = name .. '_slider',
value = bonus.value,
maximum_value = bonus.max,
value_step = bonus.scale,
discrete_values = true,
style = 'notched_slider',
tags = {
counter = name .. '_count',
is_percentage = bonus.is_percentage
}
}
slider.style.width = config.gui_display_width['slider']
slider.style.horizontally_stretchable = true
local count = parent.add{
type = 'label',
name = name .. '_count',
caption = value,
style = 'heading_2_label',
}
count.style.width = config.gui_display_width['count']
return slider
end)
:on_value_changed(function(player, element, _event)
if element.tags.is_percentage then
element.parent[element.tags.counter].caption = format_number(element.slider_value * 100) .. ' %'
else
element.parent[element.tags.counter].caption = format_number(element.slider_value)
end
local r = bonus_gui_pts_needed(player)
local frame = Gui.get_left_element(player, bonus_container)
local disp = frame.container['bonus_st_1'].disp.table
disp[bonus_gui_control_pts_n_count.name].caption = r
disp[bonus_gui_control_pts_r_count.name].caption = tonumber(disp[bonus_gui_control_pts_a_count.name].caption) - r
end)
--- A vertical flow containing all the bonus data
-- @element bonus_data_set
local bonus_data_set =
Gui.element(function(_, parent, name)
local bonus_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.scroll_table(bonus_set, config.gui_display_width['half'] * 2, 3, 'disp')
for k, v in pairs(config.conversion) do
bonus_gui_slider(disp, 'bonus_display_' .. k, {'bonus.display-' .. k}, {'bonus.display-' .. k .. '-tooltip'}, config.player_bonus[v])
end
bonus_gui_slider(disp, 'bonus_display_personal_battery_recharge', {'bonus.display-personal-battery-recharge'}, {'bonus.display-personal-battery-recharge-tooltip'}, config.player_special_bonus['personal_battery_recharge'])
return bonus_set
end)
--- The main container for the bonus gui
-- @element bonus_container
bonus_container =
Gui.element(function(definition, parent)
local player = Gui.get_player_from_element(parent)
local container = Gui.container(parent, definition.name, config.gui_display_width['half'] * 2)
bonus_control_set(container, 'bonus_st_1')
bonus_data_set(container, 'bonus_st_2')
local frame = Gui.get_left_element(player, bonus_container)
local disp = frame.container['bonus_st_1'].disp.table
local n = bonus_gui_pts_needed(player)
disp[bonus_gui_control_pts_n_count.name].caption = n
local r = tonumber(disp[bonus_gui_control_pts_a_count.name].caption) - n
disp[bonus_gui_control_pts_r_count.name].caption = r
apply_bonus(player)
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
--- Button on the top flow used to toggle the bonus container
-- @element toggle_left_element
Gui.left_toolbar_button('item/exoskeleton-equipment', {'bonus.main-tooltip'}, bonus_container, function(player)
return Roles.player_allowed(player, 'gui/bonus')
end)
Event.add(defines.events.on_player_created, function(event)
if event.player_index ~= 1 then
return
end
for k, v in pairs(config.force_bonus) do
game.players[event.player_index].force[k] = v.value
end
for k, v in pairs(config.surface_bonus) do
game.players[event.player_index].surface[k] = v.value
end
end)
Event.add(Roles.events.on_role_assigned, function(event)
apply_bonus(game.players[event.player_index])
end)
Event.add(Roles.events.on_role_unassigned, function(event)
apply_bonus(game.players[event.player_index])
end)
--- When a player respawns re-apply bonus
Event.add(defines.events.on_player_respawned, function(event)
local player = game.players[event.player_index]
local frame = Gui.get_left_element(player, bonus_container)
local disp = frame.container['bonus_st_1'].disp.table
local n = bonus_gui_pts_needed(player)
disp[bonus_gui_control_pts_n_count.name].caption = n
local r = tonumber(disp[bonus_gui_control_pts_a_count.name].caption) - n
disp[bonus_gui_control_pts_r_count.name].caption = r
if r >= 0 then
apply_bonus(player)
end
end)
--- When a player dies allow them to have instant respawn
Event.add(defines.events.on_player_died, function(event)
local player = game.players[event.player_index]
if Roles.player_has_flag(player, 'instant-respawn') then
player.ticks_to_respawn = 120
end
end)
Event.on_nth_tick(config.player_special_bonus_rate, function(_)
for _, player in pairs(game.connected_players) do
apply_periodic_bonus(player)
end
end)

View File

@@ -0,0 +1,114 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local Model = require 'modules.gui.debug.model' --- @dep modules.gui.debug.model
local Color = require 'utils.color_presets' --- @dep utils.color_presets
local dump = Model.dump
local Public = {}
local ignore = {
_G = true,
assert = true,
collectgarbage = true,
error = true,
getmetatable = true,
ipairs = true,
load = true,
loadstring = true,
next = true,
pairs = true,
pcall = true,
print = true,
rawequal = true,
rawlen = true,
rawget = true,
rawset = true,
select = true,
setmetatable = true,
tonumber = true,
tostring = true,
type = true,
xpcall = true,
_VERSION = true,
['module'] = true,
require = true,
package = true,
unpack = true,
table = true,
string = true,
bit32 = true,
math = true,
debug = true,
serpent = true,
log = true,
table_size = true,
global = true,
remote = true,
commands = true,
settings = true,
rcon = true,
script = true,
util = true,
mod_gui = true,
game = true,
rendering = true
}
local header_name = Gui.uid_name()
local left_panel_name = Gui.uid_name()
local right_panel_name = Gui.uid_name()
Public.name = '_G'
function Public.show(container)
local main_flow = container.add {type = 'flow', direction = 'horizontal'}
local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name}
local left_panel_style = left_panel.style
left_panel_style.width = 300
for key, value in pairs(_G) do
if not ignore[key] then
local header =
left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = tostring(key)}
Gui.set_data(header, value)
end
end
local right_panel = main_flow.add {type = 'text-box', name = right_panel_name}
right_panel.read_only = true
right_panel.selectable = true
local right_panel_style = right_panel.style
right_panel_style.vertically_stretchable = true
right_panel_style.horizontally_stretchable = true
right_panel_style.maximal_width = 1000
right_panel_style.maximal_height = 1000
Gui.set_data(left_panel, {right_panel = right_panel, selected_header = nil})
end
Gui.on_click(
header_name,
function(event)
local element = event.element
local value = Gui.get_data(element)
local left_panel = element.parent.parent
local left_panel_data = Gui.get_data(left_panel)
local right_panel = left_panel_data.right_panel
local selected_header = left_panel_data.selected_header
if selected_header then
selected_header.style.font_color = Color.white
end
element.style.font_color = Color.orange
left_panel_data.selected_header = element
local content = dump(value)
right_panel.text = content
end
)
return Public

View File

@@ -0,0 +1,169 @@
local Event = require 'utils.event'
local table = require 'overrides.table'
local Gui = require 'utils.gui'
local Model = require 'modules.gui.debug.model'
local format = string.format
local insert = table.insert
local events = defines.events
-- Constants
local events_to_keep = 10
-- Local vars
local Public = {
name = 'Events'
}
local name_lookup = {}
-- GUI names
local checkbox_name = Gui.uid_name()
local filter_name = Gui.uid_name()
local clear_filter_name = Gui.uid_name()
-- global tables
local enabled = {}
local last_events = {}
global.debug_event_view = {
enabled = enabled,
last_events = last_events,
filter = ''
}
function Public.on_open_debug()
local tbl = global.debug_event_view
if tbl then
enabled = tbl.enabled
last_events = tbl.last_events
else
enabled = {}
last_events = {}
global.debug_event_view = {
enabled = enabled,
last_events = last_events
}
end
Public.on_open_debug = nil
end
-- Local functions
local function event_callback(event)
local id = event.name
if not enabled[id] then
return
end
local name = name_lookup[id]
if not last_events[name] then
last_events[name] = {}
end
insert(last_events[name], 1, event)
last_events[name][events_to_keep + 1] = nil
event.name = nil
local str = format('%s (id = %s): %s', name, id, Model.dump(event))
game.print(str)
log(str)
end
local function on_gui_checked_state_changed(event)
local element = event.element
local name = element.caption
local id = events[name]
local state = element.state and true or false
element.state = state
if state then
enabled[id] = true
else
enabled[id] = false
end
end
-- GUI
-- Create a table with events sorted by their names
local grid_builder = {}
for name, _ in pairs(events) do
grid_builder[#grid_builder + 1] = name
end
table.sort(grid_builder)
local function redraw_event_table(gui_table, filter)
for _, event_name in pairs(grid_builder) do
if filter == '' or event_name:find(filter) then
local index = events[event_name]
gui_table.add({type = 'flow'}).add {
name = checkbox_name,
type = 'checkbox',
state = enabled[index] or false,
caption = event_name
}
end
end
end
function Public.show(container)
local filter = global.debug_event_view.filter
local main_frame_flow = container.add({type = 'flow', direction = 'vertical'})
local filter_flow = main_frame_flow.add({type = 'flow', direction = 'horizontal'})
filter_flow.add({type = 'label', caption = 'filter'})
local filter_textfield = filter_flow.add({type = 'textfield', name = filter_name, text = filter})
local clear_button = filter_flow.add({type = 'button', name = clear_filter_name, caption = 'clear'})
local scroll_pane = main_frame_flow.add({type = 'scroll-pane'})
local gui_table = scroll_pane.add({type = 'table', column_count = 3, draw_horizontal_lines = true})
Gui.set_data(filter_textfield, gui_table)
Gui.set_data(clear_button, {gui_table = gui_table, filter_textfield = filter_textfield})
redraw_event_table(gui_table, filter)
end
Gui.on_checked_state_changed(checkbox_name, on_gui_checked_state_changed)
Gui.on_text_changed(
filter_name,
function(event)
local element = event.element
local gui_table = Gui.get_data(element)
local filter = element.text:gsub(' ', '_')
global.debug_event_view.filter = filter
element.text = filter
gui_table.clear()
redraw_event_table(gui_table, filter)
end
)
Gui.on_click(
clear_filter_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local filter_textfield = data.filter_textfield
local gui_table = data.gui_table
filter_textfield.text = ''
global.debug_event_view.filter = ''
gui_table.clear()
redraw_event_table(gui_table, '')
end
)
-- Event registers (TODO: turn to removable hooks.. maybe)
for name, id in pairs(events) do
name_lookup[id] = name
Event.add(id, event_callback)
end
return Public

View File

@@ -0,0 +1,130 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local Datastore = require 'expcore.datastore' --- @dep expcore.datastore
local Color = require 'utils.color_presets' --- @dep utils.color_presets
local Model = require 'modules.gui.debug.model' --- @dep modules.gui.debug.model
local dump = Model.dump
local concat = table.concat
local Public = {}
local header_name = Gui.uid_name()
local left_panel_name = Gui.uid_name()
local right_panel_name = Gui.uid_name()
local input_text_box_name = Gui.uid_name()
local refresh_name = Gui.uid_name()
Public.name = 'Datastore'
function Public.show(container)
local main_flow = container.add {type = 'flow', direction = 'horizontal'}
local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name}
local left_panel_style = left_panel.style
left_panel_style.width = 300
for name in pairs(table.keysort(Datastore.debug())) do
local header = left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = name}
Gui.set_data(header, name)
end
local right_flow = main_flow.add {type = 'flow', direction = 'vertical'}
local right_top_flow = right_flow.add {type = 'flow', direction = 'horizontal'}
local input_text_box = right_top_flow.add {type = 'text-box', name = input_text_box_name}
local input_text_box_style = input_text_box.style
input_text_box_style.horizontally_stretchable = true
input_text_box_style.height = 32
input_text_box_style.maximal_width = 1000
local refresh_button =
right_top_flow.add {type = 'sprite-button', name = refresh_name, sprite = 'utility/reset', tooltip = 'refresh'}
local refresh_button_style = refresh_button.style
refresh_button_style.width = 32
refresh_button_style.height = 32
local right_panel = right_flow.add {type = 'text-box', name = right_panel_name}
right_panel.read_only = true
right_panel.selectable = true
local right_panel_style = right_panel.style
right_panel_style.vertically_stretchable = true
right_panel_style.horizontally_stretchable = true
right_panel_style.maximal_width = 1000
right_panel_style.maximal_height = 1000
local data = {
right_panel = right_panel,
input_text_box = input_text_box,
selected_header = nil
}
Gui.set_data(input_text_box, data)
Gui.set_data(left_panel, data)
Gui.set_data(refresh_button, data)
end
Gui.on_click(
header_name,
function(event)
local element = event.element
local tableName = Gui.get_data(element)
local left_panel = element.parent.parent
local data = Gui.get_data(left_panel)
local right_panel = data.right_panel
local selected_header = data.selected_header
local input_text_box = data.input_text_box
if selected_header then
selected_header.style.font_color = Color.white
end
element.style.font_color = Color.orange
data.selected_header = element
input_text_box.text = tableName
input_text_box.style.font_color = Color.black
local content = Datastore.debug(tableName)
local content_string = {}
for key, value in pairs(content) do
content_string[#content_string+1] = key:gsub('^%l', string.upper)..' = '..dump(value)
end
right_panel.text = concat(content_string, '\n')
end
)
local function update_dump(text_input, data)
local content = Datastore.debug(text_input.text)
local content_string = {}
for key, value in pairs(content) do
content_string[#content_string+1] = key:gsub('^%l', string.upper)..' = '..dump(value)
end
data.right_panel.text = concat(content_string, '\n')
end
Gui.on_text_changed(
input_text_box_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
update_dump(element, data)
end
)
Gui.on_click(
refresh_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local input_text_box = data.input_text_box
update_dump(input_text_box, data)
end
)
return Public

View File

@@ -0,0 +1,128 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local ExpGui = require 'expcore.gui' --- @dep utils.global
local Color = require 'utils.color_presets' --- @dep utils.color_presets
local Model = require 'modules.gui.debug.model' --- @dep modules.gui.debug.model
local dump = Model.dump
local dump_text = Model.dump_text
local concat = table.concat
local Public = {}
local header_name = Gui.uid_name()
local left_panel_name = Gui.uid_name()
local right_panel_name = Gui.uid_name()
local input_text_box_name = Gui.uid_name()
local refresh_name = Gui.uid_name()
Public.name = 'Elements'
function Public.show(container)
local main_flow = container.add {type = 'flow', direction = 'horizontal'}
local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name}
local left_panel_style = left_panel.style
left_panel_style.width = 300
for element_id, file_path in pairs(ExpGui.file_paths) do
local header = left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = element_id..' - '..file_path}
Gui.set_data(header, element_id)
end
local right_flow = main_flow.add {type = 'flow', direction = 'vertical'}
local right_top_flow = right_flow.add {type = 'flow', direction = 'horizontal'}
local input_text_box = right_top_flow.add {type = 'text-box', name = input_text_box_name}
local input_text_box_style = input_text_box.style
input_text_box_style.horizontally_stretchable = true
input_text_box_style.height = 32
input_text_box_style.maximal_width = 1000
local refresh_button =
right_top_flow.add {type = 'sprite-button', name = refresh_name, sprite = 'utility/reset', tooltip = 'refresh'}
local refresh_button_style = refresh_button.style
refresh_button_style.width = 32
refresh_button_style.height = 32
local right_panel = right_flow.add {type = 'text-box', name = right_panel_name}
right_panel.read_only = true
right_panel.selectable = true
local right_panel_style = right_panel.style
right_panel_style.vertically_stretchable = true
right_panel_style.horizontally_stretchable = true
right_panel_style.maximal_width = 1000
right_panel_style.maximal_height = 1000
local data = {
right_panel = right_panel,
input_text_box = input_text_box,
selected_header = nil
}
Gui.set_data(input_text_box, data)
Gui.set_data(left_panel, data)
Gui.set_data(refresh_button, data)
end
Gui.on_click(
header_name,
function(event)
local element = event.element
local element_id = Gui.get_data(element)
local left_panel = element.parent.parent
local data = Gui.get_data(left_panel)
local right_panel = data.right_panel
local selected_header = data.selected_header
local input_text_box = data.input_text_box
if selected_header then
selected_header.style.font_color = Color.white
end
element.style.font_color = Color.orange
data.selected_header = element
input_text_box.text = concat {'Gui.defines[', element_id, ']'}
input_text_box.style.font_color = Color.black
local content = dump(ExpGui.debug_info[element_id]) or 'nil'
right_panel.text = content
end
)
local function update_dump(text_input, data, player)
local suc, ouput = dump_text(text_input.text, player)
if not suc then
text_input.style.font_color = Color.red
else
text_input.style.font_color = Color.black
data.right_panel.text = ouput
end
end
Gui.on_text_changed(
input_text_box_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
update_dump(element, data, event.player)
end
)
Gui.on_click(
refresh_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local input_text_box = data.input_text_box
update_dump(input_text_box, data, event.player)
end
)
return Public

View File

@@ -0,0 +1,133 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local Model = require 'modules.gui.debug.model' --- @dep modules.gui.debug.model
local Color = require 'utils.color_presets' --- @dep utils.color_presets
local dump = Model.dump
local dump_text = Model.dump_text
local concat = table.concat
local Public = {}
local ignore = {tokens = true, data_store = true, datastores = true}
local header_name = Gui.uid_name()
local left_panel_name = Gui.uid_name()
local right_panel_name = Gui.uid_name()
local input_text_box_name = Gui.uid_name()
local refresh_name = Gui.uid_name()
Public.name = 'global'
function Public.show(container)
local main_flow = container.add {type = 'flow', direction = 'horizontal'}
local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name}
local left_panel_style = left_panel.style
left_panel_style.width = 300
for key, _ in pairs(global) do
if not ignore[key] then
local header =
left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = tostring(key)}
Gui.set_data(header, key)
end
end
local right_flow = main_flow.add {type = 'flow', direction = 'vertical'}
local right_top_flow = right_flow.add {type = 'flow', direction = 'horizontal'}
local input_text_box = right_top_flow.add {type = 'text-box', name = input_text_box_name}
local input_text_box_style = input_text_box.style
input_text_box_style.horizontally_stretchable = true
input_text_box_style.height = 32
input_text_box_style.maximal_width = 1000
local refresh_button =
right_top_flow.add {type = 'sprite-button', name = refresh_name, sprite = 'utility/reset', tooltip = 'refresh'}
local refresh_button_style = refresh_button.style
refresh_button_style.width = 32
refresh_button_style.height = 32
local right_panel = right_flow.add {type = 'text-box', name = right_panel_name}
right_panel.read_only = true
right_panel.selectable = true
local right_panel_style = right_panel.style
right_panel_style.vertically_stretchable = true
right_panel_style.horizontally_stretchable = true
right_panel_style.maximal_width = 1000
right_panel_style.maximal_height = 1000
local data = {
right_panel = right_panel,
input_text_box = input_text_box,
selected_header = nil,
selected_token_id = nil
}
Gui.set_data(input_text_box, data)
Gui.set_data(left_panel, data)
Gui.set_data(refresh_button, data)
end
Gui.on_click(
header_name,
function(event)
local element = event.element
local key = Gui.get_data(element)
local left_panel = element.parent.parent
local data = Gui.get_data(left_panel)
local right_panel = data.right_panel
local selected_header = data.selected_header
local input_text_box = data.input_text_box
if selected_header then
selected_header.style.font_color = Color.white
end
element.style.font_color = Color.orange
data.selected_header = element
input_text_box.text = concat {"global['", key, "']"}
input_text_box.style.font_color = Color.black
local content = dump(global[key]) or 'nil'
right_panel.text = content
end
)
local function update_dump(text_input, data, player)
local suc, ouput = dump_text(text_input.text, player)
if not suc then
text_input.style.font_color = Color.red
else
text_input.style.font_color = Color.black
data.right_panel.text = ouput
end
end
Gui.on_text_changed(
input_text_box_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
update_dump(element, data, event.player)
end
)
Gui.on_click(
refresh_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local input_text_box = data.input_text_box
update_dump(input_text_box, data, event.player)
end
)
return Public

View File

@@ -0,0 +1,113 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local Color = require 'utils.color_presets' --- @dep utils.color_presets
local Public = {}
local pages = {
require 'modules.gui.debug.redmew_global_view',
require 'modules.gui.debug.expcore_datastore_view',
require 'modules.gui.debug.expcore_gui_view',
require 'modules.gui.debug.global_view',
require 'modules.gui.debug.package_view',
require 'modules.gui.debug._g_view',
require 'modules.gui.debug.event_view'
}
local main_frame_name = Gui.uid_name()
local close_name = Gui.uid_name()
local tab_name = Gui.uid_name()
function Public.open_dubug(player)
for i = 1, #pages do
local page = pages[i]
local callback = page.on_open_debug
if callback then
callback()
end
end
local center = player.gui.center
local frame = center[main_frame_name]
if frame then
return
end
--[[
local screen_element = player.gui.screen
frame = screen_element.add{type = 'frame', name = main_frame_name, caption = 'Debuggertron 3000'}
frame.style.size = {900, 600}
frame.auto_center = true
]]
frame = center.add {type = 'frame', name = main_frame_name, caption = 'Debuggertron 3002', direction = 'vertical'}
local frame_style = frame.style
frame_style.height = 600
frame_style.width = 900
local tab_flow = frame.add {type = 'flow', direction = 'horizontal'}
local container = frame.add {type = 'flow'}
container.style.vertically_stretchable = true
local data = {}
for i = 1, #pages do
local page = pages[i]
local tab_button = tab_flow.add({type = 'flow'}).add {type = 'button', name = tab_name, caption = page.name}
local tab_button_style = tab_button.style
Gui.set_data(tab_button, {index = i, frame_data = data})
if i == 1 then
tab_button_style.font_color = Color.orange
data.selected_index = i
data.selected_tab_button = tab_button
data.container = container
Gui.set_data(frame, data)
page.show(container)
end
end
frame.add {type = 'button', name = close_name, caption = 'Close'}
end
Gui.on_click(
tab_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local index = data.index
local frame_data = data.frame_data
local selected_index = frame_data.selected_index
if selected_index == index then
return
end
local selected_tab_button = frame_data.selected_tab_button
selected_tab_button.style.font_color = Color.black
frame_data.selected_tab_button = element
frame_data.selected_index = index
element.style.font_color = Color.orange
local container = frame_data.container
Gui.clear(container)
pages[index].show(container)
end
)
Gui.on_click(
close_name,
function(event)
local frame = event.player.gui.center[main_frame_name]
if frame then
Gui.destroy(frame)
end
end
)
return Public

View File

@@ -0,0 +1,147 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local table = require 'overrides.table' --- @dep overrides.table
local gui_names = Gui.names
local type = type
local concat = table.concat
local inspect = table.inspect
local pcall = pcall
local loadstring = loadstring ---@diagnostic disable-line
local rawset = rawset
local Public = {}
local luaObject = {'{', nil, ", name = '", nil, "'}"}
local luaPlayer = {"{LuaPlayer, name = '", nil, "', index = ", nil, '}'}
local luaEntity = {"{LuaEntity, name = '", nil, "', unit_number = ", nil, '}'}
local luaGuiElement = {"{LuaGuiElement, name = '", nil, "'}"}
local function get(obj, prop)
return obj[prop]
end
local function get_name_safe(obj)
local s, r = pcall(get, obj, 'name')
if not s then
return 'nil'
else
return r or 'nil'
end
end
local function get_lua_object_type_safe(obj)
local s, r = pcall(get, obj, 'help')
if not s then
return
end
return r():match('Lua%a+')
end
local function inspect_process(item)
if type(item) ~= 'table' or type(item.__self) ~= 'userdata' then
return item
end
local suc, valid = pcall(get, item, 'valid')
if not suc then
-- no 'valid' property
return get_lua_object_type_safe(item) or '{NoHelp LuaObject}'
end
if not valid then
return '{Invalid LuaObject}'
end
local obj_type = get_lua_object_type_safe(item)
if not obj_type then
return '{NoHelp LuaObject}'
end
if obj_type == 'LuaPlayer' then
luaPlayer[2] = item.name or 'nil'
luaPlayer[4] = item.index or 'nil'
return concat(luaPlayer)
elseif obj_type == 'LuaEntity' then
luaEntity[2] = item.name or 'nil'
luaEntity[4] = item.unit_number or 'nil'
return concat(luaEntity)
elseif obj_type == 'LuaGuiElement' then
local name = item.name
luaGuiElement[2] = gui_names and gui_names[name] or name or 'nil'
return concat(luaGuiElement)
else
luaObject[2] = obj_type
luaObject[4] = get_name_safe(item)
return concat(luaObject)
end
end
local inspect_options = {process = inspect_process}
function Public.dump(data)
return inspect(data, inspect_options)
end
local dump = Public.dump
function Public.dump_ignore_builder(ignore)
local function process(item)
if ignore[item] then
return nil
end
return inspect_process(item)
end
local options = {process = process}
return function(data)
return inspect(data, options)
end
end
function Public.dump_function(func)
local res = {'upvalues:\n'}
local i = 1
while true do
local n, v = debug.getupvalue(func, i)
if n == nil then
break
elseif n ~= '_ENV' then
res[#res + 1] = n
res[#res + 1] = ' = '
res[#res + 1] = dump(v)
res[#res + 1] = '\n'
end
i = i + 1
end
return concat(res)
end
function Public.dump_text(text, player)
local func = loadstring('return ' .. text)
if not func then
return false
end
rawset(game, 'player', player)
local suc, var = pcall(func)
rawset(game, 'player', nil)
if not suc then
return false
end
return true, dump(var)
end
return Public

View File

@@ -0,0 +1,161 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local Color = require 'utils.color_presets' --- @dep utils.color_presets
local Model = require 'modules.gui.debug.model' --- @dep modules.gui.debug.model
local dump_function = Model.dump_function
local loaded = _G.package.loaded
local Public = {}
local ignore = {
_G = true,
package = true,
coroutine = true,
table = true,
string = true,
bit32 = true,
math = true,
debug = true,
serpent = true,
['overrides.math'] = true,
util = true,
['overrides.inspect'] = true,
['mod-gui'] = true
}
local file_label_name = Gui.uid_name()
local left_panel_name = Gui.uid_name()
local breadcrumbs_name = Gui.uid_name()
local top_panel_name = Gui.uid_name()
local variable_label_name = Gui.uid_name()
local text_box_name = Gui.uid_name()
Public.name = 'package'
function Public.show(container)
local main_flow = container.add {type = 'flow', direction = 'horizontal'}
local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name}
local left_panel_style = left_panel.style
left_panel_style.width = 300
for name, file in pairs(loaded) do
if not ignore[name] then
local file_label =
left_panel.add({type = 'flow'}).add {type = 'label', name = file_label_name, caption = name}
Gui.set_data(file_label, file)
end
end
local right_flow = main_flow.add {type = 'flow', direction = 'vertical'}
local breadcrumbs = right_flow.add {type = 'label', name = breadcrumbs_name}
local top_panel = right_flow.add {type = 'scroll-pane', name = top_panel_name}
local top_panel_style = top_panel.style
top_panel_style.height = 200
top_panel_style.maximal_width = 1000
top_panel_style.horizontally_stretchable = true
local text_box = right_flow.add {type = 'text-box', name = text_box_name}
text_box.read_only = true
text_box.selectable = true
local text_box_style = text_box.style
text_box_style.vertically_stretchable = true
text_box_style.horizontally_stretchable = true
text_box_style.maximal_width = 1000
text_box_style.maximal_height = 1000
local data = {
left_panel = left_panel,
breadcrumbs = breadcrumbs,
top_panel = top_panel,
text_box = text_box,
selected_file_label = nil,
selected_variable_label = nil
}
Gui.set_data(left_panel, data)
Gui.set_data(top_panel, data)
end
Gui.on_click(
file_label_name,
function(event)
local element = event.element
local file = Gui.get_data(element)
local left_panel = element.parent.parent
local data = Gui.get_data(left_panel)
local selected_file_label = data.selected_file_label
if selected_file_label then
selected_file_label.style.font_color = Color.white
end
element.style.font_color = Color.orange
data.selected_file_label = element
local top_panel = data.top_panel
local text_box = data.text_box
Gui.clear(top_panel)
local file_type = type(file)
if file_type == 'table' then
for k, v in pairs(file) do
local label =
top_panel.add({type = 'flow'}).add {type = 'label', name = variable_label_name, caption = k}
Gui.set_data(label, v)
end
elseif file_type == 'function' then
text_box.text = dump_function(file)
else
text_box.text = tostring(file)
end
end
)
Gui.on_click(
variable_label_name,
function(event)
local element = event.element
local variable = Gui.get_data(element)
local top_panel = element.parent.parent
local data = Gui.get_data(top_panel)
local text_box = data.text_box
local variable_type = type(variable)
if variable_type == 'table' then
Gui.clear(top_panel)
for k, v in pairs(variable) do
local label =
top_panel.add({type = 'flow'}).add {type = 'label', name = variable_label_name, caption = k}
Gui.set_data(label, v)
end
return
end
local selected_label = data.selected_variable_label
if selected_label and selected_label.valid then
selected_label.style.font_color = Color.white
end
element.style.font_color = Color.orange
data.selected_variable_label = element
if variable_type == 'function' then
text_box.text = dump_function(variable)
else
text_box.text = tostring(variable)
end
end
)
return Public

View File

@@ -0,0 +1,129 @@
local Gui = require 'utils.gui' --- @dep utils.gui
local Global = require 'utils.global' --- @dep utils.global
local Token = require 'utils.token' --- @dep utils.token
local Color = require 'utils.color_presets' --- @dep utils.color_presets
local Model = require 'modules.gui.debug.model' --- @dep modules.gui.debug.model
local dump = Model.dump
local dump_text = Model.dump_text
local concat = table.concat
local Public = {}
local header_name = Gui.uid_name()
local left_panel_name = Gui.uid_name()
local right_panel_name = Gui.uid_name()
local input_text_box_name = Gui.uid_name()
local refresh_name = Gui.uid_name()
Public.name = 'Global'
function Public.show(container)
local main_flow = container.add {type = 'flow', direction = 'horizontal'}
local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name}
local left_panel_style = left_panel.style
left_panel_style.width = 300
for token_id, token_name in pairs(Global.names) do
local header = left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = token_name}
Gui.set_data(header, token_id)
end
local right_flow = main_flow.add {type = 'flow', direction = 'vertical'}
local right_top_flow = right_flow.add {type = 'flow', direction = 'horizontal'}
local input_text_box = right_top_flow.add {type = 'text-box', name = input_text_box_name}
local input_text_box_style = input_text_box.style
input_text_box_style.horizontally_stretchable = true
input_text_box_style.height = 32
input_text_box_style.maximal_width = 1000
local refresh_button =
right_top_flow.add {type = 'sprite-button', name = refresh_name, sprite = 'utility/reset', tooltip = 'refresh'}
local refresh_button_style = refresh_button.style
refresh_button_style.width = 32
refresh_button_style.height = 32
local right_panel = right_flow.add {type = 'text-box', name = right_panel_name}
right_panel.read_only = true
right_panel.selectable = true
local right_panel_style = right_panel.style
right_panel_style.vertically_stretchable = true
right_panel_style.horizontally_stretchable = true
right_panel_style.maximal_width = 1000
right_panel_style.maximal_height = 1000
local data = {
right_panel = right_panel,
input_text_box = input_text_box,
selected_header = nil
}
Gui.set_data(input_text_box, data)
Gui.set_data(left_panel, data)
Gui.set_data(refresh_button, data)
end
Gui.on_click(
header_name,
function(event)
local element = event.element
local token_id = Gui.get_data(element)
local left_panel = element.parent.parent
local data = Gui.get_data(left_panel)
local right_panel = data.right_panel
local selected_header = data.selected_header
local input_text_box = data.input_text_box
if selected_header then
selected_header.style.font_color = Color.white
end
element.style.font_color = Color.orange
data.selected_header = element
input_text_box.text = concat {'global.tokens[', token_id, ']'}
input_text_box.style.font_color = Color.black
local content = dump(Token.get_global(token_id)) or 'nil'
right_panel.text = content
end
)
local function update_dump(text_input, data, player)
local suc, ouput = dump_text(text_input.text, player)
if not suc then
text_input.style.font_color = Color.red
else
text_input.style.font_color = Color.black
data.right_panel.text = ouput
end
end
Gui.on_text_changed(
input_text_box_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
update_dump(element, data, event.player)
end
)
Gui.on_click(
refresh_name,
function(event)
local element = event.element
local data = Gui.get_data(element)
local input_text_box = data.input_text_box
update_dump(input_text_box, data, event.player)
end
)
return Public

View File

@@ -0,0 +1,185 @@
--[[-- Gui Module - Landfill
- Landfill blueprint
@gui Landfill
@alias landfill_container
]]
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Event = require 'utils.event' --- @dep utils.event
local Roles = require 'expcore.roles' --- @dep expcore.roles
local rolling_stocks = {}
local function landfill_init()
for name, _ in pairs(game.get_filtered_entity_prototypes({{filter = 'rolling-stock'}})) do
rolling_stocks[name] = true
end
end
local function rotate_bounding_box(box)
return {
left_top = {
x = -box.right_bottom.y,
y = box.left_top.x
},
right_bottom = {
x = -box.left_top.y,
y = box.right_bottom.x
}
}
end
local function curve_flip_lr(oc)
local nc = table.deepcopy(oc)
for r=1, 8 do
for c=1, 8 do
nc[r][c] = oc[r][9 - c]
end
end
return nc
end
local function curve_flip_d(oc)
local nc = table.deepcopy(oc)
for r=1, 8 do
for c=1, 8 do
nc[r][c] = oc[c][r]
end
end
return nc
end
local curves = {}
curves[1] = {
{0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 1, 1, 1, 0},
{0, 0, 0, 1, 1, 1, 1, 0},
{0, 0, 0, 1, 1, 1, 0, 0},
{0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0, 0}
}
curves[6] = curve_flip_d(curves[1])
curves[3] = curve_flip_lr(curves[6])
curves[4] = curve_flip_d(curves[3])
curves[5] = curve_flip_lr(curves[4])
curves[2] = curve_flip_d(curves[5])
curves[7] = curve_flip_lr(curves[2])
curves[8] = curve_flip_d(curves[7])
local curve_n = {}
for i, map in ipairs(curves) do
curve_n[i] = {}
local index = 1
for r=1, 8 do
for c=1, 8 do
if map[r][c] == 1 then
curve_n[i][index] = {
['x'] = c - 5,
['y'] = r - 5
}
index = index + 1
end
end
end
end
local function landfill_gui_add_landfill(blueprint)
local entities = blueprint.get_blueprint_entities()
local tile_index = 0
local new_tiles = {}
for _, ent in pairs(entities) do
-- vehicle
if not (rolling_stocks[ent.name] or ent.name == 'offshore-pump') then
-- curved rail, special
if ent.name ~= 'curved-rail' then
local box = game.entity_prototypes[ent.name].collision_box or game.entity_prototypes[ent.name].selection_box
if game.entity_prototypes[ent.name].collision_mask['ground-tile'] == nil then
if ent.direction then
if ent.direction ~= defines.direction.north then
box = rotate_bounding_box(box)
if ent.direction ~= defines.direction.east then
box = rotate_bounding_box(box)
if ent.direction ~= defines.direction.south then
box = rotate_bounding_box(box)
end
end
end
end
for y = math.floor(ent.position.y + box.left_top.y), math.floor(ent.position.y + box.right_bottom.y), 1 do
for x = math.floor(ent.position.x + box.left_top.x), math.floor(ent.position.x + box.right_bottom.x), 1 do
tile_index = tile_index + 1
new_tiles[tile_index] = {
name = 'landfill',
position = {x, y}
}
end
end
end
-- curved rail
else
local curve_mask = curve_n[ent.direction or 8]
for m=1, #curve_mask do
new_tiles[tile_index + 1] = {
name = 'landfill',
position = {curve_mask[m].x + ent.position.x, curve_mask[m].y + ent.position.y}
}
tile_index = tile_index + 1
end
end
end
end
local old_tiles = blueprint.get_blueprint_tiles()
if old_tiles then
for _, old_tile in pairs(old_tiles) do
new_tiles[tile_index + 1] = {
name = 'landfill',
position = {old_tile.position.x, old_tile.position.y}
}
tile_index = tile_index + 1
end
end
return {tiles = new_tiles}
end
-- @element toolbar_button
Gui.toolbar_button('item/landfill', {'landfill.main-tooltip'}, function(player)
return Roles.player_allowed(player, 'gui/landfill')
end)
:on_click(function(player, _, _)
if player.cursor_stack and player.cursor_stack.valid_for_read then
if player.cursor_stack.type == 'blueprint' and player.cursor_stack.is_blueprint_setup() then
local modified = landfill_gui_add_landfill(player.cursor_stack)
if modified and next(modified.tiles) then
player.cursor_stack.set_blueprint_tiles(modified.tiles)
end
end
else
player.print{'landfill.cursor-none'}
end
end)
Event.add(defines.events.on_player_joined_game, landfill_init)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,315 @@
---- module inserter
-- @gui Module
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Event = require 'utils.event' --- @dep utils.event
local Roles = require 'expcore.roles' --- @dep expcore.roles
local config = require 'config.module' --- @dep config.module
local Selection = require 'modules.control.selection' --- @dep modules.control.selection
local SelectionModuleArea = 'ModuleArea'
--- 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 module_container
local machine_name = {}
for k, _ in pairs(config.machine) do
table.insert(machine_name, k)
end
local prod_module_names = {}
local function get_module_name()
for name, item in pairs(game.item_prototypes) do
if item.module_effects and item.module_effects.productivity and item.module_effects.productivity.bonus > 0 then
prod_module_names[#prod_module_names + 1] = name
end
end
end
local elem_filter = {
name = {{
filter = 'name',
name = machine_name
}},
normal = {{
filter = 'type',
type = 'module'
}, {
filter = 'name',
name = prod_module_names,
mode = 'and',
invert = true
}},
prod = {{
filter = 'type',
type = 'module'
}}
}
local function clear_module(player, area, machine)
for _, entity in pairs(player.surface.find_entities_filtered{area=area, name=machine, force=player.force}) do
for _, r in pairs(player.surface.find_entities_filtered{position=entity.position, name='item-request-proxy', force=player.force}) do
if r then
r.destroy{raise_destroy=true}
end
end
local m_current_module = entity.get_module_inventory()
if m_current_module then
local m_current_module_content = m_current_module.get_contents()
if m_current_module_content then
for k, m in pairs(m_current_module_content) do
player.surface.spill_item_stack(entity.bounding_box.left_top, {name=k, count=m}, true, player.force, false)
end
end
m_current_module.clear()
end
end
end
local function apply_module(player, area, machine, modules)
for _, entity in pairs(player.surface.find_entities_filtered{area=area, name=machine, force=player.force}) do
local m_current_recipe
if entity.prototype.crafting_speed then
m_current_recipe= entity.get_recipe()
end
if m_current_recipe then
if config.module_allowed[m_current_recipe.name] then
entity.surface.create_entity{name='item-request-proxy', target=entity, position=entity.position, force=entity.force, modules=modules['n']}
entity.last_user = player
else
entity.surface.create_entity{name='item-request-proxy', target=entity, position=entity.position, force=entity.force, modules=modules['p']}
entity.last_user = player
end
else
entity.surface.create_entity{name='item-request-proxy', target=entity, position=entity.position, force=entity.force, modules=modules['n']}
entity.last_user = player
end
end
end
--- when an area is selected to add protection to the area
Selection.on_selection(SelectionModuleArea, function(event)
local area = aabb_align_expand(event.area)
local player = game.get_player(event.player_index)
local frame = Gui.get_left_element(player, module_container)
local scroll_table = frame.container.scroll.table
for i=1, config.default_module_row_count do
local mma = scroll_table['module_mm_' .. i .. '_0'].elem_value
if mma then
local mm = {
['n'] = {},
['p'] = {}
}
for j=1, game.entity_prototypes[mma].module_inventory_size, 1 do
local mmo = scroll_table['module_mm_' .. i .. '_' .. j].elem_value
if mmo then
if mm['n'][mmo] then
mm['n'][mmo] = mm['n'][mmo] + 1
mm['p'][mmo] = mm['p'][mmo] + 1
else
mm['n'][mmo] = 1
mm['p'][mmo] = 1
end
end
end
for k, v in pairs(mm['p']) do
if k:find('productivity') then
local module_name = k:gsub('productivity', 'effectivity')
mm['p'][module_name] = (mm['p'][module_name] or 0) + v
mm['p'][k] = nil
end
end
if mm then
clear_module(player, area, mma)
apply_module(player, area, mma, mm)
end
end
end
end)
local function row_set(player, element)
local frame = Gui.get_left_element(player, module_container)
local scroll_table = frame.container.scroll.table
if scroll_table[element .. '0'].elem_value then
for i=1, config.module_slot_max do
if i <= game.entity_prototypes[scroll_table[element .. '0'].elem_value].module_inventory_size then
if config.machine[scroll_table[element .. '0'].elem_value].prod then
scroll_table[element .. i].elem_filters = elem_filter.prod
else
scroll_table[element .. i].elem_filters = elem_filter.normal
end
scroll_table[element .. i].enabled = true
scroll_table[element .. i].elem_value = config.machine[scroll_table[element .. '0'].elem_value].module
else
scroll_table[element .. i].enabled = false
scroll_table[element .. i].elem_value = nil
end
end
else
local mf = elem_filter.normal
for i=1, config.module_slot_max do
scroll_table[element .. i].enabled = false
scroll_table[element .. i].elem_filters = mf
scroll_table[element .. i].elem_value = nil
end
end
end
local button_apply =
Gui.element{
type = 'button',
caption = 'Apply',
style = 'button'
}:on_click(function(player)
if Selection.is_selecting(player, SelectionModuleArea) then
Selection.stop(player)
else
Selection.start(player, SelectionModuleArea)
end
end)
module_container =
Gui.element(function(definition, parent)
local container = Gui.container(parent, definition.name, (config.module_slot_max + 2) * 36)
Gui.header(container, 'Module Inserter', '', true)
local scroll_table = Gui.scroll_table(container, (config.module_slot_max + 2) * 36, config.module_slot_max + 1)
for i=1, config.default_module_row_count do
scroll_table.add{
name = 'module_mm_' .. i .. '_0',
type = 'choose-elem-button',
elem_type = 'entity',
elem_filters = elem_filter.name,
style = 'slot_button'
}
for j=1, config.module_slot_max do
scroll_table.add{
name = 'module_mm_' .. i .. '_' .. j,
type = 'choose-elem-button',
elem_type = 'item',
elem_filters = elem_filter.normal,
style = 'slot_button',
enabled = false
}
end
end
button_apply(container)
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
Gui.left_toolbar_button('item/productivity-module-3', {'module.main-tooltip'}, module_container, function(player)
return Roles.player_allowed(player, 'gui/module')
end)
Event.add(defines.events.on_gui_elem_changed, function(event)
if event.element.name:sub(1, 10) == 'module_mm_' then
if event.element.name:sub(-1) == '0' then
row_set(game.players[event.player_index], 'module_mm_' .. event.element.name:sub(-3):sub(1, 1) .. '_')
end
end
end)
Event.add(defines.events.on_player_joined_game, get_module_name)
Event.add(defines.events.on_entity_settings_pasted, function(event)
local source = event.source
local destination = event.destination
local player = game.players[event.player_index]
if not player then
return
end
if not source or not source.valid then
return
end
if not destination or not destination.valid then
return
end
-- rotate machine also
if config.copy_paste_rotation then
if (source.name == destination.name or source.prototype.fast_replaceable_group == destination.prototype.fast_replaceable_group) then
if source.supports_direction and destination.supports_direction and source.type ~= 'transport-belt' then
local destination_box = destination.bounding_box
local ltx = destination_box.left_top.x
local lty = destination_box.left_top.y
local rbx = destination_box.right_bottom.x
local rby = destination_box.right_bottom.y
local old_direction = destination.direction
destination.direction = source.direction
if ltx ~= destination_box.left_top.x or lty ~= destination_box.left_top.y or rbx ~= destination_box.right_bottom.x or rby ~= destination_box.right_bottom.y then
destination.direction = old_direction
end
end
end
end
if config.copy_paste_module then
if source.name ~= destination.name then
return
end
local source_inventory = source.get_module_inventory()
if not source_inventory then
return
end
local source_inventory_content = source_inventory.get_contents()
if not source_inventory_content then
return
end
clear_module(player, destination.bounding_box, destination.name)
if next(source_inventory_content) ~= nil then
apply_module(player, destination.bounding_box, destination.name, {['n']=source_inventory_content, ['p']=source_inventory_content})
end
end
end)

View File

@@ -0,0 +1,434 @@
--[[-- Gui Module - Player List
- Adds a player list to show names and play time; also includes action buttons which can preform actions to players
@gui Player-List
@alias player_list
]]
-- luacheck:ignore 211/Colors
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Datastore = require 'expcore.datastore' --- @dep expcore.datastore
local Event = require 'utils.event' --- @dep utils.event
local config = require 'config.gui.player_list_actions' --- @dep config.gui.player_list_actions
local Colors = require 'utils.color_presets' --- @dep utils.color_presets
local format_time = _C.format_time --- @dep expcore.common
--- Stores all data for the warp gui
local PlayerListData = Datastore.connect('PlayerListData')
PlayerListData:set_serializer(Datastore.name_serializer)
local SelectedPlayer = PlayerListData:combine('SelectedPlayer')
local SelectedAction = PlayerListData:combine('SelectedAction')
-- Set the config to use these stores
config.set_datastores(SelectedPlayer, SelectedAction)
--- Button used to open the action bar
-- @element open_action_bar
local open_action_bar =
Gui.element{
type = 'sprite-button',
sprite = 'utility/expand_dots_white',
tooltip = {'player-list.open-action-bar'},
style = 'frame_button',
name = Gui.unique_static_name
}
:style{
padding = -2,
width = 8,
height = 14
}
:on_click(function(player, element, _)
local selected_player_name = element.parent.name
local old_selected_player_name = SelectedPlayer:get(player)
if selected_player_name == old_selected_player_name then
SelectedPlayer:remove(player)
else
SelectedPlayer:set(player, selected_player_name)
end
end)
--- Button used to close the action bar
-- @element close_action_bar
local close_action_bar =
Gui.element{
type = 'sprite-button',
sprite = 'utility/close_black',
tooltip = {'player-list.close-action-bar'},
style = 'slot_sized_button_red'
}
:style(Gui.sprite_style(30, -1, { top_margin = -1, right_margin = -1 }))
:on_click(function(player, _)
SelectedPlayer:remove(player)
SelectedAction:remove(player)
end)
--- Button used to confirm a reason
-- @element reason_confirm
local reason_confirm =
Gui.element{
type = 'sprite-button',
sprite = 'utility/confirm_slot',
tooltip = {'player-list.reason-confirm'},
style = 'slot_sized_button_green'
}
:style(Gui.sprite_style(30, -1, { left_margin = -2, right_margin = -1 }))
:on_click(function(player, element)
local reason = element.parent.entry.text
local action_name = SelectedAction:get(player)
local reason_callback = config.buttons[action_name].reason_callback
if reason == nil or not reason:find("%S") then reason = 'no reason given' end
reason_callback(player, reason)
SelectedPlayer:remove(player)
SelectedAction:remove(player)
element.parent.entry.text = ''
end)
--- Set of elements that are used to make up a row of the player table
-- @element add_player_base
local add_player_base =
Gui.element(function(_, parent, player_data)
-- Add the button to open the action bar
local toggle_action_bar_flow = parent.add{ type = 'flow', name = player_data.name }
open_action_bar(toggle_action_bar_flow)
-- Add the player name
local player_name = parent.add{
type = 'label',
name = 'player-name-'..player_data.index,
caption = player_data.name,
tooltip = {'player-list.open-map', player_data.name, player_data.tag, player_data.role_name}
}
player_name.style.padding = {0, 2,0, 0}
player_name.style.font_color = player_data.chat_color
-- Add the time played label
local alignment = Gui.alignment(parent, 'player-time-'..player_data.index)
local time_label = alignment.add{
name = 'label',
type = 'label',
caption = player_data.caption,
tooltip = player_data.tooltip
}
time_label.style.padding = 0
return player_name
end)
:on_click(function(player, element, event)
local selected_player_name = element.caption
local selected_player = game.players[selected_player_name]
if event.button == defines.mouse_button_type.left then
-- LMB will open the map to the selected player
local position = selected_player.position
event.player.zoom_to_world(position, 1.75)
else
-- RMB will toggle the settings
local old_selected_player_name = SelectedPlayer:get(player)
if selected_player_name == old_selected_player_name then
SelectedPlayer:remove(player)
SelectedAction:remove(player)
else
SelectedPlayer:set(player, selected_player_name)
end
end
end)
-- Removes the three elements that are added as part of the base
local function remove_player_base(parent, player)
Gui.destroy_if_valid(parent[player.name])
Gui.destroy_if_valid(parent['player-name-'..player.index])
Gui.destroy_if_valid(parent['player-time-'..player.index])
end
-- Update the time label for a player using there player time data
local function update_player_base(parent, player_time)
local time_element = parent[player_time.element_name]
if time_element and time_element.valid then
time_element.label.caption = player_time.caption
time_element.label.tooltip = player_time.tooltip
end
end
--- Adds all the buttons and flows that make up the action bar
-- @element add_action_bar
local add_action_bar_buttons =
Gui.element(function(_, parent)
close_action_bar(parent)
-- Loop over all the buttons in the config
for action_name, button_data in pairs(config.buttons) do
-- Added the permission flow
local permission_flow = parent.add{ type = 'flow', name = action_name }
permission_flow.visible = false
-- Add the buttons under that permission
for _, button in ipairs(button_data) do
button(permission_flow)
end
end
return parent
end)
--- Updates the visible state of the action bar buttons
local function update_action_bar(element)
local player = Gui.get_player_from_element(element)
local selected_player_name = SelectedPlayer:get(player)
if not selected_player_name then
-- Hide the action bar when no player is selected
element.visible = false
else
local selected_player = game.players[selected_player_name]
if not selected_player.connected then
-- If the player is offline then reest stores
element.visible = false
SelectedPlayer:remove(player)
SelectedAction:remove(player)
else
-- Otherwise check what actions the player is allowed to use
element.visible = true
for action_name, buttons in pairs(config.buttons) do
if buttons.auth and not buttons.auth(player, selected_player) then
element[action_name].visible = false
elseif Roles.player_allowed(player, action_name) then
element[action_name].visible = true
end
end
end
end
end
--- Main player list container for the left flow
-- @element player_list_container
local player_list_container =
Gui.element(function(definition, parent)
-- Draw the internal container
local container = Gui.container(parent, definition.name, 200)
-- Draw the scroll table for the players
local scroll_table = Gui.scroll_table(container, 184, 3)
-- Change the style of the scroll table
local scroll_table_style = scroll_table.style
scroll_table_style.padding = {1, 0,1, 2}
-- Add the action bar
local action_bar = Gui.footer(container, nil, nil, false, 'action_bar')
-- Change the style of the action bar
local action_bar_style = action_bar.style
action_bar_style.height = 35
action_bar_style.padding = {1, 3}
action_bar.visible = false
-- Add the buttons to the action bar
add_action_bar_buttons(action_bar)
-- Add the reason bar
local reason_bar = Gui.footer(container, nil, nil, false, 'reason_bar')
-- Change the style of the reason bar
local reason_bar_style = reason_bar.style
reason_bar_style.height = 35
reason_bar_style.padding = {-1, 3}
reason_bar.visible = false
-- Add the text entry for the reason bar
local reason_field =
reason_bar.add{
name = 'entry',
type = 'textfield',
style = 'stretchable_textfield',
tooltip = {'player-list.reason-entry'}
}
-- Change the style of the text entry
local reason_entry_style = reason_field.style
reason_entry_style.padding = 0
reason_entry_style.height = 28
reason_entry_style.minimal_width = 160
-- Add the confirm reason button
reason_confirm(reason_bar)
-- Return the exteral container
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow(true)
--- Button on the top flow used to toggle the player list container
-- @element toggle_player_list
Gui.left_toolbar_button('entity/character', {'player-list.main-tooltip'}, player_list_container, function(player)
return Roles.player_allowed(player, 'gui/player-list')
end)
-- Get caption and tooltip format for a player
local function get_time_formats(online_time, afk_time)
local tick = game.tick > 0 and game.tick or 1
local percent = math.round(online_time/tick, 3)*100
local caption = format_time(online_time)
local tooltip = {'player-list.afk-time', percent, format_time(afk_time, {minutes=true, long=true})}
return caption, tooltip
end
-- Get the player time to be used to update time label
local function get_player_times()
local ctn = 0
local player_times = {}
for _, player in pairs(game.connected_players) do
ctn = ctn + 1
-- Add the player time details to the array
local caption, tooltip = get_time_formats(player.online_time, player.afk_time)
player_times[ctn] = {
element_name = 'player-time-'..player.index,
caption = caption,
tooltip = tooltip
}
end
return player_times
end
-- Get a sorted list of all online players
local function get_player_list_order()
-- Sort all the online players into roles
local players = {}
for _, player in pairs(game.connected_players) do
local highest_role = Roles.get_player_highest_role(player)
if not players[highest_role.name] then
players[highest_role.name] = {}
end
table.insert(players[highest_role.name], player)
end
-- Sort the players from roles into a set order
local ctn = 0
local player_list_order = {}
for _, role_name in pairs(Roles.config.order) do
if players[role_name] then
for _, player in pairs(players[role_name]) do
ctn = ctn + 1
-- Add the player data to the array
local caption, tooltip = get_time_formats(player.online_time, player.afk_time)
player_list_order[ctn] = {
name = player.name,
index = player.index,
tag = player.tag,
role_name = role_name,
chat_color = player.chat_color,
caption = caption,
tooltip = tooltip
}
end
end
end
--[[Adds fake players to the player list
local tick = game.tick+1
for i = 1, 10 do
local online_time = math.random(1, tick)
local afk_time = math.random(online_time-(tick/10), tick)
local caption, tooltip = get_time_formats(online_time, afk_time)
player_list_order[ctn+i] = {
name='Player '..i,
index=0-i,
tag='',
role_name = 'Fake Player',
chat_color = table.get_random_dictionary_entry(Colors),
caption = caption,
tooltip = tooltip
}
end--]]
return player_list_order
end
--- Update the play times every 30 sections
Event.on_nth_tick(1800, function()
local player_times = get_player_times()
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, player_list_container)
local scroll_table = frame.container.scroll.table
for _, player_time in pairs(player_times) do
update_player_base(scroll_table, player_time)
end
end
end)
--- When a player leaves only remove they entry
Event.add(defines.events.on_player_left_game, function(event)
local remove_player = game.players[event.player_index]
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, player_list_container)
local scroll_table = frame.container.scroll.table
remove_player_base(scroll_table, remove_player)
local selected_player_name = SelectedPlayer:get(player)
if selected_player_name == remove_player.name then
SelectedPlayer:remove(player)
SelectedAction:remove(player)
end
end
end)
--- All other events require a full redraw of the table
local function redraw_player_list()
local player_list_order = get_player_list_order()
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, player_list_container)
local scroll_table = frame.container.scroll.table
scroll_table.clear()
for _, next_player_data in ipairs(player_list_order) do
add_player_base(scroll_table, next_player_data)
end
end
end
Event.add(defines.events.on_player_joined_game, redraw_player_list)
Event.add(Roles.events.on_role_assigned, redraw_player_list)
Event.add(Roles.events.on_role_unassigned, redraw_player_list)
--- When the action player is changed the action bar will update
SelectedPlayer:on_update(function(player_name, selected_player)
local player = game.players[player_name]
local frame = Gui.get_left_element(player, player_list_container)
local scroll_table = frame.container.scroll.table
update_action_bar(frame.container.action_bar)
for _, next_player in pairs(game.connected_players) do
local element = scroll_table[next_player.name][open_action_bar.name]
local style = 'frame_button'
if next_player.name == selected_player then
style = 'tool_button'
end
element.style = style
local element_style = element.style
element_style.padding = -2
element_style.width = 8
element_style.height = 14
end
end)
--- When the action name is changed the reason input will update
SelectedAction:on_update(function(player_name, selected_action)
local player = game.players[player_name]
local frame = Gui.get_left_element(player, player_list_container)
local element = frame.container.reason_bar
if selected_action then
-- if there is a new value then check the player is still online
local selected_player_name = SelectedPlayer:get(player_name)
local selected_player = game.players[selected_player_name]
if selected_player.connected then
element.visible = true
else
-- Clear if the player is offline
SelectedPlayer:remove(player)
SelectedAction:remove(player)
end
else
element.visible = false
end
end)

View File

@@ -0,0 +1,219 @@
---- module pd
-- @gui PlayerData
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Event = require 'utils.event' --- @dep utils.event
local PlayerData = require 'expcore.player_data' --- @dep expcore.player_data
require 'modules.data.statistics'
local format_time = _C.format_time --- @dep expcore.common
local format_number = require('util').format_number --- @dep util
local pd_container
local label_width = {
['name'] = 135,
['count'] = 105,
['total'] = 480
}
local function format_time_short(value)
return format_time(value*3600, {
hours=true,
minutes=true,
seconds=false
})
end
local function format_number_n(n)
return format_number(math.floor(n)) .. string.format('%.2f', n % 1):sub(2)
end
local playerStats = PlayerData.Statistics
local computed_stats = {
DamageDeathRatio = {
default = format_number_n(0),
calculate = function(player_name)
return format_number_n(playerStats['DamageDealt']:get(player_name, 0) / playerStats['Deaths']:get(player_name, 1))
end
},
KillDeathRatio = {
default = format_number_n(0),
calculate = function(player_name)
return format_number_n(playerStats['Kills']:get(player_name, 0) / playerStats['Deaths']:get(player_name, 1))
end
},
SessionTime = {
default = format_time_short(0),
calculate = function(player_name)
return format_time_short((playerStats['Playtime']:get(player_name, 0) - playerStats['AfkTime']:get(player_name, 0)) / playerStats['JoinCount']:get(player_name, 1))
end
},
BuildRatio = {
default = format_number_n(0),
calculate = function(player_name)
return format_number_n(playerStats['MachinesBuilt']:get(player_name, 0) / playerStats['MachinesRemoved']:get(player_name, 1))
end
},
RocketPerHour = {
default = format_number_n(0),
calculate = function(player_name)
return format_number_n(playerStats['RocketsLaunched']:get(player_name, 0) * 60 / playerStats['Playtime']:get(player_name, 1))
end
},
TreeKillPerMinute = {
default = format_number_n(0),
calculate = function(player_name)
return format_number_n(playerStats['TreesDestroyed']:get(player_name, 0) / playerStats['Playtime']:get(player_name, 1))
end
},
NetPlayTime = {
default = format_time_short(0),
calculate = function(player_name)
return format_time_short((playerStats['Playtime']:get(player_name, 0) - playerStats['AfkTime']:get(player_name, 0)))
end
},
AFKTimeRatio = {
default = format_number_n(0),
calculate = function(player_name)
return format_number_n(playerStats['AfkTime']:get(player_name, 0) * 100 / playerStats['Playtime']:get(player_name, 1))
end
},
}
local label =
Gui.element(function(_, parent, width, caption, tooltip, name)
local new_label = parent.add{
type = 'label',
caption = caption,
tooltip = tooltip,
name = name,
style = 'heading_2_label'
}
new_label.style.width = width
return new_label
end)
local pd_data_set =
Gui.element(function(_, parent, name)
local pd_data_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.scroll_table(pd_data_set, label_width['total'], 4, 'disp')
for _, stat_name in pairs(PlayerData.Statistics.metadata.display_order) do
local child = PlayerData.Statistics[stat_name]
local metadata = child.metadata
local value = metadata.stringify_short and metadata.stringify_short(0) or metadata.stringify and metadata.stringify(0) or format_number(0)
label(disp, label_width['name'], metadata.name or {'exp-statistics.'..stat_name}, metadata.tooltip or {'exp-statistics.'..stat_name..'-tooltip'})
label(disp, label_width['count'], {'readme.data-format', value, metadata.unit or ''}, metadata.value_tooltip or {'exp-statistics.'..stat_name..'-tooltip'}, stat_name)
end
for stat_name, data in pairs(computed_stats) do
label(disp, label_width['name'], {'exp-statistics.'..stat_name}, {'exp-statistics.'..stat_name..'-tooltip'})
label(disp, label_width['count'], {'readme.data-format', data.default, ''}, {'exp-statistics.'..stat_name..'-tooltip'}, stat_name)
end
return pd_data_set
end)
local function pd_update(table, player_name)
for _, stat_name in pairs(PlayerData.Statistics.metadata.display_order) do
local child = PlayerData.Statistics[stat_name]
local metadata = child.metadata
local value = child:get(player_name)
if metadata.stringify_short then
value = metadata.stringify_short(value or 0)
elseif metadata.stringify then
value = metadata.stringify(value or 0)
else
value = format_number(value or 0)
end
table[stat_name].caption = {'readme.data-format', value, metadata.unit or ''}
end
for stat_name, data in pairs(computed_stats) do
table[stat_name].caption = {'readme.data-format', data.calculate(player_name), ''}
end
end
local pd_username_player =
Gui.element(function(definition, parent, player_list)
return parent.add{
name = definition.name,
type = 'drop-down',
items = player_list,
selected_index = #player_list > 0 and 1
}
end)
:style{
horizontally_stretchable = true
}:on_selection_changed(function(_, element, _)
local player_name = game.connected_players[element.selected_index]
local table = element.parent.parent.parent.parent['pd_st_2'].disp.table
pd_update(table, player_name)
end)
:static_name(Gui.unique_static_name)
local pd_username_update =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = 'update'
}:style{
width = 128
}:on_click(function(_, element, _)
local player_index = element.parent[pd_username_player.name].selected_index
if player_index > 0 then
local player_name = game.connected_players[player_index]
local table = element.parent.parent.parent.parent['pd_st_2'].disp.table
pd_update(table, player_name)
end
end)
local pd_username_set =
Gui.element(function(_, parent, name, player_list)
local pd_username_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.scroll_table(pd_username_set, label_width['total'], 2, 'disp')
pd_username_player(disp, player_list)
pd_username_update(disp)
return pd_username_set
end)
pd_container =
Gui.element(function(definition, parent)
local container = Gui.container(parent, definition.name, label_width['total'])
local player_list = {}
for _, player in pairs(game.connected_players) do
table.insert(player_list, player.name)
end
pd_username_set(container, 'pd_st_1', player_list)
pd_data_set(container, 'pd_st_2')
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
Gui.left_toolbar_button('item/power-armor-mk2', 'Player Data GUI', pd_container, function(player)
return Roles.player_allowed(player, 'gui/playerdata')
end)
local function gui_player_list_update()
local player_list = {}
for _, player in pairs(game.connected_players) do
table.insert(player_list, player.name)
end
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, pd_container)
frame.container['pd_st_1'].disp.table[pd_username_player.name].items = player_list
end
end
Event.add(defines.events.on_player_joined_game, gui_player_list_update)
Event.add(defines.events.on_player_left_game, gui_player_list_update)

View File

@@ -0,0 +1,165 @@
---- Production Data
-- @gui Production
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Event = require 'utils.event' --- @dep utils.event
local Roles = require 'expcore.roles' --- @dep expcore.roles
local production_container
local precision = {
[1] = defines.flow_precision_index.five_seconds,
[2] = defines.flow_precision_index.one_minute,
[3] = defines.flow_precision_index.ten_minutes,
[4] = defines.flow_precision_index.one_hour,
[5] = defines.flow_precision_index.ten_hours
}
local font_color = {
-- positive
[1] = {r = 0.3, g = 1, b = 0.3},
-- negative
[2] = {r = 1, g = 0.3, b = 0.3}
}
local function format_n(n)
local _i, _j, m, i, f = string.format('%.1f', n):find('([-]?)(%d+)([.]?%d*)')
i = i:reverse():gsub('(%d%d%d)', '%1,')
if f ~= '' then
return m .. i:reverse():gsub('^,', '') .. f
else
return m .. i:reverse():gsub('^,', '') .. '.0'
end
end
--- Display group
-- @element production_data_group
local production_data_group =
Gui.element(function(_definition, parent, i)
local item
if i == 0 then
item = parent.add{
type = 'drop-down',
name = 'production_0_e',
items = {'5s', '1m', '10m', '1h', '10h'},
selected_index = 3
}
item.style.width = 80
else
item = parent.add{
type = 'choose-elem-button',
name = 'production_' .. i .. '_e',
elem_type = 'item',
style = 'slot_button'
}
item.style.height = 32
item.style.width = 32
end
local data_1 = parent.add{
type = 'label',
name = 'production_' .. i .. '_1',
caption = '0.0',
style = 'heading_2_label'
}
data_1.style.width = 90
data_1.style.horizontal_align = 'right'
data_1.style.font_color = font_color[1]
local data_2 = parent.add{
type = 'label',
name = 'production_' .. i .. '_2',
caption = '0.0',
style = 'heading_2_label'
}
data_2.style.width = 90
data_2.style.horizontal_align = 'right'
data_2.style.font_color = font_color[2]
local data_3 = parent.add{
type = 'label',
name = 'production_' .. i .. '_3',
caption = '0.0',
style = 'heading_2_label'
}
data_3.style.width = 90
data_3.style.horizontal_align = 'right'
data_3.style.font_color = font_color[1]
return item
end)
--- A vertical flow containing all the production data
-- @element production_data_set
local production_data_set =
Gui.element(function(_, parent, name)
local production_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.scroll_table(production_set, 350, 4, 'disp')
production_data_group(disp, 0)
disp['production_0_1'].caption = {'production.label-prod'}
disp['production_0_2'].caption = {'production.label-con'}
disp['production_0_3'].caption = {'production.label-bal'}
for i=1, 8 do
production_data_group(disp, i)
end
return production_set
end)
production_container =
Gui.element(function(definition, parent)
local container = Gui.container(parent, definition.name, 350)
production_data_set(container, 'production_st')
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
Gui.left_toolbar_button('entity/assembling-machine-3', {'production.main-tooltip'}, production_container, function(player)
return Roles.player_allowed(player, 'gui/production')
end)
Event.on_nth_tick(60, function()
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, production_container)
local stat = player.force.item_production_statistics
local precision_value = precision[frame.container['production_st'].disp.table['production_0_e'].selected_index]
local table = frame.container['production_st'].disp.table
for i=1, 8 do
local production_prefix = 'production_' .. i
local item = table[production_prefix .. '_e'].elem_value
if item then
local add = stat.get_flow_count{name=item, input=true, precision_index=precision_value, count=false} / 60
local minus = stat.get_flow_count{name=item, input=false, precision_index=precision_value, count=false} / 60
local sum = add - minus
table[production_prefix .. '_1'].caption = format_n(add)
table[production_prefix .. '_2'].caption = format_n(minus)
table[production_prefix .. '_3'].caption = format_n(sum)
if sum < 0 then
table[production_prefix .. '_3'].style.font_color = font_color[2]
else
table[production_prefix .. '_3'].style.font_color = font_color[1]
end
else
table[production_prefix .. '_1'].caption = '0.0'
table[production_prefix .. '_2'].caption = '0.0'
table[production_prefix .. '_3'].caption = '0.0'
table[production_prefix .. '_3'].style.font_color = font_color[1]
end
end
end
end)

View File

@@ -0,0 +1,485 @@
--[[-- Gui Module - Readme
- Adds a main gui that contains lots of important information about our server
@gui Readme
@alias readme
]]
local Event = require 'utils.event' --- @dep utils.event
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Commands = require 'expcore.commands' --- @dep expcore.commands
local PlayerData = require 'expcore.player_data' --- @dep expcore.player_data
local External = require 'expcore.external' --- @dep expcore.external
local format_time = _C.format_time --- @dep expcore.common
local format_number = require('util').format_number --- @dep util
local tabs = {}
local function Tab(caption, tooltip, element_define)
tabs[#tabs+1] = {caption, tooltip, element_define}
end
local frame_width = 595 -- controls width of top descriptions
local title_width = 270 -- controls the centering of the titles
local scroll_height = 275 -- controls the height of the scrolls
--- Sub content area used within the content areas
-- @element sub_content
local sub_content =
Gui.element{
type = 'frame',
direction = 'vertical',
style = 'inside_deep_frame'
}
:style{
horizontally_stretchable = true,
horizontal_align = 'center',
padding = {2, 2},
top_margin = 2
}
--- Table which has a title above it above it
-- @element title_table
local title_table =
Gui.element(function(_, parent, bar_size, caption, column_count)
Gui.title_label(parent, bar_size, caption)
return parent.add{
type = 'table',
column_count = column_count,
style = 'bordered_table'
}
end)
:style{
padding = 0,
cell_padding = 0,
vertical_align = 'center',
horizontally_stretchable = true
}
--- Scroll to be used with Gui.title_label tables
-- @element title_table_scroll
local title_table_scroll =
Gui.element{
type = 'scroll-pane',
direction = 'vertical',
horizontal_scroll_policy = 'never',
vertical_scroll_policy = 'auto',
style = 'scroll_pane_under_subheader'
}
:style{
padding = {1, 3},
maximal_height = scroll_height,
horizontally_stretchable = true,
}
--- Used to connect to servers in server list
-- @element join_server
local join_server =
Gui.element(function(_, parent, server_id, wrong_version)
local status = External.get_server_status(server_id) or 'Offline'
if wrong_version then status = 'Version' end
local flow = parent.add{ name = server_id, type = 'flow' }
local button = flow.add{
type = 'sprite-button',
sprite = 'utility/circuit_network_panel_white', --- network panel white, warning white, download white
hovered_sprite = 'utility/circuit_network_panel_black', --- network panel black, warning black, download black
tooltip = {'readme.servers-connect-'..status, wrong_version}
}
if status == 'Offline' or status == 'Current' then
button.enabled = false
button.sprite = 'utility/circuit_network_panel_black'
elseif status == 'Version' then
button.enabled = false
button.sprite = 'utility/shuffle'
elseif status == 'Password' then
button.sprite = 'utility/warning_white'
button.hovered_sprite = 'utility/warning'
elseif status == 'Modded' then
button.sprite = 'utility/downloading_white'
button.hovered_sprite = 'utility/downloading'
end
return button
end)
:style(Gui.sprite_style(20, -1))
:on_click(function(player, element, _)
local server_id = element.parent.name
External.request_connection(player, server_id, true)
end)
--- Content area for the welcome tab
-- @element welcome_content
Tab({'readme.welcome-tab'}, {'readme.welcome-tooltip'},
Gui.element(function(_, parent)
local server_details = { name='ExpGaming S0 - Local', welcome='Failed to load description: disconnected from external api.', reset_time='Non Set', branch='Unknown'}
if External.valid() then server_details = External.get_current_server() end
local container = parent.add{ type='flow', direction='vertical' }
local player = Gui.get_player_from_element(parent)
-- Set up the top flow with logos
local top_flow = container.add{ type='flow' }
top_flow.add{ type='sprite', sprite='file/modules/gui/logo.png' }
local top_vertical_flow = top_flow.add{ type='flow', direction='vertical' }
top_flow.add{ type='sprite', sprite='file/modules/gui/logo.png' }
top_vertical_flow.style.horizontal_align = 'center'
-- Add the title and description to the top flow
Gui.title_label(top_vertical_flow, 62, 'Welcome to '..server_details.name)
Gui.centered_label(top_vertical_flow, 380, server_details.welcome)
Gui.bar(container)
-- Get the names of the roles the player has
local player_roles = Roles.get_player_roles(player)
local role_names = {}
for i, role in ipairs(player_roles) do
role_names[i] = role.name
end
-- Add the other information to the gui
container.add{ type='flow' }.style.height = 4
local online_time = format_time(game.tick, {days=true, hours=true, minutes=true, long=true})
Gui.centered_label(sub_content(container), frame_width, {'readme.welcome-general', server_details.reset_time, online_time})
Gui.centered_label(sub_content(container), frame_width, {'readme.welcome-roles', table.concat(role_names, ', ')})
Gui.centered_label(sub_content(container), frame_width, {'readme.welcome-chat'})
return container
end))
--- Content area for the rules tab
-- @element rules_content
Tab({'readme.rules-tab'}, {'readme.rules-tooltip'},
Gui.element(function(_, parent)
local container = parent.add{ type='flow', direction='vertical' }
-- Add the title and description to the content
Gui.title_label(container, title_width-3, {'readme.rules-tab'})
Gui.centered_label(container, frame_width, {'readme.rules-general'})
Gui.bar(container)
container.add{ type='flow' }
-- Add a table for the rules
local rules = Gui.scroll_table(container, scroll_height, 1)
rules.style = 'bordered_table'
rules.style.cell_padding = 4
-- Add the rules to the table
for i = 1, 15 do
Gui.centered_label(rules, 565, {'readme.rules-'..i})
end
return container
end))
--- Content area for the commands tab
-- @element commands_content
Tab({'readme.commands-tab'}, {'readme.commands-tooltip'},
Gui.element(function(_, parent)
local container = parent.add{ type='flow', direction='vertical' }
local player = Gui.get_player_from_element(parent)
-- Add the title and description to the content
Gui.title_label(container, title_width-20, {'readme.commands-tab'})
Gui.centered_label(container, frame_width, {'readme.commands-general'})
Gui.bar(container)
container.add{ type='flow' }
-- Add a table for the commands
local commands = Gui.scroll_table(container, scroll_height, 2)
commands.style = 'bordered_table'
commands.style.cell_padding = 0
-- Add the rules to the table
for name, command in pairs(Commands.get(player)) do
Gui.centered_label(commands, 120, name)
Gui.centered_label(commands, 450, command.help)
end
return container
end))
--- Content area for the servers tab
-- @element servers_content
Tab({'readme.servers-tab'}, {'readme.servers-tooltip'},
Gui.element(function(_, parent)
local container = parent.add{ type='flow', direction='vertical' }
-- Add the title and description to the content
Gui.title_label(container, title_width-10, {'readme.servers-tab'})
Gui.centered_label(container, frame_width, {'readme.servers-general'})
Gui.bar(container)
container.add{ type='flow' }
-- Draw the scroll
local scroll_pane = title_table_scroll(container)
scroll_pane.style.maximal_height = scroll_height + 20 -- the text is a bit shorter
-- Add the factorio servers
if External.valid() then
local factorio_servers = title_table(scroll_pane, 225, {'readme.servers-factorio'}, 3)
local current_version = External.get_current_server().version
for server_id, server in pairs(External.get_servers()) do
Gui.centered_label(factorio_servers, 110, server.short_name)
Gui.centered_label(factorio_servers, 436, server.description)
join_server(factorio_servers, server_id, current_version ~= server.version and server.version)
end
else
local factorio_servers = title_table(scroll_pane, 225, {'readme.servers-factorio'}, 2)
for i = 1, 8 do
Gui.centered_label(factorio_servers, 110, {'readme.servers-'..i})
Gui.centered_label(factorio_servers, 460, {'readme.servers-d'..i})
end
end
-- Add the external links
local external_links = title_table(scroll_pane, 235, {'readme.servers-external'}, 2)
for _, key in ipairs{'discord', 'website', 'patreon', 'status', 'github'} do
local upper_key = key:gsub("^%l", string.upper)
Gui.centered_label(external_links, 110, upper_key)
Gui.centered_label(external_links, 460, {'links.'..key}, {'readme.servers-open-in-browser'})
end
return container
end))
--- Content area for the servers tab
-- @element backers_content
Tab({'readme.backers-tab'}, {'readme.backers-tooltip'},
Gui.element(function(_, parent)
local container = parent.add{ type='flow', direction='vertical' }
-- Add the title and description to the content
Gui.title_label(container, title_width-10, {'readme.backers-tab'})
Gui.centered_label(container, frame_width, {'readme.backers-general'})
Gui.bar(container)
container.add{ type='flow' }
-- Find which players will go where
local done = {}
local groups = {
{ _roles={'Senior Administrator', 'Administrator'}, _title={'readme.backers-management'}, _width=230 },
{ _roles={'Board Member', 'Senior Backer'}, _title={'readme.backers-board'}, _width=145 }, -- change role to board
{ _roles={'Sponsor', 'Supporter'}, _title={'readme.backers-backers'}, _width=196 }, -- change to backer
{ _roles={'Moderator', 'Trainee'}, _title={'readme.backers-staff'}, _width=235 },
{ _roles={}, _time=3*3600*60, _title={'readme.backers-active'}, _width=235 },
}
-- Fill by player roles
for player_name, player_roles in pairs(Roles.config.players) do
for _, players in ipairs(groups) do
for _, role_name in pairs(players._roles) do
if table.contains(player_roles, role_name) then
done[player_name] = true
table.insert(players, player_name)
break
end
end
end
end
-- Fill by active times
for _, player in pairs(game.players) do
if not done[player.name] then
for _, players in ipairs(groups) do
if players._time and player.online_time > players._time then
table.insert(players, player.name)
end
end
end
end
-- Add the different tables
local scroll_pane = title_table_scroll(container)
for _, players in ipairs(groups) do
local table = title_table(scroll_pane, players._width, players._title, 4)
for _, player_name in ipairs(players) do
Gui.centered_label(table, 140, player_name)
end
if #players < 4 then
for i = 1, 4-#players do
Gui.centered_label(table, 140)
end
end
end
return container
end))
--- Content area for the player data tab
-- @element commands_content
Tab({'readme.data-tab'}, {'readme.data-tooltip'},
Gui.element(function(_, parent)
local container = parent.add{ type='flow', direction='vertical' }
local player = Gui.get_player_from_element(parent)
local player_name = player.name
local enum = PlayerData.PreferenceEnum
local preference = PlayerData.DataSavingPreference:get(player_name)
local preference_meta = PlayerData.DataSavingPreference.metadata
preference = enum[preference]
-- Add the title and description to the content
Gui.title_label(container, title_width, {'readme.data-tab'})
Gui.centered_label(container, frame_width, {'readme.data-general'})
Gui.bar(container)
container.add{ type='flow' }
local scroll_pane = title_table_scroll(container)
-- Add the required area
local required = title_table(scroll_pane, 250, {'readme.data-required'}, 2)
Gui.centered_label(required, 150, preference_meta.name, preference_meta.tooltip)
Gui.centered_label(required, 420, {'expcore-data.preference-'..enum[preference]}, preference_meta.value_tooltip)
for name, child in pairs(PlayerData.Required.children) do
local metadata = child.metadata
local value = child:get(player_name)
if value ~= nil or metadata.show_always then
if metadata.stringify then value = metadata.stringify(value) end
Gui.centered_label(required, 150, metadata.name or {'exp-required.'..name}, metadata.tooltip or {'exp-required.'..name..'-tooltip'})
Gui.centered_label(required, 420, tostring(value), metadata.value_tooltip or {'exp-required.'..name..'-value-tooltip'})
end
end
-- Add the settings area
if preference <= enum.Settings then
local settings = title_table(scroll_pane, 255, {'readme.data-settings'}, 2)
for name, child in pairs(PlayerData.Settings.children) do
local metadata = child.metadata
local value = child:get(player_name)
if not metadata.permission or Roles.player_allowed(player, metadata.permission) then
if metadata.stringify then value = metadata.stringify(value) end
if value == nil then value = 'None set' end
Gui.centered_label(settings, 150, metadata.name or {'exp-settings.'..name}, metadata.tooltip or {'exp-settings.'..name..'-tooltip'})
Gui.centered_label(settings, 420, tostring(value), metadata.value_tooltip or {'exp-settings.'..name..'-value-tooltip'})
end
end
end
-- Add the statistics area
if preference <= enum.Statistics then
local count = 4
local statistics = title_table(scroll_pane, 250, {'readme.data-statistics'}, 4)
for _, name in pairs(PlayerData.Statistics.metadata.display_order) do
local child = PlayerData.Statistics[name]
local metadata = child.metadata
local value = child:get(player_name)
if value ~= nil or metadata.show_always then
count = count - 2
if metadata.stringify then value = metadata.stringify(value)
else value = format_number(value or 0) end
Gui.centered_label(statistics, 150, metadata.name or {'exp-statistics.'..name}, metadata.tooltip or {'exp-statistics.'..name..'-tooltip'})
Gui.centered_label(statistics, 130, {'readme.data-format', value, metadata.unit or ''}, metadata.value_tooltip or {'exp-statistics.'..name..'-tooltip'})
end
end
if count > 0 then for i = 1, count do Gui.centered_label(statistics, 140) end end
end
-- Add the misc area
local skip = {DataSavingPreference=true, Settings=true, Statistics=true, Required=true}
local count = 0; for _ in pairs(PlayerData.All.children) do count = count + 1 end
if preference <= enum.All and count > 4 then
local misc = title_table(scroll_pane, 232, {'readme.data-misc'}, 2)
for name, child in pairs(PlayerData.All.children) do
if not skip[name] then
local metadata = child.metadata
local value = child:get(player_name)
if value ~= nil or metadata.show_always then
if metadata.stringify then value = metadata.stringify(value) end
Gui.centered_label(misc, 150, metadata.name or name, metadata.tooltip)
Gui.centered_label(misc, 420, tostring(value), metadata.value_tooltip)
end
end
end
end
return container
end))
--- Main readme container for the center flow
-- @element readme
local readme_toggle
local readme =
Gui.element(function(definition, parent)
local container = parent.add{
name = definition.name,
type = 'frame',
style = 'invisible_frame'
}
-- Add the left hand side of the frame back, removed because of frame_tabbed_pane style
local left_alignment = Gui.alignment(container, nil, nil, 'bottom')
left_alignment.style.padding = {32, 0,0, 0}
local left_side =
left_alignment.add{
type = 'frame',
style = 'frame_without_right_side'
}
left_side.style.vertically_stretchable = true
left_side.style.padding = 0
left_side.style.width = 5
-- Add the tab pane
local tab_pane = container.add{
name = 'pane',
type = 'tabbed-pane',
style = 'frame_tabbed_pane'
}
-- Add the different content areas
for _, tab_details in ipairs(tabs) do
local tab = tab_pane.add{ type = 'tab', style = 'frame_tab', caption = tab_details[1], tooltip = tab_details[2] }
tab_pane.add_tab(tab, tab_details[3](tab_pane))
end
return container
end)
:static_name(Gui.unique_static_name)
:on_open(function(player)
Gui.toggle_toolbar_button(player, readme_toggle, true)
end)
:on_close(function(player, element)
Gui.toggle_toolbar_button(player, readme_toggle, false)
Gui.destroy_if_valid(element)
end)
--- Toggle button for the readme gui
-- @element readme_toggle
readme_toggle =
Gui.toolbar_button('virtual-signal/signal-info', {'readme.main-tooltip'}, function(player)
return Roles.player_allowed(player, 'gui/readme')
end)
:on_click(function(player, _)
local center = player.gui.center
if center[readme.name] then
player.opened = nil
else
player.opened = readme(center)
end
end)
--- When a player joins the game for the first time show this gui
Event.add(defines.events.on_player_created, function(event)
local player = game.players[event.player_index]
local element = readme(player.gui.center)
element.pane.selected_tab_index = 1
player.opened = element
end)
--- When a player joins clear center unless the player has something open
Event.add(defines.events.on_player_joined_game, function(event)
local player = game.players[event.player_index]
if not player.opened then
player.gui.center.clear()
end
end)
--- When a player respawns clear center unless the player has something open
Event.add(defines.events.on_player_respawned, function(event)
local player = game.players[event.player_index]
if not player.opened then
player.gui.center.clear()
end
end)

View File

@@ -0,0 +1,328 @@
--- research gui
-- @gui Research
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Global = require 'utils.global' --- @dep utils.global
local Event = require 'utils.event' --- @dep utils.event
local Roles = require 'expcore.roles' --- @dep expcore.roles
local config = require 'config.research' --- @dep config.research
local format_time = _C.format_time --- @dep expcore.common
local research = {}
Global.register(research, function(tbl)
research = tbl
end)
research.time = {}
research.res_queue_enable = false
local research_time_format = {
hours=true,
minutes=true,
seconds=true,
time=true,
string=true
}
local empty_time = format_time(0, {
hours=true,
minutes=true,
seconds=true,
time=true,
string=true,
null=true
})
local font_color = {
-- positive
[1] = {r = 0.3, g = 1, b = 0.3},
-- negative
[2] = {r = 1, g = 0.3, b = 0.3}
}
local res = {
['lookup_name'] = {},
['disp'] = {}
}
do
local res_total = 0
local i = 1
for k, v in pairs(config.milestone) do
research.time[i] = 0
res['lookup_name'][k] = i
res_total = res_total + v * 60
res['disp'][i] = {
raw_name = k,
target = res_total,
target_disp = format_time(res_total, research_time_format),
}
i = i + 1
end
end
local function research_add_log()
local result_data = {}
for i=1, #research.time, 1 do
result_data[res['disp'][i]['raw_name']] = research.time[i]
end
game.write_file(config.file_name, game.table_to_json(result_data) .. '\n', true, 0)
end
local function research_res_n(res_)
local res_n = 1
for k, _ in pairs(res_) do
if research.time[k] == 0 then
res_n = k - 1
break
end
end
if research.time[#res_] and research.time[#res_] > 0 then
if res_n == 1 then
res_n = #res_
end
end
if res_n < 3 then
res_n = 3
elseif res_n > (#research.time - 5) then
res_n = #research.time - 5
end
return res_n
end
local function research_notification(event)
if config.inf_res[event.research.name] then
if event.research.name == 'mining-productivity-4' then
if event.research.level == 5 then
-- Add run result to log
research_add_log()
end
if config.bonus_inventory.enabled then
if (event.research.level - 1) <= math.ceil(config.bonus_inventory.limit / config.bonus_inventory.rate) then
event.research.force[config.bonus_inventory.name] = math.max((event.research.level - 1) * config.bonus_inventory.rate, config.bonus_inventory.limit)
end
end
if config.pollution_ageing_by_research then
game.map_settings.pollution.ageing = math.min(10, event.research.level / 5)
end
else
if not (event.by_script) then
game.print{'expcom-res.inf', format_time(game.tick, research_time_format), event.research.name, event.research.level - 1}
end
end
else
if not (event.by_script) then
game.print{'expcom-res.msg', format_time(game.tick, research_time_format), event.research.name}
end
if config.bonus_inventory.enabled then
if event.research.name == 'mining-productivity-1' or event.research.name == 'mining-productivity-2' or event.research.name == 'mining-productivity-3' then
event.research.force[config.bonus_inventory.name] = event.research.level * config.bonus_inventory.rate
end
end
end
end
local function research_gui_update()
local res_disp = {}
local res_n = research_res_n(res['disp'])
for i=1, 8, 1 do
res_disp[i] = {
['name'] = '',
['target'] = '',
['attempt'] = '',
['difference'] = '',
['difference_color'] = font_color[1]
}
local res_i = res_n + i - 3
if res['disp'][res_i] then
res_disp[i]['name'] = {'expcom-res.res-name', res['disp'][res_i]['raw_name'], game.technology_prototypes[res['disp'][res_i]['raw_name']].localised_name}
if research.time[res_i] == 0 then
res_disp[i]['target'] = res['disp'][res_i].target_disp
res_disp[i]['attempt'] = empty_time
res_disp[i]['difference'] = empty_time
res_disp[i]['difference_color'] = font_color[1]
else
res_disp[i]['target'] = res['disp'][res_i].target_disp
res_disp[i]['attempt'] = format_time(research.time[res_i], research_time_format)
if research.time[res_i] < res['disp'][res_i].target then
res_disp[i]['difference'] = '-' .. format_time(res['disp'][res_i].target - research.time[res_i], research_time_format)
res_disp[i]['difference_color'] = font_color[1]
else
res_disp[i]['difference'] = format_time(research.time[res_i] - res['disp'][res_i].target, research_time_format)
res_disp[i]['difference_color'] = font_color[2]
end
end
end
end
return res_disp
end
--- Display label for the clock display
-- @element research_gui_clock_display
local research_gui_clock =
Gui.element{
type = 'label',
name = Gui.unique_static_name,
caption = empty_time,
style = 'heading_2_label'
}
--- A vertical flow containing the clock
-- @element research_clock_set
local research_clock_set =
Gui.element(function(_, parent, name)
local research_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.scroll_table(research_set, 390, 1, 'disp')
research_gui_clock(disp)
return research_set
end)
--- Display group
-- @element research_data_group
local research_data_group =
Gui.element(function(_definition, parent, i)
local name = parent.add{
type = 'label',
name = 'research_' .. i .. '_name',
caption = '',
style = 'heading_2_label'
}
name.style.width = 180
name.style.horizontal_align = 'left'
local target = parent.add{
type = 'label',
name = 'research_' .. i .. '_target',
caption = '',
style = 'heading_2_label'
}
target.style.width = 70
target.style.horizontal_align = 'right'
local attempt = parent.add{
type = 'label',
name = 'research_' .. i .. '_attempt',
caption = '',
style = 'heading_2_label'
}
attempt.style.width = 70
attempt.style.horizontal_align = 'right'
local difference = parent.add{
type = 'label',
name = 'research_' .. i .. '_difference',
caption = '',
style = 'heading_2_label'
}
difference.style.width = 70
difference.style.horizontal_align = 'right'
difference.style.font_color = font_color[1]
end)
--- A vertical flow containing the data
-- @element research_data_set
local research_data_set =
Gui.element(function(_, parent, name)
local research_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.scroll_table(research_set, 390, 4, 'disp')
local res_disp = research_gui_update()
research_data_group(disp, 0)
disp['research_0_name'].caption = {'expcom-res.name'}
disp['research_0_target'].caption = {'expcom-res.target'}
disp['research_0_attempt'].caption = {'expcom-res.attempt'}
disp['research_0_difference'].caption = {'expcom-res.difference'}
for i=1, 8, 1 do
research_data_group(disp, i)
local research_name_i = 'research_' .. i
disp[research_name_i .. '_name'].caption = res_disp[i]['name']
disp[research_name_i .. '_target'].caption = res_disp[i]['target']
disp[research_name_i .. '_attempt'].caption = res_disp[i]['attempt']
disp[research_name_i .. '_difference'].caption = res_disp[i]['difference']
disp[research_name_i .. '_difference'].style.font_color = res_disp[i]['difference_color']
end
return research_set
end)
local research_container =
Gui.element(function(definition, parent)
local container = Gui.container(parent, definition.name, 390)
research_clock_set(container, 'research_st_1')
research_data_set(container, 'research_st_2')
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
Gui.left_toolbar_button('item/space-science-pack', {'expcom-res.main-tooltip'}, research_container, function(player)
return Roles.player_allowed(player, 'gui/research')
end)
Event.add(defines.events.on_research_finished, function(event)
research_notification(event)
if res['lookup_name'][event.research.name] == nil then
return
end
local n_i = res['lookup_name'][event.research.name]
research.time[n_i] = game.tick
local res_disp = research_gui_update()
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, research_container)
local disp = frame.container['research_st_2'].disp.table
for i=1, 8, 1 do
local research_name_i = 'research_' .. i
disp[research_name_i .. '_name'].caption = res_disp[i]['name']
disp[research_name_i .. '_target'].caption = res_disp[i]['target']
disp[research_name_i .. '_attempt'].caption = res_disp[i]['attempt']
disp[research_name_i .. '_difference'].caption = res_disp[i]['difference']
disp[research_name_i .. '_difference'].style.font_color = res_disp[i]['difference_color']
end
end
end)
Event.on_nth_tick(60, function()
local current_time = format_time(game.tick, research_time_format)
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, research_container)
local disp = frame.container['research_st_1'].disp.table
disp[research_gui_clock.name].caption = current_time
end
end)

View File

@@ -0,0 +1,610 @@
--[[-- Gui Module - Rocket Info
- Adds a rocket infomation gui which shows general stats, milestones and build progress of rockets
@gui Rocket-Info
@alias rocket_info
]]
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Event = require 'utils.event' --- @dep utils.event
local config = require 'config.gui.rockets' --- @dep config.gui.rockets
local Colors = require 'utils.color_presets' --- @dep utils.color_presets
local Rockets = require 'modules.control.rockets' --- @dep modules.control.rockets
local format_time = _C.format_time --- @dep expcore.common
local time_formats = {
caption = function(value) return format_time(value, {minutes=true, seconds=true}) end,
caption_hours = function(value) return format_time(value) end,
tooltip = function(value) return format_time(value, {minutes=true, seconds=true, long=true}) end,
tooltip_hours = function(value) return format_time(value, {hours=true, minutes=true, seconds=true, long=true}) end
}
--- Check if a player is allowed to use certain interactions
local function check_player_permissions(player, action)
if not config.progress['allow_'..action] then
return false
end
if config.progress[action..'_admins_only'] and not player.admin then
return false
end
if config.progress[action..'_role_permission']
and not Roles.player_allowed(player, config.progress[action..'_role_permission']) then
return false
end
return true
end
--- Button to toggle the auto launch on a rocket silo
-- @element toggle_launch
local toggle_launch =
Gui.element{
type = 'sprite-button',
sprite = 'utility/play',
tooltip = {'rocket-info.toggle-rocket-tooltip'},
name = Gui.unique_static_name
}
:style(Gui.sprite_style(16))
:on_click(function(_, element, _)
local rocket_silo_name = element.parent.name:sub(8)
local rocket_silo = Rockets.get_silo_entity(rocket_silo_name)
if rocket_silo.auto_launch then
element.sprite = 'utility/play'
element.tooltip = {'rocket-info.toggle-rocket-tooltip'}
rocket_silo.auto_launch = false
else
element.sprite = 'utility/stop'
element.tooltip = {'rocket-info.toggle-rocket-tooltip-disabled'}
rocket_silo.auto_launch = true
end
end)
--- Button to remotely launch a rocket from a silo
-- @element launch_rocket
local launch_rocket =
Gui.element{
type = 'sprite-button',
sprite = 'utility/center',
tooltip = {'rocket-info.launch-tooltip'},
name = Gui.unique_static_name
}
:style(Gui.sprite_style(16, -1))
:on_click(function(player, element, _)
local rocket_silo_name = element.parent.name:sub(8)
local silo_data = Rockets.get_silo_data_by_name(rocket_silo_name)
if silo_data.entity.launch_rocket() then
element.enabled = false
else
player.print({'rocket-info.launch-failed'}, Colors.orange_red)
end
end)
--- XY cords that allow zoom to map when pressed
-- @element silo_cords
local silo_cords =
Gui.element(function(definition, parent, silo_data)
local silo_name = silo_data.silo_name
local pos = silo_data.position
local tooltip = config.progress.allow_zoom_to_map and {'rocket-info.progress-label-tooltip'} or nil
-- Add the x cord flow
local flow_x = parent.add{
type ='flow',
name = 'label-x-'..silo_name,
caption = silo_name
}
flow_x.style.padding = {0, 2,0, 1}
-- Add the x cord label
local label_x = flow_x.add{
type = 'label',
caption = {'rocket-info.progress-x-pos', pos.x},
tooltip = tooltip
}
-- Add the y cord flow
local flow_y = parent.add{
type ='flow',
name = 'label-y-'..silo_name,
caption = silo_name
}
flow_y.style.padding = {0, 2,0, 1}
-- Add the y cord label
local label_y = flow_y.add{
type = 'label',
caption = {'rocket-info.progress-y-pos', pos.y},
tooltip = tooltip
}
if config.progress.allow_zoom_to_map then
definition:triggers_events(label_x)
definition:triggers_events(label_y)
end
end)
:on_click(function(player, element, _)
local rocket_silo_name = element.parent.caption
local rocket_silo = Rockets.get_silo_entity(rocket_silo_name)
player.zoom_to_world(rocket_silo.position, 2)
end)
--- Base element for each rocket in the progress list
-- @element rocket_entry
local rocket_entry =
Gui.element(function(_, parent, silo_data)
local silo_name = silo_data.silo_name
local player = Gui.get_player_from_element(parent)
-- Add the toggle auto launch if the player is allowed it
if check_player_permissions(player, 'toggle_active') then
local flow = parent.add{ type = 'flow', name = 'toggle-'..silo_name}
local button = toggle_launch(flow)
button.tooltip = silo_data.toggle_tooltip
button.sprite = silo_data.toggle_sprite
end
-- Add the remote launch if the player is allowed it
if check_player_permissions(player, 'remote_launch') then
local flow = parent.add{ type = 'flow', name = 'launch-'..silo_name}
local button = launch_rocket(flow)
button.enabled = silo_data.allow_launch
end
-- Draw the silo cords element
silo_cords(parent, silo_data)
-- Add a progress label
local alignment = Gui.alignment(parent, silo_name)
local element =
alignment.add{
type = 'label',
name = 'label',
caption = silo_data.progress_caption,
tooltip = silo_data.progress_tooltip
}
-- Return the progress label
return element
end)
--- Data label which contains a name and a value label pair
-- @element data_label
local data_label =
Gui.element(function(_, parent, label_data)
local data_name = label_data.name
local data_subname = label_data.subname
local data_fullname = data_subname and data_name..data_subname or data_name
-- Add the name label
local name_label = parent.add{
type = 'label',
name = data_fullname..'-label',
caption = {'rocket-info.data-caption-'..data_name, data_subname},
tooltip = {'rocket-info.data-tooltip-'..data_name, data_subname}
}
name_label.style.padding = {0, 2}
--- Right aligned label to store the data
local alignment = Gui.alignment(parent, data_fullname)
local element =
alignment.add{
type = 'label',
name = 'label',
caption = label_data.value,
tooltip = label_data.tooltip
}
element.style.padding = {0, 2}
return element
end)
-- Used to update the captions and tooltips on the data labels
local function update_data_labels(parent, data_label_data)
for _, label_data in ipairs(data_label_data) do
local data_name = label_data.subname and label_data.name..label_data.subname or label_data.name
if not parent[data_name] then
data_label(parent, label_data)
else
local data_label_element = parent[data_name].label
data_label_element.tooltip = label_data.tooltip
data_label_element.caption = label_data.value
end
end
end
local function get_progress_data(force_name)
local force_silos = Rockets.get_silos(force_name)
local progress_data = {}
for _, silo_data in pairs(force_silos) do
local rocket_silo = silo_data.entity
if not rocket_silo or not rocket_silo.valid then
-- Remove from list if not valid
force_silos[silo_data.name] = nil
table.insert(progress_data, {
silo_name = silo_data.name,
remove = true
})
else
-- Get the progress caption and tooltip
local progress_color = Colors.white
local progress_caption = {'rocket-info.progress-caption', rocket_silo.rocket_parts}
local progress_tooltip = {'rocket-info.progress-tooltip', silo_data.launched or 0}
local status = rocket_silo.status == defines.entity_status.waiting_to_launch_rocket
if status and silo_data.awaiting_reset then
progress_caption = {'rocket-info.progress-launched'}
progress_color = Colors.green
elseif status then
progress_caption = {'rocket-info.progress-caption', 100}
progress_color = Colors.cyan
else
silo_data.awaiting_reset = false
end
-- Get the toggle button data
local toggle_tooltip = {'rocket-info.toggle-rocket-tooltip-disabled'}
local toggle_sprite = 'utility/play'
if rocket_silo.auto_launch then
toggle_tooltip = {'rocket-info.toggle-rocket-tooltip'}
toggle_sprite = 'utility/stop'
end
-- Insert the gui data
table.insert(progress_data, {
silo_name = silo_data.name,
position = rocket_silo.position,
allow_launch = not silo_data.awaiting_reset and status or false,
progress_color = progress_color,
progress_caption = progress_caption,
progress_tooltip = progress_tooltip,
toggle_tooltip = toggle_tooltip,
toggle_sprite = toggle_sprite
})
end
end
return progress_data
end
--- Update the build progress section
local function update_build_progress(parent, progress_data)
local show_message = true
for _, silo_data in ipairs(progress_data) do
parent.parent.no_silos.visible = false
parent.visible = true
local silo_name = silo_data.silo_name
local progress_label = parent[silo_name]
if silo_data.remove then
-- Remove the rocket from the list
Gui.destroy_if_valid(parent['toggle-'..silo_name])
Gui.destroy_if_valid(parent['launch-'..silo_name])
Gui.destroy_if_valid(parent['label-x-'..silo_name])
Gui.destroy_if_valid(parent['label-y-'..silo_name])
Gui.destroy_if_valid(parent[silo_name])
elseif not progress_label then
-- Add the rocket to the list
show_message = false
rocket_entry(parent, silo_data)
else
show_message = false
-- Update the existing labels
progress_label = progress_label.label
progress_label.caption = silo_data.progress_caption
progress_label.tooltip = silo_data.progress_tooltip
progress_label.style.font_color = silo_data.progress_color
-- Update the toggle button
local toggle_button = parent['toggle-'..silo_name]
if toggle_button then
toggle_button = toggle_button[toggle_launch.name]
toggle_button.tooltip = silo_data.toggle_tooltip
toggle_button.sprite = silo_data.toggle_sprite
end
-- Update the launch button
local launch_button = parent['launch-'..silo_name]
if launch_button then
launch_button = launch_button[launch_rocket.name]
launch_button.enabled = silo_data.allow_launch
end
end
end
if show_message then parent.parent.no_silos.visible = true parent.visible = false end
end
--- Gets the label data for all the different stats
local function get_stats_data(force_name)
local force_rockets = Rockets.get_rocket_count(force_name)
local stats = Rockets.get_stats(force_name)
local stats_data = {}
-- Format the first launch data
if config.stats.show_first_rocket then
local value = stats.first_launch or 0
table.insert(stats_data, {
name = 'first-launch',
value = time_formats.caption_hours(value),
tooltip = time_formats.tooltip_hours(value)
})
end
-- Format the last launch data
if config.stats.show_last_rocket then
local value = stats.last_launch or 0
table.insert(stats_data, {
name = 'last-launch',
value = time_formats.caption_hours(value),
tooltip = time_formats.tooltip_hours(value)
})
end
-- Format fastest launch data
if config.stats.show_fastest_rocket then
local value = stats.fastest_launch or 0
table.insert(stats_data, {
name = 'fastest-launch',
value = time_formats.caption_hours(value),
tooltip = time_formats.tooltip_hours(value)
})
end
-- Format total rocket data
if config.stats.show_total_rockets then
local total_rockets = Rockets.get_game_rocket_count()
total_rockets = total_rockets == 0 and 1 or total_rockets
local percentage = math.round(force_rockets/total_rockets, 3)*100
table.insert(stats_data, {
name = 'total-rockets',
value = force_rockets,
tooltip = {'rocket-info.value-tooltip-total-rockets', percentage}
})
end
-- Format game avg data
if config.stats.show_game_avg then
local avg = force_rockets > 0 and math.floor(game.tick/force_rockets) or 0
table.insert(stats_data, {
name = 'avg-launch',
value = time_formats.caption(avg),
tooltip = time_formats.tooltip(avg)
})
end
-- Format rolling avg data
for _, avg_over in pairs(config.stats.rolling_avg) do
local avg = Rockets.get_rolling_average(force_name, avg_over)
table.insert(stats_data, {
name = 'avg-launch-n',
subname = avg_over,
value = time_formats.caption(avg),
tooltip = time_formats.tooltip(avg)
})
end
-- Return formated data
return stats_data
end
--- Gets the label data for the milestones
local function get_milestone_data(force_name)
local force_rockets = Rockets.get_rocket_count(force_name)
local milestone_data = {}
for _, milestone in ipairs(config.milestones) do
if milestone <= force_rockets then
local time = Rockets.get_rocket_time(force_name, milestone)
table.insert(milestone_data, {
name = 'milestone-n',
subname = milestone,
value = time_formats.caption_hours(time),
tooltip = time_formats.tooltip_hours(time)
})
else
table.insert(milestone_data, {
name = 'milestone-n',
subname = milestone,
value = {'rocket-info.data-caption-milestone-next'},
tooltip = {'rocket-info.data-tooltip-milestone-next'}
})
break
end
end
return milestone_data
end
-- Button to toggle a section dropdown
-- @element toggle_section
local toggle_section =
Gui.element{
type = 'sprite-button',
sprite = 'utility/expand_dark',
hovered_sprite = 'utility/expand',
tooltip = {'rocket-info.toggle-section-tooltip'},
name = Gui.unique_static_name
}
:style(Gui.sprite_style(20))
:on_click(function(_, element, _)
local header_flow = element.parent
local flow_name = header_flow.caption
local flow = header_flow.parent.parent[flow_name]
if Gui.toggle_visible_state(flow) then
element.sprite = 'utility/collapse_dark'
element.hovered_sprite = 'utility/collapse'
element.tooltip = {'rocket-info.toggle-section-collapse-tooltip'}
else
element.sprite = 'utility/expand_dark'
element.hovered_sprite = 'utility/expand'
element.tooltip = {'rocket-info.toggle-section-tooltip'}
end
end)
-- Draw a section header and main scroll
-- @element rocket_list_container
local section =
Gui.element(function(definition, parent, section_name, table_size)
-- Draw the header for the section
local header = Gui.header(
parent,
{'rocket-info.section-caption-'..section_name},
{'rocket-info.section-tooltip-'..section_name},
true,
section_name..'-header'
)
definition:triggers_events(header.parent.header_label)
-- Right aligned button to toggle the section
header.caption = section_name
toggle_section(header)
-- Table used to store the data
local scroll_table = Gui.scroll_table(parent, 215, table_size, section_name)
scroll_table.parent.visible = false
-- Return the flow table
return definition:no_events(scroll_table)
end)
:on_click(function(_, element, event)
event.element = element.parent.alignment[toggle_section.name]
toggle_section:raise_event(event)
end)
--- Main gui container for the left flow
-- @element rocket_list_container
local rocket_list_container =
Gui.element(function(definition, parent)
-- Draw the internal container
local container = Gui.container(parent, definition.name, 200)
-- Set the container style
local style = container.style
style.padding = 0
local player = Gui.get_player_from_element(parent)
local force_name = player.force.name
-- Draw stats section
if config.stats.show_stats then
update_data_labels(section(container, 'stats', 2), get_stats_data(force_name))
end
-- Draw milestones section
if config.milestones.show_milestones then
update_data_labels(section(container, 'milestones', 2), get_milestone_data(force_name))
end
-- Draw build progress list
if config.progress.show_progress then
local col_count = 3
if check_player_permissions(player, 'remote_launch') then col_count = col_count+1 end
if check_player_permissions(player, 'toggle_active') then col_count = col_count+1 end
local progress = section(container, 'progress', col_count)
-- Label used when there are no active silos
local no_silos = progress.parent.add{
type = 'label',
name = 'no_silos',
caption = {'rocket-info.progress-no-silos'}
}
no_silos.style.padding = {1, 2}
update_build_progress(progress, get_progress_data(force_name))
end
-- Return the exteral container
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow(function(player)
return player.force.rockets_launched > 0 and Roles.player_allowed(player, 'gui/rocket-info')
end)
--- Button on the top flow used to toggle the container
-- @element toggle_rocket_info
Gui.left_toolbar_button('item/satellite', {'rocket-info.main-tooltip'}, rocket_list_container, function(player)
return Roles.player_allowed(player, 'gui/rocket-info')
end)
--- Update the gui for all players on a force
local function update_rocket_gui_all(force_name)
local stats = get_stats_data(force_name)
local milestones = get_milestone_data(force_name)
local progress = get_progress_data(force_name)
for _, player in pairs(game.forces[force_name].players) do
local frame = Gui.get_left_element(player, rocket_list_container)
local container = frame.container
update_data_labels(container.stats.table, stats)
update_data_labels(container.milestones.table, milestones)
update_build_progress(container.progress.table, progress)
end
end
--- Event used to update the stats when a rocket is launched
Event.add(defines.events.on_rocket_launched, function(event)
local force = event.rocket_silo.force
update_rocket_gui_all(force.name)
end)
--- Update only the progress gui for a force
local function update_rocket_gui_progress(force_name)
local progress = get_progress_data(force_name)
for _, player in pairs(game.forces[force_name].players) do
local frame = Gui.get_left_element(player, rocket_list_container)
local container = frame.container
update_build_progress(container.progress.table, progress)
end
end
--- Event used to set a rocket silo to be awaiting reset
Event.add(defines.events.on_rocket_launch_ordered, function(event)
local silo = event.rocket_silo
local silo_data = Rockets.get_silo_data(silo)
silo_data.awaiting_reset = true
update_rocket_gui_progress(silo.force.name)
end)
Event.on_nth_tick(150, function()
for _, force in pairs(game.forces) do
if #Rockets.get_silos(force.name) > 0 then
update_rocket_gui_progress(force.name)
end
end
end)
--- Adds a silo to the list when it is built
local function on_built(event)
local entity = event.created_entity
if entity.valid and entity.name == 'rocket-silo' then
update_rocket_gui_progress(entity.force.name)
end
end
Event.add(defines.events.on_built_entity, on_built)
Event.add(defines.events.on_robot_built_entity, on_built)
--- Redraw the progress section on role change
local function role_update_event(event)
if not config.progress.show_progress then return end
local player = game.players[event.player_index]
local container = Gui.get_left_element(player, rocket_list_container).container
local progress_scroll = container.progress
Gui.destroy_if_valid(progress_scroll.table)
local col_count = 3
if check_player_permissions(player, 'remote_launch') then col_count = col_count+1 end
if check_player_permissions(player, 'toggle_active') then col_count = col_count+1 end
local progress = progress_scroll.add{
type = 'table',
name = 'table',
column_count = col_count
}
update_build_progress(progress, get_progress_data(player.force.name))
end
Event.add(Roles.events.on_role_assigned, role_update_event)
Event.add(Roles.events.on_role_unassigned, role_update_event)
return rocket_list_container

View File

@@ -0,0 +1,374 @@
--[[-- Gui Module - Science Info
- Adds a science info gui that shows production usage and net for the different science packs as well as an eta
@gui Science-Info
@alias science_info
]]
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.gui
local Event = require 'utils.event' --- @dep utils.event
local config = require 'config.gui.science' --- @dep config.gui.science
local Production = require 'modules.control.production' --- @dep modules.control.production
local format_time = _C.format_time --- @dep expcore.common
local null_time_short = {'science-info.eta-time', format_time(0, {hours=true, minutes=true, seconds=true, time=true, null=true})}
local null_time_long = format_time(0, {hours=true, minutes=true, seconds=true, long=true, null=true})
--- Data label that contains the value and the surfix
-- @element production_label
local production_label =
Gui.element(function(_, parent, production_label_data)
local name = production_label_data.name
local tooltip = production_label_data.tooltip
local color = production_label_data.color
-- Add an alignment for the number
local alignment = Gui.alignment(parent, name)
-- Add the main value label
local element =
alignment.add{
name = 'label',
type = 'label',
caption = production_label_data.caption,
tooltip = tooltip
}
-- Change the style
element.style.font_color = color
-- Add the surfix label
local surfix_element =
parent.add{
name = 'surfix-'..name,
type = 'label',
caption = {'science-info.unit', production_label_data.surfix},
tooltip = tooltip
}
-- Change the style
local surfix_element_style = surfix_element.style
surfix_element_style.font_color = color
surfix_element_style.right_margin = 1
-- Return the value label
return element
end)
-- Get the data that is used with the production label
local function get_production_label_data(name, tooltip, value, cutout, secondary)
local data_colour = Production.get_color(config.color_cutoff * cutout, value, secondary)
local surfix, caption = Production.format_number(value)
return {
name = name,
caption = caption,
surfix = surfix,
tooltip = tooltip,
color = data_colour
}
end
-- Updates a prodution label to match the current data
local function update_production_label(parent, production_label_data)
local name = production_label_data.name
local tooltip = production_label_data.tooltip
local color = production_label_data.color
-- Update the production label
local production_label_element = parent[name] and parent[name].label or production_label(parent, production_label_data)
production_label_element.caption = production_label_data.caption
production_label_element.tooltip = production_label_data.tooltip
production_label_element.style.font_color = color
-- Update the surfix label
local surfix_element = parent['surfix-'..name]
surfix_element.caption = {'science-info.unit', production_label_data.surfix}
surfix_element.tooltip = tooltip
surfix_element.style.font_color = color
end
--- Adds 4 elements that show the data for a science pack
-- @element science_pack_base
local science_pack_base =
Gui.element(function(_, parent, science_pack_data)
local science_pack = science_pack_data.science_pack
-- Draw the icon for the science pack
local icon_style = science_pack_data.icon_style
local pack_icon =
parent.add{
name = 'icon-'..science_pack,
type = 'sprite-button',
sprite = 'item/'..science_pack,
tooltip = {'item-name.'..science_pack},
style = icon_style
}
-- Change the style of the icon
local pack_icon_style = pack_icon.style
pack_icon.ignored_by_interaction = true
pack_icon_style.height = 55
if icon_style == 'slot_button' then
pack_icon_style.padding = {0, -2}
pack_icon_style.width = 36
end
-- Draw the delta flow
local delta_flow =
parent.add{
name = 'delta-'..science_pack,
type = 'frame',
style = 'bordered_frame'
}
delta_flow.style.padding = {0, 3}
-- Draw the delta flow table
local delta_table =
delta_flow.add{
name = 'table',
type = 'table',
column_count = 2
}
delta_table.style.padding = 0
-- Draw the production labels
update_production_label(delta_table, science_pack_data.positive)
update_production_label(delta_table, science_pack_data.negative)
update_production_label(parent, science_pack_data.net)
-- Return the pack icon
return pack_icon
end)
local function get_science_pack_data(player, science_pack)
local force = player.force
-- Check that some packs have been made
local total = Production.get_production_total(force, science_pack)
if total.made == 0 then return end
local minute = Production.get_production(force, science_pack, defines.flow_precision_index.one_minute)
local hour = Production.get_production(force, science_pack, defines.flow_precision_index.one_hour)
-- Get the icon style
local icon_style = 'slot_button'
local flux = Production.get_fluctuations(force, science_pack, defines.flow_precision_index.one_minute)
if minute.net > 0 and flux.net > -config.color_flux/2 then
icon_style = 'slot_sized_button_green'
elseif flux.net < -config.color_flux then
icon_style = 'slot_sized_button_red'
elseif minute.made > 0 then
icon_style = 'yellow_slot_button'
end
-- Return the pack data
return {
science_pack = science_pack,
icon_style = icon_style,
positive = get_production_label_data(
'pos-'..science_pack,
{'science-info.pos-tooltip', total.made},
minute.made, hour.made
),
negative = get_production_label_data(
'neg-'..science_pack,
{'science-info.neg-tooltip', total.used},
-minute.used, hour.used
),
net = get_production_label_data(
'net-'..science_pack,
{'science-info.net-tooltip', total.net},
minute.net, minute.net > 0 and hour.net or 0,
minute.made+minute.used
)
}
end
local function update_science_pack(pack_table, science_pack_data)
if not science_pack_data then return end
local science_pack = science_pack_data.science_pack
pack_table.parent.non_made.visible = false
-- Update the icon
local pack_icon = pack_table['icon-'..science_pack] or science_pack_base(pack_table, science_pack_data)
local icon_style = science_pack_data.icon_style
pack_icon.style = icon_style
local pack_icon_style = pack_icon.style
pack_icon_style.height = 55
if icon_style == 'slot_button' then
pack_icon_style.padding = {0, -2}
pack_icon_style.width = 36
end
-- Update the production labels
local delta_table = pack_table['delta-'..science_pack].table
update_production_label(delta_table, science_pack_data.positive)
update_production_label(delta_table, science_pack_data.negative)
update_production_label(pack_table, science_pack_data.net)
end
--- Gets the data that is used with the eta label
local function get_eta_label_data(player)
local force = player.force
-- If there is no current research then return no research
local research = force.current_research
if not research then
return { research = false }
end
local limit
local progress = force.research_progress
local remaining = research.research_unit_count*(1-progress)
-- Check for the limiting science pack
for _, ingredient in pairs(research.research_unit_ingredients) do
local pack_name = ingredient.name
local required = ingredient.amount * remaining
local time = Production.get_consumsion_eta(force, pack_name, defines.flow_precision_index.one_minute, required)
if not limit or limit < time then
limit = time
end
end
-- Return the caption and tooltip
return limit and limit > 0 and {
research = true,
caption = format_time(limit, {hours=true, minutes=true, seconds=true, time=true}),
tooltip = format_time(limit, {hours=true, minutes=true, seconds=true, long=true})
} or { research = false }
end
-- Updates the eta label
local function update_eta_label(element, eta_label_data)
-- If no research selected show null
if not eta_label_data.research then
element.caption = null_time_short
element.tooltip = null_time_long
return
end
-- Update the element
element.caption = {'science-info.eta-time', eta_label_data.caption}
element.tooltip = eta_label_data.tooltip
end
--- Main task list container for the left flow
-- @element task_list_container
local science_info_container =
Gui.element(function(definition, parent)
local player = Gui.get_player_from_element(parent)
-- Draw the internal container
local container = Gui.container(parent, definition.name, 200)
-- Draw the header
Gui.header(container, {'science-info.main-caption'}, {'science-info.main-tooltip'})
-- Draw the scroll table for the tasks
local scroll_table = Gui.scroll_table(container, 178, 4)
-- Draw the no packs label
local no_packs_label =
scroll_table.parent.add{
name = 'non_made',
type = 'label',
caption = {'science-info.no-packs'}
}
-- Change the style of the no packs label
local no_packs_style = no_packs_label.style
no_packs_style.padding = {2, 4}
no_packs_style.single_line = false
no_packs_style.width = 200
-- Add the footer and eta
if config.show_eta then
-- Draw the footer
local footer = Gui.footer(container, {'science-info.eta-caption'}, {'science-info.eta-tooltip'}, true)
-- Draw the eta label
local eta_label =
footer.add{
name = 'label',
type = 'label',
caption = null_time_short,
tooltip = null_time_long,
style = 'heading_1_label'
}
-- Update the eta
update_eta_label(eta_label, get_eta_label_data(player))
end
-- Add packs which have been made
for _, science_pack in ipairs(config) do
update_science_pack(scroll_table, get_science_pack_data(player, science_pack))
end
-- Return the exteral container
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
--- Button on the top flow used to toggle the task list container
-- @element toggle_science_info
Gui.left_toolbar_button('entity/lab', {'science-info.main-tooltip'}, science_info_container, function(player)
return Roles.player_allowed(player, 'gui/science-info')
end)
--- Updates the gui every 1 second
Event.on_nth_tick(60, function()
local force_pack_data = {}
local force_eta_data = {}
for _, player in pairs(game.connected_players) do
local force_name = player.force.name
local frame = Gui.get_left_element(player, science_info_container)
local container = frame.container
-- Update the science packs
local scroll_table = container.scroll.table
local pack_data = force_pack_data[force_name]
if not pack_data then
-- No data in cache so it needs to be generated
pack_data = {}
force_pack_data[force_name] = pack_data
for _, science_pack in ipairs(config) do
local next_data = get_science_pack_data(player, science_pack)
pack_data[science_pack] = next_data
update_science_pack(scroll_table, next_data)
end
else
-- Data found in cache is no need to generate it
for _, next_data in pairs(pack_data) do
update_science_pack(scroll_table, next_data)
end
end
-- Update the eta times
if not config.show_eta then return end
local eta_label = container.footer.alignment.label
local eta_data = force_eta_data[force_name]
if not eta_data then
-- No data in chache so it needs to be generated
eta_data = get_eta_label_data(player)
force_eta_data[force_name] = eta_data
update_eta_label(eta_label, eta_data)
else
-- Data found in chache is no need to generate it
update_eta_label(eta_label, eta_data)
end
end
end)

View File

@@ -0,0 +1,88 @@
--[[-- Gui Module - Server UPS
- Adds a server ups counter in the top right and a command to toggle is
@gui server-ups
@alias server_ups
]]
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Event = require 'utils.event' --- @dep utils.event
local Commands = require 'expcore.commands' --- @dep expcore.commands
local External = require 'expcore.external' --- @dep expcore.external
--- Stores the visible state of server ups
local PlayerData = require 'expcore.player_data' --- @dep expcore.player_data
local UsesServerUps = PlayerData.Settings:combine('UsesServerUps')
UsesServerUps:set_default(false)
UsesServerUps:set_metadata{
permission = 'command/server-ups',
stringify = function(value) return value and 'Visible' or 'Hidden' end
}
--- Label to show the server ups
-- @element server_ups
local server_ups =
Gui.element{
type = 'label',
caption = 'SUPS = 60.0',
name = Gui.unique_static_name
}
:style{
font = 'default-game'
}
--- Change the visible state when your data loads
UsesServerUps:on_load(function(player_name, visible)
local player = game.players[player_name]
local label = player.gui.screen[server_ups.name]
if not External.valid() or not global.ext.var.server_ups then visible = false end
label.visible = visible
end)
--- Toggles if the server ups is visbile
-- @command server-ups
Commands.new_command('server-ups', 'Toggle the server UPS display')
:add_alias('sups', 'ups')
:register(function(player)
local label = player.gui.screen[server_ups.name]
if not External.valid() then
label.visible = false
return Commands.error{'expcom-server-ups.no-ext'}
end
label.visible = not label.visible
UsesServerUps:set(player, label.visible)
end)
-- Set the location of the label
-- 1920x1080: x=1455, y=30 (ui scale 100%)
local function set_location(event)
local player = game.players[event.player_index]
local label = player.gui.screen[server_ups.name]
local res = player.display_resolution
local uis = player.display_scale
-- below ups and clock
-- label.location = {x=res.width-423*uis, y=50*uis}
label.location = {x=res.width-363*uis, y=31*uis}
end
-- Draw the label when the player joins
Event.add(defines.events.on_player_created, function(event)
local player = game.players[event.player_index]
local label = server_ups(player.gui.screen)
label.visible = false
set_location(event)
end)
-- Update the caption for all online players
-- percentage of game speed
Event.on_nth_tick(60, function()
if External.valid() then
local caption = External.get_server_ups() .. ' (' .. string.format('%.1f', External.get_server_ups() * 5 / 3) .. '%)'
for _, player in pairs(game.connected_players) do
player.gui.screen[server_ups.name].caption = caption
end
end
end)
-- Update when res or ui scale changes
Event.add(defines.events.on_player_display_resolution_changed, set_location)
Event.add(defines.events.on_player_display_scale_changed, set_location)

View File

@@ -0,0 +1,199 @@
---- module surveillance
-- @gui surveillance
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Event = require 'utils.event' --- @dep utils.event
local cctv_player =
Gui.element(function(definition, parent, player_list)
return parent.add{
name = definition.name,
type = 'drop-down',
items = player_list,
selected_index = #player_list > 0 and 1
}
end)
:style{
horizontally_stretchable = true
}
:static_name(Gui.unique_static_name)
local cctv_status =
Gui.element{
type = 'drop-down',
items = {{'surveillance.status-enable'}, {'surveillance.status-disable'}},
selected_index = 2
}:style{
width = 96
}:on_selection_changed(function(_, element, _)
if element.selected_index == 1 then
element.parent.parent.parent.cctv_display.visible = true
else
element.parent.parent.parent.cctv_display.visible = false
end
end)
local cctv_type =
Gui.element{
type = 'drop-down',
name = Gui.unique_static_name,
items = {{'surveillance.type-player'}, {'surveillance.type-static'}, {'surveillance.type-player-loop'}},
selected_index = 1
}:style{
width = 96
}
local cctv_location =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = {'surveillance.func-set'}
}:style{
width = 48
}:on_click(function(player, element, _)
element.parent.parent.parent.cctv_display.position = player.position
end)
local zoom_in =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = '+'
}:style{
width = 32
}:on_click(function(_, element, _)
local display = element.parent.parent.parent.cctv_display
if display.zoom < 2.0 then
display.zoom = display.zoom + 0.05
end
end)
local zoom_out =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = '-'
}:style{
width = 32
}:on_click(function(_, element, _)
local display = element.parent.parent.parent.cctv_display
if display.zoom > 0.2 then
display.zoom = display.zoom - 0.05
end
end)
local camera_set =
Gui.element(function(_, parent, name, player_list)
local camera_set = parent.add{type='flow', direction='vertical', name=name}
local buttons = Gui.scroll_table(camera_set, 480, 6, 'buttons')
cctv_player(buttons, player_list)
cctv_status(buttons)
cctv_type(buttons)
cctv_location(buttons)
zoom_out(buttons)
zoom_in(buttons)
local camera = camera_set.add{
type = 'camera',
name = 'cctv_display',
position = {x=0, y=0},
surface_index = game.surfaces['nauvis'].index,
zoom = 0.75,
}
camera.visible = false
camera.style.minimal_width = 480
camera.style.minimal_height = 290
return camera_set
end)
local cctv_container =
Gui.element(function(definition, parent)
local container = Gui.container(parent, definition.name, 480)
local scroll = container.add{name='scroll', type='scroll-pane', direction='vertical'}
scroll.style.maximal_height = 704
local player_list = {}
for _, player in pairs(game.connected_players) do
table.insert(player_list, player.name)
end
camera_set(scroll, 'cctv_st_1', player_list)
camera_set(scroll, 'cctv_st_2', player_list)
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
Gui.left_toolbar_button('entity/radar', {'surveillance.main-tooltip'}, cctv_container, function(player)
return Roles.player_allowed(player, 'gui/surveillance')
end)
local function gui_update()
local player_list = {}
for _, player in pairs(game.connected_players) do
table.insert(player_list, player.name)
end
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, cctv_container)
frame.container.scroll['cctv_st_1'].buttons.table[cctv_player.name].items = player_list
frame.container.scroll['cctv_st_2'].buttons.table[cctv_player.name].items = player_list
end
end
Event.add(defines.events.on_player_joined_game, gui_update)
Event.add(defines.events.on_player_left_game, gui_update)
Event.add(defines.events.on_tick, function(_)
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, cctv_container)
for i=1, 2 do
local scroll_table_name = 'cctv_st_' .. i
local current_camera_set = frame.container.scroll[scroll_table_name]
local switch_index = current_camera_set.buttons.table[cctv_type.name].selected_index
if (switch_index == 1) or (switch_index == 3) then
local selected_index = current_camera_set.buttons.table[cctv_player.name].selected_index
if selected_index ~= 0 then
selected_index = current_camera_set.buttons.table[cctv_player.name].items[selected_index]
current_camera_set['cctv_display'].position = game.players[selected_index].position
current_camera_set['cctv_display'].surface_index = game.players[selected_index].surface_index
else
current_camera_set['cctv_display'].position = {x=0, y=0}
current_camera_set['cctv_display'].surface_index = game.surfaces['nauvis'].index
end
end
end
end
end)
Event.on_nth_tick(600, function(_)
for _, player in pairs(game.connected_players) do
local frame = Gui.get_left_element(player, cctv_container)
for i=1, 2 do
local current_camera_set = frame.container.scroll['cctv_st_' .. i]
if current_camera_set.buttons.table[cctv_type.name].selected_index == 3 then
local item_n = #current_camera_set.buttons.table[cctv_player.name].items
if item_n ~= 0 then
if current_camera_set.buttons.table[cctv_player.name].selected_index < item_n then
current_camera_set.buttons.table[cctv_player.name].selected_index = current_camera_set.buttons.table[cctv_player.name].selected_index + 1
else
current_camera_set.buttons.table[cctv_player.name].selected_index = 1
end
end
end
end
end
end)

View File

@@ -0,0 +1,782 @@
--[[-- Gui Module - Task List
- Adds a task list to the game which players can add, remove and edit items on
@gui Task-List
@alias task_list
]]
local Gui = require "expcore.gui" --- @dep expcore.gui
local Event = require "utils.event" --- @dep utils.event
local Roles = require "expcore.roles" --- @dep expcore.roles
local Datastore = require "expcore.datastore" --- @dep expcore.datastore
local config = require "config.gui.tasks" --- @dep config.gui.tasks
local Tasks = require "modules.control.tasks" --- @dep modules.control.tasks
local format_time = _C.format_time --- @dep expcore.common
--- Stores all data for the task gui by player
local TaskGuiData = Datastore.connect("TaskGuiData")
TaskGuiData:set_serializer(Datastore.name_serializer)
local PlayerIsEditing = TaskGuiData:combine("PlayerIsEditing")
PlayerIsEditing:set_default(false)
local PlayerIsCreating = TaskGuiData:combine("PlayerIsCreating")
PlayerIsCreating:set_default(false)
local PlayerSelected = TaskGuiData:combine("PlayerSelected")
PlayerSelected:set_default(nil)
-- Styles used for sprite buttons
local Styles = {
sprite22 = {
height = 22,
width = 22,
padding = -2
},
footer_button = {
height = 29,
maximal_width = 268,
horizontally_stretchable = true,
padding = -2
}
}
--- If a player is allowed to use the edit buttons
local function check_player_permissions(player, task)
if task then
-- When a task is given check if the player can edit it
local allow_edit_task = config.allow_edit_task
-- Check if the player being the last to edit will override existing permisisons
if config.user_can_edit_own_tasks and task.last_edit_name == player.name then
return true
end
-- Check player has permisison based on value in the config
if allow_edit_task == "all" then
return true
elseif allow_edit_task == "admin" then
return player.admin
elseif allow_edit_task == "expcore.roles" then
return Roles.player_allowed(player, config.expcore_roles_allow_edit_task)
end
-- Return false as all other condidtions have not been met
return false
else
-- When a task is not given check if the player can add a new task
local allow_add_task = config.allow_add_task
-- Check player has permisison based on value in the config
if allow_add_task == "all" then
return true
elseif allow_add_task == "admin" then
return player.admin
elseif allow_add_task == "expcore.roles" then
return Roles.player_allowed(player, config.expcore_roles_allow_add_task)
end
-- Return false as all other condidtions have not been met
return false
end
end
--- Elements
--- Button displayed in the header bar, used to add a new task
-- @element add_new_task
local add_new_task =
Gui.element {
type = "sprite-button",
sprite = "utility/add",
tooltip = {"task-list.add-tooltip"},
style = "tool_button",
name = Gui.unique_static_name
}:style(Styles.sprite22):on_click(
function(player, _, _)
-- Disable editing
PlayerIsEditing:set(player, false)
-- Clear selected
PlayerSelected:set(player, nil)
-- Open task create footer
PlayerIsCreating:set(player, true)
end
)
--- Header displayed when no tasks are in the task list
-- @element no_tasks_found
local no_tasks_found =
Gui.element(
function(_, parent)
local header =
parent.add {
name = "no_tasks_found_element",
type = "frame",
style = "negative_subheader_frame"
}
header.style.horizontally_stretchable = true
-- Flow used for centering the content in the subheader
local center =
header.add {
type = "flow",
style = "centering_horizontal_flow"
}
center.style.horizontally_stretchable = true
center.add {
name = "header_label",
type = "label",
style = "bold_label",
caption = {"", "[img=utility/warning_white] ", {"task-list.no-tasks"}},
tooltip = {"task-list.no-tasks-tooltip"}
}
return header
end
)
--- Frame element with the right styling
-- @element subfooter_frame
local subfooter_frame =
Gui.element(
function(_, parent, name)
return parent.add {
type = "frame",
name = name,
direction = "vertical",
style = "subfooter_frame"
}
end
):style(
{
padding = 5,
use_header_filler = false,
horizontally_stretchable = true
}
)
--- Label element preset
-- @element subfooter_label
local subfooter_label =
Gui.element(
function(_, parent, caption)
return parent.add {
name = "footer_label",
type = "label",
style = "heading_1_label",
caption = caption
}
end
)
--- Action flow that contains action buttons
-- @element subfooter_actions
local subfooter_actions =
Gui.element {
type = "flow",
name = "actions"
}
--- Button element with a flow around it to fix duplicate name inside of the scroll flow
-- @element task_list_item
local task_list_item =
Gui.element(
function(definition, parent, task)
local flow = parent.add {
type = "flow",
name = "task-" .. task.task_id,
caption = task.task_id
}
flow.style.horizontally_stretchable = true
local button = flow.add {
name = definition.name,
type = "button",
style = "list_box_item",
caption = task.title,
tooltip = { "task-list.last-edit", task.last_edit_name, format_time(task.last_edit_time) }
}
button.style.horizontally_stretchable = true
button.style.horizontally_squashable = true
return button
end
):on_click(
function(player, element, _)
local task_id = element.parent.caption
PlayerSelected:set(player, task_id)
end
):static_name(Gui.unique_static_name)
--- Scrollable list of all tasks
-- @element task_list
local task_list =
Gui.element(
function(_, parent)
local scroll_pane =
parent.add {
name = "scroll",
type = "scroll-pane",
direction = "vertical",
horizontal_scroll_policy = "never",
vertical_scroll_policy = "auto",
style = "scroll_pane_under_subheader"
}
scroll_pane.style.horizontally_stretchable = true
scroll_pane.style.padding = 0
scroll_pane.style.maximal_height = 224
local flow =
scroll_pane.add {
name = "task_list",
type = "flow",
direction = "vertical"
}
flow.style.vertical_spacing = 0
flow.style.horizontally_stretchable = true
return flow
end
)
--- Button element inside the task view footer to start editing a task
-- @element task_view_edit_button
local task_view_edit_button =
Gui.element {
type = "button",
name = Gui.unique_static_name,
caption = {"", "[img=utility/rename_icon_normal] ", {"task-list.edit"}},
tooltip = {"task-list.edit-tooltip"},
style = "shortcut_bar_button"
}:style(Styles.footer_button):on_click(
function(player, _, _)
local selected = PlayerSelected:get(player)
PlayerIsEditing:set(player, true)
Tasks.set_editing(selected, player.name, true)
end
)
--- Button to close the task view footer
-- @element task_view_close_button
local task_view_close_button =
Gui.element{
type = "sprite-button",
sprite = "utility/collapse_dark",
hovered_sprite = "utility/collapse",
tooltip = {"task-list.close-tooltip"}
}
:style(Styles.sprite22):on_click(
function(player, _, _)
PlayerSelected:set(player, nil)
end
)
--- Button to delete the task inside the task view footer
-- @element task_view_delete_button
local task_view_delete_button =
Gui.element {
type = "button",
name = Gui.unique_static_name,
caption = {"", "[img=utility/trash] ", {"task-list.delete"}},
tooltip = {"task-list.delete-tooltip"},
style = "shortcut_bar_button_red"
}:style(Styles.footer_button):on_click(
function(player, _, _)
local selected = PlayerSelected:get(player)
PlayerSelected:set(player, nil)
Tasks.remove_task(selected)
end
)
--- Subfooter inside the tasklist container that holds all the elements for viewing a task
-- @element task_view_footer
local task_view_footer =
Gui.element(
function(_, parent)
local footer = subfooter_frame(parent, "view")
local flow = footer.add{ type = "flow" }
subfooter_label(flow, {"task-list.view-footer-header"})
local alignment = Gui.alignment(flow)
task_view_close_button(alignment)
local title_label =
footer.add {
type = "label",
name = "title"
}
title_label.style.padding = 4
title_label.style.font = "default-bold"
title_label.style.single_line = false
local body_label =
footer.add {
type = "label",
name = "body"
}
body_label.style.padding = 4
body_label.style.single_line = false
local action_flow = subfooter_actions(footer)
task_view_delete_button(action_flow)
task_view_edit_button(action_flow)
return footer
end
)
local message_pattern = "(.-)\n(.*)"
--- Parse a string into a message object with title and body
-- @tparam string str message data
local function parse_message(str)
-- Trim the spaces of the string
local trimmed = string.gsub(str, "^%s*(.-)%s*$", "%1")
local message = { title = "", body = "" }
local title, body = string.match(trimmed, message_pattern)
if not title then
-- If it doesn't match the pattern return the str as a title
message.title = trimmed
else
message.title = title
message.body = body
end
return message
end
-- Button variable initialisation because it is used inside the textfield element events
local task_edit_confirm_button
local task_create_confirm_button
--- Textfield element used in both the task create and edit footers
-- @element task_message_textfield
local task_message_textfield =
Gui.element {
name = Gui.unique_static_name,
type = "text-box",
text = ""
}:style(
{
maximal_width = 268,
minimal_height = 100,
horizontally_stretchable = true
}
):on_text_changed(
function(player, element, _)
local isEditing = PlayerIsEditing:get(player)
local isCreating = PlayerIsCreating:get(player)
local valid = string.len(element.text) > 5
if isCreating then
element.parent.actions[task_create_confirm_button.name].enabled = valid
elseif isEditing then
element.parent.actions[task_edit_confirm_button.name].enabled = valid
end
end
)
--- Button to confirm the changes inside the task edit footer
-- @element task_edit_confirm_button
task_edit_confirm_button =
Gui.element {
type = "button",
name = Gui.unique_static_name,
caption = {"", "[img=utility/check_mark] ", {"task-list.confirm"}},
tooltip = {"task-list.confirm-tooltip"},
style = "shortcut_bar_button_green"
}:style(Styles.footer_button):on_click(
function(player, element, _)
local selected = PlayerSelected:get(player)
PlayerIsEditing:set(player, false)
local new_message = element.parent.parent[task_message_textfield.name].text
local parsed = parse_message(new_message)
Tasks.update_task(selected, player.name, parsed.title, parsed.body)
Tasks.set_editing(selected, player.name, nil)
end
)
--- Button to discard the changes inside the task edit footer
-- @element edit_task_discard_button
local edit_task_discard_button =
Gui.element {
type = "button",
caption = {"", "[img=utility/close_black] ", {"task-list.discard"}},
tooltip = {"task-list.discard-tooltip"},
style = "shortcut_bar_button_red"
}:style(Styles.footer_button):on_click(
function(player, _, _)
local selected = PlayerSelected:get(player)
Tasks.set_editing(selected, player.name, nil)
PlayerIsEditing:set(player, false)
end
)
--- Subfooter inside the tasklist container that holds all the elements for editing a task
-- @element task_edit_footer
local task_edit_footer =
Gui.element(
function(_, parent)
local footer = subfooter_frame(parent, "edit")
subfooter_label(footer, {"task-list.edit-footer-header"})
task_message_textfield(footer)
local action_flow = subfooter_actions(footer)
edit_task_discard_button(action_flow)
task_edit_confirm_button(action_flow)
return footer
end
)
--- Button to confirm the changes inside the task create footer
-- @element task_create_confirm_button
task_create_confirm_button =
Gui.element {
type = "button",
name = Gui.unique_static_name,
caption = {"", "[img=utility/check_mark] ", {"task-list.confirm"}},
tooltip = {"task-list.confirm-tooltip"},
style = "shortcut_bar_button_green",
enabled = false
}:style(Styles.footer_button):on_click(
function(player, element, _)
local message = element.parent.parent[task_message_textfield.name].text
PlayerIsCreating:set(player, false)
local parsed = parse_message(message)
local task_id = Tasks.add_task(player.force.name, player.name, parsed.title, parsed.body)
PlayerSelected:set(player, task_id)
end
)
--- Button to discard the changes inside the task create footer
-- @element task_create_discard_button
local task_create_discard_button =
Gui.element {
type = "button",
caption = {"", "[img=utility/close_black] ", {"task-list.discard"}},
tooltip = {"task-list.discard-tooltip"},
style = "shortcut_bar_button_red"
}:style(Styles.footer_button):on_click(
function(player, _, _)
PlayerIsCreating:set(player, false)
end
)
--- Subfooter inside the tasklist container that holds all the elements to create a new task
-- @element task_create_footer
local task_create_footer =
Gui.element(
function(_, parent)
local footer = subfooter_frame(parent, "create")
subfooter_label(footer, {"task-list.create-footer-header"})
task_message_textfield(footer)
local action_flow = subfooter_actions(footer)
task_create_discard_button(action_flow)
task_create_confirm_button(action_flow)
return footer
end
)
--- Clear and repopulate the task list with all current tasks
local repopulate_task_list = function(task_list_element)
local force = Gui.get_player_from_element(task_list_element).force
local task_ids = Tasks.get_force_task_ids(force.name)
task_list_element.clear()
-- Set visibility of the no_tasks_found element depending on the amount of tasks still in the task manager
task_list_element.parent.parent.no_tasks_found_element.visible = #task_ids == 0
-- Add each task to the flow
for _, task_id in ipairs(task_ids) do
-- Add the task
local task = Tasks.get_task(task_id)
task_list_item(task_list_element, task)
end
end
--- Main task list container for the left flow
-- @element task_list_container
local task_list_container =
Gui.element(
function(definition, parent)
-- Draw the internal container
local container = Gui.container(parent, definition.name, 268)
container.style.maximal_width = 268
container.style.minimal_width = 268
-- Draw the header
local header = Gui.header(container, {"task-list.main-caption"}, {"task-list.sub-tooltip"}, true)
-- Draw the new task button
local player = Gui.get_player_from_element(parent)
local add_new_task_element = add_new_task(header)
add_new_task_element.visible = check_player_permissions(player)
-- Draw no task found element
no_tasks_found(container)
-- Draw task list element
local task_list_element = task_list(container)
repopulate_task_list(task_list_element)
local task_view_footer_element = task_view_footer(container)
local task_edit_footer_element = task_edit_footer(container)
local task_create_footer_element = task_create_footer(container)
task_view_footer_element.visible = false
task_edit_footer_element.visible = false
task_create_footer_element.visible = false
-- Return the external container
return container.parent
end
):static_name(Gui.unique_static_name):add_to_left_flow(
function(player)
local task_ids = Tasks.get_force_task_ids(player.force.name)
return #task_ids > 0
end
)
--- Button on the top flow used to toggle the task list container
-- @element toggle_left_element
Gui.left_toolbar_button(
"utility/not_enough_repair_packs_icon",
{"task-list.main-tooltip"},
task_list_container,
function(player)
return Roles.player_allowed(player, "gui/task-list")
end
)
-- Function to update a single task and some of the elements inside the container
local update_task = function(player, task_list_element, task_id)
local task = Tasks.get_task(task_id)
local task_ids = Tasks.get_force_task_ids(player.force.name)
-- Set visibility of the no_tasks_found element depending on the amount of tasks still in the task manager
task_list_element.parent.parent.no_tasks_found_element.visible = #task_ids == 0
-- Task no longer exists so should be removed from the list
if not task then
task_list_element["task-" .. task_id].destroy()
return
end
local flow = task_list_element["task-" .. task_id]
if not flow then
-- If task does not exist yet add it to the list
task_list_item(task_list_element, task)
else
-- If the task exists update the caption and tooltip
local button = flow[task_list_item.name]
button.caption = task.title
button.tooltip = {"task-list.last-edit", task.last_edit_name, format_time(task.last_edit_time)}
end
end
-- Update the footer task edit view
local update_task_edit_footer = function(player, task_id)
local task = Tasks.get_task(task_id)
local frame = Gui.get_left_element(player, task_list_container)
local edit_flow = frame.container.edit
local message_element = edit_flow[task_message_textfield.name]
message_element.focus()
message_element.text = task.title .. "\n" .. task.body
end
-- Update the footer task view
local update_task_view_footer = function(player, task_id)
local task = Tasks.get_task(task_id)
local frame = Gui.get_left_element(player, task_list_container)
local view_flow = frame.container.view
local has_permission = check_player_permissions(player, task)
local title_element = view_flow.title
local body_element = view_flow.body
local edit_button_element = view_flow.actions[task_view_edit_button.name]
local delete_button_element = view_flow.actions[task_view_delete_button.name]
edit_button_element.visible = has_permission
delete_button_element.visible = has_permission
title_element.caption = task.title
body_element.caption = task.body
local players_editing = table.get_keys(task.currently_editing)
if #players_editing > 0 then
edit_button_element.tooltip = {"task-list.edit-tooltip", table.concat(players_editing, ", ")}
else
edit_button_element.tooltip = {"task-list.edit-tooltip-none"}
end
end
--- When a new task is added it will update the task list for everyone on that force
--- Or when a task is updated it will update the specific task elements
Tasks.on_update(
function(task_id, curr_state, prev_state)
-- Get the force to update, task is nil when removed
local force
if curr_state then
force = game.forces[curr_state.force_name]
else
force = game.forces[prev_state.force_name]
end
-- Update the task for all the players on the force
for _, player in pairs(force.connected_players) do
-- Update the task view elements if the player currently being looped over has this specific task selected
local selected = PlayerSelected:get(player)
if selected == task_id then
if curr_state then
update_task_view_footer(player, selected)
else
PlayerSelected:set(player, nil)
end
end
local frame = Gui.get_left_element(player, task_list_container)
local task_list_element = frame.container.scroll.task_list
-- Update the task that was changed
update_task(player, task_list_element, task_id)
end
end
)
-- When a player is creating a new task.
PlayerIsCreating:on_update(
function(player_name, curr_state, _)
local player = game.players[player_name]
local frame = Gui.get_left_element(player, task_list_container)
local create = frame.container.create
-- Clear the textfield
local message_element = frame.container.create[task_message_textfield.name]
local confirm_button_element = frame.container.create.actions[task_create_confirm_button.name]
message_element.focus()
message_element.text = ""
confirm_button_element.enabled = false
if curr_state then
create.visible = true
else
create.visible = false
end
end
)
-- When a player selects a different warp from the list
PlayerSelected:on_update(
function(player_name, curr_state, prev_state)
local player = game.players[player_name]
local frame = Gui.get_left_element(player, task_list_container)
local task_list_element = frame.container.scroll.task_list
local view_flow = frame.container.view
local edit_flow = frame.container.edit
local isEditing = PlayerIsEditing:get(player)
local isCreating = PlayerIsCreating:get(player)
-- If the selection has an previous state re-enable the button list element
if prev_state then
task_list_element["task-" .. prev_state][task_list_item.name].enabled = true
end
if curr_state then
-- Disable the selected element
task_list_element["task-" .. curr_state][task_list_item.name].enabled = false
-- Update the view footer
update_task_view_footer(player, curr_state)
-- If a player is creating then remove the creation dialogue
if isCreating then
PlayerIsCreating:set(player, false)
end
-- Depending on if the player is currently editing change the current task edit footer to the current task
if isEditing then
update_task_edit_footer(player, curr_state)
Tasks.set_editing(prev_state, player.name, nil)
Tasks.set_editing(curr_state, player.name, true)
view_flow.visible = false
edit_flow.visible = true
else
view_flow.visible = true
edit_flow.visible = false
end
else
-- If curr_state nil then hide footer elements and set editing to nil for prev_state
if prev_state and Tasks.get_task(prev_state) then
Tasks.set_editing(prev_state, player.name, nil)
end
view_flow.visible = false
edit_flow.visible = false
end
end
)
-- When the edit view opens or closes
PlayerIsEditing:on_update(
function(player_name, curr_state, _)
local player = game.players[player_name]
local frame = Gui.get_left_element(player, task_list_container)
local view_flow = frame.container.view
local edit_flow = frame.container.edit
local selected = PlayerSelected:get(player)
if curr_state then
update_task_edit_footer(player, selected)
view_flow.visible = false
edit_flow.visible = true
else
view_flow.visible = true
edit_flow.visible = false
end
end
)
--- Makes sure the right buttons are present when roles change
local function role_update_event(event)
local player = game.players[event.player_index]
local container = Gui.get_left_element(player, task_list_container).container
-- Update the view task
local selected = PlayerSelected:get(player)
if selected then
update_task_view_footer(player, selected)
PlayerSelected:set(player, selected)
-- button to edit the task.
-- Resetting the players selected task to make sure the player does not see an
end
-- Update the new task button and create footer in case the user can now add them
local has_permission = check_player_permissions(player)
local add_new_task_element = container.header.alignment[add_new_task.name]
add_new_task_element.visible = has_permission
local isCreating = PlayerIsCreating:get(player)
if isCreating and not has_permission then
PlayerIsCreating:set(player, false)
end
end
Event.add(Roles.events.on_role_assigned, role_update_event)
Event.add(Roles.events.on_role_unassigned, role_update_event)
--- Redraw all tasks and clear editing/creating after joining or changing force
local function reset_task_list(event)
-- Repopulate the task list
local player = game.players[event.player_index]
local frame = Gui.get_left_element(player, task_list_container)
local task_list_element = frame.container.scroll.task_list
repopulate_task_list(task_list_element)
-- Check if the selected task is still valid
local selected = PlayerSelected:get(player)
if selected and Tasks.get_task(selected) ~= nil then
PlayerIsCreating:set(player, false)
PlayerIsEditing:set(player, false)
PlayerSelected:set(player, nil)
end
end
Event.add(defines.events.on_player_joined_game, reset_task_list)
Event.add(defines.events.on_player_changed_force, reset_task_list)

View File

@@ -0,0 +1,523 @@
local Gui = require "expcore.gui" --- @dep expcore.gui
local PlayerData = require 'expcore.player_data' --- @dep expcore.player_data
-- Used to store the state of the toolbar when a player leaves
local ToolbarState = PlayerData.Settings:combine('ToolbarState')
ToolbarState:set_metadata{
stringify = function(value)
local buttons, favourites = 0, 0
for _, state in ipairs(value) do
buttons = buttons + 1
if state.favourite then
favourites = favourites + 1
end
end
return string.format("Buttons: %d, Favourites: %d", buttons, favourites)
end
}
-- Styles used for sprite buttons
local button_size = 20
local Styles = {
header = Gui.sprite_style(22),
item = Gui.sprite_style(button_size)
}
--- Set the style of the fake toolbar element
local function copy_style(src, dst)
dst.style = src.style.name
dst.style.height = button_size
dst.style.width = button_size
dst.style.padding = -2
end
local toolbar_container, move_up, move_down, toggle_toolbar
--- Reorder the buttons relative to each other, this will update the datastore
local function move_toolbar_button(player, item, offset)
local old_index = item.get_index_in_parent()
local new_index = old_index + offset
-- Ideally the following would all happen in on_update but this had too much latency
-- Swap the position in the list
local list = item.parent
local other_item = list.children[new_index]
list.swap_children(old_index, new_index)
-- Swap the position in the top flow, offset by 1 because of settings button
local top_flow = Gui.get_top_flow(player)
top_flow.swap_children(old_index+1, new_index+1)
-- Check if the element has a left element to move
local element_define = Gui.defines[item.tags.top_element_uid]
local other_define = Gui.defines[other_item.tags.top_element_uid]
if element_define.left_flow_element and other_define.left_flow_element then
local left_element = Gui.get_left_element(player, element_define.left_flow_element)
local other_left_element = Gui.get_left_element(player, other_define.left_flow_element)
local left_index = left_element.get_index_in_parent()
local other_index = other_left_element.get_index_in_parent()
left_element.parent.swap_children(left_index, other_index)
end
-- If we are moving in/out of first/last place we need to update the move buttons
local last_index = #list.children
if old_index == 1 then -- Moving out of index 1
other_item.move[move_up.name].enabled = false
item.move[move_up.name].enabled = true
elseif new_index == 1 then -- Moving into index 1
other_item.move[move_up.name].enabled = true
item.move[move_up.name].enabled = false
elseif old_index == last_index then -- Moving out of the last index
other_item.move[move_down.name].enabled = false
item.move[move_down.name].enabled = true
elseif new_index == last_index then -- Moving into the last index
other_item.move[move_down.name].enabled = true
item.move[move_down.name].enabled = false
end
-- Update the datastore state
ToolbarState:update(player, function(_, order)
local tmp = order[old_index]
order[old_index] = order[new_index]
order[new_index] = tmp
end)
end
--- Reorder the toolbar buttons
local function reorder_toolbar_menu(player)
local frame = Gui.get_left_element(player, toolbar_container)
local list = frame.container.scroll.list
local order = ToolbarState:get(player)
local last_index = #order
-- Reorder the buttons
for index, state in ipairs(order) do
local element_define = Gui.defines[state.element_uid]
-- Switch item order
local item = list[element_define.name]
list.swap_children(index, item.get_index_in_parent())
-- Check if the player is allowed to see the button
local allowed = element_define.authenticator
if type(allowed) == 'function' then allowed = allowed(player) end
-- Update the checkbox state and item visibility
local toolbar_button = Gui.get_top_element(player, element_define)
toolbar_button.visible = allowed and state.favourite or false
item.checkbox.state = state.favourite
-- Update the state if the move buttons
item.move[move_up.name].enabled = index ~= 1
item.move[move_down.name].enabled = index ~= last_index
end
-- Update the state of the toggle button
local button = frame.container.header.alignment[toggle_toolbar.name]
button.enabled = Gui.top_flow_has_visible_elements(player)
button.toggled = Gui.get_top_flow(player).parent.visible
end
--- Resets the toolbar to its default state when pressed
-- @element reset_toolbar
local reset_toolbar =
Gui.element {
type = "sprite-button",
sprite = "utility/reset",
style = "shortcut_bar_button_red",
tooltip = {"toolbar.reset"},
name = Gui.unique_static_name
}
:style(Gui.sprite_style(Styles.header.width, -1))
:on_click(function(player)
ToolbarState:set(player, nil)
Gui.toggle_top_flow(player, true)
reorder_toolbar_menu(player)
end)
--- Replaces the default method for opening and closing the toolbar
-- @element toggle_toolbar
toggle_toolbar =
Gui.element {
type = "sprite-button",
sprite = "utility/bookmark",
tooltip = {"toolbar.toggle"},
style = "tool_button",
auto_toggle = true,
name = Gui.unique_static_name
}
:style(Styles.header)
:on_click(function(player, element)
Gui.toggle_top_flow(player, element.toggled)
end)
--- Move an element up the list
-- @element move_up
move_up =
Gui.element {
type = "sprite-button",
sprite = "utility/speed_up",
tooltip = {"toolbar.move-up"},
name = Gui.unique_static_name
}
:style(Styles.item)
:on_click(function(player, element)
local item = element.parent.parent
move_toolbar_button(player, item, -1)
end)
--- Move an element down the list
-- @element move_down
move_down =
Gui.element {
type = "sprite-button",
sprite = "utility/speed_down",
tooltip = {"toolbar.move-down"},
name = Gui.unique_static_name
}
:style(Styles.item)
:on_click(function(player, element)
local item = element.parent.parent
move_toolbar_button(player, item, 1)
end)
--- A flow which represents one item in the toolbar list
-- @element toolbar_list_item
local toolbar_list_item =
Gui.element(function(definition, parent, element_define)
local flow = parent.add {
type = "frame",
style = "shortcut_selection_row",
name = element_define.name,
tags = {
top_element_uid = element_define.uid
}
}
flow.style.horizontally_stretchable = true
flow.style.vertical_align = "center"
-- Add the button and the icon edit button
local element = element_define(flow)
local player = Gui.get_player_from_element(parent)
local top_element = Gui.get_top_element(player, element_define)
copy_style(top_element, element)
-- Add the checkbox that can toggle the visibility
local checkbox = flow.add{
type = "checkbox",
name = "checkbox",
caption = element_define.tooltip or element_define.caption or "None",
state = top_element.visible or false,
tags = {
top_element_name = element_define.name
}
}
definition:triggers_events(checkbox)
checkbox.style.width = 180
-- Add the buttons used to move the flow up and down
local move_flow = flow.add{ type = "flow", name = "move" }
move_flow.style.horizontal_spacing = 0
move_up(move_flow)
move_down(move_flow)
return definition:no_events(flow)
end)
:on_checked_changed(function(player, element)
local top_flow = Gui.get_top_flow(player)
local top_element = top_flow[element.tags.top_element_name]
local had_visible = Gui.top_flow_has_visible_elements(player)
top_element.visible = element.state
-- Check if we are on the edge case between 0 and 1 visible elements
if element.state and not had_visible then
Gui.toggle_top_flow(player, true)
local container = element.parent.parent.parent.parent
local button = container.header.alignment[toggle_toolbar.name]
button.toggled = true
button.enabled = true
elseif not element.state and not Gui.top_flow_has_visible_elements(player) then
Gui.toggle_top_flow(player, false)
local container = element.parent.parent.parent.parent
local button = container.header.alignment[toggle_toolbar.name]
button.toggled = false
button.enabled = false
end
-- Update the datastore state
ToolbarState:update(player, function(_, order)
local index = element.parent.get_index_in_parent()
order[index].favourite = element.state
end)
end)
--- Scrollable list of all toolbar buttons
-- @element toolbar_list
local toolbar_list =
Gui.element(function(_, parent)
-- This is a scroll pane for the list
local scroll_pane = parent.add {
name = "scroll",
type = "scroll-pane",
direction = "vertical",
horizontal_scroll_policy = "never",
vertical_scroll_policy = "auto",
style = "scroll_pane_under_subheader"
}
scroll_pane.style.horizontally_stretchable = true
scroll_pane.style.padding = 0
scroll_pane.style.maximal_height = 224
-- This flow is the list, we need a linear list because of get_index_in_parent
local flow = scroll_pane.add {
name = "list",
type = "flow",
direction = "vertical"
}
flow.style.vertical_spacing = 0
flow.style.horizontally_stretchable = true
return flow
end)
--- Main toolbar container for the left flow
-- @element toolbar_container
toolbar_container =
Gui.element(function(definition, parent)
-- Draw the internal container
local container = Gui.container(parent, definition.name, 268)
container.style.maximal_width = 268
container.style.minimal_width = 268
-- Draw the header
local player = Gui.get_player_from_element(parent)
local header = Gui.header(container, {"toolbar.main-caption"}, {"toolbar.main-tooltip"}, true)
-- Draw the toolbar control buttons
local toggle_element = toggle_toolbar(header)
toggle_element.toggled = Gui.get_top_flow(player).visible
reset_toolbar(header)
-- Draw toolbar list element
local list_element = toolbar_list(container)
local flow_order = Gui.get_top_flow_order(player)
for _, element_define in ipairs(flow_order) do
-- Ensure the element exists
local element = list_element[element_define.name]
if not element then
element = toolbar_list_item(list_element, element_define)
end
-- Set the visible state
local allowed = element_define.authenticator
if type(allowed) == 'function' then allowed = allowed(player) end
element.visible = allowed or false
end
-- Set the state of the move buttons for the first and last element
local children = list_element.children
children[1].move[move_up.name].enabled = false
children[#children].move[move_down.name].enabled = false
-- Return the external container
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow(false)
--- Set the default value for the datastore
local datastore_id_map = {}
local toolbar_default_state = {}
ToolbarState:set_default(toolbar_default_state)
--- Get the datastore id for this element define, to best of ability it should be unique between versions
local function to_datastore_id(element_define)
-- First try to use the tooltip locale string
local tooltip = element_define.tooltip
if type(tooltip) == "table" then
return tooltip[1]:gsub("%.(.+)", "")
end
-- Then try to use the caption or sprite
return element_define.caption or element_define.sprite
end
--- For all top element, register an on click which will copy their style
for index, element_define in ipairs(Gui.top_elements) do
-- This is a bit hacky, the gui system cant have multiple handlers registered
local prev_handler = element_define[Gui.events.on_toolbar_button_toggled]
-- Add the handler for when the button is toggled
element_define:on_event(Gui.events.on_toolbar_button_toggled, function(player, element, event)
if prev_handler then prev_handler(player, element, event) end -- Kind of hacky but works
local frame = Gui.get_left_element(player, toolbar_container)
if not frame then return end -- Gui might not be loaded yet
local button = frame.container.scroll.list[element_define.name][element_define.name]
local toolbar_button = Gui.get_top_element(player, element_define)
copy_style(toolbar_button, button)
end)
-- Insert the element into the id map
local id = to_datastore_id(element_define)
if datastore_id_map[id] then
error(string.format("All toolbar elements need a unique id to be saved correctly, %d (%s) and %d (%s) share the id %s",
datastore_id_map[id].uid, datastore_id_map[id].defined_at, element_define.uid, element_define.defined_at, id
))
end
datastore_id_map[id] = element_define
-- Add the element to the default state
table.insert(toolbar_default_state, {
element_uid = element_define.uid,
favourite = true,
})
end
--- Get the top order based on the players settings
Gui.inject_top_flow_order(function(player)
local order = ToolbarState:get(player)
local elements = {}
for index, state in ipairs(order) do
elements[index] = Gui.defines[state.element_uid]
end
return elements
end)
--- Get the left order based on the player settings, with toolbar menu first, and all remaining after
Gui.inject_left_flow_order(function(player)
local order = Gui.get_top_flow_order(player)
local elements, element_map = { toolbar_container }, { [toolbar_container] = true }
-- Add the flows that have a top element
for _, element_define in ipairs(order) do
if element_define.left_flow_element then
table.insert(elements, element_define.left_flow_element)
element_map[element_define.left_flow_element] = true
end
end
-- Add the flows that dont have a top element
for _, element_define in ipairs(Gui.left_elements) do
if not element_map[element_define] then
table.insert(elements, element_define)
end
end
return elements
end)
--- Overwrite the default toggle behaviour and instead toggle this menu
Gui.core_defines.hide_top_flow:on_click(function(player, _, _)
Gui.toggle_left_element(player, toolbar_container)
end)
--- Overwrite the default toggle behaviour and instead toggle this menu
Gui.core_defines.show_top_flow:on_click(function(player, _, _)
Gui.toggle_left_element(player, toolbar_container)
end)
--- Overwrite the default update top flow
local _update_top_flow = Gui.update_top_flow
function Gui.update_top_flow(player)
_update_top_flow(player) -- Call the original
local order = ToolbarState:get(player)
for index, state in ipairs(order) do
local element_define = Gui.defines[state.element_uid]
local top_element = Gui.get_top_element(player, element_define)
top_element.visible = top_element.visible and state.favourite or false
end
end
--- Uncompress the data to be more useable
ToolbarState:on_load(function(player_name, value)
-- If there is no value, do nothing
if value == nil then return end
-- Create a hash map of the favourites
local favourites = {}
for _, id in ipairs(value[2]) do
favourites[id] = true
end
-- Read the order from the value
local elements = {}
local element_hash = {}
for index, id in ipairs(value[1]) do
local element = datastore_id_map[id]
if element and not element_hash[element.uid] then
element_hash[element.uid] = true
elements[index] = {
element_uid = element.uid,
favourite = favourites[id] or false,
}
end
end
-- Add any in the default state that are missing
for _, state in ipairs(toolbar_default_state) do
if not element_hash[state.element_uid] then
table.insert(elements, table.deep_copy(state))
end
end
-- Create a hash map of the open left flows
local left_flows = {}
for _, id in ipairs(value[3]) do
local element = datastore_id_map[id]
if element.left_flow_element then
left_flows[element.left_flow_element] = true
end
end
-- Set the visible state of all left flows
local player = game.get_player(player_name)
for _, left_element in ipairs(Gui.left_elements) do
Gui.toggle_left_element(player, left_element, left_flows[left_element] or false)
end
-- Set the toolbar visible state
Gui.toggle_top_flow(player, value[4])
-- Set the data now and update now, ideally this would be on_update but that had too large of a latency
ToolbarState:raw_set(player_name, elements)
Gui.reorder_top_flow(player)
Gui.reorder_left_flow(player)
reorder_toolbar_menu(player)
return elements
end)
--- Save the current state of the players toolbar menu
ToolbarState:on_save(function(player_name, value)
if value == nil then return nil end -- Don't save default
local order, favourites, left_flows = {}, {}, {}
local player = game.get_player(player_name)
local top_flow_open = Gui.get_top_flow(player).parent.visible
for index, state in ipairs(value) do
-- Add the element to the order array
local element_define = Gui.defines[state.element_uid]
local id = to_datastore_id(element_define)
order[index] = id
-- If its a favourite then insert it
if state.favourite then
table.insert(favourites, id)
end
-- If it has a left flow and its open then insert it
if element_define.left_flow_element then
local left_element = Gui.get_left_element(player, element_define.left_flow_element)
if left_element.visible then
table.insert(left_flows, id)
end
end
end
return { order, favourites, left_flows, top_flow_open }
end)

View File

@@ -0,0 +1,528 @@
--[[-- Gui Module - Virtual Layer
- Adds a virtual layer to store power to save space.
@gui Virtual Layer
@alias vlayer_container
]]
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Event = require 'utils.event' --- @dep utils.event
local format_number = require('util').format_number --- @dep util
local config = require 'config.vlayer' --- @dep config.vlayer
local vlayer = require 'modules.control.vlayer'
local Selection = require 'modules.control.selection' --- @dep modules.control.selection
local SelectionConvertArea = 'VlayerConvertChest'
--- 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
local vlayer_gui_control_type
local vlayer_gui_control_list
local vlayer_control_type_list = {
[1] = 'energy',
[2] = 'circuit',
[3] = 'storage_input',
[4] = 'storage_output'
}
local function pos_to_gps_string(pos)
return '[gps=' .. string.format('%.1f', pos.x) .. ',' .. string.format('%.1f', pos.y) .. ']'
end
local function format_energy(amount, unit)
if amount < 1 then
return '0 ' .. unit
end
local suffix = ''
local suffix_list = {
['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
--- When an area is selected to add protection to the area
Selection.on_selection(SelectionConvertArea, function(event)
local area = aabb_align_expand(event.area)
local player = game.get_player(event.player_index)
if not player then
return nil
end
local entities = player.surface.find_entities_filtered{area=area, name='steel-chest', force=player.force}
local frame = Gui.get_left_element(player, vlayer_container)
local disp = frame.container['vlayer_st_2'].disp.table
local target = vlayer_control_type_list[disp[vlayer_gui_control_type.name].selected_index]
if #entities == 0 then
player.print{'vlayer.steel-chest-detect'}
return nil
elseif #entities > 1 then
player.print{'vlayer.result-unable', {'vlayer.control-type-' .. target:gsub('_', '-')}, {'vlayer.result-multiple'}}
return nil
end
if not entities[1] then
return nil
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 = e.circuit_connected_entities
if not e.get_inventory(defines.inventory.chest).is_empty() then
player.print{'vlayer.steel-chest-empty'}
return nil
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 nil
end
e.destroy()
if target == 'energy' then
if not vlayer.create_energy_interface(player.surface, e_pos, player) then
player.print{'vlayer.result-unable', {'vlayer.control-type-energy'}, {'vlayer.result-space'}}
return nil
end
elseif target == 'circuit' then
vlayer.create_circuit_interface(player.surface, e_pos, e_circ, player)
elseif target == 'storage_input' then
vlayer.create_input_interface(player.surface, e_pos, e_circ, player)
elseif target == 'storage_output' then
vlayer.create_output_interface(player.surface, e_pos, e_circ, player)
end
game.print{'vlayer.interface-result', player.name, pos_to_gps_string(e_pos), {'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.element{
type = 'label',
name = 'vlayer_display_item_solar_name',
caption = {'vlayer.display-item-solar'},
style = 'heading_2_label'
}:style{
width = 200
}
local vlayer_gui_display_item_solar_count =
Gui.element{
type = 'progressbar',
name = 'vlayer_display_item_solar_count',
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.element{
type = 'label',
name = 'vlayer_display_item_accumulator_name',
caption = {'vlayer.display-item-accumulator'},
style = 'heading_2_label'
}:style{
width = 200
}
local vlayer_gui_display_item_accumulator_count =
Gui.element{
type = 'progressbar',
name = 'vlayer_display_item_accumulator_count',
caption = '',
value = 0,
style = 'electric_satisfaction_statistics_progressbar'
}:style{
width = 200,
font = 'heading-2'
}
--- Display label for the remaining surface area
-- @element vlayer_gui_display_signal_remaining_surface_area_name
local vlayer_gui_display_signal_remaining_surface_area_name =
Gui.element{
type = 'label',
name = 'vlayer_display_signal_remaining_surface_area_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_remaining_surface_area_count =
Gui.element{
type = 'label',
name = 'vlayer_display_signal_remaining_surface_area_count',
caption = '0',
style = 'heading_2_label'
}:style{
width = 200,
height = 28,
horizontal_align = 'right'
}
--- Display label for the sustained energy production
-- @element vlayer_gui_display_signal_sustained_name
local vlayer_gui_display_signal_sustained_name =
Gui.element{
type = 'label',
name = 'vlayer_display_signal_sustained_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.element{
type = 'label',
name = 'vlayer_display_signal_sustained_count',
caption = '0',
style = 'heading_2_label'
}:style{
width = 200,
height = 28,
horizontal_align = 'right'
}
--- Display label for the current energy production
-- @element vlayer_gui_display_signal_production_name
local vlayer_gui_display_signal_production_name =
Gui.element{
type = 'label',
name = 'vlayer_display_signal_production_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.element{
type = 'progressbar',
name = 'vlayer_display_signal_production_count',
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.element{
type = 'label',
name = 'vlayer_display_signal_capacity_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.element{
type = 'progressbar',
name = 'vlayer_display_signal_capacity_count',
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.element(function(_, parent, name)
local vlayer_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.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_remaining_surface_area_name(disp)
vlayer_gui_display_signal_remaining_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 frame = Gui.get_left_element(player, vlayer_container)
local disp = frame.container['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.element{
type = 'drop-down',
name = Gui.unique_static_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_changed(function(player, _, _)
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.element{
type = 'drop-down',
name = Gui.unique_static_name
}:style{
width = 200
}
--- A button to refresh the remove list
-- @element vlayer_gui_control_refresh
local vlayer_gui_control_refresh =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = {'vlayer.control-refresh'}
}:style{
width = 200
}:on_click(function(player, _, _)
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.element{
type = 'button',
name = Gui.unique_static_name,
caption = {'vlayer.control-see'}
}:style{
width = 200
}:on_click(function(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]] and i[vlayer_control_type_list[target]][n] then
local pos = i[vlayer_control_type_list[target]][n].position
if pos then
player.zoom_to_world(pos, 2)
player.print{'vlayer.result-interface-location', {'vlayer.control-type-' .. vlayer_control_type_list[target]:gsub('_', '-')}, pos_to_gps_string(pos)}
end
end
end
end)
--- A button used to build the vlayer interface
-- @element vlayer_gui_control_build
local vlayer_gui_control_build =
Gui.element{
type = 'button',
name = Gui.unique_static_name,
caption = {'vlayer.control-build'}
}:style{
width = 200
}:on_click(function(player, _, _)
if Selection.is_selecting(player, SelectionConvertArea) then
Selection.stop(player)
else
Selection.start(player, SelectionConvertArea)
player.print{'expcom-waterfill.entered-area-selection'}
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.element{
type = 'button',
name = Gui.unique_static_name,
caption = {'vlayer.control-remove'}
}:style{
width = 200
}:on_click(function(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_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), {'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.element(function(_, parent, name)
local vlayer_set = parent.add{type='flow', direction='vertical', name=name}
local disp = Gui.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)
vlayer_gui_control_build(disp)
vlayer_gui_control_remove(disp)
return vlayer_set
end)
--- The main container for the vlayer gui
-- @element vlayer_container
vlayer_container =
Gui.element(function(definition, parent)
local player = Gui.get_player_from_element(parent)
local container = Gui.container(parent, definition.name, 400)
vlayer_display_set(container, 'vlayer_st_1')
local control_set = vlayer_control_set(container, 'vlayer_st_2')
control_set.visible = Roles.player_allowed(player, 'gui/vlayer-edit')
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
--- Button on the top flow used to toggle the task list container
-- @element toggle_left_element
Gui.left_toolbar_button('entity/solar-panel', {'vlayer.main-tooltip'}, vlayer_container, function(player)
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 frame = Gui.get_left_element(player, vlayer_container)
frame.container['vlayer_st_2'].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']) .. ' / ' .. format_number(items['solar-panel'])
},
[vlayer_gui_display_item_accumulator_count.name] = {
val = (items_alloc['accumulator'] / math.max(items['accumulator'], 1)),
cap = format_number(items_alloc['accumulator']) .. ' / ' .. format_number(items['accumulator'])
},
[vlayer_gui_display_signal_remaining_surface_area_count.name] = {
cap = format_number(stats.remaining_surface_area)
},
[vlayer_gui_display_signal_sustained_count.name] = {
cap = format_energy(stats.energy_sustained, '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 frame = Gui.get_left_element(player, vlayer_container)
local disp = frame.container['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)

View File

@@ -0,0 +1,902 @@
--[[-- Gui Module - Warp List
- Adds a warp list gui which allows players to add and remove warp points
@gui Warps-List
@alias warp_list
]]
local Gui = require 'expcore.gui' --- @dep expcore.gui
local Datastore = require 'expcore.datastore' --- @dep expcore.datastore
local Global = require 'utils.global' --- @dep utils.global
local Event = require 'utils.event' --- @dep utils.event
local Roles = require 'expcore.roles' --- @dep expcore.roles
local Colors = require 'utils.color_presets' --- @dep utils.color_presets
local config = require 'config.gui.warps' --- @dep config.gui.warps
local Warps = require 'modules.control.warps' --- @dep modules.control.warps
local format_time, player_return = _C.format_time, _C.player_return --- @dep expcore.common
--- Stores all data for the warp gui
local WrapGuiData = Datastore.connect('WrapGuiData')
WrapGuiData:set_serializer(Datastore.name_serializer)
local PlayerInRange = WrapGuiData:combine('PlayerInRange')
PlayerInRange:set_default(false)
local PlayerCooldown = WrapGuiData:combine('PlayerCooldown')
PlayerCooldown:set_default(0)
--- Table that stores a boolean value of weather to keep the warp gui open
local keep_gui_open = {}
Global.register(keep_gui_open, function(tbl)
keep_gui_open = tbl
end)
--- Styles used for sprite buttons
local Styles = {
sprite22 = { height = 22, width = 22, padding = -2 },
sprite32 = { height = 32, width = 32, left_margin = 1 }
}
--- Status icon of a warp
local warp_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]',
}
--- Returns if a player is allowed to edit the given warp
--- If a player is allowed to use the edit buttons
local function check_player_permissions(player, action, warp)
-- Check if the action is allow edit and then check bypass settings
if action == 'allow_edit_warp' then
-- Check if the warp is the spawn then it cant be edited
local spawn_id = Warps.get_spawn_warp_id(player.force.name)
if spawn_id == warp.warp_id then
return false
end
-- Check if the player being the last to edit will override existing permisisons
if config.user_can_edit_own_warps and warp.last_edit_name == player.name then
return true
end
end
-- Check player has permission based on value in the config
local action_config = config[action]
if action_config == 'all' then
return true
elseif action_config == 'admin' then
return player.admin
elseif action_config == 'expcore.roles' then
return Roles.player_allowed(player, config['expcore_roles_'..action])
end
-- Return false as all other conditions have not been met
return false
end
--- Will add a new warp to the list, checks if the player is too close to an existing one
-- @element add_new_warp
local add_new_warp =
Gui.element{
type = 'sprite-button',
sprite = 'utility/add',
tooltip = {'warp-list.add-tooltip'},
style = 'shortcut_bar_button',
name = Gui.unique_static_name
}
:style(Styles.sprite22)
:on_click(function(player, _)
-- Add the new warp
local force_name = player.force.name
local surface = player.surface
local position = player.position
-- Check if the warp is too close to water
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_return({'expcore-commands.command-fail', {'warp-list.too-close-to-water', config.standard_proximity_radius + 1}}, 'orange_red', player)
if game.player then game.player.play_sound{ path = 'utility/wire_pickup' } end
for _, tile in pairs(water_tiles) do
rendering.draw_sprite{
sprite = 'utility/rail_path_not_possible',
x_scale = 0.5,
y_scale = 0.5,
target = tile.position,
surface = surface,
players = {player},
time_to_live = 60
}
end
return
end
-- Check if there are player entities in the way (has a bigger radius because the enities that can be placed by a player are larger)
local entities = surface.find_entities_filtered{
radius = config.standard_proximity_radius + 2.5,
position = position,
collision_mask = {
'item-layer', 'object-layer', 'player-layer', 'water-tile'
}
}
-- Remove 1 because that is the current player
if #entities > 1 then
player_return({'expcore-commands.command-fail', {'warp-list.too-close-to-entities', config.standard_proximity_radius + 2.5}}, 'orange_red', player)
if game.player then game.player.play_sound{path='utility/wire_pickup'} end
local character = player.character
for _, entity in pairs(entities) do
if entity ~= character then
rendering.draw_sprite{
sprite = 'utility/rail_path_not_possible',
x_scale = 0.5,
y_scale = 0.5,
target = entity,
surface = surface,
players = {player},
time_to_live = 60
}
end
end
return
end
-- Create the warp
local warp_id = Warps.add_warp(force_name, surface, position, player.name)
Warps.make_warp_tag(warp_id)
Warps.make_warp_area(warp_id)
end)
--- Warp icon button, this will trigger a warp when the player is able to
-- @element warp_icon_button
local warp_icon_button =
Gui.element(function(definition, parent, warp)
local warp_position = warp.position
-- The SpritePath type is not the same as the SignalID type
local sprite = warp.icon.type .. '/' ..warp.icon.name
if warp.icon.type == 'virtual' then
sprite = 'virtual-signal/' ..warp.icon.name
end
-- Draw the element
return parent.add{
type = 'sprite-button',
sprite = sprite,
name = definition.name,
tooltip = {'warp-list.goto-tooltip', warp_position.x, warp_position.y},
style = 'slot_button'
}
end)
:style(Styles.sprite32)
:static_name(Gui.unique_static_name)
:on_click(function(player, element, _)
if element.type == 'choose-elem-button' then return end
local warp_id = element.parent.caption
Warps.teleport_player(warp_id, player)
-- Reset the warp cooldown if the player does not have unlimited warps
if not check_player_permissions(player, 'bypass_warp_cooldown') then
PlayerCooldown:set(player, config.update_smoothing*config.cooldown_duration)
end
PlayerInRange:set(player, warp_id)
end)
--- The button that is visible when the warp is in edit state
-- @element warp_icon_editing
local warp_icon_editing =
Gui.element(function(definition, parent, warp)
return parent.add{
name = definition.name,
type = 'choose-elem-button',
elem_type = 'signal',
signal = {type = warp.icon.type, name = warp.icon.name},
tooltip = {'warp-list.goto-edit'}
}
end)
:static_name(Gui.unique_static_name)
:style(Styles.sprite32)
--- Warp label, visible if the player is not in edit state
-- @element warp_label
local warp_label =
Gui.element(function(definition, parent, warp)
local last_edit_name = warp.last_edit_name
local last_edit_time = warp.last_edit_time
-- Draw the element
return parent.add{
type = 'label',
caption = warp.name,
tooltip = {'warp-list.last-edit', last_edit_name, format_time(last_edit_time)},
name = definition.name
}
end)
:style{
single_line = true,
left_padding = 2,
right_padding = 2,
horizontally_stretchable = true
}
:on_click(function(player, element, _)
local warp_id = element.parent.caption
local warp = Warps.get_warp(warp_id)
local position = warp.position
player.zoom_to_world(position, 1.5)
end)
:static_name(Gui.unique_static_name)
--- Warp status, visible if the player is not in edit state
--- This will show if the warp is connected or not
-- @element warp_status
local warp_status =
Gui.element{
type = 'label',
caption = '[img=utility/electricity_icon_unplugged]', -- Temporary icon
name = Gui.unique_static_name
}
:style{
-- When editing mode because textbox is larger the icon would move up.
top_padding = 1,
single_line = false,
}
--- Warp textfield, visible if the player is in edit state
-- @element warp_textfield
local warp_textfield =
Gui.element(function(definition, parent, warp)
-- Draw the element
return parent.add{
type = 'textfield',
text = warp.name,
clear_and_focus_on_right_click = true,
name = definition.name
}
end)
:style{
-- Required fields to make it squashable and strechable.
minimal_width = 10,
maximal_width = 300,
horizontally_squashable = "on",
horizontally_stretchable = "on",
-- Other styling
height = 22,
padding = -2,
left_margin = 2,
right_margin = 2,
}
:on_confirmed(function(player, element, _)
local warp_id = element.parent.caption
local warp_name = element.text
local warp_icon = element.parent.parent['icon-'..warp_id][warp_icon_editing.name].elem_value
Warps.set_editing(warp_id, player.name)
Warps.update_warp(warp_id, warp_name, warp_icon, player.name)
end)
:static_name(Gui.unique_static_name)
--- Confirms the edit to name or icon of the warp
-- @element confirm_edit_button
local confirm_edit_button =
Gui.element{
type = 'sprite-button',
sprite = 'utility/confirm_slot',
tooltip = {'warp-list.confirm-tooltip'},
style = 'shortcut_bar_button_green',
name = Gui.unique_static_name
}
:style(Styles.sprite22)
:on_click(function(player, element)
local warp_id = element.parent.caption
local warp_name = element.parent.parent['name-'..warp_id][warp_textfield.name].text
local warp_icon = element.parent.parent['icon-'..warp_id][warp_icon_editing.name].elem_value
Warps.set_editing(warp_id, player.name)
Warps.update_warp(warp_id, warp_name, warp_icon, player.name)
end)
--- Cancels the editing changes of the selected warp name or icon
-- @element cancel_edit_button
local cancel_edit_button =
Gui.element{
type = 'sprite-button',
sprite = 'utility/close_black',
tooltip = {'warp-list.cancel-tooltip'},
style = 'shortcut_bar_button_red',
name = Gui.unique_static_name
}
:style(Styles.sprite22)
:on_click(function(player, element)
local warp_id = element.parent.caption
-- Check if this is the first edit, if so remove the warp.
local warp = Warps.get_warp(warp_id)
if warp.updates == 1 then
Warps.remove_warp(warp_id)
return
end
Warps.set_editing(warp_id, player.name)
end)
--- Removes a warp from the list, including the physical area and map tag
-- @element remove_warp_button
local remove_warp_button =
Gui.element{
type = 'sprite-button',
sprite = 'utility/trash',
tooltip = {'warp-list.remove-tooltip'},
style = 'shortcut_bar_button_red',
name = Gui.unique_static_name
}
:style(Styles.sprite22)
:on_click(function(_, element)
local warp_id = element.parent.caption
Warps.remove_warp(warp_id)
end)
--- Opens edit mode for the warp
-- @element edit_warp_button
local edit_warp_button =
Gui.element{
type = 'sprite-button',
sprite = 'utility/rename_icon_normal',
tooltip = {'warp-list.edit-tooltip-none'},
style = 'shortcut_bar_button',
name = Gui.unique_static_name
}
:style(Styles.sprite22)
:on_click(function(player, element)
local warp_id = element.parent.caption
Warps.set_editing(warp_id, player.name, true)
end)
local update_all_warp_elements
--- Set of three elements which make up each row of the warp table
-- @element add_warp_elements
local add_warp_elements =
Gui.element(function(_, parent, warp)
-- Add icon flow, this will contain the warp button and warp icon edit button
local icon_flow = parent.add{
name = 'icon-'..warp.warp_id,
type = 'flow',
caption = warp.warp_id
}
icon_flow.style.padding = 0
-- Add the button and the icon edit button
warp_icon_button(icon_flow, warp)
warp_icon_editing(icon_flow, warp)
-- Add name flow, this will contain the warp label and textbox
local name_flow = parent.add{
type = 'flow',
name = 'name-'..warp.warp_id,
caption = warp.warp_id
}
name_flow.style.padding = 0
-- Add the label and textfield of the warp
warp_status(name_flow)
warp_label(name_flow, warp)
warp_textfield(name_flow, warp)
-- Add button flow, this will contain buttons to manage this specific warp
local button_flow = parent.add{
type = 'flow',
name = 'button-'..warp.warp_id,
caption = warp.warp_id
}
button_flow.style.padding = 0
-- Add both edit state buttons
confirm_edit_button(button_flow)
cancel_edit_button(button_flow)
edit_warp_button(button_flow)
remove_warp_button(button_flow)
-- Return the warp flow elements
return { icon_flow, name_flow, button_flow }
end)
-- Removes the three elements that are added as part of the warp base
local function remove_warp_elements(parent, warp_id)
Gui.destroy_if_valid(parent['icon-'..warp_id])
Gui.destroy_if_valid(parent['name-'..warp_id])
Gui.destroy_if_valid(parent['button-'..warp_id])
end
--- This timer controls when a player is able to warp, eg every 60 seconds
-- @element warp_timer
local warp_timer =
Gui.element{
type = 'progressbar',
name = Gui.unique_static_name,
tooltip = {'warp-list.timer-tooltip-zero', config.cooldown_duration},
minimum_value = 0,
maximum_value = config.cooldown_duration*config.update_smoothing
}
:style{
horizontally_stretchable = true,
color = Colors.light_blue
}
local warp_list_container
-- Helper function to style and enable or disable a button element
local function update_warp_elements(element, warp, warp_player_is_on, on_cooldown, bypass_warp_proximity)
-- Check if button element is valid
if not element or not element.valid then return end
local label_style = element.parent.parent['name-'..warp.warp_id][warp_label.name].style
local warp_status_element = element.parent.parent['name-'..warp.warp_id][warp_status.name]
-- If player is not on a warp
if not warp_player_is_on then
-- If player is allowed to warp without being on a warp. If not then disable the warp location
if bypass_warp_proximity then
local position = warp.position
element.tooltip = {'warp-list.goto-bypass', position.x, position.y}
element.enabled = true
warp_status_element.tooltip = {'warp-list.goto-bypass', position.x, position.y}
warp_status_element.caption = warp_status_icons.bypass
label_style.font = 'default-semibold'
else
element.tooltip = {'warp-list.goto-disabled'}
element.enabled = false
warp_status_element.tooltip = {'warp-list.goto-disabled'}
warp_status_element.caption = warp_status_icons.not_available
label_style.font = 'default'
end
-- If player is on the warp that is being updated
elseif warp_player_is_on.warp_id == warp.warp_id then
element.tooltip = {'warp-list.goto-same-warp'}
element.enabled = false
warp_status_element.tooltip = {'warp-list.goto-same-warp'}
warp_status_element.caption = warp_status_icons.current
label_style.font = 'default'
-- If player is on cooldown
elseif on_cooldown then
element.tooltip = {'warp-list.goto-cooldown'}
element.enabled = false
warp_status_element.tooltip = {'warp-list.goto-cooldown'}
warp_status_element.caption = warp_status_icons.cooldown
label_style.font = 'default'
else
-- If the warp the player is standing on is the same as the warp that is being updated
local warp_electric_network_id = warp.electric_pole and warp.electric_pole.electric_network_id or -1
local player_warp_electric_network_id = warp_player_is_on.electric_pole and warp_player_is_on.electric_pole.electric_network_id or -2
if warp_electric_network_id == player_warp_electric_network_id then
local position = warp.position
element.tooltip = {'warp-list.goto-tooltip', position.x, position.y}
element.enabled = true
warp_status_element.tooltip = {'warp-list.goto-tooltip', position.x, position.y}
warp_status_element.caption = warp_status_icons.connected
label_style.font = 'default-semibold'
-- If the warp is not on the same network but the player is allowed to warp without being on a warp
elseif bypass_warp_proximity then
local position = warp.position
element.tooltip = {'warp-list.goto-bypass-different-network', position.x, position.y}
element.enabled = true
warp_status_element.tooltip = {'warp-list.goto-bypass-different-network', position.x, position.y}
warp_status_element.caption = warp_status_icons.bypass
label_style.font = 'default-semibold'
-- If the warp is on a different network than the one the player is standing on
else
element.tooltip = {'warp-list.goto-different-network'}
element.enabled = false
warp_status_element.tooltip = {'warp-list.goto-different-network'}
warp_status_element.caption = warp_status_icons.different
label_style.font = 'default'
end
end
end
--- Update the warp buttons for a player
function update_all_warp_elements(player, timer, warp_id)
-- Get the warp table
local frame = Gui.get_left_element(player, warp_list_container)
local scroll_table = frame.container.scroll.table
-- Check if the player is currenty on cooldown
timer = timer or PlayerCooldown:get(player)
local on_cooldown = timer > 0
-- Get the warp the player is on
warp_id = warp_id or PlayerInRange:get(player)
local warp_player_is_on = warp_id and Warps.get_warp(warp_id) or nil
-- Check player permission
local bypass_warp_proximity = check_player_permissions(player, 'bypass_warp_proximity')
-- Change the enabled state of the warp buttons
local warp_ids = Warps.get_force_warp_ids(player.force.name)
for _, next_warp_id in pairs(warp_ids) do
local element = scroll_table['icon-'..next_warp_id][warp_icon_button.name]
local next_warp = Warps.get_warp(next_warp_id)
update_warp_elements(element, next_warp, warp_player_is_on, on_cooldown, bypass_warp_proximity)
end
end
--- Updates a warp for a player
local function update_warp(player, warp_table, warp_id)
local warp = Warps.get_warp(warp_id)
-- If the warp no longer exists then remove the warp elements from the warp table
if not warp then
remove_warp_elements(warp_table, warp_id)
return
end
-- Create the warp elements if they do not already exist
if not warp_table['icon-'..warp_id] then
add_warp_elements(warp_table, warp)
end
local icon_flow = warp_table['icon-'..warp_id]
local name_flow = warp_table['name-'..warp_id]
local button_flow = warp_table['button-'..warp_id]
-- Create local references to the elements for this warp
local warp_icon_element = icon_flow[warp_icon_button.name]
local warp_icon_edit_element = icon_flow[warp_icon_editing.name]
local label_element = name_flow[warp_label.name]
local textfield_element = name_flow[warp_textfield.name]
local cancel_edit_element = button_flow[cancel_edit_button.name]
local confirm_edit_element = button_flow[confirm_edit_button.name]
local edit_warp_element = button_flow[edit_warp_button.name]
local remove_warp_element = button_flow[remove_warp_button.name]
-- Hide the edit button if the player is not allowed to edit the warp
local player_allowed_edit = check_player_permissions(player, 'allow_edit_warp', warp)
local players_editing = table.get_keys(warp.currently_editing)
edit_warp_element.visible = player_allowed_edit
-- Set the tooltip of the edit button
if #players_editing > 0 then
edit_warp_element.hovered_sprite = 'utility/warning_icon'
edit_warp_element.tooltip = {'warp-list.edit-tooltip', table.concat(players_editing, ', ')}
else
edit_warp_element.hovered_sprite = edit_warp_element.sprite
edit_warp_element.tooltip = {'warp-list.edit-tooltip-none'}
end
-- Set the visibility of the warp elements based on whether the user is editing or not
local player_is_editing = warp.currently_editing[player.name]
if player_is_editing then
-- Set the icon elements visibility
warp_icon_element.visible = false
warp_icon_edit_element.visible = true
-- Set the name elements visibility
label_element.visible = false
textfield_element.visible = true
textfield_element.focus()
warp_table.parent.scroll_to_element(textfield_element, 'top-third')
-- Set the edit buttons
cancel_edit_element.visible = true
confirm_edit_element.visible = true
-- Set the warp buttons
edit_warp_element.visible = false
remove_warp_element.visible = false
else
-- Set the icon elements visibility
warp_icon_element.visible = true
-- The SpritePath type is not the same as the SignalID type
local sprite = warp.icon.type .. '/' ..warp.icon.name
if warp.icon.type == 'virtual' then
sprite = 'virtual-signal/' ..warp.icon.name
end
warp_icon_element.sprite = sprite
-- Set icon edit to the warps icon
warp_icon_edit_element.elem_value = warp.icon
warp_icon_edit_element.visible = false
-- Set the name elements visibility
label_element.visible = true
label_element.caption = warp.name
textfield_element.visible = false
textfield_element.text = warp.name
-- Set the edit buttons
cancel_edit_element.visible = false
confirm_edit_element.visible = false
-- Set the warp buttons
edit_warp_element.visible = true and player_allowed_edit
remove_warp_element.visible = true and player_allowed_edit
end
local timer = PlayerCooldown:get(player)
local current_warp_id = PlayerInRange:get(player)
local to_warp = current_warp_id and Warps.get_warp(current_warp_id) or nil
local bypass_warp_proximity = check_player_permissions(player, 'bypass_warp_proximity')
update_warp_elements(warp_icon_element, warp, to_warp, timer > 0, bypass_warp_proximity)
end
-- Update all the warps for a player
local function update_all_warps(player, warp_table)
local warp_ids = Warps.get_force_warp_ids(player.force.name)
warp_table.clear()
for _, warp_id in ipairs(warp_ids) do
update_warp(player, warp_table, warp_id)
end
end
-- Update all warps for all players on a force
local function update_all_warp_force(force)
local warp_ids = Warps.get_force_warp_ids(force.name)
for _, player in pairs(force.connected_players) do
local frame = Gui.get_left_element(player, warp_list_container)
local warp_table = frame.container.scroll.table
warp_table.clear() -- Needed to re-sort the warps
for _, warp_id in ipairs(warp_ids) do
update_warp(player, warp_table, warp_id)
end
end
end
--- Main warp list container for the left flow
-- @element warp_list_container
warp_list_container =
Gui.element(function(definition, parent)
local player = Gui.get_player_from_element(parent)
-- Check if user has permission to add warps
local allow_add_warp = check_player_permissions(player, 'allow_add_warp')
-- Draw the internal container
local container = Gui.container(parent, definition.name, allow_add_warp and 268 or 220)
-- Draw the header
local header = Gui.header(
container,
{'warp-list.main-caption'},
{
'warp-list.sub-tooltip',
config.cooldown_duration,
config.standard_proximity_radius,
{'warp-list.sub-tooltip-current',warp_status_icons.current},
{'warp-list.sub-tooltip-connected',warp_status_icons.connected},
{'warp-list.sub-tooltip-different',warp_status_icons.different},
{'warp-list.sub-tooltip-cooldown',warp_status_icons.cooldown},
{'warp-list.sub-tooltip-not_available',warp_status_icons.not_available},
{'warp-list.sub-tooltip-bypass',warp_status_icons.bypass},
},
true
)
-- Draw the new warp button
local add_new_warp_element = add_new_warp(header)
add_new_warp_element.visible = allow_add_warp
-- Draw the scroll table for the warps
local scroll_table = Gui.scroll_table(container, 250, 3)
-- Set the scroll panel to always show the scrollbar (not doing this will result in a changing gui size)
scroll_table.parent.vertical_scroll_policy = 'always'
-- Change the style of the scroll table
local scroll_table_style = scroll_table.style
scroll_table_style.top_cell_padding = 3
scroll_table_style.bottom_cell_padding = 3
-- Draw the warp cooldown progress bar
local warp_timer_element = warp_timer(container)
-- Change the progress of the warp timer
local timer = PlayerCooldown:get(player)
if timer > 0 then
warp_timer_element.tooltip = {'warp-list.timer-tooltip', math.floor(timer/config.update_smoothing)}
warp_timer_element.value = 1 - (timer/config.update_smoothing/config.cooldown_duration)
else
warp_timer_element.tooltip = {'warp-list.timer-tooltip-zero', config.cooldown_duration}
warp_timer_element.value = 1
end
-- Add any existing warps
update_all_warps(player, scroll_table)
-- Return the external container
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow()
--- Button on the top flow used to toggle the warp list container
-- @element toggle_warp_list
Gui.left_toolbar_button(config.default_icon.type ..'/'..config.default_icon.name, {'warp-list.main-tooltip'}, warp_list_container, function(player)
return Roles.player_allowed(player, 'gui/warp-list')
end)
:on_event(Gui.events.on_visibility_changed_by_click, function(player, _,event)
-- Set gui keep open state for player that clicked the button: true if visible, false if invisible
keep_gui_open[player.name] = event.state
end)
--- When the name of a warp is updated this is triggered
Warps.on_update(function(_, warp, old_warp)
-- Get the force to update, warp is nil when removed
if warp then
update_all_warp_force(game.forces[warp.force_name])
else
update_all_warp_force(game.forces[old_warp.force_name])
end
end)
--- When the player leaves or enters range of a warp this is triggered
PlayerInRange:on_update(function(player_name, warp_id)
local player = game.players[player_name]
-- Change if the frame is visible based on if the player is in range
if not keep_gui_open[player.name] then
Gui.toggle_left_element(player, warp_list_container, warp_id ~= nil)
end
update_all_warp_elements(player, nil, warp_id)
end)
--- Update the warp cooldown progress bars to match the current cooldown
PlayerCooldown:on_update(function(player_name, player_cooldown)
-- Get the progress bar element
local player = game.players[player_name]
local frame = Gui.get_left_element(player, warp_list_container)
local warp_timer_element = frame.container[warp_timer.name]
-- Set the progress
if player_cooldown and player_cooldown > 0 then
warp_timer_element.tooltip = {'warp-list.timer-tooltip', math.floor(player_cooldown/config.update_smoothing)}
warp_timer_element.value = 1 - (player_cooldown/config.update_smoothing/config.cooldown_duration)
else
warp_timer_element.tooltip = {'warp-list.timer-tooltip-zero', config.cooldown_duration}
warp_timer_element.value = 1
end
-- Trigger update of buttons if cooldown is now 0
if player_cooldown == 0 then
update_all_warp_elements(player, player_cooldown, nil)
end
end)
--- Handles updating the timer and checking distance from a warp
local r2 = config.standard_proximity_radius^2
local rs2 = config.spawn_proximity_radius^2
local mr2 = config.minimum_distance^2
Event.on_nth_tick(math.floor(60/config.update_smoothing), function()
PlayerCooldown:update_all(function(_, player_cooldown)
if player_cooldown > 0 then return player_cooldown - 1 end
end)
local force_warps = {}
local warps = {}
for _, player in pairs(game.connected_players) do
local was_in_range = PlayerInRange:get(player)
-- Get the ids of all the warps on the players force
local force_name = player.force.name
local warp_ids = force_warps[force_name]
if not warp_ids then
warp_ids = Warps.get_force_warp_ids(force_name)
force_warps[force_name] = warp_ids
end
-- Check if the force has any warps
local closest_warp = nil
local closest_distance = nil
if #warp_ids > 0 then
local surface = player.surface
local pos = player.position
local px, py = pos.x, pos.y
-- Loop over each warp
for _, warp_id in ipairs(warp_ids) do
-- Check if warp id is cached
local warp = warps[warp_id]
if not warp then
warp = Warps.get_warp(warp_id)
warps[warp_id] = warp
end
-- Check if the player is within range
local warp_pos = warp.position
if warp.surface == surface then
local dx, dy = px-warp_pos.x, py-warp_pos.y
local dist = (dx*dx)+(dy*dy)
if closest_distance == nil or dist < closest_distance then
closest_warp = warp
closest_distance = dist
if dist < r2 then break end
end
end
end
-- Check the dist to the closest warp
local in_range = closest_warp ~= nil and (closest_warp.warp_id == warp_ids.spawn and closest_distance < rs2 or closest_distance < r2)
if was_in_range and not in_range then
PlayerInRange:set(player, nil)
elseif not was_in_range and in_range then
---@cast closest_warp -nil
PlayerInRange:set(player, closest_warp.warp_id)
end
-- Change the enabled state of the add warp button
local frame = Gui.get_left_element(player, warp_list_container)
local add_warp_element = frame.container.header.alignment[add_new_warp.name]
local old_closest_warp_name = add_warp_element.tooltip[2] or closest_warp and closest_warp.name
local was_able_to_make_warp = add_warp_element.enabled
local can_make_warp = closest_distance == nil or closest_distance > mr2
if can_make_warp and not was_able_to_make_warp then
add_warp_element.enabled = true
add_warp_element.tooltip = {'warp-list.add-tooltip'}
elseif not can_make_warp and was_able_to_make_warp or closest_warp and (old_closest_warp_name ~= closest_warp.name) then
---@cast closest_warp -nil
add_warp_element.enabled = false
add_warp_element.tooltip = {'warp-list.too-close', closest_warp.name}
end
end
end
end)
--- When a player is created make sure that there is a spawn warp created
Event.add(defines.events.on_player_created, function(event)
-- If the force has no spawn then make a spawn warp
local player = game.players[event.player_index]
local force = player.force
local spawn_id = Warps.get_spawn_warp_id(force.name)
if not spawn_id then
local spawn_position = force.get_spawn_position(player.surface)
spawn_id = Warps.add_warp(force.name, player.surface, spawn_position, nil, 'Spawn')
Warps.set_spawn_warp(spawn_id, force)
Warps.make_warp_tag(spawn_id)
local entities = player.surface.find_entities_filtered{type='electric-pole', position=spawn_position, radius=20, limit=1}
if entities and entities[1] then
local warp = Warps.get_warp(spawn_id)
warp.electric_pole = entities[1]
end
end
end)
--- Update the warps when the player joins
Event.add(defines.events.on_player_joined_game, function(event)
local player = game.players[event.player_index]
local frame = Gui.get_left_element(player, warp_list_container)
local scroll_table = frame.container.scroll.table
update_all_warps(player, scroll_table)
end)
--- Makes sure the right buttons are present when roles change
local function role_update_event(event)
local player = game.players[event.player_index]
local container = Gui.get_left_element(player, warp_list_container).container
-- Check if user has permission to add warps
local allow_add_warp = check_player_permissions(player, 'allow_add_warp')
-- Update container size depending on whether the player is allowed to add warps
container.parent.style.width = allow_add_warp and 268 or 220
-- Update the warps, incase the user can now edit them
local scroll_table = container.scroll.table
update_all_warps(player, scroll_table)
-- Update the new warp button incase the user can now add them
local add_new_warp_element = container.header.alignment[add_new_warp.name]
add_new_warp_element.visible = allow_add_warp
end
Event.add(Roles.events.on_role_assigned, role_update_event)
Event.add(Roles.events.on_role_unassigned, role_update_event)
--- When a chart tag is removed or edited make sure it is not one that belongs to a warp
local function maintain_tag(event)
if not event.player_index then return end
local tag = event.tag
local force_name = event.force.name
local warp_ids = Warps.get_force_warp_ids(force_name)
for _, warp_id in pairs(warp_ids) do
local warp = Warps.get_warp(warp_id)
local warp_tag = warp.tag
if not warp_tag or not warp_tag.valid or warp_tag == tag then
if event.name == defines.events.on_chart_tag_removed then
warp.tag = nil
end
Warps.make_warp_tag(warp_id)
end
end
end
Event.add(defines.events.on_chart_tag_modified, maintain_tag)
Event.add(defines.events.on_chart_tag_removed, maintain_tag)