403 lines
11 KiB
Lua
403 lines
11 KiB
Lua
local mpp_util = require("mpp.mpp_util")
|
|
local common = require("layouts.common")
|
|
local renderer = require("mpp.render_util")
|
|
local drawing = require("mpp.drawing")
|
|
|
|
local floor, ceil = math.floor, math.ceil
|
|
local min, max = math.min, math.max
|
|
local EAST, NORTH, SOUTH, WEST = mpp_util.directions()
|
|
|
|
local simple = require("layouts.simple")
|
|
|
|
---@class SparseLayout : SimpleLayout
|
|
local layout = table.deepcopy(simple)
|
|
|
|
layout.name = "sparse"
|
|
layout.translation = {"", "[entity=transport-belt] ", {"mpp.settings_layout_choice_sparse"}}
|
|
|
|
layout.restrictions.miner_size = {1, 10e3}
|
|
layout.restrictions.miner_radius = {1, 10e3}
|
|
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.lamp_available = true
|
|
layout.restrictions.module_available = true
|
|
layout.restrictions.pipe_available = true
|
|
layout.restrictions.coverage_tuning = false
|
|
|
|
---@param state SimpleState
|
|
---@return PlacementAttempt
|
|
function layout:_placement_attempt(state, shift_x, shift_y)
|
|
local grid = state.grid
|
|
local M = state.miner
|
|
local size, area = state.miner.size, state.miner.area
|
|
local miners = {}
|
|
local heuristic_values = common.init_heuristic_values()
|
|
local row_index = 1
|
|
local lane_layout = {}
|
|
|
|
for uy = shift_y, state.coords.th + size, area do
|
|
local column_index = 1
|
|
local y = uy - M.extent_negative
|
|
lane_layout[#lane_layout+1] = {y=y, row_index = row_index}
|
|
for ux = shift_x, state.coords.tw, area do
|
|
local x = ux - M.extent_negative
|
|
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,
|
|
direction = row_index % 2 == 1 and SOUTH or NORTH
|
|
}
|
|
if tile.neighbors_outer > 0 then
|
|
miners[#miners+1] = miner
|
|
end
|
|
column_index = column_index + 1
|
|
end
|
|
row_index = row_index + 1
|
|
end
|
|
|
|
local result = {
|
|
sx=shift_x,
|
|
sy=shift_y,
|
|
bx = 1,
|
|
by = 1,
|
|
miners=miners,
|
|
lane_layout=lane_layout,
|
|
heuristics = heuristic_values,
|
|
heuristic_score = #miners,
|
|
unconsumed = 0,
|
|
}
|
|
|
|
common.process_postponed(state, result, miners, {})
|
|
|
|
common.finalize_heuristic_values(result, heuristic_values, state.coords)
|
|
|
|
result.heuristic_score = #miners
|
|
|
|
return result
|
|
end
|
|
|
|
---@param self SimpleLayout
|
|
---@param state SimpleState
|
|
function layout:prepare_layout_attempts(state)
|
|
local c = state.coords
|
|
local m = state.miner
|
|
local attempts = {}
|
|
state.attempts = attempts
|
|
state.best_attempt_index = 1
|
|
state.attempt_index = 1 -- first attempt is used up
|
|
|
|
local function calc_slack(tw, near, far)
|
|
local fullsize = far * 2 + 1
|
|
local count = ceil(tw / fullsize)
|
|
local overrun = count * fullsize - tw
|
|
local slack = overrun % 2
|
|
--local start = far-near-floor(overrun / 2) - slack
|
|
local start = (far-near)-floor(overrun / 2) - slack
|
|
return count, start, slack
|
|
end
|
|
|
|
local countx, slackw2, modx = calc_slack(c.tw, floor(m.size/2), floor(m.area/2))
|
|
local county, slackh2, mody = calc_slack(c.th, floor(m.size/2), floor(m.area/2))
|
|
|
|
for sy = slackh2, slackh2 + mody do
|
|
for sx = slackw2, slackw2 + modx do
|
|
attempts[#attempts+1] = {
|
|
sx,
|
|
sy,
|
|
cx = countx,
|
|
cy = county,
|
|
slackw = slackw2,
|
|
slackh = slackh2,
|
|
modx = modx,
|
|
mody = mody,
|
|
}
|
|
end
|
|
end
|
|
|
|
return "init_layout_attempt"
|
|
end
|
|
|
|
---@param self SimpleLayout
|
|
---@param state SimpleState
|
|
function layout:init_layout_attempt(state)
|
|
|
|
local attempt = state.attempts[state.attempt_index]
|
|
|
|
state.best_attempt = self:_placement_attempt(state, attempt[1], attempt[2])
|
|
state.best_attempt_score = #state.best_attempt.miners
|
|
|
|
if state.debug_dump then
|
|
state.best_attempt.heuristic_score = state.best_attempt_score
|
|
state.saved_attempts = {}
|
|
state.saved_attempts[#state.saved_attempts+1] = state.best_attempt
|
|
end
|
|
|
|
if #state.attempts > 1 then
|
|
return "layout_attempt"
|
|
end
|
|
|
|
return "prepare_miner_layout"
|
|
end
|
|
|
|
function layout:_get_layout_heuristic(state)
|
|
return function(attempt) return #attempt.miners end
|
|
end
|
|
|
|
---Bruteforce the best solution
|
|
---@param self SimpleLayout
|
|
---@param state SimpleState
|
|
function layout:layout_attempt(state)
|
|
local attempt_state = state.attempts[state.attempt_index]
|
|
|
|
---@type PlacementAttempt
|
|
local current_attempt = self:_placement_attempt(state, attempt_state[1], attempt_state[2])
|
|
local current_attempt_score = #current_attempt.miners
|
|
|
|
if current_attempt_score < state.best_attempt_score then
|
|
state.best_attempt_index = state.attempt_index
|
|
state.best_attempt = current_attempt
|
|
state.best_attempt_score = current_attempt_score
|
|
end
|
|
|
|
if state.attempt_index >= #state.attempts then
|
|
return "prepare_miner_layout"
|
|
end
|
|
state.attempt_index = state.attempt_index + 1
|
|
return true
|
|
end
|
|
|
|
---@param self SimpleLayout
|
|
---@param state SimpleState
|
|
---@return PlacementSpecification[]
|
|
function layout:_get_pipe_layout_specification(state)
|
|
local pipe_layout = {}
|
|
|
|
local M = state.miner
|
|
local attempt = state.best_attempt
|
|
local gutter = M.outer_span
|
|
|
|
for _, pre_lane in pairs(state.miner_lanes) do
|
|
if not pre_lane[1] then goto continue_lanes end
|
|
local y = pre_lane[1].y + M.pipe_left
|
|
local sx = attempt.sx + M.outer_span - 1
|
|
---@type MinerPlacement[]
|
|
local lane = table.mapkey(pre_lane, function(t) return t.column end) -- make array with intentional gaps between miners
|
|
|
|
local current_start, current_length = nil, 0
|
|
for i = 1, state.miner_max_column do
|
|
local miner = lane[i]
|
|
if miner and current_start then
|
|
local start_shift = 0
|
|
if current_start == 1 then start_shift = gutter * 2 end
|
|
pipe_layout[#pipe_layout+1] = {
|
|
structure="horizontal",
|
|
x=sx + start_shift + (current_start-1) * M.area - gutter * 2 + 1,
|
|
y=y,
|
|
w=current_length * M.area+gutter * 2 - 1 - start_shift,
|
|
}
|
|
current_start, current_length = nil, 0
|
|
elseif not miner and not current_start then
|
|
current_start, current_length = i, 1
|
|
elseif current_start then
|
|
current_length = current_length + 1
|
|
elseif i > 1 then
|
|
pipe_layout[#pipe_layout+1] = {
|
|
structure="horizontal",
|
|
x=sx+(i-1)*M.area-gutter*2+1,
|
|
y=y,
|
|
w=gutter*2-1
|
|
}
|
|
end
|
|
end
|
|
|
|
::continue_lanes::
|
|
end
|
|
|
|
for i = 1, state.miner_lane_count do
|
|
local lane = attempt.lane_layout[i]
|
|
pipe_layout[#pipe_layout+1] = {
|
|
structure="cap_vertical",
|
|
x=attempt.sx + M.outer_span - 1,
|
|
y=lane.y+M.pipe_left,
|
|
skip_up=i == 1,
|
|
skip_down=i == state.miner_lane_count,
|
|
}
|
|
end
|
|
|
|
return pipe_layout
|
|
end
|
|
|
|
|
|
---@param self SimpleLayout
|
|
---@param state SimpleState
|
|
function layout:prepare_belt_layout(state)
|
|
local m = state.miner
|
|
local g = state.grid
|
|
local attempt = state.best_attempt
|
|
|
|
local miner_lanes = state.miner_lanes
|
|
local miner_lane_count = state.miner_lane_count
|
|
local miner_max_column = state.miner_max_column
|
|
|
|
for _, lane in pairs(miner_lanes) do
|
|
table.sort(lane, function(a, b) return a.x < b.x end)
|
|
end
|
|
|
|
local builder_belts = {}
|
|
state.builder_belts = builder_belts
|
|
|
|
local function get_lane_length(lane) if lane then return lane[#lane].x end return 0 end
|
|
local function que_entity(t) builder_belts[#builder_belts+1] = t end
|
|
|
|
local belt_lanes = {}
|
|
state.belts = belt_lanes
|
|
local longest_belt = 0
|
|
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.area - m.outer_span + m.area * (i-1)
|
|
|
|
local belt = {x1=attempt.sx + 1, x2=attempt.sx + 1, y=y, built=false, lane1=lane1, lane2=lane2}
|
|
belt_lanes[#belt_lanes+1] = belt
|
|
|
|
if lane1 or lane2 then
|
|
local x1 = attempt.sx + 1 + pipe_adjust
|
|
local x2 = max(get_lane_length(lane1)+m.out_x+1, get_lane_length(lane2)+ m.out_x + 1)
|
|
longest_belt = max(longest_belt, x2 - x1 + 1)
|
|
belt.x1, belt.x2, belt.built = x1, x2, true
|
|
|
|
for x = x1, x2 do
|
|
que_entity{
|
|
name=state.belt_choice,
|
|
thing="belt",
|
|
grid_x=x,
|
|
grid_y=y,
|
|
direction=defines.direction.west,
|
|
}
|
|
end
|
|
end
|
|
|
|
if lane2 then
|
|
for _, miner in ipairs(lane2) do
|
|
local out_x = m.out_x
|
|
|
|
for ny = y + 1, y + m.outer_span * 2 - 1 do
|
|
que_entity{
|
|
name=state.belt_choice,
|
|
thing="belt",
|
|
grid_x=miner.x + out_x,
|
|
grid_y=ny,
|
|
direction=defines.direction.north,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
state.belt_count = #belt_lanes
|
|
|
|
return "prepare_pole_layout"
|
|
end
|
|
|
|
---@param self SimpleLayout
|
|
---@param state SimpleState
|
|
function layout:prepare_pole_layout(state)
|
|
local c = state.coords
|
|
local m = state.miner
|
|
local G = state.grid
|
|
local P = state.pole
|
|
local attempt = state.best_attempt
|
|
|
|
local pole_proto = game.entity_prototypes[state.pole_choice] or {supply_area_distance=3, max_wire_distance=9}
|
|
local supply_area_distance, supply_radius, supply_area = 3.5, 3, 6
|
|
if pole_proto then
|
|
supply_area = pole_proto.supply_area_distance
|
|
supply_area_distance = pole_proto.supply_area_distance or supply_area_distance
|
|
supply_radius = floor(supply_area_distance)
|
|
supply_area = floor(supply_area_distance * 2)
|
|
end
|
|
|
|
local builder_power_poles = {}
|
|
state.builder_power_poles = builder_power_poles
|
|
|
|
local pole_step = min(floor(pole_proto.max_wire_distance), supply_area + 2)
|
|
state.pole_step = pole_step
|
|
|
|
local miner_lane_width = (state.miner_max_column-1) * m.area + state.miner_max_column
|
|
local pole_start = m.outer_span
|
|
|
|
local function place_pole_lane(y, iy, skip_light)
|
|
local pole_lane = {}
|
|
local ix = 1
|
|
for x = attempt.sx + pole_start, c.tw+m.size, pole_step do
|
|
local built = false
|
|
if G:needs_power(x, y, P) then
|
|
built = true
|
|
end
|
|
local pole = {x=x, y=y, ix=ix, iy=iy, built=built}
|
|
pole_lane[ix] = pole
|
|
ix = ix + 1
|
|
end
|
|
|
|
local backtrack_built = false
|
|
for pole_i = #pole_lane, 1, -1 do
|
|
local no_light = (P.wire < 9) and (pole_i % 2 == 0) or nil
|
|
---@type GridPole
|
|
local backtrack_pole = pole_lane[pole_i]
|
|
if backtrack_built or backtrack_pole.built then
|
|
backtrack_built = true
|
|
backtrack_pole.built = true
|
|
|
|
builder_power_poles[#builder_power_poles+1] = {
|
|
name=state.pole_choice,
|
|
thing="pole",
|
|
grid_x = backtrack_pole.x,
|
|
grid_y = backtrack_pole.y,
|
|
no_light = skip_light or no_light,
|
|
}
|
|
|
|
end
|
|
end
|
|
|
|
return pole_lane
|
|
end
|
|
|
|
local initial_y = attempt.sy + m.outer_span - 1
|
|
local iy, between_lane = 1, 0
|
|
local y_max, y_step = initial_y + c.th + m.area + 1, m.area * 2
|
|
for y = initial_y, y_max, y_step do
|
|
|
|
if y + y_step > y_max then -- last pole lane
|
|
local backstep = m.outer_span * 2 - 1
|
|
if state.miner_lanes[between_lane+1] then
|
|
backstep = ceil(m.size/2)
|
|
end
|
|
place_pole_lane(y - backstep)
|
|
elseif (m.outer_span * 2 + 2) > supply_area then -- single pole can't supply two lanes
|
|
place_pole_lane(y, iy)
|
|
if y ~= initial_y then
|
|
iy = iy + 1
|
|
place_pole_lane(y - m.outer_span * 2 + 1, iy, true)
|
|
end
|
|
else
|
|
local backstep = y == initial_y and 0 or ceil(m.size/2)
|
|
place_pole_lane(y - backstep)
|
|
end
|
|
iy = iy + 1
|
|
between_lane = between_lane + 2
|
|
end
|
|
|
|
return "prepare_lamp_layout"
|
|
end
|
|
|
|
return layout
|