local mod_meta = minetest.get_mod_storage() digtron.layout = {} digtron.adjacent = {} ------------------------------------------------------------------------------------ -- Inventory -- indexed by digtron_id, set to true whenever the detached inventory's contents change local dirty_inventories = {} local detached_inventory_callbacks = { -- Called when a player wants to move items inside the inventory. -- Return value: number of items allowed to move. allow_move = function(inv, from_list, from_index, to_list, to_index, count, player) --allow anything in "main" if to_list == "main" then return count end --only allow fuel items in "fuel" local stack = inv:get_stack(from_list, from_index) if minetest.get_craft_result({method="fuel", width=1, items={stack}}).time ~= 0 then return stack:get_count() end return 0 end, -- Called when a player wants to put something into the inventory. -- Return value: number of items allowed to put. -- Return value -1: Allow and don't modify item count in inventory. allow_put = function(inv, listname, index, stack, player) -- Only allow fuel items to be placed in fuel if listname == "fuel" then if minetest.get_craft_result({method="fuel", width=1, items={stack}}).time ~= 0 then return stack:get_count() else return 0 end end return stack:get_count() -- otherwise, allow all drops end, -- Called when a player wants to take something out of the inventory. -- Return value: number of items allowed to take. -- Return value -1: Allow and don't modify item count in inventory. allow_take = function(inv, listname, index, stack, player) return stack:get_count() end, -- Called after the actual action has happened, according to what was -- allowed. -- No return value. on_move = function(inv, from_list, from_index, to_list, to_index, count, player) dirty_inventories[inv:get_location().name] = true end, on_put = function(inv, listname, index, stack, player) dirty_inventories[inv:get_location().name] = true end, on_take = function(inv, listname, index, stack, player) dirty_inventories[inv:get_location().name] = true end, } -- If the detached inventory doesn't exist, reads saved metadata version of the inventory and creates it -- Doesn't do anything if the detached inventory already exists, the detached inventory is authoritative digtron.retrieve_inventory = function(digtron_id) local inv = minetest.get_inventory({type="detached", name=digtron_id}) if inv == nil then inv = minetest.create_detached_inventory(digtron_id, detached_inventory_callbacks) local inv_string = mod_meta:get_string("inv_"..digtron_id) if inv_string ~= "" then local inventory_table = minetest.deserialize(inv_string) for listname, invlist in pairs(inventory_table) do inv:set_size(listname, #invlist) inv:set_list(listname, invlist) end end end return inv end -- Stores contents of detached inventory as a metadata string local persist_inventory = function(digtron_id) local inv = minetest.get_inventory({type="detached", name=digtron_id}) if inv == nil then minetest.log("error", "[Digtron] persist_inventory attempted to record a nonexistent inventory " .. digtron_id) return end local lists = inv:get_lists() local persist = {} for listname, invlist in pairs(lists) do local inventory = {} for i, stack in ipairs(invlist) do table.insert(inventory, stack:to_string()) -- convert into strings for serialization end persist[listname] = inventory end mod_meta:set_string("inv_"..digtron_id, minetest.serialize(persist)) end minetest.register_globalstep(function(dtime) for digtron_id, _ in pairs(dirty_inventories) do persist_inventory(digtron_id) dirty_inventories[digtron_id] = nil end end) -------------------------------------------------------------------------------------- local create_new_id = function() local last_id = mod_meta:get_int("last_id") -- returns 0 when uninitialized, so 0 will never be a valid digtron_id. local new_id = last_id + 1 mod_meta:set_int("last_id", new_id) -- ensure each call to this method gets a unique number local digtron_id = "digtron_id_" .. tostring(new_id) local inv = minetest.create_detached_inventory(digtron_id, detached_inventory_callbacks) return digtron_id, inv end -- Deletes a Digtron record. Note: just throws everything away, this is not digtron.deconstruct. local dispose_id = function(digtron_id) minetest.remove_detached_inventory(digtron_id) digtron.layout[digtron_id] = nil digtron.adjacent[digtron_id] = nil mod_meta:set_string("inv_"..digtron_id, "") mod_meta:set_string("layout_"..digtron_id, "") mod_meta:set_string("adjacent_"..digtron_id, "") mod_meta:set_string("name_"..digtron_id, "") end -------------------------------------------------------------------------------------------- -- Name digtron.get_name = function(digtron_id) return mod_meta:get_string("name_"..digtron_id) end digtron.set_name = function(digtron_id, digtron_name) mod_meta:set_string("name_"..digtron_id, digtron_name) end ------------------------------------------------------------------------------------------------------- -- Layout local get_persist_table_function = function(identifier) return function(digtron_id, tbl) mod_meta:set_string(identifier .."_"..digtron_id, minetest.serialize(tbl)) digtron[identifier][digtron_id] = tbl end end local get_retrieve_table_function = function(identifier) return function(digtron_id) local current = digtron[identifier][digtron_id] if current then return current end local tbl_string = mod_meta:get_string(identifier.."_"..digtron_id) if tbl_string ~= "" then current = minetest.deserialize(tbl_string) if current then digtron[identifier][digtron_id] = current end return current end end end local persist_layout = get_persist_table_function("layout") local retrieve_layout = get_retrieve_table_function("layout") local persist_adjacent = get_persist_table_function("adjacent") local retrieve_adjacent = get_retrieve_table_function("adjacent") local cardinal_directions = { {x=1,y=0,z=0}, {x=-1,y=0,z=0}, {x=0,y=1,z=0}, {x=0,y=-1,z=0}, {x=0,y=0,z=1}, {x=0,y=0,z=-1}, } -- recursive function searches out all connected unassigned digtron nodes local get_all_adjacent_digtron_nodes get_all_adjacent_digtron_nodes = function(pos, digtron_nodes, digtron_adjacent, player_name) for _, dir in ipairs(cardinal_directions) do local test_pos = vector.add(pos, dir) local test_hash = minetest.hash_node_position(test_pos) if not (digtron_nodes[test_hash] or digtron_adjacent[test_hash]) then -- don't test twice local test_node = minetest.get_node(test_pos) local group_value = minetest.get_item_group(test_node.name, "digtron") if group_value > 0 then local meta = minetest.get_meta(test_pos) if meta:contains("digtron_id") then -- Node is part of an existing digtron, don't incorporate it digtron_adjacent[test_hash] = true --elseif TODO test for protected node status using player_name else --test_node.group_value = group_value -- for later ease of reference digtron_nodes[test_hash] = test_node get_all_adjacent_digtron_nodes(test_pos, digtron_nodes, digtron_adjacent, player_name) -- recurse end else -- don't record details, the content of this node will change as the digtron moves digtron_adjacent[test_hash] = true end end end end -------------------------------------------------------------------------------------------------------- -- Construct and deconstruct local origin_hash = minetest.hash_node_position({x=0,y=0,z=0}) -- Returns the id of the new Digtron record, or nil on failure digtron.construct = function(root_pos, player_name) local node = minetest.get_node(root_pos) -- TODO: a more generic test? Not needed with the more generic controller design, as far as I can tell if node.name ~= "digtron:controller" then -- Called on an incorrect node minetest.log("error", "[Digtron] digtron.construct called with pos " .. minetest.pos_to_string(root_pos) .. " but the node at this location was " .. node.name) return nil end local root_meta = minetest.get_meta(root_pos) if root_meta:contains("digtron_id") then -- Already constructed. TODO: validate that the digtron_id actually exists as well minetest.log("error", "[Digtron] digtron.construct called with pos " .. minetest.pos_to_string(root_pos) .. " but the controller at this location was already part of a constructed Digtron.") return nil end local root_hash = minetest.hash_node_position(root_pos) local digtron_nodes = {[root_hash] = node} -- Nodes that are part of Digtron local digtron_adjacent = {} -- Nodes that are adjacent to Digtron but not a part of it get_all_adjacent_digtron_nodes(root_pos, digtron_nodes, digtron_adjacent, player_name) local digtron_id, digtron_inv = create_new_id(root_pos) local layout = {} for hash, node in pairs(digtron_nodes) do local relative_hash = hash - root_hash + origin_hash local current_meta if hash == root_hash then current_meta = root_meta -- we're processing the controller, we already have a reference to its meta else current_meta = minetest.get_meta(minetest.get_position_from_hash(hash)) end local current_meta_table = current_meta:to_table() if current_meta_table.fields.digtron_id then -- Trying to incorporate part of an existing digtron, should be impossible. minetest.log("error", "[Digtron] digtron.construct tried to incorporate a Digtron node of type " .. node.name .. " at " .. minetest.pos_to_string(minetest.get_position_from_hash(hash)) .. " that was already assigned to digtron id " .. current_meta_table.fields.digtron_id) dispose_id(digtron_id) return nil end -- Process inventories specially -- TODO Builder inventory gets turned into an itemname in a special key in the builder's meta -- fuel and main get added to corresponding detached inventory lists for listname, items in pairs(current_meta_table.inventory) do local count = #items -- increase the corresponding detached inventory size digtron_inv:set_size(listname, digtron_inv:get_size(listname) + count) for _, stack in ipairs(items) do digtron_inv:add_item(listname, stack) end -- erase actual items from stored layout metadata, the detached inventory is authoritative -- store the inventory size so the inventory can be easily recreated current_meta_table.inventory[listname] = #items end node.param1 = nil -- we don't care about param1, wipe it to save space layout[relative_hash] = {meta = current_meta_table, node = node} end digtron.set_name(digtron_id, root_meta:get_string("infotext")) persist_inventory(digtron_id) persist_layout(digtron_id, layout) persist_adjacent(digtron_id, digtron_adjacent) -- Wipe out the inventories of all in-world nodes, it's stored in the mod_meta now. -- Wait until now to do it in case the above loop fails partway through. for hash, node in pairs(digtron_nodes) do local digtron_meta if hash == root_hash then digtron_meta = root_meta -- we're processing the controller, we already have a reference to its meta else digtron_meta = minetest.get_meta(minetest.get_position_from_hash(hash)) end local inv = digtron_meta:get_inventory() for listname, items in pairs(inv:get_lists()) do for i = 1, #items do inv:set_stack(listname, i, ItemStack("")) end end digtron_meta:set_string("digtron_id", digtron_id) end minetest.debug("constructed id " .. digtron_id) return digtron_id end -- Returns pos, node, and meta for the digtron node provided the in-world node matches the layout -- returns nil otherwise local get_valid_data = function(digtron_id, root_hash, hash, data, function_name) local ipos = minetest.get_position_from_hash(hash + root_hash - origin_hash) local node = minetest.get_node(ipos) local imeta = minetest.get_meta(ipos) if data.node.name ~= node.name then minetest.log("error", "[Digtron] " .. function_name .. " tried interacting with one of ".. digtron_id .. "'s " .. data.node.name .. "s at " .. minetest.pos_to_string(ipos) .. " but the node at that location was of type " .. node.name) elseif imeta:get_string("digtron_id") ~= digtron_id then minetest.log("error", "[Digtron] " .. function_name .. " tried interacting with ".. digtron_id .. "'s " .. data.node.name .. " at " .. minetest.pos_to_string(ipos) .. " but the node at that location had a non-matching digtron_id value of \"" .. imeta:get_string("digtron_id") .. "\"") else return ipos, node, imeta end end -- Turns the Digtron back into pieces digtron.deconstruct = function(digtron_id, root_pos, player_name) local root_meta = minetest.get_meta(root_pos) root_meta:set_string("infotext", digtron.get_name(digtron_id)) local layout = retrieve_layout(digtron_id) local inv = digtron.retrieve_inventory(digtron_id) local root_hash = minetest.hash_node_position(root_pos) -- Write metadata and inventory to in-world node at this location for hash, data in pairs(layout) do local ipos, node, imeta = get_valid_data(digtron_id, root_hash, hash, data, "digtron.deconstruct") if ipos then local iinv = imeta:get_inventory() for listname, size in pairs(data.meta.inventory) do iinv:set_size(listname, size) for i, itemstack in ipairs(inv:get_list(listname)) do -- add everything, putting leftovers back in the main inventory inv:set_stack(listname, i, iinv:add_item(listname, itemstack)) end end -- TODO: special handling for builder node inventories -- Ensure node metadata fields are all set, too for field, value in pairs(data.meta.fields) do imeta:set_string(field, value) end -- Clear digtron_id, this node is no longer part of an active digtron imeta:set_string("digtron_id", "") end end dispose_id(digtron_id) end -- Removes the in-world nodes of a digtron -- Does not destroy its layout info digtron.remove_from_world = function(digtron_id, root_pos, player_name) local layout = retrieve_layout(digtron_id) local root_hash = minetest.hash_node_position(root_pos) local nodes_to_destroy = {} for hash, data in pairs(layout) do local ipos, node, imeta = get_valid_data(digtron_id, root_hash, hash, data, "digtron.destroy") if ipos then table.insert(nodes_to_destroy, ipos) end end -- TODO: voxelmanip might be better here? minetest.bulk_set_node(nodes_to_destroy, {name="air"}) end digtron.build_to_world = function(digtron_id, root_pos, player_name) local layout = retrieve_layout(digtron_id) local root_hash = minetest.hash_node_position(root_pos) local nodes_to_create = {} local permitted = true for hash, data in pairs(layout) do local ipos = minetest.get_position_from_hash(hash + root_hash - origin_hash) local node = minetest.get_node(ipos) local node_def = minetest.registered_nodes[node.name] -- TODO: lots of testing needed here if not (node_def and node_def.buildable_to) then minetest.chat_send_all("not permitted due to " .. node.name .. " at " .. minetest.pos_to_string(ipos)) permitted = false break end end if permitted then -- TODO: voxelmanip might be better here, less likely than with destroy though since metadata needs to be written for hash, data in pairs(layout) do local ipos = minetest.get_position_from_hash(hash + root_hash - origin_hash) minetest.set_node(ipos, data.node) local meta = minetest.get_meta(ipos) meta:set_string("digtron_id", digtron_id) for field, value in pairs(data.meta.fields) do meta:set_string(field, value) end -- Not needed - local inventories not used by active digtron, will be restored if deconstructed -- local inv = meta:get_inventory() -- for listname, size in pairs(data.meta.inventory) do -- inv:set_size(listname, size) -- end end end return permitted end --------------------------------------------------------------------------------- -- Misc digtron.can_dig = function(pos, digger) local meta = minetest.get_meta(pos) if meta:get_string("digtron_id") ~= "" then return false end return true end