commit 46999c1eda91e05d4b6f8bc36f853894e1f7eee4 Author: Leslie Krause Date: Tue Apr 9 10:35:00 2019 -0400 Build 01 - initial beta version - included support files for public release diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..4f7c8a1 --- /dev/null +++ b/README.txt @@ -0,0 +1,129 @@ +Protector Redux Mod v1.0 +By Leslie Krause + +Protector Redux offers an efficient and flexible node-based protection scheme for players +on survival and creative servers. The mod was inspired by TenPlus1's "Protector Redo", +which continues the legacy of Zeg9 and glomie's original protection mods. + +Repository +---------------------- + + * Browse source code: + https://bitbucket.org/sorcerykid/protector + + * Download archive: + https://bitbucket.org/sorcerykid/protector/get/master.zip + https://bitbucket.org/sorcerykid/protector/get/master.tar.gz + +Revision History +---------------------- + +Version 1.0b (09-Apr-2019) + - initial beta version + +Dependencies +---------------------- + + * Default Mod (required) + https://github.com/minetest/minetest_game/default + + * ActiveFormspecs Mod (required) + https://bitbucket.org/sorcerykid/formspecs + +Compatability +---------------------- + +Minetest 0.4.15+ required + +Installation +---------------------- + + 1) Unzip the archive into the mods directory of your game + 2) Rename the protector-master directory to "protector" + +Source Code License +---------------------- + +The MIT License (MIT) + +Copyright (c) 2019, Leslie Krause +Copyright (c) 2016, TenPlus1 + +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. + +For more details: +https://opensource.org/licenses/MIT + +Multimedia License (textures, sounds, and models) +---------------------------------------------------------- + +Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) + + /textures/protector_display.png + by TenPlus1 + modified by maikerumine + + /textures/protector_logo.png + by TenPlus1 + + /textures/protector_mask.png + by sorcerykid + + /textures/protector_mask_saved.png + by sorcerykid + + /textures/protector_side1.png (Originally WTFPL) + by Zeg9 + + /textures/protector_side2.png (Originally WTFPL) + by by AndrejIT + + /textures/protector_top1.png (Originally WTFPL) + by Hugo Locurcio + + /textures/protector_top2.png (Originally WTFPL) + by AndrejIT + + /textures/protector_wand.png (Unspecified License) + by Anonymous_moose + http://forum.minetest.net/viewtopic.php?f=95&t=12703 + + +You are free to: +Share — copy and redistribute the material in any medium or format. +Adapt — remix, transform, and build upon the material for any purpose, even commercially. +The licensor cannot revoke these freedoms as long as you follow the license terms. + +Under the following terms: + +Attribution — You must give appropriate credit, provide a link to the license, and +indicate if changes were made. You may do so in any reasonable manner, but not in any way +that suggests the licensor endorses you or your use. + +No additional restrictions — You may not apply legal terms or technological measures that +legally restrict others from doing anything the license permits. + +Notices: + +You do not have to comply with the license for elements of the material in the public +domain or where your use is permitted by an applicable exception or limitation. +No warranties are given. The license may not give you all of the permissions necessary +for your intended use. For example, other rights such as publicity, privacy, or moral +rights may limit how you use the material. + +For more details: +http://creativecommons.org/licenses/by-sa/3.0/ diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..be3a964 --- /dev/null +++ b/depends.txt @@ -0,0 +1,2 @@ +default +formspecs diff --git a/description.txt b/description.txt new file mode 100644 index 0000000..a157f77 --- /dev/null +++ b/description.txt @@ -0,0 +1,2 @@ +Protector Redux offers an efficient and flexible node-based protection scheme for players +on survival and creative servers. diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..6f3277b --- /dev/null +++ b/init.lua @@ -0,0 +1,920 @@ +-------------------------------------------------------- +-- Minetest :: Protection Redux Mod (protector) +-- +-- See README.txt for licensing and other information. +-- Copyright (c) 2016-2019, Leslie E. Krause +-- +-- ./games/minetest_game/mods/protector/init.lua +-------------------------------------------------------- + +local OWNER_ANYBODY = "_anybody" +local OWNER_SOMEBODY = "_somebody" +local OWNER_NOBODY = "" -- do not change this value! + +local member_limit = 8 +local max_tool_range = 10 +local protector_radius = tonumber( minetest.setting_get( "protector_radius" ) or 5 ) + +minetest.register_privilege( "superuser", { + description = "Bypass ownership and protection checks.", + give_to_singleplayer = true, +} ) + +--------------------------- +-- Helper Functions +--------------------------- + +local function is_superuser( name ) + return minetest.check_player_privs( name, "superuser" ) +end + +local function get_members( meta ) + return meta:get_string( "members" ):split( " " ) +end + +local function set_members( meta, members ) + meta:set_string( "members", table.concat( members, " " ) ) +end + +local function is_member( meta, name ) + for _, n in pairs( get_members( meta ) ) do + if n == name then + return true + end + end + return false +end + +local function is_owner( meta, name ) + return name == meta:get_string( "owner" ) +end + +local function add_member( meta, name ) + if is_member( meta, name ) or is_owner( meta, name ) then + return + end + + local members = get_members( meta ) + + if #members < member_limit then + table.insert( members, name ) + end + + set_members( meta, members ) +end + +local function del_member( meta, name ) + local members = get_members( meta ) + + for i, n in pairs( members ) do + if n == name then + table.remove( members, i ) + break + end + end + + set_members( meta, members ) +end + +local function get_area_stats( meta ) + local bitmap = meta:get_string( "bitmap" ) + local node_total = #bitmap + local lock_count = 0 + + for node_count = 1, node_total do + if string.byte( bitmap, node_count ) == 120 then + lock_count = lock_count + 1 + end + end + + return node_total, lock_count +end + +local function is_area_locked( meta ) + local bitmap = meta:get_string( "bitmap" ) + return bitmap ~= "" +end + +local function is_area_locked_at( meta, source_pos, target_pos ) + local bitmap = meta:get_string( "bitmap" ) + + -- if there's no bitmap, then the entire area is unlocked + if bitmap == "" then return false end + + -- check for dot in bitmap, indicating that position is locked + local voxel_area = VoxelArea:new( { + MinEdge = vector.subtract( source_pos, protector_radius ), + MaxEdge = vector.add( source_pos, protector_radius ) + } ) + + -- TODO: Sanity check for correct bitmap length! + + local idx = voxel_area:indexp( target_pos ) + return string.byte( bitmap, idx ) == 120 +end + +local function lock_area( meta, source_pos, content_ids ) + local pos1 = vector.subtract( source_pos, protector_radius ) + local pos2 = vector.add( source_pos, protector_radius ) + + local voxel_manip = minetest.get_voxel_manip( ) + local min_pos, max_pos = voxel_manip:read_from_map( pos1, pos2 ) + local map_buffer = voxel_manip:get_data( ) + local voxel_area = VoxelArea:new( { MinEdge = min_pos, MaxEdge = max_pos } ) + + local node_data = { } + local lock_count = 0 + + -- indicate all non-buildable nodes as dot in bitmap + for idx in voxel_area:iterp( pos1, pos2 ) do + local is_buildable = content_ids[ map_buffer[ idx ] ] + + table.insert( node_data, is_buildable and " " or "x" ) + + if not is_buildable then + lock_count = lock_count + 1 + end + end + + -- bitmap is compressed by engine, so no need for optimization + meta:set_string( "bitmap", table.concat( node_data ) ) + + return #node_data, lock_count +end + +local function unlock_area( meta ) + meta:set_string( "bitmap", "" ) +end + +local function create_bitmap( is_buildable ) + return string.rep( is_buildable and " " or "x", math.pow( protector_radius * 2 + 1, 3 ) ) +end + +local function get_bitmap_raw( meta ) + local bitmap = meta:get_string( "bitmap" ) + return bitmap ~= "" and bitmap or nil +end + +local function set_bitmap_raw( meta, bitmap ) + meta:set_string( "bitmap", bitmap ) + return get_area_stats( meta ) +end + +--------------------------- +-- Formspec Handlers +--------------------------- + +local function open_shared_editor( pos, meta, player_name ) + local ignore_air = true + local ignore_water = true + local ignore_lava = true + + local function get_formspec( ) + local formspec = "size[8,7]" + .. default.gui_bg + .. default.gui_bg_img + .. default.gui_slots + .. "label[0.0,0.0;Protector Properties (Shared)]" + .. "box[0.0,0.6;7.9,0.1;#111111]" + .. "box[0.0,6.1;7.9,0.1;#111111]" + .. "label[0.0,1.0;Members: (type player name then click '+' to add)]" + .. "button_exit[6.0,6.5;2.0,0.5;close;Close]" + + if is_area_locked( meta, pos ) then + local node_total, lock_count = get_area_stats( meta ) + + formspec = formspec + .. "label[0.0,4.5;Bitmap Mask: (click 'Unlock' to disengage bitmap mask)]" + .. "button_exit[0.0,5.2;2.0,0.5;unlock;Unlock]" + .. string.format( "label[2.0,5.2;Contains %d nodes (%d locked, %d unlocked)]", node_total, lock_count, node_total - lock_count ) + else + formspec = formspec + .. "label[0.0,4.5;Bitmap Mask: (click 'Lock' to engage bitmap mask)]" + .. "button_exit[0.0,5.2;2.0,0.5;lock;Lock]" + .. "checkbox[2.0,5.0;ignore_air;Ignore Air;" .. tostring( ignore_air ) .. "]" + .. "checkbox[3.8,5.0;ignore_water;Ignore Water;" .. tostring( ignore_water ) .. "]" + .. "checkbox[6.0,5.0;ignore_lava;Ignore Lava;" .. tostring( ignore_lava ) .. "]" + end + + local members = get_members( meta ) + local count = 0 + + for _, member in pairs( members ) do + if count < member_limit then + formspec = formspec + .. string.format( "button[%0.2f,%0.2f;1.5,0.5;;%s]", count % 4 * 2, 0.8 + math.floor( count / 4 + 1 ), member ) + .. string.format( "button[%0.2f,%0.2f;0.75,0.5;del_member_%s;X]", count % 4 * 2 + 1.25, 0.8 + math.floor( count / 4 + 1 ), member ) + end + count = count + 1 + end + + if count < member_limit then + formspec = formspec + .. string.format( "field[%0.2f,%0.2f;1.433,0.5;member_name;;]", count % 4 * 2 + 1 / 3, 0.8 + math.floor( count / 4 + 1 ) + 1 / 3 ) + .. string.format( "button[%0.2f,%0.2f;0.75,0.5;add_member;+]", count % 4 * 2 + 1.25, 0.8 + math.floor( count / 4 + 1 ) ) + end + + return formspec + end + + local function on_close( pos, player, fields ) + if fields.close then + return + + elseif fields.ignore_air then + ignore_air = fields.ignore_air == "true" + + elseif fields.ignore_water then + ignore_water = fields.ignore_water == "true" + + elseif fields.ignore_lava then + ignore_lava = fields.ignore_lava == "true" + + elseif fields.lock then + local content_ids = { } + + if ignore_water then + content_ids[ minetest.get_content_id( "default:water_flowing" ) ] = true + content_ids[ minetest.get_content_id( "default:water_source" ) ] = true + end + if ignore_lava then + content_ids[ minetest.get_content_id( "default:lava_flowing" ) ] = true + content_ids[ minetest.get_content_id( "default:lava_source" ) ] = true + end + if ignore_air then + content_ids[ minetest.get_content_id( "air" ) ] = true + end + + local node_total, lock_count = lock_area( meta, pos, content_ids ) + minetest.chat_send_player( player_name, string.format( "Protection area updated (%d of %d nodes locked).", lock_count, node_total ) ) + + elseif fields.unlock then + unlock_area( meta ) + minetest.chat_send_player( player_name, "Protection area updated (all nodes unlocked)." ) + + elseif fields.add_member then + if string.match( fields.member_name, "^[a-zA-Z0-9_-]+$" ) and string.len( fields.member_name ) <= 25 then + add_member( meta, fields.member_name ) + minetest.update_form( player:get_player_name( ), get_formspec( meta ) ) + end + + elseif not fields.quit then + fields.member_name = nil + + local fname = next( fields, nil ) -- use next since we only care about the name of the first button + if fname then + local member_name = string.match( fname, "^del_member_(.+)" ) + if member_name then + del_member( meta, member_name ) + minetest.update_form( player:get_player_name( ), get_formspec( meta ) ) + end + end + end + end + + minetest.create_form( pos, player_name, get_formspec( ), on_close ) +end + +local function open_public_editor( pos, meta, player_name ) + local ignore_air = true + local ignore_water = false + local ignore_lava = false + local allow_doors = meta:get_string( "allow_doors" ) == "true" + local allow_chests = meta:get_string( "allow_chests" ) == "true" + + local function get_formspec( ) + local formspec = "size[8,5]" + .. default.gui_bg + .. default.gui_bg_img + .. default.gui_slots + .. "label[0.0,0.0;Protector Properties (Public)]" + .. "box[0.0,0.6;7.9,0.1;#111111]" + .. "box[0.0,4.1;7.9,0.1;#111111]" + .. "label[0.0,1.0;Permissions:]" + .. "button_exit[6.0,4.5;2.0,0.5;close;Close]" + + .. "checkbox[0.0,1.4;allow_doors;Allow Steel Doors;" .. tostring( allow_doors ) .. "]" + .. "checkbox[4.0,1.4;allow_chests;Allow Locked Chests;" .. tostring( allow_chests ) .. "]" + + if is_area_locked( meta, pos ) then + local node_total, lock_count = get_area_stats( meta ) + + formspec = formspec + .. "label[0.0,2.5;Bitmap Mask: (click 'Unlock' to disengage bitmap mask)]" + .. "button_exit[0.0,3.2;2.0,0.5;unlock;Unlock]" + .. string.format( "label[2.0,3.2;Contains %d nodes (%d locked, %d unlocked)]", node_total, lock_count, node_total - lock_count ) + else + formspec = formspec + .. "label[0.0,2.5;Bitmap Mask: (click 'Lock' to engage bitmap mask)]" + .. "button_exit[0.0,3.2;2.0,0.5;lock;Lock]" + .. "checkbox[2.0,3.0;ignore_air;Ignore Air;" .. tostring( ignore_air ) .. "]" + .. "checkbox[3.8,3.0;ignore_water;Ignore Water;" .. tostring( ignore_water ) .. "]" + .. "checkbox[6.0,3.0;ignore_lava;Ignore Lava;" .. tostring( ignore_lava ) .. "]" + end + + return formspec + end + + local function on_close( pos, player, fields ) + if fields.close then + return + + elseif fields.ignore_air then + ignore_air = fields.ignore_air == "true" + + elseif fields.ignore_water then + ignore_water = fields.ignore_water == "true" + + elseif fields.ignore_lava then + ignore_lava = fields.ignore_lava == "true" + + elseif fields.allow_chests then + allow_chests = fields.allow_chests == "true" + meta:set_string( "allow_chests", allow_chests and "true" or "false" ) + + elseif fields.allow_doors then + allow_doors = fields.allow_doors == "true" + meta:set_string( "allow_doors", allow_doors and "true" or "false" ) + + elseif fields.lock then + local content_ids = { } + + if ignore_water then + content_ids[ minetest.get_content_id( "default:water_flowing" ) ] = true + content_ids[ minetest.get_content_id( "default:water_source" ) ] = true + end + if ignore_lava then + content_ids[ minetest.get_content_id( "default:lava_flowing" ) ] = true + content_ids[ minetest.get_content_id( "default:lava_source" ) ] = true + end + if ignore_air then + content_ids[ minetest.get_content_id( "air" ) ] = true + end + + local node_total, lock_count = lock_area( meta, pos, content_ids ) + minetest.chat_send_player( player_name, string.format( "Protection area updated (%d of %d nodes locked).", lock_count, node_total ) ) + + elseif fields.unlock then + unlock_area( meta ) + minetest.chat_send_player( player_name, "Protection area updated (all nodes unlocked)." ) + end + end + + minetest.create_form( pos, player_name, get_formspec( ), on_close ) +end + +--------------------------- +-- Protection Handlers +--------------------------- + +-- Info Level: +-- 0 for no info +-- 1 for "This area is owned by !" if you can't dig +-- 2 for "This area is owned by . +-- 3 for checking protector overlaps + +local function can_dig( radius, target_pos, player_name, is_strict, info_level ) + if not player_name then return false end + + -- Privileged users can override protection + if minetest.check_player_privs( player_name, "superuser" ) and info_level == 1 then + return true + end + + if info_level == 3 then info_level = 1 end + + local pos_list = minetest.find_nodes_in_area( + vector.subtract( target_pos, radius or protector_radius ), + vector.add( target_pos, radius or protector_radius ), + { "protector:protect", "protector:protect2", "protector:protect3" } + ) + + for _, pos in pairs( pos_list ) do + local meta = minetest.get_meta( pos ) + local owner = meta:get_string( "owner" ) + local members = meta:get_string( "members" ) + local is_public = minetest.get_node( pos ).name == "protector:protect3" + + if owner ~= player_name then + + if is_strict or not is_public and not is_member( meta, player_name ) or is_area_locked_at( meta, pos, target_pos ) then + if info_level == 1 then + minetest.chat_send_player( player_name, "This area is owned by " .. owner .. "!" ) + elseif info_level == 2 then + minetest.chat_send_player( player_name, "This area is owned by " .. owner .. "." ) + end + + if members ~= "" then + minetest.chat_send_player( player_name, "Protector located at " .. minetest.pos_to_string( pos ) .. " with members: " .. members .. "." ) + else + minetest.chat_send_player( player_name, "Protector located at " .. minetest.pos_to_string( pos ) .. "." ) + end + return false + end + end + + if info_level == 2 then + minetest.chat_send_player( player_name, "This area is owned by " .. owner .. "." ) + + if members ~= "" then + minetest.chat_send_player( player_name, "Protector located at " .. minetest.pos_to_string( pos ) .. " with members: " .. members .. "." ) + else + minetest.chat_send_player( player_name, "Protector located at " .. minetest.pos_to_string( pos ) .. "." ) + end + return false + end + end + + if info_level == 2 then + if #positions == 0 then + minetest.chat_send_player( player_name, "This area is not protected." ) + end + minetest.chat_send_player( digger, "You can build here." ) + end + + return true +end + +local old_is_protected = minetest.is_protected + +minetest.is_protected = function ( pos, player_name ) + local owner = minetest.get_meta( pos ):get_string( "owner" ) + local player = minetest.get_player_by_name( player_name ) + + -- owner = _anybody -> always FALSE + -- owner = -> always FALSE + -- owner = _somebody -> always TRUE + -- owner = -> always TRUE + -- owner = _nobody / null -> return protection + + -- never allow digging of nodes owned by stranger or by OWNER_SOMEBODY (strictly private ownership) + -- always allow digging of nodes owned by digger or by OWNER_ANYBODY (strictly public ownership) + -- otherwise verify protection rules for nodes owned by OWNER_NOBODY (default ownership, when undefined) + + -- prevent long-range dig exploit (evading protectors in unloaded areas) + if vector.distance( pos, player:getpos( ) ) > max_tool_range then + return true + end + + if owner == player_name or owner == OWNER_ANYBODY or owner == OWNER_NOBODY and can_dig( protector_radius, pos, player_name, false, 1 ) then + return old_is_protected( pos, player_name ) + else + return true + end +end + +--------------------------- +-- Anti-Grief Hooks +--------------------------- + +local function allow_place( target_pos, player_name, node_name ) + if is_superuser( player_name ) then return true end + + local pos_list = minetest.find_nodes_in_area( + vector.subtract( target_pos, protector_radius ), + vector.add( target_pos, protector_radius ), + { "protector:protect3" } + ) + + for _, pos in pairs( pos_list ) do + local meta = minetest.get_meta( pos ) + local allow_doors = meta:get_string( "allow_doors" ) == "true" + local allow_chests = meta:get_string( "allow_chests" ) == "true" + + -- check restrictions of public protector + if node_name == "default:chest_locked" and not allow_chests or node_name == "doors:door_steel" and not allow_doors then + return false + end + end + return true +end + +minetest.override_item( "default:chest_locked", { + allow_place = function ( target_pos, player ) + local player_name = player:get_player_name( ) + if not allow_place( target_pos, player_name, "default:chest_locked" ) then + minetest.chat_send_player( player_name, "You are not allowed to place locked chests here!" ) + return false + end + return true + end +} ) + +minetest.override_item( "doors:door_steel", { + allow_place = function ( target_pos, player ) + local player_name = player:get_player_name( ) + if not allow_place( target_pos, player_name, "doors:door_steel" ) then + minetest.chat_send_player( player_name, "You are not allowed to place steel doors here!" ) + return false + end + return true + end +} ) + +--------------------------- +-- Tool Definitions +--------------------------- + +minetest.register_tool( "protector:protection_wand", { + description = "Protection Wand", + range = 5, + inventory_image = "protector_wand.png", + on_use = function( itemstack, player, pointed_thing ) + local pos = pointed_thing.under or vector.round( vector.offset_y( player:getpos( ) ) ) -- if pointing at air, get player position instead + local player_name = player:get_player_name( ) + + -- find the protector nodes + local pos_list = minetest.find_nodes_in_area( + vector.subtract( pos, protector_radius ), + vector.add( pos, protector_radius ), + { "protector:protect", "protector:protect2", "protector:protect3" } + ) + + if #pos_list == 0 then + minetest.chat_send_player( player_name, "This area is not protected." ) + return + end + for i = 1, math.min( 5, #pos_list ) do + local owner = minetest.get_meta( pos_list[ i ] ):get_string( "owner" ) or "" + local members = minetest.get_meta( pos_list[ i ] ):get_string( "members" ) or "" + + if i == 1 then + minetest.chat_send_player( player_name, "This area is owned by " .. owner .. "." ) + end + if members ~= "" then + minetest.chat_send_player( player_name, "Protector located at " .. minetest.pos_to_string( pos_list[ i ] ) .. " with members: " .. members .. "." ) + else + minetest.chat_send_player( player_name, "Protector located at " .. minetest.pos_to_string( pos_list[ i ] ) .. "." ) + end + end + end, +} ) + +minetest.register_craftitem( "protector:protection_mask", { + description = "Protection Mask (Point to protector and use)", + inventory_image = "protector_mask.png", + wield_image = "protector_mask.png", + groups = { flammable = 3 }, + + on_use = function( cur_stack, player, pointed_thing ) + if pointed_thing.type == "node" then + local player_name = player:get_player_name( ) + local player_inv = player:get_inventory( ) + local node_meta = minetest.get_meta( pointed_thing.under ) + local node_name = minetest.get_node( pointed_thing.under ).name + + if node_name ~= "protector:protect" and node_name ~= "protector:protect2" and node_name ~= "protector:protect3" then + return cur_stack + end + + -- only owner of protector is permitted to copy mask + if not is_superuser( player_name ) and not is_owner( node_meta, player_name ) then + minetest.chat_send_player( player_name, "Access denied. Failed to copy protection mask." ) + return cur_stack + end + + local new_stack = ItemStack( "protector:protection_mask_saved" ) + local new_stack_meta = new_stack:get_meta( ) + + local bitmap = get_bitmap_raw( node_meta ) or create_bitmap( true ) + local node_total, lock_count = set_bitmap_raw( new_stack_meta, bitmap ) + local title = string.format( "%d of %d nodes locked", lock_count, node_total ) + + new_stack_meta:set_string( "title", title ) + new_stack_meta:set_string( "description", "Protection Mask (" .. title .. ")" ) + new_stack_meta:set_string( "owner", player_name ) + + minetest.chat_send_player( player_name, "Protection mask copied (" .. title .. ")" ) + + cur_stack:take_item( ) + + if cur_stack:is_empty( ) then + cur_stack:replace( new_stack ) + elseif player_inv:room_for_item( "main", new_stack ) then + player_inv:add_item( "main", new_stack ) + else + minetest.add_item( player:getpos( ), new_stack ) + end + end + return cur_stack + end +} ) + +minetest.register_craftitem( "protector:protection_mask_saved", { + inventory_image = "protector_mask_saved.png", + wield_image = "protector_mask_saved.png", + stack_max = 1, + groups = { flammable = 3, not_in_creative_inventory = 1 }, + + on_use = function( cur_stack, player, pointed_thing ) + if pointed_thing.type == "node" then + local player_name = player:get_player_name( ) + local node_meta = minetest.get_meta( pointed_thing.under ) + local node_name = minetest.get_node( pointed_thing.under ).name + + if node_name ~= "protector:protect" and node_name ~= "protector:protect2" and node_name ~= "protector:protect3" then + return cur_stack + end + + -- only owner of protector is permitted to paste mask + if not is_superuser( player_name ) and not is_owner( node_meta, player_name ) then + minetest.chat_send_player( player_name, "Access denied. Failed to paste protection mask." ) + return cur_stack + end + + local cur_stack_meta = cur_stack:get_meta( ) + local bitmap = get_bitmap_raw( cur_stack_meta ) + local node_total, lock_count = set_bitmap_raw( node_meta, bitmap ) + + minetest.chat_send_player( player_name, string.format( "Protection mask pasted (%d of %d nodes locked).", lock_count, node_total ) ) + end + + return cur_stack + end +} ) + +--------------------------- +-- Node Definitions +--------------------------- + +local function on_place( itemstack, placer, pointed_thing ) + if pointed_thing.type == "node" then + if not can_dig( protector_radius * 2, pointed_thing.above, placer:get_player_name( ), true, 3 ) then + minetest.chat_send_player( placer:get_player_name( ), "Overlaps into above player's protected area." ) + else + return minetest.item_place( itemstack, placer, pointed_thing ) + end + end + return itemstack +end + +minetest.register_node( "protector:protect", { + description = "Protection Stone (Shared)", + tiles = { + "protector_top1.png", + "protector_top1.png", + "protector_side1.png" + }, + sounds = default.node_sound_stone_defaults( ), + groups = { dig_immediate = 2, unbreakable = 1 }, + is_ground_content = false, + paramtype = "light", + light_source = 4, + + can_dig = function( pos, player ) + return can_dig( 1, pos, player:get_player_name( ), true, 1 ) + end, + + on_place = on_place, + + after_place_node = function( pos, placer ) + local meta = minetest.get_meta( pos ) + local player_name = placer:get_player_name( ) or "singleplayer" + + meta:set_string( "owner", player_name ) + meta:set_string( "infotext", "Protection (owned by " .. player_name .. ")" ) + end, + + on_rightclick = function( pos, node, clicker, itemstack ) + local meta = minetest.get_meta( pos ) + local player_name = clicker:get_player_name( ) + + if is_owner( meta, player_name ) or is_superuser( player_name ) then + open_shared_editor( pos, meta, player_name ) + end + end, + + on_punch = function( pos, node, puncher ) + local meta = minetest.get_meta( pos ) + local player_name = puncher:get_player_name( ) + + if is_owner( meta, player_name ) or is_superuser( player_name ) then + minetest.add_entity( pos, "protector:display") + end + end, + + on_blast = function() end, +} ) + +minetest.register_node( "protector:protect2", { + description = "Protection Badge (Shared)", + tiles = { "protector_logo.png" }, + wield_image = "protector_logo.png", + inventory_image = "protector_logo.png", + sounds = default.node_sound_stone_defaults( ), + groups = { dig_immediate = 2, unbreakable = 1 }, + paramtype = "light", + paramtype2 = "wallmounted", + legacy_wallmounted = true, + light_source = 4, + drawtype = "nodebox", + sunlight_propagates = true, + walkable = false, + node_box = { + type = "wallmounted", + wall_top = { -0.375, 0.4375, -0.5, 0.375, 0.5, 0.5 }, + wall_bottom = { -0.375, -0.5, -0.5, 0.375, -0.4375, 0.5 }, + wall_side = { -0.5, -0.5, -0.375, -0.4375, 0.5, 0.375 }, + }, + selection_box = { type = "wallmounted" }, + + can_dig = function( pos, player ) + return can_dig( 1, pos, player:get_player_name( ), true, 1 ) + end, + + on_place = on_place, + + after_place_node = function( pos, placer ) + local meta = minetest.get_meta( pos ) + local player_name = placer:get_player_name( ) or "singleplayer" + + meta:set_string( "owner", player_name ) + meta:set_string( "infotext", "Protection (owned by " .. player_name .. ")" ) + meta:set_string( "members", "" ) + end, + + on_rightclick = function( pos, node, clicker, itemstack ) + local meta = minetest.get_meta( pos ) + local player_name = clicker:get_player_name( ) + + if is_owner( meta, player_name ) or is_superuser( player_name ) then + open_shared_editor( pos, meta, player_name ) + end + end, + + on_punch = function( pos, node, puncher ) + local meta = minetest.get_meta( pos ) + local player_name = puncher:get_player_name( ) + + if is_owner( meta, player_name ) or is_superuser( player_name ) then + minetest.add_entity( pos, "protector:display" ) + end + end, + + on_blast = function( ) end, +} ) + +minetest.register_node( "protector:protect3", { + description = "Protection Stone (Public)", + tiles = { + "protector_top2.png", + "protector_top2.png", + "protector_side2.png" + }, + sounds = default.node_sound_stone_defaults( ), + groups = { dig_immediate = 2, unbreakable = 1 }, + is_ground_content = false, + paramtype = "light", + light_source = 4, + + can_dig = function( pos, player ) + return can_dig( 1, pos, player:get_player_name( ), true, 1 ) + end, + + on_place = on_place, + + after_place_node = function( pos, placer ) + local meta = minetest.get_meta( pos ) + local player_name = placer:get_player_name( ) or "singleplayer" + + meta:set_string( "owner", player_name ) + meta:set_string( "infotext", "Protection (owned by " .. player_name .. ")" ) + meta:set_string( "allow_doors", "false" ) + meta:set_string( "allow_chests", "false" ) + + local node_total, lock_count = lock_area( meta, pos, { + [minetest.get_content_id( "default:water_flowing" )] = true, + [minetest.get_content_id( "default:water_source" )] = true, + [minetest.get_content_id( "default:lava_flowing" )] = true, + [minetest.get_content_id( "default:lava_source" )] = true, + [minetest.get_content_id( "air" )] = true + } ) + minetest.chat_send_player( player_name, string.format( "Protection area updated (%d of %d nodes locked).", lock_count, node_total ) ) + end, + + on_rightclick = function( pos, node, clicker, itemstack ) + local meta = minetest.get_meta( pos ) + local player_name = clicker:get_player_name( ) + + if is_owner( meta, player_name ) or is_superuser( player_name ) then + open_public_editor( pos, meta, player_name ) + end + end, + + on_punch = function( pos, node, puncher ) + local meta = minetest.get_meta( pos ) + local player_name = puncher:get_player_name( ) + + if is_owner( meta, player_name ) or is_superuser( player_name ) then + minetest.add_entity( pos, "protector:display" ) + end + end, + + on_blast = function() end, +} ) + +--------------------------- +-- Entity Definition +--------------------------- + +minetest.register_entity( "protector:display", { + physical = false, + collisionbox = { 0, 0, 0, 0, 0, 0 }, + visual = "wielditem", + -- wielditem is scaled to 1.5 times original node size? + visual_size = { x = 1.0 / 1.5, y = 1.0 / 1.5 }, + textures = { "protector:display_node" }, + timer = 0, + + on_step = function( self, dtime ) + self.timer = self.timer + dtime + + if self.timer > 7 then + self.object:remove( ) + end + end, +} ) + +local r = protector_radius + +-- NB: this node definition is only a basis for the entity above +minetest.register_node( "protector:display_node", { + tiles = { "protector_display.png" }, + use_texture_alpha = true, + walkable = false, + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = { + -- west face + { -r + 0.55, -r + 0.55, -r + 0.55, -r + 0.45, r + 0.55, r + 0.55 }, + -- north face + { -r + 0.55, -r + 0.55, r + 0.45, r + 0.55, r + 0.55, r + 0.55 }, + -- east face + { r + 0.45, -r + 0.55, -r + 0.55, r + 0.55, r + 0.55, r + 0.55 }, + -- south face + { -r + 0.55, -r + 0.55, -r + 0.55, r + 0.55, r + 0.55, -r + 0.45 }, + -- top face + { -r + 0.55, r + 0.45, -r + 0.55, r + 0.55, r + 0.55, r + 0.55 }, + -- bottom face + { -r + 0.55, -r + 0.55, -r + 0.55, r + 0.55, -r + 0.45, r +0.55 }, + -- center (surround protector) + { -0.55, -0.55, -0.55, 0.55, 0.55, 0.55 }, + }, + }, + selection_box = { + type = "regular", + }, + paramtype = "light", + groups = { dig_immediate = 3, not_in_creative_inventory = 1 }, + drop = "", +} ) + +--------------------------- +-- Crafting Recipes +--------------------------- + +minetest.register_craft( { + output = "protector:protect3", + recipe = { + { "default:stone", "default:stone", "default:stone" }, + { "default:stone", "default:mese", "default:stone" }, + { "default:stone", "default:stone", "default:stone" }, + } +} ) + +minetest.register_craft( { + output = "protector:protect2", + recipe = { + { "default:stone", "default:copper_ingot", "default:stone" }, + { "default:copper_ingot", "default:mese", "default:copper_ingot" }, + { "default:stone", "default:copper_ingot", "default:stone" }, + } +} ) + +minetest.register_craft( { + output = "protector:protect", + recipe = { + { "default:stone", "default:steel_ingot", "default:stone" }, + { "default:steel_ingot", "default:mese", "default:steel_ingot" }, + { "default:stone", "default:steel_ingot", "default:stone" }, + } +} ) + +minetest.register_craft( { + output = "protector:protection_wand", + recipe = { + { "default:mese_crystal" }, + { "default:stick" }, + } +} ) + +minetest.register_craft( { + output = "protector:protection_mask", + recipe = { + { "", "default:steel_ingot", "" }, + { "default:steel_ingot", "default:mese_crystal", "default:steel_ingot" }, + { "", "default:steel_ingot", "" }, + } +} ) + diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..5546eb4 --- /dev/null +++ b/mod.conf @@ -0,0 +1,4 @@ +name = protector +title = Protector Redux +author = sorcerykid +license = MIT diff --git a/textures/protector_display.png b/textures/protector_display.png new file mode 100644 index 0000000..bdc9284 Binary files /dev/null and b/textures/protector_display.png differ diff --git a/textures/protector_logo.png b/textures/protector_logo.png new file mode 100644 index 0000000..b9ac3d6 Binary files /dev/null and b/textures/protector_logo.png differ diff --git a/textures/protector_mask.png b/textures/protector_mask.png new file mode 100644 index 0000000..88bd042 Binary files /dev/null and b/textures/protector_mask.png differ diff --git a/textures/protector_mask_saved.png b/textures/protector_mask_saved.png new file mode 100644 index 0000000..0235be6 Binary files /dev/null and b/textures/protector_mask_saved.png differ diff --git a/textures/protector_side1.png b/textures/protector_side1.png new file mode 100644 index 0000000..899d956 Binary files /dev/null and b/textures/protector_side1.png differ diff --git a/textures/protector_side2.png b/textures/protector_side2.png new file mode 100644 index 0000000..e9d9b99 Binary files /dev/null and b/textures/protector_side2.png differ diff --git a/textures/protector_top1.png b/textures/protector_top1.png new file mode 100644 index 0000000..638ef26 Binary files /dev/null and b/textures/protector_top1.png differ diff --git a/textures/protector_top2.png b/textures/protector_top2.png new file mode 100644 index 0000000..1782c00 Binary files /dev/null and b/textures/protector_top2.png differ diff --git a/textures/protector_wand.png b/textures/protector_wand.png new file mode 100644 index 0000000..238aab1 Binary files /dev/null and b/textures/protector_wand.png differ