commit 8067c2c8197773d625a78f53153e441d258609d5 Author: Cédric Ronvel Date: Mon Nov 25 15:36:18 2019 +0100 publish diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d77379 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 cronvel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..962bbdb --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ + + +## Respawn + +-- Manage respawn points, interesting places, teleportation and death records + + +### Features + +* Create/manage as many respawn points you want, to be used randomly (e.g. on player death or first connection) +* Create/manage global map place +* Each player can have its own personal places (stored and used separately from the global places) +* Each place and respawn retains not only the position, **but also a direction where to look**, + ideal for creating great spot on the map where you already look at the right thing once teleported +* Place have a short name (i.e. an ID, without space) and a full name (that can have space) +* Check close to which place you are +* Powerful teleport command + * teleport self or teleport other (with different privileges) + * teleport to a respawn point, a global place, an own place, a coordinate, your last death place, or next to a player +* Every death are logged, with the reason and the place it happened (if closed to a known global place) +* List any type of places +* List any player death +* Have a setting allowing player to respawn to their personal own place named "home" instead of using a regular respawn point + (disabled by default) + + + +### Commands overview + +For the complete syntax, see the in-game help. + +* /list_respawns: List all respawn points. +* /reset_respawns: Reset respawn points. Require the "server" privilege. +* /set_respawn: Create a respawn point on your current player position. Require the "server" privilege. +* /list_places: List all (global) places. +* /reset_places: Reset (global) places, i.e. remove all places at once. Require the "server" and "place" privileges. +* /set_place: Create a (global) place on your current player position, also accept a full name for tasteful place names. + Require the "place" privilege. +* /remove_place: Remove one of the (global) place. Require the "place" privilege. +* /list_own_places: List all your personal own places. +* /reset_own_places: Reset all your personal own places, i.e. remove them all at once. +* /set_own_place: Create a personal own place on your current player position, also accept a full name for tasteful place names. +* /remove_own_place: Remove one of your personal own place. +* /reset_all_player_places: Remove all personal places of all players at once. Require the "server" and "place" privileges. +* /where: Tell you where you are, i.e. close to which place you are, if there is anyone close to you. + Search on both your own places list and the global places list. Require no privileges, but with the "locate" privilege + you can also see your coordinate and you can also locate any player. +* /teleport: teleport yourself to a respawn point, a global place, a personal own place, your last death place, a coordinate, + or a player. Require the "teleport" privilege. +* /teleport_other: teleport anyone to a respawn point, a global place, one of your personal own place (not their), + their last death place, a coordinate, close to you or any player. Require the "teleport" and "teleport_other" privileges. +* /list_deaths: list your or any player deaths with the cause and the place it occurs. + + + +### Privileges + +* teleport: Can use /teleport to self teleport to a registered respawn point, global place, own place, last death place. + Can teleport to xyz coordinates or close to another player in conjunction with the "locate" privilege. +* teleport_other: Can use /teleport_other to teleport any player to a registered respawn point, global place, own place (yours), + last death place (their), or close to self. + Can teleport to xyz coordinates or close to another player in conjunction with the "locate" privilege. +* place: Can use /set_place, /remove_place, /reset_places and /reset_all_player_places to manage global places. +* locate: Can use advanced /where command to locate other player and output coordinate, extend /teleport and /teleport_other + to support xyz coordinates and teleporting close to another player. + + + +### Settings + +* enable_home_respawn: if enabled (default: disabled), the player can respawn at their home instead of a regular respawn point. + Note: should not be confused with the *sethome* mod's home, the player should have typed the command `set_own_place home` + for this to work. + diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..8c26f7c --- /dev/null +++ b/api.lua @@ -0,0 +1,531 @@ + +local S = respawn.S + + + +-- Load from storage or config +respawn.load = function() + -- Respawn points + respawn.respawn_points = respawn.load_db( "respawn" ) + + if respawn.respawn_points == nil then + -- If not found, then try to default to some values + respawn.reset_respawns() + end + + -- Server global named places/fine points of view + respawn.places = respawn.load_db( "places" ) or {} + + -- Per player named places/fine points of view + respawn.player_places = respawn.load_db( "player_places" ) or {} + + -- Per player death + respawn.player_deaths = respawn.load_db( "player_deaths" ) or {} +end + + + +-- Reset respawn to default value +respawn.reset_respawns = function() + respawn.respawn_points = {} + respawn.save_db( "respawn" , respawn.respawn_points ) + return true +end + + + +-- data contains pos and look +respawn.set_respawn = function( spawn_id , data ) + spawn_id = spawn_id or 1 + respawn.respawn_points[ spawn_id ] = data + respawn.save_db( "respawn" , respawn.respawn_points ) + return true +end + + + +respawn.reset_places = function() + respawn.places = {} + respawn.save_db( "places" , respawn.places ) + return true +end + + + +respawn.set_place = function( place_name , data ) + if not place_name or type( place_name ) ~= "string" or place_name == "" then return false end + respawn.places[ place_name ] = data + respawn.save_db( "places" , respawn.places ) + return true +end + + + +respawn.remove_place = function( place_name ) + if not place_name or type( place_name ) ~= "string" or place_name == "" then return false end + respawn.places[ place_name ] = nil + respawn.save_db( "places" , respawn.places ) + return true +end + + + +-- Remove all players' places +respawn.reset_all_players_places = function() + respawn.player_places = {} + respawn.save_db( "player_places" , respawn.player_places ) + return true +end + + + +-- Reset personal places for one player only +respawn.reset_player_places = function( player ) + if not player then return false end + local player_name = player:get_player_name() + if not player_name then return false end + respawn.player_places[ player_name ] = {} + respawn.save_db( "player_places" , respawn.player_places ) + return true +end + + + +respawn.set_player_place = function( player , place_name , data ) + if not player then return false end + local player_name = player:get_player_name() + if not player_name then return false end + if not place_name or type( place_name ) ~= "string" or place_name == "" then place_name = "home" end + if not respawn.player_places[ player_name ] then respawn.player_places[ player_name ] = {} end + respawn.player_places[ player_name ][ place_name ] = data + respawn.save_db( "player_places" , respawn.player_places ) + return true +end + + + +respawn.remove_player_place = function( player , place_name ) + if not player then return false end + local player_name = player:get_player_name() + if not player_name then return false end + if not place_name or type( place_name ) ~= "string" or place_name == "" then return false end + + -- Nothing to do + if not respawn.player_places[ player_name ] then return true end + + respawn.player_places[ player_name ][ place_name ] = nil + respawn.save_db( "player_places" , respawn.player_places ) + return true +end + + + +-- TODO: for instance it just counts how many there are +respawn.output_respawn_points = function( player ) + if not player then return false end + + local player_name = player:get_player_name() + if player_name == "" then return false end + + minetest.chat_send_player( player_name , S("There are @1 respawn points." , #respawn.respawn_points) ) +end + + + +respawn.output_places = function( player ) + if not player then return false end + + local player_name = player:get_player_name() + if player_name == "" then return false end + + local places_str = "" + local count = 0 + + for key , value in pairs( respawn.places ) do + if value.full_name then + places_str = places_str .. "\n " .. value.full_name .. " [" .. key .. "]" + else + places_str = places_str .. "\n [" .. key .. "]" + end + + count = count + 1 + end + + if count == 0 then + minetest.chat_send_player( player_name , S("There is no place defined.") ) + else + minetest.chat_send_player( player_name , S("Global places:@1" , places_str ) ) + end +end + + + +respawn.output_player_places = function( player ) + if not player then return false end + + local player_name = player:get_player_name() + if player_name == "" then return false end + + if not respawn.player_places[ player_name ] then + minetest.chat_send_player( player_name , S("You have no own place defined.") ) + return true + end + + local places_str = "" + local count = 0 + + for key , value in pairs( respawn.player_places[ player_name ] ) do + if value.full_name then + places_str = places_str .. "\n " .. value.full_name .. " [" .. key .. "]" + else + places_str = places_str .. "\n [" .. key .. "]" + end + + count = count + 1 + end + + if count == 0 then + minetest.chat_send_player( player_name , S("You have no own place defined.") ) + else + minetest.chat_send_player( player_name , S("Your personal places:@1" , places_str ) ) + end +end + + + +respawn.teleport = function( player , point ) + if not player or not point then return false end + + player:set_pos( point.pos ) + + if point.look then + player:set_look_horizontal( point.look.h ) + player:set_look_vertical( point.look.v ) + end + + return true +end + + + +respawn.teleport_to_respawn = function( player , spawn_id ) + spawn_id = spawn_id or math.random( #respawn.respawn_points ) + + local point = respawn.respawn_points[ spawn_id ] + if not point then point = respawn.respawn_points[ 1 ] end + + return respawn.teleport( player , point ) +end + + + +respawn.teleport_to_place = function( player , place_name ) + local point = respawn.places[ place_name ] + return respawn.teleport( player , point ) +end + + + +respawn.teleport_to_player_place = function( player , place_name ) + if not player then return false end + + local player_name = player:get_player_name() + if player_name == "" or not respawn.player_places[ player_name ] then return false end + + local point = respawn.player_places[ player_name ][ place_name ] + return respawn.teleport( player , point ) +end + + + +respawn.teleport_to_other_player_place = function( player , other_player , place_name ) + if not player or not other_player then return false end + + local other_player_name = other_player:get_player_name() + if other_player_name == "" or not respawn.player_places[ other_player_name ] then return false end + + local point = respawn.player_places[ other_player_name ][ place_name ] + return respawn.teleport( player , point ) +end + + + +respawn.teleport_to_other_player = function( player , other_player ) + if not player or not other_player then return false end + + local pos = other_player:get_pos() + + -- Avoid to invade one's personal place ^^ + pos.x = pos.x + math.random( -2 , 2 ) + pos.y = pos.y + math.random( 0 , 1 ) + pos.z = pos.z + math.random( -2 , 2 ) + + return respawn.teleport( player , { pos = pos } ) +end + + + +respawn.teleport_to_player_last_death_place = function( player ) + if not player then return false end + + local player_name = player:get_player_name() + if player_name == "" or not respawn.player_deaths[ player_name ] or #respawn.player_deaths[ player_name ] == 0 then return false end + + local point = respawn.player_deaths[ player_name ][ #respawn.player_deaths[ player_name ] ] ; + return respawn.teleport( player , point ) +end + + + +respawn.teleport_delay = function( player , type , id , delay ) + minetest.after( delay , function() + if type == "respawn" then + respawn.teleport_to_respawn( player , id ) + elseif type == "place" then + respawn.teleport_to_place( player , id ) + elseif type == "player_place" then + respawn.teleport_to_player_place( player , id ) + elseif type == "last_death" then + respawn.teleport_to_player_last_death_place( player ) + end + end ) +end + + + +-- a and b are position +function squared_distance( a , b ) + return ( a.x - b.x ) * ( a.x - b.x ) + ( a.y - b.y ) * ( a.y - b.y ) + ( a.z - b.z ) * ( a.z - b.z ) +end + + + +respawn.closest_thing = function( list , pos , max_dist , max_squared_dist ) + if not max_dist and not max_squared_dist then max_dist = 64000 end + if not max_squared_dist then max_squared_dist = max_dist * max_dist end + + local closest_squared_dist = max_squared_dist + local closest_place + local closest_place_name + local squared_dist + + for place_name , place in pairs( list ) do + squared_dist = squared_distance( pos , place.pos ) + + if squared_dist < closest_squared_dist then + closest_squared_dist = squared_dist + closest_place = place + closest_place_name = place_name + end + end + + return closest_place_name , closest_place , closest_squared_dist +end + + + +respawn.closest_respawn = function( pos , max_dist , max_squared_dist ) + return respawn.closest_thing( respawn.respawn_points , pos , max_dist , max_squared_dist ) +end + + + +respawn.closest_place = function( pos , max_dist , max_squared_dist ) + return respawn.closest_thing( respawn.places , pos , max_dist , max_squared_dist ) +end + + + +respawn.closest_player_place = function( player_name , pos , max_dist , max_squared_dist ) + if respawn.player_places[ player_name ] then + return respawn.closest_thing( respawn.player_places[ player_name ] , pos , max_dist , max_squared_dist ) + end +end + + + +respawn.closest_place_or_player_place = function( player_name , pos , max_dist ) + local place_name , place , square_dist , place_name2 , place2 , square_dist2 + + -- Use the chat player for player place, it makes more sense + place_name , place , square_dist = respawn.closest_player_place( player_name , pos , max_dist ) + + if place_name then + place_name2 , place2 , square_dist2 = respawn.closest_place( pos , nil , square_dist ) + + if place_name2 then + return place_name2 , place2 , square_dist2 + else + return place_name , place , square_dist + end + else + return respawn.closest_place( pos , max_dist ) + end +end + + + +respawn.respawn = function( player ) + -- We use a delay because returning true has no effect despite what the doc tells + -- so we teleport after the regular spawn + + if minetest.settings:get_bool("enable_home_respawn") then + local player_name = player:get_player_name() + if player_name and respawn.player_places[ player_name ] and respawn.player_places[ player_name ].home then + respawn.teleport_delay( player , "player_place" , "home" , 0 ) + return true + end + end + + if #respawn.respawn_points > 0 then + respawn.teleport_delay( player , "respawn" , math.random( #respawn.respawn_points ) , 0 ) + return true + end + + -- If no respawn points defined, let the default behavior kick in... then add the actual default spawn to our list! + minetest.after( 0.5 , function() + local pos = player:get_pos() + + -- Check if there is still no respawn point and if the player is still available + if #respawn.respawn_points > 0 or not pos then return end + + respawn.set_respawn( 1 , { + pos = pos , + look = { h = player:get_look_horizontal() , v = player:get_look_vertical() } + } ) + end ) +end + + + +respawn.add_death_log = function( player , data ) + if not player then return false end + local player_name = player:get_player_name() + if not player_name then return false end + + if not respawn.player_deaths[ player_name ] then respawn.player_deaths[ player_name ] = {} end + table.insert( respawn.player_deaths[ player_name ] , data ) + respawn.save_db( "player_deaths" , respawn.player_deaths ) + return true +end + + + +local function message_node_name( node_name ) + node_name = node_name:gsub( "[^:]+:" , "" ) + node_name = node_name:gsub( "_" , " " ) + return node_name +end + + + +local function message_biome_name( biome_name ) + biome_name = biome_name:gsub( "[^:]+:" , "" ) + biome_name = biome_name:gsub( "_" , " " ) + return biome_name +end + + + +respawn.death_message = function( player_name , data ) + local place = S("at some unknown place") + + if data.place and data.place ~= "" and type( data.place ) == "string" then + place = "near " .. message_biome_name( data.place ) + elseif data.biome and data.biome ~= "" and type( data.biome ) == "string" then + place = "near " .. message_biome_name( data.biome ) + end + + if data.by_type == "player" then + if data.using and data.using ~= "" and type( data.using ) == "string" then + return S("@1 was killed by @2, using @3, @4." , player_name , data.by , message_node_name( data.using ) , place ) + else + return S("@1 was killed by @2 @3." , player_name , data.by , place ) + end + + elseif data.by_type == "entity" then + -- For instance there is no difference between player and entity death messages + -- Also it's worth noting that we need to use message_node_name() because sometime we got an entity type as name (e.g. mobs_xxx:mob_type) + if data.using and data.using ~= "" and type( data.using ) == "string" then + return S("@1 was killed by @2, using @3, @4." , player_name , message_node_name( data.by ) , message_node_name( data.using ) , place ) + else + return S("@1 was killed by @2 @3." , player_name , message_node_name( data.by ) , place ) + end + + elseif data.by_type == "fall" then + return S("@1 has fallen @2." , player_name , place ) + + elseif data.by_type == "drown" then + if data.by and data.by ~= "" and type( data.by ) == "string" then + return S("@1 has drown in @2, @3." , player_name , message_node_name( data.by ) , place ) + else + return S("@1 has drown @2." , player_name , place ) + end + + elseif data.by_type == "node" then + if data.by and data.by ~= "" and type( data.by ) == "string" then + return S("@1 should not play with @2, @3." , player_name , message_node_name( data.by ) , place ) + else + return S("@1 should not play with dangerous things @2." , player_name , place ) + end + end + + return S("@1 was killed @2." , player_name , place ) +end + + + +respawn.death = function( player , data ) + if not player then return false end + local player_name = player:get_player_name() + if not player_name then return false end + + local pos = player:get_pos() + data.pos = pos + + local place_name , place = respawn.closest_place( pos , 80 ) + + if place then + data.place = place.full_name or place_name + end + + local biome_data = minetest.get_biome_data( pos ) + + if biome_data then + data.biome = minetest.get_biome_name( biome_data.biome ) + end + + respawn.add_death_log( player , data ) + minetest.chat_send_all( respawn.death_message( player_name , data ) ) + + return true +end + + + +respawn.output_deaths = function( chat_player , player_name ) + local chat_player_name = chat_player:get_player_name() + if chat_player_name == "" then return false end + + if not player_name then player_name = chat_player_name end + + if not respawn.player_deaths[ player_name ] then + minetest.chat_send_player( chat_player_name , S("@1 hasn't died already.", player_name ) ) + return true + end + + local deaths_str = "" + local count = 0 + + for key , value in pairs( respawn.player_deaths[ player_name ] ) do + deaths_str = deaths_str .. "\n " .. key .. ": " .. respawn.death_message( player_name , value ) + count = count + 1 + end + + if count == 0 then + minetest.chat_send_player( chat_player_name , S("@1 hasn't died already.") ) + else + minetest.chat_send_player( chat_player_name , S("@1 has died @2 times: @3" , player_name , count , deaths_str ) ) + end +end + diff --git a/commands.lua b/commands.lua new file mode 100644 index 0000000..f7e1120 --- /dev/null +++ b/commands.lua @@ -0,0 +1,570 @@ + +local S = respawn.S + + + +minetest.register_privilege( "teleport", { + description = S("Can use /teleport to self teleport to a registered respawn point, global place, own place, last death place. Can teleport to xyz coordinates or close to another player in conjunction with the \"locate\" privilege.") , + give_to_singleplayer = false +} ) + + + +minetest.register_privilege( "teleport_other", { + description = S("Can use /teleport_other to teleport any player to a registered respawn point, global place, own place (yours), last death place (their), or close to self. Can teleport to xyz coordinates or close to another player in conjunction with the \"locate\" privilege.") , + give_to_singleplayer = false +} ) + + + +minetest.register_privilege( "place", { + description = S("Can use /set_place, /remove_place, /reset_places and /reset_all_player_places to manage global places.") , + give_to_singleplayer = false +} ) + + + +minetest.register_privilege( "locate", { + description = S("Can use advanced /where command to locate other player and output coordinate, extend /teleport and /teleport_other to support xyz coordinates and teleporting close to another player.") , + give_to_singleplayer = false +} ) + + + +-- Join an array of string +function join( tab , delimiter , first , last ) + if not delimiter then delimiter = "" end + if not first then first = 1 end + if not last then last = #tab end + + local str = tab[ first ] or "" + + for i = first + 1, last , 1 do + str = str .. delimiter .. tab[ i ] + end + + return str +end + + + +minetest.register_chatcommand( "list_respawns", { + description = S("List all respawn points."), + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + return respawn.output_respawn_points( player ) + end +} ) + + + +minetest.register_chatcommand( "reset_respawns", { + description = S("Reset respawn points."), + privs = { server = true }, + func = function( player_name , param ) + if respawn.reset_respawns() then + return true, S("Respawn points reset." ) + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "set_respawn", { + description = S("Create a respawn point on your current player position. Without argument it set the first respawn point, with 'new' it appends a new respawn point."), + params = S("[|new]"), + privs = { server = true }, + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + local parts = string.split( param , " " ) + local spawn_id + + if parts[1] == "new" then + spawn_id = #respawn.respawn_points + 1 + else + spawn_id = tonumber( parts[1] ) or 1 + end + + if respawn.set_respawn( spawn_id , { + pos = player:get_pos() , + look = { h = player:get_look_horizontal() , v = player:get_look_vertical() } + } ) then + return true, S("Respawn point @1 set!", spawn_id) + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "list_places", { + description = S("List all global places."), + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + return respawn.output_places( player ) + end +} ) + + + +minetest.register_chatcommand( "reset_places", { + description = S("Reset all global places."), + privs = { server = true , place = true }, + func = function( player_name , param ) + if respawn.reset_places() then + return true, S("All global places removed." ) + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "set_place", { + description = S("Create a new global named place on your current player position."), + params = S(" []"), + privs = { place = true }, + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + local parts = string.split( param , " " ) + local place_name = parts[1] + local full_name = join( parts , " " , 2 ) + if full_name == "" then full_name = nil end + + if not place_name or place_name == "" then + return false, S("Missing place name!") + end + + if respawn.set_place( place_name , { + pos = player:get_pos() , + look = { h = player:get_look_horizontal() , v = player:get_look_vertical() } , + full_name = full_name + } ) then + return true, S("Place \"@1\" set!", full_name or place_name ) + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "remove_place", { + description = S("Remove a global place."), + params = S(""), + privs = { place = true }, + func = function( player_name , param ) + local parts = string.split( param , " " ) + local place_name = parts[1] + + if not place_name or place_name == "" then + return false, S("Missing place name!") + end + + if respawn.remove_place( place_name ) then + return true, S("Place removed.") + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "list_own_places", { + description = S("List all personal places."), + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + return respawn.output_player_places( player ) + end +} ) + + + +minetest.register_chatcommand( "reset_all_players_places", { + description = S("Reset all players' places."), + privs = { server = true , place = true }, + func = function( player_name , param ) + if respawn.reset_all_players_places() then + return true, S("All players' places removed." ) + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "reset_own_places", { + description = S("Reset your personal places."), + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + if respawn.reset_player_places( player ) then + return true, S("All your personal places removed." ) + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "set_own_place", { + description = S("Create a new personal named place on your current player position. Without argument, it set your home."), + params = S(" []"), + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + local parts = string.split( param , " " ) + local place_name = parts[1] + local full_name = join( parts , " " , 2 ) + if full_name == "" then full_name = nil end + + if not place_name or place_name == "" then + place_name = "home" + end + + if respawn.set_player_place( player , place_name , { + pos = player:get_pos() , + look = { h = player:get_look_horizontal() , v = player:get_look_vertical() } , + full_name = full_name + } ) then + return true, S("Personal place \"@1\"set!", full_name or place_name ) + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "remove_own_place", { + description = S("Remove a personal place."), + params = S(""), + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + local parts = string.split( param , " " ) + local place_name = parts[1] + + if not place_name or place_name == "" then + return false, S("Missing place!") + end + + if respawn.remove_player_place( player , place_name ) then + return true, S("Personal place removed.") + end + + return false, S("Something went wrong...") + end +} ) + + + +minetest.register_chatcommand( "where", { + description = S("Find the place where you are, if there is one close enough. If you have the \"locate\" privilege you can also see coordinate and other people location."), + params = S("[]"), + func = function( chat_player_name , param ) + local parts = string.split( param , " " ) + local player_name = parts[1] or chat_player_name + + local player = minetest.get_player_by_name( player_name ) + + if not player or not chat_player_name then + return false, S("Player not found!") + end + + local has_locate = minetest.check_player_privs( chat_player_name, { locate = true } ) + + if not has_locate and player_name ~= chat_player_name then + return false, S("You can't locate other player (missing the \"locate\" privilege)!") + end + + local pos = player:get_pos() + local max_dist = 80 + + -- Use the chat player for player place, it makes more sense + place_name , place = respawn.closest_place_or_player_place( chat_player_name , pos , max_dist ) + + if place_name then + if has_locate then + return true, S("@1 is near @2 (@3, @4, @5).", player_name, place.full_name or place_name, pos.x , pos.y, pos.z) + end + + return true, S("@1 is near @2.", player_name, place.full_name or place_name) + end + + if has_locate then + return true, S("@1 is at (@2, @3, @4).", player_name, pos.x , pos.y, pos.z) + end + + return false, S("No place found near you.") + end +} ) + + + +minetest.register_chatcommand( "teleport", { + description = S("Teleport to a map respawn point, a place or more. First argument can be respawn/spawn, place/global, own_place/own/home, death (for last death place), xyz (for coordinates), player (teleport close to another player), if omitted it searches for own place first, then for global place. Last argument can be avoided: for respawn it would move to a random respawn point, for own place it would go to the home."), + params = S(" [] | xyz "), + privs = { teleport = true }, + func = function( player_name , param ) + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + local parts = string.split( param , " " ) + local type = parts[1] or nil + + if not type then + return false, S("Missing type!") + end + + local id = parts[2] or nil + + if type == "respawn" or type =="spawn" then + if respawn.teleport_to_respawn( player , tonumber( id ) , true ) then + if id then + return true, S("Teleported to the respawn n°@1.", id) + else + return true, S("Teleported to a random respawn point.") + end + end + elseif type == "place" or type == "global" then + if respawn.teleport_to_place( player , id ) then + return true, S("Teleported to @1.", respawn.places[ id ].full_name or id) + end + elseif type == "own" or type == "own_place" then + if respawn.teleport_to_player_place( player , id ) then + return true, S("Teleported to @1.", respawn.player_places[ player_name ][ id ].full_name or id) + end + elseif type == "death" then + if respawn.teleport_to_player_last_death_place( player ) then + return true, S("Teleported to last death place.") + end + elseif type == "xyz" then + if not minetest.check_player_privs( player_name, { locate = true } ) then + return false, S("You can't teleport to coordinate (missing the \"locate\" privilege)!") + end + + if #parts < 4 then + return false, S("Missing x y z arguments") + end + + local pos = { + x = tonumber( parts[2] ) , + y = tonumber( parts[3] ) , + z = tonumber( parts[4] ) + } + if pos.x and pos.y and pos.z then + if respawn.teleport( player , { pos = pos } ) then + return true, S("Teleported to (@1, @2, @3).", pos.x , pos.y , pos.z) + end + end + elseif type == "player" then + if not minetest.check_player_privs( player_name, { locate = true } ) then + return false, S("You can't teleport to a player (missing the \"locate\" privilege)!") + end + + if not id then + return false, S("Missing the other player name argument") + end + + local other_player = minetest.get_player_by_name( id ) + + if not other_player then + return false, S("Player \"@1\" not found!", id) + end + + if respawn.teleport_to_other_player( player , other_player ) then + return true, S("Teleported close to @1.", id) + end + elseif respawn.teleport_to_player_place( player , type ) then + return true, S("Teleported to @1.", respawn.player_places[ player_name ][ type ].full_name or type) + elseif respawn.teleport_to_place( player , type ) then + return true, S("Teleported to @1.", respawn.places[ type ].full_name or type) + end + + return false, S("Respawn point or place not found!") + end +} ) + + + +-- Mostly a copy/paste of /teleport command +minetest.register_chatcommand( "teleport_other", { + description = S("Teleport another player to a map respawn point, a place or more. First argument is the player name, second argument can be respawn/spawn, place/global, death (for last death place), xyz (for coordinates), player (teleport close to another player), here (teleport close to you), if omitted it will search for global place. Last argument can be avoided: for respawn it would move to a random respawn point."), + params = S(" [] [] | xyz "), + privs = { teleport = true , teleport_other = true }, + func = function( performer_name , param ) + local performer = minetest.get_player_by_name( performer_name ) + + local parts = string.split( param , " " ) + local player_name = parts[1] or nil + + local player = minetest.get_player_by_name( player_name ) + + if not player then + return false, S("Player not found!") + end + + local type = parts[2] or nil + + if not type then + return false, S("Missing type!") + end + + local id = parts[3] or nil + + if type == "respawn" or type =="spawn" then + if respawn.teleport_to_respawn( player , tonumber( id ) , true ) then + if id then + minetest.chat_send_all( S("@1 teleported @2 to the respawn n°@3.", performer_name , player_name , id) ) + return true + else + minetest.chat_send_all( S("@1 teleported @2 to a random respawn point.", performer_name , player_name) ) + return true + end + end + elseif type == "place" or type == "global" then + if respawn.teleport_to_place( player , id ) then + minetest.chat_send_all( S("@1 teleported @2 to @3.", performer_name , player_name , respawn.places[ id ].full_name or id) ) + return true + end + elseif type == "own" or type == "own_place" then + if not performer then + return false, S("Player not found!") + end + + if respawn.teleport_to_other_player_place( player , performer , id ) then + minetest.chat_send_all( S("@1 teleported @2 to @3.", performer_name , player_name , respawn.player_places[ performer_name ][ id ].full_name or id) ) + return true + end + elseif type == "death" then + if respawn.teleport_to_player_last_death_place( player ) then + minetest.chat_send_all( S("@1 teleported @2 to the last death place.", performer_name , player_name ) ) + return true + end + elseif type == "xyz" then + if not minetest.check_player_privs( performer_name, { locate = true } ) then + return false, S("You can't teleport other to coordinate (missing the \"locate\" privilege)!") + end + + if #parts < 5 then + return false, S("Missing x y z arguments") + end + + local pos = { + x = tonumber( parts[3] ) , + y = tonumber( parts[4] ) , + z = tonumber( parts[5] ) + } + if pos.x and pos.y and pos.z then + if respawn.teleport( player , { pos = pos } ) then + minetest.chat_send_all( S("@1 teleported @2 to (@3, @4, @5).", performer_name , player_name , pos.x , pos.y , pos.z ) ) + return true + end + end + elseif type == "player" then + if not minetest.check_player_privs( performer_name, { locate = true } ) then + return false, S("You can't teleport to a player (missing the \"locate\" privilege)!") + end + + if not id then + return false, S("Missing the other player name argument") + end + + local other_player = minetest.get_player_by_name( id ) + + if not other_player then + return false, S("Player \"@1\" not found!", id) + end + + if respawn.teleport_to_other_player( player , other_player ) then + minetest.chat_send_all( S("@1 teleported @2 close to @3.", performer_name , player_name , id ) ) + return true + end + elseif type == "here" then + if not performer then + return false, S("Player not found!") + end + + if respawn.teleport_to_other_player( player , performer ) then + minetest.chat_send_all( S("@1 teleported @2 close to @3.", performer_name , player_name , performer_name ) ) + return true + end + elseif respawn.teleport_to_other_player_place( player , performer , type ) then + minetest.chat_send_all( S("@1 teleported @2 to @3.", performer_name , player_name , respawn.player_places[ performer_name ][ type ].full_name or type) ) + return true + elseif respawn.teleport_to_place( player , type ) then + minetest.chat_send_all( S("@1 teleported @2 to @3.", performer_name , player_name , respawn.places[ type ].full_name or type) ) + return true + end + + return false, S("Respawn point or place not found!") + end +} ) + + + +minetest.register_chatcommand( "list_deaths", { + description = S("List all deaths of a player. Without argument it applies to the current player."), + params = S("[]"), + func = function( chat_player_name , param ) + local chat_player = minetest.get_player_by_name( chat_player_name ) + + if not chat_player then + return false, S("Player (chat) not found!") + end + + local parts = string.split( param , " " ) + local player_name = parts[1] or nil + + return respawn.output_deaths( chat_player , player_name ) + end +} ) + diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..4ad96d5 --- /dev/null +++ b/depends.txt @@ -0,0 +1 @@ +default diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..c6ca28d --- /dev/null +++ b/init.lua @@ -0,0 +1,158 @@ +-- Global respawn namespace +respawn = {} +respawn.path = minetest.get_modpath( minetest.get_current_modname() ) +respawn.S = minetest.get_translator( "respawn" ) + +-- Load files +dofile( respawn.path .. "/storage.lua" ) +dofile( respawn.path .. "/api.lua" ) +dofile( respawn.path .. "/commands.lua" ) + +respawn.load() + + + +minetest.register_on_respawnplayer( function( player ) + respawn.respawn( player ) + -- returning true has no effect despite what the doc tells + return true +end ) + + + +minetest.register_on_newplayer( function( player ) + respawn.respawn( player ) + -- returning true has no effect despite what the doc tells + return true +end ) + + + +minetest.register_on_punchplayer( function( player, hitter, time_from_last_punch, tool_capabilities, dir, damage ) + local hp = player:get_hp() + + if hp <= 0 or hp - damage > 0 or not hitter then return end + + if hitter:is_player() then + respawn.death( player , { + by_type = "player" , + by = hitter:get_player_name() , + using = hitter:get_wielded_item():get_name() , + damage = damage + } ) + else + local properties = hitter:get_properties() + local luaEntity = hitter:get_luaentity() + + --[[ Debug to find some hidden names + minetest.chat_send_all( "----- debug hitter's properties -----" ) + for k,v in pairs( properties ) do + minetest.chat_send_all( "" .. k .. ": " .. tostring( v ) ) + end + + minetest.chat_send_all( "----- debug hitter's nametag attributes -----" ) + for k,v in pairs( hitter:get_nametag_attributes() ) do + minetest.chat_send_all( "" .. k .. ": " .. tostring( v ) ) + end + + minetest.chat_send_all( "----- debug hitter:get_luaentity() -----" ) + for k,v in pairs( hitter:get_luaentity() ) do + minetest.chat_send_all( "" .. k .. ": " .. tostring( v ) ) + end + + minetest.chat_send_all( "----- debug hitter:get_luaentity()'s meta table -----" ) + for k,v in pairs( getmetatable( hitter:get_luaentity() ) ) do + minetest.chat_send_all( "" .. k .. ": " .. tostring( v ) ) + end + + minetest.chat_send_all( "----- debug hitter:get_luaentity()'s meta table's __index -----" ) + for k,v in pairs( getmetatable( hitter:get_luaentity() ).__index ) do + minetest.chat_send_all( "" .. k .. ": " .. tostring( v ) ) + end + + minetest.chat_send_all( "----- debug hitter.name -----" .. ( hitter.name or "(none)" ) ) + minetest.chat_send_all( "----- debug hitter.nametag -----" .. ( hitter.nametag or "(none)" ) ) + minetest.chat_send_all( "----- debug hitter.nametag2 -----" .. ( hitter.nametag2 or "(none)" ) ) + minetest.chat_send_all( "----- debug hitter:get_luaentity().name -----" .. ( hitter:get_luaentity().name or "(none)" ) ) + minetest.chat_send_all( "----- debug hitter:get_luaentity().nametag -----" .. ( hitter:get_luaentity().nametag or "(none)" ) ) + minetest.chat_send_all( "----- debug hitter:get_luaentity().nametag2 -----" .. ( hitter:get_luaentity().nametag2 or "(none)" ) ) + minetest.chat_send_all( "----- debug properties.name -----" .. ( properties.name or "(none)" ) ) + minetest.chat_send_all( "----- debug properties.nametag -----" .. ( properties.nametag or "(none)" ) ) + minetest.chat_send_all( "----- debug properties.nametag2 -----" .. ( properties.nametag2 or "(none)" ) ) + --]] + + local name + + -- mobs_humans uses that, not sure how standard it is + if luaEntity.given_name and luaEntity.given_name ~= "" and type( luaEntity.given_name ) == "string" then name = luaEntity.given_name + -- aliveai uses that, not sure how standard it is + elseif luaEntity.botname and luaEntity.botname ~= "" and type( luaEntity.botname ) == "string" then name = luaEntity.botname + -- Never seen it set, but seems to me a good way to set an entity proper name + elseif properties.name and properties.name ~= "" and type( properties.name ) == "string" then name = properties.name + -- nametag2 is set by mobs_redo as a backup when changing nametag to display health, so it's reliable but rarely there + elseif properties.nametag2 and properties.nametag2 ~= "" and type( properties.nametag2 ) == "string" then name = properties.nametag2 + -- nametag is not reliable, can be just the health display + --elseif properties.nametag and properties.nametag ~= "" and type( properties.nametag ) == "string" then name = properties.nametag + -- Usually, this is the generic name of the entity (its kind) rather than its proper name + else name = luaEntity.name + end + + respawn.death( player , { + by_type = "entity" , + by = name , + using = hitter:get_wielded_item():get_name() , + damage = damage + } ) + end +end ) + + + +minetest.register_on_player_hpchange( function( player, hp_change, reason ) + local hp = player:get_hp() + local by_type + local by + + if hp <= 0 or hp + hp_change > 0 then return end + + local pos = player:get_pos() + + if reason.type == "fall" then + by_type = "fall" + elseif reason.type=="drown" then + by_type = "drown" + local eye_pos = vector.add( { x = 0, z = 0, y = player:get_properties().eye_height } , pos ) + by = minetest.get_node( eye_pos ).name + elseif reason.type == "node_damage" then + -- from deathlist mod + by_type = "node" + local eye_pos = vector.add( { x = 0, z = 0, y = player:get_properties().eye_height } , pos ) + local killing_node_head_name = minetest.get_node( eye_pos ).name + local killing_node_head = minetest.registered_nodes[ killing_node_head_name ] + local killing_node_feet_name = minetest.get_node( pos ).name + local killing_node_feet = minetest.registered_nodes[ killing_node_feet_name ] + by = killing_node_feet_name + + if ( killing_node_head.node_damage or 0 ) > ( killing_node_feet.node_damage or 0 ) then + by = killing_node_head_name + end + elseif reason.type == "punch" then + -- do nothing, it should already be done by minetest.register_on_punchplayer() + --by_type = "punch" + return + elseif reason.type == "set_hp" then + -- maybe /killme + by_type = "unknown" + elseif reason.type == "respawn" then + -- Usually, we don't get there because respawn give hp rather than removing them + return + else + by_type = "unknown" + end + + respawn.death( player , { + by_type = by_type , + by = by , + damage = - hp_change + } ) +end ) diff --git a/locale/respawn.fr.tr b/locale/respawn.fr.tr new file mode 100644 index 0000000..2aa5da4 --- /dev/null +++ b/locale/respawn.fr.tr @@ -0,0 +1 @@ +# textdomain: respawn diff --git a/locale/template.txt b/locale/template.txt new file mode 100644 index 0000000..2aa5da4 --- /dev/null +++ b/locale/template.txt @@ -0,0 +1 @@ +# textdomain: respawn diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..c7035dd --- /dev/null +++ b/mod.conf @@ -0,0 +1,6 @@ +name = respawn +description = Manage respawn points, interesting places, teleportation and death records +release = 1 +author = cronvel +title = Respawn +depends = default diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..d714ef6 Binary files /dev/null and b/screenshot.png differ diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..bb80a2f --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,3 @@ +# When enabled, players can respawn in their home place, if they defined one +# using the command: /set_own_place home +enable_home_respawn (Enable home respawn) bool false diff --git a/storage.lua b/storage.lua new file mode 100644 index 0000000..f7b3106 --- /dev/null +++ b/storage.lua @@ -0,0 +1,62 @@ + +-- Maybe those storage helpers should have their own mod? + +local storage_base_path = minetest.get_worldpath() .. "/" +local storage_ext = "." .. minetest.get_current_modname() .. ".db" +storage_files = {} + +respawn.load_db = function( key ) + local file = storage_files[ key ] + + if file then + file:seek( "set" , 0 ) + else + local file_path = storage_base_path .. key .. storage_ext + file = io.open( file_path , "r+" ) + + if not file then + return nil + end + + storage_files[ key ] = file + end + + -- We only load the first line, after that line, there is garbage, see .save_db() comment. + local str = file:read( "*l" ) + local value + + if str and str ~= "" then + value = minetest.parse_json( str ) + if value then + return value + end + end + + return nil +end + + + +respawn.save_db = function( key , value ) + local file = storage_files[ key ] + + if file then + file:seek( "set" , 0 ) + else + local file_path = storage_base_path .. key .. storage_ext + file = io.open( file_path , "w+" ) + + if not file then + error( "Can't create file: " .. file_path ) + end + + storage_files[ key ] = file + end + + -- We can't truncate files in Lua, and that sucks big time... + -- So to improve perf and not opening/closing/overwriting files everytime something needs to be written we use this trick: + -- we add a new line after writing JSON. + -- When loading, we only load the first line. + file:write( minetest.write_json( value ) , "\n" ) +end +