--[[ Minetest+specing Client-Side Mod (CSM) for automatically moving items between inventories Copyright (C) 2021 Fedja Beader This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --]] -- general idea: -- register multiple locations (node, inventory, detached) hereafter refered to as 'machines' -- each machine can be either a converter or storage -- each converter has an associated recipe -- -> accepts predetermined amount of different items and produces an amount of output items -- -> for now, we assume that output is only one. (battery recipe has two - bucket and battery) -- -- in each step, we: -- 1) pick one machine from the list -- 2) go through all other machines and collect available amount of input material -- 3) move as much input material as possible (up to max_stack), such that there will be no recipe leftovers --[[ register_on_jumpdrive_jump(function (jump) l:info("translating coordinates on jump") for i,m in pairs(machines) do translate_position( jump, m.pos) end end) --]] local l = Logger:new("invnet") local mod_storage = core.get_mod_storage() local invnet = minetest.deserialize(mod_storage:get_string("network")) or {} invnet.enable = invnet.enable or false invnet.machines = invnet.machines or {} local function invnet_reset() invnet.enable = false invnet.machines = {} end local function invnet_list() for i,m in pairs(invnet.machines) do l:info("Machine #" .. i .. " at " .. minetest.pos_to_string(m.pos) .. ": ") for name,count in pairs(m.recipe or {}) do l:info(" recipe consumes " .. count .. " of " .. name) end for name,count in pairs(m.output or {}) do l:info(" recipe produces " .. count .. " of " .. name) end end end local function invnet_setup_item_move() -- Pick a machine local machines = invnet.machines local machine = nil for _ = 1,100 do local m_index = math.random(1, #machines) machine = machines[m_index] l:debug("Testing machine " .. m_index) if machine.recipe then -- skip chests break end end if not machine or not machine.recipe then return false end l:info("Selected machine at " .. minetest.pos_to_string(machine.pos)) -- for each input item adjust total max_stack to minimum of them all, then -- try to move at once as many stacks as there are items in the recipe local sources = {} local max_stack = math.huge -- max transfer size local total_stacks = 0 for item_name,_ in pairs(machine.recipe) do max_stack = math.min(max_stack, ItemStack(item_name):get_stack_max()) --l:debug(" corrected stack max to " .. max_stack .. " for " .. item_name) end l:debug(" corrected stack max to " .. max_stack) for item_name,count in pairs(machine.recipe) do local from = ItemLocations:new() for _,src_machine in pairs(machines) do from:collect(src_machine.pos, src_machine.output_list_name, item_name) end from:sort_by_count_dsc() -- larger stacks first for i = 1,count do if i > #from.locations or from.locations[i].count < max_stack then l:debug(" insufficient stacks for item " .. item_name .. ", got only " .. (i-1)) return false end end sources[item_name] = from total_stacks = total_stacks + count end -- Do we have enough free slots to deposit items to? local to = ItemLocations:new() to:collect(machine.pos, machine.input_list_name, "") if #to.locations < total_stacks then l:debug(" insufficient space to deposit " .. total_stacks .. " stacks") return false end -- If we made it to here, then we have enough items to craft the recipe => setup moving -- list of from_loc & to_loc & count local move_list = {} local dst_i = 1 for item_name,from in pairs(sources) do local count = machine.recipe[item_name] for i = 1,count do local src_loc = from.locations[i] local dst_loc = to.locations[dst_i] dst_i = dst_i + 1 l:debug(" will move " .. max_stack .. "*" .. item_name .. " from " .. src_loc:to_string() .. " to " .. dst_loc:to_string()) table.insert(move_list, { src = src_loc, dst = dst_loc, count = max_stack }) end end -- schedule actual moves for i,item in pairs(move_list) do queue:prepend(function() return queued_move_stack(item.src, item.dst, item.count) end) if math.mod(i,5) == 4 then -- add a small delay queue:prepend(function() return 0.2 end) end end return true end function invnet_step() if not invnet.enable then return end --l:debug("inventory network step") local my_pos = core.localplayer:get_pos() for _,m in pairs(invnet.machines) do local distance = vector.distance(my_pos, m.pos) if distance > 8 then l:info("Too far (" .. distance .. ") from inventory " .. minetest.pos_to_string(m.pos) .. ", waiting") core.after(15, invnet_step) -- use after as it takes forever return end end if invnet_setup_item_move() then -- setup as many hops as possible --queue:append(invnet_step) core.after(2, invnet_step) -- use after as it takes forever queue:walk() else core.after(4, invnet_step) -- use after as it takes forever end end local function get_machine_in_list(pos) for i,m in pairs(invnet.machines) do if vector.distance(m.pos, pos) < 0.1 then return i end end return #invnet.machines+1 end local wait_for_punch = false core.register_on_punchnode(function(pos, node) if not wait_for_punch then return end local machine_index = get_machine_in_list(pos) if node.name == "biofuel:refinery" or node.name == "biofuel:refinery_active" then local m = {} m.recipe = { ["default:stick"] = 4, } m.input_list_name = "src" m.output = { ["biofuel:bottle_fuel"] = 1 } m.output_list_name = "dst" m.pos = pos invnet.machines[machine_index] = m l:info("Added refinery") elseif node.name == "pipeworks:autocrafter" then local inv = core.get_inventory(pos) local item_string = inv["output"][1]:get_name() if item_string == "" then l:err("autocrafter unconfigured") else local output_gain = inv["output"][1]:get_count() local m = {} m.recipe = {} for _,stack in pairs(inv["recipe"]) do local name = stack:get_name() if name ~= "" then m.recipe[name] = (m.recipe[name] or 0) + 1 end end m.input_list_name = "src" m.output = { [item_string] = output_gain } m.output_list_name = "dst" m.pos = pos invnet.machines[machine_index] = m l:info("Added autocrafter for: " .. output_gain .. " of " .. item_string) end else l:info("Unknown node punched: " .. node.name .. " adding it as source chest") local inv = core.get_inventory(pos) local m = {} if inv["src"] then m.input_list_name = "src" elseif inv["main"] then m.input_list_name = "main" end if inv["dst"] then m.output_list_name = "dst" elseif inv["main"] then m.output_list_name = "main" end if m.input_list_name and m.output_list_name then m.pos = pos invnet.machines[machine_index] = m else l:err(" no inventory lists? Skipped adding.") end end mod_storage:set_string("network", minetest.serialize(invnet)) end) local function help() return ".invnet: inventory networks (automatic item moving between inventories), usage:\n" .. ".invnet add -- toggles adding nodes to the network\n" .. ".invnet list -- lists all inventories in the network\n" .. ".invnet reset -- clears the entire network\n" .. ".invnet start -- make other players jealous\n" .. ".invnet stop -- regain use of your chat\n" end core.register_chatcommand("invnet", { func = function(param) local args = string.split(param, ' ') if args[1] == "add" then if wait_for_punch then wait_for_punch = false else l:info("punch nodes to add them into inventory network. repeat this command to stop") wait_for_punch = true end elseif args[1] == "list" then invnet_list() elseif args[1] == "reset" then invnet_reset() elseif args[1] == "start" then invnet.enable = true invnet_step() elseif args[1] == "stop" then invnet.enable = false else l:info(help()) end end, })