439 lines
19 KiB
Lua
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
|