local floor, ceil = math.floor, math.ceil local min, max = math.min, math.max local common = require("layouts.common") local simple = require("layouts.simple") local mpp_util = require("mpp.mpp_util") local pole_grid_mt = require("mpp.pole_grid_mt") local drawing = require("mpp.drawing") local EAST, NORTH, SOUTH, WEST = mpp_util.directions() local table_insert = table.insert ---@class CompactLayout : SimpleLayout local layout = table.deepcopy(simple) ---@class CompactState : SimpleState layout.name = "compact" layout.translation = {"", "[entity=underground-belt] ", {"mpp.settings_layout_choice_compact"}} layout.restrictions.miner_size = {3, 10e3} layout.restrictions.miner_radius = {1, 10e3} layout.restrictions.uses_underground_belts = true layout.restrictions.pole_omittable = true layout.restrictions.pole_width = {1, 1} layout.restrictions.pole_length = {7.5, 10e3} layout.restrictions.pole_supply_area = {2.5, 10e3} layout.restrictions.coverage_tuning = true layout.restrictions.lamp_available = true layout.restrictions.module_available = true layout.restrictions.pipe_available = true ---@param state CompactState ---@return PlacementAttempt function layout:_placement_attempt(state, shift_x, shift_y) local grid = state.grid local M = state.miner local size = M.size local miners, postponed = {}, {} local heuristic_values = common.init_heuristic_values() local lane_layout = {} local bx, by = shift_x + M.size - 1, shift_y + M.size - 1 local heuristic = self:_get_miner_placement_heuristic(state) local row_index = 1 for y_float = shift_y, state.coords.th + size, size + 0.5 do local y = ceil(y_float) local column_index = 1 lane_layout[#lane_layout+1] = {y = y, row_index = row_index} for x = shift_x, state.coords.tw, size do local tile = grid:get_tile(x, y) --[[@as GridTile]] local miner = { x = x, y = y, origin_x = x + M.x, origin_y = y + M.y, tile = tile, line = row_index, column = column_index, } if tile.neighbors_outer > 0 and heuristic(tile) then miners[#miners+1] = miner common.add_heuristic_values(heuristic_values, M, tile) elseif tile.neighbors_outer > 0 then postponed[#postponed+1] = miner end column_index = column_index + 1 end row_index = row_index + 1 end local result = { sx = shift_x, sy = shift_y, bx = bx, by = by, miners = miners, lane_layout = lane_layout, heuristics = heuristic_values, heuristic_score = -(0/0), unconsumed = 0, } common.process_postponed(state, result, miners, postponed) common.finalize_heuristic_values(result, heuristic_values, state.coords) return result end ---@param self CompactLayout ---@param state CompactState function layout:prepare_belt_layout(state) local belts = {} state.belts = belts state.builder_belts = {} local coverage = mpp_util.calculate_pole_spacing(state, state.miner_max_column, state.miner_lane_count, true) state.builder_power_poles = {} if coverage.capable_span then self:_placement_capable(state) else self:_placement_incapable(state) end return "prepare_belts" end ---Placement of belts+poles when they tile nicely (enough reach/supply area) ---@param self CompactLayout ---@param state CompactState function layout:_placement_capable(state) local M, C, P, G = state.miner, state.coords, state.pole, state.grid local attempt = state.best_attempt local miner_lane_count = state.miner_lane_count local coverage = mpp_util.calculate_pole_spacing(state, state.miner_max_column, state.miner_lane_count, true) local steps = {} for i = coverage.pole_start, coverage.full_miner_width, coverage.pole_step do table.insert(steps, i) end local power_grid = pole_grid_mt.new() state.power_grid = power_grid --local transport_layout = {} --state.transport_layout = transport_layout local iy = 1 for i = 1, miner_lane_count, 2 do y = attempt.sy + floor((M.size + 0.5) * i) --local transport_layout_lane = {} --transport_layout[ceil(i / 2)] = transport_layout_lane local backtrack_build = false for step_i = #steps, 1, -1 do local x = attempt.sx + steps[step_i] local has_consumers = G:needs_power(x, y, P) if backtrack_build or step_i == 1 or has_consumers then backtrack_build = true power_grid:add_pole{ grid_x = x, grid_y = y, ix = step_i, iy = iy, backtracked = backtrack_build, has_consumers = has_consumers, built = has_consumers, connections = {}, } --transport_layout_lane[step_i] = {x=x, y=y, built=true} -- table_insert(builder_power_poles, { -- name=P.name, -- thing="pole", -- grid_x=x, -- grid_y=y, -- ix = step_i, -- iy = iy, -- }) else power_grid:add_pole{ grid_x = x, grid_y = y, ix = step_i, iy = iy, backtracked = false, has_consumers = has_consumers, built = false, connections = {}, } end end iy = iy + 1 end local connectivity = power_grid:find_connectivity(state.pole) state.power_connectivity = connectivity end ---@param self CompactLayout ---@param state CompactState function layout:prepare_belts(state) local M, C, P, G = state.miner, state.coords, state.pole, state.grid local attempt = state.best_attempt local miner_lanes = state.miner_lanes local miner_lane_count = state.miner_lane_count local builder_power_poles = state.builder_power_poles local connectivity = state.power_connectivity local power_grid = state.power_grid --local debug_draw = drawing(state, false, false) if power_grid then local connected = power_grid:ensure_connectivity(connectivity) for _, pole in pairs(connected) do table_insert(builder_power_poles, { name = P.name, thing = "pole", grid_x = pole.grid_x, grid_y = pole.grid_y, ix = pole.ix, iy = pole.iy, }) end end local builder_belts = state.builder_belts local belt_name = state.belt.name local underground_name = state.belt.related_underground_belt --[[@as string]] local belts = {} state.belts = belts state.belt_count = 0 local pipe_adjust = state.place_pipes and -1 or 0 for i_lane = 1, miner_lane_count, 2 do local iy = ceil(i_lane / 2) local lane1 = miner_lanes[i_lane] local lane2 = miner_lanes[i_lane+1] --local transport_layout_lane = transport_layout[ceil(i_lane / 2)] local function get_lane_length(lane, out_x) if lane and lane[#lane] then return lane[#lane].x+out_x end return 0 end local y = attempt.sy + floor((M.size + 0.5) * i_lane) local x1 = attempt.sx + pipe_adjust local belt = {x1=x1, x2=attempt.sx, y=y, built=false, lane1=lane1, lane2=lane2} belts[#belts+1] = belt if not lane1 and not lane2 then goto continue_lane end local x2 = max(get_lane_length(lane1, M.output_rotated[SOUTH].x), get_lane_length(lane2, M.out_x)) state.belt_count = state.belt_count + 1 belt.x2, belt.built = x2, true -- debug_draw:draw_circle{x = x1, y = y, width = 7} -- debug_draw:draw_circle{x = x2, y = y, width = 7} if not power_grid then goto continue_lane end local previous_start, first_interval = x1, true local power_poles, pole_count, belt_intervals = power_grid[iy], #power_grid[iy], {} for i, pole in pairs(power_poles) do if not pole.built then goto continue_poles end local next_x = pole.grid_x table_insert(belt_intervals, { x1 = previous_start, x2 = min(x2, next_x - 1), is_first = first_interval, }) previous_start = next_x + 2 first_interval = false if x2 < next_x then --debug_draw:draw_circle{x = x2, y = y, color = {1, 0, 0}} break end ::continue_poles:: end if previous_start <= x2 then --debug_draw:draw_circle{x = x2, y = y, color = {0, 1, 0}} table_insert(belt_intervals, { x1 = previous_start, x2 = x2, is_last = true, }) end for i, interval in pairs(belt_intervals) do local bx1, bx2 = interval.x1, interval.x2 builder_belts[#builder_belts+1] = { name = interval.is_first and belt_name or underground_name, thing = "belt", direction=WEST, grid_x = bx1, grid_y = y, type = not interval.is_first and "input" or nil } for x = bx1 + 1, bx2 - 1 do builder_belts[#builder_belts+1] = { name = belt_name, thing = "belt", direction=WEST, grid_x = x, grid_y = y, } end builder_belts[#builder_belts+1] = { name = i == #belt_intervals and belt_name or underground_name, thing = "belt", direction=WEST, grid_x = bx2, grid_y = y, type = not interval.is_last and "output" or nil } end ::continue_lane:: end return "prepare_pole_layout" end ---Placement of belts+poles when they don't tile nicely (not enough reach/supply area) ---@param self CompactLayout ---@param state CompactState function layout:_placement_incapable(state) end ---@param self CompactLayout ---@param state CompactState function layout:_placement_belts_small(state) local m = state.miner local attempt = state.best_attempt local belt_choice = state.belt_choice local underground_belt = state.belt.related_underground_belt local power_poles = {} state.builder_power_poles = power_poles ---@type table local miner_lanes = {} local miner_lane_count = 0 -- highest index of a lane, because using # won't do the job if a lane is missing local belts = state.belts for _, miner in ipairs(attempt.miners) do local index = miner.line miner_lane_count = max(miner_lane_count, index) if not miner_lanes[index] then miner_lanes[index] = {} end local line = miner_lanes[index] line[#line+1] = miner end for _, lane in ipairs(miner_lanes) do table.sort(lane, function(a, b) return a.x < b.x end) end ---@param lane MinerPlacement[] local function get_lane_length(lane) if lane then return lane[#lane].x + m.out_x end return 0 end ---@param lane MinerPlacement[] local function get_lane_column(lane) if lane and #lane > 0 then return lane[#lane].column end return 0 end state.belt_count = 0 local que_entity = create_entity_que(state.builder_belts) local function belts_filled(x1, y, w) for x = x1, x1 + w do que_entity{name=belt_choice, direction=WEST, grid_x=x, grid_y=y, thing="belt"} end end local pipe_adjust = state.place_pipes and -1 or 0 for i = 1, miner_lane_count, 2 do local lane1 = miner_lanes[i] local lane2 = miner_lanes[i+1] local y = attempt.sy + m.size * i + ceil(i/2) local x0 = attempt.sx + 1 local column_count = max(get_lane_column(lane1), get_lane_column(lane2)) if column_count == 0 then goto continue_lane end state.belt_count = state.belt_count + 1 local indices = {} if lane1 then for _, v in ipairs(lane1) do indices[v.column] = v end end if lane2 then for _, v in ipairs(lane2) do indices[v.column] = v end end if state.place_pipes then que_entity{ name=state.belt_choice, thing="belt", grid_x = x0 + pipe_adjust, grid_y = y, direction=WEST, } end belts[#belts+1] = { x1 = x0 + pipe_adjust, x2 = x0 + column_count * m.size, y = y, lane1 = lane1, lane2 = lane2, } for j = 1, column_count do local x1 = x0 + (j-1) * m.size if j % 2 == 1 then -- part one if indices[j] or indices[j+1] then que_entity{ name=state.belt_choice, thing="belt", grid_x=x1, grid_y=y, direction=WEST, } local stopper = (j+1 > column_count) and state.belt_choice or underground_belt que_entity{ name=stopper, thing="belt", grid_x=x1+1, grid_y=y, direction=WEST, type="output", } -- power_poles[#power_poles+1] = { -- x=x1+3, y=y, -- ix=1+floor(i/2), iy=1+floor(j/2), -- built = true, -- } power_poles[#power_poles+1] = { name=state.pole_choice, thing="pole", grid_x = x1+3, grid_y = y, ix=1+floor(i/2), iy=1+floor(j/2), } else -- just a passthrough belt belts_filled(x1, y, m.size - 1) end elseif j % 2 == 0 then -- part two if indices[j-1] or indices[j] then que_entity{ name=belt_choice, thing="belt", grid_x=x1+2, grid_y=y, direction=WEST, } que_entity{ name=underground_belt, thing="belt", grid_x=x1+1, grid_y=y, direction=WEST, } else -- just a passthrough belt belts_filled(x1, y, m.size - 1) end end end ::continue_lane:: end end ---@param self CompactLayout function layout:_placement_belts_large(state) local m = state.miner local attempt = state.best_attempt local belt_choice = state.belt_choice local underground_belt = game.entity_prototypes[belt_choice].related_underground_belt.name local power_poles = {} state.builder_power_poles = power_poles local belts = state.belts ---@type table local miner_lanes = {{}} local miner_lane_count = 0 -- highest index of a lane, because using # won't do the job if a lane is missing for _, miner in ipairs(attempt.miners) do local index = miner.line miner_lane_count = max(miner_lane_count, index) if not miner_lanes[index] then miner_lanes[index] = {} end local line = miner_lanes[index] line[#line+1] = miner end state.miner_lane_count = miner_lane_count local que_entity = create_entity_que(state.builder_belts) for _, lane in pairs(miner_lanes) do table.sort(lane, function(a, b) return a.center.x < b.center.x end) end ---@param lane MinerPlacement[] local function get_lane_length(lane) if lane and #lane > 0 then return lane[#lane].center.x or 0 end return 0 end ---@param lane MinerPlacement[] local function get_lane_column(lane) if lane and #lane > 0 then return lane[#lane].column or 0 end return 0 end local function belts_filled(x1, y, w) for x = x1, x1 + w do que_entity{name=belt_choice, direction=WEST, grid_x=x, grid_y=y, thing="belt"} end end local pipe_adjust = state.place_pipes and -1 or 0 for i = 1, miner_lane_count, 2 do local lane1 = miner_lanes[i] local lane2 = miner_lanes[i+1] local y = attempt.sy + m.size * i + ceil(i/2) local x0 = attempt.sx + 1 local column_count = max(get_lane_column(lane1), get_lane_column(lane2)) if column_count == 0 then goto continue_lane end local indices = {} if lane1 then for _, v in ipairs(lane1) do indices[v.column] = v end end if lane2 then for _, v in ipairs(lane2) do indices[v.column] = v end end if state.place_pipes then que_entity{ name=state.belt_choice, thing="belt", grid_x = x0 + pipe_adjust, grid_y = y, direction=WEST, } end belts[#belts+1] = { x1 = x0 + pipe_adjust, x2 = x0 + column_count * m.size, y = y, lane1 = lane1, lane2 = lane2, } for j = 1, column_count do local x1 = x0 + (j-1) * m.size if j % 3 == 1 then -- part one if indices[j] or indices[j+1] or indices[j+2] then que_entity{ name=belt_choice, grid_x=x1, grid_y=y, thing="belt", direction=WEST, } local stopper = (j+1 > column_count) and state.belt_choice or underground_belt que_entity{ name=stopper, grid_x=x1+1, grid_y=y, thing="belt", direction=WEST, type="output", } -- power_poles[#power_poles+1] = { -- x=x1+3, y=y, -- ix=1+floor(i/2), iy=1+floor(j/2), -- built = true, -- } power_poles[#power_poles+1] = { name=state.pole_choice, thing="pole", grid_x = x1+3, grid_y = y, ix=1+floor(i/2), iy=1+floor(j/2), } else -- just a passthrough belt belts_filled(x1, y, m.size - 1) end elseif j % 3 == 2 then -- part two if indices[j-1] or indices[j] or indices[j+1] then que_entity{ name=underground_belt, grid_x=x1+1, grid_y=y, thing="belt", direction=WEST, type="input", } que_entity{ name=belt_choice, grid_x=x1+2, grid_y=y, thing="belt", direction=WEST, } else -- just a passthrough belt belts_filled(x1, y, m.size - 1) end elseif j % 3 == 0 then belts_filled(x1, y, m.size - 1) end end ::continue_lane:: end end ---@param self CompactLayout ---@param state CompactState function layout:prepare_pole_layout(state) return "prepare_lamp_layout" end ---@param self CompactLayout ---@param state CompactState function layout:prepare_lamp_layout(state) local next_step = "expensive_deconstruct" if state.lamp_choice ~= true then return next_step end local lamps = {} state.builder_lamps = lamps local grid = state.grid local sx, sy = state.pole.size, 0 local lamp_spacing = true if state.pole.wire > 7 then lamp_spacing = false end for _, pole in ipairs(state.builder_power_poles) do local x, y = pole.grid_x + sx, pole.grid_y + sy local ix, iy = pole.ix, pole.iy local tile = grid:get_tile(x, y) local skippable_lamp = iy % 2 == 1 and ix % 2 == 1 if tile and (not lamp_spacing or skippable_lamp) then lamps[#lamps+1] = { name="small-lamp", thing="lamp", grid_x = x, grid_y = y, } end end return next_step end return layout