1774 lines
66 KiB
Lua
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
|