439 lines
19 KiB
Lua

local table = require("__flib__.table")
-- Each production graph bracket, from highest to lowest
-- Used in find_precision_bracket()
local FLOW_PRECISION_BRACKETS = {
defines.flow_precision_index.one_thousand_hours,
defines.flow_precision_index.two_hundred_fifty_hours,
defines.flow_precision_index.fifty_hours,
defines.flow_precision_index.ten_hours,
defines.flow_precision_index.one_hour,
defines.flow_precision_index.ten_minutes,
defines.flow_precision_index.one_minute,
defines.flow_precision_index.five_seconds
}
-- The length of each precision bracket, in ticks
local FLOW_PRECISION_BRACKETS_LENGTHS = {
[defines.flow_precision_index.one_thousand_hours] = 1000*60*60*60,
[defines.flow_precision_index.two_hundred_fifty_hours] = 250*60*60*60,
[defines.flow_precision_index.fifty_hours] = 50*60*60*60,
[defines.flow_precision_index.ten_hours] = 10*60*60*60,
[defines.flow_precision_index.one_hour] = 1*60*60*60,
[defines.flow_precision_index.ten_minutes] = 10*60*60,
[defines.flow_precision_index.one_minute] = 1*60*60,
[defines.flow_precision_index.five_seconds] = 5*60,
}
local function find_possible_existing_completion_time(global_force, new_milestone)
for _, complete_milestone in pairs(global_force.complete_milestones) do
if complete_milestone.type == new_milestone.type and
complete_milestone.name == new_milestone.name and
complete_milestone.quantity == new_milestone.quantity then
return complete_milestone.completion_tick, complete_milestone.lower_bound_tick
end
end
return nil, nil
end
function merge_new_milestones(force_name, new_loaded_milestones)
local global_force = global.forces[force_name]
local new_complete = {}
local new_incomplete = {}
local new_milestones_by_group = {}
local current_group = "Other"
for i, new_loaded_milestone in pairs(new_loaded_milestones) do
if new_loaded_milestone.type == "group" then
current_group = new_loaded_milestone.name
elseif new_loaded_milestone.type ~= "alias" then
local new_milestone = table.deep_copy(new_loaded_milestone)
new_milestone.sort_index = i
new_milestone.group = current_group
new_milestones_by_group[current_group] = new_milestones_by_group[current_group] or {}
local next_milestone = new_milestone
while next_milestone ~= nil do
local completion_tick, lower_bound_tick = find_possible_existing_completion_time(global_force, next_milestone)
if completion_tick == nil then
table.insert(new_incomplete, next_milestone)
-- Intentionally insert the same reference in both new_milestones_by_group and new_incomplete/new_incomplete
table.insert(new_milestones_by_group[current_group], next_milestone)
next_milestone = nil
else
next_milestone.completion_tick = completion_tick
next_milestone.lower_bound_tick = lower_bound_tick
table.insert(new_complete, next_milestone)
table.insert(new_milestones_by_group[current_group], next_milestone)
if next_milestone.next then
next_milestone = create_next_milestone(force_name, next_milestone)
else
next_milestone = nil
end
end
end
end
end
global_force.complete_milestones = new_complete
global_force.incomplete_milestones = new_incomplete
global_force.milestones_by_group = new_milestones_by_group
end
function initialize_alias_table()
global.production_aliases = {}
for _, loaded_milestone in pairs(global.loaded_milestones) do
if loaded_milestone.type == "alias" then
local valid_alias = false
if game.item_prototypes[loaded_milestone.equals] ~= nil then
-- This is an item alias
valid_alias = game.item_prototypes[loaded_milestone.name] ~= nil
elseif game.entity_prototypes[loaded_milestone.equals] ~= nil then
-- This is an entity alias
valid_alias = game.entity_prototypes[loaded_milestone.name] ~= nil
end
if valid_alias then
global.production_aliases[loaded_milestone.equals] = {} or global.production_aliases[loaded_milestone.equals]
table.insert(global.production_aliases[loaded_milestone.equals], {name=loaded_milestone.name, quantity=loaded_milestone.quantity})
end
end
end
end
function mark_milestone_reached(global_force, milestone, tick, milestone_index, lower_bound_tick) -- lower_bound_tick is optional
milestone.completion_tick = tick
if lower_bound_tick then milestone.lower_bound_tick = lower_bound_tick end
table.insert(global_force.complete_milestones, milestone)
table.remove(global_force.incomplete_milestones, milestone_index)
sort_milestones(global_force.milestones_by_group[milestone.group])
end
function parse_next_formula(next_formula)
if next_formula == nil or string.len(next_formula) < 2 then return nil, nil end
local operator = string.sub(next_formula, 1, 1)
local next_value = tonumber(string.sub(next_formula, 2))
if next_value == nil then return nil, nil end
if operator == '*' then operator = 'x' end
if operator == 'x' then
if next_value <= 1 then return nil, nil end
elseif operator == '+' then
if next_value <= 0 then return nil, nil end
else
return nil, nil
end
return operator, next_value
end
function create_next_milestone(force_name, milestone)
local operator, next_value = parse_next_formula(milestone.next)
if operator == nil then
game.forces[force_name].print({"", {"milestones.message_invalid_next"}, milestone.next})
return
end
local new_milestone = table.deep_copy(milestone)
if operator == '+' then
new_milestone.quantity = milestone.quantity + next_value
elseif operator == 'x' then
new_milestone.quantity = milestone.quantity * next_value
end
new_milestone.lower_bound_tick = nil
new_milestone.completion_tick = nil
new_milestone.sort_index = milestone.sort_index + 0.0001 -- Should work until there's 10000 iterations of an infinite milestone, lol
new_milestone.hidden = nil
return new_milestone
end
function floor_to_nearest_minute(tick)
return (tick - (tick % (60*60)))
end
function ceil_to_nearest_minute(tick)
local modulo = tick % (60*60)
if modulo == 0 then return tick end
return tick - modulo + 60*60
end
local function get_total_count(stats, item_name, is_consumption)
if is_consumption then
return stats.get_output_count(item_name)
else
return stats.get_input_count(item_name)
end
end
local function find_precision_bracket(milestone, stats, is_consumption)
local total_count = get_total_count(stats, milestone.name, is_consumption)
local previous_bracket = "ALL"
for _, bracket in pairs(FLOW_PRECISION_BRACKETS) do
-- The first bracket that does NOT match the total count indicates the upper bound first production time.
-- e.g: if total_count = 4, and 4 were created in the last 1000 hours, 4 were created in the last 250 hours, and 2 were created in the last 50 hours,
-- then the first creation was between 50 and 250 hours ago, and we should search the 250 hours precision bracket.
if FLOW_PRECISION_BRACKETS_LENGTHS[bracket] <= game.tick then -- Skip bracket if the game is not long enough
local bracket_count = stats.get_flow_count{name=milestone.name, input=(not is_consumption), precision_index=bracket, count=true}
if bracket_count <= total_count - milestone.quantity then
return previous_bracket
end
end
previous_bracket = bracket
end
-- If we haven't found any count drop after going through all brackets
-- then the item was produced during most recent bracket, which is within the last 5 seconds (improbable but could happen).
return previous_bracket
end
local function find_sample_in_precision_bracket(milestone, bracket, stats, is_consumption)
local total_count = get_total_count(stats, milestone.name, is_consumption)
local bracket_count = stats.get_flow_count{name=milestone.name, input=(not is_consumption), precision_index=bracket, count=true}
local count_before_bracket = total_count - bracket_count
local count_this_bracket = 0
for i = 300,1,-1 do -- Start from oldest
local sample_count = stats.get_flow_count{name=milestone.name, input=(not is_consumption), precision_index=bracket, count=true, sample_index=i}
count_this_bracket = count_this_bracket + sample_count
if count_this_bracket + count_before_bracket >= milestone.quantity then
return i
end
end
-- We should almost never reach this point because we already determined this is the bracket where the milestone was reached.
-- It can happen when a mod creates items on tick 0 (i.e. before the game starts).
log("Couldn't find sample! milestone: " ..serpent.line(milestone).. ", bracket: " ..bracket..
", count_before_bracket: " ..count_before_bracket.. ", count_this_bracket: " ..count_this_bracket)
return 0
end
local function get_tick_bounds_from_sample(bracket, sample_index)
local sample_precision_in_ticks = FLOW_PRECISION_BRACKETS_LENGTHS[bracket] / 300
local upper_bound_ticks_ago = sample_precision_in_ticks * sample_index
local lower_bound_ticks_ago = sample_precision_in_ticks * (sample_index - 1)
return upper_bound_ticks_ago, lower_bound_ticks_ago
end
-- Converts from "X ticks ago" to "X ticks since start of the game"
local function get_realtime_tick_bounds(lower_bound_ticks_ago, upper_bound_ticks_ago, bracket)
local lower_bound_ticks, upper_bound_ticks = game.tick - lower_bound_ticks_ago, game.tick - upper_bound_ticks_ago
log("lower_bound_ticks: " ..lower_bound_ticks.. " - upper_bound_ticks: " ..upper_bound_ticks)
-- Samples are bound to absolute game time. e.g. sample #3 for defines.flow_precision_index.one_minute corresponds to ticks 25 to 36.
-- Floor to the real bounds of the sample.
if bracket ~= "ALL" then
local sample_precision_in_ticks = FLOW_PRECISION_BRACKETS_LENGTHS[bracket] / 300
local sample_offset = lower_bound_ticks % sample_precision_in_ticks
lower_bound_ticks = lower_bound_ticks - sample_offset + 1
upper_bound_ticks = upper_bound_ticks - sample_offset
log("sample_offset: " ..sample_offset)
log("lower_bound_ticks: " ..lower_bound_ticks.. " - upper_bound_ticks: " ..upper_bound_ticks)
end
return lower_bound_ticks, upper_bound_ticks
end
local function find_production_tick_bounds(milestone, stats, is_consumption)
local precision_bracket = find_precision_bracket(milestone, stats, is_consumption)
log("bracket to search: " ..precision_bracket)
local lower_bound_ticks_ago, upper_bound_ticks_ago
if precision_bracket == "ALL" then
-- Completion time is over 1000 hours ago, there are no samples to go through
-- All we know is it's between tick 0 and 1000 hours ago
lower_bound_ticks_ago, upper_bound_ticks_ago = game.tick, FLOW_PRECISION_BRACKETS_LENGTHS[defines.flow_precision_index.one_thousand_hours]
else
local sample_index = find_sample_in_precision_bracket(milestone, precision_bracket, stats, is_consumption)
if sample_index == 0 then
return game.tick, game.tick -- Created this exact tick, usually on tick 0 before the start of the game
end
lower_bound_ticks_ago, upper_bound_ticks_ago = get_tick_bounds_from_sample(precision_bracket, sample_index)
end
lower_bound_real_time, upper_bound_real_time = get_realtime_tick_bounds(lower_bound_ticks_ago, upper_bound_ticks_ago, precision_bracket)
return lower_bound_real_time, upper_bound_real_time
end
function find_completion_tick_bounds(milestone, item_stats, fluid_stats, kill_stats)
if milestone.type == "technology" then
return 0, game.tick -- No way to know past research time
elseif milestone.type == "item" then
return find_production_tick_bounds(milestone, item_stats, false)
elseif milestone.type == "fluid" then
return find_production_tick_bounds(milestone, fluid_stats, false)
elseif milestone.type == "item_consumption" then
return find_production_tick_bounds(milestone, item_stats, true)
elseif milestone.type == "fluid_consumption" then
return find_production_tick_bounds(milestone, fluid_stats, true)
elseif milestone.type == "kill" then
return find_production_tick_bounds(milestone, kill_stats, false)
end
end
function sort_milestones(milestones)
table.sort(milestones, function(a,b)
if a.completion_tick and not b.completion_tick then return true end -- a comes first
if not a.completion_tick and b.completion_tick then return false end -- b comes first
if a.completion_tick == b.completion_tick then return a.sort_index < b.sort_index end
return a.completion_tick < b.completion_tick
end)
end
function filter_hidden_milestones(milestones, show_incomplete)
local visible_milestones = {}
for _, milestone in pairs(milestones) do
if milestone.completion_tick ~= nil or (show_incomplete and not milestone.hidden) then
table.insert(visible_milestones, milestone)
end
end
return visible_milestones
end
function backfill_completion_times(force)
log("Backfilling completion times for " .. force.name)
local backfilled_anything = false
local item_stats = force.item_production_statistics
local fluid_stats = force.fluid_production_statistics
local kill_stats = force.kill_count_statistics
local technologies = force.technologies
local global_force = global.forces[force.name]
local i = 1
while i <= #global_force.incomplete_milestones do
local milestone = global_force.incomplete_milestones[i]
if is_valid_milestone(milestone) and is_milestone_reached(milestone, global_force, technologies) then
local lower_bound, upper_bound = find_completion_tick_bounds(milestone, item_stats, fluid_stats, kill_stats)
log("Tick bounds for " ..milestone.name.. " : " ..lower_bound.. " - " ..upper_bound)
if milestone.next then
local next_milestone = create_next_milestone(force.name, milestone)
table.insert(global_force.incomplete_milestones, next_milestone)
table.insert(global_force.milestones_by_group[next_milestone.group], next_milestone)
end
mark_milestone_reached(global_force, milestone, upper_bound, i, lower_bound)
backfilled_anything = true
else
i = i + 1
end
end
sort_milestones(global_force.complete_milestones)
for _group_name, group_milestones in pairs(global_force.milestones_by_group) do
sort_milestones(group_milestones)
end
return backfilled_anything
end
function is_production_milestone_reached(milestone, global_force)
local stats
if milestone.type == "item" or milestone.type == "item_consumption" then
stats = global_force.item_stats
elseif milestone.type == "fluid" or milestone.type == "fluid_consumption" then
stats = global_force.fluid_stats
elseif milestone.type == "kill" then
stats = global_force.kill_stats
else
error("Invalid milestone type! " .. milestone.type)
end
local milestone_count
if milestone.type == "item_consumption" or milestone.type == "fluid_consumption" then
milestone_count = stats.get_output_count(milestone.name)
else
milestone_count = stats.get_input_count(milestone.name)
end
-- Aliases
if global.production_aliases[milestone.name] then
for _, alias in pairs(global.production_aliases[milestone.name]) do
local alias_count = stats.get_input_count(alias.name)
if alias_count then
milestone_count = milestone_count or 0 -- Could be nil
milestone_count = milestone_count + alias_count * alias.quantity
end
end
end
return milestone_count and milestone_count >= milestone.quantity
end
function is_tech_milestone_reached(milestone, technology)
if milestone.type == "technology" and
technology.name == milestone.name and
-- strict > because the level we get is the current researchable level, not the researched level
-- if technology.level == technology.prototype.level then this is just a non-repeating tech with a number at the end e.g. 'Electronics 3'
(technology.researched or (technology.level > milestone.quantity and technology.level > technology.prototype.level)) then
return true
end
return false
end
function is_milestone_reached(milestone, global_force, technologies)
if milestone.type == "technology" then
local technology = technologies[milestone.name]
return is_tech_milestone_reached(milestone, technology)
else
return is_production_milestone_reached(milestone, global_force)
end
end
function remove_invalid_milestones_all_forces()
for _force_name, global_force in pairs(global.forces) do
remove_invalid_milestones_for_force(global_force)
end
end
function remove_invalid_milestones_for_force(global_force)
remove_invalid_milestones(global_force.complete_milestones)
remove_invalid_milestones(global_force.incomplete_milestones)
for _group_name, milestones_by_group in pairs(global_force.milestones_by_group) do
remove_invalid_milestones(milestones_by_group, true)
end
end
function remove_invalid_milestones(milestones, silent)
local i = 1
while i <= #milestones do
local milestone = milestones[i]
if is_valid_milestone(milestone) then
i = i + 1
else
table.remove(milestones, i)
if not silent then
table.insert(global.delayed_chat_messages, {"milestones.message_invalid_item", milestone.name})
end
end
end
end
function is_valid_milestone(milestone)
local prototype
if milestone.type == "item" or milestone.type == "item_consumption" then
prototype = game.item_prototypes[milestone.name]
elseif milestone.type == "fluid" or milestone.type == "fluid_consumption" then
prototype = game.fluid_prototypes[milestone.name]
elseif milestone.type == "technology" then
prototype = game.technology_prototypes[milestone.name]
elseif milestone.type == "kill" then
prototype = game.entity_prototypes[milestone.name]
else
return false
end
return prototype ~= nil
end
function sprite_prefix(milestone)
if milestone.type == "item" or milestone.type == "item_consumption" then
return "item"
elseif milestone.type == "fluid" or milestone.type == "fluid_consumption" then
return "fluid"
elseif milestone.type == "kill" then
return "entity"
elseif milestone.type == "technology" then
return "technology"
else
error("Unknown milestone type: " .. milestone.type)
end
end