require("init") require("utils") function MergingChests.GetChestSize(data, entity) if entity.name == data.id then return 1, 1 elseif util.string_starts_with(entity.name, "wide-"..data.type.."-chest") then return tonumber(string.sub(entity.name, string.len("wide-"..data.type.."-chest-") + 1)), 1 elseif util.string_starts_with(entity.name, "high-"..data.type.."-chest") then return 1, tonumber(string.sub(entity.name, string.len("high-"..data.type.."-chest-") + 1)) elseif util.string_starts_with(entity.name, data.type.."-warehouse-") then local width, height = table.unpack(util.split(string.sub(entity.name, string.len(data.type.."-warehouse-") + 1), "x")) return tonumber(width), tonumber(height) elseif util.string_starts_with(entity.name, data.type.."-trashdump-") then local width, height = table.unpack(util.split(string.sub(entity.name, string.len(data.type.."-trashdump-") + 1), "x")) return tonumber(width), tonumber(height) else return 0, 0 end end function MergingChests.GetChestName(data, width, height) if height == 1 then return "wide-"..data.type.."-chest-"..width elseif width == 1 then return "high-"..data.type.."-chest-"..height elseif width <= settings.startup["warehouse-threshold"].value or height <= settings.startup["warehouse-threshold"].value then return data.type.."-warehouse-"..width.."x"..height else return data.type.."-trashdump-"..width.."x"..height end end function MergingChests.MoveInventories(from, to) local j = 1 local l = 1 local toInventory = to[j].get_inventory(1) local bar = 0 for _, entity in ipairs(from) do bar = bar + entity.get_inventory(1).get_bar() - 1 end for i = 1, #from do local fromInventory = from[i].get_inventory(1) for k = 1, #fromInventory do if fromInventory[k].valid_for_read then toInventory[l].set_stack(fromInventory[k]) l = l + 1 if l > #toInventory then j = j + 1 if j > #to then -- should never happen but just to be sure goto setbar end toInventory = to[j].get_inventory(1) l = 1 end end end end -- evenly distribute bar count over chests ::setbar:: local remainingBar = bar local remainingEntites = #to for _, entity in ipairs(to) do local singleBar = math.min(math.round(remainingBar / remainingEntites), 65535) entity.get_inventory(1).set_bar(singleBar + 1) remainingBar = remainingBar - singleBar remainingEntites = remainingEntites - 1 end end function MergingChests.MoveLogisticRequests(player_index, from, to) if game.players[player_index].mod_settings["WideChestsLogistic_copy-requests-on-split"].value and #to > #from then -- copy requests from "from" entities to all "to" entities (don"t split them up) for _, entity_to in ipairs(to) do for _, entity_from in ipairs(from) do for from_slot_index = 1, entity_from.request_slot_count do local request = entity_from.get_request_slot(from_slot_index) if request then entity_to.set_request_slot(request, entity_to.request_slot_count + 1) end end end end else local to_index = 1 for _, entity_from in ipairs(from) do for from_slot_index = 1, entity_from.request_slot_count do local request = entity_from.get_request_slot(from_slot_index) if request then to[to_index].set_request_slot(request, to[to_index].request_slot_count + 1) to_index = to_index + 1 if to_index > #to then to_index = 1 end end end end end end function MergingChests.ReconnectCircuits(from, to) local fromSet = { } for _, entity in ipairs(from) do fromSet[entity] = entity end local connections = { } local red = false local green = false for _, entity in ipairs(from) do for __, connection in ipairs(entity.circuit_connection_definitions) do if not fromSet[connection.target_entity] then table.insert(connections, connection) red = red or connection.wire == defines.wire_type.red green = green or connection.wire == defines.wire_type.green end end end if #connections > 0 then --connect all "to" entities together for i = 1, #to - 1 do if red then to[i].connect_neighbour{wire = defines.wire_type.red, target_entity = to[i + 1]} end if green then to[i].connect_neighbour{wire = defines.wire_type.green, target_entity = to[i + 1]} end end for _, connection in ipairs(connections) do local closestEntity = nil local min = nil for __, entity in ipairs(to) do local diffX = entity.position.x - connection.target_entity.position.x local diffY = entity.position.y - connection.target_entity.position.y if not min or (diffX * diffX + diffY * diffY < min) then min = diffX * diffX + diffY * diffY closestEntity = entity end end closestEntity.connect_neighbour(connection) end end end -- merging functions function MergingChests.FindChestsBounds(data, entities) local minX, minY, maxX, maxY = nil, nil, nil, nil for _, entity in ipairs(entities) do local width, height = MergingChests.GetChestSize(data, entity) local floorX = math.floor(entity.position.x) local floorY = math.floor(entity.position.y) if not minX or (minX > floorX) then minX = floorX end if not minY or (minY > floorY) then minY = floorY end if not maxX or (maxX < floorX) then maxX = floorX end if not maxY or (maxY < floorY) then maxY = floorY end end return { minX = minX, minY = minY, maxX = maxX, maxY = maxY } end function MergingChests.SortIntoGroups(data, entities) local mapBounds = MergingChests.FindChestsBounds(data, entities) local chestMap = { } -- fill map for _, entity in ipairs(entities) do chestMap[math.floor(entity.position.x)] = chestMap[math.floor(entity.position.x)] or { } chestMap[math.floor(entity.position.x)][math.floor(entity.position.y)] = entity end local groups = { } local merged = false repeat merged = false local xStart, yStart, width, height = MergingChests.FindLargestChest(chestMap, mapBounds) if width > 1 or height > 1 then -- fill new group and used entities remove from map local newGroup = { } for x = xStart, xStart + width - 1 do for y = yStart, yStart + height - 1 do table.insert(newGroup, chestMap[x][y]) chestMap[x][y] = nil end end table.insert(groups, { entities = newGroup, width = width, height = height, position = { x = xStart + width / 2, y = yStart + height / 2 } }) merged = true end until not merged return groups end function MergingChests.FindLargestChest(map, area) local maxX = 0 local maxY = 0 local maxWidth = 0 local maxHeight = 0 local row = { } for x = area.minX, area.maxX do for y = area.minY, area.maxY do if map[x] and map[x][y] then row[y] = (row[y] or 0) + 1 else row[y] = 0 end end local y, width, height = MergingChests.FindLargestAreaUnderHistogram(row, area.minY, area.maxY) if width * height > maxWidth * maxHeight then maxX = x - width + 1 maxY = y maxWidth = width maxHeight = height end end return maxX, maxY, maxWidth, maxHeight end function MergingChests.FindLargestAreaUnderHistogram(row, min, max) local maxY = 0 local maxWidth = 0 local maxHeight = 0 local stack = { } local top = 0 local y = 0 local n = max - min + 1 local function CalculateAreaAndUpdate() local peak = stack[top] top = top - 1 local width = row[peak + min] local height = top == 0 and y or (y - stack[top] - 1) if maxWidth * maxHeight < width * height and MergingChests.CheckWhitelist(width, height) then maxY = y + min - height maxWidth = width maxHeight = height end end while y < n do if top == 0 or row[stack[top] + min] <= row[y + min] then top = top + 1 stack[top] = y y = y + 1 else CalculateAreaAndUpdate() end end while top > 0 do CalculateAreaAndUpdate() end return maxY, maxWidth, maxHeight end function MergingChests.CreateMergedChest(data, group, player) local newChestName if group.width > settings.startup["warehouse-threshold"].value and group.height > settings.startup["warehouse-threshold"].value then newChestName = data.type.."-trashdump-"..group.width.."x"..group.height elseif group.width > 1 and group.height > 1 then newChestName = data.type.."-warehouse-"..group.width.."x"..group.height elseif group.width > 1 then newChestName = "wide-"..data.type.."-chest-"..group.width elseif group.height > 1 then newChestName = "high-"..data.type.."-chest-"..group.height end return player.surface.create_entity{name = newChestName, position = group.position, force = player.force} end -- splitting functions function MergingChests.CreateSplitedChests(data, entity, player) local width, height = MergingChests.GetChestSize(data, entity) local position = { x = entity.position.x - (width - 1) / 2, y = entity.position.y - (height - 1) / 2 } local entities = { } for dX = 0, width - 1 do for dY = 0, height - 1 do table.insert(entities, entity.surface.create_entity{name = data.id, position = { x = position.x + dX, y = position.y + dY }, force = player.force}) end end return entities end -- event handlers function MergingChests.OnPlayerSelectedArea(event) if event.item and event.item == "merge-chest-selector" then local player = game.players[event.player_index] -- use event entities and remove everything but mergable chests local chestGroups = groupByName(event.entities) for id, entities in pairs(chestGroups) do local data = MergingChests.MergableChestIdToData[id] for _, group in ipairs(MergingChests.SortIntoGroups(data, entities)) do if #group.entities > 1 then local newChest = MergingChests.CreateMergedChest(data, group, player) MergingChests.MoveInventories(group.entities, { newChest }) if data.logistic then MergingChests.MoveLogisticRequests(event.player_index, group.entities, { newChest }) end MergingChests.ReconnectCircuits(group.entities, { newChest }) for _, entity in ipairs(group.entities) do entity.destroy() end end end end end end function MergingChests.OnPlayerAltSelectedArea(event) if event.item and event.item == "merge-chest-selector" then local player = game.players[event.player_index] -- use event entities and remove everything but merged chests for _, data in pairs(MergingChests.MergableChestIdToData) do local entities = event.entities for i = #entities, 1, -1 do local entity = entities[i] if math.max(MergingChests.GetChestSize(data, entity)) > 1 then local newEntities = MergingChests.CreateSplitedChests(data, entity, player) MergingChests.MoveInventories({ entity }, newEntities) if data.logistic then MergingChests.MoveLogisticRequests(event.player_index, { entity }, newEntities) end MergingChests.ReconnectCircuits({ entity }, newEntities) table.remove(entities, i) entity.destroy() end end end end end function MergingChests.OnShortCut(event) if event.prototype_name == "merge-chest-selector" then local player = game.players[event.player_index] if player.clear_cursor() then local stack = player.cursor_stack if player.cursor_stack and stack.can_set_stack({ name = "merge-chest-selector" }) then stack.set_stack({ name = "merge-chest-selector" }) end end end end function MergingChests.RotateEntityClockwise(entity) entity.position = { x = -entity.position.y, y = entity.position.x } entity.direction = ((entity.direction or 0) + 2) % 8 end function MergingChests.RotateEntityCounterclockwise(entity) entity.position = { x = entity.position.y, y = -entity.position.x } entity.direction = ((entity.direction or 0) - 2 + 8) % 8 end function MergingChests.RotateTileClockwise(tile) tile.position = { x = -tile.position.y - 1, y = tile.position.x } end function MergingChests.RotateTileCounterclockwise(tile) tile.position = { x = tile.position.y, y = -tile.position.x - 1 } end function MergingChests.GetBlueprintInHand(player_index) player = game.players[player_index] local cursor = player.cursor_stack if player.is_cursor_blueprint() and cursor.valid_for_read then if cursor.is_blueprint_book and cursor.active_index then local blueprint_inventory = cursor.get_inventory(defines.inventory.item_main) if blueprint_inventory.get_item_count() == 0 then return nil end cursor = blueprint_inventory[cursor.active_index] end for _, entity in ipairs(cursor.get_blueprint_entities() or {}) do for _, data in pairs(MergingChests.MergableChestIdToData) do local width, height = MergingChests.GetChestSize(data, entity) if math.max(width, height) > 1 and not MergingChests.CheckWhitelist(height, width) then return nil end end end return cursor end return nil end function MergingChests.HandleBlueprintRotate(player_index, rotate_entity, rotate_tile) local blueprint = MergingChests.GetBlueprintInHand(player_index) if blueprint ~= nil then local entities = blueprint.get_blueprint_entities() for _, entity in ipairs(entities) do local is_merged_chest = false for _, data in pairs(MergingChests.MergableChestIdToData) do local width, height = MergingChests.GetChestSize(data, entity) if width ~= height then entity.name = MergingChests.GetChestName(data, height, width) rotate_entity(entity) is_merged_chest = true break end end if not is_merged_chest then rotate_entity(entity) end end blueprint.set_blueprint_entities(entities) local tiles = blueprint.get_blueprint_tiles() if tiles ~= nil then for _, tile in ipairs(tiles) do rotate_tile(tile) end blueprint.set_blueprint_tiles(tiles) end end end function MergingChests.OnRotateBlueprintClockwise(event) MergingChests.HandleBlueprintRotate(event.player_index, MergingChests.RotateEntityClockwise, MergingChests.RotateTileClockwise) end function MergingChests.OnRotateBlueprintCounterClockwise(event) MergingChests.HandleBlueprintRotate(event.player_index, MergingChests.RotateEntityCounterclockwise, MergingChests.RotateTileCounterclockwise) end script.on_event(defines.events.on_player_selected_area, MergingChests.OnPlayerSelectedArea) script.on_event(defines.events.on_player_alt_selected_area, MergingChests.OnPlayerAltSelectedArea) script.on_event(defines.events.on_lua_shortcut, MergingChests.OnShortCut) script.on_event("WideChests_rotate-blueprint-clockwise", MergingChests.OnRotateBlueprintClockwise) script.on_event("WideChests_rotate-blueprint-couterclockwise", MergingChests.OnRotateBlueprintCounterClockwise)