1774 lines
66 KiB
Lua

require "util"
require "libs/array_pair"
require "libs/ore_tracker"
local mod_gui = require("mod-gui")
local v = require "semver"
local mod_version = "0.10.14"
-- Sanity: site names aren't allowed to be longer than this, to prevent them
-- kicking the buttons off the right edge of the screen
local MAX_SITE_NAME_LENGTH = 50
resmon = {
on_click = {},
endless_resources = {},
filters = {},
-- updated `on_tick` to contain `ore_tracker.get_entity_cache()`
entity_cache = nil,
}
function string.starts_with(haystack, needle)
return string.sub(haystack, 1, string.len(needle)) == needle
end
function string.ends_with(haystack, needle)
return string.sub(haystack, -string.len(needle)) == needle
end
function resmon.init_globals()
for index, _ in pairs(game.players) do
resmon.init_player(index)
end
end
function resmon.on_player_created(event)
resmon.init_player(event.player_index)
end
-- migration v0.8.0: remove remote viewers and put players back into the right entity if available
local function migrate_remove_remote_viewer(player, player_data)
local real_char = player_data.real_character
if not real_char or not real_char.valid then
player.print { "YARM-warn-no-return-possible" }
return
end
player.character = real_char
if player_data.remote_viewer and player_data.remote_viewer.valid then
player_data.remote_viewer.destroy()
end
player_data.real_character = nil
player_data.remote_viewer = nil
player_data.viewing_site = nil
end
local function migrate_remove_minimum_resource_amount(force_data)
for _, site in pairs(force_data.ore_sites) do
if site.minimum_resource_amount then site.minimum_resource_amount = nil end
end
end
function resmon.init_player(player_index)
local player = game.players[player_index]
resmon.init_force(player.force)
-- migration v0.7.402: YARM_root now in mod_gui, destroy the old one
local old_root = player.gui.left.YARM_root
if old_root and old_root.valid then old_root.destroy() end
local root = mod_gui.get_frame_flow(player).YARM_root
if root and root.buttons and (
-- migration v0.8.0: expando now a set of filter buttons, destroy the root and recreate later
root.buttons.YARM_expando
-- migration v0.TBD: add toggle bg button
or not root.buttons.YARM_toggle_bg
or not root.buttons.YARM_toggle_surfacesplit
or not root.buttons.YARM_toggle_lite)
then
root.destroy()
end
if not global.player_data then global.player_data = {} end
local player_data = global.player_data[player_index]
if not player_data then player_data = {} end
if not player_data.gui_update_ticks or player_data.gui_update_ticks == 60 then player_data.gui_update_ticks = 300 end
if not player_data.overlays then player_data.overlays = {} end
if player_data.viewing_site then migrate_remove_remote_viewer(player, player_data) end
global.player_data[player_index] = player_data
end
function resmon.init_force(force)
if not global.force_data then global.force_data = {} end
local force_data = global.force_data[force.name]
if not force_data then force_data = {} end
if not force_data.ore_sites then
force_data.ore_sites = {}
else
resmon.migrate_ore_sites(force_data)
resmon.migrate_ore_entities(force_data)
resmon.sanity_check_sites(force, force_data)
end
migrate_remove_minimum_resource_amount(force_data)
global.force_data[force.name] = force_data
end
local function table_contains(haystack, needle)
for _, candidate in pairs(haystack) do
if candidate == needle then
return true
end
end
return false
end
function resmon.sanity_check_sites(force, force_data)
local discarded_sites = {}
local missing_ores = {}
for name, site in pairs(force_data.ore_sites) do
local entity_prototype = game.entity_prototypes[site.ore_type]
if not entity_prototype or not entity_prototype.valid then
discarded_sites[#discarded_sites + 1] = name
if not table_contains(missing_ores, site.ore_type) then
missing_ores[#missing_ores + 1] = site.ore_type
end
if site.chart_tag and site.chart_tag.valid then
site.chart_tag.destroy()
end
force_data.ore_sites[name] = nil
end
end
if #discarded_sites == 0 then return end
local discard_message = "YARM-warnings.discard-multi-missing-ore-type-multi"
if #missing_ores == 1 then
discard_message = "YARM-warnings.discard-multi-missing-ore-type-single"
if #discarded_sites == 1 then
discard_message = "YARM-warnings.discard-single-missing-ore-type-single"
end
end
force.print { discard_message, table.concat(discarded_sites, ', '), table.concat(missing_ores, ', ') }
log { "", force.name, ' was warned: ', { discard_message, table.concat(discarded_sites, ', '),
table.concat(missing_ores, ', ') } }
end
local function position_to_string(entity)
-- scale it up so (hopefully) any floating point component disappears,
-- then force it to be an integer with %d. not using util.positiontostr
-- as it uses %g and keeps the floating point component.
return string.format("%d,%d", entity.x * 100, entity.y * 100)
end
function resmon.migrate_ore_entities(force_data)
for name, site in pairs(force_data.ore_sites) do
-- v0.7.15: instead of tracking entities, track their positions and
-- re-find the entity when needed.
if site.known_positions then
site.known_positions = nil
end
if site.entities then
site.entity_positions = array_pair.new()
for _, ent in pairs(site.entities) do
if ent.valid then
array_pair.insert(site.entity_positions, ent.position)
end
end
site.entities = nil
end
-- v0.7.107: change to using the site position as a table key, to
-- allow faster searching for already-added entities.
if site.entity_positions then
site.entity_table = {}
site.entity_count = 0
local iter = array_pair.iterator(site.entity_positions)
while iter.has_next() do
pos = iter.next()
local key = position_to_string(pos)
site.entity_table[key] = pos
site.entity_count = site.entity_count + 1
end
site.entity_positions = nil
end
-- v0.8.6: The entities are now tracked by the ore_tracker, and
-- sites need only maintain ore tracker indices.
if site.entity_table then
site.tracker_indices = {}
site.entity_count = 0
for _, pos in pairs(site.entity_table) do
local ent = site.surface.find_entity(site.ore_type, pos)
if ent and ent.valid then
local index = ore_tracker.add_entity(ent)
site.tracker_indices[index] = true
site.entity_count = site.entity_count + 1
end
end
site.entity_table = nil
end
end
end
function resmon.migrate_ore_sites(force_data)
for name, site in pairs(force_data.ore_sites) do
if not site.remaining_permille then
site.remaining_permille = math.floor(site.amount * 1000 / site.initial_amount)
end
if not site.ore_per_minute then site.ore_per_minute = 0 end
if not site.scanned_ore_per_minute then site.scanned_ore_per_minute = 0 end
if not site.lifetime_ore_per_minute then site.lifetime_ore_per_minute = 0 end
if not site.etd_minutes then site.etd_minutes = 1 / 0 end
if not site.scanned_etd_minutes then site.scanned_etd_minutes = -1 end
if not site.lifetime_etd_minutes then site.lifetime_etd_minutes = 1 / 0 end
if not site.etd_is_lifetime then site.etd_is_lifetime = 1 end
if not site.etd_minutes_delta then site.etd_minutes_delta = 0 end
if not site.ore_per_minute_delta then site.ore_per_minute_delta = 0 end
end
end
local function find_resource_at(surface, position)
-- The position we get is centered in its tile (e.g., {8.5, 17.5}).
-- Sometimes, the resource does not cover the center, so search the full tile.
local top_left = { x = position.x - 0.5, y = position.y - 0.5 }
local bottom_right = { x = position.x + 0.5, y = position.y + 0.5 }
local stuff = surface.find_entities_filtered { area = { top_left, bottom_right }, type = 'resource' }
if #stuff < 1 then return nil end
return stuff[1] -- there should never be another resource at the exact same coordinates
end
local function find_center(area)
local xpos = (area.left + area.right) / 2
local ypos = (area.top + area.bottom) / 2
return { x = xpos, y = ypos }
end
local function find_center_tile(area)
local center = find_center(area)
return { x = math.floor(center.x), y = math.floor(center.y) }
end
function resmon.on_player_selected_area(event)
if event.item ~= 'yarm-selector-tool' then return end
local player_data = global.player_data[event.player_index]
local entities = event.entities
if #entities < 1 then
entities = { find_resource_at(event.surface, {
x = 0.5 + math.floor((event.area.left_top.x + event.area.right_bottom.x) / 2),
y = 0.5 + math.floor((event.area.left_top.y + event.area.right_bottom.y) / 2)
}) }
end
if #entities < 1 then
-- if we have an expanding site, submit it. else, just drop the current site
if player_data.current_site and player_data.current_site.is_site_expanding then
resmon.submit_site(event.player_index)
else
resmon.clear_current_site(event.player_index)
end
return
end
local entities_by_type = {}
for _, entity in pairs(entities) do
if entity.prototype.type == 'resource' then
entities_by_type[entity.name] = entities_by_type[entity.name] or {}
table.insert(entities_by_type[entity.name], entity)
end
end
player_data.todo = player_data.todo or {}
for _, group in pairs(entities_by_type) do table.insert(player_data.todo, group) end
-- note: resmon.update_players() (via on_tick) will continue the operation from here
end
function resmon.clear_current_site(player_index)
local player = game.players[player_index]
local player_data = global.player_data[player_index]
player_data.current_site = nil
while #player_data.overlays > 0 do
table.remove(player_data.overlays).destroy()
end
end
function resmon.add_resource(player_index, entity)
if not entity.valid then return end
local player = game.players[player_index]
local player_data = global.player_data[player_index]
if player_data.current_site and player_data.current_site.ore_type ~= entity.name then
if player_data.current_site.finalizing then
resmon.submit_site(player_index)
else
resmon.clear_current_site(player_index)
end
end
if not player_data.current_site then
player_data.current_site = {
added_at = game.tick,
surface = entity.surface,
force = player.force,
ore_type = entity.name,
ore_name = entity.prototype.localised_name,
tracker_indices = {},
entity_count = 0,
initial_amount = 0,
amount = 0,
extents = {
left = entity.position.x,
right = entity.position.x,
top = entity.position.y,
bottom = entity.position.y,
},
next_to_scan = {},
entities_to_be_overlaid = {},
next_to_overlay = {},
etd_minutes = -1,
scanned_etd_minutes = -1,
lifetime_etd_minutes = -1,
ore_per_minute = -1,
scanned_ore_per_minute = -1,
lifetime_ore_per_minute = -1,
etd_is_lifetime = 1,
last_ore_check = nil, -- used for ETD easing; initialized when needed,
last_modified_amount = nil, -- but I wanted to _show_ that they can exist.
etd_minutes_delta = 0,
ore_per_minute_delta = 0,
}
end
if player_data.current_site.is_site_expanding then
player_data.current_site.has_expanded = true -- relevant for the console output
if not player_data.current_site.original_amount then
player_data.current_site.original_amount = player_data.current_site.amount
end
end
resmon.add_single_entity(player_index, entity)
-- note: resmon.scan_current_site() (via on_tick) will continue the operation from here
end
function resmon.add_single_entity(player_index, entity)
local player_data = global.player_data[player_index]
local site = player_data.current_site
local tracker_index = ore_tracker.add_entity(entity)
-- Don't re-add the same entity multiple times
if site.tracker_indices[tracker_index] then return end
-- Reset the finalizing timer
if site.finalizing then site.finalizing = false end
-- Memorize this entity
site.tracker_indices[tracker_index] = true
site.entity_count = site.entity_count + 1
table.insert(site.next_to_scan, entity)
site.amount = site.amount + entity.amount
-- Resize the site bounds if necessary
if entity.position.x < site.extents.left then
site.extents.left = entity.position.x
elseif entity.position.x > site.extents.right then
site.extents.right = entity.position.x
end
if entity.position.y < site.extents.top then
site.extents.top = entity.position.y
elseif entity.position.y > site.extents.bottom then
site.extents.bottom = entity.position.y
end
-- Give visible feedback, too
resmon.put_marker_at(entity.surface, entity.position, player_data)
end
function resmon.put_marker_at(surface, pos, player_data)
if math.floor(pos.x) % settings.global["YARM-overlay-step"].value ~= 0 or
math.floor(pos.y) % settings.global["YARM-overlay-step"].value ~= 0 then
return
end
local overlay = surface.create_entity { name = "rm_overlay",
force = game.forces.neutral,
position = pos }
overlay.minable = false
overlay.destructible = false
overlay.operable = false
table.insert(player_data.overlays, overlay)
end
local function shift_position(position, direction)
if direction == defines.direction.north then
return { x = position.x, y = position.y - 1 }
elseif direction == defines.direction.northeast then
return { x = position.x + 1, y = position.y - 1 }
elseif direction == defines.direction.east then
return { x = position.x + 1, y = position.y }
elseif direction == defines.direction.southeast then
return { x = position.x + 1, y = position.y + 1 }
elseif direction == defines.direction.south then
return { x = position.x, y = position.y + 1 }
elseif direction == defines.direction.southwest then
return { x = position.x - 1, y = position.y + 1 }
elseif direction == defines.direction.west then
return { x = position.x - 1, y = position.y }
elseif direction == defines.direction.northwest then
return { x = position.x - 1, y = position.y - 1 }
else
return position
end
end
function resmon.scan_current_site(player_index)
local site = global.player_data[player_index].current_site
local to_scan = math.min(30, #site.next_to_scan)
local max_dist = settings.global["YARM-grow-limit"].value
for i = 1, to_scan do
local entity = table.remove(site.next_to_scan, 1)
local entity_position = entity.position
local surface = entity.surface
site.first_center = site.first_center or find_center(site.extents)
-- Look in every direction around this entity...
for _, dir in pairs(defines.direction) do
-- ...and if there's a resource, add it
local search_pos = shift_position(entity_position, dir)
if max_dist < 0 or util.distance(search_pos, site.first_center) < max_dist then
local found = find_resource_at(surface, search_pos)
if found and found.name == site.ore_type then
resmon.add_single_entity(player_index, found)
end
end
end
end
end
local function format_number(n) -- credit http://richard.warburton.it
local left, num, right = string.match(n, '^([^%d]*%d)(%d*)(.-)$')
return left .. (num:reverse():gsub('(%d%d%d)', '%1,'):reverse()) .. right
end
local si_prefixes = { '', ' k', ' M', ' G' }
local function format_number_si(n)
for i = 1, #si_prefixes do
if n < 1000 then
return string.format('%d%s', n, si_prefixes[i])
end
n = math.floor(n / 1000)
end
-- 1,234 T resources? I guess we should support it...
return string.format('%s T', format_number(n))
end
local octant_names = {
[0] = "E",
[1] = "SE",
[2] = "S",
[3] = "SW",
[4] = "W",
[5] = "NW",
[6] = "N",
[7] = "NE",
}
local function get_octant_name(vector)
local radians = math.atan2(vector.y, vector.x)
local octant = math.floor(8 * radians / (2 * math.pi) + 8.5) % 8
return octant_names[octant]
end
function resmon.finalize_site(player_index)
local player = game.players[player_index]
local player_data = global.player_data[player_index]
local site = player_data.current_site
site.finalizing = true
site.finalizing_since = game.tick
site.initial_amount = site.amount
site.ore_per_minute = 0
site.remaining_permille = 1000
site.center = find_center_tile(site.extents)
--[[ don't rename a site we've expanded! (if the site name changes it'll create a new site
instead of replacing the existing one) ]]
if not site.is_site_expanding then
site.name = string.format("%s %d", get_octant_name(site.center), util.distance({ x = 0, y = 0 }, site.center))
if settings.global["YARM-site-prefix-with-surface"].value then
site.name = string.format("%s %s", site.surface.name, site.name)
end
end
resmon.count_deposits(site, site.added_at % settings.global["YARM-ticks-between-checks"].value)
end
function resmon.update_chart_tag(site)
local is_chart_tag_enabled = settings.global["YARM-map-markers"].value
if not is_chart_tag_enabled then
if site.chart_tag and site.chart_tag.valid then
-- chart tags were just disabled, so remove them from the world
site.chart_tag.destroy()
site.chart_tag = nil
end
return
end
if not site.chart_tag or not site.chart_tag.valid then
if not site.force or not site.force.valid or not site.surface.valid then return end
local chart_tag = {
position = site.center,
text = site.name,
}
site.chart_tag = site.force.add_chart_tag(site.surface, chart_tag)
if not site.chart_tag then return end -- may fail if chunk is not currently charted accd. to @Bilka
end
local display_value = resmon.generate_display_site_amount(site, nil, 1)
local prototype = game.entity_prototypes[site.ore_type]
site.chart_tag.text =
string.format('%s - %s %s', site.name, display_value, resmon.get_rich_text_for_products(prototype))
return
end
function resmon.generate_display_site_amount(site, player, short)
local format_func = short and format_number_si or format_number
local entity_prototype = game.entity_prototypes[site.ore_type]
if resmon.is_endless_resource(site.ore_type, entity_prototype) then
local normal_site_amount = entity_prototype.normal_resource_amount * site.entity_count
local val = (normal_site_amount == 0 and 0) or (100 * site.amount / normal_site_amount)
return site.entity_count .. " x " .. format_number(string.format("%.1f%%", val))
end
local amount_display = format_func(site.amount)
if not settings.global["YARM-adjust-for-productivity"].value then return amount_display end
local amount_prod_display =
format_func(math.floor(site.amount * (1 + (player or site).force.mining_drill_productivity_bonus)))
if not settings.global["YARM-productivity-show-raw-and-adjusted"].value then
return amount_prod_display
elseif settings.global["YARM-productivity-parentheses-part-is"].value == "adjusted" then
return string.format("%s (%s)", amount_display, amount_prod_display)
else
return string.format("%s (%s)", amount_prod_display, amount_display)
end
end
function resmon.get_rich_text_for_products(proto)
if not proto or not proto.mineable_properties or not proto.mineable_properties.products then
return '' -- only supporting resource entities...
end
local result = ''
for _, product in pairs(proto.mineable_properties.products) do
result = result .. string.format('[%s=%s]', product.type, product.name)
end
return result
end
function resmon.submit_site(player_index)
local player = game.players[player_index]
local player_data = global.player_data[player_index]
local force_data = global.force_data[player.force.name]
local site = player_data.current_site
force_data.ore_sites[site.name] = site
resmon.clear_current_site(player_index)
if (site.is_site_expanding) then
if (site.has_expanded) then
-- reset statistics, the site didn't actually just grow a bunch of ore in existing tiles
site.last_ore_check = nil
site.last_modified_amount = nil
local amount_added = site.amount - site.original_amount
local sign = amount_added < 0 and '' or '+' -- format_number will handle the negative sign for us (if needed)
player.print { "YARM-site-expanded", site.name, format_number(site.amount), site.ore_name,
sign .. format_number(amount_added) }
end
--[[ NB: deliberately not outputting anything in the case where the player cancelled (or
timed out) a site expansion without expanding anything (to avoid console spam) ]]
if site.chart_tag and site.chart_tag.valid then
site.chart_tag.destroy()
end
else
player.print { "YARM-site-submitted", site.name, format_number(site.amount), site.ore_name }
end
resmon.update_chart_tag(site)
-- clear site expanding state so we can re-expand the same site again (and get sensible numbers!)
if (site.is_site_expanding) then
site.is_site_expanding = nil
site.has_expanded = nil
site.original_amount = nil
end
resmon.update_force_members_ui(player)
end
function resmon.is_endless_resource(ent_name, proto)
if resmon.endless_resources[ent_name] ~= nil then
return resmon.endless_resources[ent_name]
end
if not proto then return false end
if proto.infinite_resource then
resmon.endless_resources[ent_name] = true
else
resmon.endless_resources[ent_name] = false
end
return resmon.endless_resources[ent_name]
end
function resmon.count_deposits(site, update_cycle)
if site.iter_fn then
resmon.tick_deposit_count(site)
return
end
local site_update_cycle = site.added_at % settings.global["YARM-ticks-between-checks"].value
if site_update_cycle ~= update_cycle then
return
end
site.iter_fn, site.iter_state, site.iter_key = pairs(site.tracker_indices)
site.update_amount = 0
end
function resmon.tick_deposit_count(site)
local index = site.iter_key
for _ = 1, 1000 do
index = site.iter_fn(site.iter_state, index)
if index == nil then
resmon.finish_deposit_count(site)
return
end
local tracking_data = resmon.entity_cache[index]
if tracking_data and tracking_data.valid then
site.update_amount = site.update_amount + tracking_data.resource_amount
else
site.tracker_indices[index] = nil -- It's permitted to delete from a table being iterated
site.entity_count = site.entity_count - 1
end
end
site.iter_key = index
end
-- as a default case, takes a diff between two values and returns a smoothed
-- easing step. however to force convergence, it does *not* smooth diffs below 1
-- and clamps smoothed diffs below 10 to be at least 1.
function resmon.smooth_clamp_diff(diff)
if math.abs(diff) < 1 then
return diff
elseif math.abs(diff) < 10 then
return math.abs(diff) / diff
end
return 0.1 * diff
end
function resmon.finish_deposit_count(site)
site.iter_key = nil
site.iter_fn = nil
site.iter_state = nil
if site.last_ore_check then
if not site.last_modified_amount then -- make sure those two values have a default
site.last_modified_amount = site.amount --
site.last_modified_tick = site.last_ore_check --
end
local delta_ore_since_last_update = site.last_modified_amount - site.amount
if delta_ore_since_last_update ~= 0 then -- only store the amount and tick from last update if it actually changed
site.last_modified_tick = site.last_ore_check --
site.last_modified_amount = site.amount --
end
local delta_ore_since_last_change = (site.update_amount - site.last_modified_amount) -- use final amount and tick to calculate
local delta_ticks = game.tick - site.last_modified_tick --
local new_ore_per_minute = (delta_ore_since_last_change * 3600 / delta_ticks) -- ease the per minute value over time
local diff_step = resmon.smooth_clamp_diff(new_ore_per_minute - site.scanned_ore_per_minute) --
site.scanned_ore_per_minute = site.scanned_ore_per_minute + diff_step --
end
local entity_prototype = game.entity_prototypes[site.ore_type]
local is_endless = resmon.is_endless_resource(site.ore_type, entity_prototype)
local minimum = is_endless and (site.entity_count * entity_prototype.minimum_resource_amount) or 0
local amount_left = site.amount - minimum
site.scanned_etd_minutes =
(site.scanned_ore_per_minute ~= 0 and amount_left / (-site.scanned_ore_per_minute))
or (amount_left == 0 and 0)
or -1
site.amount = site.update_amount
amount_left = site.amount - minimum
site.amount_left = amount_left
if settings.global["YARM-adjust-over-percentage-sites"].value then
site.initial_amount = math.max(site.initial_amount, site.amount)
end
site.last_ore_check = game.tick
site.remaining_permille = resmon.calc_remaining_permille(site)
local age_minutes = (game.tick - site.added_at) / 3600
local depleted = site.initial_amount - site.amount
site.lifetime_ore_per_minute = -depleted / age_minutes
site.lifetime_etd_minutes =
(site.lifetime_ore_per_minute ~= 0 and amount_left / (-site.lifetime_ore_per_minute))
or (amount_left == 0 and 0)
or -1
local old_etd_minutes = site.etd_minutes
local old_ore_per_minute = site.ore_per_minute
if site.scanned_etd_minutes == -1 or site.lifetime_etd_minutes <= site.scanned_etd_minutes then
site.ore_per_minute = site.lifetime_ore_per_minute
site.etd_minutes = site.lifetime_etd_minutes
site.etd_is_lifetime = 1
else
site.ore_per_minute = site.scanned_ore_per_minute
site.etd_minutes = site.scanned_etd_minutes
site.etd_is_lifetime = 0
end
site.etd_minutes_delta = site.etd_minutes - old_etd_minutes
site.ore_per_minute_delta = site.ore_per_minute - old_ore_per_minute
-- these are just to prevent errant NaNs
site.etd_minutes_delta = (site.etd_minutes_delta ~= site.etd_minutes_delta) and 0 or site.etd_minutes_delta
site.ore_per_minute_delta =
(site.ore_per_minute_delta ~= site.ore_per_minute_delta) and 0 or site.ore_per_minute_delta
resmon.update_chart_tag(site)
script.raise_event(on_site_updated, {
force_name = site.force.name,
site_name = site.name,
amount = site.amount,
ore_per_minute = site.ore_per_minute,
remaining_permille = site.remaining_permille,
ore_type = site.ore_type,
etd_minutes = site.etd_minutes,
})
end
function resmon.calc_remaining_permille(site)
local entity_prototype = game.entity_prototypes[site.ore_type]
local minimum = resmon.is_endless_resource(site.ore_type, entity_prototype)
and (site.entity_count * entity_prototype.minimum_resource_amount) or 0
local amount_left = site.amount - minimum
local initial_amount_available = site.initial_amount - minimum
return initial_amount_available <= 0 and 0 or math.floor(amount_left * 1000 / initial_amount_available)
end
local function site_comparator_default(left, right)
if left.remaining_permille ~= right.remaining_permille then
return left.remaining_permille < right.remaining_permille
elseif left.added_at ~= right.added_at then
return left.added_at < right.added_at
else
return left.name < right.name
end
end
local function site_comparator_by_ore_type(left, right)
if left.ore_type ~= right.ore_type then
return left.ore_type < right.ore_type
else
return site_comparator_default(left, right)
end
end
local function site_comparator_by_ore_count(left, right)
if left.amount ~= right.amount then
return left.amount < right.amount
else
return site_comparator_default(left, right)
end
end
local function site_comparator_by_etd(left, right)
-- infinite time to depletion is indicated when etd_minutes == -1
-- we want sites with infinite depletion time at the end of the list
if left.etd_minutes ~= right.etd_minutes then
if left.etd_minutes >= 0 and right.etd_minutes >= 0 then
-- these are both real etd estimates so sort normally
return left.etd_minutes < right.etd_minutes
else
-- left and right are not equal AND one of them is -1
-- (they are not both -1 because then they'd be equal)
-- and we want -1 to be at the end of the list
-- so reverse the sort order in this case
return left.etd_minutes > right.etd_minutes
end
else
return site_comparator_default(left, right)
end
end
local function site_comparator_by_alpha(left, right)
return left.name < right.name
end
local function sites_in_order(sites, comparator)
-- damn in-place table.sort makes us make a copy first...
local ordered_sites = {}
for _, site in pairs(sites) do
table.insert(ordered_sites, site)
end
table.sort(ordered_sites, comparator)
local i = 0
local n = #ordered_sites
return function()
i = i + 1
if i <= n then return ordered_sites[i] end
end
end
local function sites_in_player_order(sites, player)
local order_by = player.mod_settings["YARM-order-by"].value
local comparator =
(order_by == 'ore-type' and site_comparator_by_ore_type)
or (order_by == 'ore-count' and site_comparator_by_ore_count)
or (order_by == 'etd' and site_comparator_by_etd)
or (order_by == 'alphabetical' and site_comparator_by_alpha)
or site_comparator_default
return sites_in_order(sites, comparator)
end
-- NB: filter names should be single words with optional underscores (_)
-- They will be used for naming GUI elements
local FILTER_NONE = "none"
local FILTER_WARNINGS = "warnings"
local FILTER_ALL = "all"
resmon.filters[FILTER_NONE] = function() return false end
resmon.filters[FILTER_ALL] = function() return true end
resmon.filters[FILTER_WARNINGS] = function(site, player)
local remaining = site.etd_minutes
local threshold_hours = site.is_summary and "timeleft_totals" or "timeleft"
return remaining ~= -1 and remaining <= player.mod_settings["YARM-warn-" .. threshold_hours].value * 60
end
function resmon.update_ui(player)
local player_data = global.player_data[player.index]
local force_data = global.force_data[player.force.name]
local show_sites_summary = player.mod_settings["YARM-show-sites-summary"].value
local frame_flow = mod_gui.get_frame_flow(player)
local root = frame_flow.YARM_root
if not root then
root = frame_flow.add { type = "frame",
name = "YARM_root",
direction = "horizontal",
style = "YARM_outer_frame_no_border" }
local buttons = root.add { type = "flow",
name = "buttons",
direction = "vertical",
style = "YARM_buttons_v" }
buttons.add { type = "button", name = "YARM_filter_" .. FILTER_NONE, style = "YARM_filter_none",
tooltip = { "YARM-tooltips.filter-none" } }
buttons.add { type = "button", name = "YARM_filter_" .. FILTER_WARNINGS, style = "YARM_filter_warnings",
tooltip = { "YARM-tooltips.filter-warnings" } }
buttons.add { type = "button", name = "YARM_filter_" .. FILTER_ALL, style = "YARM_filter_all",
tooltip = { "YARM-tooltips.filter-all" } }
buttons.add { type = "button", name = "YARM_toggle_bg", style = "YARM_toggle_bg",
tooltip = { "YARM-tooltips.toggle-bg" } }
buttons.add { type = "button", name = "YARM_toggle_surfacesplit", style = "YARM_toggle_surfacesplit",
tooltip = { "YARM-tooltips.toggle-surfacesplit" } }
buttons.add { type = "button", name = "YARM_toggle_lite", style = "YARM_toggle_lite",
tooltip = { "YARM-tooltips.toggle-lite" } }
if not player_data.active_filter then player_data.active_filter = FILTER_WARNINGS end
resmon.update_ui_filter_buttons(player, player_data.active_filter)
end
if root.sites and root.sites.valid then
root.sites.destroy()
end
if not force_data or not force_data.ore_sites then return end
local is_full = root.buttons.YARM_toggle_lite.style.name ~= "YARM_toggle_lite_on"
local column_count = is_full and 12 or 5
local sites_gui = root.add { type = "table", column_count = column_count, name = "sites", style = "YARM_site_table" }
sites_gui.style.horizontal_spacing = 5
local column_alignments = sites_gui.style.column_alignments
if is_full then
column_alignments[1] = 'left' -- rename button
column_alignments[2] = 'left' -- surface name
column_alignments[3] = 'left' -- site name
column_alignments[4] = 'right' -- remaining percent
column_alignments[5] = 'right' -- site amount
column_alignments[6] = 'left' -- ore name
column_alignments[7] = 'right' -- ore per minute
column_alignments[8] = 'left' -- ETD
column_alignments[9] = 'right' -- ETD
column_alignments[10] = 'left' -- ETD
column_alignments[11] = 'center' -- ETD
column_alignments[12] = 'left' -- buttons
else
column_alignments[1] = 'left' -- surface name
column_alignments[2] = 'left' -- site name
column_alignments[3] = 'left' -- ore name
column_alignments[4] = 'right' -- ETD
column_alignments[5] = 'left' -- buttons
end
local site_filter = resmon.filters[player_data.active_filter] or resmon.filters[FILTER_NONE]
local surface_filters = { false }
if root.buttons.YARM_toggle_surfacesplit.style.name == "YARM_toggle_surfacesplit_on" then
surface_filters = resmon.surface_filters()
end
local surface_num = 0
local rendered_last = false
for _, surface_filter in pairs(surface_filters) do
local sites = resmon.get_sites_on_surface(force_data, player, surface_filter)
if next(sites) then
local will_render_sites
local will_render_totals
local summary = show_sites_summary and resmon.generate_summaries(player, sites) or {}
for summary_site in sites_in_player_order(summary, player) do
if site_filter(summary_site, player) then will_render_totals = true end
end
for _, site in pairs(sites) do
if site_filter(site, player) then will_render_sites = true end
end
surface_num = surface_num + 1
if surface_num > 1 and rendered_last and (will_render_totals or will_render_sites) then
for _ = 1, column_count do sites_gui.add { type = "line" } end
for _ = 1, column_count do sites_gui.add { type = "line" } end
for _ = 1, column_count do sites_gui.add { type = "line" } end
end
rendered_last = rendered_last or will_render_totals or will_render_sites
local row = 1
for summary_site in sites_in_player_order(summary, player) do
if resmon.print_single_site(site_filter, summary_site, player, sites_gui, player_data, row, is_full) then
row = row + 1
end
end
if will_render_totals and will_render_sites then
if is_full then
sites_gui.add { type = "label" }.style.maximal_height = 5
end
sites_gui.add { type = "label" }.style.maximal_height = 5
sites_gui.add { type = "label", caption = { "YARM-category-sites" } }
local start = is_full and 4 or 3
for _ = start, column_count do sites_gui.add { type = "label" }.style.maximal_height = 5 end
end
row = 1
for _, site in pairs(sites) do
if resmon.print_single_site(site_filter, site, player, sites_gui, player_data, row, is_full) then
row = row + 1
end
end
end
end
end
function resmon.get_sites_on_surface(force_data, player, surface_filter)
local filtered_sites = {}
for site in sites_in_player_order(force_data.ore_sites, player) do
if resmon.site_is_on_surface(site, surface_filter) then
table.insert(filtered_sites, site)
end
end
return filtered_sites
end
function resmon.surface_filters()
local surface_filters = {}
for k in pairs(game.surfaces) do table.insert(surface_filters, k) end
return surface_filters
end
function resmon.site_is_on_surface(site, surface_filter)
return not surface_filter or site.surface.name == surface_filter
end
function resmon.generate_summaries(player, sites)
local summary = {}
for _, site in pairs(sites) do
local entity_prototype = game.entity_prototypes[site.ore_type]
local is_endless = resmon.is_endless_resource(site.ore_type, entity_prototype) and 1 or nil
local root = mod_gui.get_frame_flow(player).YARM_root
local summary_id = site.ore_type ..
(root.buttons.YARM_toggle_surfacesplit.style.name == "YARM_toggle_surfacesplit_on" and site.surface.name or "")
if not summary[summary_id] then
summary[summary_id] = {
name = "Total " .. summary_id,
ore_type = site.ore_type,
ore_name = site.ore_name,
initial_amount = 0,
amount = 0,
ore_per_minute = 0,
etd_minutes = 0,
is_summary = 1,
entity_count = 0,
remaining_permille = (is_endless and 0 or 1000),
site_count = 0,
etd_minutes_delta = 0,
ore_per_minute_delta = 0,
surface = site.surface,
}
end
local summary_site = summary[summary_id]
summary_site.site_count = summary_site.site_count + 1
summary_site.initial_amount = summary_site.initial_amount + site.initial_amount
summary_site.amount = summary_site.amount + site.amount
summary_site.ore_per_minute = summary_site.ore_per_minute + site.ore_per_minute
summary_site.entity_count = summary_site.entity_count + site.entity_count
summary_site.remaining_permille = resmon.calc_remaining_permille(summary_site)
local minimum = is_endless and (summary_site.entity_count * entity_prototype.minimum_resource_amount) or 0
local amount_left = summary_site.amount - minimum
summary_site.etd_minutes =
(summary_site.ore_per_minute ~= 0 and amount_left / (-summary_site.ore_per_minute))
or (amount_left == 0 and 0)
or -1
summary_site.etd_minutes_delta = summary_site.etd_minutes_delta + (site.etd_minutes_delta or 0)
summary_site.ore_per_minute_delta = summary_site.ore_per_minute_delta + (site.ore_per_minute_delta or 0)
end
return summary
end
function resmon.on_click.set_filter(event)
local new_filter = string.sub(event.element.name, 1 + string.len("YARM_filter_"))
local player = game.players[event.player_index]
local player_data = global.player_data[event.player_index]
player_data.active_filter = new_filter
resmon.update_ui_filter_buttons(player, new_filter)
resmon.update_ui(player)
end
function resmon.update_ui_filter_buttons(player, active_filter)
-- rarely, it might be possible to arrive here before the YARM GUI gets created
local root = mod_gui.get_frame_flow(player).YARM_root
-- in that case, leave it for a later update_ui call.
if not root or not root.valid then return end
local buttons_container = root.buttons
for filter_name, _ in pairs(resmon.filters) do
local is_active_filter = filter_name == active_filter
local button = buttons_container["YARM_filter_" .. filter_name]
if button and button.valid then
local style_name = button.style.name
local is_active_style = style_name:ends_with("_on")
if is_active_style and not is_active_filter then
button.style = string.sub(style_name, 1, string.len(style_name) - 3)
elseif is_active_filter and not is_active_style then
button.style = style_name .. "_on"
end
end
end
end
function resmon.print_single_site(site_filter, site, player, sites_gui, player_data, row, is_full)
if not site_filter(site, player) then return end
-- TODO: This shouldn't be part of printing the site! It cancels the deletion
-- process after 2 seconds pass.
if site.deleting_since and site.deleting_since + 120 < game.tick then
site.deleting_since = nil
end
local color = resmon.site_color(site, player)
local el = nil
local root = mod_gui.get_frame_flow(player).YARM_root
if not site.is_summary then
if is_full then
if player_data.renaming_site == site.name then
sites_gui.add { type = "button",
name = "YARM_rename_site_" .. site.name,
tooltip = { "YARM-tooltips.rename-site-cancel" },
style = "YARM_rename_site_cancel" }
else
sites_gui.add { type = "button",
name = "YARM_rename_site_" .. site.name,
tooltip = { "YARM-tooltips.rename-site-named", site.name },
style = "YARM_rename_site" }
end
end
local surf_name = root.buttons.YARM_toggle_surfacesplit.style.name == "YARM_toggle_surfacesplit_on"
and site.surface.name or ""
el = sites_gui.add { type = "label", name = "YARM_label_surface_" .. site.name, caption = surf_name }
el.style.font_color = color
el = sites_gui.add { type = "label", name = "YARM_label_site_" .. site.name, caption = site.name }
el.style.font_color = color
else
if is_full then
sites_gui.add { type = "label" }
end
local surface = (root.buttons.YARM_toggle_surfacesplit.style.name == "YARM_toggle_surfacesplit_on" and row == 1)
and site.surface.name or ""
sites_gui.add { type = "label", caption = surface }
local totals = row == 1 and { "YARM-category-totals" } or ""
sites_gui.add { type = "label", caption = totals }
end
if is_full then
el = sites_gui.add { type = "label", name = "YARM_label_percent_" .. site.name,
caption = string.format("%.1f%%", site.remaining_permille / 10) }
el.style.font_color = color
local display_amount = resmon.generate_display_site_amount(site, player, nil)
el = sites_gui.add { type = "label", name = "YARM_label_amount_" .. site.name,
caption = display_amount }
el.style.font_color = color
end
local entity_prototype = game.entity_prototypes[site.ore_type]
el = sites_gui.add { type = "label", name = "YARM_label_ore_name_" .. site.name,
caption = is_full and { "", resmon.get_rich_text_for_products(entity_prototype), " ", site.ore_name }
or resmon.get_rich_text_for_products(entity_prototype) }
el.style.font_color = color
if is_full then
el = sites_gui.add { type = "label", name = "YARM_label_ore_per_minute_" .. site.name,
caption = resmon.render_speed(site, player) }
el.style.font_color = color
resmon.render_arrow_for_percent_delta(sites_gui, -1 * site.ore_per_minute_delta, site.ore_per_minute)
end
el = sites_gui.add { type = "label", name = "YARM_label_etd_" .. site.name,
caption = resmon.time_to_deplete(site) }
el.style.font_color = color
if is_full then
resmon.render_arrow_for_percent_delta(sites_gui, site.etd_minutes_delta, site.etd_minutes)
if not site.is_summary then
local etd_icon = site.etd_is_lifetime == 1 and "[img=quantity-time]" or "[img=utility/played_green]"
el = sites_gui.add { type = "label", name = "YARM_label_etd_header_" .. site.name,
caption = { "YARM-time-to-deplete", etd_icon } }
el.style.font_color = color
else
sites_gui.add { type = "label", caption = "" }
end
end
local site_buttons = sites_gui.add { type = "flow", name = "YARM_site_buttons_" .. site.name,
direction = "horizontal", style = "YARM_buttons_h" }
if not site.is_summary then
site_buttons.add { type = "button",
name = "YARM_goto_site_" .. site.name,
tooltip = { "YARM-tooltips.goto-site" },
style = "YARM_goto_site" }
if is_full then
if site.deleting_since then
site_buttons.add { type = "button",
name = "YARM_delete_site_" .. site.name,
tooltip = { "YARM-tooltips.delete-site-confirm" },
style = "YARM_delete_site_confirm" }
else
site_buttons.add { type = "button",
name = "YARM_delete_site_" .. site.name,
tooltip = { "YARM-tooltips.delete-site" },
style = "YARM_delete_site" }
end
if site.is_site_expanding then
site_buttons.add { type = "button",
name = "YARM_expand_site_" .. site.name,
tooltip = { "YARM-tooltips.expand-site-cancel" },
style = "YARM_expand_site_cancel" }
else
site_buttons.add { type = "button",
name = "YARM_expand_site_" .. site.name,
tooltip = { "YARM-tooltips.expand-site" },
style = "YARM_expand_site" }
end
end
end
return true
end
function resmon.render_arrow_for_percent_delta(sites_gui, delta, amount)
local percent_delta = (100 * (delta or 0) / (amount or 0)) / 5
local hue = percent_delta >= 0 and (1 / 3) or 0
local saturation = math.min(math.abs(percent_delta), 1)
local value = math.min(0.5 + math.abs(percent_delta / 2), 1)
sites_gui.add({ type = "label", caption = (amount == 0 and "") or (delta or 0) >= 0 and "" or "" }).style.font_color =
resmon.hsv2rgb(hue, saturation, value)
end
function resmon.time_to_deplete(site)
local ups_adjust = settings.global["YARM-nominal-ups"].value / 60
local minutes = (site.etd_minutes and (site.etd_minutes / ups_adjust)) or -1
if minutes == -1 or minutes == math.huge then return { "YARM-etd-never" } end
local hours = math.floor(minutes / 60)
local days = math.floor(hours / 24)
hours = hours % 24
minutes = minutes % 60
local time_frag = { "YARM-etd-hour-fragment",
{ "", string.format("%02d", hours), ":", string.format("%02d", math.floor(minutes)) } }
if days > 0 then
return { "", { "YARM-etd-day-fragment", days }, " ", time_frag }
elseif minutes > 0 then
return time_frag
elseif site.amount_left == 0 then
return { "YARM-etd-now" }
else
return { "YARM-etd-under-1m" }
end
end
function resmon.render_speed(site, player)
local ups_adjust = settings.global["YARM-nominal-ups"].value / 60
local speed = ups_adjust * site.ore_per_minute
local entity_prototype = game.entity_prototypes[site.ore_type]
if resmon.is_endless_resource(site.ore_type, entity_prototype) then
local normal_site_amount = entity_prototype.normal_resource_amount * site.entity_count
local speed_display = (normal_site_amount == 0 and 0) or (100 * speed) / normal_site_amount
return resmon.speed_to_human("%.3f%%", speed_display, -0.001)
end
local speed_display = resmon.speed_to_human("%.1f", speed, -0.1)
if not settings.global["YARM-adjust-for-productivity"].value then
return speed_display
end
local speed_prod = speed * (1 + (player or site).force.mining_drill_productivity_bonus)
local speed_prod_display = resmon.speed_to_human("%.1f", speed_prod, -0.1)
if not settings.global["YARM-productivity-show-raw-and-adjusted"].value then
return speed_prod_display
elseif settings.global["YARM-productivity-parentheses-part-is"].value == "adjusted" then
return { "", speed_display, " (", speed_prod_display, ")" }
else
return { "", speed_prod_display, " (", speed_display, ")" }
end
end
function resmon.speed_to_human(format, speed, limit)
local speed_display =
speed < limit and { "YARM-ore-per-minute", format_number(string.format(format, speed)) } or
speed < 0 and { "YARM-ore-per-minute", { "", "<", string.format(format, -0.1) } } or ""
return speed_display
end
function resmon.site_color(site, player)
local threshold_type = site.is_summary and "timeleft_totals" or "timeleft"
local threshold = player.mod_settings["YARM-warn-" .. threshold_type].value * 60
local minutes = site.etd_minutes
if minutes == -1 then minutes = threshold end
local factor = (threshold == 0 and 1) or (minutes / threshold)
if factor > 1 then factor = 1 end
local hue = factor / 3
return resmon.hsv2rgb(hue, 1, 1)
end
function resmon.hsv2rgb(h, s, v)
local r, g, b
local i = math.floor(h * 6);
local f = h * 6 - i;
local p = v * (1 - s);
local q = v * (1 - f * s);
local t = v * (1 - (1 - f) * s);
i = i % 6
if i == 0 then
r, g, b = v, t, p
elseif i == 1 then
r, g, b = q, v, p
elseif i == 2 then
r, g, b = p, v, t
elseif i == 3 then
r, g, b = p, q, v
elseif i == 4 then
r, g, b = t, p, v
elseif i == 5 then
r, g, b = v, p, q
end
return { r = r, g = g, b = b }
end
function resmon.on_click.YARM_rename_confirm(event)
local player = game.players[event.player_index]
local player_data = global.player_data[event.player_index]
local force_data = global.force_data[player.force.name]
local old_name = player_data.renaming_site
local new_name = player.gui.center.YARM_site_rename.new_name.text
if string.len(new_name) > MAX_SITE_NAME_LENGTH then
player.print { 'YARM-err-site-name-too-long', MAX_SITE_NAME_LENGTH }
return
end
local site = force_data.ore_sites[old_name]
force_data.ore_sites[old_name] = nil
force_data.ore_sites[new_name] = site
site.name = new_name
resmon.update_chart_tag(site)
player_data.renaming_site = nil
player.gui.center.YARM_site_rename.destroy()
resmon.update_force_members_ui(player)
end
function resmon.on_click.YARM_rename_cancel(event)
local player = game.players[event.player_index]
local player_data = global.player_data[event.player_index]
player_data.renaming_site = nil
player.gui.center.YARM_site_rename.destroy()
resmon.update_force_members_ui(player)
end
function resmon.on_click.rename_site(event)
local site_name = string.sub(event.element.name, 1 + string.len("YARM_rename_site_"))
local player = game.players[event.player_index]
local player_data = global.player_data[event.player_index]
if player.gui.center.YARM_site_rename then
resmon.on_click.YARM_rename_cancel(event)
return
end
player_data.renaming_site = site_name
local root = player.gui.center.add { type = "frame",
name = "YARM_site_rename",
caption = { "YARM-site-rename-title", site_name },
direction = "horizontal" }
root.add { type = "textfield", name = "new_name" }.text = site_name
root.add { type = "button", name = "YARM_rename_confirm", caption = { "YARM-site-rename-confirm" } }
root.add { type = "button", name = "YARM_rename_cancel", caption = { "YARM-site-rename-cancel" } }
player.opened = root
resmon.update_force_members_ui(player)
end
function resmon.on_gui_closed(event)
if event.gui_type ~= defines.gui_type.custom then return end
if not event.element or not event.element.valid then return end
if event.element.name ~= "YARM_site_rename" then return end
resmon.on_click.YARM_rename_cancel(event)
end
function resmon.on_click.remove_site(event)
local site_name = string.sub(event.element.name, 1 + string.len("YARM_delete_site_"))
local player = game.players[event.player_index]
local force_data = global.force_data[player.force.name]
local site = force_data.ore_sites[site_name]
if site.deleting_since then
force_data.ore_sites[site_name] = nil
if site.chart_tag and site.chart_tag.valid then
site.chart_tag.destroy()
end
else
site.deleting_since = event.tick
end
resmon.update_force_members_ui(player)
end
function resmon.on_click.goto_site(event)
local site_name = string.sub(event.element.name, 1 + string.len("YARM_goto_site_"))
local player = game.players[event.player_index]
local force_data = global.force_data[player.force.name]
local site = force_data.ore_sites[site_name]
if game.active_mods["space-exploration"] ~= nil then
local zone = remote.call("space-exploration", "get_zone_from_surface_index",
{ surface_index = site.surface.index })
if not zone then
-- the zone is not available for some reason.
player.print { "YARM-spaceexploration-zone-unavailable" }
log("YARM: Unavailable to view SE zone at " .. serpent.line(site.center) .. " on surface " .. site.surface)
return
end -- TODO: need to show some error logs for this
remote.call("space-exploration", "remote_view_start",
{
player = player,
zone_name = zone.name,
position = site.center,
location_name = site.name,
freeze_history = true
})
else
player.open_map(site.center)
end
resmon.update_force_members_ui(player)
end
-- one button handler for both the expand_site and expand_site_cancel buttons
function resmon.on_click.expand_site(event)
local site_name = string.sub(event.element.name, 1 + string.len("YARM_expand_site_"))
local player = game.players[event.player_index]
local player_data = global.player_data[event.player_index]
local force_data = global.force_data[player.force.name]
local site = force_data.ore_sites[site_name]
local are_we_cancelling_expand = site.is_site_expanding
--[[ we want to submit the site if we're cancelling the expansion (mostly because submitting the
site cleans up the expansion-related variables on the site) or if we were adding a new site
and decide to expand an existing one
--]]
if are_we_cancelling_expand or player_data.current_site then
resmon.submit_site(event.player_index)
end
--[[ this is to handle cancelling an expansion (by clicking the red button) - submitting the site is
all we need to do in this case ]]
if are_we_cancelling_expand then
resmon.update_force_members_ui(player)
return
end
resmon.pull_YARM_item_to_cursor_if_possible(event.player_index)
if player.cursor_stack.valid_for_read and player.cursor_stack.name == "yarm-selector-tool" then
site.is_site_expanding = true
player_data.current_site = site
resmon.update_force_members_ui(player)
resmon.start_recreate_overlay_existing_site(event.player_index)
end
end
function resmon.on_click.toggle_bg(event)
local player = game.players[event.player_index]
local root = mod_gui.get_frame_flow(player).YARM_root
if not root then return end
root.style = (root.style.name == "YARM_outer_frame_no_border_bg")
and "YARM_outer_frame_no_border" or "YARM_outer_frame_no_border_bg"
local button = root.buttons.YARM_toggle_bg
button.style = button.style.name == "YARM_toggle_bg" and "YARM_toggle_bg_on" or "YARM_toggle_bg"
resmon.update_ui(player)
end
function resmon.on_click.toggle_surfacesplit(event)
local player = game.players[event.player_index]
local root = mod_gui.get_frame_flow(player).YARM_root
if not root then return end
local button = root.buttons.YARM_toggle_surfacesplit
button.style =
button.style.name == "YARM_toggle_surfacesplit" and "YARM_toggle_surfacesplit_on" or "YARM_toggle_surfacesplit"
resmon.update_ui(player)
end
function resmon.on_click.toggle_lite(event)
local player = game.players[event.player_index]
local root = mod_gui.get_frame_flow(player).YARM_root
if not root then return end
local button = root.buttons.YARM_toggle_lite
button.style =
button.style.name == "YARM_toggle_lite" and "YARM_toggle_lite_on" or "YARM_toggle_lite"
resmon.update_ui(player)
end
function resmon.pull_YARM_item_to_cursor_if_possible(player_index)
local player = game.players[player_index]
if player.cursor_stack.valid_for_read then -- already have something?
if player.cursor_stack.name == "yarm-selector-tool" then return end
player.clear_cursor() -- and it's not a selector tool, so Q it away
end
player.cursor_stack.set_stack { name = "yarm-selector-tool" }
end
function resmon.on_get_selection_tool(event)
resmon.pull_YARM_item_to_cursor_if_possible(event.player_index)
end
function resmon.start_recreate_overlay_existing_site(player_index)
local site = global.player_data[player_index].current_site
site.is_overlay_being_created = true
-- forcible cleanup in case we got interrupted during a previous background overlay attempt
site.entities_to_be_overlaid = {}
site.entities_to_be_overlaid_count = 0
site.next_to_overlay = {}
site.next_to_overlay_count = 0
for index in pairs(site.tracker_indices) do
local tracking_data = resmon.entity_cache[index]
if tracking_data then
local ent = tracking_data.entity
if ent and ent.valid then
local key = position_to_string(ent.position)
site.entities_to_be_overlaid[key] = ent.position
site.entities_to_be_overlaid_count = site.entities_to_be_overlaid_count + 1
end
end
end
end
function resmon.process_overlay_for_existing_site(player_index)
local player_data = global.player_data[player_index]
local site = player_data.current_site
if site.next_to_overlay_count == 0 then
if site.entities_to_be_overlaid_count == 0 then
resmon.end_overlay_creation_for_existing_site(player_index)
return
else
local ent_key, ent_pos = next(site.entities_to_be_overlaid)
site.next_to_overlay[ent_key] = ent_pos
site.next_to_overlay_count = site.next_to_overlay_count + 1
end
end
local to_scan = math.min(30, site.next_to_overlay_count)
for i = 1, to_scan do
local ent_key, ent_pos = next(site.next_to_overlay)
local entity = site.surface.find_entity(site.ore_type, ent_pos)
local entity_position = entity.position
local surface = entity.surface
local key = position_to_string(entity_position)
-- put marker down
resmon.put_marker_at(surface, entity_position, player_data)
-- remove it from our to-do lists
site.entities_to_be_overlaid[key] = nil
site.entities_to_be_overlaid_count = site.entities_to_be_overlaid_count - 1
site.next_to_overlay[key] = nil
site.next_to_overlay_count = site.next_to_overlay_count - 1
-- Look in every direction around this entity...
for _, dir in pairs(defines.direction) do
-- ...and if there's a resource that's not already overlaid, add it
local found = find_resource_at(surface, shift_position(entity_position, dir))
if found and found.name == site.ore_type then
local offsetkey = position_to_string(found.position)
if site.entities_to_be_overlaid[offsetkey] ~= nil and site.next_to_overlay[offsetkey] == nil then
site.next_to_overlay[offsetkey] = found.position
site.next_to_overlay_count = site.next_to_overlay_count + 1
end
end
end
end
end
function resmon.end_overlay_creation_for_existing_site(player_index)
local site = global.player_data[player_index].current_site
site.is_overlay_being_created = false
site.finalizing = true
site.finalizing_since = game.tick
end
function resmon.update_force_members_ui(player)
for _, p in pairs(player.force.players) do
resmon.update_ui(p)
end
end
function resmon.on_gui_click(event)
if resmon.on_click[event.element.name] then
resmon.on_click[event.element.name](event)
elseif string.starts_with(event.element.name, "YARM_filter_") then
resmon.on_click.set_filter(event)
elseif string.starts_with(event.element.name, "YARM_delete_site_") then
resmon.on_click.remove_site(event)
elseif string.starts_with(event.element.name, "YARM_rename_site_") then
resmon.on_click.rename_site(event)
elseif string.starts_with(event.element.name, "YARM_goto_site_") then
resmon.on_click.goto_site(event)
elseif string.starts_with(event.element.name, "YARM_expand_site_") then
resmon.on_click.expand_site(event)
elseif string.starts_with(event.element.name, "YARM_toggle_bg") then
resmon.on_click.toggle_bg(event)
elseif string.starts_with(event.element.name, "YARM_toggle_surfacesplit") then
resmon.on_click.toggle_surfacesplit(event)
elseif string.starts_with(event.element.name, "YARM_toggle_lite") then
resmon.on_click.toggle_lite(event)
end
end
function resmon.update_players(event)
-- At tick 0 on an MP server initial join, on_init may not have run
if not global.player_data then return end
for index, player in pairs(game.players) do
local player_data = global.player_data[index]
if not player_data then
resmon.init_player(index)
elseif not player.connected and player_data.current_site then
resmon.clear_current_site(index)
end
if player_data.current_site then
local site = player_data.current_site
if #site.next_to_scan > 0 then
resmon.scan_current_site(index)
elseif not site.finalizing then
resmon.finalize_site(index)
elseif site.finalizing_since + 120 == event.tick then
resmon.submit_site(index)
end
if site.is_overlay_being_created then
resmon.process_overlay_for_existing_site(index)
end
else
local todo = player_data.todo or {}
if #todo > 0 then
for _, entity in pairs(table.remove(todo)) do
resmon.add_resource(index, entity)
end
end
end
if event.tick % player_data.gui_update_ticks == 15 + index then
resmon.update_ui(player)
end
end
end
function resmon.update_forces(event)
-- At tick 0 on an MP server initial join, on_init may not have run
if not global.force_data then return end
local update_cycle = event.tick % settings.global["YARM-ticks-between-checks"].value
for _, force in pairs(game.forces) do
local force_data = global.force_data[force.name]
if not force_data then
resmon.init_force(force)
elseif force_data and force_data.ore_sites then
for _, site in pairs(force_data.ore_sites) do
resmon.count_deposits(site, update_cycle)
end
end
end
end
local function profiler_output(message, stopwatch)
local output = { "", message, " - ", stopwatch }
log(output)
for _, player in pairs(game.players) do
player.print(output)
end
end
local function on_tick_internal(event)
ore_tracker.on_tick(event)
resmon.entity_cache = ore_tracker.get_entity_cache()
resmon.update_players(event)
resmon.update_forces(event)
end
local function on_tick_internal_with_profiling(event)
local big_stopwatch = game.create_profiler()
local stopwatch = game.create_profiler()
ore_tracker.on_tick(event)
stopwatch.stop()
profiler_output("ore_tracker", stopwatch)
resmon.entity_cache = ore_tracker.get_entity_cache()
stopwatch.reset()
resmon.update_players(event)
stopwatch.stop()
profiler_output("update_players", stopwatch)
stopwatch.reset()
resmon.update_forces(event)
stopwatch.stop()
profiler_output("update_forces", stopwatch)
big_stopwatch.stop()
profiler_output("total on_tick", big_stopwatch)
end
function resmon.on_tick(event)
local wants_profiling = settings.global["YARM-debug-profiling"].value or false
if wants_profiling then
on_tick_internal_with_profiling(event)
else
on_tick_internal(event)
end
end
function resmon.on_load()
ore_tracker.on_load()
end