211 lines
5.9 KiB
Lua
211 lines
5.9 KiB
Lua
local bounding_box = require("__flib__/bounding-box")
|
|
|
|
--- @param box BoundingBox
|
|
--- @return number
|
|
function bounding_box.area(box)
|
|
return bounding_box.width(box) * bounding_box.height(box)
|
|
end
|
|
|
|
--- @alias ChestGroup
|
|
--- | { entities: LuaEntity[], bounding_box: BoundingBox, merged_chest_name: string }
|
|
|
|
--- @param event chest_merged_event
|
|
local function raise_on_chest_merged(event)
|
|
script.raise_event(MergingChests.on_chest_merged_event_name, event)
|
|
end
|
|
|
|
--- @param entities LuaEntity[]
|
|
--- @return { [string]: LuaEntity[] }
|
|
local function group_by_name(entities)
|
|
local groups = { }
|
|
for _, entity in ipairs(entities) do
|
|
local entity_name = entity.name
|
|
if not groups[entity_name] then
|
|
groups[entity_name] = { }
|
|
end
|
|
table.insert(groups[entity_name], entity)
|
|
end
|
|
return groups
|
|
end
|
|
|
|
--- @alias Histogram
|
|
--- | number[]
|
|
--- | { min_index: integer, max_index: integer }
|
|
|
|
--- @param histogram Histogram
|
|
--- @param is_size_allowed fun(width: integer, height: integer): boolean
|
|
--- @return integer index, integer width, integer height
|
|
local function find_largest_rectangle_under_histogram(histogram, is_size_allowed)
|
|
local max_index = 0
|
|
local max_width = 0
|
|
local max_height = 0
|
|
|
|
local stack = { }
|
|
local top = 0
|
|
local index = 0
|
|
|
|
local function calculate_area_and_update()
|
|
local peak = stack[top]
|
|
top = top - 1
|
|
|
|
local width = histogram[peak + histogram.min_index]
|
|
local height = top == 0 and index or (index - stack[top] - 1)
|
|
|
|
if width * height > max_width * max_height and is_size_allowed(width, height) then
|
|
max_index = index + histogram.min_index - height
|
|
max_width = width
|
|
max_height = height
|
|
end
|
|
end
|
|
|
|
while index < histogram.max_index - histogram.min_index + 1 do
|
|
if top == 0 or histogram[stack[top] + histogram.min_index] <= histogram[index + histogram.min_index] then
|
|
top = top + 1
|
|
stack[top] = index
|
|
index = index + 1
|
|
else
|
|
calculate_area_and_update()
|
|
end
|
|
end
|
|
|
|
while top > 0 do
|
|
calculate_area_and_update()
|
|
end
|
|
|
|
return max_index, max_width, max_height
|
|
end
|
|
|
|
--- @param grid Grid
|
|
--- @param is_size_allowed fun(width: integer, height: integer): boolean
|
|
--- @return BoundingBox | nil
|
|
local function find_largest_rectangle(grid, is_size_allowed)
|
|
--- @type BoundingBox | nil
|
|
local rectangle = nil
|
|
|
|
--- @type Histogram
|
|
local histogram = {
|
|
min_index = grid.min_y,
|
|
max_index = grid.max_y
|
|
}
|
|
for x = grid.min_x, grid.max_x do
|
|
for y = grid.min_y, grid.max_y do
|
|
if grid[x] and grid[x][y] then
|
|
histogram[y] = (histogram[y] or 0) + 1
|
|
else
|
|
histogram[y] = 0
|
|
end
|
|
end
|
|
|
|
local y_start, width, height = find_largest_rectangle_under_histogram(histogram, is_size_allowed)
|
|
|
|
if width * height > (rectangle and bounding_box.area(rectangle) or 0) then
|
|
rectangle = {
|
|
left_top = {
|
|
x = x - width + 1,
|
|
y = y_start
|
|
},
|
|
right_bottom = {
|
|
x = x + 1,
|
|
y = y_start + height
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
return rectangle
|
|
end
|
|
|
|
--- @param entities LuaEntity[]
|
|
--- @return ChestGroup[]
|
|
local function group_chests(entities)
|
|
local chest_name = entities[1].name
|
|
local chest_grid = MergingChests.entities_to_grid(entities)
|
|
|
|
local groups = { }
|
|
local merged = false
|
|
|
|
repeat
|
|
merged = false
|
|
|
|
--- @param width integer
|
|
--- @param height integer
|
|
--- @return boolean
|
|
local function is_size_allowed(width, height)
|
|
return game.entity_prototypes[MergingChests.get_merged_chest_name(chest_name, width, height)] ~= nil
|
|
end
|
|
|
|
local rectangle = find_largest_rectangle(chest_grid, is_size_allowed)
|
|
|
|
if rectangle and (bounding_box.width(rectangle) > 1 or bounding_box.height(rectangle) > 1) then
|
|
--- @type ChestGroup
|
|
local group = {
|
|
entities = { },
|
|
merged_chest_name = MergingChests.get_merged_chest_name(chest_name, bounding_box.width(rectangle), bounding_box.height(rectangle)),
|
|
bounding_box = rectangle
|
|
}
|
|
for x = rectangle.left_top.x, rectangle.right_bottom.x - 1 do
|
|
for y = rectangle.left_top.y, rectangle.right_bottom.y - 1 do
|
|
table.insert(group.entities, chest_grid[x][y])
|
|
chest_grid[x][y] = nil
|
|
end
|
|
end
|
|
|
|
table.insert(groups, group)
|
|
merged = true
|
|
end
|
|
until not merged
|
|
|
|
return groups
|
|
end
|
|
|
|
--- @param player LuaPlayer
|
|
--- @param chest_name string
|
|
--- @param position MapPosition
|
|
--- @return LuaEntity?
|
|
local function create_merged_chest(player, chest_name, position)
|
|
return player.surface.create_entity({
|
|
name = chest_name,
|
|
position = position,
|
|
force = player.force,
|
|
raise_built = true
|
|
})
|
|
end
|
|
|
|
local function on_player_selected_area(event)
|
|
if event.item and event.item == MergingChests.merge_selection_tool_name then
|
|
local player = game.players[event.player_index]
|
|
|
|
local entity_groups = group_by_name(event.entities)
|
|
for _, entities in pairs(entity_groups) do
|
|
for _, chest_group_to_merge in ipairs(group_chests(entities)) do
|
|
if MergingChests.can_move_inventories(chest_group_to_merge.entities, chest_group_to_merge.merged_chest_name, bounding_box.area(chest_group_to_merge.bounding_box)) then
|
|
local merged_chest = create_merged_chest(player, chest_group_to_merge.merged_chest_name, bounding_box.center(chest_group_to_merge.bounding_box))
|
|
if merged_chest then
|
|
MergingChests.move_inventories(chest_group_to_merge.entities, { merged_chest })
|
|
MergingChests.move_inventory_bar(chest_group_to_merge.entities, { merged_chest })
|
|
MergingChests.reconnect_circuits(chest_group_to_merge.entities, { merged_chest })
|
|
|
|
raise_on_chest_merged({
|
|
player_index = event.player_index,
|
|
surface = event.surface,
|
|
merged_chest = merged_chest,
|
|
split_chests = chest_group_to_merge.entities
|
|
})
|
|
|
|
for _, entity in ipairs(chest_group_to_merge.entities) do
|
|
entity.destroy({ raise_destroy = true })
|
|
end
|
|
end
|
|
else
|
|
player.create_local_flying_text({
|
|
text = 'flying-text.'..MergingChests.prefix_with_modname('items-would-be-deleted-merge'),
|
|
position = chest_group_to_merge.entities[1].position
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
script.on_event(defines.events.on_player_selected_area, on_player_selected_area)
|