-- -- Minetest advmarkers mod -- -- © 2019 by luk3yx -- advmarkers = { dated_death_markers = false } -- Get the mod storage local storage = minetest.get_mod_storage() local hud = {} local use_sscsm = false advmarkers.last_coords = {} local abs, type = math.abs, type -- Convert positions to/from strings local function pos_to_string(pos) if type(pos) == 'table' then pos = minetest.pos_to_string(vector.round(pos)) end if type(pos) == 'string' then return pos end end local function dir_ok(n) return type(n) == 'number' and abs(n) < 31000 end local function string_to_pos(pos) if type(pos) == 'string' then pos = minetest.string_to_pos(pos) end if type(pos) == 'table' and dir_ok(pos.x) and dir_ok(pos.y) and dir_ok(pos.z) then return vector.round(pos) end end local get_player_by_name = minetest.get_player_by_name local get_connected_players = minetest.get_connected_players if minetest.get_modpath('cloaking') then get_player_by_name = cloaking.get_player_by_name get_connected_players = cloaking.get_connected_players end -- Adds compatibility alias and coerces the first argument to a player object local is_player = minetest.is_player local function add_compat_function(func_name) local func = assert(advmarkers[func_name]) local function wrapper(player, ...) if not is_player(player) then player = get_player_by_name(player) if not player then return end end return func(player, ...) end advmarkers[func_name] = wrapper advmarkers[func_name:gsub('waypoint', 'marker')] = wrapper end -- Set the HUD position function advmarkers.set_hud_pos(player, pos, title) local name = player:get_player_name() pos = string_to_pos(pos) if not pos then return end if not title then title = pos.x .. ', ' .. pos.y .. ', ' .. pos.z end if hud[name] then player:hud_change(hud[name], 'name', title) player:hud_change(hud[name], 'world_pos', pos) else hud[name] = player:hud_add({ hud_elem_type = 'waypoint', name = title, text = 'm', number = 0xbf360c, world_pos = pos }) end minetest.chat_send_player(name, 'Waypoint set to ' .. minetest.colorize('#bf360c', title)) return true end add_compat_function('set_hud_pos') -- Get and save player storage local function get_storage(player) local raw = player:get_meta():get_string('advmarkers:waypoints') or '' local version = raw:sub(1, 1) if version == '0' then -- Player meta: 0{"Marker name": {"x": 1, "y": 2, "z": 3}} return minetest.parse_json(raw:sub(2)) elseif version == '' then -- Mod storage: return {["marker-Marker name"] = "(1,2,3)"} local pname = player:get_player_name() local res = {} raw = minetest.deserialize(storage:get_string(pname)) if raw then for name, pos in pairs(raw) do if name:sub(1, 7) == 'marker-' then res[name:sub(8)] = string_to_pos(pos) end end end return res end end local function save_storage(player, markers) local name = player:get_player_name() local meta = player:get_meta() if next(markers) then meta:set_string('advmarkers:waypoints', '0' .. minetest.write_json(markers)) else meta:set_string('advmarkers:waypoints', '') end storage:set_string(name, '') if use_sscsm and sscsm.has_sscsms_enabled(name) then sscsm.com_send(name, 'advmarkers:update', markers) end return true end -- Add a waypoint function advmarkers.set_waypoint(player, pos, name) local data = get_storage(player) data[tostring(name)] = string_to_pos(pos) return save_storage(player, data) end add_compat_function('set_waypoint') -- Delete a waypoint function advmarkers.delete_waypoint(player, name) local data = get_storage(player) data[name] = nil return save_storage(player, data) end add_compat_function('delete_waypoint') -- Get a waypoint function advmarkers.get_waypoint(player, name) return get_storage(player)[name] end add_compat_function('get_waypoint') -- Rename a waypoint function advmarkers.rename_waypoint(player, oldname, newname) oldname, newname = tostring(oldname), tostring(newname) local pos = advmarkers.get_waypoint(player, oldname) if not pos or not advmarkers.set_waypoint(player, pos, newname) then return end if oldname ~= newname then advmarkers.delete_waypoint(player, oldname) end return true end add_compat_function('rename_waypoint') -- Get waypoint names function advmarkers.get_waypoint_names(player, sorted) local data = get_storage(player) local res = {} for name, _ in pairs(data) do table.insert(res, name) end if sorted or sorted == nil then table.sort(res) end return res end add_compat_function('get_waypoint_names') -- Display a waypoint function advmarkers.display_waypoint(player, name) return advmarkers.set_hud_pos(player, advmarkers.get_waypoint(player, name), name) end add_compat_function('display_waypoint') -- Export waypoints function advmarkers.export(player, raw) local s = {} for name, pos in pairs(get_storage(player)) do s['marker-' .. name] = pos_to_string(pos) end if raw == 'M' then s = minetest.compress(minetest.serialize(s)) s = 'M' .. minetest.encode_base64(s) elseif not raw then s = minetest.compress(minetest.write_json(s)) s = 'J' .. minetest.encode_base64(s) end return s end add_compat_function('export') -- Import waypoints - Note that this won't import strings made by older -- versions of the CSM. function advmarkers.import(player, s) if type(s) ~= 'table' then if s:sub(1, 1) ~= 'J' then return end s = minetest.decode_base64(s:sub(2)) local success, msg = pcall(minetest.decompress, s) if not success then return end s = minetest.parse_json(msg) if type(s) ~= 'table' then return end end -- Parse the exported table local data = get_storage(player) for field, pos in pairs(s) do if type(field) == 'string' and type(pos) == 'string' and field:sub(1, 7) == 'marker-' then pos = string_to_pos(pos) if pos then -- Prevent collisions local name = field:sub(8) local c = 0 while data[name] and not vector.equals(data[name], pos) and c < 50 do name = name .. '_' c = c + 1 end -- Sanity check if c < 50 then data[name] = string_to_pos(pos) end end end end return save_storage(player, data) end add_compat_function('import') -- Get the waypoints formspec local formspec_list = {} local selected_name = {} function advmarkers.display_formspec(player) local pname = player:get_player_name() local formspec = 'size[5.25,8]' .. 'label[0,0;Waypoint list]' .. 'button_exit[0,7.5;1.3125,0.5;display;Display]' .. 'button[1.3125,7.5;1.3125,0.5;teleport;Teleport]' .. 'button[2.625,7.5;1.3125,0.5;rename;Rename]' .. 'button[3.9375,7.5;1.3125,0.5;delete;Delete]' .. 'textlist[0,0.75;5,6;marker;' -- Iterate over all the waypoints local selected = 1 formspec_list[pname] = advmarkers.get_waypoint_names(player) for id, name in ipairs(formspec_list[pname]) do if id > 1 then formspec = formspec .. ',' end if not selected_name[pname] then selected_name[pname] = name end if name == selected_name[pname] then selected = id end formspec = formspec .. '##' .. minetest.formspec_escape(name) end -- Close the text list and display the selected waypoint position formspec = formspec .. ';' .. tostring(selected) .. ']' local pos = selected_name[pname] and advmarkers.get_waypoint(player, selected_name[pname]) if pos then formspec = formspec .. 'label[0,6.75;Waypoint position: ' .. minetest.formspec_escape(tostring(pos.x) .. ', ' .. tostring(pos.y) .. ', ' .. tostring(pos.z)) .. ']' else -- Draw over the buttons formspec = formspec .. 'button_exit[0,7.5;5.25,0.5;quit;Close dialog]' .. 'label[0,6.75;No waypoints. Add one with "/add_wp".]' end -- Display the formspec return minetest.show_formspec(pname, 'advmarkers-ssm', formspec) end add_compat_function('display_formspec') -- Get waypoint position function advmarkers.get_chatcommand_pos(player, pos) local pname = player:get_player_name() -- Validate the position if pos == 'h' or pos == 'here' then pos = player:get_pos() elseif pos == 't' or pos == 'there' then if not advmarkers.last_coords[pname] then return false, 'No "there" position found!' end pos = advmarkers.last_coords[pname] else pos = string_to_pos(pos) if not pos then return false, 'Invalid position!' end end return pos end add_compat_function('get_chatcommand_pos') local function register_chatcommand_alias(old, ...) local def = assert(minetest.registered_chatcommands[old]) def.name = nil for i = 1, select('#', ...) do minetest.register_chatcommand(select(i, ...), table.copy(def)) end end -- Open the waypoints GUI minetest.register_chatcommand('mrkr', { params = '', description = 'Open the advmarkers GUI', func = function(pname, param) if param == '' then advmarkers.display_formspec(pname) else local pos, err = advmarkers.get_chatcommand_pos(pname, param) if not pos then return false, err end if not advmarkers.set_hud_pos(pname, pos) then return false, 'Error setting the waypoint!' end end end }) register_chatcommand_alias('mrkr', 'wp', 'wps', 'waypoint', 'waypoints') -- Add a waypoint minetest.register_chatcommand('add_mrkr', { params = ' ', description = 'Adds a waypoint.', func = function(pname, param) -- Get the parameters local s, e = param:find(' ') if not s or not e then return false, 'Invalid syntax! See /help add_mrkr for more info.' end local raw_pos = param:sub(1, s - 1) local name = param:sub(e + 1) -- Get the position local pos, err = advmarkers.get_chatcommand_pos(pname, raw_pos) if not pos then return false, err end -- Validate the name if not name or #name < 1 then return false, 'Invalid name!' end -- Set the waypoint return advmarkers.set_waypoint(pname, pos, name), 'Done!' end }) register_chatcommand_alias('add_mrkr', 'add_wp', 'add_waypoint') -- Set the HUD minetest.register_on_player_receive_fields(function(player, formname, fields) local pname = player:get_player_name() if formname == 'advmarkers-ignore' then return true elseif formname ~= 'advmarkers-ssm' then return end local name = false if fields.marker then local event = minetest.explode_textlist_event(fields.marker) if event.index then name = formspec_list[pname][event.index] end else name = selected_name[pname] end if name then if fields.display then if not advmarkers.display_waypoint(player, name) then minetest.chat_send_player(pname, 'Error displaying waypoint!') end elseif fields.rename then minetest.show_formspec(pname, 'advmarkers-ssm', 'size[6,3]' .. 'label[0.35,0.2;Rename waypoint]' .. 'field[0.3,1.3;6,1;new_name;New name;' .. minetest.formspec_escape(name) .. ']' .. 'button[0,2;3,1;cancel;Cancel]' .. 'button[3,2;3,1;rename_confirm;Rename]') elseif fields.rename_confirm then if fields.new_name and #fields.new_name > 0 then if advmarkers.rename_waypoint(pname, name, fields.new_name) then selected_name[pname] = fields.new_name else minetest.chat_send_player(pname, 'Error renaming waypoint!') end advmarkers.display_formspec(pname) else minetest.chat_send_player(pname, 'Please enter a new name for the waypoint.' ) end elseif fields.teleport then minetest.show_formspec(pname, 'advmarkers-ssm', 'size[6,2.2]' .. 'label[0.35,0.25;' .. minetest.formspec_escape( 'Teleport to a waypoint\n - ' .. name ) .. ']' .. 'button[0,1.25;3,1;cancel;Cancel]' .. 'button_exit[3,1.25;3,1;teleport_confirm;Teleport]') elseif fields.teleport_confirm then -- Teleport with /teleport local pos = advmarkers.get_waypoint(pname, name) if not pos then minetest.chat_send_player(pname, 'Error teleporting to waypoint!') elseif minetest.check_player_privs(pname, 'teleport') then player:set_pos(pos) minetest.chat_send_player(pname, 'Teleported to waypoint "' .. name .. '".') else minetest.chat_send_player(pname, 'Insufficient privileges!') end elseif fields.delete then minetest.show_formspec(pname, 'advmarkers-ssm', 'size[6,2]' .. 'label[0.35,0.25;Are you sure you want to delete this ' .. 'waypoint?]' .. 'button[0,1;3,1;cancel;Cancel]' .. 'button[3,1;3,1;delete_confirm;Delete]') elseif fields.delete_confirm then advmarkers.delete_waypoint(pname, name) selected_name[pname] = nil advmarkers.display_formspec(pname) elseif fields.cancel then advmarkers.display_formspec(pname) elseif name ~= selected_name[pname] then selected_name[pname] = name if not fields.quit then advmarkers.display_formspec(pname) end end elseif fields.display or fields.delete then minetest.chat_send_player(pname, 'Please select a waypoint.') end return true end) -- Auto-add waypoints on death. minetest.register_on_dieplayer(function(player) local name if advmarkers.dated_death_markers then name = os.date('Death on %Y-%m-%d %H:%M:%S') else name = 'Death waypoint' end local pos = player:get_pos() advmarkers.last_coords[player] = pos advmarkers.set_waypoint(player, pos, name) minetest.chat_send_player(player:get_player_name(), 'Added waypoint "' .. name .. '".') end) -- Allow string exporting minetest.register_chatcommand('mrkr_export', { params = '', description = 'Exports an advmarkers string containing all your waypoints.', func = function(name, param) local export if param == 'old' then export = advmarkers.export(name, 'M') else export = advmarkers.export(name) end minetest.show_formspec(name, 'advmarkers-ignore', 'field[_;Your waypoint export string;' .. minetest.formspec_escape(export) .. ']') end }) register_chatcommand_alias('mrkr_export', 'wp_export', 'waypoint_export') -- String importing minetest.register_chatcommand('mrkr_import', { params = '', description = 'Imports an advmarkers string. This will not overwrite ' .. 'existing waypoints that have the same name.', func = function(name, param) if advmarkers.import(name, param) then return true, 'Waypoints imported!' else return false, 'Invalid advmarkers string!' end end }) register_chatcommand_alias('mrkr_import', 'wp_import', 'waypoint_import') -- Find co-ordinates sent in chat messages local function get_coords(msg) if msg:byte(1) == 1 or #msg > 1000 then return end local pos = msg:match('%-?[0-9%.]+, *%-?[0-9%.]+, *%-?[0-9%.]+') if pos then return string_to_pos(pos) end end -- Get global co-ords table.insert(minetest.registered_on_chat_messages, 1, function(_, msg) if msg:sub(1, 1) == '/' then return end local pos = get_coords(msg) if pos then advmarkers.last_coords = {} for _, player in ipairs(get_connected_players()) do advmarkers.last_coords[player:get_player_name()] = pos end end end) -- Override chat_send_player to get PMed co-ords etc local old_chat_send_player = minetest.chat_send_player function minetest.chat_send_player(name, msg, ...) if type(name) == 'string' and type(msg) == 'string' and get_player_by_name(name) then local pos = get_coords(msg) if pos then advmarkers.last_coords[name] = pos end end return old_chat_send_player(name, msg, ...) end -- Clean up variables if a player leaves minetest.register_on_leaveplayer(function(player) local name = player:get_player_name() hud[name] = nil formspec_list[name] = nil selected_name[name] = nil advmarkers.last_coords[name] = nil end) -- Add '/mrkrthere' minetest.register_chatcommand('mrkrthere', { params = '', description = 'Alias for "/mrkr there".', func = function(name, _) return minetest.registered_chatcommands['mrkr'].func(name, 'there') end }) minetest.register_chatcommand('clrmrkr', { params = '', description = 'Hides the displayed waypoint.', func = function(name, _) local player = minetest.get_player_by_name(name) if not hud[name] or not player then return false, 'No waypoint is currently being displayed!' end player:hud_remove(hud[name]) hud[name] = nil return true, 'Hidden the currently displayed waypoint.' end, }) register_chatcommand_alias('clrmrkr', 'clear_marker', 'clrwp', 'clear_waypoint') -- SSCSM support if not minetest.global_exists('sscsm') or not sscsm.register then return end if not sscsm.register_on_com_receive then minetest.log('warning', '[advmarkers] The SSCSM mod is outdated!') return end use_sscsm = true sscsm.register({ name = 'advmarkers', file = minetest.get_modpath('advmarkers') .. '/sscsm.lua', }) -- SSCSM communication sscsm.register_on_com_receive('advmarkers:delete', function(name, param) if type(param) == 'string' then advmarkers.delete_waypoint(name, param) end end) sscsm.register_on_com_receive('advmarkers:set', function(name, param) if type(param) == 'table' and type(param.name) == 'string' and type(param.pos) == 'string' then local pos = string_to_pos(param.pos) if pos then advmarkers.set_waypoint(name, pos, param.name) end end end) sscsm.register_on_com_receive('advmarkers:display', function(name, param) if type(param) ~= 'string' or not advmarkers.display_waypoint(name, param) then minetest.chat_send_player(name, 'Error displaying waypoint!') end end) -- Send waypoint list once SSCSMs are loaded. sscsm.register_on_sscsms_loaded(function(name) local player = minetest.get_player_by_name(name) sscsm.com_send(name, 'advmarkers:update', get_storage(player)) end)