advmarkers/init.lua

398 lines
11 KiB
Lua

--
-- Minetest advmarkers mod
--
-- © 2019 by luk3yx
--
advmarkers = {
dated_death_markers = false,
}
local DEFAULT_COLOUR = 0xbf360c
-- Get the mod storage
local storage = minetest.get_mod_storage()
local hud = {}
advmarkers.last_coords = {}
local abs, type = math.abs, type
local vector_copy = vector.copy or vector.new
local round = math.round or function(n)
return math.floor(n + 0.5)
end
-- 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
-- Create a normal table so that adding additional keys (like "colour")
-- is guaranteed to work correctly.
return {x = round(pos.x), y = round(pos.y), z = round(pos.z)}
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
-- Set the HUD position
function advmarkers.set_hud_pos(player, pos, title, colour)
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
colour = colour or DEFAULT_COLOUR
if hud[name] then
player:hud_change(hud[name], 'name', title)
player:hud_change(hud[name], 'world_pos', pos)
player:hud_change(hud[name], 'number', colour)
else
hud[name] = player:hud_add({
hud_elem_type = 'waypoint',
name = title,
text = 'm',
number = colour,
world_pos = pos
})
end
minetest.chat_send_player(name, 'Waypoint set to ' ..
minetest.colorize(("#%06x"):format(colour), title))
return true
end
function advmarkers.is_waypoint_shown(player, wp_name)
local pname = player:get_player_name()
local hud_elem = hud[pname] and player:hud_get(hud[pname])
if hud_elem and hud_elem.name == wp_name then
local pos = advmarkers.get_waypoint(player, wp_name)
if pos then
return vector.equals(hud_elem.world_pos, pos)
end
end
return false
end
function advmarkers.clear_hud(player)
local name = player:get_player_name()
if hud[name] then
player:hud_remove(hud[name])
hud[name] = nil
return true
end
end
-- Get and save player storage
local storage_cache = {}
local function get_storage(player)
local pname = player:get_player_name()
if storage_cache[pname] then
return storage_cache[pname]
end
local raw = player:get_meta():get_string('advmarkers:waypoints') or ''
local version = raw:sub(1, 1)
local res
if version == '0' then
-- Player meta: 0{"Marker name": {"x": 1, "y": 2, "z": 3}}
res = minetest.parse_json(raw:sub(2))
elseif version == '' then
-- Mod storage: return {["marker-Marker name"] = "(1,2,3)"}
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
end
-- Cache the waypoint list
res = res or {}
storage_cache[pname] = res
return res
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, '')
return true
end
-- Add a waypoint
function advmarkers.set_waypoint(player, pos, name, colour)
pos = string_to_pos(pos)
if not pos then
return false, "Invalid position!"
end
name = tostring(name)
if name == "" then
return false, "No name specified!"
end
if #name > 100 then
return false, "Waypoint name too long!"
end
local data = get_storage(player)
local count = 0
for _ in pairs(data) do count = count + 1 end
if count >= 100 then
return false, "You have too many waypoints!"
end
-- Add the colour (string_to_pos makes a copy of pos)
pos.colour = colour ~= DEFAULT_COLOUR and colour or nil
data[name] = pos
return save_storage(player, data)
end
-- Delete a waypoint
function advmarkers.delete_waypoint(player, name)
local data = get_storage(player)
data[name] = nil
return save_storage(player, data)
end
-- Get a waypoint
function advmarkers.get_waypoint(player, name)
local pos = get_storage(player)[name]
return pos and vector_copy(pos), pos and pos.colour or DEFAULT_COLOUR
end
-- Rename a waypoint
function advmarkers.rename_waypoint(player, oldname, newname, colour)
if oldname == newname and not colour then return true end
local data = get_storage(player)
if data[newname] and oldname ~= newname then return end
local wp = data[oldname]
if wp and colour then
wp.colour = colour ~= DEFAULT_COLOUR and colour or nil
end
data[newname], data[oldname] = wp, nil
return save_storage(player, data)
end
-- Get waypoint names
function advmarkers.get_waypoint_names(player, sorted)
local data = get_storage(player)
local res = {}
for name in pairs(data) do
res[#res + 1] = name
end
if sorted or sorted == nil then table.sort(res) end
return res
end
-- Display a waypoint
function advmarkers.display_waypoint(player, name)
local pos, colour = advmarkers.get_waypoint(player, name)
return advmarkers.set_hud_pos(player, pos, name, colour)
end
-- 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 not raw then
s = minetest.compress(minetest.write_json(s))
s = 'J' .. minetest.encode_base64(s)
end
return s
end
-- Import waypoints
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
local data = get_storage(player)
-- Limit the total number of waypoints
local count = 0
for _ in pairs(data) do count = count + 1 end
-- Parse the export table
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 and #field <= 107 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
count = count + 1
if count >= 100 then
break
end
end
end
end
return save_storage(player, data)
end
-- 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
-- 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
minetest.register_on_chat_message(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
storage_cache[name] = nil
advmarkers.last_coords[name] = nil
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)
-- Load other files
local modpath = minetest.get_modpath("advmarkers")
dofile(modpath .. "/chatcommands.lua")
dofile(modpath .. "/gui.lua")
-- Backwards compatibility
for _, func_name in ipairs({"set_hud_pos", "set_waypoint", "delete_waypoint",
"get_waypoint", "rename_waypoint", "get_waypoint_names", "display_waypoint",
"export", "import", "display_formspec", "get_chatcommand_pos"}) do
local func = assert(advmarkers[func_name])
local function wrapper(player, ...)
if type(player) == "string" then
minetest.log("warning", "[advmarkers] Calling advmarkers." ..
func_name .. "() with a player name is deprecated.")
player = get_player_by_name(player)
if not player then return end
end
return func(player, ...)
end
if func_name:find("waypoint", 1, true) then
local old_name = func_name:gsub('waypoint', 'marker')
advmarkers[old_name] = function(...)
minetest.log("warning", "[advmarkers] advmarkers." .. old_name ..
"() is deprecated, use advmarkers." .. func_name ..
"() instead.")
return wrapper(...)
end
end
advmarkers[func_name] = wrapper
end