local mod_meta = minetest.get_mod_storage() local cache = {} --minetest.debug(dump(mod_meta:to_table())) -- Wipes mod_meta --for field, value in pairs(mod_meta:to_table().fields) do -- mod_meta:set_string(field, "") --end ------------------------------------------------------------------------------------ -- 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(digtron_id..":inv") 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(digtron_id..":inv", 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 digtron_id = "digtron" .. tostring(math.random(1, 2^21)) -- TODO: use SecureRandom() -- It's super unlikely that we'll get a collision, but what the heck - maybe something will go -- wrong with the random number source while mod_meta:get_string(digtron_id..":layout") ~= "" do digtron_id = "digtron" .. tostring(math.random(1, 2^21)) end 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.disassemble. local dispose_callbacks = {} local dispose_id = function(digtron_id) -- name doesn't bother caching mod_meta:set_string(digtron_id..":name", "") -- inventory handles itself specially too mod_meta:set_string(digtron_id..":inv", "") minetest.remove_detached_inventory(digtron_id) -- clears the cache tables for i, func in ipairs(dispose_callbacks) do func(digtron_id) end end -------------------------------------------------------------------------------------------- -- Name -- Not bothering with a dynamic table store for names, they're just strings with no need for serialization or deserialization digtron.get_name = function(digtron_id) return mod_meta:get_string(digtron_id..":name") end digtron.set_name = function(digtron_id, digtron_name) -- Don't allow a name to be set for a non-existent Digtron if mod_meta:get(digtron_id..":layout") then mod_meta:set_string(digtron_id..":name", digtron_name) end end ------------------------------------------------------------------------------------------------------- -- Tables stored to metadata and cached locally local get_table_functions = function(identifier) cache[identifier] = {} -- add a callback for dispose_id table.insert(dispose_callbacks, function(digtron_id) mod_meta:set_string(digtron_id..":"..identifier, "") cache[identifier][digtron_id] = nil end) return function(digtron_id, tbl) minetest.chat_send_all("persisting " .. identifier .. " " .. digtron_id) mod_meta:set_string(digtron_id..":"..identifier, minetest.serialize(tbl)) cache[identifier][digtron_id] = tbl end, function(digtron_id) minetest.chat_send_all("retrieving " .. identifier .. " " .. digtron_id) local current = cache[identifier][digtron_id] if current then return current end local tbl_string = mod_meta:get_string(digtron_id..":"..identifier) if tbl_string ~= "" then current = minetest.deserialize(tbl_string) if current then cache[identifier][digtron_id] = current end return current end end end local persist_layout, retrieve_layout = get_table_functions("layout") local persist_adjacent, retrieve_adjacent = get_table_functions("adjacent") local persist_bounding_box, retrieve_bounding_box = get_table_functions("bounding_box") local persist_pos, retrieve_pos = get_table_functions("pos") digtron.get_pos = retrieve_pos ------------------------------------------------------------------------------------------------------- -- Layout creation helpers 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}, } local update_bounding_box = function(bounding_box, pos) bounding_box.minp.x = math.min(bounding_box.minp.x, pos.x) bounding_box.minp.y = math.min(bounding_box.minp.y, pos.y) bounding_box.minp.z = math.min(bounding_box.minp.z, pos.z) bounding_box.maxp.x = math.max(bounding_box.maxp.x, pos.x) bounding_box.maxp.y = math.max(bounding_box.maxp.y, pos.y) bounding_box.maxp.z = math.max(bounding_box.maxp.z, pos.z) end -- 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, bounding_box, 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 update_bounding_box(bounding_box, test_pos) get_all_adjacent_digtron_nodes(test_pos, digtron_nodes, digtron_adjacent, bounding_box, 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 -------------------------------------------------------------------------------------------------------- -- assemble and disassemble 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.assemble = 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. There's only going to be the one type of controller. if node.name ~= "digtron:controller" then -- Called on an incorrect node minetest.log("error", "[Digtron] digtron.assemble 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 assembled. TODO: validate that the digtron_id actually exists as well minetest.log("error", "[Digtron] digtron.assemble called with pos " .. minetest.pos_to_string(root_pos) .. " but the controller at this location was already part of a assembled 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. -- Initialize with the controller, it won't be added by get_all_adjacent_digtron_nodes local digtron_adjacent = {} -- Nodes that are adjacent to Digtron but not a part of it local bounding_box = {minp=vector.new(root_pos), maxp=vector.new(root_pos)} get_all_adjacent_digtron_nodes(root_pos, digtron_nodes, digtron_adjacent, bounding_box, 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.assemble 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 bounding_box.minp = vector.subtract(bounding_box.minp, root_pos) bounding_box.maxp = vector.subtract(bounding_box.maxp, root_pos) 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) persist_bounding_box(digtron_id, bounding_box) persist_pos(digtron_id, root_pos) -- 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 node_meta if hash == root_hash then node_meta = root_meta -- we're processing the controller, we already have a reference to its meta else node_meta = minetest.get_meta(minetest.get_position_from_hash(hash)) end local inv = node_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 node_meta:set_string("digtron_id", digtron_id) node_meta:mark_as_private("digtron_id") end minetest.log("action", "Digtron " .. digtron_id .. " assembled at " .. minetest.pos_to_string(root_pos) .. " by " .. player_name) 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 node_pos = minetest.get_position_from_hash(hash + root_hash - origin_hash) local node = minetest.get_node(node_pos) local node_meta = minetest.get_meta(node_pos) local target_digtron_id = node_meta:get_string("digtron_id") 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(node_pos) .. " but the node at that location was of type " .. node.name) return elseif target_digtron_id ~= digtron_id then if target_digtron_id ~= "" then minetest.log("error", "[Digtron] " .. function_name .. " tried interacting with ".. digtron_id .. "'s " .. data.node.name .. " at " .. minetest.pos_to_string(node_pos) .. " but the node at that location had a non-matching digtron_id value of \"" .. target_digtron_id .. "\"") return else -- Allow digtron to recover from bad map metadata writes, the bane of Digtron 1.0's existence minetest.log("warning", "[Digtron] " .. function_name .. " tried interacting with ".. digtron_id .. "'s " .. data.node.name .. " at " .. minetest.pos_to_string(node_pos) .. " but the node at that location had no digtron_id in its metadata. " .. "Since the node type matched the layout, however, it was included anyway. It's possible " .. "its metadata was not written correctly by a previous Digtron activity.") return node_pos, node, node_meta end end return node_pos, node, node_meta end -- Turns the Digtron back into pieces digtron.disassemble = function(digtron_id, player_name) local bbox = retrieve_bounding_box(digtron_id) local root_pos = retrieve_pos(digtron_id) 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) if not (layout and inv) then minetest.log("error", "digtron.disassemble was unable to find either layout or inventory record for " .. digtron_id .. ", disassembly was impossible. Clearing any other remaining data for this id.") dispose_id(digtron_id) return end 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 node_pos, node, node_meta = get_valid_data(digtron_id, root_hash, hash, data, "digtron.disassemble") if node_pos then local node_inv = node_meta:get_inventory() for listname, size in pairs(data.meta.inventory) do node_inv: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, node_inv: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 node_meta:set_string(field, value) end -- Clear digtron_id, this node is no longer part of an active digtron node_meta:set_string("digtron_id", "") end end dispose_id(digtron_id) return root_pos 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) if not layout then minetest.log("error", "Unable to find layout record for " .. digtron_id .. ", wiping any remaining metadata for this id to prevent corruption. Sorry!") local meta = minetest.get_meta(root_pos) meta:set_string("digtron_id", "") dispose_id(digtron_id) return end local root_hash = minetest.hash_node_position(root_pos) local nodes_to_destroy = {} for hash, data in pairs(layout) do local node_pos, node, node_meta = get_valid_data(digtron_id, root_hash, hash, data, "digtron.destroy") if node_pos then table.insert(nodes_to_destroy, node_pos) 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 node_pos = minetest.get_position_from_hash(hash + root_hash - origin_hash) local node = minetest.get_node(node_pos) 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(node_pos)) 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 node_pos = minetest.get_position_from_hash(hash + root_hash - origin_hash) minetest.set_node(node_pos, data.node) local meta = minetest.get_meta(node_pos) for field, value in pairs(data.meta.fields) do meta:set_string(field, value) end meta:set_string("digtron_id", digtron_id) meta:mark_as_private("digtron_id") -- Not needed - local inventories not used by active digtron, will be restored if disassembled -- local inv = meta:get_inventory() -- for listname, size in pairs(data.meta.inventory) do -- inv:set_size(listname, size) -- end end local bbox = retrieve_bounding_box(digtron_id) persist_bounding_box(digtron_id, bbox) persist_pos(digtron_id, root_pos) end return permitted end --------------------------------------------------------------------------------- -- Misc -- If the digtron node has an assigned ID and a layout for that ID exists and -- a matching node exists in the layout then don't let it be dug. -- TODO: add protection check? digtron.can_dig = function(pos, digger) local meta = minetest.get_meta(pos) local digtron_id = meta:get_string("digtron_id") if digtron_id == "" then return true end local node = minetest.get_node(pos) local bbox = retrieve_bounding_box(digtron_id) local root_pos = retrieve_pos(digtron_id) local layout = retrieve_layout(digtron_id) if bbox == nil or root_pos == nil or layout == nil then -- Somehow, this belongs to a digtron id that's missing information that should exist in persistence. local missing = "" if bbox == nil then missing = missing .. "bounding_box " end if root_pos == nil then missing = missing .. "root_pos " end if layout == nil then missing = missing .. "layout " end minetest.log("error", "[Digtron] can_dig was called on a " .. node.name .. " at location " .. minetest.pos_to_string(pos) .. " that claimed to belong to " .. digtron_id .. ". However, layout and/or location data are missing: " .. missing) -- May be better to do this to prevent node duplication. But we're already in bug land here so tread gently. --minetest.remove_node(pos) --return false return true end local root_hash = minetest.hash_node_position(root_pos) local here_hash = minetest.hash_node_position(pos) local layout_hash = here_hash - root_hash + origin_hash local layout_data = layout[layout_hash] if layout_data == nil or layout_data.node == nil then minetest.log("error", "[Digtron] can_dig was called on a " .. node.name .. " at location " .. minetest.pos_to_string(pos) .. " that claimed to belong to " .. digtron_id .. ". However, the layout for that digtron_id didn't contain any corresponding node at its location.") return true end if layout_data.node.name ~= node.name or layout_data.node.param2 ~= node.param2 then minetest.log("error", "[Digtron] can_dig was called on a " .. node.name .. " with param2 " .. node.param2 .." at location " .. minetest.pos_to_string(pos) .. " that belonged to " .. digtron_id .. ". However, the layout for that digtron_id contained a " .. layout_data.node.name .. "with param2 ".. layout_data.node.param2 .. " at its location.") return true end -- We're part of a valid Digtron. No touchy. return false end -- put this on all Digtron nodes. If other inventory types are added (eg, batteries) -- update this. digtron.on_blast = function(pos, intensity) if intensity < 1.0 then return end -- The Almighty Digtron ignores weak-ass explosions local meta = minetest.get_meta(pos) local digtron_id = meta:get_string("digtron_id") if digtron_id ~= "" then if not digtron.disassemble(digtron_id, "an explosion") then minetest.log("error", "[Digtron] a digtron node at " .. minetest.pos_to_string(pos) .. " was hit by an explosion and had digtron_id " .. digtron_id .. " but didn't have a root position recorded, so it could not be disassembled.") return end end local drops = {} default.get_inventory_drops(pos, "main", drops) default.get_inventory_drops(pos, "fuel", drops) local node = minetest.get_node(pos) table.insert(drops, ItemStack(node.name)) minetest.remove_node(pos) return drops end ------------------------------------------------------------------------------------ -- Creative trash -- This is wrapped in an after() call as a workaround for to https://github.com/minetest/minetest/issues/8827 if minetest.get_modpath("creative") then minetest.after(1, function() if minetest.get_inventory({type="detached", name="creative_trash"}) then if minetest.remove_detached_inventory("creative_trash") then -- Create the trash field local trash = minetest.create_detached_inventory("creative_trash", { -- Allow the stack to be placed and remove it in on_put() -- This allows the creative inventory to restore the stack allow_put = function(inv, listname, index, stack, player) return stack:get_count() end, on_put = function(inv, listname, index, stack, player) local stack = inv:get_stack(listname, index) local stack_meta = stack:get_meta() local digtron_id = stack_meta:get_string("digtron_id") if digtron_id ~= "" then minetest.log("action", player:get_player_name() .. " disposed of " .. digtron_id .. " in the creative inventory's trash receptacle.") dispose_id(digtron_id) end inv:set_list(listname, {}) end, }) trash:set_size("main", 1) end end end) end