mirror of
https://github.com/Xevion/project-cybersyn.git
synced 2025-12-09 10:08:15 -06:00
507 lines
17 KiB
Lua
507 lines
17 KiB
Lua
--By Mami
|
|
local min = math.min
|
|
local max = math.max
|
|
local abs = math.abs
|
|
local ceil = math.ceil
|
|
local INF = math.huge
|
|
local btest = bit32.btest
|
|
local band = bit32.band
|
|
local table_remove = table.remove
|
|
local random = math.random
|
|
|
|
|
|
|
|
---@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
|
|
if station.deliveries_total == 0 and station.entity_comb1.valid then
|
|
set_comb_operation_with_check(map_data, station.entity_comb1, OPERATION_PRIMARY_IO)
|
|
end
|
|
end
|
|
|
|
---@param map_data MapData
|
|
---@param r_station_id uint
|
|
---@param p_station_id uint
|
|
---@param item_type string
|
|
---@param min_slots_to_move int
|
|
local function get_valid_train(map_data, r_station_id, p_station_id, item_type, min_slots_to_move)
|
|
--NOTE: this code is the critical section for amortized 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
|
|
|
|
---@type uint?
|
|
local best_train = nil
|
|
local best_capacity = 0
|
|
local best_dist = INF
|
|
local valid_train_exists = false
|
|
|
|
local is_fluid = item_type == "fluid"
|
|
local trains = map_data.available_trains[network_name]
|
|
if trains then
|
|
for train_id, _ in pairs(trains) do
|
|
local train = map_data.trains[train_id]
|
|
local layout_id = train.layout_id
|
|
--check cargo capabilities
|
|
--check layout validity for both stations
|
|
local capacity = (is_fluid and train.fluid_capacity) or train.item_slot_capacity
|
|
if
|
|
capacity >= min_slots_to_move and
|
|
btest(netand, train.network_flag) and
|
|
(r_station.allows_all_trains or r_station.accepted_layouts[layout_id]) and
|
|
(p_station.allows_all_trains or p_station.accepted_layouts[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(train.entity.front_stock, p_station.entity_stop) - DEPOT_PRIORITY_MULT*train.priority
|
|
|
|
local dist = d_to_p_dist
|
|
if capacity > best_capacity or (capacity == best_capacity and dist < best_dist) then
|
|
best_capacity = capacity
|
|
best_dist = dist
|
|
best_train = train_id
|
|
end
|
|
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_id uint
|
|
---@param primary_item_name string
|
|
local function send_train_between(map_data, r_station_id, p_station_id, train_id, primary_item_name)
|
|
--trains and stations expected to be of the same network
|
|
local economy = map_data.economy
|
|
local r_station = map_data.stations[r_station_id]
|
|
local p_station = map_data.stations[p_station_id]
|
|
local train = map_data.trains[train_id]
|
|
---@type string
|
|
local network_name = r_station.network_name
|
|
|
|
local manifest = {}
|
|
|
|
for k, v in pairs(r_station.tick_signals) do
|
|
---@type string
|
|
local item_name = v.signal.name
|
|
local item_type = v.signal.type
|
|
local r_item_count = v.count
|
|
local r_effective_item_count = r_item_count + (r_station.deliveries[item_name] or 0)
|
|
if r_effective_item_count < 0 and r_item_count < 0 then
|
|
local r_threshold = r_station.p_count_or_r_threshold_per_item[item_name]
|
|
local p_effective_item_count = p_station.p_count_or_r_threshold_per_item[item_name]
|
|
--could be an item that is not present at the station
|
|
if p_effective_item_count and p_effective_item_count >= r_threshold then
|
|
local item = {name = item_name, type = item_type, count = min(-r_effective_item_count, p_effective_item_count)}
|
|
if item_name == primary_item_name then
|
|
manifest[#manifest + 1] = manifest[1]
|
|
manifest[1] = item
|
|
else
|
|
manifest[#manifest + 1] = item
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--locked slots is only taken into account after the train is already approved for dispatch
|
|
local locked_slots = p_station.locked_slots
|
|
local total_slots_left = train.item_slot_capacity
|
|
if locked_slots > 0 then
|
|
local total_cw = #train.entity.cargo_wagons
|
|
total_slots_left = min(total_slots_left, max(total_slots_left - total_cw*locked_slots, total_cw))
|
|
end
|
|
local total_liquid_left = train.fluid_capacity
|
|
|
|
local i = 1
|
|
while i <= #manifest do
|
|
local item = manifest[i]
|
|
if item.count < 1000 then
|
|
local hello = true
|
|
end
|
|
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 = get_stack_size(map_data, item.name)
|
|
local slots = 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 = map_data.total_ticks
|
|
p_station.last_delivery_tick = map_data.total_ticks
|
|
|
|
r_station.deliveries_total = r_station.deliveries_total + 1
|
|
p_station.deliveries_total = p_station.deliveries_total + 1
|
|
|
|
for item_i, 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
|
|
|
|
if item_i > 1 then
|
|
--prevent deliveries from being processed for these items until their stations are re-polled
|
|
local item_network_name = network_name..":"..item.name
|
|
economy.all_r_stations[item_network_name] = nil
|
|
economy.all_p_stations[item_network_name] = nil
|
|
end
|
|
end
|
|
|
|
remove_available_train(map_data, train_id, train)
|
|
local depot_id = train.depot_id
|
|
if depot_id then
|
|
map_data.depots[depot_id].available_train_id = nil
|
|
train.depot_id = nil
|
|
end
|
|
|
|
train.status = STATUS_D_TO_P
|
|
train.p_station_id = p_station_id
|
|
train.r_station_id = r_station_id
|
|
train.manifest = manifest
|
|
train.last_manifest_tick = map_data.total_ticks
|
|
|
|
set_manifest_schedule(train.entity, train.depot_name, train.se_depot_surface_i, p_station.entity_stop, r_station.entity_stop, manifest, depot_id ~= nil)
|
|
set_comb2(map_data, p_station)
|
|
set_comb2(map_data, r_station)
|
|
if p_station.entity_comb1.valid then
|
|
set_comb_operation_with_check(map_data, p_station.entity_comb1, OPERATION_PRIMARY_IO_ACTIVE)
|
|
end
|
|
if r_station.entity_comb1.valid then
|
|
set_comb_operation_with_check(map_data, r_station.entity_comb1, OPERATION_PRIMARY_IO_ACTIVE)
|
|
end
|
|
end
|
|
|
|
---@param map_data MapData
|
|
---@param mod_settings CybersynModSettings
|
|
local function tick_poll_train(map_data, mod_settings)
|
|
local tick_data = map_data.tick_data
|
|
--NOTE: the following has undefined behavior if last_train is deleted, this should be ok since the following doesn't care how inconsistent our access pattern is
|
|
local train_id, train = next(map_data.trains, tick_data.last_train)
|
|
tick_data.last_train = train_id
|
|
|
|
if train and train.manifest and train.entity and train.last_manifest_tick + mod_settings.stuck_train_time*mod_settings.tps < map_data.total_ticks then
|
|
send_stuck_train_alert(train.entity, train.depot_name)
|
|
end
|
|
end
|
|
---@param map_data MapData
|
|
local function tick_poll_comb(map_data)
|
|
local tick_data = map_data.tick_data
|
|
--NOTE: the following has undefined behavior if last_comb is deleted
|
|
local comb_id, comb = next(map_data.to_comb, tick_data.last_comb)
|
|
tick_data.last_comb = comb_id
|
|
|
|
if comb and comb.valid then
|
|
combinator_update(map_data, comb)
|
|
end
|
|
end
|
|
---@param map_data MapData
|
|
---@param mod_settings CybersynModSettings
|
|
local function tick_poll_station(map_data, mod_settings)
|
|
local tick_data = map_data.tick_data
|
|
local all_r_stations = map_data.economy.all_r_stations
|
|
local all_p_stations = map_data.economy.all_p_stations
|
|
local all_names = map_data.economy.all_names
|
|
|
|
local station_id
|
|
local station
|
|
while true do--choose a station
|
|
tick_data.i = (tick_data.i or 0) + 1
|
|
if tick_data.i > #map_data.active_station_ids then
|
|
tick_data.i = nil
|
|
map_data.tick_state = STATE_DISPATCH
|
|
return true
|
|
end
|
|
station_id = map_data.active_station_ids[tick_data.i]
|
|
station = map_data.stations[station_id]
|
|
if station then
|
|
if station.display_update then
|
|
update_combinator_display(map_data, station.entity_comb1, station.display_failed_request)
|
|
station.display_update = station.display_failed_request
|
|
station.display_failed_request = nil
|
|
end
|
|
if station.network_name and station.deliveries_total < station.entity_stop.trains_limit then
|
|
break
|
|
end
|
|
else
|
|
--lazy delete removed stations
|
|
table_remove(map_data.active_station_ids, tick_data.i)
|
|
tick_data.i = tick_data.i - 1
|
|
end
|
|
end
|
|
station.r_threshold = mod_settings.r_threshold
|
|
station.priority = 0
|
|
station.locked_slots = 0
|
|
station.network_flag = mod_settings.network_flag
|
|
local signals = get_signals(station)
|
|
station.tick_signals = signals
|
|
station.p_count_or_r_threshold_per_item = {}
|
|
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 and item_count ~= 0 then
|
|
--NOTE: thresholds must be >0 or they can cause a crash
|
|
station.r_threshold = abs(item_count)
|
|
elseif item_name == LOCKED_SLOTS then
|
|
station.locked_slots = max(item_count, 0)
|
|
end
|
|
signals[k] = nil
|
|
end
|
|
if item_name == station.network_name then
|
|
station.network_flag = item_count
|
|
signals[k] = nil
|
|
end
|
|
else
|
|
signals[k] = nil
|
|
end
|
|
end
|
|
for k, v in pairs(signals) do
|
|
---@type string
|
|
local item_name = v.signal.name
|
|
local item_count = v.count
|
|
local effective_item_count = item_count + (station.deliveries[item_name] or 0)
|
|
|
|
local flag = true
|
|
if station.is_r then
|
|
local r_threshold = get_threshold(map_data, station, v.signal)
|
|
if -effective_item_count >= r_threshold and -item_count >= r_threshold then
|
|
flag = false
|
|
local item_network_name = station.network_name..":"..item_name
|
|
local stations = all_r_stations[item_network_name]
|
|
if stations == nil then
|
|
stations = {}
|
|
all_r_stations[item_network_name] = stations
|
|
all_names[#all_names + 1] = item_network_name
|
|
all_names[#all_names + 1] = v.signal
|
|
end
|
|
stations[#stations + 1] = station_id
|
|
station.p_count_or_r_threshold_per_item[item_name] = r_threshold
|
|
end
|
|
end
|
|
if flag then
|
|
if station.is_p and effective_item_count > 0 and item_count > 0 then
|
|
local item_network_name = station.network_name..":"..item_name
|
|
local stations = all_p_stations[item_network_name]
|
|
if stations == nil then
|
|
stations = {}
|
|
all_p_stations[item_network_name] = stations
|
|
end
|
|
stations[#stations + 1] = station_id
|
|
station.p_count_or_r_threshold_per_item[item_name] = effective_item_count
|
|
else
|
|
signals[k] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
---@param map_data MapData
|
|
---@param mod_settings CybersynModSettings
|
|
local function tick_dispatch(map_data, mod_settings)
|
|
--we do not dispatch more than one train per tick
|
|
--psuedo-randomize what item (and what station) to check first so if trains available is low they choose orders psuedo-randomly
|
|
--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
|
|
--NOTE: the above isn't even the full story, we can only use one edge per item per tick, which might break the assumptions of the simplex algorithm causing it to give imperfect solutions.
|
|
|
|
local all_r_stations = map_data.economy.all_r_stations
|
|
local all_p_stations = map_data.economy.all_p_stations
|
|
local all_names = map_data.economy.all_names
|
|
local stations = map_data.stations
|
|
|
|
local r_stations
|
|
local p_stations
|
|
local item_name
|
|
local item_type
|
|
while true do
|
|
local size = #all_names
|
|
if size == 0 then
|
|
map_data.tick_state = STATE_INIT
|
|
return true
|
|
end
|
|
|
|
--randomizing the ordering should only matter if we run out of available trains
|
|
local name_i = size <= 2 and 2 or 2*random(size/2)
|
|
|
|
local item_network_name = all_names[name_i - 1]--[[@as string]]
|
|
local signal = all_names[name_i]--[[@as SignalID]]
|
|
|
|
--swap remove
|
|
all_names[name_i - 1] = all_names[size - 1]
|
|
all_names[name_i] = all_names[size]
|
|
all_names[size] = nil
|
|
all_names[size - 1] = nil
|
|
|
|
r_stations = all_r_stations[item_network_name]
|
|
p_stations = all_p_stations[item_network_name]
|
|
if r_stations then
|
|
if p_stations then
|
|
item_name = signal.name--[[@as string]]
|
|
item_type = signal.type
|
|
break
|
|
else
|
|
for i, id in ipairs(r_stations) do
|
|
local station = stations[id]
|
|
if station then
|
|
station.display_failed_request = true
|
|
station.display_update = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local max_threshold = INF
|
|
while true do
|
|
local r_station_i = nil
|
|
local r_threshold = nil
|
|
local best_prior = -INF
|
|
local best_lru = INF
|
|
for i, id in ipairs(r_stations) do
|
|
local station = stations[id]
|
|
--NOTE: the station at r_station_id could have been deleted and reregistered since last poll, this check here prevents it from being processed for a delivery in that case
|
|
if station and station.deliveries_total < station.entity_stop.trains_limit then
|
|
local threshold = station.p_count_or_r_threshold_per_item[item_name]
|
|
if threshold <= max_threshold and (station.priority > best_prior or (station.priority == best_prior and station.last_delivery_tick < best_lru)) then
|
|
r_station_i = i
|
|
r_threshold = threshold
|
|
best_prior = station.priority
|
|
best_lru = station.last_delivery_tick
|
|
end
|
|
end
|
|
end
|
|
if not r_station_i then
|
|
return false
|
|
end
|
|
|
|
local r_station_id = r_stations[r_station_i]
|
|
local r_station = stations[r_station_id]
|
|
|
|
max_threshold = 0
|
|
local best_i = 0
|
|
local best_train = nil
|
|
local best_dist = INF
|
|
local best_prior = -INF
|
|
local can_be_serviced = false
|
|
for j, p_station_id in ipairs(p_stations) do
|
|
local p_station = stations[p_station_id]
|
|
if p_station and p_station.deliveries_total < p_station.entity_stop.trains_limit then
|
|
local effective_count = p_station.p_count_or_r_threshold_per_item[item_name]
|
|
if effective_count >= r_threshold then
|
|
local prior = p_station.priority
|
|
local slot_threshold = item_type == "fluid" and r_threshold or ceil(r_threshold/get_stack_size(map_data, item_name))
|
|
local train, d = get_valid_train(map_data, r_station_id, p_station_id, item_type, slot_threshold)
|
|
if prior > best_prior or (prior == best_prior and d < best_dist) then
|
|
if train then
|
|
best_i = j
|
|
best_dist = d
|
|
best_train = train
|
|
best_prior = prior
|
|
can_be_serviced = true
|
|
elseif d < INF then
|
|
best_i = j
|
|
can_be_serviced = true
|
|
end
|
|
end
|
|
end
|
|
if effective_count > max_threshold then
|
|
max_threshold = effective_count
|
|
end
|
|
end
|
|
end
|
|
if best_train then
|
|
send_train_between(map_data, r_station_id, table_remove(p_stations, best_i), best_train, item_name)
|
|
return false
|
|
else
|
|
if can_be_serviced then
|
|
send_missing_train_alert_for_stops(r_station.entity_stop, stations[p_stations[best_i]].entity_stop)
|
|
end
|
|
r_station.display_failed_request = true
|
|
r_station.display_update = true
|
|
end
|
|
|
|
table_remove(r_stations, r_station_i)
|
|
end
|
|
end
|
|
---@param map_data MapData
|
|
---@param mod_settings CybersynModSettings
|
|
function tick(map_data, mod_settings)
|
|
map_data.total_ticks = map_data.total_ticks + 1
|
|
if map_data.tick_state == STATE_INIT then
|
|
map_data.economy.all_p_stations = {}
|
|
map_data.economy.all_r_stations = {}
|
|
map_data.economy.all_names = {}
|
|
map_data.tick_state = STATE_POLL_STATIONS
|
|
for i, id in pairs(map_data.warmup_station_ids) do
|
|
local station = map_data.stations[id]
|
|
if station then
|
|
if station.last_delivery_tick + mod_settings.warmup_time*mod_settings.tps < map_data.total_ticks then
|
|
map_data.active_station_ids[#map_data.active_station_ids + 1] = id
|
|
map_data.warmup_station_ids[i] = nil
|
|
end
|
|
else
|
|
map_data.warmup_station_ids[i] = nil
|
|
end
|
|
end
|
|
tick_poll_train(map_data, mod_settings)
|
|
tick_poll_comb(map_data)
|
|
end
|
|
|
|
if map_data.tick_state == STATE_POLL_STATIONS then
|
|
for i = 1, mod_settings.update_rate do
|
|
if tick_poll_station(map_data, mod_settings) then break end
|
|
end
|
|
elseif map_data.tick_state == STATE_DISPATCH then
|
|
for i = 1, mod_settings.update_rate do
|
|
tick_dispatch(map_data, mod_settings)
|
|
end
|
|
end
|
|
end
|