gate_functions.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. local MP = minetest.get_modpath(minetest.get_current_modname())
  2. -- Given a facedir, returns a set of all the corresponding directions
  3. local get_dirs = function(facedir)
  4. local dirs = {}
  5. local top = {[0]={x=0, y=1, z=0},
  6. {x=0, y=0, z=1},
  7. {x=0, y=0, z=-1},
  8. {x=1, y=0, z=0},
  9. {x=-1, y=0, z=0},
  10. {x=0, y=-1, z=0}}
  11. dirs.back = minetest.facedir_to_dir(facedir)
  12. dirs.top = top[math.floor(facedir/4)]
  13. dirs.right = {
  14. x=dirs.top.y*dirs.back.z - dirs.back.y*dirs.top.z,
  15. y=dirs.top.z*dirs.back.x - dirs.back.z*dirs.top.x,
  16. z=dirs.top.x*dirs.back.y - dirs.back.x*dirs.top.y
  17. }
  18. dirs.front = vector.multiply(dirs.back, -1)
  19. dirs.bottom = vector.multiply(dirs.top, -1)
  20. dirs.left = vector.multiply(dirs.right, -1)
  21. return dirs
  22. end
  23. -- Returns the axis that dir points along
  24. local dir_to_axis = function(dir)
  25. if dir.x ~= 0 then
  26. return "x"
  27. elseif dir.y ~= 0 then
  28. return "y"
  29. else
  30. return "z"
  31. end
  32. end
  33. -- Given a hinge definition, turns it into an axis and placement that can be used by the door rotation.
  34. local interpret_hinge = function(hinge_def, pos, node_dirs)
  35. local axis = dir_to_axis(node_dirs[hinge_def.axis])
  36. local placement
  37. if type(hinge_def.offset) == "string" then
  38. placement = vector.add(pos, node_dirs[hinge_def.offset])
  39. elseif type(hinge_def.offset) == "table" then
  40. placement = vector.new(0,0,0)
  41. local divisor = 0
  42. for _, val in pairs(hinge_def.offset) do
  43. placement = vector.add(placement, node_dirs[val])
  44. divisor = divisor + 1
  45. end
  46. placement = vector.add(pos, vector.divide(placement, divisor))
  47. else
  48. placement = pos
  49. end
  50. return axis, placement
  51. end
  52. --------------------------------------------------------------------------
  53. -- Rotation (slightly more complex than sliding)
  54. local facedir_rotate = {
  55. ['x'] = {
  56. [-1] = {[0]=4, 5, 6, 7, 22, 23, 20, 21, 0, 1, 2, 3, 13, 14, 15, 12, 19, 16, 17, 18, 10, 11, 8, 9}, -- 270 degrees
  57. [1] = {[0]=8, 9, 10, 11, 0, 1, 2, 3, 22, 23, 20, 21, 15, 12, 13, 14, 17, 18, 19, 16, 6, 7, 4, 5}, -- 90 degrees
  58. },
  59. ['y'] = {
  60. [-1] = {[0]=3, 0, 1, 2, 19, 16, 17, 18, 15, 12, 13, 14, 7, 4, 5, 6, 11, 8, 9, 10, 21, 22, 23, 20}, -- 270 degrees
  61. [1] = {[0]=1, 2, 3, 0, 13, 14, 15, 12, 17, 18, 19, 16, 9, 10, 11, 8, 5, 6, 7, 4, 23, 20, 21, 22}, -- 90 degrees
  62. },
  63. ['z'] = {
  64. [-1] = {[0]=16, 17, 18, 19, 5, 6, 7, 4, 11, 8, 9, 10, 0, 1, 2, 3, 20, 21, 22, 23, 12, 13, 14, 15}, -- 270 degrees
  65. [1] = {[0]=12, 13, 14, 15, 7, 4, 5, 6, 9, 10, 11, 8, 20, 21, 22, 23, 0, 1, 2, 3, 16, 17, 18, 19}, -- 90 degrees
  66. }
  67. }
  68. --90 degrees CW about x-axis: (x, y, z) -> (x, -z, y)
  69. --90 degrees CCW about x-axis: (x, y, z) -> (x, z, -y)
  70. --90 degrees CW about y-axis: (x, y, z) -> (-z, y, x)
  71. --90 degrees CCW about y-axis: (x, y, z) -> (z, y, -x)
  72. --90 degrees CW about z-axis: (x, y, z) -> (y, -x, z)
  73. --90 degrees CCW about z-axis: (x, y, z) -> (-y, x, z)
  74. local rotate_pos = function(axis, direction, pos)
  75. if axis == "x" then
  76. if direction < 0 then
  77. return {x= pos.x, y= -pos.z, z= pos.y}
  78. else
  79. return {x= pos.x, y= pos.z, z= -pos.y}
  80. end
  81. elseif axis == "y" then
  82. if direction < 0 then
  83. return {x= -pos.z, y= pos.y, z= pos.x}
  84. else
  85. return {x= pos.z, y= pos.y, z= -pos.x}
  86. end
  87. else
  88. if direction < 0 then
  89. return {x= -pos.y, y= pos.x, z= pos.z}
  90. else
  91. return {x= pos.y, y= -pos.x, z= pos.z}
  92. end
  93. end
  94. end
  95. local rotate_pos_displaced = function(pos, origin, axis, direction)
  96. -- position in space relative to origin
  97. local newpos = vector.subtract(pos, origin)
  98. newpos = rotate_pos(axis, direction, newpos)
  99. -- Move back to original reference frame
  100. return vector.add(newpos, origin)
  101. end
  102. local get_buildable_to = function(pos)
  103. return minetest.registered_nodes[minetest.get_node(pos).name].buildable_to
  104. end
  105. local get_door_layout = function(pos, facedir, player)
  106. if facedir > 23 then return nil end -- A bug in another mod once resulted in bad param2s being written to nodes, this will at least prevent crashes if something like that happens again.
  107. -- This method does a flood-fill looking for all nodes that meet the following criteria:
  108. -- belongs to a "castle_gate" group
  109. -- has the same "back" direction as the initial node
  110. -- is accessible via up, down, left or right directions unless one of those directions goes through an edge that one of the two nodes has marked as a gate edge
  111. local door = {}
  112. door.all = {}
  113. door.contains_protected_node = false
  114. door.directions = get_dirs(facedir)
  115. door.previous_move = minetest.get_meta(pos):get_string("previous_move")
  116. -- temporary pointsets used while searching
  117. local to_test = {}
  118. local tested = {}
  119. local can_slide_to = {}
  120. local castle_gate_group_value -- this will be populated from the first gate node we encounter, which will be the one that was clicked on
  121. local test_pos = pos
  122. while test_pos ~= nil do
  123. local test_pos_hash = minetest.hash_node_position(test_pos)
  124. tested[test_pos_hash] = true -- track nodes we've looked at
  125. local test_node = minetest.get_node(test_pos)
  126. if test_node.name == "ignore" then
  127. --array is next to unloaded nodes, too dangerous to do anything. Abort.
  128. return nil
  129. end
  130. if minetest.is_protected(test_pos, player:get_player_name()) and not minetest.check_player_privs(player, "protection_bypass") then
  131. door.contains_protected_node = true
  132. end
  133. local test_node_def = minetest.registered_nodes[test_node.name]
  134. if test_node_def.buildable_to then
  135. can_slide_to[test_pos_hash] = true
  136. end
  137. if test_node_def.paramtype2 == "facedir" and test_node.param2 <= 23 then -- prospective door nodes need to be of type facedir and have a valid param2
  138. local test_node_dirs = get_dirs(test_node.param2)
  139. local coplanar = vector.equals(test_node_dirs.back, door.directions.back) -- the "back" vector needs to point in the same direction as the rest of the door
  140. if castle_gate_group_value == nil and test_node_def.groups.castle_gate ~= nil then
  141. castle_gate_group_value = test_node_def.groups.castle_gate -- read the group value from the first gate node encountered
  142. end
  143. if coplanar and test_node_def.groups.castle_gate == castle_gate_group_value then
  144. local entry = {["pos"] = test_pos, ["node"] = test_node}
  145. table.insert(door.all, entry) -- it's definitely a gate node of some sort.
  146. if test_node_def._gate_hinge ~= nil then -- it's a hinge type of node, need to do extra work
  147. local axis, placement = interpret_hinge(test_node_def._gate_hinge, test_pos, test_node_dirs)
  148. if door.hinge == nil then -- this is the first hinge we've encountered.
  149. door.hinge = {axis=axis, placement=placement}
  150. door.directions = test_node_dirs -- force the door as a whole to use the same reference frame as the first hinge
  151. elseif door.hinge.axis ~= axis then -- there was a previous hinge. Do they rotate on the same axis?
  152. return nil -- Misaligned hinge axes, door cannot rotate.
  153. else
  154. local axis_dir = {x=0, y=0, z=0}
  155. axis_dir[axis] = 1
  156. local displacement = vector.normalize(vector.subtract(placement, door.hinge.placement)) -- check if this new hinge is displaced relative to the first hinge on any axis other than the rotation axis
  157. if not (vector.equals(displacement, axis_dir) or vector.equals(displacement, vector.multiply(axis_dir, -1))) then
  158. return nil -- Misaligned hinge offset, door cannot rotate.
  159. end
  160. end
  161. end
  162. can_slide_to[test_pos_hash] = true -- since this is part of the door, other parts of the door can slide into it
  163. local test_directions = {"top", "bottom", "left", "right"}
  164. for _, dir in pairs(test_directions) do
  165. local adjacent_pos = vector.add(test_pos, door.directions[dir])
  166. local adjacent_node = minetest.get_node(adjacent_pos)
  167. local adjacent_def = minetest.registered_nodes[adjacent_node.name]
  168. local adjacent_pos_hash = minetest.hash_node_position(adjacent_pos)
  169. if adjacent_def.buildable_to then
  170. can_slide_to[adjacent_pos_hash] = true
  171. end
  172. if test_node_def._gate_edges == nil or not test_node_def._gate_edges[dir] then -- if we ourselves are an edge node, don't look in the direction we're an edge in
  173. if tested[adjacent_pos_hash] == nil then -- don't look at nodes that have already been looked at
  174. if adjacent_def.paramtype2 == "facedir" then -- all doors are facedir nodes so we can pre-screen some targets
  175. local edge_points_back_at_test_pos = false
  176. -- Look at the adjacent node's definition. If it's got gate edges, check if they point back at us.
  177. if adjacent_def._gate_edges ~= nil then
  178. local adjacent_directions = get_dirs(adjacent_node.param2)
  179. for dir, val in pairs(adjacent_def._gate_edges) do
  180. if vector.equals(vector.add(adjacent_pos, adjacent_directions[dir]), test_pos) then
  181. edge_points_back_at_test_pos = true
  182. break
  183. end
  184. end
  185. end
  186. if not edge_points_back_at_test_pos then
  187. table.insert(to_test, adjacent_pos_hash)
  188. end
  189. end
  190. end
  191. end
  192. end
  193. end
  194. end
  195. test_pos = table.remove(to_test)
  196. if test_pos ~= nil then
  197. test_pos = minetest.get_position_from_hash(test_pos)
  198. end
  199. end
  200. if door.hinge == nil then
  201. --sliding door, evaluate which directions it can go
  202. door.can_slide = {top=true, bottom=true, left=true, right=true}
  203. for _,door_node in pairs(door.all) do
  204. door.can_slide.top = door.can_slide.top and can_slide_to[minetest.hash_node_position(vector.add(door_node.pos, door.directions.top))]
  205. door.can_slide.bottom = door.can_slide.bottom and can_slide_to[minetest.hash_node_position(vector.add(door_node.pos, door.directions.bottom))]
  206. door.can_slide.left = door.can_slide.left and can_slide_to[minetest.hash_node_position(vector.add(door_node.pos, door.directions.left))]
  207. door.can_slide.right = door.can_slide.right and can_slide_to[minetest.hash_node_position(vector.add(door_node.pos, door.directions.right))]
  208. end
  209. else
  210. --rotating door, evaluate which direction it can go. Slightly more complicated.
  211. local origin = door.hinge.placement
  212. local axis = door.hinge.axis
  213. local backfront = dir_to_axis(door.directions.back)
  214. local leftright = dir_to_axis(door.directions.right)
  215. door.swings = {}
  216. for _, direction in pairs({-1, 1}) do
  217. door.swings[direction] = true
  218. for _, door_node in pairs(door.all) do
  219. origin[axis] = door_node.pos[axis]
  220. if not vector.equals(door_node.pos, origin) then -- There's no obstruction if the node is literally located along the rotation axis
  221. local newpos = rotate_pos_displaced(door_node.pos, origin, axis, direction)
  222. local newnode = minetest.get_node(newpos)
  223. local newdef = minetest.registered_nodes[newnode.name]
  224. if not newdef.buildable_to then -- check if the destination node is free.
  225. door.swings[direction] = false
  226. break
  227. end
  228. local swing_corner = {} -- the corner of the square "arc" that a Minetest gate swings through
  229. local scan_dir
  230. swing_corner[axis] = door_node.pos[axis]
  231. swing_corner[backfront] = newpos[backfront]
  232. swing_corner[leftright] = door_node.pos[leftright]
  233. if not (vector.equals(newpos, swing_corner) or vector.equals(door_node.pos, swing_corner)) then -- we're right next to the hinge, no need for further testing
  234. scan_dir = vector.direction(newpos, swing_corner) -- get the direction from the new door position toward the swing corner
  235. repeat
  236. newpos = vector.add(newpos, scan_dir) -- we start with newpos on the destination node, which has already been tested.
  237. if not get_buildable_to(newpos) then
  238. door.swings[direction] = false
  239. end
  240. until vector.equals(newpos, swing_corner) or door.swings[direction] == false
  241. if not (vector.equals(newpos, door_node.pos) or door.swings[direction] == false) then
  242. scan_dir = vector.direction(newpos, door_node.pos)
  243. newpos = vector.add(newpos, scan_dir) -- the first step here is a freebie since we've already checked swing_corner
  244. while not (vector.equals(newpos, door_node.pos) or door.swings[direction] == false) do
  245. if not get_buildable_to(newpos) then
  246. door.swings[direction] = false
  247. end
  248. newpos = vector.add(newpos, scan_dir)
  249. end
  250. end
  251. end
  252. end
  253. if door.swings[direction] == false then
  254. break
  255. end
  256. end
  257. end
  258. end
  259. return door
  260. end
  261. local slide_gate = function(door, direction)
  262. for _, door_node in pairs(door.all) do
  263. door_node.pos = vector.add(door_node.pos, door.directions[direction])
  264. end
  265. door.previous_move = direction
  266. end
  267. local rotate_door = function (door, direction)
  268. if not door.swings[direction] then
  269. return false
  270. end
  271. local origin = door.hinge.placement
  272. local axis = door.hinge.axis
  273. for _, door_node in pairs(door.all) do
  274. door_node.pos = rotate_pos_displaced(door_node.pos, origin, axis, direction)
  275. door_node.node.param2 = facedir_rotate[axis][direction][door_node.node.param2]
  276. end
  277. return true
  278. end
  279. ----------------------------------------------------------------------------------------------------
  280. -- When creating new gate pieces use this as the "on_rightclick" method of their node definitions
  281. -- if you want the player to be able to trigger the gate by clicking on that particular node.
  282. -- If you just want the node to move with the gate and not trigger it this isn't necessary,
  283. -- only the "castle_gate" group is needed for that.
  284. castle_gates.trigger_gate = function(pos, node, player)
  285. local door = get_door_layout(pos, node.param2, player)
  286. if door ~= nil then
  287. for _, door_node in pairs(door.all) do
  288. minetest.set_node(door_node.pos, {name="air"})
  289. end
  290. local door_moved = false
  291. if door.can_slide ~= nil then -- this is a sliding door
  292. if door.previous_move == "top" and door.can_slide.top then
  293. slide_gate(door, "top")
  294. door_moved = true
  295. elseif door.previous_move == "bottom" and door.can_slide.bottom then
  296. slide_gate(door, "bottom")
  297. door_moved = true
  298. elseif door.previous_move == "left" and door.can_slide.left then
  299. slide_gate(door, "left")
  300. door_moved = true
  301. elseif door.previous_move == "right" and door.can_slide.right then
  302. slide_gate(door, "right")
  303. door_moved = true
  304. end
  305. if not door_moved then -- reverse door's direction for next time
  306. if door.previous_move == "top" and door.can_slide.bottom then
  307. door.previous_move = "bottom"
  308. elseif door.previous_move == "bottom" and door.can_slide.top then
  309. door.previous_move = "top"
  310. elseif door.previous_move == "left" and door.can_slide.right then
  311. door.previous_move = "right"
  312. elseif door.previous_move == "right" and door.can_slide.left then
  313. door.previous_move = "left"
  314. else
  315. -- find any open direction
  316. for slide_dir, enabled in pairs(door.can_slide) do
  317. if enabled then
  318. door.previous_move = slide_dir
  319. break
  320. end
  321. end
  322. end
  323. end
  324. elseif door.hinge ~= nil then -- this is a hinged door
  325. if door.previous_move == "deosil" then
  326. door_moved = rotate_door(door, 1)
  327. elseif door.previous_move == "widdershins" then
  328. door_moved = rotate_door(door, -1)
  329. end
  330. if not door_moved then
  331. if door.previous_move == "deosil" then
  332. door.previous_move = "widdershins"
  333. else
  334. door.previous_move = "deosil"
  335. end
  336. end
  337. end
  338. for _, door_node in pairs(door.all) do
  339. minetest.set_node(door_node.pos, door_node.node)
  340. minetest.get_meta(door_node.pos):set_string("previous_move", door.previous_move)
  341. end
  342. if door_moved then
  343. minetest.after(1, function()
  344. castle_gates.trigger_gate(door.all[1].pos, door.all[1].node, player)
  345. end)
  346. end
  347. end
  348. end