-- -- Minetest player cloaking mod: Core functions -- -- Copyright © 2018-2021 by luk3yx -- cloaking = {} -- Expose the real get_connected_players and get_player_by_name for mods that -- can use them. cloaking.get_player_by_name = minetest.get_player_by_name cloaking.get_objects_inside_radius = minetest.get_objects_inside_radius cloaking.get_objects_in_area = minetest.get_objects_in_area cloaking.get_server_status = minetest.get_server_status local cloaked_players = {} local chatcommands_modified = false -- Override built-in functions function minetest.get_player_by_name(player) if cloaked_players[player] then return nil else return cloaking.get_player_by_name(player) end end local function remove_cloaked_players(list) for i = #list, 1, -1 do if cloaked_players[list[i]:get_player_name()] then table.remove(list, i) end end end function minetest.get_objects_inside_radius(pos, radius) local objs = cloaking.get_objects_inside_radius(pos, radius) remove_cloaked_players(objs) return objs end if cloaking.get_objects_in_area then function minetest.get_objects_in_area(pos1, pos2) local objs = cloaking.get_objects_in_area(pos1, pos2) remove_cloaked_players(objs) return objs end end local override_statusline = false if minetest.settings:get_bool('show_statusline_on_connect') ~= nil then override_statusline = true end function minetest.get_server_status(name, joined) if override_statusline and joined == true then override_statusline = false end local status = cloaking.get_server_status(name, joined) if not status or status == "" then return status end local e = status:find('}', 1, true) if not e then return status end local motd = status:sub(e) status = status:sub(1, status:find('{', 1, true)) local players = {} for _, player in ipairs(minetest.get_connected_players()) do table.insert(players, player:get_player_name()) end players = table.concat(players, ', ') status = status .. players .. motd return status end -- Change the on-join status if override_statusline then minetest.settings:set_bool('show_statusline_on_connect', false) minetest.register_on_joinplayer(function(player) if not override_statusline then return end local name = player:get_player_name() local status = minetest.get_server_status(name, 'cloaking') if status and status ~= '' then minetest.chat_send_player(name, status) end end) end -- Don't allow chat or chatcommands in all commands that don't have the -- allow_while_cloaked parameter set. local delayed_uncloak local function override_chatcommands() local chatcommands = minetest.registered_chatcommands for cmd_name, def in pairs(chatcommands) do if not def.allow_while_cloaked and not def._allow_while_cloaked then local real_cmd = def.func chatcommands[cmd_name].func = function(name, param) if cloaked_players[name] then local pass, r1, r2 if def._disallow_while_cloaked then pass = false else pass, r1, r2 = pcall(real_cmd, name, param) end if pass then return r1, r2 else return false, "You may not execute this chatcommand " .. "while cloaked. Please use /uncloak if you want " .. "to execute this chatcommand." end else return real_cmd(name, param) end end end end local c = 0 for _, func in ipairs(minetest.registered_on_leaveplayers) do c = c + 1 local f = func if f ~= delayed_uncloak then minetest.registered_on_leaveplayers[c] = function(p, t, cloaked) if cloaked ~= 'cloaking' and cloaked_players[p:get_player_name()] then return end return f(p, t) end end end end -- Handle chat messages function cloaking.on_chat_message(name, message) if message:sub(1, 1) ~= "/" and cloaked_players[name] then minetest.chat_send_player(name, "You cannot use chat while cloaked." .. " Please use /uncloak if you want to use chat.") return true end end table.insert(minetest.registered_on_chat_messages, 1, cloaking.on_chat_message) minetest.callback_origins[cloaking.on_chat_message] = { mod = 'cloaking', name = 'on_chat_message' } -- Disallow some built-in commands. for _, cmd in ipairs({'me', 'msg'}) do if minetest.chatcommands[cmd] then minetest.override_chatcommand(cmd, { _disallow_while_cloaked = true }) end end -- Get player and name local function player_and_name(player, n) if type(player) == "string" then player = cloaking.get_player_by_name(player) end local victim = player:get_player_name() if n then if cloaked_players[victim] then return false end elseif n ~= nil then if not cloaked_players[victim] then return false end end return player, victim end -- 0.4.X compatibility local selectionbox = 'selectionbox' if not minetest.features.object_independent_selectionbox then selectionbox = 'collisionbox' end -- "Hide" players local hidden = {} function cloaking.hide_player(player_or_name, preserve_attrs) -- Sanity check local player, victim = player_and_name(player_or_name, true) if not player then return end -- Save existing attributes if preserve_attrs or preserve_attrs == nil then if hidden[victim] then return end hidden[victim] = { player:get_properties(), player:get_nametag_attributes() } else hidden[victim] = nil end -- Hide the player player:set_properties({ visual_size = {x = 0, y = 0, z = 0}, [selectionbox] = {0,0,0,0,0,0}, makes_footstep_sound = false, show_on_minimap = false, }) player:set_nametag_attributes({ text = ' ', color = {r = 0, g = 0, b = 0, a = 0} }) end -- Remove original attributes when players leave minetest.register_on_leaveplayer(function(player) hidden[player:get_player_name()] = nil end) -- "Unhide" players function cloaking.unhide_player(player_or_name) -- Sanity check local player, victim = player_and_name(player_or_name, true) if not player or hidden[victim] == nil then return end -- Get the data local data = hidden[victim] or {} hidden[victim] = nil -- Use sensible defaults if the data does not exist. if not data[1] then local box = false if minetest.features.object_independent_selectionbox then box = player:get_properties().collisionbox end box = box or {-0.3,-1,-0.3,0.3,0.75,0.3} data = {{ visual_size = {x = 1, y = 2, z = 1}, [selectionbox] = box, makes_footstep_sound = true, show_on_minimap = true, }} end -- Make the player visible player:set_properties(data[1]) player:set_nametag_attributes(data[2] or { text = victim, color = {r = 255, g = 255, b = 255, a = 255} }) end -- The cloak and uncloak functions local use_areas = minetest.global_exists('areas') and areas.hud function cloaking.cloak(player_or_name) if not chatcommands_modified then override_chatcommands() end local player, victim = player_and_name(player_or_name, true) if not player then return end cloaking.hide_player(player, false) local t = nil if use_areas and areas.hud[victim] then t = areas.hud[victim] end for _, f in ipairs(minetest.registered_on_leaveplayers) do if f ~= delayed_uncloak then f(player, false, 'cloaking') end end cloaked_players[victim] = true -- TODO: Get the highest ID somehow local t_id = t and t.areasId for id = 0, 100 do if id ~= t_id and player:hud_get(id) then player:hud_remove(id) end end if t then areas.hud[victim] = t player:hud_change(areas.hud[victim].areasId, "text", "Cloaked") areas.hud[victim].oldAreas = "" end minetest.log('verbose', victim .. ' was cloaked.') end function cloaking.uncloak(player_or_name) local player, victim = player_and_name(player_or_name, false) if not player then return end cloaked_players[victim] = nil hidden[victim] = false cloaking.unhide_player(player) -- In singleplayer, there is no joined the game message by default. if victim == "singleplayer" then minetest.chat_send_all("*** " .. victim .. " joined the game.") end for _, f in ipairs(minetest.registered_on_joinplayers) do f(player) end minetest.log('verbose', victim .. ' was uncloaked.') end -- API functions cloaking.auto_uncloak = cloaking.uncloak -- This function removes the player from the cloaked players table on the next -- server step. -- Defined as a local above override_chatcommands(). function delayed_uncloak(player) local victim = player:get_player_name() if cloaked_players[victim] then minetest.after(0, function() cloaked_players[victim] = nil if use_areas and areas.hud[victim] then areas.hud[victim] = nil end end) end end -- Register cloaking.delayed_uncloak "manually" so that the profiler can't -- hijack it, preventing it from running. table.insert(minetest.registered_on_leaveplayers, delayed_uncloak) minetest.callback_origins[delayed_uncloak] = { mod = 'cloaking', name = 'delayed_uncloak' } -- Override minetest.get_connected_players(), required for Minetest 5.2.0+. local get_connected_players = minetest.get_connected_players function minetest.get_connected_players() local res = get_connected_players() remove_cloaked_players(res) return res end -- There's currently no way to check if cloaked players will appear in the -- unmodified minetest.get_connected_players(). function cloaking.get_connected_players() local players = minetest.get_connected_players() for name, cloaked in pairs(cloaked_players) do if cloaked then local player = cloaking.get_player_by_name(name) -- The player may be nil if they have just left but haven't been -- removed from the cloaked_players table yet. if player then players[#players + 1] = player end end end return players end function cloaking.get_cloaked_players() local players = {} for player, cloaked in pairs(cloaked_players) do if cloaked then table.insert(players, player) end end return players end -- Allow mods to get a list of cloaked and uncloaked player names. function cloaking.get_connected_names() local a = cloaking.get_cloaked_players() for _, player in ipairs(minetest.get_connected_players()) do table.insert(a, player:get_player_name()) end return a end function cloaking.is_cloaked(player) if type(player) ~= "string" then player = player:get_player_name() end return cloaked_players[player] and true or false end -- Prevent cloaked players dying minetest.register_on_player_hpchange(function(player, hp_change) if player and hp_change < 0 then local name = player:get_player_name() if cloaked_players[name] then hp_change = 0 end end return hp_change end, true)