--this adds a button that stashes/sorts your inventory into nearby chests in some kind of intelligent way - mewmew -- modified by gerkiz local Global = require 'utils.global' local SpamProtection = require 'utils.spam_protection' local Color = require 'utils.color_presets' local Event = require 'utils.event' local BottomFrame = require 'utils.gui.bottom_frame' local Gui = require 'utils.gui' local Task = require 'utils.task_token' local auto_stash_button_name = Gui.uid_name() local floor = math.floor local module_name = '[color=blue][Autostash][/color] ' local this = { floating_text_y_offsets = {}, whitelist = {}, insert_to_neutral_chests = false, insert_into_furnace = false, insert_into_wagon = false, bottom_button = false, small_radius = 2, limit_containers = 50, enabled = true } local Public = { } Global.register( this, function (t) this = t end ) local bps_blacklist = { ['blueprint-book'] = true, ['blueprint'] = true } local on_init_token = Task.register( function () local tooltip if this.insert_into_furnace and this.insert_into_wagon then tooltip = 'Sort your inventory into nearby chests.\nLMB: Everything, excluding quickbar items.\nRMB: Only ores to nearby chests, excluding quickbar items.\nCTRL+RMB: Fill nearby furnaces.\nSHIFT+LMB: Everything onto filtered slots to wagon/chests.\nSHIFT+RMB: Only ores to wagon' elseif this.insert_into_furnace then tooltip = 'Sort your inventory into nearby chests.\nLMB: Everything, excluding quickbar items.\nRMB: Only ores to nearby chests, excluding quickbar items.\nCTRL+RMB: Fill nearby furnaces.' elseif this.insert_into_wagon then tooltip = 'Sort your inventory into nearby chests.\nLMB: Everything, excluding quickbar items.\nRMB: Only ores to nearby chests, excluding quickbar items.\nSHIFT+LMB: Everything onto filtered slots to wagon/chests.\nSHIFT+RMB: Only ores to wagon' else tooltip = 'Sort your inventory into nearby chests.\nLMB: Everything, excluding quickbar items.\nRMB: Only ores to nearby chests, excluding quickbar items.' end this.tooltip = tooltip end ) local delay_tooltip_token = Task.register( function (event) local player_index = event.player_index local player = game.get_player(player_index) if not player or not player.valid then return end if Gui.get_mod_gui_top_frame() then local frame = Gui.get_button_flow(player)[auto_stash_button_name] if frame and frame.valid then frame.tooltip = this.tooltip end else local frame = player.gui.top[auto_stash_button_name] if frame and frame.valid then frame.tooltip = this.tooltip end end BottomFrame.add_inner_frame({ player = player, element_name = auto_stash_button_name, tooltip = this.tooltip, sprite = 'item/wooden-chest' }) end ) local function create_floaty_text(surface, position, name, count) if this.floating_text_y_offsets[position.x .. '_' .. position.y] then this.floating_text_y_offsets[position.x .. '_' .. position.y] = this.floating_text_y_offsets[position.x .. '_' .. position.y] - 0.5 else this.floating_text_y_offsets[position.x .. '_' .. position.y] = 0 end surface.create_entity( { name = 'flying-text', position = { position.x, position.y + this.floating_text_y_offsets[position.x .. '_' .. position.y] }, text = { '', '-', count, ' ', game.item_prototypes[name].localised_name }, color = { r = 255, g = 255, b = 255 } } ) end local function prepare_floaty_text(list, surface, position, name, count) local str = surface.index .. ',' .. position.x .. ',' .. position.y if not list[str] then list[str] = {} end if not list[str][name] then list[str][name] = { surface = surface, position = position, count = 0 } end list[str][name].count = list[str][name].count + count end local function chest_is_valid(chest) if this.dungeons_initial_level ~= nil then -- transport chests always are valid targets if chest.name == 'blue-chest' or chest.name == 'red-chest' then return true end end for _, e in pairs( chest.surface.find_entities_filtered( { type = { 'inserter', 'loader' }, area = { { chest.position.x - 1, chest.position.y - 1 }, { chest.position.x + 1, chest.position.y + 1 } } } ) ) do if e.name ~= 'long-handed-inserter' then if e.position.x == chest.position.x then if e.direction == 0 or e.direction == 4 then return false end end if e.position.y == chest.position.y then if e.direction == 2 or e.direction == 6 then return false end end end end local i1 = chest.surface.find_entity('long-handed-inserter', { chest.position.x - 2, chest.position.y }) if i1 then if i1.direction == 2 or i1.direction == 6 then return false end end local i2 = chest.surface.find_entity('long-handed-inserter', { chest.position.x + 2, chest.position.y }) if i2 then if i2.direction == 2 or i2.direction == 6 then return false end end local i3 = chest.surface.find_entity('long-handed-inserter', { chest.position.x, chest.position.y - 2 }) if i3 then if i3.direction == 0 or i3.direction == 4 then return false end end local i4 = chest.surface.find_entity('long-handed-inserter', { chest.position.x, chest.position.y + 2 }) if i4 then if i4.direction == 0 or i4.direction == 4 then return false end end return true end local function sort_entities_by_distance(position, entities) local t = {} local distance local index local size_of_entities = #entities if size_of_entities < 2 then return entities end for _, entity in pairs(entities) do distance = (entity.position.x - position.x) ^ 2 + (entity.position.y - position.y) ^ 2 index = floor(distance) + 1 if not t[index] then t[index] = {} end table.insert(t[index], entity) end local i = 0 local containers = {} for _, range in pairs(t) do for _, entity in pairs(range) do i = i + 1 if i >= (this.limit_containers or 50) then return containers end containers[i] = entity end end return containers end local function get_nearby_chests(player, a, furnace, wagon) local r = player.force.character_reach_distance_bonus + 10 local r_square = r * r local chests, inventories = {}, {} local size_of_chests = 0 local area = { { player.position.x - r, player.position.y - r }, { player.position.x + r, player.position.y + r } } area = a or area local container_type = { 'container', 'logistic-container', 'linked-container' } local inventory_type = defines.inventory.chest local containers = {} local i = 0 if furnace then container_type = { 'furnace' } inventory_type = defines.inventory.furnace_source end if wagon then container_type = { 'cargo-wagon', 'logistic-container' } inventory_type = defines.inventory.cargo_wagon end local forces = player.force if this.insert_to_neutral_chests then forces = { player.force, 'neutral' } end for _, e in pairs(player.surface.find_entities_filtered({ type = container_type, area = area, force = forces })) do if ((player.position.x - e.position.x) ^ 2 + (player.position.y - e.position.y) ^ 2) <= r_square then i = i + 1 containers[i] = e end end containers = sort_entities_by_distance(player.position, containers) for _, entity in pairs(containers) do size_of_chests = size_of_chests + 1 chests[size_of_chests] = entity inventories[size_of_chests] = entity.get_inventory(inventory_type) end return { chest = chests, inventory = inventories } end local function insert_to_furnace(player_inventory, chests, name, count, floaty_text_list) local try = 0 local to_insert = floor(count / #chests.chest) if to_insert <= 0 then if count > 0 then to_insert = count else return end end local variate = count % #chests.chest local chests_available = #chests.chest local tries = #chests.chest ::retry:: --Attempt to store into furnaces. for chestnr, chest in pairs(chests.chest) do local chest_inventory = chests.inventory[chestnr] local amount = to_insert if variate > 0 then amount = amount + 1 variate = variate - 1 end if amount <= 0 then return end if chest_inventory then if (chest.type == 'furnace' or chest.type == 'assembling-machine') then if name == 'stone' then local valid_to_insert = (amount % 2 == 0) if valid_to_insert then if chest_inventory.can_insert({ name = name, count = amount }) then local inserted_count = chest_inventory.insert({ name = name, count = amount }) if inserted_count < 0 then return end player_inventory.remove({ name = name, count = inserted_count }) prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) count = count - inserted_count if count <= 0 then return end end else try = try + 1 if try <= tries then chests_available = chests_available - 1 to_insert = floor(count / chests_available) variate = count % chests_available goto retry end end else if chest_inventory.can_insert({ name = name, count = amount }) then local inserted_count = chest_inventory.insert({ name = name, count = amount }) if inserted_count < 0 then return end player_inventory.remove({ name = name, count = inserted_count }) prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) count = count - inserted_count if count <= 0 then return end end end end end end to_insert = floor(count / #chests.chest) variate = count % #chests.chest for _, chest in pairs(chests.chest) do -- fuel if chest.type == 'furnace' or chest.type == 'assembling-machine' then local amount = to_insert if variate > 0 then amount = amount + 1 variate = variate - 1 end if amount <= 0 then return end local chest_inventory = chest.get_inventory(defines.inventory.chest) if chest_inventory and chest_inventory.can_insert({ name = name, count = amount }) then local inserted_count = chest_inventory.insert({ name = name, count = amount }) if inserted_count < 0 then return end player_inventory.remove({ name = name, count = inserted_count }) prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) count = count - inserted_count if count <= 0 then return end end end end end local function insert_into_wagon(stack, chests, name, floaty_text_list) -- Attempt to load filtered cargo wagon for chestnr, chest in pairs(chests.chest) do if chest.type == 'cargo-wagon' then local chest_inventory = chests.inventory[chestnr] if chest_inventory.can_insert(stack) then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end end end end local function insert_into_wagon_filtered(stack, chests, name, floaty_text_list) -- Attempt to load filtered cargo wagon for chestnr, chest in pairs(chests.chest) do if chest.type == 'cargo-wagon' then local chest_inventory = chests.inventory[chestnr] for index = 1, 40 do if chest_inventory.can_insert(stack) then if chest_inventory.get_filter(index) ~= nil then local n = chest_inventory.get_filter(index) if n == name then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end end end end end end -- Attempt to load filtered slots for chestnr, chest in pairs(chests.chest) do if chest.type == 'logistic-container' then local chest_inventory = chests.inventory[chestnr] for index = 1, chest.request_slot_count do if chest_inventory.can_insert(stack) then if chest.get_request_slot(index) ~= nil then local n = chest.get_request_slot(index) if n and n.name == name then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end end end end end end end local function insert_item_into_chest(stack, chests, filtered_chests, name, floaty_text_list, previous_insert) local container = { ['container'] = true, ['logistic-container'] = true, ['linked-container'] = true } --Attemp to store in chest that stored last same item if previous_insert.name == name and previous_insert.full ~= nil then local chest_inventory = chests.inventory[previous_insert.full] if chest_inventory and chest_inventory.can_insert(stack) then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chests.chest[previous_insert.full].surface, chests.chest[previous_insert.full].position, name, inserted_count) if stack.count <= 0 then return previous_insert.full end end end --- Attempt to store in req slots that are filtered for chestnr, chest in pairs(chests.chest) do if chest.type == 'logistic-container' then local chest_inventory = chests.inventory[chestnr] for index = 1, chest.request_slot_count do if chest_inventory.can_insert(stack) then if chest.get_request_slot(index) ~= nil then local n = chest.get_request_slot(index) if n and n.name == name then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end end end end end end --Attempt to store in chests that already have the same item. for chestnr, chest in pairs(chests.chest) do if container[chest.type] then if chest.request_slot_count and chest.request_slot_count > 0 then goto continue end local chest_inventory = chests.inventory[chestnr] if chest_inventory and chest_inventory.find_item_stack(stack.name) then if chest_inventory.can_insert(stack) then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end end ::continue:: end end --Attempt to store in empty chests. for chestnr, chest in pairs(filtered_chests.chest) do if container[chest.type] then if chest.request_slot_count and chest.request_slot_count > 0 then goto continue end local chest_inventory = filtered_chests.inventory[chestnr] if not chest_inventory then break end local count = chest_inventory.get_item_count() == 0 if count then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end ::continue:: end end local item_prototypes = game.item_prototypes --Attempt to store in chests with same item subgroup. local item_subgroup = game.item_prototypes[name].subgroup.name if item_subgroup then for chestnr, chest in pairs(filtered_chests.chest) do if chest.request_slot_count and chest.request_slot_count > 0 then goto continue end if container[chest.type] then local chest_inventory = filtered_chests.inventory[chestnr] if not chest_inventory then break end local content = chest_inventory.get_contents() if chest_inventory.can_insert(stack) then for equal_name, _ in pairs(content) do local t = item_prototypes[equal_name] if t and t.subgroup.name == item_subgroup then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end end end end ::continue:: end end --Attempt to store in mixed chests. for chestnr, chest in pairs(filtered_chests.chest) do if container[chest.type] then if chest.request_slot_count and chest.request_slot_count > 0 then goto continue end local chest_inventory = filtered_chests.inventory[chestnr] if not chest_inventory then break end if chest_inventory.can_insert(stack) then local inserted_count = chest_inventory.insert(stack) stack.count = stack.count - inserted_count prepare_floaty_text(floaty_text_list, chest.surface, chest.position, name, inserted_count) if stack.count <= 0 then return chestnr end end ::continue:: end end end local function auto_stash(player, event) local button = event.button local ctrl = event.control local shift = event.shift if not player.character then player.print(module_name .. 'It seems that you are not in the realm of the living.', Color.warning) return end if not player.character.valid then player.print(module_name .. 'It seems that you are not in the realm of the living.', Color.warning) return end local inventory = player.get_main_inventory() if inventory.is_empty() then player.print(module_name .. 'Inventory is empty.', Color.warning) return end local floaty_text_list = {} local chests = { chest = {}, inventory = {} } local r = this.small_radius local area = { { player.position.x - r, player.position.y - r }, { player.position.x + r, player.position.y + r } } if ctrl then if button == defines.mouse_button_type.right and this.insert_into_furnace then chests = get_nearby_chests(player, nil, true, false) end elseif shift then if button == defines.mouse_button_type.right and this.insert_into_wagon or button == defines.mouse_button_type.left and this.insert_into_wagon then chests = get_nearby_chests(player, area, false, true) end else chests = get_nearby_chests(player) end if not chests.chest or not chests.chest[1] then player.print(module_name .. 'No valid nearby containers found.', Color.warning) return end local filtered_chests = { chest = {}, inventory = {} } for index, e in pairs(chests.chest) do if chest_is_valid(e) then filtered_chests.chest[index] = e filtered_chests.inventory[index] = chests.inventory[index] end end this.floating_text_y_offsets = {} local hotbar_items = {} for i = 1, 100, 1 do local prototype = player.get_quick_bar_slot(i) if prototype then hotbar_items[prototype.name] = true end end local furnaceList = { ['coal'] = 0, ['iron-ore'] = 0, ['copper-ore'] = 0, ['stone'] = 0 } local full_insert = { full = nil, name = nil } for i = #inventory, 1, -1 do if not inventory[i].valid_for_read then goto continue end local name = inventory[i].name local is_resource = this.whitelist[name] if not hotbar_items[name] and not bps_blacklist[name] then if ctrl and this.insert_into_furnace then if button == defines.mouse_button_type.right then if is_resource then furnaceList[name] = (furnaceList[name] or 0) + inventory[i].count end end elseif shift and this.insert_into_wagon then -- insert into wagon if button == defines.mouse_button_type.right then -- insert all ores into wagon if is_resource then full_insert = { full = insert_into_wagon(inventory[i], chests, name, floaty_text_list), name = name } end end if button == defines.mouse_button_type.left then -- insert all filtered into wagon full_insert = { full = insert_into_wagon_filtered(inventory[i], chests, name, floaty_text_list), name = name } end elseif button == defines.mouse_button_type.right then -- only ores to nearby chests if is_resource then full_insert = { full = insert_item_into_chest(inventory[i], chests, filtered_chests, name, floaty_text_list, full_insert), name = name } end elseif button == defines.mouse_button_type.left then -- all items to nearby chests full_insert = { full = insert_item_into_chest(inventory[i], chests, filtered_chests, name, floaty_text_list, full_insert), name = name } end if not full_insert.success then hotbar_items[#hotbar_items + 1] = name end end ::continue:: end for furnaceName, furnaceCount in pairs(furnaceList) do insert_to_furnace(inventory, chests, furnaceName, furnaceCount, floaty_text_list) end for _, texts in pairs(floaty_text_list) do for name, text in pairs(texts) do create_floaty_text(text.surface, text.position, name, text.count) end end local c = this.floating_text_y_offsets for k, _ in pairs(c) do this.floating_text_y_offsets[k] = nil end end local function create_gui_button(player, bottom_frame_data) local tooltip = this.tooltip local button bottom_frame_data = bottom_frame_data or BottomFrame.get_player_data(player) if Gui.get_mod_gui_top_frame() then button = Gui.add_mod_button( player, { type = 'sprite-button', name = auto_stash_button_name, sprite = 'item/wooden-chest', tooltip = tooltip, style = Gui.button_style } ) if button then button.style.font_color = { 165, 165, 165 } button.style.font = 'heading-3' button.style.minimal_height = 36 button.style.maximal_height = 36 button.style.minimal_width = 40 button.style.padding = -2 end else button = player.gui.top[auto_stash_button_name] or player.gui.top.add( { type = 'sprite-button', sprite = 'item/wooden-chest', name = auto_stash_button_name, tooltip = tooltip, style = Gui.button_style } ) button.style.font_color = { r = 0.11, g = 0.8, b = 0.44 } button.style.font = 'heading-1' button.style.minimal_height = 40 button.style.maximal_width = 40 button.style.minimal_width = 38 button.style.maximal_height = 38 button.style.padding = 1 button.style.margin = 0 end if this.bottom_button then if bottom_frame_data ~= nil and not bottom_frame_data.top then if button and button.valid then button.destroy() end end end end local function do_whitelist() if not this.enabled then return end Task.delay(on_init_token, {}) local resources = game.entity_prototypes local items = game.item_prototypes this.whitelist = {} for k, _ in pairs(resources) do if resources[k] and resources[k].type == 'resource' and resources[k].mineable_properties then if resources[k].mineable_properties.products and resources[k].mineable_properties.products[1] then local r = resources[k].mineable_properties.products[1].name this.whitelist[r] = true elseif resources[k].mineable_properties.products and resources[k].mineable_properties.products[2] then local r = resources[k].mineable_properties.products[2].name this.whitelist[r] = true end end end for k, _ in pairs(items) do if items[k] and items[k].group.name == 'resource-refining' then local r = items[k].name this.whitelist[r] = true end end end local function on_player_joined_game(event) if not this.enabled then return end local player = game.get_player(event.player_index) create_gui_button(player) if this.bottom_button then Task.delay(delay_tooltip_token, { player_index = player.index }) end end Gui.on_click( auto_stash_button_name, function (event) local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Autostash click') if is_spamming then return end local player = event.player if not player or not player.valid or not player.character then return end auto_stash(event.player, event) end ) function Public.insert_into_furnace(value) this.insert_into_furnace = value or false end function Public.insert_into_wagon(value) this.insert_into_wagon = value or false end function Public.bottom_button(value) this.bottom_button = value or false end function Public.limit_containers(value) this.limit_containers = value or 50 end function Public.insert_to_neutral_chests(value) this.insert_to_neutral_chests = value or false end function Public.set_dungeons_initial_level(value) this.dungeons_initial_level = value end function Public.set_enabled(value) this.enabled = value or false end Event.on_configuration_changed(do_whitelist) Event.on_init(do_whitelist) Event.add(defines.events.on_player_joined_game, on_player_joined_game) Event.add( BottomFrame.events.bottom_quickbar_location_changed, function (event) if not this.enabled then return end local player_index = event.player_index if not player_index then return end local player = game.get_player(player_index) if not player or not player.valid then return end local bottom_frame_data = event.data create_gui_button(player, bottom_frame_data) end ) return Public