diff --git a/hud.lua b/hud.lua index 200cff8..7ca00ae 100644 --- a/hud.lua +++ b/hud.lua @@ -6,22 +6,23 @@ local LABEL_ROTATION_HINT = "rotation hint" local TEX_ROTATE_FACE = "nc_extended_rotating_hud_rotate_face.png" local TEX_ROTATE_EDGE = "nc_extended_rotating_hud_rotate_edge.png" + local hud = {} -local function crosshair_hud_element(texture) - return { - label = LABEL_ROTATION_HINT, - hud_elem_type = "image", - text = texture, - position = { x = 0.5, y = 0.5 }, - offset = { x = 0, y = 0 }, - alignment = { x = 0, y = 0 }, - scale = { x = 1, y = 1 }, - quick = true - } -end +function hud.update_player_hud(player, state) + local function crosshair_hud_element(texture) + return { + label = LABEL_ROTATION_HINT, + hud_elem_type = "image", + text = texture, + position = { x = 0.5, y = 0.5 }, + offset = { x = 0, y = 0 }, + alignment = { x = 0, y = 0 }, + scale = { x = 1, y = 1 }, + quick = true + } + end -local function update_player_hud(player, state) local mode = state and state.mode if mode == "face" then local texture = TEX_ROTATE_FACE @@ -32,6 +33,5 @@ local function update_player_hud(player, state) nodecore.hud_set(player, { label = LABEL_ROTATION_HINT, ttl = 0 }) end end -hud.update_player_hud = update_player_hud return hud diff --git a/init.lua b/init.lua index 97563aa..2038823 100755 --- a/init.lua +++ b/init.lua @@ -1,104 +1,115 @@ -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 registry = include("registry") - +local ipairs, ItemStack, minetest, vector, include, nodecore + = ipairs, ItemStack, minetest, vector, include, nodecore + +rawset(_G, "nc_extended_rotating", {}) +nc_extended_rotating.hud = include("hud") +nc_extended_rotating.registry = include("registry") +nc_extended_rotating.rotate = include("rotate") +nc_extended_rotating.utility = include("utility") -- Depends on `rotate`. +nc_extended_rotating.state = include("state") -- Depends on `registry`, `rotate` and `utility`. + +local update_player_hud = nc_extended_rotating.hud.update_player_hud +local is_rotatable = nc_extended_rotating.registry.is_rotatable +local register_rotatable = nc_extended_rotating.registry.register_rotatable; +local rotate_node = nc_extended_rotating.rotate.rotate_node +local update_rotating_state = nc_extended_rotating.state.update_rotating_state +local get_rotating_state = nc_extended_rotating.state.get_rotating_state + +-- TODO: Fix HUD showing rotation hint when we wouldn't / can't rotate. -- TODO: Add crosshair indicators for rotating around edges. -- 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 = 2.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 update the their HUD and to decide what to do when a rightclick occurs. -local rotating_state = {} +nodecore.register_playerstep({ + label = "nc_extended_rotating:update", + action = function(player, data) + local state = update_rotating_state(player, data) + update_player_hud(player, state) + end, +}) +-- Register rotatable nodes from NodeCore. +do + 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 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. + 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) - -- 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 + 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 - rotate.rotate_node(pos, node, state.facedir) - return true + register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP) + register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP) 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, ...) +local function handle_rightclick(pos, node, clicker) + local state = get_rotating_state(clicker, pos, node) + if (not state) or (not state.success) then return end + rotate_node(pos, node, state.facedir) end - --- Calculates the state for the specified `player` and player `data`. --- This state contains information about how the player would rotate a block. -local function calculate_rotating_state(player, data) - local state = {} - - local pointed_thing = data.raycast() - if (not pointed_thing) or pointed_thing.type ~= "node" then return nil end - - state.pos = pointed_thing.under - state.node = minetest.get_node(state.pos) - if not registry.is_rotatable(state.node) then return nil end - - if vector.equals(pointed_thing.above, pointed_thing.under) then return nil end - state.face = pointed_thing.above - pointed_thing.under - - -- When player is sneaking, must be empty-handed for rotating to work. - local is_sneaking = player:get_player_control().sneak - if is_sneaking and (not player:get_wielded_item():is_empty()) then return nil end - - 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 +-- Replace `on_rightclick` of rotatable nodes from NodeCore. +do + local function replace_optics_on_rightclick(name) + local def = minetest.registered_nodes[name] + def.on_rightclick = handle_rightclick end - -- Sneaking causes the direction of the rotation to be inverted. - if is_sneaking then degrees = -degrees end - state.invert = is_sneaking + -- Panels can't have their `on_rightclick` overridden completely. + -- We need to call the default implementation when holding a "pin". + local function replace_panel_on_rightclick(name, pin) + local def = minetest.registered_nodes[name] + local default_rightclick = def.on_rightclick + def.on_rightclick = function(pos, node, clicker, item_stack, ...) + if nodecore.protection_test(pos, clicker) or ItemStack(item_stack):get_name() == pin + then return default_rightclick(pos, node, clicker, item_stack, ...) + else return handle_rightclick(pos, node, clicker) + end + end + end - 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 + for _, lens_state in ipairs({ "", "_on", "_glow", "_glow_start" }) do + replace_optics_on_rightclick("nc_optics:lens" .. lens_state) end + for _, lens_state in ipairs({ "", "_on", "_gated" }) do + replace_optics_on_rightclick("nc_optics:prism" .. lens_state) end - return state + replace_panel_on_rightclick("nc_doors:panel_plank" , "nc_woodwork:staff") + replace_panel_on_rightclick("nc_doors:panel_cobble", "nc_lode:rod_tempered") end -nodecore.register_playerstep({ - label = "extended rotating update", - action = function(player, data) - local name = player:get_player_name() - rotating_state[name] = calculate_rotating_state(player, data) - hud.update_player_hud(player, rotating_state[name]) - end, -}) - -minetest.register_on_leaveplayer(function(player) - local name = player:get_player_name() - rotating_state[name] = nil -end) +-- 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, ...) + if pointed_thing.type == "node" then + -- Player must sneak in order for this to run. + -- Non-sneak is handled by node's `on_rightclick`. + if minetest.is_player(placer) and placer:get_player_control().sneak then + local pos = pointed_thing.under + local node = minetest.get_node(pos) + if is_rotatable(node) then + handle_rightclick(pos, node, placer) + return -- Skip default behavior. + end + end + end + -- Call the default function, so we can still get the "scaling" functionality. + default_on_place(item_stack, placer, pointed_thing, ...) +end diff --git a/registry.lua b/registry.lua index b015812..e5df75e 100644 --- a/registry.lua +++ b/registry.lua @@ -1,73 +1,34 @@ -local minetest, nodecore - = minetest, nodecore +local minetest + = minetest -local registry = {} +-- Table of nodes that can be rotated by the mod. 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 registry = {} + +function registry.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] +function registry.is_rotatable(node) + return not not registered_rotatables[node.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) +function registry.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 5345894..26bb8c6 100644 --- a/rotate.lua +++ b/rotate.lua @@ -5,8 +5,6 @@ local math_min, math_max, math_floor, math_rad local minetest, vector = minetest, vector -local rotate = {} - -- Axis lookup table based on how Minetest's `facedir` operates. local AXIS_LOOKUP = { vector.new( 0, 1, 0), -- +Y @@ -50,24 +48,12 @@ for up_index, up in ipairs(AXIS_LOOKUP) do end end --- 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.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 -- Returns a `facedir` constructed from the specified `up` and `back` vectors. local function facedir_from_vectors(up, back) @@ -75,10 +61,22 @@ local function facedir_from_vectors(up, back) local back_index = axis_vector_to_index(back) return FACEDIR_LOOKUP[up_index][back_index] end -rotate.facedir_from_vectors = facedir_from_vectors + + +local rotate = {} + +-- Rotates (or "spins") the specified `node` to face `facedir`, +-- emulating the behavior of NodeCore's spin functionality. +function rotate.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 -- Returns the specified `facedir` rotated around `vec` by `degrees` counter-clickwise. -local function rotate_facedir(facedir, vec, degrees) +function rotate.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 @@ -88,11 +86,10 @@ local function rotate_facedir(facedir, vec, degrees) 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 -- one matching Minetest's rotation algorithm pointing to `facedir`. -local function rotate_box_by_facedir(box, facedir) +function rotate.rotate_box_by_facedir(box, facedir) if facedir == 0 then return end local axis_index = math_floor(facedir / 4) @@ -115,6 +112,5 @@ local function rotate_box_by_facedir(box, facedir) box.min.y, box.max.y = math_min(box.min.y, box.max.y), math_max(box.min.y, box.max.y) box.min.z, box.max.z = math_min(box.min.z, box.max.z), math_max(box.min.z, box.max.z) end -rotate.rotate_box_by_facedir = rotate_box_by_facedir return rotate diff --git a/state.lua b/state.lua new file mode 100755 index 0000000..c093013 --- /dev/null +++ b/state.lua @@ -0,0 +1,83 @@ +local math_round, minetest, vector + = math.round, minetest, vector + +local fix_rotatable_facedir = nc_extended_rotating.registry.fix_rotatable_facedir; +local is_rotatable = nc_extended_rotating.registry.is_rotatable; +local rotate_facedir = nc_extended_rotating.rotate.rotate_facedir; +local rotation_vector_from_lookat = nc_extended_rotating.utility.rotation_vector_from_lookat; + +-- Distance at which we want to rotate by "pushing" on an edge. +local EDGE_DISTANCE = 2.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 update the their HUD and to decide what to do when a rightclick occurs. +local rotating_state = {} + +-- Calculates the state for the specified `player` and player `data`. +-- This state contains information about how the player would rotate a block. +local function calculate_rotating_state(player, data) + local state = {} + + local pointed_thing = data.raycast() + if (not pointed_thing) or pointed_thing.type ~= "node" then return nil end + + state.pos = pointed_thing.under + state.node = minetest.get_node(state.pos) + if not is_rotatable(state.node) then return nil end + + if vector.equals(pointed_thing.above, pointed_thing.under) then return nil end + state.face = pointed_thing.above - pointed_thing.under + + -- When player is sneaking, must be empty-handed for rotating to work. + local is_sneaking = player:get_player_control().sneak + if is_sneaking and (not player:get_wielded_item():is_empty()) then return nil end + + local degrees = 90 + local r = 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. + if is_sneaking then degrees = -degrees end + state.invert = is_sneaking + + state.facedir = rotate_facedir(state.node.param2, state.axis, -degrees) + state.facedir = fix_rotatable_facedir(state.node, state.facedir) + state.success = state.facedir and state.facedir ~= state.node.param2 + + return state +end + +minetest.register_on_leaveplayer(function(player) + local name = player:get_player_name() + rotating_state[name] = nil +end) + + +local state = {} + +function state.update_rotating_state(player, data) + local rot_state = calculate_rotating_state(player, data) + rotating_state[player:get_player_name()] = rot_state + return rot_state +end + +function state.get_rotating_state(player, pos, node) + if not minetest.is_player(player) then return nil end + local rot_state = rotating_state[player:get_player_name()] + if not rot_state then return nil end + + if not vector.equals(pos, rot_state.pos) then return nil end + if node.name ~= rot_state.node.name or node.param2 ~= rot_state.node.param2 then return nil end + + return rot_state +end + +return state diff --git a/utility.lua b/utility.lua index 9949081..f42da82 100644 --- a/utility.lua +++ b/utility.lua @@ -1,12 +1,8 @@ -local math_abs, math_sign - = math.abs, math.sign -local minetest, vector, include - = minetest, vector, include +local minetest, vector, math_abs, math_sign + = minetest, vector, math.abs, math.sign -local rotate = include("rotate") - -local utility = {} +local rotate_box_by_facedir = nc_extended_rotating.rotate.rotate_box_by_facedir -- Default selection boxes for a node that doesn't have one them explicitly. local DEFAULT_SELECTION_BOX = { min = vector.new(-0.5, -0.5, -0.5), @@ -25,15 +21,18 @@ local function get_node_active_selection_box(node) box = { min = vector.new(box[1], box[2], box[3]), max = vector.new(box[4], box[5], box[6]) } -- Rotate the box to face `facedir`. - rotate.rotate_box_by_facedir(box, node.param2) + rotate_box_by_facedir(box, node.param2) return box end + +local utility = {} + -- 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) +function utility.rotation_vector_from_lookat(node, pointed_thing, edge_distance) local box = get_node_active_selection_box(node) local center = (box.min + box.max) / 2 @@ -45,6 +44,5 @@ local function rotation_vector_from_lookat(node, pointed_thing, edge_distance) return point:combine(bounds, function(p, b) return b - math_abs(p) <= edge_distance and math_sign(p) or 0 end) end -utility.rotation_vector_from_lookat = rotation_vector_from_lookat return utility