mirror of
https://github.com/Xevion/project-cybersyn.git
synced 2025-12-11 00:08:13 -06:00
beginning to add depot logic
This commit is contained in:
532
cybersyn/scripts/central-planning.lua
Normal file
532
cybersyn/scripts/central-planning.lua
Normal file
@@ -0,0 +1,532 @@
|
||||
--By Mami
|
||||
local get_distance = require("__flib__.misc").get_distance
|
||||
local math = math
|
||||
local INF = math.huge
|
||||
local btest = bit32.btest
|
||||
local band = bit32.band
|
||||
|
||||
local create_loading_order_condition = {type = "inactivity", compare_type = "and", ticks = 120}
|
||||
---@param stop LuaEntity
|
||||
---@param manifest Manifest
|
||||
function create_loading_order(stop, manifest)
|
||||
local condition = {}
|
||||
for _, item in ipairs(manifest) do
|
||||
local cond_type
|
||||
if item.type == "fluid" then
|
||||
cond_type = "fluid_count"
|
||||
else
|
||||
cond_type = "item_count"
|
||||
end
|
||||
|
||||
condition[#condition + 1] = {
|
||||
type = cond_type,
|
||||
compare_type = "and",
|
||||
condition = {comparator = "≥", first_signal = {type = item.type, name = item.name}, constant = item.count}
|
||||
}
|
||||
end
|
||||
condition[#condition + 1] = create_loading_order_condition
|
||||
return {station = stop.backer_name, wait_conditions = condition}
|
||||
end
|
||||
|
||||
local create_unloading_order_condition = {{type = "empty", compare_type = "and"}}
|
||||
---@param stop LuaEntity
|
||||
function create_unloading_order(stop)
|
||||
return {station = stop.backer_name, wait_conditions = create_unloading_order_condition}
|
||||
end
|
||||
|
||||
local create_inactivity_order_condition = {{type = "inactivity", compare_type = "and", ticks = 120}}
|
||||
---@param depot_name string
|
||||
function create_inactivity_order(depot_name)
|
||||
return {station = depot_name, wait_conditions = create_inactivity_order_condition}
|
||||
end
|
||||
|
||||
---@param stop LuaEntity
|
||||
local function create_direct_to_station_order(stop)
|
||||
return {rail = stop.connected_rail, rail_direction = stop.connected_rail_direction}
|
||||
end
|
||||
|
||||
---@param depot_name string
|
||||
function create_depot_schedule(depot_name)
|
||||
return {current = 1, records = {create_inactivity_order(depot_name)}}
|
||||
end
|
||||
|
||||
---@param depot_name string
|
||||
---@param p_stop LuaEntity
|
||||
---@param r_stop LuaEntity
|
||||
---@param manifest Manifest
|
||||
function create_manifest_schedule(depot_name, p_stop, r_stop, manifest)
|
||||
return {current = 1, records = {
|
||||
create_inactivity_order(depot_name),
|
||||
create_direct_to_station_order(p_stop),
|
||||
create_loading_order(p_stop, manifest),
|
||||
create_direct_to_station_order(r_stop),
|
||||
create_unloading_order(r_stop),
|
||||
}}
|
||||
end
|
||||
|
||||
---@param station Station
|
||||
local function get_signals(station)
|
||||
local comb = station.entity_comb1
|
||||
if comb.valid and (comb.status == defines.entity_status.working or comb.status == defines.entity_status.low_power) then
|
||||
return comb.get_merged_signals(defines.circuit_connector_id.combinator_input)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param depot Depot
|
||||
local function set_depot_signals(depot)
|
||||
local comb = depot.entity_comb
|
||||
if depot.network_name and comb.valid and (comb.status == defines.entity_status.working or comb.status == defines.entity_status.low_power) then
|
||||
depot.priority = 0
|
||||
depot.network_flag = 1
|
||||
local signals = comb.get_merged_signals(defines.circuit_connector_id.combinator_input)
|
||||
if signals then
|
||||
for k, v in pairs(signals) do
|
||||
local item_name = v.signal.name
|
||||
local item_count = v.count
|
||||
if item_name then
|
||||
if item_name == SIGNAL_PRIORITY then
|
||||
depot.priority = item_count
|
||||
end
|
||||
if item_name == depot.network_name then
|
||||
depot.network_flag = item_count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
depot.priority = 0
|
||||
depot.network_flag = 0
|
||||
end
|
||||
end
|
||||
|
||||
---@param map_data MapData
|
||||
---@param comb LuaEntity
|
||||
---@param signals ConstantCombinatorParameters[]?
|
||||
function set_combinator_output(map_data, comb, signals)
|
||||
if comb.valid then
|
||||
local out = map_data.to_output[comb.unit_number]
|
||||
if out.valid then
|
||||
out.get_or_create_control_behavior().parameters = signals
|
||||
else
|
||||
--TODO: error logging?
|
||||
end
|
||||
else
|
||||
--TODO: error logging?
|
||||
end
|
||||
end
|
||||
|
||||
---@param map_data MapData
|
||||
---@param station Station
|
||||
local function set_comb2(map_data, station)
|
||||
if station.entity_comb2 then
|
||||
local deliveries = station.deliveries
|
||||
local signals = {}
|
||||
for item_name, count in pairs(deliveries) do
|
||||
local i = #signals + 1
|
||||
local item_type = game.item_prototypes[item_name].type
|
||||
signals[i] = {index = i, signal = {type = item_type, name = item_name}, count = count}
|
||||
end
|
||||
set_combinator_output(map_data, station.entity_comb2, signals)
|
||||
end
|
||||
end
|
||||
|
||||
---@param map_data MapData
|
||||
---@param station Station
|
||||
---@param manifest Manifest
|
||||
function remove_manifest(map_data, station, manifest, sign)
|
||||
local deliveries = station.deliveries
|
||||
for i, item in ipairs(manifest) do
|
||||
deliveries[item.name] = deliveries[item.name] + sign*item.count
|
||||
if deliveries[item.name] == 0 then
|
||||
deliveries[item.name] = nil
|
||||
end
|
||||
end
|
||||
set_comb2(map_data, station)
|
||||
station.deliveries_total = station.deliveries_total - 1
|
||||
end
|
||||
|
||||
---@param map_data MapData
|
||||
---@param station Station
|
||||
---@param signal SignalID
|
||||
local function get_thresholds(map_data, station, signal)
|
||||
local comb2 = station.entity_comb2
|
||||
if comb2 and comb2.valid then
|
||||
local count = comb2.get_merged_signal(signal, defines.circuit_connector_id.combinator_input)
|
||||
if count > 0 then
|
||||
return station.r_threshold, count
|
||||
elseif count < 0 then
|
||||
return -count, station.p_threshold
|
||||
end
|
||||
end
|
||||
return station.r_threshold, station.p_threshold
|
||||
end
|
||||
|
||||
---@param stop0 LuaEntity
|
||||
---@param stop1 LuaEntity
|
||||
local function get_stop_dist(stop0, stop1)
|
||||
return get_distance(stop0.position, stop1.position)
|
||||
end
|
||||
|
||||
|
||||
---@param station Station
|
||||
---@param layout_id uint
|
||||
local function station_accepts_layout(station, layout_id)
|
||||
return station.accepted_layouts[layout_id]
|
||||
end
|
||||
|
||||
|
||||
|
||||
---@param map_data MapData
|
||||
---@param r_station_id uint
|
||||
---@param p_station_id uint
|
||||
---@param item_type string
|
||||
local function get_valid_train(map_data, r_station_id, p_station_id, item_type)
|
||||
--NOTE: this code is the critical section for run-time optimization
|
||||
local r_station = map_data.stations[r_station_id]
|
||||
local p_station = map_data.stations[p_station_id]
|
||||
---@type string
|
||||
local network_name = p_station.network_name
|
||||
|
||||
local p_to_r_dist = get_stop_dist(p_station.entity_stop, r_station.entity_stop)
|
||||
local netand = band(p_station.network_flag, r_station.network_flag)
|
||||
if p_to_r_dist == INF or netand == 0 then
|
||||
return nil, INF
|
||||
end
|
||||
|
||||
local best_train = nil
|
||||
local best_dist = INF
|
||||
local valid_train_exists = false
|
||||
|
||||
local is_fluid = item_type == "fluid"
|
||||
for depot_id, train_id in pairs(map_data.trains_available[network_name]) do
|
||||
local depot = map_data.depots[depot_id]
|
||||
local train = map_data.trains[train_id]
|
||||
--check cargo capabilities
|
||||
--check layout validity for both stations
|
||||
if
|
||||
depot.network_name == network_name and
|
||||
btest(netand, depot.network_flag)
|
||||
((is_fluid and train.fluid_capacity > 0) or (not is_fluid and train.item_slot_capacity > 0)) and
|
||||
station_accepts_layout(r_station, train.layout_id) and
|
||||
station_accepts_layout(p_station, train.layout_id)
|
||||
then
|
||||
valid_train_exists = true
|
||||
--check if exists valid path
|
||||
--check if path is shortest so we prioritize locality
|
||||
local d_to_p_dist = get_stop_dist(depot.entity_stop, p_station.entity_stop) - DEPOT_PRIORITY_MULT*depot.priority
|
||||
|
||||
local dist = d_to_p_dist
|
||||
if dist < best_dist then
|
||||
best_dist = dist
|
||||
best_train = train
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if valid_train_exists then
|
||||
return best_train, best_dist + p_to_r_dist
|
||||
else
|
||||
return nil, p_to_r_dist
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@param map_data MapData
|
||||
---@param r_station_id uint
|
||||
---@param p_station_id uint
|
||||
---@param train Train
|
||||
---@param primary_item_name string
|
||||
---@param economy Economy
|
||||
local function send_train_between(map_data, r_station_id, p_station_id, train, primary_item_name, economy)
|
||||
--trains and stations expected to be of the same network
|
||||
local r_station = map_data.stations[r_station_id]
|
||||
local p_station = map_data.stations[p_station_id]
|
||||
|
||||
local requests = {}
|
||||
local manifest = {}
|
||||
|
||||
local r_signals = get_signals(r_station)
|
||||
if r_signals then
|
||||
for k, v in pairs(r_signals) do
|
||||
local item_name = v.signal.name
|
||||
local item_count = v.count
|
||||
local item_type = v.signal.type
|
||||
if item_name and item_type and item_type ~= "virtual" then
|
||||
local effective_item_count = item_count + (r_station.deliveries[item_name] or 0)
|
||||
local r_threshold, p_threshold = get_thresholds(map_data, r_station, v.signal)
|
||||
if -effective_item_count >= r_threshold then
|
||||
requests[item_name] = -effective_item_count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local p_signals = get_signals(p_station)
|
||||
if p_signals then
|
||||
for k, v in pairs(p_signals) do
|
||||
local item_name = v.signal.name
|
||||
local item_count = v.count
|
||||
local item_type = v.signal.type
|
||||
if item_name and item_type and item_type ~= "virtual" then
|
||||
local effective_item_count = item_count + (p_station.deliveries[item_name] or 0)
|
||||
local r_threshold, p_threshold = get_thresholds(map_data, p_station, v.signal)
|
||||
if effective_item_count >= p_threshold then
|
||||
local r = requests[item_name]
|
||||
if r then
|
||||
local item = {name = item_name, count = math.min(r, effective_item_count), type = item_type}
|
||||
if item_name == primary_item_name then
|
||||
manifest[#manifest + 1] = manifest[1]
|
||||
manifest[1] = item
|
||||
else
|
||||
manifest[#manifest + 1] = item
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local locked_slots = math.max(p_station.locked_slots, r_station.locked_slots)
|
||||
local total_slots_left = train.item_slot_capacity
|
||||
if locked_slots > 0 then
|
||||
total_slots_left = math.max(total_slots_left - #train.entity.cargo_wagons*locked_slots, math.min(total_slots_left, #train.entity.cargo_wagons))
|
||||
end
|
||||
local total_liquid_left = train.fluid_capacity
|
||||
|
||||
local i = 1
|
||||
while i <= #manifest do
|
||||
local item = manifest[i]
|
||||
local keep_item = false
|
||||
if item.type == "fluid" then
|
||||
if total_liquid_left > 0 then
|
||||
if item.count > total_liquid_left then
|
||||
item.count = total_liquid_left
|
||||
end
|
||||
total_liquid_left = 0--no liquid merging
|
||||
keep_item = true
|
||||
end
|
||||
elseif total_slots_left > 0 then
|
||||
local stack_size = game.item_prototypes[item.name].stack_size
|
||||
local slots = math.ceil(item.count/stack_size)
|
||||
if slots > total_slots_left then
|
||||
item.count = total_slots_left*stack_size
|
||||
end
|
||||
total_slots_left = total_slots_left - slots
|
||||
keep_item = true
|
||||
end
|
||||
if keep_item then
|
||||
i = i + 1
|
||||
else--swap remove
|
||||
manifest[i] = manifest[#manifest]
|
||||
manifest[#manifest] = nil
|
||||
end
|
||||
end
|
||||
|
||||
r_station.last_delivery_tick = economy.total_ticks
|
||||
p_station.last_delivery_tick = economy.total_ticks
|
||||
|
||||
r_station.deliveries_total = r_station.deliveries_total + 1
|
||||
p_station.deliveries_total = p_station.deliveries_total + 1
|
||||
|
||||
for _, item in ipairs(manifest) do
|
||||
assert(item.count > 0, "main.lua error, transfer amount was not positive")
|
||||
|
||||
r_station.deliveries[item.name] = (r_station.deliveries[item.name] or 0) + item.count
|
||||
p_station.deliveries[item.name] = (p_station.deliveries[item.name] or 0) - item.count
|
||||
|
||||
local item_network_name = (r_station.network_name and item.name + ":" + r_station.network_name) or item.name
|
||||
local r_stations = economy.r_stations_all[item_network_name]
|
||||
local p_stations = economy.p_stations_all[item_network_name]
|
||||
for i, id in ipairs(r_stations) do
|
||||
if id == r_station_id then
|
||||
table.remove(r_stations, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
for i, id in ipairs(p_stations) do
|
||||
if id == p_station_id then
|
||||
table.remove(p_stations, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
map_data.trains_available[train.entity.id] = nil
|
||||
train.status = STATUS_D_TO_P
|
||||
train.p_station_id = p_station_id
|
||||
train.r_station_id = r_station_id
|
||||
train.manifest = manifest
|
||||
|
||||
train.entity.schedule = create_manifest_schedule(train.depot_name, p_station.entity_stop, r_station.entity_stop, manifest)
|
||||
set_comb2(map_data, p_station)
|
||||
set_comb2(map_data, r_station)
|
||||
end
|
||||
|
||||
---@param map_data MapData
|
||||
function tick(map_data, mod_settings)
|
||||
local total_ticks = map_data.total_ticks
|
||||
local stations = map_data.stations
|
||||
---@type Economy
|
||||
local economy = {
|
||||
r_stations_all = {},
|
||||
p_stations_all = {},
|
||||
total_ticks = total_ticks,
|
||||
}
|
||||
local r_stations_all = economy.r_stations_all
|
||||
local p_stations_all = economy.p_stations_all
|
||||
local all_names = {}
|
||||
|
||||
for depot_id, _ in pairs(map_data.trains_available) do
|
||||
set_depot_signals(map_data.depots[depot_id])
|
||||
end
|
||||
|
||||
for station_id, station in pairs(stations) do
|
||||
if station.network_name and station.deliveries_total < station.entity_stop.trains_limit then
|
||||
station.r_threshold = mod_settings.r_threshold
|
||||
station.p_threshold = mod_settings.p_threshold
|
||||
station.priority = 0
|
||||
station.locked_slots = 0
|
||||
station.network_flag = 1
|
||||
local signals = get_signals(station)
|
||||
if signals then
|
||||
for k, v in pairs(signals) do
|
||||
local item_name = v.signal.name
|
||||
local item_count = v.count
|
||||
local item_type = v.signal.type
|
||||
if item_name then
|
||||
if item_type == "virtual" then
|
||||
if item_name == SIGNAL_PRIORITY then
|
||||
station.priority = item_count
|
||||
elseif item_name == REQUEST_THRESHOLD then
|
||||
station.r_threshold = math.abs(item_count)
|
||||
elseif item_name == PROVIDE_THRESHOLD then
|
||||
station.p_threshold = math.abs(item_count)
|
||||
elseif item_name == LOCKED_SLOTS then
|
||||
station.locked_slots = math.max(item_count, 0)
|
||||
end
|
||||
signals[k] = nil
|
||||
end
|
||||
if item_name == station.network_name then
|
||||
station.network_flag = item_count
|
||||
end
|
||||
else
|
||||
signals[k] = nil
|
||||
end
|
||||
end
|
||||
for k, v in pairs(signals) do
|
||||
local item_name = v.signal.name
|
||||
local item_count = v.count
|
||||
local effective_item_count = item_count + (station.deliveries[item_name] or 0)
|
||||
local r_threshold, p_threshold = get_thresholds(map_data, station, v.signal)
|
||||
|
||||
if item_name then
|
||||
if -effective_item_count >= r_threshold then
|
||||
local item_network_name = item_name + ":" + station.network_name
|
||||
if r_stations_all[item_network_name] == nil then
|
||||
r_stations_all[item_network_name] = {}
|
||||
p_stations_all[item_network_name] = {}
|
||||
all_names[#all_names + 1] = item_network_name
|
||||
all_names[#all_names + 1] = v.signal
|
||||
end
|
||||
table.insert(r_stations_all[item_name], station_id)
|
||||
elseif effective_item_count >= p_threshold then
|
||||
local item_network_name = item_name + ":" + station.network_name
|
||||
if r_stations_all[item_network_name] == nil then
|
||||
r_stations_all[item_network_name] = {}
|
||||
p_stations_all[item_network_name] = {}
|
||||
all_names[#all_names + 1] = item_network_name
|
||||
all_names[#all_names + 1] = v.signal
|
||||
end
|
||||
table.insert(p_stations_all[item_name], station_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--we do not dispatch more than one train per station per tick
|
||||
--psuedo-randomize what item (and what station) to check first so if trains available is low they choose orders psuedo-randomly
|
||||
--NOTE: It may be better for performance to update stations one tick at a time rather than all at once, however this does mean more redundant data will be generated and discarded each tick. Once we have a performance test-bed it will probably be worth checking.
|
||||
local start_i = 2*(total_ticks%(#all_names/2)) + 1
|
||||
for item_i = 0, #all_names - 1, 2 do
|
||||
local item_network_name = all_names[(start_i + item_i - 1)%#all_names + 1]
|
||||
local signal = all_names[(start_i + item_i)%#all_names + 1]
|
||||
local item_name = signal.name
|
||||
local item_type = signal.type
|
||||
local r_stations = r_stations_all[item_network_name]
|
||||
local p_stations = p_stations_all[item_network_name]
|
||||
|
||||
--NOTE: this is an approximation algorithm for solving the assignment problem (bipartite graph weighted matching), the true solution would be to implement the simplex algorithm but I strongly believe most factorio players would prefer run-time efficiency over perfect train routing logic
|
||||
if #r_stations > 0 and #p_stations > 0 then
|
||||
if #r_stations <= #p_stations then
|
||||
--probably backpressure, prioritize locality
|
||||
repeat
|
||||
local i = total_ticks%#r_stations + 1
|
||||
local r_station_id = table.remove(r_stations, i)
|
||||
|
||||
local best = 0
|
||||
local best_train = nil
|
||||
local best_dist = INF
|
||||
local highest_prior = -INF
|
||||
local could_have_been_serviced = false
|
||||
for j, p_station_id in ipairs(p_stations) do
|
||||
local train, d = get_valid_train(map_data, r_station_id, p_station_id, item_type)
|
||||
local prior = stations[p_station_id].priority
|
||||
if prior > highest_prior or (prior == highest_prior and d < best_dist) then
|
||||
if train then
|
||||
best = j
|
||||
best_dist = d
|
||||
best_train = train
|
||||
highest_prior = prior
|
||||
elseif d < INF then
|
||||
could_have_been_serviced = true
|
||||
best = j
|
||||
end
|
||||
end
|
||||
end
|
||||
if best_train then
|
||||
send_train_between(map_data, r_station_id, p_stations[best], best_train, item_name, economy)
|
||||
elseif could_have_been_serviced then
|
||||
send_missing_train_alert_for_stops(stations[r_station_id].entity_stop, stations[p_stations[best]].entity_stop)
|
||||
end
|
||||
until #r_stations == 0
|
||||
else
|
||||
--prioritize round robin
|
||||
repeat
|
||||
local j = total_ticks%#p_stations + 1
|
||||
local p_station_id = table.remove(p_stations, j)
|
||||
|
||||
local best = 0
|
||||
local best_train = nil
|
||||
local lowest_tick = INF
|
||||
local highest_prior = -INF
|
||||
local could_have_been_serviced = false
|
||||
for i, r_station_id in ipairs(r_stations) do
|
||||
local r_station = stations[r_station_id]
|
||||
local prior = r_station.priority
|
||||
if prior > highest_prior or (prior == highest_prior and r_station.last_delivery_tick < lowest_tick) then
|
||||
local train, d = get_valid_train(map_data, r_station_id, p_station_id, item_type)
|
||||
if train then
|
||||
best = i
|
||||
best_train = train
|
||||
lowest_tick = r_station.last_delivery_tick
|
||||
highest_prior = prior
|
||||
elseif d < INF then
|
||||
could_have_been_serviced = true
|
||||
best = i
|
||||
end
|
||||
end
|
||||
end
|
||||
if best_train then
|
||||
send_train_between(map_data, r_stations[best], p_station_id, best_train, item_name, economy)
|
||||
elseif could_have_been_serviced then
|
||||
send_missing_train_alert_for_stops(stations[r_stations[best]].entity_stop, stations[p_station_id].entity_stop)
|
||||
end
|
||||
until #p_stations == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user