diff --git a/mods/CORE/mcl_util/init.lua b/mods/CORE/mcl_util/init.lua index 4684234e5..094ecec63 100644 --- a/mods/CORE/mcl_util/init.lua +++ b/mods/CORE/mcl_util/init.lua @@ -728,3 +728,240 @@ function mcl_util.set_bone_position(obj, bone, pos, rot) obj:set_bone_position(bone, pos or current_pos, rot or current_rot) end end + +---Return a function to use in `on_place`. +--- +---Allow to bypass the `buildable_to` node field in a `on_place` callback. +--- +---You have to make sure that the nodes you return true for have `buildable_to = true`. +---@param func fun(node_name: string): boolean Return `true` if node must not replace the buildable_to node which have `node_name` +---@return fun(itemstack: ItemStack, placer: ObjectRef, pointed_thing: pointed_thing, param2: integer): ItemStack? +function mcl_util.bypass_buildable_to(func) + -------------------------- + -- MINETEST CODE: UTILS -- + -------------------------- + + local function copy_pointed_thing(pointed_thing) + return { + type = pointed_thing.type, + above = pointed_thing.above and vector.copy(pointed_thing.above), + under = pointed_thing.under and vector.copy(pointed_thing.under), + ref = pointed_thing.ref, + } + end + + local function user_name(user) + return user and user:get_player_name() or "" + end + + -- Returns a logging function. For empty names, does not log. + local function make_log(name) + return name ~= "" and minetest.log or function() end + end + + local function check_attached_node(p, n, group_rating) + local def = core.registered_nodes[n.name] + local d = vector.zero() + if group_rating == 3 then + -- always attach to floor + d.y = -1 + elseif group_rating == 4 then + -- always attach to ceiling + d.y = 1 + elseif group_rating == 2 then + -- attach to facedir or 4dir direction + if (def.paramtype2 == "facedir" or + def.paramtype2 == "colorfacedir") then + -- Attach to whatever facedir is "mounted to". + -- For facedir, this is where tile no. 5 point at. + + -- The fallback vector here is in case 'facedir to dir' is nil due + -- to voxelmanip placing a wallmounted node without resetting a + -- pre-existing param2 value that is out-of-range for facedir. + -- The fallback vector corresponds to param2 = 0. + d = core.facedir_to_dir(n.param2) or vector.new(0, 0, 1) + elseif (def.paramtype2 == "4dir" or + def.paramtype2 == "color4dir") then + -- Similar to facedir handling + d = core.fourdir_to_dir(n.param2) or vector.new(0, 0, 1) + end + elseif def.paramtype2 == "wallmounted" or + def.paramtype2 == "colorwallmounted" then + -- Attach to whatever this node is "mounted to". + -- This where tile no. 2 points at. + + -- The fallback vector here is used for the same reason as + -- for facedir nodes. + d = core.wallmounted_to_dir(n.param2) or vector.new(0, 1, 0) + else + d.y = -1 + end + local p2 = vector.add(p, d) + local nn = core.get_node(p2).name + local def2 = core.registered_nodes[nn] + if def2 and not def2.walkable then + return false + end + return true + end + + return function(itemstack, placer, pointed_thing, param2) + ------------------- + -- MINETEST CODE -- + ------------------- + local def = itemstack:get_definition() + if def.type ~= "node" or pointed_thing.type ~= "node" then + return itemstack + end + + local under = pointed_thing.under + local oldnode_under = minetest.get_node_or_nil(under) + local above = pointed_thing.above + local oldnode_above = minetest.get_node_or_nil(above) + local playername = user_name(placer) + local log = make_log(playername) + + if not oldnode_under or not oldnode_above then + log("info", playername .. " tried to place" + .. " node in unloaded position " .. minetest.pos_to_string(above)) + return itemstack + end + + local olddef_under = minetest.registered_nodes[oldnode_under.name] + olddef_under = olddef_under or minetest.nodedef_default + local olddef_above = minetest.registered_nodes[oldnode_above.name] + olddef_above = olddef_above or minetest.nodedef_default + + if not olddef_above.buildable_to and not olddef_under.buildable_to then + log("info", playername .. " tried to place" + .. " node in invalid position " .. minetest.pos_to_string(above) + .. ", replacing " .. oldnode_above.name) + return itemstack + end + + --------------------- + -- CUSTOMIZED CODE -- + --------------------- + + -- Place above pointed node + local place_to = vector.copy(above) + + -- If node under is buildable_to, check for callback result and place into it instead + if olddef_under.buildable_to and not func(oldnode_under.name) then + log("info", "node under is buildable to") + place_to = vector.copy(under) + end + + ------------------- + -- MINETEST CODE -- + ------------------- + + if minetest.is_protected(place_to, playername) then + log("action", playername + .. " tried to place " .. def.name + .. " at protected position " + .. minetest.pos_to_string(place_to)) + minetest.record_protection_violation(place_to, playername) + return itemstack + end + + local oldnode = minetest.get_node(place_to) + local newnode = {name = def.name, param1 = 0, param2 = param2 or 0} + + -- Calculate direction for wall mounted stuff like torches and signs + if def.place_param2 ~= nil then + newnode.param2 = def.place_param2 + elseif (def.paramtype2 == "wallmounted" or + def.paramtype2 == "colorwallmounted") and not param2 then + local dir = vector.subtract(under, above) + newnode.param2 = minetest.dir_to_wallmounted(dir) + -- Calculate the direction for furnaces and chests and stuff + elseif (def.paramtype2 == "facedir" or + def.paramtype2 == "colorfacedir" or + def.paramtype2 == "4dir" or + def.paramtype2 == "color4dir") and not param2 then + local placer_pos = placer and placer:get_pos() + if placer_pos then + local dir = vector.subtract(above, placer_pos) + newnode.param2 = minetest.dir_to_facedir(dir) + log("info", "facedir: " .. newnode.param2) + end + end + + local metatable = itemstack:get_meta():to_table().fields + + -- Transfer color information + if metatable.palette_index and not def.place_param2 then + local color_divisor = nil + if def.paramtype2 == "color" then + color_divisor = 1 + elseif def.paramtype2 == "colorwallmounted" then + color_divisor = 8 + elseif def.paramtype2 == "colorfacedir" then + color_divisor = 32 + elseif def.paramtype2 == "color4dir" then + color_divisor = 4 + elseif def.paramtype2 == "colordegrotate" then + color_divisor = 32 + end + if color_divisor then + local color = math.floor(metatable.palette_index / color_divisor) + local other = newnode.param2 % color_divisor + newnode.param2 = color * color_divisor + other + end + end + + -- Check if the node is attached and if it can be placed there + local an = minetest.get_item_group(def.name, "attached_node") + if an ~= 0 and + not check_attached_node(place_to, newnode, an) then + log("action", "attached node " .. def.name .. + " cannot be placed at " .. minetest.pos_to_string(place_to)) + return itemstack + end + + log("action", playername .. " places node " + .. def.name .. " at " .. minetest.pos_to_string(place_to)) + + -- Add node and update + minetest.add_node(place_to, newnode) + + -- Play sound if it was done by a player + if playername ~= "" and def.sounds and def.sounds.place then + minetest.sound_play(def.sounds.place, { + pos = place_to, + exclude_player = playername, + }, true) + end + + local take_item = true + + -- Run callback + if def.after_place_node then + -- Deepcopy place_to and pointed_thing because callback can modify it + local place_to_copy = vector.copy(place_to) + local pointed_thing_copy = copy_pointed_thing(pointed_thing) + if def.after_place_node(place_to_copy, placer, itemstack, + pointed_thing_copy) then + take_item = false + end + end + + -- Run script hook + for _, callback in ipairs(minetest.registered_on_placenodes) do + -- Deepcopy pos, node and pointed_thing because callback can modify them + local place_to_copy = vector.copy(place_to) + local newnode_copy = {name = newnode.name, param1 = newnode.param1, param2 = newnode.param2} + local oldnode_copy = {name = oldnode.name, param1 = oldnode.param1, param2 = oldnode.param2} + local pointed_thing_copy = copy_pointed_thing(pointed_thing) + if callback(place_to_copy, newnode_copy, placer, oldnode_copy, itemstack, pointed_thing_copy) then + take_item = false + end + end + + if take_item then + itemstack:take_item() + end + return itemstack + end +end diff --git a/mods/ITEMS/mcl_core/nodes_misc.lua b/mods/ITEMS/mcl_core/nodes_misc.lua index 5b589332b..bf5300510 100644 --- a/mods/ITEMS/mcl_core/nodes_misc.lua +++ b/mods/ITEMS/mcl_core/nodes_misc.lua @@ -262,6 +262,8 @@ for i = 0, 14 do --minetest.LIGHT_MAX walkable = false, light_source = i, drop = "", + buildable_to = true, + node_placement_prediction = "", inventory_image = "mcl_core_light_" .. i .. ".png", wield_image = "mcl_core_light_" .. i .. ".png", sunlight_propagates = true, @@ -272,6 +274,9 @@ for i = 0, 14 do --minetest.LIGHT_MAX itemstack:set_name("mcl_core:light_" .. ((i == 14) and 0 or i + 1)) return itemstack end, + on_place = mcl_util.bypass_buildable_to(function(node_name) + return string.match(node_name, "^mcl_core:light_(%d+)$") + end), after_place_node = function(pos, placer, itemstack, pointed_thing) if placer == nil then return