Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -1264,14 +1264,14 @@ stds.factorio_defines = {
},
flow_precision_index = {
fields = {
'fifty_hours',
'one_hour',
'one_minute',
'one_second',
'five_seconds',
'one_minute',
'ten_minutes',
'one_hour',
'ten_hours',
'fifty_hours',
'two_hundred_fifty_hours',
'one_thousand_hours',
'ten_hours',
'ten_minutes',
'two_hundred_fifty_hours'
}
},
group_state = {
Expand Down
6 changes: 6 additions & 0 deletions config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,12 @@ storage.config = {
['stone'] = 1,
['uranium-ore'] = 5,
},
},
-- adds a small display always on screen for production stats
production_hud = {
enabled = true,
starting_items = { 'iron-ore', 'copper-ore', 'coal', 'stone' },
limit = 40, -- limit of tracked items per-player
}
}

Expand Down
3 changes: 3 additions & 0 deletions control.lua
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ end
if config.market_chest.enabled then
require 'features.market_chest'
end
if config.production_hud.enabled then
require 'features.gui.production_hud'
end
if config.experience.enabled then
require 'features.gui.experience'
end
Expand Down
338 changes: 338 additions & 0 deletions features/gui/production_hud.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
-- This feature adds a small HUD for production stats,
-- similar to Factorio built-in "P", but always on screen
-- made by RedRafe
-- ======================================================= --

local config = require 'config'.production_hud
local Event = require 'utils.event'
local Global = require 'utils.global'
local Gui = require 'utils.gui'
local math = require 'utils.math'
local table = require 'utils.table'
local math_abs = math.abs
local round_sig = math.round_sig

local Public = {}

local player_settings = {
--[player.index] = {
-- precision_index = defines.flow_precision_index.ten_minutes,
-- items = { 'iron-ore', 'copper-ore' },
--}
}

Global.register({
player_settings = player_settings,
}, function(tbl)
player_settings = tbl.player_settings
end)

local item_p = prototypes.item
local fluid_p = prototypes.fluid
local to_time = {
[defines.flow_precision_index.five_seconds] = '5s',
[defines.flow_precision_index.one_minute] = '1m',
[defines.flow_precision_index.ten_minutes] = '10m',
[defines.flow_precision_index.one_hour] = '1h',
[defines.flow_precision_index.ten_hours] = '10h',
[defines.flow_precision_index.fifty_hours] = '50h',
[defines.flow_precision_index.two_hundred_fifty_hours] = '250h',
[defines.flow_precision_index.one_thousand_hours] = '1000h',
}
local si_prefixes = {
{ 'Q', 1e30 }, -- quetta
{ 'R', 1e27 }, -- ronna
{ 'Y', 1e24 }, -- yotta
{ 'Z', 1e21 }, -- zetta
{ 'E', 1e18 }, -- exa
{ 'P', 1e15 }, -- peta
{ 'T', 1e12 }, -- tera
{ 'G', 1e09 }, -- giga
{ 'M', 1e06 }, -- mega
{ 'k', 1e03 }, -- kilo
}

---@param value number
---@return string
local function format_si(value)
if value == 0 then
return '0'
end

local abs_value = math_abs(round_sig(value, 3))
for i = #si_prefixes, 1, -1 do
local suffix = si_prefixes[i]
if abs_value >= suffix[2] then
local scaled = value / suffix[2]
return ('%.2f%s'):format(scaled, suffix[1])
end
end

if value > 100 then
return ('%d'):format(value)
elseif value > 10 then
return ('%.1f'):format(value)
end
return ('%.2f'):format(value)
end

-- == GUI =====================================================================

local main_frame_name = Gui.uid_name()
local main_button_name = Gui.uid_name()
local action_scroll_precision_index = Gui.uid_name()
local action_add_item = Gui.uid_name()
local action_remove_item = Gui.uid_name()

---@param parent LuaGuiElement
---@param items string[]
local function init_hud(parent, items)
Gui.clear(parent)
Public.sanitize_player_settings(parent.player_index)

--- Appending rows
for _, name in pairs(items) do
local flow = parent.add { type = 'flow', direction = 'horizontal', name = name }
flow.add {
type = 'sprite-button',
name = action_remove_item,
sprite = (item_p[name] and 'item.' or 'fluid.') .. name,
tooltip = {
'production_hud.item_tooltip',
{ '?', { 'item-name.' .. name }, { 'entity-name.' .. name }, { 'fluid-name.' .. name } },
},
tags = { name = name },
}
Gui.set_style(flow, { padding = 2, vertical_align = 'center' })
Gui.add_pusher(flow)

local vert = flow.add { type = 'flow', direction = 'vertical', name = 'stats' }
Gui.set_style(vert, { vertical_align = 'center', vertical_spacing = 0, horizontal_align = 'right' })

for _, info in pairs({
{ name = 'plus', font_color = { 150, 255, 150 }, caption = '---' },
{ name = 'minus', font_color = { 255, 150, 150 }, caption = '---' },
}) do
local l = vert.add {
type = 'label',
name = info.name,
caption = info.caption,
}
Gui.set_style(l, { font_color = info.font_color, minimal_width = 52, horizontal_align = 'right' })
end
end
end

---@param player_index uint
Public.sanitize_player_settings = function(player_index)
local s = player_settings[player_index]
if not (s and s.items) then
return
end

for i = #s.items, 1, -1 do
local name = s.items[i]
if item_p[name] == nil and fluid_p[name] == nil then
table.remove(s.items, i)
end
end
end

---@param player LuaPlayer
Public.toggle = function(event)
Public.toggle_main_button(event.player)
end

---@param player LuaPlayer
Public.toggle_main_button = function(player)
local frame = player.gui.screen[main_frame_name]
if frame and frame.valid then
frame.visible = not frame.visible
else
Public.get_main_frame(player)
end
end

---@param player LuaPlayer
Public.get_main_frame = function(player)
local frame = player.gui.screen[main_frame_name]
if frame and frame.valid then
return Public.update_main_frame(player)
end

local data = {}
local settings = player_settings[player.index]

frame = player.gui.screen.add {
type = 'frame',
name = main_frame_name,
direction = 'vertical',
}
Gui.set_style(frame, { padding = 4, width = 256 })

do -- header
local flow, label, button
flow = frame.add { type = 'flow', direction = 'horizontal' }
flow.drag_target = frame

label = flow.add({ type = 'label', style = 'subheader_caption_label', caption = 'Production' })
label.drag_target = frame
Gui.set_style(label, { left_padding = 0 })

Gui.add_pusher(flow).drag_target = frame

--- Display time
button = flow.add {
type = 'sprite-button',
name = action_scroll_precision_index,
caption = to_time[settings.precision_index],
style = 'frame_button',
}
data.action_scroll_precision_index = button
Gui.set_style(button, { height = 24, width = 48, font_color = { 255, 255, 255 } })
Gui.set_data(button, data)

--- Add new
button = flow.add {
type = 'choose-elem-button',
name = action_add_item,
style = 'frame_button',
tooltip = { 'production_hud.new_item_tooltip' },
elem_type = 'signal',
signal = { type = 'virtual', name = 'shape-cross' },
}
Gui.set_style(button, { size = 24, font_color = { 255, 255, 255 } })
Gui.set_data(button, data)
end

do -- body
local panel = frame.add { type = 'frame', style = 'quick_bar_inner_panel' }
local tbl = panel.add { type = 'table', column_count = 2, draw_horizontal_lines = true }
data.table = tbl
end

Gui.set_data(frame, data)
Public.update_main_frame(player)
frame.force_auto_center()
end

---@param player LuaPlayer
Public.update_main_frame = function(player)
local frame = player.gui.screen[main_frame_name]
if not (frame and frame.valid and frame.visible) then
return
end

local data = Gui.get_data(frame)
local tbl = data.table

local settings = player_settings[player.index]
if #settings.items ~= table_size(tbl.children) then
init_hud(tbl, settings.items)
end

local item_stats = player.force.get_item_production_statistics(player.physical_surface)
local fluid_stats = player.force.get_fluid_production_statistics(player.physical_surface)

for _, name in pairs(settings.items) do
local children = tbl[name]
local stats = ((item_p[name] ~= nil) and item_stats or fluid_stats)
local minus = stats.get_flow_count { name = name, category = 'output', precision_index = settings.precision_index }
local plus = stats.get_flow_count { name = name, category = 'input', precision_index = settings.precision_index }
local count = stats.get_input_count(name) - stats.get_output_count(name)

children.stats.plus.caption = format_si(plus)
children.stats.minus.caption = format_si(minus)
children[action_remove_item].number = count
end
end

Gui.allow_player_to_toggle_top_element_visibility(main_button_name)
Gui.on_click(main_button_name, Public.toggle)

Gui.on_click(action_scroll_precision_index, function(event)
local settings = player_settings[event.player_index]
settings.precision_index = (settings.precision_index + 1) % table_size(defines.flow_precision_index)

local data = Gui.get_data(event.element)
data.action_scroll_precision_index.caption = to_time[settings.precision_index]

Public.update_main_frame(event.player)
end)

Gui.on_elem_changed(action_add_item, function(event)
local player = event.player
local element = event.element
local items = player_settings[player.index].items
local item = element.elem_value and element.elem_value.name
element.elem_value = { name = 'shape-cross', type = 'virtual' }

if not item then
player.print({ 'production_hud.err_invalid_item' }, { sound_path = 'utility/cannot_build' })
return
end

if item_p[item] == nil and fluid_p[item] == nil then
player.print({ 'production_hud.err_invalid_item' }, { sound_path = 'utility/cannot_build' })
return
end

if table.contains(items, item) then
player.print({ 'production_hud.err_item_already_present' }, { sound_path = 'utility/cannot_build' })
return
end

if #items >= config.limit then
player.print({ 'production_hud.err_limit_reached', config.limit }, { sound_path = 'utility/cannot_build' })
return
end

table.insert(items, item)
player.play_sound{ path = 'utility/armor_insert' }
Public.update_main_frame(player)
end)

Gui.on_click(action_remove_item, function(event)
if event.button ~= defines.mouse_button_type.right then
return
end

table.remove_element(player_settings[event.player_index].items, event.element.tags.name)
event.player.play_sound{ path = 'utility/armor_remove' }
Public.update_main_frame(event.player)
end)

-- == EVENTS ==================================================================

Event.add(defines.events.on_player_created, function(event)
local player = game.get_player(event.player_index)
if not (player and player.valid) then
return
end

Gui.add_top_element(player, {
name = main_button_name,
type = 'sprite-button',
sprite = 'utility/side_menu_production_icon',
tooltip = { 'production_hud.feature_tooltip' },
})

player_settings[player.index] = {
precision_index = defines.flow_precision_index.ten_minutes,
items = table.deepcopy(config.starting_items or {}),
}
end)

Event.on_nth_tick(307, function()
for _, p in pairs(game.connected_players) do
Public.update_main_frame(p)
end
end)

Event.on_configuration_changed(function()
for _, p in pairs(game.players) do
Public.sanitize_player_settings(p.index)
end
end)

-- ============================================================================
10 changes: 9 additions & 1 deletion locale/en/redmew_features.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,12 @@ success_destination=[color=blue][Teleporter][/color] You have been teleported to
description=Automatically trade materials with the Market from anywhere. The [font=default-semibold][color=128,205,240]Market[/color][/font] will constantly provide your selected [font=default-semibold][color=255,230,192]Offer[/color][/font] as long as the selected [font=default-semibold][color=255,230,192]Request[/color][/font] is provided to make the trade.\n\nThe exchange fee is computed based on the ratio of the worths of selected items.
requests_tooltip=Select an item that will be removed
offers_tooltip=Select an item that will be provided
item_tooltip=This item is worth __1__ [img=item/coin] __plural_for_parameter__1__{1=coin|rest=coins}__
item_tooltip=This item is worth __1__ [img=item/coin] __plural_for_parameter__1__{1=coin|rest=coins}__

[production_hud]
feature_tooltip=Production HUD
item_tooltip=__1__\n__CONTROL_RIGHT_CLICK__ to remove
new_item_tooltip=__CONTROL_LEFT_CLICK__ to add new item
err_invalid_item=[color=blue][Production HUD][/color] Invalid item selected. Please select another item or fluid to track.
err_item_already_present=[color=blue][Production HUD][/color] Item already tracked. Please select another item or fluid to track.
err_limit_reached=[color=blue][Production HUD][/color] You have reached the limit of __1__ items to track. Please remove some before you add any new ones.
Loading