Files
factorio-learning-environment/fle/env/tools/agent/connect_entities/server.lua

1323 lines
53 KiB
Lua

-- connect_entities
local MAX_SERIALIZATION_ITERATIONS = 1000 -- Maximum iterations for serializing belt groups
local MAX_PLACEMENT_ATTEMPTS = 50 -- Maximum attempts to find placeable positions
local MAX_PATH_LENGTH = 1000 -- Maximum number of path points to process
local MAX_STRAIGHT_SECTIONS = 100
local MAX_UNDERGROUND_SEGMENTS = 50
-- Wire reach values for different electric pole types
local wire_reach = {
['small-electric-pole'] = 4,
['medium-electric-pole'] = 9,
['big-electric-pole'] = 30,
['substation'] = 18
}
local default_connect_types = {
['pipe-to-ground'] = 'pipe',
['underground-belt'] = 'transport-belt',
['express-underground-belt'] = 'express-transport-belt',
['fast-underground-belt'] = 'fast-underground-belt',
}
local underground_ranges = {
['pipe-to-ground'] = 8,
['underground-belt'] = 4,
['fast-underground-belt'] = 4,
['express-underground-belt'] = 4
}
local function is_within_pole_range(position, pole)
local dx = position.x - pole.position.x
local dy = position.y - pole.position.y
local wire_reach = wire_reach[pole.name] or 4
return (dx * dx + dy * dy) <= wire_reach * wire_reach
end
local function get_electric_network_at_position(position)
-- Get all electric poles near the position
local nearby_poles = game.surfaces[1].find_entities_filtered{
position = position,
radius = 9, -- Maximum pole reach is 9 for medium poles
type = "electric-pole"
}
-- Check if any pole's wire reaches this position
for _, pole in pairs(nearby_poles) do
if is_within_pole_range(position, pole) then
return pole.electric_network_id
end
end
return nil
end
local function are_positions_in_same_network(pos1, pos2)
local network1 = get_electric_network_at_position(pos1)
local network2 = get_electric_network_at_position(pos2)
return network1 and network2 and network1 == network2
end
local function is_position_saturated(position, reach)
-- Get nearby poles
local nearby_poles = game.surfaces[1].find_entities_filtered{
position = position,
radius = 9,
type = "electric-pole"
}
-- Check each corner of a 1x1 tile centered on the position
local corners = {
{x = position.x - reach/2, y = position.y - reach/2},
{x = position.x - reach/2, y = position.y + reach/2},
{x = position.x + reach/2, y = position.y + reach/2},
{x = position.x + reach/2, y = position.y - reach/2}
}
-- For each corner, check if it's within range of any existing pole
for _, corner in pairs(corners) do
local corner_covered = false
for _, pole in pairs(nearby_poles) do
if is_within_pole_range(corner, pole) then
corner_covered = true
break
end
end
if not corner_covered then
return false -- Found an uncovered corner
end
end
return true -- All corners are covered
end
function get_step_size(connection_type)
return wire_reach[connection_type] or 1
end
function math.round(x)
return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)
end
local function has_valid_fluidbox(entity)
return entity.fluidbox and #entity.fluidbox > 0 and entity.fluidbox[1] and entity.fluidbox[1].get_fluid_system_id
end
local function are_fluidboxes_connected(entity1, entity2)
if has_valid_fluidbox(entity1) and has_valid_fluidbox(entity2) then
return entity1.fluidbox[1].get_fluid_system_id() == entity2.fluidbox[1].get_fluid_system_id()
end
return false
end
-- Calculate maximum possible underground sections based on inventory
local function calculate_max_underground_sections(player, underground_type)
local available_count = 0
for _, inv in pairs({defines.inventory.character_main}) do
if player.get_inventory(inv) then
available_count = available_count + player.get_inventory(inv).get_item_count(underground_type)
end
end
-- Each underground section requires 2 entities (entrance and exit)
return math.floor(available_count / 2)
end
-- Helper function to check if an entity type is an underground variant
local function is_underground_type(entity_type)
return underground_ranges[entity_type] ~= nil
end
-- Modified split_section_into_underground_segments to respect inventory limits
local function split_section_into_underground_segments(section, path, range, max_segments)
local segments = {}
-- Add margin of 1 at start and end of section
local effective_start = section.start_index + 1
local effective_end = section.end_index - 1
-- Check if section is still long enough after adding margins
if effective_end - effective_start < 2 then
return segments -- Return empty segments if section is too short
end
local current_start = effective_start
local segment_count = 0
local iteration_count = 0
local MAX_ITERATIONS = math.min(section.length * 2, MAX_UNDERGROUND_SEGMENTS) -- Prevent excessive iterations
while current_start + 1 < effective_end and segment_count < max_segments do
iteration_count = iteration_count + 1
if iteration_count > MAX_ITERATIONS then
-- game.print("Warning: Maximum iterations reached while splitting underground segments")
break
end
-- Calculate end index for this segment
local end_index = math.min(current_start + range, effective_end)
-- Only create segment if there's at least 1 tile gap between entrance and exit
if end_index > current_start then
table.insert(segments, {
entrance_index = current_start,
exit_index = end_index,
direction = section.direction
})
segment_count = segment_count + 1
-- Move to position after the exit for next iteration
current_start = end_index + 1
else
-- If we can't create a valid segment, move forward
current_start = current_start + 1
end
end
return segments
end
-- Find straight sections in path suitable for underground entities
local function find_straight_sections(path, min_length)
local sections = {}
local current_section = {
start_index = 1,
direction = nil,
length = 0
}
for i = 1, #path - 1 do --Always start slightly into the path, and away from the end to prevent connection issues.
local current_pos = path[i].position
local next_pos = path[i + 1].position
local dx = next_pos.x - current_pos.x
local dy = next_pos.y - current_pos.y
local current_direction = {dx = dx, dy = dy}
-- Normalize direction
if math.abs(dx) > math.abs(dy) then
current_direction = {dx = dx/math.abs(dx), dy = 0}
else
current_direction = {dx = 0, dy = dy/math.abs(dy)}
end
if not current_section.direction then
current_section.direction = current_direction
end
-- Check if we're still going in the same direction
if current_section.direction.dx == current_direction.dx and
current_section.direction.dy == current_direction.dy then
current_section.length = current_section.length + 1
else
-- Direction changed, check if previous section was long enough
if current_section.length >= min_length then
table.insert(sections, {
start_index = current_section.start_index,
end_index = i,
direction = current_section.direction,
length = current_section.length
})
end
-- Start new section
current_section = {
start_index = i,
direction = current_direction,
length = 1
}
end
end
-- Check final section
if current_section.length >= min_length then
table.insert(sections, {
start_index = current_section.start_index,
end_index = #path,
direction = current_section.direction,
length = current_section.length
})
end
return sections
end
-- Helper function to serialize a belt group
local function serialize_belt_group(entity)
if not entity or not entity.valid or entity.type ~= "transport-belt" then
return nil
end
local serialized = {}
local seen = {}
local iteration_count = 0
local function get_connected_belt_entities(belt, is_output)
local connected_entities = {}
local seen_owners = {}
-- Get connected lines
local connected_lines = {}
if is_output then
if #belt.belt_neighbours['outputs'] then
for _, line in pairs(belt.belt_neighbours['outputs']) do
table.insert(connected_lines, line)
end
end
else
if #belt.belt_neighbours['inputs'] then
for _, line in pairs(belt.belt_neighbours['inputs']) do
table.insert(connected_lines, line)
end
end
end
-- game.print("connected lines "..#connected_lines)
-- Convert lines to unique belt entities
for _, line in pairs(connected_lines) do
if line and line.valid and not seen_owners[line.unit_number] then
seen_owners[line.unit_number] = true
table.insert(connected_entities, line)
end
end
-- game.print("connected entities "..#connected_entities)
return connected_entities
end
local function serialize_connected_belts(belt, is_output)
iteration_count = iteration_count + 1
if iteration_count > MAX_SERIALIZATION_ITERATIONS then
-- game.print("Warning: Belt serialization reached iteration limit")
return
end
if not belt or not belt.valid or seen[belt.unit_number] then
return
end
seen[belt.unit_number] = true
local belt_data = global.utils.serialize_entity(belt)
table.insert(serialized, belt_data)
-- Get connected belt entities
local next_belts = get_connected_belt_entities(belt, is_output)
for _, connected_belt in pairs(next_belts) do
if connected_belt.valid and not seen[connected_belt.unit_number] then
serialize_connected_belts(connected_belt, is_output)
end
end
end
-- Start serialization from the given entity
serialize_connected_belts(entity, false) -- Follow input direction
serialize_connected_belts(entity, true) -- Follow output direction
return serialized
end
local function are_poles_connected(entity1, entity2)
if not (entity1 and entity2) then return false end
if not (entity1.electric_network_id and entity2.electric_network_id) then return false end
return entity1.electric_network_id == entity2.electric_network_id
end
local function is_placeable(position)
local invalid_tiles = {
["water"] = true,
["deepwater"] = true,
["water-green"] = true,
["deepwater-green"] = true,
["water-shallow"] = true,
["water-mud"] = true,
}
local entities = game.surfaces[1].find_entities_filtered{
position = position,
collision_mask = "player-layer"
}
if #entities == 1 then
if entities[1].name == "character" then
return not invalid_tiles[game.surfaces[1].get_tile(position.x, position.y).name]
end
end
return #entities == 0 and not invalid_tiles[game.surfaces[1].get_tile(position.x, position.y).name]
end
local function find_placeable_neighbor(pos, previous_pos)
local directions = {
{dx = 0, dy = -1}, -- up
{dx = 1, dy = 0}, -- right
{dx = 0, dy = 1}, -- down
{dx = -1, dy = 0} -- left
}
if previous_pos then
local desired_dx = pos.x - previous_pos.x
local desired_dy = pos.y - previous_pos.y
table.sort(directions, function(a, b)
local a_score = math.abs(a.dx - desired_dx) + math.abs(a.dy - desired_dy)
local b_score = math.abs(b.dx - desired_dx) + math.abs(b.dy - desired_dy)
return a_score < b_score
end)
end
for _, dir in ipairs(directions) do
local test_pos = {x = pos.x + dir.dx, y = pos.y + dir.dy}
if is_placeable(test_pos) then
return test_pos
end
end
return nil
end
local function interpolate_manhattan(pos1, pos2)
local interpolated = {}
local dx = pos2.x - pos1.x
local dy = pos2.y - pos1.y
local manhattan_distance = math.abs(dx) + math.abs(dy)
-- game.print("Distance3 "..manhattan_distance)
if manhattan_distance > 2 then
local steps = math.max(math.abs(dx), math.abs(dy))
local x_step = math.floor((dx / steps)*2)/2
local y_step = math.floor((dy / steps)*2)/2
for i = 1, steps - 1 do
local new_pos_x = {x = pos1.x + math.round(x_step * i), y = pos1.y}
if is_placeable(new_pos_x) then
table.insert(interpolated, {position = new_pos_x})
else
local neighbor = find_placeable_neighbor(new_pos_x, pos1)
if neighbor then
table.insert(interpolated, {position = neighbor})
end
end
local new_pos_y = {x = pos1.x + math.round(x_step * i), y = pos1.y + math.round(y_step * i)}
if is_placeable(new_pos_y) then
table.insert(interpolated, {position = new_pos_y})
else
local neighbor = find_placeable_neighbor(new_pos_y, pos1)
if neighbor then
table.insert(interpolated, {position = neighbor})
end
end
end
--elseif math.abs(dx) == 1 and math.abs(dy) == 1 then
-- local mid_pos = {x = pos2.x, y = pos1.y}
-- if is_placeable(mid_pos) then
-- table.insert(interpolated, {position = mid_pos})
-- else
-- mid_pos = {x = pos1.x, y = pos2.y}
-- if is_placeable(mid_pos) then
-- table.insert(interpolated, {position = mid_pos})
-- else
-- mid_pos = find_placeable_neighbor(mid_pos, pos1)
-- if mid_pos then
-- table.insert(interpolated, {position = mid_pos})
-- end
-- end
-- end
--end
elseif math.abs(dx) == 1 and math.abs(dy) == 1 then --and math.abs(dx) <= 1.5 and math.abs(dy) <= 1.5 then
-- Try first horizontal then vertical movement
local mid_pos = {x = pos2.x, y = pos1.y}
if is_placeable(mid_pos) then
table.insert(interpolated, {position = mid_pos})
else
-- Try vertical then horizontal movement
mid_pos = {x = pos1.x, y = pos2.y}
if is_placeable(mid_pos) then
table.insert(interpolated, {position = mid_pos})
else
-- If neither orthogonal position works, try to find a neighbor
local neighbor = find_placeable_neighbor(mid_pos, pos1)
if neighbor then
table.insert(interpolated, {position = neighbor})
end
end
end
end
return interpolated
end
local function place_at_position(player, connection_type, current_position, dir, serialized_entities, dry_run, counter_state, is_underground_exit)
local is_electric_pole = wire_reach[connection_type] ~= nil
local placement_position = current_position
local existing_entity = nil
for _, entity in pairs(serialized_entities) do
if entity.position.x == placement_position.x and entity.position.y == placement_position.y then
return
end
end
if is_electric_pole then
if is_position_saturated(current_position, wire_reach[connection_type]) then
return -- No need to place another pole
end
placement_position = game.surfaces[1].find_non_colliding_position(connection_type, current_position, 2, 0.1)
if not placement_position then
error("Cannot find suitable position to place " .. connection_type)
end
else
local entities = game.surfaces[1].find_entities_filtered{
position = current_position,
type = {"beam", "resource", "player"},
invert=true
}
for _, entity in pairs(entities) do
if entity.name == connection_type then
existing_entity = entity
break
end
end
end
if existing_entity then
-- game.print("Existing entity "..existing_entity.name)
-- Get the existing network ID before any modifications
--local existing_network_id = has_valid_fluidbox(existing_entity) and existing_entity.fluidbox[1].get_fluid_system_id()
-- Update direction if needed
if existing_entity.name ~= connection_type then
if existing_entity.direction ~= dir then
existing_entity.direction = dir
end
end
-- For pipes, merge networks
if connection_type == 'pipe' then
for _, serialized in ipairs(serialized_entities) do
local entity = game.surfaces[1].find_entity(connection_type, serialized.position)
if entity and has_valid_fluidbox(entity) then
existing_entity.connect_neighbour(entity)
end
end
end
-- Update or add to serialized list
for i, serialized in ipairs(serialized_entities) do
if serialized.position.x == current_position.x and serialized.position.y == current_position.y then
serialized_entities[i] = global.utils.serialize_entity(existing_entity)
return existing_entity
end
end
table.insert(serialized_entities, global.utils.serialize_entity(existing_entity))
return existing_entity
else
-- For underground entities, we need to set the correct type (input/output)
local entity_variant = {
name = connection_type,
position = placement_position,
direction = dir,
force = player.force,
type = is_underground_exit and "output" or "input",
move_stuck_players=true
}
-- We can just teleport away here to avoid collision as we dont adhere by distance rules in connect_entities
player.teleport({placement_position.x+2, placement_position.y+2})
local can_place
if connection_type == 'pipe' or connection_type == 'pipe-to-ground' or connection_type == 'underground-pipe' then
-- Use permissive surface check for pipe placement to allow tight spaces
can_place = game.surfaces[1].can_place_entity({
name = connection_type,
position = placement_position,
direction = dir,
force = player.force
})
else
can_place = global.utils.can_place_entity(player, connection_type, placement_position, dir)
end
--local can_place = global.utils.avoid_entity(1, connection_type, placement_position, dir)
--if not can_build then
-- error("Cannot place the entity at the specified position: x="..position.x..", y="..position.y)
--end
--local player_position = player.position
-- player.teleport({placement_position.x, placement_position.y})
--local can_place = global.actions.can_place_entity(1, connection_type, dir, placement_position.x, placement_position.y)--game.surfaces[1].can_place_entity(entity_variant)
--player.teleport(player_position)
--local can_place = global.utils.avoid_entity(1, connection_type, placement_position, dir)
rendering.draw_circle{only_in_alt_mode=true, width = 0.25, color = {r = 0, g = 1, b = 0}, surface = player.surface, radius = 0.5, filled = false, target = placement_position, time_to_live = 12000}
if dry_run and can_place == false then
-- Define the area where the entity will be placed
local target_area = {
{x = placement_position.x - 1 / 2, y = placement_position.y - 1 / 2},
{x = placement_position.x + 1 / 2, y = placement_position.y + 1 / 2}
}
-- Check for collision with other entities
local entities = player.surface.find_entities_filtered{area = target_area, force = player.force}
for _, entity in pairs(entities) do
-- game.print("1 "..entity.name)
end
error("Cannot connect due to placement blockage 1.")
end
-- Place entity
if can_place and not dry_run then
--global.utils.avoid_entity(player.index, connection_type, placement_position, dir)
local placed_entity = game.surfaces[1].create_entity(entity_variant)
if placed_entity then
player.remove_item({name = connection_type, count = 1})
counter_state.place_counter = counter_state.place_counter + 1
table.insert(serialized_entities, global.utils.serialize_entity(placed_entity))
return placed_entity
end
elseif can_place and dry_run then
counter_state.place_counter = counter_state.place_counter + 1
return nil
else
-- game.print("Avoiding entity at " .. placement_position.x.. ", " .. placement_position.y)
-- global.utils.avoid_entity(player.index, connection_type, placement_position, dir)
-- error("Cannot place entity")
--local entities = player.surface.find_entities_filtered{position=placement_position, radius=0.5, type = {"beam", "resource", "player"}, invert=true}
--local can_place = #entities == 0
--if can_place then
-- local tile = player.surface.get_tile(placement_position.x, placement_position.y)
-- if is_water_tile(tile.name) then
-- can_place = false
-- end
--end
--
--if can_place then
-- local placed_entity = player.surface.create_entity({
-- name = connection_type,
-- position = placement_position,
-- direction = dir,
-- force = player.force
-- })
--
-- if placed_entity then
-- player.remove_item({name = connection_type, count = 1})
-- counter_state.place_counter = counter_state.place_counter + 1
-- table.insert(serialized_entities, global.utils.serialize_entity(placed_entity))
-- return placed_entity
-- end
--
--end
end
end
-- Check inventory
local has_item = false
for _, inv in pairs({defines.inventory.character_main}) do
if player.get_inventory(inv) then
local count = player.get_inventory(inv).get_item_count(connection_type)
if count > 0 then
has_item = true
break
end
end
end
if not has_item then
error("You do not have the required item in their inventory.")
end
-- We can just teleport away here to avoid collision as we dont adhere by distance rules in connect_entities
player.teleport({placement_position.x+2, placement_position.y+2})
local can_place
if connection_type == 'pipe' or connection_type == 'pipe-to-ground' or connection_type == 'underground-pipe' then
-- Use permissive surface check for pipe placement to allow tight spaces
can_place = game.surfaces[1].can_place_entity({
name = connection_type,
position = placement_position,
direction = dir,
force = player.force
})
else
can_place = global.utils.can_place_entity(player, connection_type, placement_position, dir)
end
--local player_position = player.position
--player.teleport({placement_position.x, placement_position.y})
--local can_place = global.actions.can_place_entity(1, connection_type, dir, placement_position.x, placement_position.y)--game.surfaces[1].can_place_entity(entity_variant)
--player.teleport(player_position)
--local can_place = global.utils.avoid_entity(1, connection_type, placement_position, dir)
rendering.draw_circle{only_in_alt_mode=true, width = 0.25, color = {r = 0, g = 1, b = 0}, surface = player.surface, radius = 0.5, filled = false, target = placement_position, time_to_live = 12000}
if dry_run and can_place == false then
local target_area = {
{x = placement_position.x - 1 / 2, y = placement_position.y - 1 / 2},
{x = placement_position.x + 1 / 2, y = placement_position.y + 1 / 2}
}
-- Check for collision with other entities
local entities = player.surface.find_entities_filtered{area = target_area, force = player.force}
for _, entity in pairs(entities) do
-- game.print("1: "..entity.name)
end
error("Cannot connect due to placement blockage.")
end
-- Place entity
if can_place and not dry_run then
--global.utils.avoid_entity(player.index, connection_type, placement_position, dir)
local placed_entity = game.surfaces[1].create_entity({
name = connection_type,
position = placement_position,
direction = dir,
force = player.force,
move_stuck_players=true
})
if placed_entity then
player.remove_item({name = connection_type, count = 1})
counter_state.place_counter = counter_state.place_counter + 1
table.insert(serialized_entities, global.utils.serialize_entity(placed_entity))
return placed_entity
end
elseif can_place and dry_run then
counter_state.place_counter = counter_state.place_counter + 1
return nil
else
-- game.print("Avoiding entity at " .. placement_position.x.. ", " .. placement_position.y)
-- global.utils.avoid_entity(player.index, connection_type, placement_position, dir)
-- error("Cannot place entity")
--local entities = player.surface.find_entities_filtered{position=placement_position, radius=0.5, type = {"beam", "resource", "player"}, invert=true}
--local can_place = #entities == 0
--if can_place then
-- local tile = player.surface.get_tile(placement_position.x, placement_position.y)
-- if is_water_tile(tile.name) then
-- can_place = false
-- end
--end
--
--if can_place then
-- local placed_entity = player.surface.create_entity({
-- name = connection_type,
-- position = placement_position,
-- direction = dir,
-- force = player.force
-- })
--
-- if placed_entity then
-- player.remove_item({name = connection_type, count = 1})
-- counter_state.place_counter = counter_state.place_counter + 1
-- table.insert(serialized_entities, global.utils.serialize_entity(placed_entity))
-- return placed_entity
-- end
--
--end
end
end
local function table_contains(tbl, element)
for _, value in ipairs(tbl) do
if value == element then
return true
end
end
return false
end
local function connect_entities(player_index, source_x, source_y, target_x, target_y, path_handle, connection_types, dry_run)
local counter_state = {place_counter = 0}
local player = global.agent_characters[player_index]
local last_placed_entity = nil
local start_position = {x = math.floor(source_x*2)/2, y = math.floor(source_y*2)/2}
local end_position = {x = target_x, y = target_y}
local raw_path = global.paths[path_handle]
-- game.print("Path length "..#raw_path)
-- game.print(serpent.line(start_position).." - "..serpent.line(end_position))
if not raw_path then
error("No path found for handle " .. path_handle .. ". Pathfinding may have failed.")
elseif raw_path == "not_found" then
error("Pathfinding failed: no valid path exists between source and target positions.")
elseif raw_path == "busy" then
error("Pathfinder is busy, try again later.")
elseif type(raw_path) ~= "table" then
error("Invalid path type: " .. type(raw_path) .. " (value: " .. serpent.line(raw_path) .. ")")
elseif #raw_path == 0 then
error("Empty path returned from pathfinder.")
end
-- game.print("Normalising", {print_skip=defines.print_skip.never})
local path = global.utils.normalise_path(raw_path, start_position, end_position)
-- Get default and underground connection types
local default_connection_type = default_connect_types[connection_types[1]] or connection_types[1]
local underground_type = nil
for _, type in ipairs(connection_types) do
if is_underground_type(type) then
underground_type = type
break
end
end
--rendering.clear()
rendering.draw_circle{only_in_alt_mode=true, width = 1, color = {r = 1, g = 0, b = 0}, surface = player.surface, radius = 0.5, filled = false, target = start_position, time_to_live = 60000}
rendering.draw_circle{only_in_alt_mode=true, width = 1, color = {r = 0, g = 1, b = 0}, surface = player.surface, radius = 0.5, filled = false, target = end_position, time_to_live = 60000}
for i = 1, #path - 1 do
rendering.draw_line{only_in_alt_mode=true, surface = player.surface, from = path[i].position, to = path[i + 1].position, color = {1, 0, 1}, width = 2, dash_length=0.25, gap_length = 0.25}
end
for i = 1, #raw_path - 1 do
rendering.draw_line{only_in_alt_mode=true, surface = player.surface, from = raw_path[i].position, to = raw_path[i + 1].position, color = {1, 1, 0}, width = 0, dash_length=0.2, gap_length = 0.2}
end
local last_position = start_position
local step_size = wire_reach[default_connection_type] or 1
local is_electric_pole = wire_reach[default_connection_type] ~= nil
for i = 1, #path-1, step_size do
global.elapsed_ticks = global.elapsed_ticks + global.utils.calculate_movement_ticks(player, last_position, path[i].position)
last_position = path[i].position
end
local serialized_entities = {}
-- Get source and target entities
local source_entity = global.utils.get_closest_entity(player, {x = source_x, y = source_y})
local target_entity = global.utils.get_closest_entity(player, {x = target_x, y = target_y})
-- Validate that entities were found
if not source_entity then
error("No entity found at source position x=" .. source_x .. " y=" .. source_y .. " within radius 3")
end
if not target_entity then
error("No entity found at target position x=" .. target_x .. " y=" .. target_y .. " within radius 3")
end
if #connection_types == 1 and connection_types[1] == 'pipe-to-ground' then
-- Calculate the direction from start to end.
local dir = global.utils.get_direction(start_position, end_position)
local entrance_dir = global.utils.get_entity_direction(underground_type, dir / 2)
-- Place the underground entrance at the start position.
place_at_position(player, underground_type, start_position, entrance_dir,
serialized_entities, dry_run, counter_state, false)
local exit_dir
if underground_type == "pipe-to-ground" then
-- For pipe-to-ground, rotate the direction 180° for the exit.
exit_dir = global.utils.get_entity_direction(underground_type, (dir / 2 + 2) % 4)
else
exit_dir = global.utils.get_entity_direction(underground_type, dir / 2)
end
-- Place the underground exit at the end position.
place_at_position(player, underground_type, end_position, exit_dir,
serialized_entities, dry_run, counter_state, true)
return {
entities = serialized_entities,
connected = true,
number_of_entities = counter_state.place_counter
}
end
if underground_type then
-- Calculate maximum possible underground sections based on inventory
local max_underground_sections = calculate_max_underground_sections(player, underground_type)
-- Find straight sections suitable for underground entities
local range = underground_ranges[underground_type]
local straight_sections = find_straight_sections(path, 4)
-- Track remaining underground sections we can create
local remaining_sections = max_underground_sections
local current_index = 1
for _, section in ipairs(straight_sections) do
if remaining_sections <= 0 then
break
end
local MAX_INDEX_PLACEMENT = 200
-- Place regular entities up to the section
while current_index < section.start_index do
local current_pos = path[current_index].position
local next_pos = path[current_index + 1].position
local dir = global.utils.get_direction(current_pos, next_pos)
place_at_position(player, default_connection_type, current_pos,
global.utils.get_entity_direction(default_connection_type, dir/2),
serialized_entities, dry_run, counter_state, false)
current_index = current_index + 1
if current_index > MAX_INDEX_PLACEMENT then
break
end
end
-- Place initial surface entity for direction change
local margin_pos = path[section.start_index].position
local margin_next_pos = path[section.start_index + 1].position
local margin_dir = global.utils.get_direction(margin_pos, margin_next_pos)
place_at_position(player, default_connection_type, margin_pos,
global.utils.get_entity_direction(default_connection_type, margin_dir/2),
serialized_entities, dry_run, counter_state, false)
-- Split the section into multiple underground segments, limited by remaining_sections
local segments = split_section_into_underground_segments(section, path, range, remaining_sections)
-- Update remaining_sections count
remaining_sections = remaining_sections - #segments
for i, segment in ipairs(segments) do
-- Place underground entrance
local entrance_pos = path[segment.entrance_index].position
local exit_pos = path[segment.exit_index].position
local dir = global.utils.get_direction(entrance_pos, exit_pos)
local entity_dir = global.utils.get_entity_direction(underground_type, dir/2)
place_at_position(player, underground_type, entrance_pos,
entity_dir,
serialized_entities, dry_run, counter_state, false)
-- Adjust direction for pipe-to-ground exit and place exit pipe
if underground_type == 'pipe-to-ground' then
entity_dir = global.utils.get_entity_direction(underground_type, (dir/2 + 2)%4)
end
place_at_position(player, underground_type, exit_pos,
entity_dir,
serialized_entities, dry_run, counter_state, true)
-- Update current_index to after the exit
current_index = segment.exit_index + 1
end
-- Place final surface entity for direction change if we had segments
if #segments > 0 then
local final_margin_pos = path[section.end_index].position
local final_prev_pos = path[section.end_index - 1].position
local final_dir = global.utils.get_direction(final_prev_pos, final_margin_pos)
place_at_position(player, default_connection_type, final_margin_pos,
global.utils.get_entity_direction(default_connection_type, final_dir/2),
serialized_entities, dry_run, counter_state, false)
end
-- If we've used all available underground sections, use regular entities for the rest
if remaining_sections <= 0 then
break
end
end
-- Place remaining regular entities to finish the connection
while current_index < #path do
local current_pos = path[current_index].position
local next_pos = path[current_index + 1].position
local dir = global.utils.get_direction(current_pos, next_pos)
place_at_position(player, default_connection_type, current_pos,
global.utils.get_entity_direction(default_connection_type, dir/2),
serialized_entities, dry_run, counter_state, false)
current_index = current_index + 1
end
-- After underground segment placement
if current_index == #path and not path[#path].has_entity then
local final_pos = path[#path].position
local prev_pos = path[#path-1].position
local dir = global.utils.get_direction(prev_pos, final_pos)
place_at_position(player, default_connection_type, final_pos,
global.utils.get_entity_direction(default_connection_type, dir/2),
serialized_entities, dry_run, counter_state, false)
end
else
-- Handle source belt orientation if it exists
if not is_electric_pole and not table_contains(connection_types, 'pipe') then
local source_pos = {x = source_x, y = source_y}
local entities = game.surfaces[1].find_entities_filtered{
position = source_pos,
name = connection_types,
force = "player"
}
if #entities > 0 and #path > 1 then
-- Calculate initial direction based on first two points in path
local initial_dir = global.utils.get_direction(path[1].position, path[2].position)
--local entity_dir = global.utils.get_entity_direction('pipe', initial_dir/2)
-- Update source belt direction if needed
local source_belt = entities[1]
if source_belt and source_belt.valid and source_belt.direction ~= initial_dir then
source_belt.direction = initial_dir
table.insert(serialized_entities, global.utils.serialize_entity(source_belt))
end
end
end
if is_electric_pole then
-- Place poles until we achieve connectivity
local last_pole = source_entity
for i = 1, #path, step_size do
local current_pos = path[i].position
local dir = global.utils.get_direction(current_pos, path[math.min(i + step_size, #path)].position)
local entity_dir = global.utils.get_entity_direction(default_connection_type, dir/2)
-- Place the pole
local placed_entity = place_at_position(player, default_connection_type, current_pos, entity_dir, serialized_entities, dry_run, counter_state)
if not dry_run then
-- Get the newly placed pole
local current_pole = placed_entity or global.utils.get_closest_entity(player, current_pos)
-- Check if we've achieved connectivity to the target
if are_poles_connected(current_pole, target_entity) then
break -- Stop placing poles once we have connectivity
end
last_pole = current_pole
end
end
-- If we haven't achieved connectivity yet, place one final pole at the target
if not dry_run and last_pole and target_entity and not are_poles_connected(last_pole, target_entity) then
local final_dir = global.utils.get_direction(path[#path].position, end_position)
-- game.print("Placing final pole at "..serpent.line(end_position))
place_at_position(player, default_connection_type, end_position,
global.utils.get_entity_direction(default_connection_type, final_dir/2),
serialized_entities, dry_run, counter_state)
end
else
if table_contains(connection_types, 'pipe') then
place_at_position(player, 'pipe', start_position, 0, serialized_entities, dry_run, counter_state)
end
for i = 1, #path-1, step_size do
local dir = global.utils.get_direction(path[i].position, path[math.min(i + step_size, #path)].position)
local placed = place_at_position(player, default_connection_type, path[i].position,
global.utils.get_entity_direction(default_connection_type, dir/2),
serialized_entities, dry_run, counter_state)
if placed then
last_placed_entity = placed
end
end
-- Handle final placement
if table_contains(connection_types, 'pipe') then
local preemptive_target = {
x = (target_x + path[#path].position.x)/2,
y = (target_y + path[#path].position.y)/2
}
-- Place intermediate and final pipes
place_at_position(player, 'pipe', path[#path].position,
global.utils.get_direction(path[#path].position, preemptive_target),
serialized_entities, dry_run, counter_state)
place_at_position(player, 'pipe', preemptive_target,
global.utils.get_direction(path[#path].position, preemptive_target),
serialized_entities, dry_run, counter_state)
place_at_position(player, 'pipe', end_position,
global.utils.get_direction(preemptive_target, end_position),
serialized_entities, dry_run, counter_state)
else
local last_path_index = #path
local second_to_last_index = math.max(1, last_path_index - 1)
local final_dir = global.utils.get_direction(
path[second_to_last_index].position,
path[last_path_index].position
)
local final_entity = place_at_position(player, default_connection_type, end_position,
global.utils.get_entity_direction(default_connection_type, final_dir/2),
serialized_entities, dry_run, counter_state)
if final_entity then
last_placed_entity = final_entity
end
-- After all belts are placed, serialize the entire belt group if we're not in dry run
if not dry_run and last_placed_entity and last_placed_entity.valid and default_connection_type:find("belt") then
-- Clear the existing serialized entities (which only contain individual placements)
serialized_entities = {}
-- Serialize the entire connected belt group
local group_data = serialize_belt_group(last_placed_entity)
if group_data then
for _, serialized in ipairs(group_data) do
table.insert(serialized_entities, serialized)
end
end
end
end
end
end
-- Check final connectivity
local is_connected = false
if source_entity and target_entity then
if is_electric_pole then
is_connected = are_poles_connected(source_entity, target_entity)
else
is_connected = are_fluidboxes_connected(source_entity, target_entity)
end
end
-- game.print("Connection status: " .. tostring(is_connected))
return {
entities = serialized_entities,
connected = is_connected,
number_of_entities = counter_state.place_counter
}
end
global.utils.normalise_path = function(original_path, start_position, end_position)
local path = {}
local seen = {} -- To track seen positions
if original_path == nil or #original_path < 1 or original_path == "not_found" then
error("Failed to find a path.")
end
if math.ceil(start_position.x) == start_position.x then
start_position.x = start_position.x + 0.5
--start_position.y = start_position.y + 0.5
end
if math.ceil(start_position.y) == start_position.y then
start_position.y = start_position.y + 0.5
end
if math.ceil(end_position.x) == end_position.x or math.ceil(end_position.y) == end_position.y then
end_position.x = end_position.x + 0.5
end_position.y = end_position.y + 0.5
end
--game.print(serpent.block(original_path))
--for i = 1, #original_path do
-- game.print(serpent.line(original_path[i]))
--end
if math.ceil(original_path[1].position.x) == original_path[1].position.x or math.ceil(original_path[1].position.y) == original_path[1].position.y then
original_path[1].position.x = original_path[1].position.x + 0.5
original_path[1].position.y = original_path[1].position.y + 0.5
end
-- Helper function to add unique positions
local function add_unique(pos, prev_pos)
local key = pos.x .. "," .. pos.y
if not seen[key] then
if is_placeable(pos) then
table.insert(path, {position = pos})
seen[key] = true
return pos
else
local alt_pos = find_placeable_neighbor(pos, prev_pos)
if alt_pos then
local alt_key = alt_pos.x .. "," .. alt_pos.y
if not seen[alt_key] then
table.insert(path, {position = alt_pos})
seen[alt_key] = true
return alt_pos
end
end
end
end
return nil
end
-- Add start position first
local previous_pos = add_unique(start_position, nil) or start_position
-- Process each segment of the path
local current_pos = previous_pos
for i = 1, #original_path do
local target_pos = original_path[i].position
local interpolated = interpolate_manhattan(current_pos, target_pos)
-- Add interpolated positions
for _, point in ipairs(interpolated) do
local new_pos = add_unique(point.position, current_pos)
if new_pos then
current_pos = new_pos
end
end
-- Add the target position
local new_pos = add_unique(target_pos, current_pos)
if new_pos then
current_pos = new_pos
end
end
-- Finally interpolate to end position if it's different from the last position
if current_pos.x ~= end_position.x or current_pos.y ~= end_position.y then
local interpolated = interpolate_manhattan(current_pos, end_position)
for _, point in ipairs(interpolated) do
local new_pos = add_unique(point.position, current_pos)
if new_pos then
current_pos = new_pos
end
end
add_unique(end_position, current_pos)
end
return path
end
-- Helper function to check if two positions are adjacent
local function are_positions_adjacent(pos1, pos2)
local dx = math.abs(pos1.x - pos2.x)
local dy = math.abs(pos1.y - pos2.y)
return (dx <= 1 and dy <= 1) and not (dx == 0 and dy == 0)
end
-- Helper function to validate belt direction compatibility
local function are_belt_directions_compatible(dir1, dir2, pos1, pos2)
-- Convert positions to vectors
local dx = pos2.x - pos1.x
local dy = pos2.y - pos1.y
-- Check if the belts are pointing in compatible directions
-- This is a simplified check - you may need to adjust based on your specific needs
if dir1 == dir2 then
-- Same direction - check if it matches position delta
local expected_dir = global.utils.get_direction(pos1, pos2)
return dir1 == expected_dir
else
-- Different directions - check if they form a valid turn
-- Add more sophisticated turn validation if needed
return are_positions_adjacent(pos1, pos2)
end
end
-- Function to validate belt connectivity
local function validate_belt_connectivity(path)
if not path or #path < 2 then
return false, "Invalid path length"
end
-- Check each segment of the path
for i = 1, #path - 1 do
local current_pos = path[i].position
local next_pos = path[i + 1].position
-- Check if positions are adjacent or properly spaced
if not are_positions_adjacent(current_pos, next_pos) then
return false, "Could not create a belt at position " ..
current_pos.x .. "," .. current_pos.y
end
-- Calculate directions for current and next position
local current_dir = global.utils.get_direction(current_pos, next_pos)
local next_dir = i < #path - 1 and
global.utils.get_direction(next_pos, path[i + 2].position) or
current_dir
-- Check direction compatibility
if not are_belt_directions_compatible(current_dir, next_dir, current_pos, next_pos) then
return false, "Invalid belt turn detected at position " ..
current_pos.x .. "," .. current_pos.y
end
-- Check if position is placeable
if not is_placeable(current_pos) then
return false, "Cannot place belt at position " ..
current_pos.x .. "," .. current_pos.y
end
end
-- Check final position
if not is_placeable(path[#path].position) then
return false, "Cannot place belt at final position"
end
return true, nil
end
-- Modify the connect_entities function to include validation
local function connect_entities_with_validation(player_index, source_x, source_y, target_x, target_y, path_handle, connection_types, dry_run)
local path = global.paths[path_handle]
if path == nil then
error("No path found")
end
-- Only perform validation for belt-type entities
for _,connection_type in pairs(connection_types) do
if wire_reach[connection_type] then
if are_positions_in_same_network(
{x = source_x, y = source_y},
{x = target_x, y = target_y}
) then
--error("Source and target positions are already connected to the same power network")
--return {}
break
end
--break
elseif connection_type:find("belt") then
-- Normalize path first
local normalized_path = global.utils.normalise_path(path,
{x = source_x, y = source_y},
{x = target_x, y = target_y})
-- Validate connectivity
local is_valid, error_message = validate_belt_connectivity(normalized_path)
if not is_valid then
error(error_message)
end
break
end
end
-- If validation passes or it's not a belt, proceed with normal connection
return connect_entities(player_index, source_x, source_y, target_x, target_y,
path_handle, connection_types, dry_run)
end
-- Using the new shortest_path function.
global.actions.connect_entities = function(player_index, source_x, source_y, target_x, target_y, path_handle, connection_type_string, dry_run, number_of_connection_entities)
local connection_types = {}
for item in string.gmatch(connection_type_string, "([^,]+)") do
-- game.print(item)
table.insert(connection_types, item)
end
--First do a dry run
local result = connect_entities_with_validation(player_index, source_x, source_y, target_x, target_y, path_handle, connection_types, true)
-- then do an actual run if dry run is false
if not dry_run then
-- Check if the player has enough entities in their inventory
local required_count = result.number_of_entities
-- game.print("Required count: " .. required_count)
-- game.print("Available count: " .. number_of_connection_entities)
if number_of_connection_entities < required_count then
error("\"You do not have enough " .. connection_type_string .. " in you inventory to complete this connection. Required number - " .. required_count .. ", Available in inventory - " .. number_of_connection_entities.."\"")
end
result = connect_entities_with_validation(player_index, source_x, source_y, target_x, target_y, path_handle, connection_types, false)
end
return result
end