From 46999c1eda91e05d4b6f8bc36f853894e1f7eee4 Mon Sep 17 00:00:00 2001 From: Leslie Krause Date: Tue, 9 Apr 2019 10:35:00 -0400 Subject: [PATCH] Build 01 - initial beta version - included support files for public release --- README.txt | 129 +++++ depends.txt | 2 + description.txt | 2 + init.lua | 920 ++++++++++++++++++++++++++++++ mod.conf | 4 + textures/protector_display.png | Bin 0 -> 163 bytes textures/protector_logo.png | Bin 0 -> 862 bytes textures/protector_mask.png | Bin 0 -> 225 bytes textures/protector_mask_saved.png | Bin 0 -> 224 bytes textures/protector_side1.png | Bin 0 -> 727 bytes textures/protector_side2.png | Bin 0 -> 992 bytes textures/protector_top1.png | Bin 0 -> 668 bytes textures/protector_top2.png | Bin 0 -> 267 bytes textures/protector_wand.png | Bin 0 -> 415 bytes 14 files changed, 1057 insertions(+) create mode 100644 README.txt create mode 100644 depends.txt create mode 100644 description.txt create mode 100644 init.lua create mode 100644 mod.conf create mode 100644 textures/protector_display.png create mode 100644 textures/protector_logo.png create mode 100644 textures/protector_mask.png create mode 100644 textures/protector_mask_saved.png create mode 100644 textures/protector_side1.png create mode 100644 textures/protector_side2.png create mode 100644 textures/protector_top1.png create mode 100644 textures/protector_top2.png create mode 100644 textures/protector_wand.png 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 0000000000000000000000000000000000000000..bdc928407c46fb59c0b53879d5211c05e226fe4f GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPNP|!@0X}05cppb#5i(`nz>Eu85&572`tPQP=jg1eh z9@qO9nPgux&Pz{PAkZUakab`Mqrd@=%rZ6x?i!B0p{}pi1NAa^y85}Sb4q9e0GG2Z AtpET3 literal 0 HcmV?d00001 diff --git a/textures/protector_logo.png b/textures/protector_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b9ac3d679be508ca1aad47f58f2b08d9e8acff81 GIT binary patch literal 862 zcmV-k1EKthP)1O{PXU?`L&K^IDds7X#uBQ6X*^F?Dkv@99eLP6ylFewT-H{^$2_9-adP z2oNB^He9GEyI59I*b0E}&ADQIL7Tqt%ynNl1WAI$PidO&i&v|&$}38LZOSMjWoc1S zVVRp7bC2Z=C#d7HLuDad5|QW~0A{;zYNr;NhteAp_9PUyzW;bHQ4?q0pL+00C=;oj zGtW_gZQM9D(=a+R#ygc?aQpZ8M0c;DKW`_wI}!+{X*MVCXv=`1k3L(Xk6y701xBM8 zlM@cCt}KAh_aG%n1yvYvcXfBI>`m5M484Z)HmCEuPXs86x(Z-6RimuqnAoJnB{xoH z??ZmN0s`34+%ba>o#SxXtw@q{OT&NWuMv)u5#X86o&W$!i_&L5k9eZ$t8#I!{5U*T z2kfQ+2*eylrp3t6*Mn7K%`j$;0N~h3CeIszf72E!G&mUR8ykA=0^*tj&t-*kbpt%d8u(HLz;h#7B_jYlei2x(E-p%h zK?WIoQ|DaJhi? zuj5FFiNK6$dC;3ouLNe7ocFVIF`Pa2 zIJdE}aqgTMH;y-$o133Hb!x?m71wr$O>PP8DmHj{bA5;Vk#3MdB|(0{4F6HVfq-sj zpq#&_i(^Q|t>lCQY%_Ql^jy8IU~;|x8jC@4?dgj~8xHV^_a`taG+Q4(`hF49go9~P zpTmTYB#8IzHsMq`8j!JKCC9a+dOU&*T$+d02pq^VW;lC@fA)*c1N(qRGkCiCxvXULF~!XDzV9>hJR|PS&v}GE2myfi9)ECv z_a3|lAhQ-JB>*{#oG%1{QW_y3F$S#^UFRrODkPl_07`3A9Os2XV3Gu_<6aX8);HDx z*xYR93h;!LO0J;CQc58JlcoSz=a@P^2|(bzr&_HNhSkF&pOgBUMZO&F^+?@rmmsW$ zc~|T3cxJ9|BW1v~n@hZUzQn+Q;KIc#tbKR^!1A*?0AUysh9N;H!`?=&KNQN75@gmgRv)82T0;mpd&bjl@6l)sVoV0W&tGZLAkEU=BmM>fK_n%{B&1o2iXt98 zX%u9W=84p*QKYbsP=??=f!3PD7_4xxYD;anHzx&Ta!j{_c?SyZGj$qqsP6HQ;f zL}E-YC?2IWCN&&ImSxoHHH=Ae8{6#;Hnaat?h}CG{3xuo`~$vrB|eM=ebxW~002ov JPDHLkV1nNeH%9;f literal 0 HcmV?d00001 diff --git a/textures/protector_side2.png b/textures/protector_side2.png new file mode 100644 index 0000000000000000000000000000000000000000..e9d9b993a47d4963ed8fa6d9d2bd4a87b3c44448 GIT binary patch literal 992 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GXl4m>B|mLR^z#W8$MC1AM#;jP%2U z0=?Z_O$_wytSk$2vLi!+Q{vz%uBhJlH}FTht@ zU2Wcixj-{V!Dt8!#Sl=seEcZT-LpMi978H@>H2pI9aiAz=3u&2zWeY0<$P~;2zN|( zV7Oo%;PtSAwLx-CwC3-9?&2LQwyqLXT6Io&;cYtYylJe=Q*w=}x z#H`hlZ*FVN;5nyzaKrO`^AEekolNj@D`EP5{=hD`RXfEbFVdQ&MBb@0HP*v APyhe` literal 0 HcmV?d00001 diff --git a/textures/protector_top1.png b/textures/protector_top1.png new file mode 100644 index 0000000000000000000000000000000000000000..638ef26c497cad89048baaad6349b1c1c4743c5d GIT binary patch literal 668 zcmV;N0%QG&P)lz8@@6Y7;3pFqI@{2tHs;1%NdMxfTn69x29l z!1j0mz^j)v)(-l(5D?o$Y!lXwi7Aycmf0eIFD^KqCn~*JI&{hFkpSvD6G9W%o!0me zkWGy2PIsA3YJ`Btk1~tpioLtrc;5i<<7em#geG)LJOKf+(i-m_p$V9(;=`xC9_&L= zq&0iJ3fHEzf}D}oTAcU9n20g;npj&(Eh)7C+}qy=GO>x6%HZ8Oa{Y;3Jb#Asz7tBu z7>f^{o7OZ9^|Z!&SI)RtEQwA0uk#uJ^zoY(W8@#MXcavm-&MZ=0000T literal 0 HcmV?d00001 diff --git a/textures/protector_top2.png b/textures/protector_top2.png new file mode 100644 index 0000000000000000000000000000000000000000..1782c00aa34a81df3a8c2beb3b58b7dfa4b372f0 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFP2=EDU)mB$C*4NWF)H5;AH!#w- zv$FJda}DtE3J(g53<-{picE@)Nr{ik%giXu$*!#^Uwvd+AyDr$PZ!4!j_aa52RWGx z1zexMy2$6Up=nJ5V>a`q$$XPGZ;zF}(AprUdZS?p({%n~@gsa+G@Mk*1bW)bf4=|B zVct^Au|VbfhYGJnVY-pCj|CsMnP>WSV$D;f0}GmJjNNyI@zq+FPKk(X&*_eOdrdjx zXT6@oTh7VnJdNaU-JWsZcDify;+-XnW^InjQTBAvw7&3+h36ix{ZA*a%^wOm+kq}* N@O1TaS?83{1OOd;V<7+l literal 0 HcmV?d00001 diff --git a/textures/protector_wand.png b/textures/protector_wand.png new file mode 100644 index 0000000000000000000000000000000000000000..238aab1b9f7c3641312b0ca3c088831fb73d9faf GIT binary patch literal 415 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP~s~C^DiB`K_4^T+5#5JPCIX^cyHLrxhxhOTUBsE2$ zJhLQ2!QIn0AiR-JoPmLn-P6S}#NzbQiyMuc0|kzKynl~Hv!}_c>&yk|Sg+RS0ToQ5 zTQ9DzU~GN z+fq#2PejRFe|F|zLCXJ*y#C+2*DqRYH&at7q_=MtQ`xrgJ8^Y;@4PLnjnO_gM{~m2 zN@L5Lv&#%-+9$meJ8&~mo&E5{xo<@aiVbbuTh7eU4`}yaAGdhs|9{QDr0RLEul>dU z$N9l}gGImidouJH+;z8UM+oF4mOc}E<}i61_nx-1{!2RB9s542*Z{+g!PC{xWt~$( F6968Grq%!e literal 0 HcmV?d00001