functions.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. local S = minetest.get_translator("travelnet")
  2. local function string_endswith(str, ends)
  3. local len = #ends
  4. if str:sub(-len) == ends then
  5. return str:sub(1, -len-1)
  6. end
  7. end
  8. local function string_startswith(str, start)
  9. local len = #start
  10. if str:sub(1, len) == start then
  11. return str:sub(len+1)
  12. end
  13. end
  14. function travelnet.is_falsey_string(str)
  15. return not str or str == ""
  16. end
  17. function travelnet.node_description(pos)
  18. local node = minetest.get_node_or_nil(pos)
  19. if not node then return end
  20. local description
  21. if minetest.get_item_group(node.name, "travelnet") == 1 then
  22. description = S("travelnet box")
  23. elseif minetest.get_item_group(node.name, "elevator") == 1 then
  24. description = S("elevator")
  25. elseif node.name == "locked_travelnet:travelnet" then
  26. description = S("locked travelnet")
  27. elseif node.name == "locked_travelnet:elevator" then
  28. description = S("locked elevator")
  29. else
  30. description = nil
  31. end
  32. return description, node.name
  33. end
  34. function travelnet.find_nearest_elevator_network(pos, owner_name)
  35. local nearest_network = false
  36. local nearest_dist = false
  37. local nearest_dist_x
  38. local nearest_dist_z
  39. local player_travelnets = travelnet.get_travelnets(owner_name)
  40. for target_network_name, network in pairs(player_travelnets) do
  41. local station_name = next(network, nil)
  42. if station_name then
  43. local station = network[station_name]
  44. if station.nr and station.pos then
  45. local dist_x = station.pos.x - pos.x
  46. local dist_z = station.pos.z - pos.z
  47. local dist = math.ceil(math.sqrt(dist_x * dist_x + dist_z * dist_z))
  48. -- find the nearest one; store network_name and (minimal) distance
  49. if not nearest_dist or dist < nearest_dist then
  50. nearest_dist = dist
  51. nearest_dist_x = dist_x
  52. nearest_dist_z = dist_z
  53. nearest_network = target_network_name
  54. end
  55. end
  56. end
  57. end
  58. return nearest_network, {
  59. x = nearest_dist_x,
  60. z = nearest_dist_z,
  61. }
  62. end
  63. function travelnet.elevator_network(pos)
  64. return tostring(pos.x) .. "," .. tostring(pos.z)
  65. end
  66. function travelnet.is_elevator(node_name)
  67. return node_name == "travelnet:elevator"
  68. end
  69. function travelnet.door_is_open(node, opposite_direction)
  70. return string.sub(node.name, -5) == "_open"
  71. -- handle doors that change their facedir
  72. or (
  73. node.param2 ~= opposite_direction
  74. and not (
  75. string_startswith(node.name, "travelnet:elevator_door")
  76. and string_endswith(node.name, "_closed")
  77. )
  78. )
  79. end
  80. function travelnet.door_is_closed(node, opposite_direction)
  81. return string.sub(node.name, -7) == "_closed"
  82. -- handle doors that change their facedir
  83. or (
  84. node.param2 == opposite_direction
  85. and not (
  86. string_startswith(node.name, "travelnet:elevator_door")
  87. and string_endswith(node.name, "_open")
  88. )
  89. )
  90. end
  91. function travelnet.param2_to_yaw(param2)
  92. if param2 == 0 then
  93. return 180
  94. elseif param2 == 1 then
  95. return 90
  96. elseif param2 == 2 then
  97. return 0
  98. elseif param2 == 3 then
  99. return 270
  100. end
  101. end
  102. function travelnet.get_network(owner_name, network_name)
  103. local player_travelnets = travelnet.get_travelnets(owner_name)
  104. if not player_travelnets then return end
  105. return player_travelnets[network_name]
  106. end
  107. function travelnet.get_ordered_stations(owner_name, network_name, is_elevator)
  108. local travelnets = travelnet.get_travelnets(owner_name)
  109. local network = travelnets[network_name]
  110. if not network then
  111. return {}
  112. end
  113. local stations = {}
  114. for k in pairs(network) do
  115. table.insert(stations, k)
  116. end
  117. if is_elevator then
  118. local ground_level = 1
  119. table.sort(stations, function(a, b)
  120. return network[a].pos.y > network[b].pos.y
  121. end)
  122. -- find ground level
  123. local vgl_timestamp = 999999999999
  124. for index,k in ipairs(stations) do
  125. local station = network[k]
  126. if not station.timestamp then
  127. station.timestamp = os.time()
  128. end
  129. if station.timestamp < vgl_timestamp then
  130. vgl_timestamp = station.timestamp
  131. ground_level = index
  132. end
  133. end
  134. for index,k in ipairs(stations) do
  135. local station = network[k]
  136. if index == ground_level then
  137. station.nr = "G"
  138. else
  139. station.nr = tostring(ground_level - index)
  140. end
  141. end
  142. -- TODO: hacky workaround for setting the "nr" field on the stations
  143. -- should be done on elevator placement instead
  144. travelnet.log("action", "creating ad-hoc elevator fields for player '" .. owner_name ..
  145. "' and network '" .. network_name .. "'")
  146. travelnet.set_travelnets(owner_name, travelnets)
  147. else
  148. -- sort the table according to the timestamp (=time the station was configured)
  149. table.sort(stations, function(a, b)
  150. return network[a].timestamp < network[b].timestamp
  151. end)
  152. end
  153. return stations
  154. end
  155. function travelnet.get_station(owner_name, station_network, station_name)
  156. local network = travelnet.get_network(owner_name, station_network)
  157. if not network then return end
  158. return network[station_name]
  159. end
  160. -- punching the travelnet updates its formspec and shows it to the player;
  161. -- however, that would be very annoying when actually trying to dig the thing.
  162. -- Thus, check if the player is wielding a tool that can dig nodes of the
  163. -- group cracky
  164. function travelnet.check_if_trying_to_dig(puncher)
  165. -- if in doubt: show formspec
  166. if not puncher or not puncher:get_wielded_item() then
  167. return false
  168. end
  169. -- show menu when in creative mode
  170. if creative and creative.is_enabled_for(puncher:get_player_name()) then
  171. return false
  172. end
  173. local tool_capabilities = puncher:get_wielded_item():get_tool_capabilities()
  174. if not tool_capabilities or not tool_capabilities["groupcaps"] or not tool_capabilities["groupcaps"]["cracky"] then
  175. return false
  176. end
  177. -- tools which can dig cracky items can start digging immediately
  178. return true
  179. end
  180. -- allow doors to open
  181. function travelnet.open_close_door(pos, player, mode)
  182. local this_node = minetest.get_node_or_nil(pos)
  183. -- give up if the area is *still* not loaded
  184. if not this_node then
  185. return
  186. end
  187. local opposite_direction = (this_node.param2 + 2) % 4
  188. local door_pos = vector.add(pos, minetest.facedir_to_dir(opposite_direction))
  189. local door_node = minetest.get_node_or_nil(door_pos)
  190. if not door_node or door_node.name == "ignore" or door_node.name == "air"
  191. or not minetest.registered_nodes[door_node.name] then
  192. return
  193. end
  194. local right_click_action = minetest.registered_nodes[door_node.name].on_rightclick
  195. if not right_click_action then return end
  196. if not minetest.registered_nodes[door_node.name].groups["door"] then
  197. return
  198. end
  199. -- Map to old API in case anyone is using it externally
  200. if mode == 0 then mode = "toggle"
  201. elseif mode == 1 then mode = "close"
  202. elseif mode == 2 then mode = "open"
  203. end
  204. -- at least for homedecor, same facedir would mean "door closed"
  205. -- do not close the elevator door if it is already closed
  206. if mode == "close" and travelnet.door_is_closed(door_node, opposite_direction) then
  207. return
  208. end
  209. -- do not open the doors if they are already open (works only on elevator-doors; not on doors in general)
  210. if mode == "open" and travelnet.door_is_open(door_node, opposite_direction) then
  211. return
  212. end
  213. if mode == "open" then
  214. local playername = player:get_player_name()
  215. minetest.after(1, function()
  216. -- Get the player again in case it doesn't exist anymore (logged out)
  217. local pplayer = minetest.get_player_by_name(playername)
  218. if pplayer then
  219. right_click_action(door_pos, door_node, pplayer, ItemStack(""))
  220. end
  221. end)
  222. else
  223. right_click_action(door_pos, door_node, player, ItemStack(""))
  224. end
  225. end
  226. travelnet.rotate_player = function(target_pos, player)
  227. local target_node = minetest.get_node_or_nil(target_pos)
  228. if target_node == nil then return end
  229. -- play sound at the target position as well
  230. if travelnet.travelnet_sound_enabled then
  231. local sound = "travelnet_travel"
  232. if travelnet.is_elevator(target_node.name) then
  233. sound = "travelnet_bell"
  234. end
  235. minetest.sound_play(sound, {
  236. pos = target_pos,
  237. gain = 0.75,
  238. max_hear_distance = 10
  239. })
  240. end
  241. -- do this only on servers where the function exists
  242. if player.set_look_horizontal then
  243. -- rotate the player so that they can walk straight out of the box
  244. local yaw = travelnet.param2_to_yaw(target_node.param2) or 0
  245. player:set_look_horizontal(math.rad(yaw))
  246. player:set_look_vertical(math.rad(0))
  247. end
  248. travelnet.open_close_door(target_pos, player, "open")
  249. end
  250. travelnet.remove_box_action = function(oldmetadata)
  251. if not oldmetadata or oldmetadata == "nil" or not oldmetadata.fields then
  252. return false, S("Could not find information about the station that is to be removed.")
  253. end
  254. local owner_name = oldmetadata.fields["owner"]
  255. local station_name = oldmetadata.fields["station_name"]
  256. local station_network = oldmetadata.fields["station_network"]
  257. -- station is not known? then just remove it
  258. if not (owner_name and station_network and station_name)
  259. or not travelnet.get_station(owner_name, station_network, station_name)
  260. then
  261. return false, S("Could not find the station that is to be removed.")
  262. end
  263. local player_travelnets = travelnet.get_travelnets(owner_name)
  264. player_travelnets[station_network][station_name] = nil
  265. travelnet.set_travelnets(owner_name, player_travelnets)
  266. return true
  267. end
  268. travelnet.remove_box_message = function(oldmetadata, digger)
  269. local removal_message = S(
  270. "Station '@1'" .. " " .. "has been REMOVED from the network '@2'.",
  271. oldmetadata.fields["station_name"],
  272. oldmetadata.fields["station_network"]
  273. )
  274. local owner_name = oldmetadata.fields["owner"]
  275. minetest.chat_send_player(owner_name, removal_message)
  276. local digger_name = digger and digger:get_player_name()
  277. if digger and owner_name ~= digger_name then
  278. minetest.chat_send_player(digger_name, removal_message)
  279. end
  280. end
  281. travelnet.remove_box = function(_, _, oldmetadata, digger)
  282. local success, reason = travelnet.remove_box_action(oldmetadata)
  283. if success then
  284. travelnet.remove_box_message(oldmetadata, digger)
  285. else
  286. minetest.chat_send_player(digger:get_player_name(), S("Error") .. ": " ..reason)
  287. end
  288. end
  289. -- privs of player are already checked by on_receive_fields before sending
  290. -- the edit form, but we need to check again in case somebody is cheating
  291. function travelnet.edit_box(pos, fields, meta, player_name)
  292. local description, node_name = travelnet.node_description(pos)
  293. local is_elevator = travelnet.is_elevator(node_name)
  294. local success, result = travelnet.actions.update_station({
  295. meta = meta,
  296. pos = pos,
  297. props = {
  298. owner_name = meta:get_string("owner"),
  299. station_network = meta:get_string("station_network"),
  300. station_name = meta:get_string("station_name"),
  301. description = description,
  302. is_elevator = is_elevator
  303. }
  304. }, fields, minetest.get_player_name(player_name))
  305. if not success then
  306. minetest.chat_send_player(player_name, result)
  307. end
  308. end
  309. function travelnet.edit_elevator(pos, fields, meta, player_name)
  310. local description, node_name = travelnet.node_description(pos)
  311. local is_elevator = travelnet.is_elevator(node_name)
  312. local success, result = travelnet.actions.update_elevator({
  313. meta = meta,
  314. pos = pos,
  315. props = {
  316. owner_name = meta:get_string("owner"),
  317. station_network = meta:get_string("station_network"),
  318. station_name = meta:get_string("station_name"),
  319. description = description,
  320. is_elevator = is_elevator
  321. }
  322. }, fields, minetest.get_player_name(player_name))
  323. if not success then
  324. minetest.chat_send_player(player_name, result)
  325. end
  326. end
  327. travelnet.can_dig = function()
  328. -- forbid digging of the travelnet
  329. return false
  330. end