An advanced-ish waypoints mod for Minetest.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

init.lua 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. --
  2. -- Minetest advmarkers mod
  3. --
  4. -- The advmarkers CSM ported to a server-side mod
  5. --
  6. advmarkers = {
  7. dated_death_markers = true
  8. }
  9. -- Get the mod storage
  10. local storage = minetest.get_mod_storage()
  11. local hud = {}
  12. advmarkers.last_coords = {}
  13. -- Convert positions to/from strings
  14. local function pos_to_string(pos)
  15. if type(pos) == 'table' then
  16. pos = minetest.pos_to_string(vector.round(pos))
  17. end
  18. if type(pos) == 'string' then
  19. return pos
  20. end
  21. end
  22. local function string_to_pos(pos)
  23. if type(pos) == 'string' then
  24. pos = minetest.string_to_pos(pos)
  25. end
  26. if type(pos) == 'table' then
  27. return vector.round(pos)
  28. end
  29. end
  30. -- Get player name or object
  31. local get_player_by_name = minetest.get_player_by_name
  32. local get_connected_players = minetest.get_connected_players
  33. if minetest.get_modpath('cloaking') then
  34. get_player_by_name = cloaking.get_player_by_name
  35. get_connected_players = cloaking.get_connected_players
  36. end
  37. local function get_player(player, t)
  38. local name
  39. if type(player) == 'string' then
  40. name = player
  41. if t ~= 0 then
  42. player = get_player_by_name(name)
  43. end
  44. else
  45. name = player:get_player_name()
  46. end
  47. if t == 0 then
  48. return name
  49. elseif t == 1 then
  50. return player
  51. end
  52. return name, player
  53. end
  54. -- Set the HUD position
  55. function advmarkers.set_hud_pos(player, pos, title)
  56. local name, player = get_player(player)
  57. pos = string_to_pos(pos)
  58. if not player or not pos then return end
  59. if not title then
  60. title = pos.x .. ', ' .. pos.y .. ', ' .. pos.z
  61. end
  62. if hud[player] then
  63. player:hud_change(hud[player], 'name', title)
  64. player:hud_change(hud[player], 'world_pos', pos)
  65. else
  66. hud[player] = player:hud_add({
  67. hud_elem_type = 'waypoint',
  68. name = title,
  69. text = 'm',
  70. number = 0xbf360c,
  71. world_pos = pos
  72. })
  73. end
  74. minetest.chat_send_player(name, 'Waypoint set to ' .. title)
  75. return true
  76. end
  77. -- Get and save player storage
  78. local function get_storage(name)
  79. name = get_player(name, 0)
  80. return minetest.deserialize(storage:get_string(name)) or {}
  81. end
  82. local function save_storage(name, data)
  83. name = get_player(name, 0)
  84. if type(data) == 'table' then
  85. data = minetest.serialize(data)
  86. end
  87. if type(data) ~= 'string' then return end
  88. if #data > 0 then
  89. storage:set_string(name, data)
  90. else
  91. storage:set_string(name, '')
  92. end
  93. return true
  94. end
  95. -- Add a waypoint
  96. function advmarkers.set_waypoint(player, pos, name)
  97. pos = pos_to_string(pos)
  98. if not pos then return end
  99. local data = get_storage(player)
  100. data['marker-' .. tostring(name)] = pos
  101. return save_storage(player, data)
  102. end
  103. advmarkers.set_marker = advmarkers.set_waypoint
  104. -- Delete a waypoint
  105. function advmarkers.delete_waypoint(player, name)
  106. local data = get_storage(player)
  107. data['marker-' .. tostring(name)] = nil
  108. return save_storage(player, data)
  109. end
  110. advmarkers.delete_marker = advmarkers.delete_waypoint
  111. -- Get a waypoint
  112. function advmarkers.get_waypoint(player, name)
  113. local data = get_storage(player)
  114. return string_to_pos(data['marker-' .. tostring(name)])
  115. end
  116. advmarkers.get_marker = advmarkers.get_waypoint
  117. -- Rename a waypoint and re-interpret the position.
  118. function advmarkers.rename_waypoint(player, oldname, newname)
  119. player = get_player(player, 0)
  120. oldname, newname = tostring(oldname), tostring(newname)
  121. local pos = advmarkers.get_waypoint(player, oldname)
  122. if not pos or not advmarkers.set_waypoint(player, pos, newname) then
  123. return
  124. end
  125. if oldname ~= newname then
  126. advmarkers.delete_waypoint(player, oldname)
  127. end
  128. return true
  129. end
  130. advmarkers.rename_marker = advmarkers.rename_waypoint
  131. -- Get waypoint names
  132. function advmarkers.get_waypoint_names(name, sorted)
  133. local data = get_storage(name)
  134. local res = {}
  135. for name, pos in pairs(data) do
  136. if name:sub(1, 7) == 'marker-' then
  137. table.insert(res, name:sub(8))
  138. end
  139. end
  140. if sorted or sorted == nil then table.sort(res) end
  141. return res
  142. end
  143. advmarkers.get_marker_names = advmarkers.get_waypoint_names
  144. -- Display a waypoint
  145. function advmarkers.display_waypoint(player, name)
  146. return advmarkers.set_hud_pos(player, advmarkers.get_waypoint(player, name),
  147. name)
  148. end
  149. advmarkers.display_marker = advmarkers.display_waypoint
  150. -- Export waypoints
  151. function advmarkers.export(player, raw)
  152. local s = get_storage(player)
  153. if raw == 'M' then
  154. s = minetest.compress(minetest.serialize(s))
  155. s = 'M' .. minetest.encode_base64(s)
  156. elseif not raw then
  157. s = minetest.compress(minetest.write_json(s))
  158. s = 'J' .. minetest.encode_base64(s)
  159. end
  160. return s
  161. end
  162. -- Import waypoints - Note that this won't import strings made by older
  163. -- versions of the CSM.
  164. function advmarkers.import(player, s)
  165. if type(s) ~= 'table' then
  166. if s:sub(1, 1) ~= 'J' then return end
  167. s = minetest.decode_base64(s:sub(2))
  168. local success, msg = pcall(minetest.decompress, s)
  169. if not success then return end
  170. s = minetest.parse_json(msg)
  171. end
  172. -- Iterate over waypoints to preserve existing ones and check for errors.
  173. if type(s) == 'table' then
  174. local data = get_storage(player)
  175. for name, pos in pairs(s) do
  176. if type(name) == 'string' and type(pos) == 'string' and
  177. name:sub(1, 7) == 'marker-' and minetest.string_to_pos(pos) and
  178. data[name] ~= pos then
  179. -- Prevent collisions
  180. local c = 0
  181. while data[name] and c < 50 do
  182. name = name .. '_'
  183. c = c + 1
  184. end
  185. -- Sanity check
  186. if c < 50 then
  187. data[name] = pos
  188. end
  189. end
  190. end
  191. return save_storage(player, data)
  192. end
  193. end
  194. -- Get the waypoints formspec
  195. local formspec_list = {}
  196. local selected_name = {}
  197. function advmarkers.display_formspec(player)
  198. player = get_player(player, 0)
  199. if not get_player_by_name(player) then return end
  200. local formspec = 'size[5.25,8]' ..
  201. 'label[0,0;Waypoint list]' ..
  202. 'button_exit[0,7.5;1.3125,0.5;display;Display]' ..
  203. 'button[1.3125,7.5;1.3125,0.5;teleport;Teleport]' ..
  204. 'button[2.625,7.5;1.3125,0.5;rename;Rename]' ..
  205. 'button[3.9375,7.5;1.3125,0.5;delete;Delete]' ..
  206. 'textlist[0,0.75;5,6;marker;'
  207. -- Iterate over all the waypoints
  208. local selected = 1
  209. formspec_list[player] = advmarkers.get_waypoint_names(player)
  210. for id, name in ipairs(formspec_list[player]) do
  211. if id > 1 then formspec = formspec .. ',' end
  212. if not selected_name[player] then selected_name[player] = name end
  213. if name == selected_name[player] then selected = id end
  214. formspec = formspec .. '##' .. minetest.formspec_escape(name)
  215. end
  216. -- Close the text list and display the selected waypoint position
  217. formspec = formspec .. ';' .. tostring(selected) .. ']'
  218. if selected_name[player] then
  219. local pos = advmarkers.get_waypoint(player, selected_name[player])
  220. if pos then
  221. pos = minetest.formspec_escape(tostring(pos.x) .. ', ' ..
  222. tostring(pos.y) .. ', ' .. tostring(pos.z))
  223. pos = 'Waypoint position: ' .. pos
  224. formspec = formspec .. 'label[0,6.75;' .. pos .. ']'
  225. end
  226. else
  227. -- Draw over the buttons
  228. formspec = formspec .. 'button_exit[0,7.5;5.25,0.5;quit;Close dialog]' ..
  229. 'label[0,6.75;No waypoints. Add one with "/add_wp".]'
  230. end
  231. -- Display the formspec
  232. return minetest.show_formspec(player, 'advmarkers-ssm', formspec)
  233. end
  234. -- Get waypoint position
  235. function advmarkers.get_chatcommand_pos(player, pos)
  236. local pname = get_player(player, 0)
  237. -- Validate the position
  238. if pos == 'h' or pos == 'here' then
  239. pos = get_player(player, 1):get_pos()
  240. elseif pos == 't' or pos == 'there' then
  241. if not advmarkers.last_coords[pname] then
  242. return false, 'No-one has used ".coords" and you have not died!'
  243. end
  244. pos = advmarkers.last_coords[pname]
  245. else
  246. pos = string_to_pos(pos)
  247. if not pos then
  248. return false, 'Invalid position!'
  249. end
  250. end
  251. return pos
  252. end
  253. local function register_chatcommand_alias(old, ...)
  254. local def = assert(minetest.registered_chatcommands[old])
  255. def.name = nil
  256. for i = 1, select('#', ...) do
  257. minetest.register_chatcommand(select(i, ...), table.copy(def))
  258. end
  259. end
  260. -- Open the waypoints GUI
  261. local csm_key = string.char(1) .. 'ADVMARKERS_SSCSM' .. string.char(1)
  262. minetest.register_chatcommand('mrkr', {
  263. params = '',
  264. description = 'Open the advmarkers GUI',
  265. func = function(pname, param)
  266. if param:sub(1, #csm_key) == csm_key then
  267. -- SSCSM communication
  268. param = param:sub(#csm_key + 1)
  269. local cmd = param:sub(1, 1)
  270. if cmd == 'D' then
  271. -- D: Delete
  272. advmarkers.delete_waypoint(pname, param:sub(2))
  273. elseif cmd == 'S' then
  274. -- S: Set
  275. local s, e = param:find(' ')
  276. if s and e then
  277. local pos = string_to_pos(param:sub(2, s - 1))
  278. if pos then
  279. advmarkers.set_waypoint(pname, pos, param:sub(e + 1))
  280. end
  281. end
  282. elseif cmd == '0' then
  283. -- 0: Display
  284. if not advmarkers.display_waypoint(pname, param:sub(2)) then
  285. minetest.chat_send_player(pname,
  286. 'Error displaying waypoint!')
  287. end
  288. end
  289. minetest.chat_send_player(pname, csm_key
  290. .. advmarkers.export(pname))
  291. elseif param == '' then
  292. advmarkers.display_formspec(pname)
  293. else
  294. local pos, err = advmarkers.get_chatcommand_pos(pname, param)
  295. if not pos then
  296. return false, err
  297. end
  298. if not advmarkers.set_hud_pos(pname, pos) then
  299. return false, 'Error setting the waypoint!'
  300. end
  301. end
  302. end
  303. })
  304. register_chatcommand_alias('mrkr', 'wp', 'wps', 'waypoint', 'waypoints')
  305. -- Add a waypoint
  306. minetest.register_chatcommand('add_mrkr', {
  307. params = '<pos / "here" / "there"> <name>',
  308. description = 'Adds a waypoint.',
  309. func = function(pname, param)
  310. -- Get the parameters
  311. local s, e = param:find(' ')
  312. if not s or not e then
  313. return false, 'Invalid syntax! See /help add_mrkr for more info.'
  314. end
  315. local pos = param:sub(1, s - 1)
  316. local name = param:sub(e + 1)
  317. -- Get the position
  318. local pos, err = advmarkers.get_chatcommand_pos(pname, pos)
  319. if not pos then
  320. return false, err
  321. end
  322. -- Validate the name
  323. if not name or #name < 1 then
  324. return false, 'Invalid name!'
  325. end
  326. -- Set the waypoint
  327. return advmarkers.set_waypoint(pname, pos, name), 'Done!'
  328. end
  329. })
  330. register_chatcommand_alias('add_mrkr', 'add_wp', 'add_waypoint')
  331. -- Set the HUD
  332. minetest.register_on_player_receive_fields(function(player, formname, fields)
  333. local pname, player = get_player(player)
  334. if formname == 'advmarkers-ignore' then
  335. return true
  336. elseif formname ~= 'advmarkers-ssm' then
  337. return
  338. end
  339. local name = false
  340. if fields.marker then
  341. local event = minetest.explode_textlist_event(fields.marker)
  342. if event.index then
  343. name = formspec_list[pname][event.index]
  344. end
  345. else
  346. name = selected_name[pname]
  347. end
  348. if name then
  349. if fields.display then
  350. if not advmarkers.display_waypoint(player, name) then
  351. minetest.chat_send_player(pname, 'Error displaying waypoint!')
  352. end
  353. elseif fields.rename then
  354. minetest.show_formspec(pname, 'advmarkers-ssm', 'size[6,3]' ..
  355. 'label[0.35,0.2;Rename waypoint]' ..
  356. 'field[0.3,1.3;6,1;new_name;New name;' ..
  357. minetest.formspec_escape(name) .. ']' ..
  358. 'button[0,2;3,1;cancel;Cancel]' ..
  359. 'button[3,2;3,1;rename_confirm;Rename]')
  360. elseif fields.rename_confirm then
  361. if fields.new_name and #fields.new_name > 0 then
  362. if advmarkers.rename_waypoint(pname, name, fields.new_name) then
  363. selected_name[pname] = fields.new_name
  364. else
  365. minetest.chat_send_player(pname, 'Error renaming waypoint!')
  366. end
  367. advmarkers.display_formspec(pname)
  368. else
  369. minetest.chat_send_player(pname,
  370. 'Please enter a new name for the waypoint.'
  371. )
  372. end
  373. elseif fields.teleport then
  374. minetest.show_formspec(pname, 'advmarkers-ssm', 'size[6,2.2]' ..
  375. 'label[0.35,0.25;' .. minetest.formspec_escape(
  376. 'Teleport to a waypoint\n - ' .. name
  377. ) .. ']' ..
  378. 'button[0,1.25;3,1;cancel;Cancel]' ..
  379. 'button_exit[3,1.25;3,1;teleport_confirm;Teleport]')
  380. elseif fields.teleport_confirm then
  381. -- Teleport with /teleport
  382. local pos = advmarkers.get_waypoint(pname, name)
  383. if not pos then
  384. minetest.chat_send_player(pname, 'Error teleporting to waypoint!')
  385. elseif minetest.check_player_privs(pname, 'teleport') then
  386. player:set_pos(pos)
  387. minetest.chat_send_player(pname, 'Teleported to waypoint "' ..
  388. name .. '".')
  389. else
  390. minetest.chat_send_player(pname, 'Insufficient privileges!')
  391. end
  392. elseif fields.delete then
  393. minetest.show_formspec(pname, 'advmarkers-ssm', 'size[6,2]' ..
  394. 'label[0.35,0.25;Are you sure you want to delete this waypoint?]' ..
  395. 'button[0,1;3,1;cancel;Cancel]' ..
  396. 'button[3,1;3,1;delete_confirm;Delete]')
  397. elseif fields.delete_confirm then
  398. advmarkers.delete_waypoint(pname, name)
  399. selected_name[pname] = nil
  400. advmarkers.display_formspec(pname)
  401. elseif fields.cancel then
  402. advmarkers.display_formspec(pname)
  403. elseif name ~= selected_name[pname] then
  404. selected_name[pname] = name
  405. if not fields.quit then
  406. advmarkers.display_formspec(pname)
  407. end
  408. end
  409. elseif fields.display or fields.delete then
  410. minetest.chat_send_player(pname, 'Please select a waypoint.')
  411. end
  412. return true
  413. end)
  414. -- Auto-add waypoints on death.
  415. minetest.register_on_dieplayer(function(player)
  416. local name
  417. if advmarkers.dated_death_markers then
  418. name = os.date('Death on %Y-%m-%d %H:%M:%S')
  419. else
  420. name = 'Death waypoint'
  421. end
  422. local pos = player:get_pos()
  423. advmarkers.last_coords[player] = pos
  424. advmarkers.set_waypoint(player, pos, name)
  425. minetest.chat_send_player(player:get_player_name(),
  426. 'Added waypoint "' .. name .. '".')
  427. end)
  428. -- Allow string exporting
  429. minetest.register_chatcommand('mrkr_export', {
  430. params = '',
  431. description = 'Exports an advmarkers string containing all your waypoints.',
  432. func = function(name, param)
  433. local export
  434. if param == 'old' then
  435. export = advmarkers.export(name, 'M')
  436. else
  437. export = advmarkers.export(name)
  438. end
  439. minetest.show_formspec(name, 'advmarkers-ignore',
  440. 'field[_;Your waypoint export string;' ..
  441. minetest.formspec_escape(export) .. ']')
  442. end
  443. })
  444. register_chatcommand_alias('mrkr_export', 'wp_export', 'waypoint_export')
  445. -- String importing
  446. minetest.register_chatcommand('mrkr_import', {
  447. params = '<advmarkers string>',
  448. description = 'Imports an advmarkers string. This will not overwrite ' ..
  449. 'existing waypoints that have the same name.',
  450. func = function(name, param)
  451. if advmarkers.import(name, param) then
  452. return true, 'Waypoints imported!'
  453. else
  454. return false, 'Invalid advmarkers string!'
  455. end
  456. end
  457. })
  458. register_chatcommand_alias('mrkr_export', 'wp_import', 'waypoint_import')
  459. -- Chat channels .coords integration.
  460. -- You do not need to have chat channels installed for this to work.
  461. local function get_coords(msg, strict)
  462. local s = 'Current Position: %-?[0-9]+, %-?[0-9]+, %-?[0-9]+%.'
  463. if strict then
  464. s = '^' .. s
  465. end
  466. local s, e = msg:find(s)
  467. local pos = false
  468. if s and e then
  469. pos = string_to_pos(msg:sub(s + 18, e - 1))
  470. end
  471. return pos
  472. end
  473. -- Get global co-ords
  474. table.insert(minetest.registered_on_chat_messages, 1, function(name, msg)
  475. if msg:sub(1, 1) == '/' then return end
  476. local pos = get_coords(msg, true)
  477. if pos then
  478. advmarkers.last_coords = {}
  479. for _, player in ipairs(get_connected_players()) do
  480. advmarkers.last_coords[player:get_player_name()] = pos
  481. end
  482. end
  483. end)
  484. -- Override chat_send_player to get PMed co-ords etc
  485. local old_chat_send_player = minetest.chat_send_player
  486. function minetest.chat_send_player(name, msg, ...)
  487. if type(name) == 'string' and type(msg) == 'string' and
  488. get_player_by_name(name) then
  489. local pos = get_coords(msg)
  490. if pos then
  491. advmarkers.last_coords[name] = pos
  492. end
  493. end
  494. return old_chat_send_player(name, msg, ...)
  495. end
  496. -- Clean up variables if a player leaves
  497. minetest.register_on_leaveplayer(function(player)
  498. local name = get_player(player, 0)
  499. hud[name] = nil
  500. formspec_list[name] = nil
  501. selected_name[name] = nil
  502. advmarkers.last_coords[name] = nil
  503. end)
  504. -- Add '/mrkrthere'
  505. minetest.register_chatcommand('mrkrthere', {
  506. params = '',
  507. description = 'Alias for "/mrkr there".',
  508. func = function(name, param)
  509. return minetest.registered_chatcommands['mrkr'].func(name, 'there')
  510. end
  511. })
  512. -- SSCSM support
  513. if minetest.global_exists('sscsm') and sscsm.register then
  514. sscsm.register({
  515. name = 'advmarkers',
  516. file = minetest.get_modpath('advmarkers') .. '/sscsm.lua',
  517. })
  518. end