Files
project-cybersyn/cybersyn/scripts/layout.lua
2023-10-01 17:35:39 -07:00

773 lines
24 KiB
Lua

--By Mami
local area = require("__flib__.bounding-box")
local abs = math.abs
local floor = math.floor
local ceil = math.ceil
local min = math.min
local max = math.max
local bit_extract = bit32.extract
local defines_front = defines.rail_direction.front
local defines_back = defines.rail_direction.back
local defines_straight = defines.rail_connection_direction.straight
local search_type = {"straight-rail", "curved-rail"}
---@param layout_pattern (0|1|2|3)[]
---@param layout (0|1|2)[]
function is_refuel_layout_accepted(layout_pattern, layout)
local valid = true
for i, v in ipairs(layout) do
local p = layout_pattern[i] or 0
if (v == 1 and (p == 1 or p == 3)) or (v == 2 and (p == 2 or p == 3)) then
valid = false
break
end
end
if valid or not layout[0] then return valid end
for i, v in irpairs(layout) do
local p = layout_pattern[i] or 0
if (v == 1 and (p == 1 or p == 3)) or (v == 2 and (p == 2 or p == 3)) then
valid = false
break
end
end
return valid
end
---@param layout_pattern (0|1|2|3)[]
---@param layout (0|1|2)[]
function is_layout_accepted(layout_pattern, layout)
local valid = true
for i, v in ipairs(layout) do
local p = layout_pattern[i] or 0
if (v == 1 and not (p == 1 or p == 3)) or (v == 2 and not (p == 2 or p == 3)) then
valid = false
break
end
end
if valid or not layout[0] then return valid end
for i, v in irpairs(layout) do
local p = layout_pattern[i] or 0
if (v == 1 and not (p == 1 or p == 3)) or (v == 2 and not (p == 2 or p == 3)) then
valid = false
break
end
end
return valid
end
---@param map_data MapData
---@param train_id uint
---@param train Train
function remove_train(map_data, train_id, train)
if train.manifest then
on_failed_delivery(map_data, train_id, train)
end
remove_available_train(map_data, train_id, train)
local layout_id = train.layout_id
local count = global.layout_train_count[layout_id]
if count <= 1 then
global.layout_train_count[layout_id] = nil
global.layouts[layout_id] = nil
for _, stop in pairs(global.stations) do
stop.accepted_layouts[layout_id] = nil
end
for _, stop in pairs(global.refuelers) do
stop.accepted_layouts[layout_id] = nil
end
else
global.layout_train_count[layout_id] = count - 1
end
map_data.trains[train_id] = nil
interface_raise_train_removed(train_id, train)
end
---@param map_data MapData
---@param train Train
function set_train_layout(map_data, train)
local carriages = train.entity.carriages
local layout = {}
local i = 1
local item_slot_capacity = 0
local fluid_capacity = 0
for _, carriage in pairs(carriages) do
if carriage.type == "cargo-wagon" then
layout[#layout + 1] = 1
local inv = carriage.get_inventory(defines.inventory.cargo_wagon)
item_slot_capacity = item_slot_capacity + #inv
elseif carriage.type == "fluid-wagon" then
layout[#layout + 1] = 2
fluid_capacity = fluid_capacity + carriage.prototype.fluid_capacity
else
layout[#layout + 1] = 0
end
i = i + 1
end
local back_movers = train.entity.locomotives["back_movers"]
if #back_movers > 0 then
--mark the layout as reversible
layout[0] = true
end
local layout_id = 0
for id, cur_layout in pairs(map_data.layouts) do
if table_compare(layout, cur_layout) then
layout = cur_layout
layout_id = id
break
end
end
if layout_id == 0 then
--define new layout
layout_id = map_data.layout_top_id
map_data.layout_top_id = map_data.layout_top_id + 1
map_data.layouts[layout_id] = layout
map_data.layout_train_count[layout_id] = 1
for _, stop in pairs(map_data.stations) do
if stop.layout_pattern then
stop.accepted_layouts[layout_id] = is_layout_accepted(stop.layout_pattern, layout) or nil
end
end
for _, stop in pairs(map_data.refuelers) do
if stop.layout_pattern then
stop.accepted_layouts[layout_id] = is_refuel_layout_accepted(stop.layout_pattern, layout) or nil
end
end
else
map_data.layout_train_count[layout_id] = map_data.layout_train_count[layout_id] + 1
end
train.layout_id = layout_id
train.item_slot_capacity = item_slot_capacity
train.fluid_capacity = fluid_capacity
end
---@param stop LuaEntity
---@param train LuaTrain
local function get_train_direction(stop, train)
local back_rail = train.back_rail
if back_rail then
local back_pos = back_rail.position
local stop_pos = stop.position
if abs(back_pos.x - stop_pos.x) < 3 and abs(back_pos.y - stop_pos.y) < 3 then
return true
end
end
return false
end
---@param map_data MapData
---@param station Station
---@param train Train
function set_p_wagon_combs(map_data, station, train)
if not station.wagon_combs or not next(station.wagon_combs) then return end
local carriages = train.entity.carriages
local manifest = train.manifest--[[@as Manifest]]
if not manifest[1] then return end
local sign = mod_settings.invert_sign and 1 or -1
local is_reversed = get_train_direction(station.entity_stop, train.entity)
local locked_slots = station.locked_slots
local percent_slots_to_use_per_wagon = 1.0
if train.item_slot_capacity > 0 then
local total_item_slots
if locked_slots > 0 then
local total_cargo_wagons = #train.entity.cargo_wagons
total_item_slots = max(train.item_slot_capacity - total_cargo_wagons*locked_slots, 1)
else
total_item_slots = train.item_slot_capacity
end
local to_be_used_item_slots = 0
for i, item in ipairs(train.manifest) do
if item.type == "item" then
to_be_used_item_slots = to_be_used_item_slots + ceil(item.count/get_stack_size(map_data, item.name))
end
end
percent_slots_to_use_per_wagon = min(to_be_used_item_slots/total_item_slots, 1.0)
end
local item_i = 1
local item = manifest[item_i]
local item_count = item.count
local fluid_i = 1
local fluid = manifest[fluid_i]
local fluid_count = fluid.count
local ivpairs = is_reversed and irpairs or ipairs
for carriage_i, carriage in ivpairs(carriages) do
--NOTE: we are not checking valid
---@type LuaEntity?
local comb = station.wagon_combs[carriage_i]
if comb and not comb.valid then
comb = nil
station.wagon_combs[carriage_i] = nil
if next(station.wagon_combs) == nil then
station.wagon_combs = nil
break
end
end
if carriage.type == "cargo-wagon" then
local inv = carriage.get_inventory(defines.inventory.cargo_wagon)
if inv then
---@type ConstantCombinatorParameters
local signals = {}
local inv_filter_i = 1
local item_slots_capacity = max(ceil((#inv - locked_slots)*percent_slots_to_use_per_wagon), 1)
while item_slots_capacity > 0 and item_i <= #manifest do
local do_inc
if item.type == "item" then
local stack_size = get_stack_size(map_data, item.name)
local i = #signals + 1
local count_to_fill = min(item_slots_capacity*stack_size, item_count)
local slots_to_fill = ceil(count_to_fill/stack_size)
signals[i] = {index = i, signal = {type = item.type, name = item.name}, count = sign*count_to_fill}
item_count = item_count - count_to_fill
item_slots_capacity = item_slots_capacity - slots_to_fill
if comb then
for j = 1, slots_to_fill do
inv.set_filter(inv_filter_i, item.name)
inv_filter_i = inv_filter_i + 1
end
train.has_filtered_wagon = true
end
do_inc = item_count == 0
else
do_inc = true
end
if do_inc then
item_i = item_i + 1
if item_i <= #manifest then
item = manifest[item_i]
item_count = item.count
else
break
end
end
end
if comb then
if bit_extract(get_comb_params(comb).second_constant, SETTING_ENABLE_SLOT_BARRING) > 0 then
inv.set_bar(inv_filter_i--[[@as uint]])
train.has_filtered_wagon = true
end
set_combinator_output(map_data, comb, signals)
end
end
elseif carriage.type == "fluid-wagon" then
local fluid_capacity = carriage.prototype.fluid_capacity
local signals = {}
while fluid_capacity > 0 and fluid_i <= #manifest do
local do_inc
if fluid.type == "fluid" then
local count_to_fill = min(fluid_count, fluid_capacity)
signals[1] = {index = 1, signal = {type = fluid.type, name = fluid.name}, count = sign*count_to_fill}
fluid_count = fluid_count - count_to_fill
fluid_capacity = 0
do_inc = fluid_count == 0
else
do_inc = true
end
if do_inc then
fluid_i = fluid_i + 1
if fluid_i <= #manifest then
fluid = manifest[fluid_i]
fluid_count = fluid.count
end
end
end
if comb then
set_combinator_output(map_data, comb, signals)
end
end
end
end
---@param map_data MapData
---@param station Station
---@param train Train
function set_r_wagon_combs(map_data, station, train)
if not station.wagon_combs then return end
local carriages = train.entity.carriages
local is_reversed = get_train_direction(station.entity_stop, train.entity)
local sign = mod_settings.invert_sign and -1 or 1
local ivpairs = is_reversed and irpairs or ipairs
for carriage_i, carriage in ivpairs(carriages) do
---@type LuaEntity?
local comb = station.wagon_combs[carriage_i]
if comb and not comb.valid then
comb = nil
station.wagon_combs[carriage_i] = nil
if next(station.wagon_combs) == nil then
station.wagon_combs = nil
break
end
end
if comb and carriage.type == "cargo-wagon" then
local inv = carriage.get_inventory(defines.inventory.cargo_wagon)
if inv then
local signals = {}
for stack_i = 1, #inv do
local stack = inv[stack_i]
if stack.valid_for_read then
local i = #signals + 1
signals[i] = {index = i, signal = {type = "item", name = stack.name}, count = sign*stack.count}
end
end
set_combinator_output(map_data, comb, signals)
end
elseif comb and carriage.type == "fluid-wagon" then
local signals = {}
local inv = carriage.get_fluid_contents()
for fluid_name, count in pairs(inv) do
local i = #signals + 1
signals[i] = {index = i, signal = {type = "fluid", name = fluid_name}, count = sign*floor(count)}
end
set_combinator_output(map_data, comb, signals)
end
end
end
---@param map_data MapData
---@param refueler Refueler
---@param train Train
function set_refueler_combs(map_data, refueler, train)
if not refueler.wagon_combs then return end
local carriages = train.entity.carriages
local signals = {}
local is_reversed = get_train_direction(refueler.entity_stop, train.entity)
local ivpairs = is_reversed and irpairs or ipairs
for carriage_i, carriage in ivpairs(carriages) do
---@type LuaEntity?
local comb = refueler.wagon_combs[carriage_i]
if comb and not comb.valid then
comb = nil
refueler.wagon_combs[carriage_i] = nil
if next(refueler.wagon_combs) == nil then
refueler.wagon_combs = nil
break
end
end
local inv = carriage.get_fuel_inventory()
if inv then
local wagon_signals
if comb then
wagon_signals = {}
local array = carriage.prototype.items_to_place_this
if array then
local a = array[1]
local name
if type(a) == "string" then
name = a
else
name = a.name
end
if game.item_prototypes[name] then
wagon_signals[1] = {index = 1, signal = {type = "item", name = a.name}, count = 1}
end
end
end
for stack_i = 1, #inv do
local stack = inv[stack_i]
if stack.valid_for_read then
if comb then
local i = #wagon_signals + 1
wagon_signals[i] = {index = i, signal = {type = "item", name = stack.name}, count = stack.count}
end
local j = #signals + 1
signals[j] = {index = j, signal = {type = "item", name = stack.name}, count = stack.count}
end
end
if comb then
set_combinator_output(map_data, comb, wagon_signals)
end
end
end
set_combinator_output(map_data, refueler.entity_comb, signals)
end
---@param map_data MapData
---@param stop Station|Refueler
function unset_wagon_combs(map_data, stop)
if not stop.wagon_combs then return end
for i, comb in pairs(stop.wagon_combs) do
if comb.valid then
set_combinator_output(map_data, comb, nil)
else
stop.wagon_combs[i] = nil
end
end
if next(stop.wagon_combs) == nil then
stop.wagon_combs = nil
end
end
local type_filter = {"inserter", "pump", "arithmetic-combinator", "loader-1x1", "loader"}
---@param map_data MapData
---@param stop Station|Refueler
---@param is_station_or_refueler boolean
---@param forbidden_entity LuaEntity?
function reset_stop_layout(map_data, stop, is_station_or_refueler, forbidden_entity)
--NOTE: station must be in auto mode
local stop_rail = stop.entity_stop.connected_rail
if stop_rail == nil then
--cannot accept deliveries
stop.layout_pattern = nil
stop.accepted_layouts = {}
return
end
local rail_direction_from_stop
if stop.entity_stop.connected_rail_direction == defines_front then
rail_direction_from_stop = defines_back
else
rail_direction_from_stop = defines_front
end
local stop_direction = stop.entity_stop.direction
local surface = stop.entity_stop.surface
local middle_x = stop_rail.position.x
local middle_y = stop_rail.position.y
local reach = LONGEST_INSERTER_REACH + 1
local search_area
local area_delta
local is_ver
if stop_direction == defines.direction.north then
search_area = {{middle_x - reach, middle_y}, {middle_x + reach, middle_y + 6}}
area_delta = {0, 7}
is_ver = true
elseif stop_direction == defines.direction.east then
search_area = {{middle_x - 6, middle_y - reach}, {middle_x, middle_y + reach}}
area_delta = {-7, 0}
is_ver = false
elseif stop_direction == defines.direction.south then
search_area = {{middle_x - reach, middle_y - 6}, {middle_x + reach, middle_y}}
area_delta = {0, -7}
is_ver = true
elseif stop_direction == defines.direction.west then
search_area = {{middle_x, middle_y - reach}, {middle_x + 6, middle_y + reach}}
area_delta = {7, 0}
is_ver = false
else
assert(false, "cybersyn: invalid stop direction")
end
local length = 1
---@type LuaEntity?
local pre_rail = stop_rail
local layout_pattern = {0}
local wagon_number = 0
for i = 1, 112 do
if pre_rail then
local rail, rail_direction, rail_connection_direction = pre_rail.get_connected_rail({rail_direction = rail_direction_from_stop, rail_connection_direction = defines_straight})
if not rail or rail_connection_direction ~= defines_straight then
-- There is a curved rail or break in the tracks at this point
-- We are assuming it's a curved rail, maybe that's a bad assumption
-- We stop searching to expand the allow list after we see a curved rail
-- We are allowing up to 3 tiles of extra allow list usage on a curved rail
length = length + 3
pre_rail = nil
else
pre_rail = rail
length = length + 2
end
end
if length >= 6 or not pre_rail then
if not pre_rail then
if length <= 0 then
-- No point searching nothing
-- Once we hit a curve and process the 3 extra tiles we break here
-- This is the only breakpoint in this for loop
break
end
-- Minimize the search_area to include only the straight section of track and the 3 tiles of the curved rail
local missing_rail_length = 6 - length
if missing_rail_length > 0 then
if stop_direction == defines.direction.north then
search_area[2][2] = search_area[2][2] - missing_rail_length
elseif stop_direction == defines.direction.east then
search_area[1][1] = search_area[1][1] + missing_rail_length
elseif stop_direction == defines.direction.south then
search_area[1][2] = search_area[1][2] + missing_rail_length
else
search_area[2][1] = search_area[2][1] - missing_rail_length
end
end
end
length = length - 7
wagon_number = wagon_number + 1
local supports_cargo = false
local supports_fluid = false
local entities = surface.find_entities_filtered({
area = search_area,
type = type_filter,
})
for _, entity in pairs(entities) do
if entity ~= forbidden_entity then
if entity.type == "inserter" then
if not supports_cargo then
local pos = entity.pickup_position
local is_there
if is_ver then
is_there = middle_x - 1 <= pos.x and pos.x <= middle_x + 1
else
is_there = middle_y - 1 <= pos.y and pos.y <= middle_y + 1
end
if is_there then
supports_cargo = true
else
pos = entity.drop_position
if is_ver then
is_there = middle_x - 1 <= pos.x and pos.x <= middle_x + 1
else
is_there = middle_y - 1 <= pos.y and pos.y <= middle_y + 1
end
if is_there then
supports_cargo = true
end
end
end
elseif entity.type == "loader-1x1" or entity.type == "loader" then
if not supports_cargo then
local direction = entity.direction
if is_ver then
if direction == defines.direction.east or defines.direction.west then
supports_cargo = true
end
elseif direction == defines.direction.north or direction == defines.direction.south then
supports_cargo = true
end
end
elseif entity.type == "pump" then
if not supports_fluid and entity.pump_rail_target then
local direction = entity.direction
if is_ver then
if direction == defines.direction.east or direction == defines.direction.west then
supports_fluid = true
end
elseif direction == defines.direction.north or direction == defines.direction.south then
supports_fluid = true
end
end
elseif entity.name == COMBINATOR_NAME then
local param = map_data.to_comb_params[entity.unit_number]
if param.operation == MODE_WAGON then
local pos = entity.position
local is_there
if is_ver then
is_there = middle_x - 2.1 <= pos.x and pos.x <= middle_x + 2.1
else
is_there = middle_y - 2.1 <= pos.y and pos.y <= middle_y + 2.1
end
if is_there then
if not stop.wagon_combs then
stop.wagon_combs = {}
end
stop.wagon_combs[wagon_number] = entity
end
end
end
end
end
if supports_cargo then
if supports_fluid then
layout_pattern[wagon_number] = 3
else
layout_pattern[wagon_number] = 1
end
elseif supports_fluid then
layout_pattern[wagon_number] = 2
else
--layout_pattern[wagon_number] = nil
end
search_area = area.move(search_area, area_delta)
end
end
stop.layout_pattern = layout_pattern
if is_station_or_refueler then
for id, layout in pairs(map_data.layouts) do
stop.accepted_layouts[id] = is_layout_accepted(layout_pattern, layout) or nil
end
else
for id, layout in pairs(map_data.layouts) do
stop.accepted_layouts[id] = is_refuel_layout_accepted(layout_pattern, layout) or nil
end
end
end
---@param map_data MapData
---@param stop Station|Refueler
---@param is_station_or_refueler boolean
---@param forbidden_entity LuaEntity?
function update_stop_if_auto(map_data, stop, is_station_or_refueler, forbidden_entity)
if not stop.allows_all_trains then
reset_stop_layout(map_data, stop, is_station_or_refueler, forbidden_entity)
end
end
---@param map_data MapData
---@param entity LuaEntity
---@param forbidden_entity LuaEntity?
---@param force boolean?
local function resolve_update_stop_from_rail(map_data, entity, forbidden_entity, force)
local id = entity.unit_number--[[@as uint]]
local is_station = true
---@type Station|Refueler
local stop = map_data.stations[id]
if not stop then
stop = map_data.refuelers[id]
is_station = false
end
if stop and stop.entity_stop.valid then
if force then
reset_stop_layout(map_data, stop, is_station, forbidden_entity)
elseif not stop.allows_all_trains then
reset_stop_layout(map_data, stop, is_station, forbidden_entity)
end
end
end
---@param map_data MapData
---@param rail LuaEntity
---@param forbidden_entity LuaEntity?
---@param force boolean?
function update_stop_from_rail(map_data, rail, forbidden_entity, force)
--NOTE: is this a correct way to figure out the direction?
---@type LuaEntity?
local rail_front = rail
---@type LuaEntity?
local rail_back = rail
---@type defines.rail_direction
for i = 1, 112 do
if rail_back then
local entity = rail_back.get_rail_segment_entity(defines_back, false)
if entity and entity.name == "train-stop" then
resolve_update_stop_from_rail(map_data, entity, forbidden_entity, force)
return
end
rail_back = rail_back.get_connected_rail({rail_direction = defines_back, rail_connection_direction = defines_straight})
end
if rail_front then
local entity = rail_front.get_rail_segment_entity(defines_front, false)
if entity and entity.name == "train-stop" then
resolve_update_stop_from_rail(map_data, entity, forbidden_entity, force)
return
end
rail_front = rail_front.get_connected_rail({rail_direction = defines_front, rail_connection_direction = defines_straight})
end
end
end
---@param map_data MapData
---@param pump LuaEntity
---@param forbidden_entity LuaEntity?
function update_stop_from_pump(map_data, pump, forbidden_entity)
if pump.pump_rail_target then
update_stop_from_rail(map_data, pump.pump_rail_target, forbidden_entity)
end
end
---@param map_data MapData
---@param inserter LuaEntity
---@param forbidden_entity LuaEntity?
function update_stop_from_inserter(map_data, inserter, forbidden_entity)
local surface = inserter.surface
local pos0 = inserter.position
local pos1 = inserter.pickup_position
local pos2 = inserter.drop_position
local has_found = false
local rails = surface.find_entities_filtered({
type = search_type,
position = pos1,
})
if rails[1] then
update_stop_from_rail(map_data, rails[1], forbidden_entity)
has_found = true
end
rails = surface.find_entities_filtered({
type = search_type,
position = pos2,
})
if rails[1] then
update_stop_from_rail(map_data, rails[1], forbidden_entity)
has_found = true
end
if has_found then
return
end
-- We need to check secondary positions because of weird modded inserters.
-- Mostly because of miniloaders not aligning with the hitbox of a rail by default.
pos1.x = pos1.x + 0.2*(pos1.x - pos0.x)
pos1.y = pos1.y + 0.2*(pos1.y - pos0.y)
pos2.x = pos2.x + 0.2*(pos2.x - pos0.x)
pos2.y = pos2.y + 0.2*(pos2.y - pos0.y)
rails = surface.find_entities_filtered({
type = search_type,
position = pos1,
})
if rails[1] then
update_stop_from_rail(map_data, rails[1], forbidden_entity)
end
rails = surface.find_entities_filtered({
type = search_type,
position = pos2,
})
if rails[1] then
update_stop_from_rail(map_data, rails[1], forbidden_entity)
end
end
---@param map_data MapData
---@param loader LuaEntity
---@param forbidden_entity LuaEntity?
function update_stop_from_loader(map_data, loader, forbidden_entity)
local surface = loader.surface
local direction = loader.direction
local loader_type = loader.loader_type
local position = loader.position
--check input/output direction and loader position, and case position and modify x or y by +/- 1 for search
if loader_type == "input" then --loading train
if direction == defines.direction.east then
position.x = position.x + 1 -- input and facing east -> move on X axis 1 to the right
elseif direction == defines.direction.south then
position.y = position.y + 1 -- input and facing south -> move on Y axis down 1 unit
elseif direction == defines.direction.west then
position.x = position.x - 1 -- input and facing west -> move on X axis 1 to the left
elseif direction == defines.direction.north then
position.y = position.y - 1 -- input and facing south -> move on Y axis up 1 unit
end
elseif loader_type == "output" then --unloading train
if direction == defines.direction.east then
position.x = position.x - 1 -- output and facing east -> move on X axis 1 to the left
elseif direction == defines.direction.south then
position.y = position.y - 1 -- output and facing south -> move on Y axis up 1 unit
elseif direction == defines.direction.west then
position.x = position.x + 1 -- output and facing west -> move on X axis 1 to the right
elseif direction == defines.direction.north then
position.y = position.y + 1 -- output and facing south -> move on Y axis down 1 unit
end
end
local rails = surface.find_entities_filtered({
type = search_type,
position = position,
})
if rails[1] then
update_stop_from_rail(map_data, rails[1], forbidden_entity)
end
end