640 lines
17 KiB
Lua
640 lines
17 KiB
Lua
--[[
|
|
Replacement tool for creative building (Mod for MineTest)
|
|
Copyright (C) 2013 Sokomine
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
--]]
|
|
|
|
-- Version 3.0
|
|
|
|
-- Changelog:
|
|
-- 09.12.2017 * Got rid of outdated minetest.env
|
|
-- * Fixed error in protection function.
|
|
-- * Fixed minor bugs.
|
|
-- * Added blacklist
|
|
-- 02.10.2014 * Some more improvements for inspect-tool. Added craft-guide.
|
|
-- 01.10.2014 * Added inspect-tool.
|
|
-- 12.01.2013 * If digging the node was unsuccessful, then the replacement will now fail
|
|
-- (instead of destroying the old node with its metadata; i.e. chests with content)
|
|
-- 20.11.2013 * if the server version is new enough, minetest.is_protected is used
|
|
-- in order to check if the replacement is allowed
|
|
-- 24.04.2013 * param1 and param2 are now stored
|
|
-- * hold sneak + right click to store new pattern
|
|
-- * right click: place one of the itmes
|
|
-- * receipe changed
|
|
-- * inventory image added
|
|
|
|
local path = minetest.get_modpath"replacer"
|
|
|
|
-- adds a function to check ownership of a node; taken from VanessaEs homedecor mod
|
|
dofile(path.."/check_owner.lua")
|
|
|
|
replacer = {}
|
|
|
|
replacer.blacklist = {};
|
|
|
|
-- playing with tnt and creative building are usually contradictory
|
|
-- (except when doing large-scale landscaping in singleplayer)
|
|
replacer.blacklist[ "tnt:boom"] = true;
|
|
replacer.blacklist[ "tnt:gunpowder"] = true;
|
|
replacer.blacklist[ "tnt:gunpowder_burning"] = true;
|
|
replacer.blacklist[ "tnt:tnt"] = true;
|
|
|
|
-- prevent accidental replacement of your protector
|
|
replacer.blacklist[ "protector:protect"] = true;
|
|
replacer.blacklist[ "protector:protect2"] = true;
|
|
|
|
-- adds a tool for inspecting nodes and entities
|
|
dofile(path.."/inspect.lua")
|
|
|
|
local function inform(name, msg)
|
|
minetest.chat_send_player(name, msg)
|
|
minetest.log("info", "[replacer] "..name..": "..msg)
|
|
end
|
|
|
|
local mode_infos = {
|
|
single = "Replace single node.",
|
|
field = "Left click: Replace field of nodes of a kind where a translucent node is in front of it. Right click: Replace field of air where no translucent node is behind the air.",
|
|
crust = "Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air. Right click: Replace air nodes which touch the crust",
|
|
chunkborder = "TODO",
|
|
}
|
|
local mode_colours = {
|
|
single = "#ffffff",
|
|
field = "#54FFAC",
|
|
crust = "#9F6200",
|
|
chunkborder = "#FF5457",
|
|
}
|
|
local modes = {"single", "field", "crust", "chunkborder"}
|
|
for n = 1,#modes do
|
|
modes[modes[n]] = n
|
|
end
|
|
|
|
local function get_data(stack)
|
|
local daten = stack:get_meta():get_string"replacer":split" " or {}
|
|
return {
|
|
name = daten[1] or "default:dirt",
|
|
param1 = tonumber(daten[2]) or 0,
|
|
param2 = tonumber(daten[3]) or 0
|
|
},
|
|
modes[daten[4]] and daten[4] or modes[1]
|
|
end
|
|
|
|
local function set_data(stack, node, mode)
|
|
mode = mode or modes[1]
|
|
local metadata = (node.name or "default:dirt") .. " "
|
|
.. (node.param1 or 0) .. " "
|
|
.. (node.param2 or 0) .." "
|
|
.. mode
|
|
local meta = stack:get_meta()
|
|
meta:set_string("replacer", metadata)
|
|
meta:set_string("color", mode_colours[mode])
|
|
stack:set_metadata(metadata)
|
|
return metadata
|
|
end
|
|
|
|
minetest.register_tool("replacer:replacer", {
|
|
description = "Node replacement tool",
|
|
inventory_image = "replacer_replacer.png",
|
|
stack_max = 1, -- it has to store information - thus only one can be stacked
|
|
liquids_pointable = true, -- it is ok to painit in/with water
|
|
--node_placement_prediction = nil,
|
|
metadata = "default:dirt", -- default replacement: common dirt
|
|
|
|
on_place = function(itemstack, placer, pt)
|
|
if not placer
|
|
or not pt then
|
|
return
|
|
end
|
|
|
|
local keys = placer:get_player_control()
|
|
local name = placer:get_player_name()
|
|
local creative_enabled = creative.is_enabled_for(name)
|
|
local has_give = minetest.check_player_privs(name, "give")
|
|
local modes_available = has_give or creative_enabled
|
|
|
|
if keys.aux1
|
|
and modes_available then
|
|
-- Change Mode when holding the fast key
|
|
local node, mode = get_data(itemstack)
|
|
mode = modes[modes[mode]%#modes+1]
|
|
set_data(itemstack, node, mode)
|
|
inform(name, "Mode changed to: "..mode..": "..mode_infos[mode])
|
|
return itemstack
|
|
end
|
|
|
|
-- If not holding shift, place node(s)
|
|
if not keys.sneak then
|
|
return replacer.replace(itemstack, placer, pt, true)
|
|
end
|
|
|
|
-- Select new node
|
|
if pt.type ~= "node" then
|
|
inform(name, "Error: No node selected.")
|
|
return
|
|
end
|
|
|
|
local node, mode = get_data(itemstack)
|
|
node = minetest.get_node_or_nil(pt.under) or node
|
|
|
|
if not modes_available then
|
|
mode = "single"
|
|
end
|
|
|
|
local inv = placer:get_inventory()
|
|
if not (creative_enabled and has_give)
|
|
and not inv:contains_item("main", node.name) then
|
|
if creative_enabled then
|
|
if minetest.get_item_group(node.name,
|
|
"not_in_creative_inventory") > 0 then
|
|
-- search for a drop available in creative inventory
|
|
local found_item = false
|
|
local drops = minetest.get_node_drops(node.name)
|
|
for i = 1,#drops do
|
|
local name = drops[i]
|
|
if minetest.registered_nodes[name]
|
|
and minetest.get_item_group(name,
|
|
"not_in_creative_inventory") == 0 then
|
|
node.name = name
|
|
found_item = true
|
|
break
|
|
end
|
|
end
|
|
if not found_item then
|
|
inform(name, "Node not in creative invenotry: \"" ..
|
|
node.name .. "\".")
|
|
return
|
|
end
|
|
end
|
|
else
|
|
local found_item = false
|
|
-- search for a drop that the player has if possible
|
|
local drops = minetest.get_node_drops(node.name)
|
|
for i = 1,#drops do
|
|
local name = drops[i]
|
|
if minetest.registered_nodes[name]
|
|
and inv:contains_item("main", name) then
|
|
node.name = name
|
|
found_item = true
|
|
break
|
|
end
|
|
end
|
|
if not found_item then
|
|
-- search for a drop available in creative inventory
|
|
-- that first configuring the replacer,
|
|
-- then digging the nodes works
|
|
for i = 1,#drops do
|
|
local name = drops[i]
|
|
if minetest.registered_nodes[name]
|
|
and minetest.get_item_group(name,
|
|
"not_in_creative_inventory") == 0 then
|
|
node.name = name
|
|
found_item = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not found_item
|
|
and not has_give then
|
|
inform(name, "Item not in your inventory: '" .. node.name ..
|
|
"'.")
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
local metadata = set_data(itemstack, node, mode)
|
|
|
|
inform(name, "Node replacement tool set to: '" .. metadata .. "'.")
|
|
|
|
return itemstack --data changed
|
|
end,
|
|
|
|
|
|
-- on_drop = func(itemstack, dropper, pos),
|
|
|
|
on_use = function(...)
|
|
-- Replace nodes
|
|
return replacer.replace(...)
|
|
end,
|
|
})
|
|
|
|
local poshash = minetest.hash_node_position
|
|
|
|
-- cache results of minetest.get_node
|
|
local known_nodes = {}
|
|
local function get_node(pos)
|
|
local i = poshash(pos)
|
|
local node = known_nodes[i]
|
|
if node then
|
|
return node
|
|
end
|
|
node = minetest.get_node(pos)
|
|
known_nodes[i] = node
|
|
return node
|
|
end
|
|
|
|
-- tests if there's a node at pos which should be replaced
|
|
local function replaceable(pos, name, pname)
|
|
return get_node(pos).name == name
|
|
and not minetest.is_protected(pos, pname)
|
|
end
|
|
|
|
local trans_nodes = {}
|
|
local function node_translucent(name)
|
|
if trans_nodes[name] ~= nil then
|
|
return trans_nodes[name]
|
|
end
|
|
local data = minetest.registered_nodes[name]
|
|
if data
|
|
and (not data.drawtype or data.drawtype == "normal") then
|
|
trans_nodes[name] = false
|
|
return false
|
|
end
|
|
trans_nodes[name] = true
|
|
return true
|
|
end
|
|
|
|
local function field_position(pos, data)
|
|
return replaceable(pos, data.name, data.pname)
|
|
and node_translucent(
|
|
get_node(vector.add(data.above, pos)).name) ~= data.right_clicked
|
|
end
|
|
|
|
local offsets_touch = {
|
|
{x=-1, y=0, z=0},
|
|
{x=1, y=0, z=0},
|
|
{x=0, y=-1, z=0},
|
|
{x=0, y=1, z=0},
|
|
{x=0, y=0, z=-1},
|
|
{x=0, y=0, z=1},
|
|
}
|
|
|
|
-- 3x3x3 hollow cube
|
|
local offsets_hollowcube = {}
|
|
for x = -1,1 do
|
|
for y = -1,1 do
|
|
for z = -1,1 do
|
|
local p = {x=x, y=y, z=z}
|
|
if x ~= 0
|
|
or y ~= 0
|
|
or z ~= 0 then
|
|
offsets_hollowcube[#offsets_hollowcube+1] = p
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- To get the crust, firstly nodes near it need to be collected
|
|
local function crust_above_position(pos, data)
|
|
-- test if the node at pos is a translucent node and not part of the crust
|
|
local nd = get_node(pos).name
|
|
if nd == data.name
|
|
or not node_translucent(nd) then
|
|
return false
|
|
end
|
|
-- test if a node of the crust is near pos
|
|
for i = 1,26 do
|
|
local p2 = offsets_hollowcube[i]
|
|
if replaceable(vector.add(pos, p2), data.name, data.pname) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- used to get nodes the crust belongs to
|
|
local function crust_under_position(pos, data)
|
|
if not replaceable(pos, data.name, data.pname) then
|
|
return false
|
|
end
|
|
for i = 1,26 do
|
|
local p2 = offsets_hollowcube[i]
|
|
if data.aboves[poshash(vector.add(pos, p2))] then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- extract the crust from the nodes the crust belongs to
|
|
local function reduce_crust_ps(data)
|
|
local newps = {}
|
|
local n = 0
|
|
for i = 1,data.num do
|
|
local p = data.ps[i]
|
|
for i = 1,6 do
|
|
local p2 = offsets_touch[i]
|
|
if data.aboves[poshash(vector.add(p, p2))] then
|
|
n = n+1
|
|
newps[n] = p
|
|
break
|
|
end
|
|
end
|
|
end
|
|
data.ps = newps
|
|
data.num = n
|
|
end
|
|
|
|
-- gets the air nodes touching the crust
|
|
local function reduce_crust_above_ps(data)
|
|
local newps = {}
|
|
local n = 0
|
|
for i = 1,data.num do
|
|
local p = data.ps[i]
|
|
if replaceable(p, "air", data.pname) then
|
|
for i = 1,6 do
|
|
local p2 = offsets_touch[i]
|
|
if replaceable(vector.add(p, p2), data.name, data.pname) then
|
|
n = n+1
|
|
newps[n] = p
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
data.ps = newps
|
|
data.num = n
|
|
end
|
|
|
|
local function mantle_position(pos, data)
|
|
if not replaceable(pos, data.name, data.pname) then
|
|
return false
|
|
end
|
|
for i = 1,6 do
|
|
if get_node(vector.add(pos, offsets_touch[i])).name ~= data.name then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- finds out positions using depth first search
|
|
local function get_ps(pos, fdata, adps, max)
|
|
adps = adps or offsets_touch
|
|
|
|
local tab = {}
|
|
local num = 0
|
|
|
|
local todo = {pos}
|
|
local ti = 1
|
|
|
|
local tab_avoid = {}
|
|
|
|
while ti ~= 0 do
|
|
local p = todo[ti]
|
|
--~ todo[ti] = nil
|
|
ti = ti-1
|
|
|
|
for _,p2 in pairs(adps) do
|
|
p2 = vector.add(p, p2)
|
|
local i = poshash(p2)
|
|
if not tab_avoid[i]
|
|
and fdata.func(p2, fdata) then
|
|
|
|
num = num+1
|
|
tab[num] = p2
|
|
|
|
ti = ti+1
|
|
todo[ti] = p2
|
|
|
|
tab_avoid[i] = true
|
|
|
|
if max
|
|
and num >= max then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return tab, num, tab_avoid
|
|
end
|
|
|
|
-- replaces one node with another one and returns if it was successful
|
|
local function replace_single_node(pos, node, nnd, player, name, inv, creative)
|
|
if minetest.is_protected(pos, name) then
|
|
return false, "Protected at "..minetest.pos_to_string(pos)
|
|
end
|
|
|
|
if replacer.blacklist[node.name] then
|
|
return false, "Replacing blocks of the type '" ..
|
|
node.name ..
|
|
"' is not allowed on this server. Replacement failed."
|
|
end
|
|
|
|
-- do not replace if there is nothing to be done
|
|
if node.name == nnd.name then
|
|
-- only the orientation was changed
|
|
if node.param1 ~= nnd.param1
|
|
or node.param2 ~= nnd.param2 then
|
|
minetest.swap_node(pos, nnd)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- does the player carry at least one of the desired nodes with him?
|
|
if not creative
|
|
and not inv:contains_item("main", nnd.name) then
|
|
return false, "You have no further '"..(nnd.name or "?")..
|
|
"'. Replacement failed."
|
|
end
|
|
|
|
local ndef = minetest.registered_nodes[node.name]
|
|
if not ndef then
|
|
return false, "Unknown node: "..node.name
|
|
end
|
|
local new_ndef = minetest.registered_nodes[nnd.name]
|
|
if not new_ndef then
|
|
return false, "Unknown node should be placed: "..nnd.name
|
|
end
|
|
|
|
-- dig the current node if needed
|
|
if not ndef.buildable_to then
|
|
-- give the player the item by simulating digging if possible
|
|
minetest.node_dig(pos, node, player)
|
|
-- test if digging worked
|
|
local dug_node = minetest.get_node_or_nil(pos)
|
|
if not dug_node
|
|
or not minetest.registered_nodes[dug_node.name].buildable_to then
|
|
return false, "Couldn't dig '".. node.name .."' properly."
|
|
end
|
|
end
|
|
|
|
-- place the node similar to how a player does it
|
|
-- (other than the pointed_thing)
|
|
local newitem, succ = new_ndef.on_place(ItemStack(nnd.name), player,
|
|
{type = "node", under = vector.new(pos), above = vector.new(pos)})
|
|
if succ == false then
|
|
return false, "Couldn't place '" .. nnd.name .. "'."
|
|
end
|
|
|
|
-- update inventory in survival mode
|
|
if not creative then
|
|
-- consume the item
|
|
inv:remove_item("main", nnd.name.." 1")
|
|
-- if placing the node didn't result in empty stack…
|
|
if newitem:to_string() ~= "" then
|
|
inv:add_item("main", newitem)
|
|
end
|
|
end
|
|
|
|
-- test whether the placed node differs from the supposed node
|
|
local placed_node = minetest.get_node(pos)
|
|
if placed_node.name ~= nnd.name then
|
|
-- Sometimes placing doesn't put the node but does something different
|
|
-- e.g. when placing snow on snow with the snow mod
|
|
return true
|
|
end
|
|
|
|
-- fix orientation if needed
|
|
if placed_node.param1 ~= nnd.param1
|
|
or placed_node.param2 ~= nnd.param2 then
|
|
minetest.swap_node(pos, nnd)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- the function which happens when the replacer is used
|
|
function replacer.replace(itemstack, user, pt, right_clicked)
|
|
if not user
|
|
or not pt then
|
|
return
|
|
end
|
|
|
|
local name = user:get_player_name()
|
|
local creative_enabled = creative.is_enabled_for(name)
|
|
|
|
if pt.type ~= "node" then
|
|
inform(name, "Error: " .. pt.type .. " is not a node.")
|
|
return
|
|
end
|
|
|
|
local pos = minetest.get_pointed_thing_position(pt, right_clicked)
|
|
local node_toreplace = minetest.get_node_or_nil(pos)
|
|
|
|
if not node_toreplace then
|
|
inform(name, "Target node not yet loaded. Please wait a " ..
|
|
"moment for the server to catch up.")
|
|
return
|
|
end
|
|
|
|
local nnd, mode = get_data(itemstack)
|
|
if node_toreplace.name == nnd.name
|
|
and node_toreplace.param1 == nnd.param1
|
|
and node_toreplace.param2 == nnd.param2 then
|
|
inform(name, "Nothing to replace.")
|
|
return
|
|
end
|
|
|
|
if replacer.blacklist[nnd.name] then
|
|
minetest.chat_send_player(name, "Placing blocks of the type '" ..
|
|
nnd.name ..
|
|
"' with the replacer is not allowed on this server. " ..
|
|
"Replacement failed.")
|
|
return
|
|
end
|
|
|
|
if not creative_enabled
|
|
and not minetest.check_player_privs(name, "give") then
|
|
mode = "single"
|
|
end
|
|
|
|
if mode == "single" then
|
|
local succ,err = replace_single_node(pos, node_toreplace, nnd, user,
|
|
name, user:get_inventory(), creative_enabled)
|
|
|
|
if not succ then
|
|
inform(name, err)
|
|
end
|
|
return
|
|
end
|
|
|
|
local ps,num
|
|
if mode == "field" then
|
|
-- get connected positions for plane field replacing
|
|
local pdif = vector.subtract(pt.above, pt.under)
|
|
local adps,n = {},1
|
|
for _,i in pairs{"x", "y", "z"} do
|
|
if pdif[i] == 0 then
|
|
for a = -1,1,2 do
|
|
local p = {x=0, y=0, z=0}
|
|
p[i] = a
|
|
adps[n] = p
|
|
n = n+1
|
|
end
|
|
end
|
|
end
|
|
if right_clicked then
|
|
pdif = vector.multiply(pdif, -1)
|
|
end
|
|
right_clicked = right_clicked and true or false
|
|
ps,num = get_ps(pos, {func=field_position, name=node_toreplace.name,
|
|
pname=name, above=pdif, right_clicked=right_clicked}, adps, 8799)
|
|
elseif mode == "crust" then
|
|
local nodename_clicked = get_node(pt.under).name
|
|
local aps,n,aboves = get_ps(pt.above, {func=crust_above_position,
|
|
name=nodename_clicked, pname=name}, nil, 8799)
|
|
if aps then
|
|
if right_clicked then
|
|
local data = {ps=aps, num=n, name=nodename_clicked, pname=name}
|
|
reduce_crust_above_ps(data)
|
|
ps,num = data.ps, data.num
|
|
else
|
|
ps,num = get_ps(pt.under, {func=crust_under_position,
|
|
name=node_toreplace.name, pname=name, aboves=aboves},
|
|
offsets_hollowcube, 8799)
|
|
if ps then
|
|
local data = {aboves=aboves, ps=ps, num=num}
|
|
reduce_crust_ps(data)
|
|
ps,num = data.ps, data.num
|
|
end
|
|
end
|
|
end
|
|
elseif mode == "chunkborder" then
|
|
ps,num = get_ps(pos, {func=mantle_position, name=node_toreplace.name,
|
|
pname=name}, nil, 8799)
|
|
end
|
|
|
|
-- reset known nodes table
|
|
known_nodes = {}
|
|
|
|
if not ps then
|
|
inform(name, "Aborted, too many nodes detected.")
|
|
return
|
|
end
|
|
|
|
-- set nodes
|
|
local inv = user:get_inventory()
|
|
for i = 1,num do
|
|
local pos = ps[i]
|
|
local succ,err = replace_single_node(pos, minetest.get_node(pos), nnd,
|
|
user, name, inv, creative_enabled)
|
|
if not succ then
|
|
inform(name, err)
|
|
return
|
|
end
|
|
end
|
|
inform(name, num.." nodes replaced.")
|
|
end
|
|
|
|
|
|
minetest.register_craft({
|
|
output = "replacer:replacer",
|
|
recipe = {
|
|
{"default:chest", "default:obsidian", "default:obsidian"},
|
|
{"default:obsidian", "default:stick", "default:obsidian"},
|
|
{"default:obsidian", "default:obsidian", "default:chest"},
|
|
}
|
|
})
|