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