mcl_util = {} -- Based on minetest.rotate_and_place --[[ Attempt to predict the desired orientation of the pillar-like node defined by `itemstack`, and place it accordingly in one of 3 possible orientations (X, Y or Z). Stacks are handled normally if the `infinitestacks` field is false or omitted (else, the itemstack is not changed). * `invert_wall`: if `true`, place wall-orientation on the ground and ground- orientation on wall This function is a simplified version of minetest.rotate_and_place. The Minetest function is seen as inappropriate because this includes mirror images of possible orientations, causing problems with pillar shadings. ]] function mcl_util.rotate_axis_and_place(itemstack, placer, pointed_thing, infinitestacks, invert_wall) local unode = minetest.get_node_or_nil(pointed_thing.under) if not unode then return end local undef = minetest.registered_nodes[unode.name] if undef and undef.on_rightclick then undef.on_rightclick(pointed_thing.under, unode, placer, itemstack, pointed_thing) return end local fdir = minetest.dir_to_facedir(placer:get_look_dir()) local wield_name = itemstack:get_name() local above = pointed_thing.above local under = pointed_thing.under local is_x = (above.x ~= under.x) local is_y = (above.y ~= under.y) local is_z = (above.z ~= under.z) local anode = minetest.get_node_or_nil(above) if not anode then return end local pos = pointed_thing.above local node = anode if undef and undef.buildable_to then pos = pointed_thing.under node = unode end if minetest.is_protected(pos, placer:get_player_name()) then minetest.record_protection_violation(pos, placer:get_player_name()) return end local ndef = minetest.registered_nodes[node.name] if not ndef or not ndef.buildable_to then return end local p2 if is_y then if invert_wall then if fdir == 3 or fdir == 1 then p2 = 12 else p2 = 6 end end elseif is_x then if invert_wall then p2 = 0 else p2 = 12 end elseif is_z then if invert_wall then p2 = 0 else p2 = 6 end end minetest.set_node(pos, {name = wield_name, param2 = p2}) if not infinitestacks then itemstack:take_item() return itemstack end end -- Wrapper of above function for use as `on_place` callback (Recommended). -- Similar to minetest.rotate_node. function mcl_util.rotate_axis(itemstack, placer, pointed_thing) mcl_util.rotate_axis_and_place(itemstack, placer, pointed_thing, minetest.settings:get_bool("creative_mode"), placer:get_player_control().sneak) return itemstack end -- Returns position of the neighbor of a double chest node -- or nil if node is invalid. -- This function assumes that the large chest is actually intact -- * pos: Position of the node to investigate -- * param2: param2 of that node -- * side: Which "half" the investigated node is. "left" or "right" function mcl_util.get_double_container_neighbor_pos(pos, param2, side) if side == "right" then if param2 == 0 then return {x=pos.x-1, y=pos.y, z=pos.z} elseif param2 == 1 then return {x=pos.x, y=pos.y, z=pos.z+1} elseif param2 == 2 then return {x=pos.x+1, y=pos.y, z=pos.z} elseif param2 == 3 then return {x=pos.x, y=pos.y, z=pos.z-1} end else if param2 == 0 then return {x=pos.x+1, y=pos.y, z=pos.z} elseif param2 == 1 then return {x=pos.x, y=pos.y, z=pos.z-1} elseif param2 == 2 then return {x=pos.x-1, y=pos.y, z=pos.z} elseif param2 == 3 then return {x=pos.x, y=pos.y, z=pos.z+1} end end end -- Iterates through all items in the given inventory and -- returns the slot of the first item which matches a condition. -- Returns nil if no item was found. --- source_inventory: Inventory to take the item from --- source_list: List name of the source inventory from which to take the item --- destination_inventory: Put item into this inventory --- destination_list: List name of the destination inventory to which to put the item into --- condition: Function which takes an itemstack and returns true if it matches the desired item condition. --- If set to nil, the slot of the first item stack will be taken unconditionally. -- dst_inventory and dst_list can also be nil if condition is nil. function mcl_util.get_eligible_transfer_item_slot(src_inventory, src_list, dst_inventory, dst_list, condition) local size = src_inventory:get_size(src_list) local stack for i=1, size do stack = src_inventory:get_stack(src_list, i) if not stack:is_empty() and (condition == nil or condition(stack, src_inventory, src_list, dst_inventory, dst_list)) then return i end end return nil end -- Returns true if given itemstack is a shulker box local is_not_shulker_box = function(itemstack) local g = minetest.get_item_group(itemstack:get_name(), "shulker_box") return g == 0 or g == nil end -- Moves a single item from one inventory to another. --- source_inventory: Inventory to take the item from --- source_list: List name of the source inventory from which to take the item --- source_stack_id: The inventory position ID of the source inventory to take the item from (-1 for first occupied slot) --- destination_inventory: Put item into this inventory --- destination_list: List name of the destination inventory to which to put the item into -- Returns true on success and false on failure -- Possible failures: No item in source slot, destination inventory full function mcl_util.move_item(source_inventory, source_list, source_stack_id, destination_inventory, destination_list) if source_stack_id == -1 then source_stack_id = mcl_util.get_first_occupied_inventory_slot(source_inventory, source_list) if source_stack_id == nil then return false end end if not source_inventory:is_empty(source_list) then local stack = source_inventory:get_stack(source_list, source_stack_id) local item = stack:get_name() if not stack:is_empty() then if not destination_inventory:room_for_item(destination_list, item) then return false end stack:take_item() source_inventory:set_stack(source_list, source_stack_id, stack) destination_inventory:add_item(destination_list, item) return true end end return false end -- Moves a single item from one container node into another. Performs a variety of high-level -- checks to prevent invalid transfers such as shulker boxes into shulker boxes --- source_pos: Position ({x,y,z}) of the node to take the item from --- destination_pos: Position ({x,y,z}) of the node to put the item into --- source_list (optional): List name of the source inventory from which to take the item. Default is normally "main"; "dst" for furnace --- source_stack_id (optional): The inventory position ID of the source inventory to take the item from (-1 for slot of the first valid item; -1 is default) --- destination_list (optional): List name of the destination inventory. Default is normally "main"; "src" for furnace -- Returns true on success and false on failure. function mcl_util.move_item_container(source_pos, destination_pos, source_list, source_stack_id, destination_list) local dpos = table.copy(destination_pos) local spos = table.copy(source_pos) local snode = minetest.get_node(spos) local dnode = minetest.get_node(dpos) local dctype = minetest.get_item_group(dnode.name, "container") local sctype = minetest.get_item_group(snode.name, "container") -- Normalize double container by forcing to always use the left segment first local normalize_double_container = function(pos, node, ctype) if ctype == 6 then pos = mcl_util.get_double_container_neighbor_pos(pos, node.param2, "right") if not pos then return false end node = minetest.get_node(pos) ctype = minetest.get_item_group(node.name, "container") -- The left segment seems incorrect? We better bail out! if ctype ~= 5 then return false end end return pos, node, ctype end spos, snode, sctype = normalize_double_container(spos, snode, sctype) dpos, dnode, dctype = normalize_double_container(dpos, dnode, dctype) if not spos or not dpos then return false end local smeta = minetest.get_meta(spos) local dmeta = minetest.get_meta(dpos) local sinv = smeta:get_inventory() local dinv = dmeta:get_inventory() -- Default source lists if not source_list then -- Main inventory for most container types if sctype == 2 or sctype == 3 or sctype == 5 or sctype == 6 then source_list = "main" -- Furnace: output elseif sctype == 4 then source_list = "dst" -- Unknown source container type. Bail out else return false end end -- Automatically select stack slot ID if set to automatic if not source_stack_id then source_stack_id = -1 end if source_stack_id == -1 then local cond = nil -- Prevent shulker box inception if dctype == 3 then cond = is_not_shulker_box end source_stack_id = mcl_util.get_eligible_transfer_item_slot(sinv, source_list, dinv, dpos, cond) if not source_stack_id then -- Try again if source is a double container if sctype == 5 then spos = mcl_util.get_double_container_neighbor_pos(spos, snode.param2, "left") smeta = minetest.get_meta(spos) sinv = smeta:get_inventory() source_stack_id = mcl_util.get_eligible_transfer_item_slot(sinv, source_list, dinv, dpos, cond) if not source_stack_id then return false end else return false end end end -- Abort transfer if shulker box wants to go into shulker box if dctype == 3 then local stack = sinv:get_stack(source_list, source_stack_id) if stack and minetest.get_item_group(stack:get_name(), "shulker_box") == 1 then return false end end -- If it's a container, put it into the container if dctype ~= 0 then -- Automatically select a destination list if omitted if not destination_list then -- Main inventory for most container types if dctype == 2 or dctype == 3 or dctype == 5 or dctype == 6 then destination_list = "main" -- Furnace source slot elseif dctype == 4 then destination_list = "src" end end if destination_list then -- Move item local ok = mcl_util.move_item(sinv, source_list, source_stack_id, dinv, destination_list) -- Try transfer to neighbor node if transfer failed and double container if not ok and dctype == 5 then dpos = mcl_util.get_double_container_neighbor_pos(dpos, dnode.param2, "left") dmeta = minetest.get_meta(dpos) dinv = dmeta:get_inventory() ok = mcl_util.move_item(sinv, source_list, source_stack_id, dinv, destination_list) end -- Update furnace if ok and dctype == 4 then -- Start furnace's timer function, it will sort out whether furnace can burn or not. minetest.get_node_timer(dpos):start(1.0) end return ok end end return false end -- Returns the ID of the first non-empty slot in the given inventory list -- or nil, if inventory is empty. function mcl_util.get_first_occupied_inventory_slot(inventory, listname) return mcl_util.get_eligible_transfer_item_slot(inventory, listname) end -- Returns true if item (itemstring or ItemStack) can be used as a furnace fuel. -- Returns false otherwise function mcl_util.is_fuel(item) return minetest.get_craft_result({method="fuel", width=1, items={item}}).time ~= 0 end -- For a given position, returns a 2-tuple: -- 1st return value: true if pos is in void -- 2nd return value: true if it is in the deadly part of the void function mcl_util.is_in_void(pos) local void = not ((pos.y < mcl_vars.mg_overworld_max and pos.y > mcl_vars.mg_overworld_min) or (pos.y < mcl_vars.mg_nether_max and pos.y > mcl_vars.mg_nether_min) or (pos.y < mcl_vars.mg_end_max and pos.y > mcl_vars.mg_end_min)) local void_deadly = false local deadly_tolerance = 64 -- the player must be this many nodes “deep” into the void to be damaged if void then -- Overworld → Void → End → Void → Nether → Void if pos.y < mcl_vars.mg_overworld_min and pos.y > mcl_vars.mg_end_max then void_deadly = pos.y < mcl_vars.mg_overworld_min - deadly_tolerance elseif pos.y < mcl_vars.mg_end_min and pos.y > mcl_vars.mg_nether_max then void_deadly = pos.y < mcl_vars.mg_end_min - deadly_tolerance elseif pos.y < mcl_vars.mg_nether_min then void_deadly = pos.y < mcl_vars.mg_nether_min - deadly_tolerance end end return void, void_deadly end -- Here come 2 simple converter functions which are important for map generators and mob spawning -- Takes an Y coordinate as input and returns: -- 1) The corresponding Minecraft layer (can be nil if void) -- 2) The corresponding Minecraft dimension ("overworld", "nether" or "end") or "void" if it is in the void -- If the Y coordinate is not located in any dimension, it will return: -- nil, "void" function mcl_util.y_to_layer(y) if y >= mcl_vars.mg_overworld_min then return y - mcl_vars.mg_overworld_min, "overworld" elseif y >= mcl_vars.mg_nether_min and y <= mcl_vars.mg_nether_max then return y - mcl_vars.mg_nether_min, "nether" elseif y >= mcl_vars.mg_end_min and y <= mcl_vars.mg_end_max then return y - mcl_vars.mg_end_min, "end" else return nil, "void" end end -- Takes a Minecraft layer and a “dimension” name -- and returns the corresponding Y coordinate for -- MineClone 2. -- mc_dimension is one of "overworld", "nether", "end" (default: "overworld"). function mcl_util.layer_to_y(layer, mc_dimension) if mc_dimension == "overworld" or mc_dimension == nil then return layer + mcl_vars.mg_overworld_min elseif mc_dimension == "nether" then return layer + mcl_vars.mg_nether_min elseif mc_dimension == "end" then return layer + mcl_vars.mg_end_min end end -- Takes a position and returns true if this position can have weather function mcl_util.has_weather(pos) -- Weather in the Overworld and the high part of the void below return pos.y <= mcl_vars.mg_overworld_max and pos.y >= mcl_vars.mg_overworld_min - 64 end -- Takes a position (pos) and returns true if compasses are working here function mcl_util.compass_works(pos) -- It doesn't work in Nether and the End, but it works in the Overworld and in the high part of the void below local _, dim = mcl_util.y_to_layer(pos.y) if dim == "nether" or dim == "end" then return false elseif dim == "void" then return pos.y <= mcl_vars.mg_overworld_max and pos.y >= mcl_vars.mg_overworld_min - 64 else return true end end -- Takes a position (pos) and returns true if clocks are working here mcl_util.clock_works = mcl_util.compass_works -- Returns a on_place function for plants -- * condition: function(pos, node) -- * A function which is called by the on_place function to check if the node can be placed -- * Must return true, if placement is allowed, false otherwise -- * pos, node: Position and node table of plant node function mcl_util.generate_on_place_plant_function(condition) return function(itemstack, placer, pointed_thing) if pointed_thing.type ~= "node" then -- no interaction possible with entities return itemstack end -- Call on_rightclick if the pointed node defines it local node = minetest.get_node(pointed_thing.under) if placer and not placer:get_player_control().sneak then if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack end end local place_pos local def_under = minetest.registered_nodes[minetest.get_node(pointed_thing.under).name] local def_above = minetest.registered_nodes[minetest.get_node(pointed_thing.above).name] if not def_under or not def_above then return itemstack end if def_under.buildable_to then place_pos = pointed_thing.under elseif def_above.buildable_to then place_pos = pointed_thing.above else return itemstack end -- Check placement rules if (condition(place_pos, node) == true) then local idef = itemstack:get_definition() local new_itemstack, success = minetest.item_place_node(itemstack, placer, pointed_thing) if success then if idef.sounds and idef.sounds.place then minetest.sound_play(idef.sounds.place, {pos=pointed_thing.above, gain=1}) end end itemstack = new_itemstack end return itemstack end end