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)