From dcf12fcdf4b99d5f38b56f7a6b1042dd28591775 Mon Sep 17 00:00:00 2001 From: copygirl Date: Wed, 18 Oct 2023 20:27:04 +0200 Subject: [PATCH] Replace rightclick interaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace on_rightclick of registered node - Replace on_place of empty hand item to handle sneak-rightclicking to invert rotation - Move registry related things to registry.lua - Fix off-by-one error in nodecore_filtered_lookup - Detect corner interactions, not just edges - Corner interactions rotate by 120° - Add rotate_node function to mimic NodeCore spin --- README.md | 9 +++-- init.lua | 111 ++++++++++++++++++++++----------------------------- mod.conf | 2 +- registry.lua | 73 +++++++++++++++++++++++++++++++++ rotate.lua | 66 ++++++++++++++---------------- utility.lua | 49 ++++++++--------------- 6 files changed, 172 insertions(+), 138 deletions(-) create mode 100644 registry.lua diff --git a/README.md b/README.md index 2f5f9cb..79b0a24 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ This mod follows in the footsteps of the useful [Extended Placement] mod for as optics and doors easily, without having to cycle through all possible rotations every time. -Rotating a node can be done in two ways, either by clicking the center or the -edge of a face. By interacting with the center, the node rotates around that -face. When interacting with the edge, the node is "pushed" that way, rotating -away from you. Sneaking inverts the direction of the rotation. +Rotating a node can be done in three ways: +- Click near the center of a face, and the node rotates 90° around it. +- Click near an edge, and the node will be rotated 90° away from you. +- Clikc near a corner, and the node will rotate 120° around it. +Sneaking inverts the direction of the rotation. At this time, nodes that can be rotated are hardcoded into the mod, since their valid orientations can't be easily extracted. A function mimicking diff --git a/init.lua b/init.lua index 0491f73..b7f35c7 100755 --- a/init.lua +++ b/init.lua @@ -1,53 +1,53 @@ -local ipairs, minetest, vector, nodecore, include - = ipairs, minetest, vector, nodecore, include +local minetest, vector, nodecore, include + = minetest, vector, nodecore, include +local math_round = math.round -local hud = include("hud") -local rotate = include("rotate") -local utility = include("utility") +local hud = include("hud") +local rotate = include("rotate") +local utility = include("utility") +local registry = include("registry") -- TODO: Add crosshair indicators for rotating around edges. --- TODO: Replace `on_rightclick` of intended nodes? --- TODO: Allow right-clicking while holding sneak with an empty hand to invert rotation. -- TODO: Add some more comments. +-- TODO: Add particles to preview rotation? -- Distance at which we want to rotate by "pushing" on an edge. -local EDGE_DISTANCE = 1.5 / 16 -- 1.5 texels (at 16² texture resolution) +local EDGE_DISTANCE = 1.5 / 16 -- texels (at 16² texture resolution) -- Contains a per-player state that holds information, created by a raycast -- done in `playerstep`, about the node this player might rotate. This is used --- to decide what to do when a rightclick occurs and to update the their HUD. +-- to update the their HUD and to decide what to do when a rightclick occurs. local rotating_state = {} --- Register rotatable nodes. -do - local function nodecore_filtered_lookup(eq_func) - local lookup = {} - for i = 0, 23 do - local facedir = nodecore.facedirs[i] - for j = 1, #lookup do - local other = nodecore.facedirs[lookup[j]] - if eq_func(facedir, other) then lookup[i] = j; break end - end - lookup[i] = lookup[i] or i - end - return lookup - end - local LENS_FILTERED_LOOKUP = nodecore_filtered_lookup( - function(a, b) return vector.equals(a.f, b.f) end) - local PRISM_FILTERED_LOOKUP = nodecore_filtered_lookup( - function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end) - local PANEL_FILTERED_LOOKUP = nodecore_filtered_lookup( - function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end) +local function handle_rotatable_rightclick(pos, node, clicker) + local state = rotating_state[clicker:get_player_name()] + if not state then return false end -- Not looking at anything rotatable. - for _, lens_state in ipairs({ "", "_on", "_glow", "_glow_start" }) do - rotate.register_rotatable("nc_optics:lens" .. lens_state, LENS_FILTERED_LOOKUP) end - for _, prism_state in ipairs({ "", "_on", "_gated" }) do - rotate.register_rotatable("nc_optics:prism" .. prism_state, PRISM_FILTERED_LOOKUP) end + -- Make sure we're still the same node that we raycasted in `playerstep`. + if not vector.equals(pos, state.pos) then return true end + if node.name ~= state.node.name or node.param2 ~= state.node.param2 then return true end + -- Only continue if the node can actually be sucessfully rotated. + if not state.success then return true end - rotate.register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP) - rotate.register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP) + rotate.rotate_node(pos, node, state.facedir) + return true end +registry.custom_on_rightclick = handle_rotatable_rightclick + +-- Override `nc_scaling`'s default empty hand right-click behavior so we can use it +-- to rotate things when holding sneak, which doesn't trigger a node's `on_rightclick`. +local default_on_place = minetest.registered_items[""].on_place +minetest.registered_items[""].on_place = function(item_stack, placer, pointed_thing, ...) + -- Player must sneak in order for this to run. Non-sneak is handled by node's `on_rightclick`. + if pointed_thing.type == "node" and minetest.is_player(placer) and placer:get_player_control().sneak then + local pos = pointed_thing.under + local node = minetest.get_node(pos) + if handle_rotatable_rightclick(pos, node, placer) then return end + end + default_on_place(item_stack, placer, pointed_thing, ...) +end + -- Calculates the state for the specified `player` and player `data`. -- This state contains information about how the player would rotate a block. @@ -59,26 +59,27 @@ local function calculate_rotating_state(player, data) state.pos = pointed_thing.under state.node = minetest.get_node(state.pos) - if not rotate.is_rotatable(state.node) then return end + if not registry.is_rotatable(state.node) then return end if vector.equals(pointed_thing.above, pointed_thing.under) then return end state.face = pointed_thing.above - pointed_thing.under - local edge, distance = utility.find_closest_edge(state.node, pointed_thing) - -- FIXME: Something in `find_closest_edge` is causing an unexpected edge to be returned, hence the additional check. - if edge and distance <= EDGE_DISTANCE and state.face:multiply(edge):length() ~= 0 then - state.axis = state.face:cross(edge):normalize() - state.mode = "edge" - else - state.axis = state.face - state.mode = "face" + local degrees = 90 + local r = utility.rotation_vector_from_lookat(state.node, pointed_thing, EDGE_DISTANCE) + state.axis = r + + local num = math_round(r.x * r.x + r.y * r.y + r.z * r.z) -- Squared length. + if num == 1 then state.mode = "face" + elseif num == 2 then state.mode = "edge"; state.axis = state.face:cross(r) + elseif num == 3 then state.mode = "mirror"; degrees = 120 end + -- Sneaking causes the direction of the rotation to be inverted. state.invert = player:get_player_control().sneak + if state.invert then degrees = -degrees end - local degrees = state.invert and 90 or -90 - state.facedir = rotate.rotate_facedir(state.node.param2, state.axis, degrees) - state.facedir = rotate.fix_rotatable_facedir(state.node, state.facedir) + state.facedir = rotate.rotate_facedir(state.node.param2, state.axis, -degrees) + state.facedir = registry.fix_rotatable_facedir(state.node, state.facedir) state.success = state.facedir and state.facedir ~= state.node.param2 return state @@ -97,19 +98,3 @@ minetest.register_on_leaveplayer(function(player) local name = player:get_player_name() rotating_state[name] = nil end) - -minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing) - if not minetest.is_player(puncher) then return end - local name = puncher:get_player_name() - local state = rotating_state[name] - if not state then return end - - -- Make sure we're still the same node that we raycasted in `playerstep`. - if not vector.equals(pos, state.pos) then return end - if node.name ~= state.node.name or node.param2 ~= state.node.param2 then return end - - -- Only continue if the node can actually be sucessfully rotated. - if not state.success then return end - - minetest.swap_node(pos, nodecore.underride({ param2 = state.facedir }, node)) -end) diff --git a/mod.conf b/mod.conf index dbe52e6..ecd202b 100755 --- a/mod.conf +++ b/mod.conf @@ -2,4 +2,4 @@ name = nc_extended_rotating title = NodeCore Extended Rotating author = copygirl description = Rotate optics and doors from NodeCore with ease. -depends = nc_api_all, nc_optics, nc_doors +depends = nc_api_all, nc_scaling, nc_optics, nc_doors diff --git a/registry.lua b/registry.lua new file mode 100644 index 0000000..b015812 --- /dev/null +++ b/registry.lua @@ -0,0 +1,73 @@ +local minetest, nodecore + = minetest, nodecore + +local registry = {} +local registered_rotatables = {} + +-- Registered rotatable nodes will call this function when their `on_rightclick` +-- is triggered instead of their default implementation. Can be overwritten. +registry.custom_on_rightclick = function(pos, node, clicker, item_stack, pointed_thing) end + +local function register_rotatable(name, facedir_lookup) + local def = minetest.registered_nodes[name] + if not def then error("Unknown node '" .. name .. "'") end + if def.paramtype2 ~= "facedir" then error("Node '" .. name .. "' must be 'facedir'") end + def.on_rightclick = function(...) registry.custom_on_rightclick(...) end + registered_rotatables[name] = { lookup = facedir_lookup } +end +registry.register_rotatable = register_rotatable + +-- Returns whether the specified `node` is rotatable by this mod. +local function is_rotatable(node) + local name = node and node.name or "" + return not not registered_rotatables[name] +end +registry.is_rotatable = is_rotatable + +-- Fixes facedir for the specified `node` when rotated to `facedir`. +-- * Returns `nil` if the node can't rotate this way. +-- * Returns `facedir` when the node can rotate this way. +-- * Returns another facedir when the node should rotate another equivalent way. +local function fix_rotatable_facedir(node, facedir) + local name = node and node.name or "" + local entry = registered_rotatables[name] + if not entry then return nil end + if not entry.lookup then return facedir end + return entry.lookup[facedir] +end +registry.fix_rotatable_facedir = fix_rotatable_facedir + + +-------------------------------------------- +-- Register rotatable nodes from NodeCore -- +-------------------------------------------- + +local function nodecore_filtered_lookup(eq_func) + local lookup = {} + for i = 0, 23 do + local facedir = nodecore.facedirs[i] + for j = 0, #lookup - 1 do + local other = nodecore.facedirs[lookup[j]] + if eq_func(facedir, other) then lookup[i] = j; break end + end + lookup[i] = lookup[i] or i + end + return lookup +end + +local LENS_FILTERED_LOOKUP = nodecore_filtered_lookup( + function(a, b) return vector.equals(a.f, b.f) end) +local PRISM_FILTERED_LOOKUP = nodecore_filtered_lookup( + function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end) +local PANEL_FILTERED_LOOKUP = nodecore_filtered_lookup( + function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end) + +for _, lens_state in ipairs({ "", "_on", "_glow", "_glow_start" }) do + register_rotatable("nc_optics:lens" .. lens_state, LENS_FILTERED_LOOKUP) end +for _, prism_state in ipairs({ "", "_on", "_gated" }) do + register_rotatable("nc_optics:prism" .. prism_state, PRISM_FILTERED_LOOKUP) end + +register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP) +register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP) + +return registry diff --git a/rotate.lua b/rotate.lua index ed6dbdb..5345894 100644 --- a/rotate.lua +++ b/rotate.lua @@ -7,8 +7,6 @@ local minetest, vector local rotate = {} -local rotatable_registry = {} - -- Axis lookup table based on how Minetest's `facedir` operates. local AXIS_LOOKUP = { vector.new( 0, 1, 0), -- +Y @@ -52,50 +50,44 @@ for up_index, up in ipairs(AXIS_LOOKUP) do end end -local function register_rotatable(name, facedir_lookup) - local def = minetest.registered_nodes[name] - if not def then error("Unknown node '" .. name .. "'") end - if def.paramtype2 ~= "facedir" then error("Node '" .. name .. "' must be 'facedir'") end - rotatable_registry[name] = { lookup = facedir_lookup } -end -rotate.register_rotatable = register_rotatable - --- Returns whether the specified `node` is rotatable by this mod. -local function is_rotatable(node) - local name = node and node.name or "" - return not not rotatable_registry[name] -end -rotate.is_rotatable = is_rotatable - --- Fixes facedir for the specified `node` when rotated to `facedir`. --- * Returns `nil` if the node can't rotate this way. --- * Returns `facedir` when the node can rotate this way. --- * Returns another facedir when the node should rotate another equivalent way. -local function fix_rotatable_facedir(node, facedir) - local name = node and node.name or "" - local entry = rotatable_registry[name] - if not entry then return nil end - if not entry.lookup then return facedir end - return entry.lookup[facedir] +-- Rotates (or "spins") the specified `node` to face `facedir`, +-- emulating the behavior of NodeCore's spin functionality. +local function rotate_node(pos, node, facedir) + local def = minetest.registered_nodes[node.name] + if (not def) or def.paramtype2 ~= "facedir" then error("Node's paramtype2 must be 'facedir'") end + minetest.swap_node(pos, nodecore.underride({ param2 = facedir }, node)) + nodecore.node_sound(pos, "place") + if def.on_spin then def.on_spin(pos, node) end end -rotate.fix_rotatable_facedir = fix_rotatable_facedir - --- Returns the specified `facedir` rotated around `axis` by `degrees` counter-clickwise. -local function rotate_facedir(facedir, axis, degrees) - if degrees % 90 ~= 0 then error("degrees must be divisible by 90") end - if axis_vector_to_index(axis) == nil then error("axis " .. tostring(axis) .. " is not an axis vector") end +rotate.rotate_node = rotate_node +-- Returns the `up` and `back` vectors that make up the specified `facedir`. +local function vectors_from_facedir(facedir) local up = AXIS_LOOKUP[1 + math_floor(facedir / 4)] local back = minetest.facedir_to_dir(facedir) + return up, back +end +rotate.vectors_from_facedir = vectors_from_facedir - up = up :rotate_around_axis(axis, math_rad(degrees)):round() - back = back:rotate_around_axis(axis, math_rad(degrees)):round() - +-- Returns a `facedir` constructed from the specified `up` and `back` vectors. +local function facedir_from_vectors(up, back) local up_index = axis_vector_to_index(up) local back_index = axis_vector_to_index(back) - return FACEDIR_LOOKUP[up_index][back_index] end +rotate.facedir_from_vectors = facedir_from_vectors + +-- Returns the specified `facedir` rotated around `vec` by `degrees` counter-clickwise. +local function rotate_facedir(facedir, vec, degrees) + -- NOTE: Removed the axis requirement so we can use this to rotate around corners. + -- However, since we `round` the result vectors this might ignore errors. + -- if degrees % 90 ~= 0 then error("degrees must be divisible by 90") end + -- if axis_vector_to_index(vec) == nil then error("axis is not an axis vector") end + local up, back = vectors_from_facedir(facedir) + up = up :rotate_around_axis(vec, math_rad(degrees)):round() + back = back:rotate_around_axis(vec, math_rad(degrees)):round() + return facedir_from_vectors(up, back) +end rotate.rotate_facedir = rotate_facedir -- Rotates the specified default orientation `{ min, max }` box to diff --git a/utility.lua b/utility.lua index 8695f10..9949081 100644 --- a/utility.lua +++ b/utility.lua @@ -29,39 +29,22 @@ local function get_node_active_selection_box(node) return box end --- Finds the closest edge of the node the player is looking at as --- a vector pointing away from the center, and the distance to it. -local function find_closest_edge(node, pointed_thing) - -- For this math to work, we assume that selection box is centered. - local max = get_node_active_selection_box(node).max - -- Point relative to the collision box we're pointing at. - local point = pointed_thing.intersection_point - pointed_thing.under - - -- Find the edge we're closest to. - local vec = vector.zero() - if math_abs(point.y / point.x) > max.x / max.y then - vec.y = math_sign(point.y) - if math_abs(point.z / point.x) > max.x / max.z - then vec.z = math_sign(point.z) - else vec.x = math_sign(point.x) - end - else - vec.x = math_sign(point.x) - if math_abs(point.z / point.y) > max.y / max.z - then vec.z = math_sign(point.z) - else vec.y = math_sign(point.y) - end - end - - local d = 1 - local v = max - point:multiply(vec):apply(math_abs) - local EPSILON = tonumber("1.19e-07") - if math_abs(v.x) > EPSILON and v.x < d then d = v.x end - if math_abs(v.y) > EPSILON and v.y < d then d = v.y end - if math_abs(v.z) > EPSILON and v.z < d then d = v.z end - - return vec, d +-- Calculates the rotation vector from where a player is looking at on this node. +-- * If looking near an edge, the vector points in the direction of that edge. +-- * If looking near a corner, the vector similarly points in that direction. +-- * Otherwise, vector simply points away from the face. +local function rotation_vector_from_lookat(node, pointed_thing, edge_distance) + local box = get_node_active_selection_box(node) + local center = (box.min + box.max) / 2 + + -- Calculate bounds, single vector describing the size of the selection box. + local bounds = box.max - center -- This extends both into positive and negative. + -- Calculate intersection point relative to these bounds. + local point = pointed_thing.intersection_point - pointed_thing.under - center + + return point:combine(bounds, function(p, b) + return b - math_abs(p) <= edge_distance and math_sign(p) or 0 end) end -utility.find_closest_edge = find_closest_edge +utility.rotation_vector_from_lookat = rotation_vector_from_lookat return utility