terraform/init.lua

1115 lines
34 KiB
Lua
Raw Permalink Normal View History

-- Privilege
minetest.register_privilege("terraform", "Ability to use terraform tools")
local function privileged(player, f, verbose)
if player ~= nil and player:get_player_name() ~= "" then
if minetest.check_player_privs(player, "terraform") then
return f()
elseif verbose then
minetest.chat_send_player(player:get_player_name(), "You need terraform privilege to perform the action")
end
end
end
-- Settings
local settings = {
undo_history_depth = minetest.settings:get("terraform.undo_history_depth") and tonumber(minetest.settings:get("terraform.undo_history_depth")) or 100,
undo_for_dig_place = minetest.settings:get("terraform.undo_for_dig_place") == "true",
}
-- In-memory history/undo engine
local history = {
2021-02-14 08:50:19 -08:00
_lists = {},
-- get list of history entries
get_list = function(self, name)
self._lists[name] = self._lists[name] or {}
return self._lists[name]
end,
-- capture a cuboid in space using voxel manipulator
capture = function(self, player, data, va, minp, maxp)
local capture = {}
for i in va:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
table.insert(capture,data[i])
end
local op = {minp = minp, maxp = maxp, data = capture}
local history = self:get_list(player:get_player_name())
table.insert(history, op)
while #history > settings.undo_history_depth do
table.remove(history, 1)
end
2021-02-14 08:50:19 -08:00
end,
-- restore state of the world map from history
undo = function(self, player)
local op = table.remove(self:get_list(player:get_player_name()))
if not op then
return
end
local vm = minetest.get_voxel_manip()
local minv,maxv = vm:read_from_map(op.minp, op.maxp)
local va = VoxelArea:new({MinEdge = minv, MaxEdge = maxv})
local si = 1
local data = vm:get_data()
for i in va:iter(op.minp.x, op.minp.y, op.minp.z, op.maxp.x, op.maxp.y, op.maxp.z) do
data[i] = op.data[si]
si = si + 1
end
vm:set_data(data)
vm:write_to_map(false)
2021-02-14 13:16:17 -08:00
end,
forget = function(self, player)
self._lists[player:get_player_name()] = nil
end
}
if settings.undo_for_dig_place then
minetest.register_on_dignode(function(pos,oldnode,player)
privileged(player, function()
history:capture(player, {minetest.get_content_id(oldnode.name)}, VoxelArea:new({MinEdge=pos,MaxEdge=pos}), pos, pos)
end)
end)
minetest.register_on_placenode(function(pos,newnode,player)
privileged(player, function()
history:capture(player, {minetest.CONTENT_AIR}, VoxelArea:new({MinEdge=pos,MaxEdge=pos}), pos, pos)
end)
end)
end
2021-02-14 13:16:17 -08:00
minetest.register_on_leaveplayer(function(player)
history:forget(player)
end)
2021-02-09 00:51:00 -08:00
-- Quirks for backward compatibility
local quirks = {
is_53_plus = minetest.features.object_step_has_moveresult
}
-- Per-player behavior settings and flags
local player_settings = {
flags = {
undo_on_aux1 = { id = "uaux1", default = true }
}
}
function player_settings.get_flag(player, name)
return player:get_meta():get_int("terraform:"..player_settings.flags[name].id, player_settings.flags[name].default and 1 or 0 ) == 1
end
function player_settings.set_flag(player, name, value)
return player:get_meta():set_int("terraform:"..player_settings.flags[name].id, value and 1 or 0 )
end
function player_settings.toggle_flag(player, name)
local new_value = not player_settings.get_flag(player, name)
player_settings.set_flag(player, name, new_value)
return new_value
end
minetest.register_chatcommand("tf", {
params = "toggle <flag>",
description = "Toggle a behavior flag.\n"..
"Supported flags:\n"..
" undo_on_aux1 - enable undo when using a tool with aux1",
privs = { terraform = true },
func = function(name, param)
local args = {}
for arg in param:gmatch("[^ ]+") do
table.insert(args, arg)
end
if args[1] ~= "toggle" then return false end
if player_settings.flags[args[2]] == nil then return false, "Unrecognized flag "..args[2] end
local new_value = player_settings.toggle_flag(minetest.get_player_by_name(name), args[2])
return true, new_value and "Enabled" or "Disabled"
end
})
-- public module API
terraform = {
2021-02-14 08:50:19 -08:00
_tools = {},
_history = history,
-- Per-player state of the tool configuration form
_latest_form = {},
-- Per-player flags for skipping light updates
skip_light = {},
2021-02-14 08:50:19 -08:00
-- register a terraform tool
register_tool = function(self, name, spec)
spec.tool_name = name
self._tools[spec.tool_name] = spec
2021-02-14 13:47:51 -08:00
if spec.init then
spec:init()
end
2021-02-14 08:50:19 -08:00
minetest.register_tool("terraform:"..spec.tool_name, {
description = spec.description,
short_description = spec.short_description,
inventory_image = spec.inventory_image,
wield_scale = {x=1,y=1,z=1},
stack_max = 1,
range = spec.range or 128.0,
liquids_pointable = true,
node_dig_prediction = "",
on_use = function(itemstack, player, target)
privileged(player, function()
terraform:show_config(player, spec.tool_name, itemstack)
end, true)
2021-02-14 08:50:19 -08:00
end,
on_secondary_use = function(itemstack, player, target)
privileged(player, function()
terraform:show_config(player, spec.tool_name, itemstack)
end, true)
2021-02-14 08:50:19 -08:00
end,
on_place = function(itemstack, player, target)
return privileged(player, function()
if player:get_player_control().aux1 and player_settings.get_flag(player, "undo_on_aux1") then
history:undo(player)
else
spec:execute(player, target, itemstack:get_meta())
end
return itemstack
end, true)
2021-02-14 08:50:19 -08:00
end,
})
end,
-- show configuration form for the specific tool
show_config = function(self, player, tool_name)
if self.blocked or not self._tools[tool_name].render_config then
2021-02-14 08:50:19 -08:00
return
end
local itemstack = player:get_wielded_item()
local form = { id = "terraform:props:"..tool_name..math.random(1,100000), tool_name = tool_name}
self._latest_form[player:get_player_name()] = form
2021-02-14 08:50:19 -08:00
local formspec = self._tools[tool_name]:render_config(player, itemstack:get_meta())
if quirks.is_53_plus then
minetest.show_formspec(player:get_player_name(), form.id, formspec)
else
self.blocked = true
minetest.after(0.25, function()
minetest.show_formspec(player:get_player_name(), form.id, formspec)
self.blocked = false
end)
end
2021-02-14 08:50:19 -08:00
end,
forget = function(self, player)
self._latest_form[player:get_player_name()] = nil
minetest.remove_detached_inventory("terraform."..player:get_player_name())
end,
2021-02-14 08:50:19 -08:00
get_inventory = function(player)
return minetest.get_inventory({type = "detached", name = "terraform."..player:get_player_name()})
end,
-- Helpers for storing inventory into settings
string_to_list = function(s,size)
-- Accept: a comma-separated list of content names and desired list size
-- Return: a table with item names, compatible with inventory lists
local result = {}
for part in s:gmatch("[^,]+") do
table.insert(result, part)
end
while #result < size do table.insert(result, "") end
return result
end,
list_to_string = function(list)
-- Accept: result of InvRef:get_list
-- Retrun: a comma-separated list of items
local result = ""
for k,v in pairs(list) do
if v.get_name ~= nil then v = v:to_string() end -- ItemStack to string
2021-02-14 08:50:19 -08:00
if v ~= "" then
if string.len(result) > 0 then
result = result..","
end
result = result..v
end
end
return result
end
}
-- Handle input from forms
minetest.register_on_player_receive_fields(function(player, formname, fields)
privileged(player, function()
local form = terraform._latest_form[player:get_player_name()]
if form ~= nil and formname == form.id then
local tool_name = form.tool_name
local tool = terraform._tools[tool_name]
if not tool.config_input then
return
end
2021-02-14 08:50:19 -08:00
local itemstack = player:get_wielded_item()
local reload = tool:config_input(player, fields, itemstack:get_meta())
2021-02-14 08:50:19 -08:00
-- update tool description in the inventory
if tool.get_description then
itemstack:get_meta():set_string("description", tool:get_description(itemstack:get_meta()))
end
2021-02-14 08:50:19 -08:00
player:set_wielded_item(itemstack)
2021-02-14 08:50:19 -08:00
if fields.quit then
terraform._latest_form[player:get_player_name()] = nil
return
end
2021-02-14 08:50:19 -08:00
if reload then
terraform:show_config(player, tool_name, itemstack)
end
2021-02-14 08:50:19 -08:00
end
end)
end)
minetest.register_on_leaveplayer(function(player)
terraform:forget(player)
end)
-- Tools
-- Brush
--
terraform:register_tool("brush", {
2021-02-14 08:50:19 -08:00
description = "Brush\n\nPaints the world with broad strokes",
short_description = "Brush",
inventory_image = "terraform_tool_brush.png",
-- 16 logical tag colors
colors = { "red", "yellow", "lime", "aqua",
"darkred", "orange", "darkgreen", "mediumblue",
"violet", "wheat", "olive", "dodgerblue" },
2021-02-21 13:39:15 -08:00
-- Modifier names and labels
modifiers = {{n="surface",l="Surface"}, {n="scatter",l="Scatter"}, {n="decor",l="Decoration"}, {n="landslide",l="Landslide"}, {n="flat", l="Flat"}},
2021-02-14 08:50:19 -08:00
max_size = 15,
2021-02-14 13:47:51 -08:00
init = function(self)
for _,shape_fn in ipairs(self.shapes) do
self.shapes[shape_fn().name] = shape_fn
end
end,
2021-02-14 08:50:19 -08:00
render_config = function(self, player, settings)
local function selection(texture, selected)
if selected then return texture.."^terraform_selection.png" end
return texture
end
local inventory = minetest.create_detached_inventory("terraform."..player:get_player_name(), {
2021-02-14 08:50:19 -08:00
allow_move = function(inv,source,sindex,dest,dindex,count)
if source == "palette" and dest ~= "palette" then
local source_stack = inv:get_stack(source, sindex)
local dest_stack = inv:get_stack(dest,dindex)
source_stack:set_count(count)
if dest_stack:get_name() == source_stack:get_name() then
dest_stack:add_item(source_stack)
else
dest_stack = source_stack
end
inv:set_stack(dest,dindex,dest_stack)
inv:set_stack(source,sindex,source_stack:get_name().." "..(source_stack:get_definition().stack_max or minetest.settings["default_stack_max"]))
2021-02-14 08:50:19 -08:00
return 0
elseif dest == "palette" and source ~= "palette" then
local stack = inv:get_stack(source, sindex, "")
stack:take_item(count)
inv:set_stack(source, sindex, stack)
return 0
elseif source == "palette" and dest == "palette" then
2021-02-14 08:50:19 -08:00
return 0
end
return count
end,
allow_take = function(inv,list,index,stack)
if list == "palette" then
return -1
end
return 1
end,
2021-02-14 08:50:19 -08:00
})
local palette = {}
local count = 0
local pattern = settings:get_string("search_text")
local skip = 40 * (settings:get_int("search_page") or 0)
for k,v in pairs(minetest.registered_nodes) do
if not pattern or string.find(k, pattern) ~= nil then
if skip > 0 then
skip = skip - 1
else
table.insert(palette, k.." "..(ItemStack(k):get_definition().stack_max or minetest.settings["default_stack_max"]))
2021-02-14 08:50:19 -08:00
if #palette >= 40 then
break
end
end
count = count + 1
end
end
while #palette < 40 do table.insert(palette, "") end
local paint = terraform.string_to_list(settings:get_string("paint"), 10)
local mask = terraform.string_to_list(settings:get_string("mask"), 10)
inventory:set_list("palette", palette)
inventory:set_list("paint", paint)
inventory:set_list("mask", mask)
spec =
"size[17,12]"..
"position[0.5,0.45]"..
"anchor[0.5,0.5]"..
"no_prepend[]"..
"real_coordinates[true]"..
2021-02-14 08:50:19 -08:00
"button_exit[14.5,10.5;2,1;quit;Close]".. -- Close button !Remember to offset when form size changes
"container[0.5,0.5]".. -- shape
"label[0,0.5; Shape:]"
local pos = 0
2021-02-14 13:47:51 -08:00
for _,shape_fn in ipairs(self.shapes) do
local shape = shape_fn().name -- Construct shape and extract the name
2021-02-14 08:50:19 -08:00
local x = pos % 3
local y = math.floor(pos / 3) + 1
spec = spec.."image_button["..x..","..y..";1,1;"..selection("terraform_shape_"..shape..".png",settings:get_string("shape") == shape)..";shape_"..shape..";]"
2021-02-14 13:27:34 -08:00
spec = spec.."tooltip[shape_"..shape..";"..shape.."]"
2021-02-14 08:50:19 -08:00
pos = pos + 1
end
spec = spec ..
"container_end[]"..
"container[0.5,4]".. -- size
"label[0,0.4; Size:]"..
"field[1,0;1,0.7;size;;"..(settings:get_int("size") or 3).."]"..
"field_close_on_enter[size;false]"..
"scrollbaroptions[min=0;max="..self.max_size..";smallstep=1;thumbsize=0;arrows=show]"..
"scrollbar[2,0;0.35,0.7;vertical;size_sb;"..(self.max_size - (settings:get_int("size") or 3)).."]"..
"container_end[]"..
2021-02-21 13:39:15 -08:00
"container[0.5, 5.5]" -- modifiers
pos = 0
for _,modifier in ipairs(self.modifiers) do
spec = spec ..
"checkbox[0,"..pos..";modifiers_"..modifier.n..";"..modifier.l..";"..(settings:get_int("modifiers_"..modifier.n) == 1 and "true" or "false").."]"
pos = pos + 0.5
end
spec = spec ..
2021-02-14 08:50:19 -08:00
"container_end[]"..
"container[4,0.5]".. -- creative
"label[0,0.5; Palette]"..
"label[4.75,0.5; Find nodes:]"..
"field[6.5,0.1;2,0.75;search_text;;"..minetest.formspec_escape(settings:get_string("search_text") or "").."]"..
"field_close_on_enter[search_text;false]"..
"button[8.5,0.1;2,0.75;search;Search]"..
"button[10.5,0.1;0.75,0.75;prev_page;<]"..
"button[11.25,0.1;0.75,0.75;next_page;>]"..
"list[detached:terraform."..player:get_player_name()..";palette;0,1;10,4]"..
"container_end[]"..
"container[4,6]".. -- paint
"label[0,0.5; Paint]"..
"list[detached:terraform."..player:get_player_name()..";paint;0,1;10,1]"..
"container_end[]"..
"container[4,8]".. -- Mask
"label[0,0.5; Mask]"..
"list[detached:terraform."..player:get_player_name()..";mask;0,1;10,1]"..
"container_end[]"
-- Color tags
spec = spec..
2021-02-14 13:33:51 -08:00
"container[0.5, 8]"..
2021-02-14 08:50:19 -08:00
"label[0,0.5; Color Tag]"
local count = 0
local size = 0.5
for _, color in ipairs(self.colors) do
local offset = size*(count % 4)
local line = 0.75 + size*math.floor(count / 4)
if quirks.is_53_plus then
local texture = "terraform_tool_brush.png^[multiply:"..color..""
spec = spec.."image_button["..offset..","..line..";"..size..","..size..";"..selection(texture,settings:get_string("color") == color)..";color_"..color..";]"
else
spec = spec.."item_image_button["..offset..","..line..";"..size..","..size..";"..minetest.formspec_escape(minetest.itemstring_with_color("terraform:brush", color))..";color_"..color..";]"
end
spec = spec.."tooltip[color_"..color..";"..color.."]"
2021-02-14 08:50:19 -08:00
count = count + 1
end
spec = spec..
"container_end[]"..
""
return spec
end,
config_input = function(self, player, fields, settings)
local refresh = false
-- Shape
for shape,_ in pairs(self.shapes) do
if fields["shape_"..shape] ~= nil then
settings:set_string("shape", shape)
refresh = true
end
end
-- Size
if fields.size_sb ~= nil and string.find(fields.size_sb, "CHG") then
local e = minetest.explode_scrollbar_event(fields.size_sb)
if e.type == "CHG" then
settings:set_int("size", math.min(math.max(self.max_size - tonumber(e.value), 0), self.max_size))
refresh = true
end
2021-02-14 13:52:08 -08:00
elseif fields.size ~= nil and tonumber(fields.size) ~= nil then
2021-02-14 08:50:19 -08:00
settings:set_int("size", math.min(math.max(tonumber(fields.size), 0), self.max_size))
end
2021-02-21 13:39:15 -08:00
-- Modifiers
for _,modifier in ipairs(self.modifiers) do
if fields["modifiers_"..modifier.n] ~= nil then
settings:set_int("modifiers_"..modifier.n, fields["modifiers_"..modifier.n] == "true" and 1 or 0)
end
2021-02-14 08:50:19 -08:00
end
-- Search
if fields.search_text ~= nil then
settings:set_string("search_text", fields.search_text or "")
refresh = true
end
if fields.search ~= nil or (fields.search_text ~= nil and fields.key_enter_field == "search_text") then
settings:set_int("search_page", 0)
refresh = true
end
if fields.prev_page ~= nil then
settings:set_int("search_page", math.max(0, (settings:get_int("search_page") or 0) - 1))
refresh = true
end
if fields.next_page ~= nil then
settings:set_int("search_page", (settings:get_int("search_page") or 0) + 1)
refresh = true
end
-- Color Tags
for _,color in ipairs(self.colors) do
if fields["color_"..color] then
settings:set_string("color", color)
refresh = true
end
end
local inv = terraform.get_inventory(player)
if inv ~= nil then
settings:set_string("paint", terraform.list_to_string(inv:get_list("paint")))
settings:set_string("mask", terraform.list_to_string(inv:get_list("mask")))
end
return refresh
end,
get_description = function(self, settings)
return "Terraform Brush ("..(settings:get_string("shape") or "sphere")..")\n"..
"size "..(settings:get_int("size") or 0).."\n"..
"paint "..(settings:get_string("paint")).."\n"..
"mask "..(settings:get_string("mask"))
end,
execute = function(self, player, target, settings)
-- Get position
local target_pos = minetest.get_pointed_thing_position(target)
if not target_pos then
return
end
-- Define size in 3d
local size = settings:get_int("size") or 3
local size_3d = vector.new(size, size, size)
2021-02-21 13:39:15 -08:00
if settings:get_int("modifiers_flat") == 1 then
size_3d = vector.new(size_3d.x, 0, size_3d.z)
end
2021-02-14 08:50:19 -08:00
-- Pick a shape
local shape_name = settings:get_string("shape") or "sphere"
if not self.shapes[shape_name] then shape_name = "sphere" end
local shape = self.shapes[shape_name]()
-- Define working area and load state
2021-02-21 13:39:15 -08:00
local minp, maxp = shape:get_bounds(player, target_pos, size_3d)
local minc, maxc = vector.new(minp), vector.new(maxp)
if settings:get_int("modifiers_landslide") == 1 then
minc.y = minc.y - 100
end
2021-02-14 08:50:19 -08:00
local v = minetest.get_voxel_manip()
2021-02-21 13:39:15 -08:00
local minv, maxv = v:read_from_map(minc, maxc)
2021-02-14 08:50:19 -08:00
local a = VoxelArea:new({MinEdge = minv, MaxEdge = maxv })
2021-02-21 13:39:15 -08:00
-- Get data
2021-02-14 08:50:19 -08:00
local data = v:get_data()
2021-02-21 13:39:15 -08:00
-- Capture history. If landslide enabled, find the lowest Y with air
minc.y = minp.y
if settings:get_int("modifiers_landslide") == 1 then
for x = minp.x, maxp.x do
for z = minp.z, maxp.z do
for y = target_pos.y, target_pos.y - 100, -1 do
if data[a:index(x, y, z)] ~= minetest.CONTENT_AIR then
if y + 1 < minc.y then
minc.y = y + 1
end
break
end
end
end
end
end
history:capture(player, data, a, minc, maxc)
2021-02-14 08:50:19 -08:00
-- Set up context
local ctx = {
size_3d = size_3d,
player = player
}
-- Prepare Paint
local paint = {}
local boundary = 0
2021-02-14 08:50:19 -08:00
for i,v in ipairs(terraform.string_to_list(settings:get_string("paint"), 10)) do
if v ~= "" then
local stack = ItemStack(v)
boundary = boundary + stack:get_count()
table.insert(paint, { id = minetest.get_content_id(stack:get_name()), boundary = boundary})
2021-02-14 08:50:19 -08:00
end
end
if #paint == 0 then
table.insert(paint, { id = minetest.CONTENT_AIR, boundary = 1 })
2021-02-14 08:50:19 -08:00
end
ctx.paint = paint
2021-02-14 08:50:19 -08:00
ctx.get_paint = function()
local sample = math.random(1, paint[#paint].boundary)
for _,v in ipairs(paint) do
if sample < v.boundary then
return v.id
end
end
return paint[#paint].id
2021-02-14 08:50:19 -08:00
end
-- Prepare Mask
local mask = {}
for i,v in ipairs(terraform.string_to_list(settings:get_string("mask"), 10)) do
if v ~= "" then
table.insert(mask, minetest.get_content_id(ItemStack(v):get_name()))
2021-02-14 08:50:19 -08:00
end
end
ctx.mask = mask
2021-02-14 08:50:19 -08:00
ctx.in_mask = function(cid)
if #mask == 0 then return true end
for i,v in ipairs(mask) do if v == cid then return true end end
return false
end
2021-02-14 13:07:45 -08:00
-- Prepare modifiers
local modifiers = {}
2021-02-21 13:39:15 -08:00
if settings:get_int("modifiers_landslide") == 1 then
table.insert(modifiers, function(i)
while data[i - a.ystride] == minetest.CONTENT_AIR and a:position(i).y > minc.y do
2021-02-21 13:39:15 -08:00
i = i - a.ystride
end
return i
2021-02-21 13:39:15 -08:00
end)
end
2021-02-14 08:50:19 -08:00
if settings:get_int("modifiers_surface") == 1 then
2021-02-14 13:07:45 -08:00
table.insert(modifiers, function(i)
2021-02-14 08:50:19 -08:00
if data[i] == minetest.CONTENT_AIR then return nil end
if a:position(i).y < maxp.y and data[i+a.ystride] == minetest.CONTENT_AIR then return i end
return nil
end)
end
if settings:get_int("modifiers_decor") == 1 then
2021-02-14 13:07:45 -08:00
table.insert(modifiers, function(i)
2021-02-14 08:50:19 -08:00
if data[i] == minetest.CONTENT_AIR then return nil end
if a:position(i).y < maxp.y and data[i+a.ystride] == minetest.CONTENT_AIR then return i+a.ystride end
return nil
end)
end
if settings:get_int("modifiers_scatter") == 1 then
2021-02-14 13:07:45 -08:00
table.insert(modifiers, function(i)
2021-02-14 08:50:19 -08:00
return math.random(1,1000) <= 50 and i or nil
end)
end
ctx.draw = function(i, paint)
if not ctx.in_mask(data[i]) then return end -- if not in mask, skip painting
2021-02-14 13:07:45 -08:00
for _,f in ipairs(modifiers) do
2021-02-14 08:50:19 -08:00
i = f(i)
if not i then return end -- if i is nil, skip painting
end
data[i] = paint or ctx.get_paint()
end
-- Paint
shape:paint(data, a, target_pos, minp, maxp, ctx)
-- Save back to map, no light information
v:set_data(data)
v:write_to_map(not terraform.skip_light[player:get_player_name()])
2021-02-14 08:50:19 -08:00
end,
-- Definition of shapes
shapes = {
2021-02-14 13:47:51 -08:00
function()
2021-02-14 08:50:19 -08:00
return {
2021-02-14 13:47:51 -08:00
name = "cube",
2021-02-14 08:50:19 -08:00
get_bounds = function(self, player, target_pos, size_3d)
return vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
2021-02-14 08:50:19 -08:00
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
for i in a:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
ctx.draw(i)
end
end,
}
end,
2021-02-14 13:47:51 -08:00
function()
2021-02-14 08:50:19 -08:00
return {
2021-02-14 13:47:51 -08:00
name = "sphere",
2021-02-14 08:50:19 -08:00
get_bounds = function(self, player, target_pos, size_3d)
return vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
for i in a:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
local ip = a:position(i)
local epsilon = 0.3
local delta = { x = ip.x - target_pos.x, y = ip.y - target_pos.y, z = ip.z - target_pos.z }
delta = { x = delta.x / (ctx.size_3d.x + epsilon), y = delta.y / (ctx.size_3d.y + epsilon), z = delta.z / (ctx.size_3d.z + epsilon) }
delta = { x = delta.x^2, y = delta.y^2, z = delta.z^2 }
if 1 > delta.x + delta.y + delta.z then
ctx.draw(i)
end
end
end,
}
end,
2021-02-14 13:47:51 -08:00
function()
2021-02-14 08:50:19 -08:00
return {
2021-02-14 13:47:51 -08:00
name = "cylinder",
2021-02-14 08:50:19 -08:00
get_bounds = function(self, player, target_pos, size_3d)
return vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
2021-02-14 08:50:19 -08:00
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
for i in a:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
local ip = a:position(i)
local epsilon = 0.3
local delta = { x = ip.x - target_pos.x, z = ip.z - target_pos.z }
delta = { x = delta.x / (ctx.size_3d.x + epsilon), z = delta.z / (ctx.size_3d.z + epsilon) }
delta = { x = delta.x^2, z = delta.z^2 }
if 1 > delta.x + delta.z then
ctx.draw(i)
end
end
end,
}
end,
2021-02-14 13:47:51 -08:00
function()
2021-02-14 08:50:19 -08:00
return {
2021-02-14 13:47:51 -08:00
name = "plateau",
2021-02-14 08:50:19 -08:00
get_bounds = function(self, player, target_pos, size_3d)
-- look up to 100 meters down
return vector.subtract(target_pos, vector.new(size_3d.x, 100, size_3d.z)), vector.add(target_pos, vector.new(size_3d.x, 0, size_3d.z))
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
local origin = a:indexp(target_pos)
local function is_solid(id)
if #ctx.mask > 0 then
return not ctx.in_mask(id)
else
return id ~= minetest.CONTENT_AIR
end
end
2021-02-14 08:50:19 -08:00
-- find deepest level (as negative)
local depth = 0
for x = -ctx.size_3d.x,ctx.size_3d.x do
for z = -ctx.size_3d.z,ctx.size_3d.z do
-- look in the circle around origin
local r = (x/(ctx.size_3d.x+0.3))^2 + (z/(ctx.size_3d.z+0.3))^2
if r < 1 then
-- scan 100 levels down
for y = 0,-100,-1 do
-- stop if the bottom is hit
local p = origin + x + y * a.ystride + z * a.zstride
if is_solid(data[p]) then
2021-02-14 08:50:19 -08:00
if y < depth then depth = y end
break
end
end
end
end
end
-- fill
for x = -ctx.size_3d.x,ctx.size_3d.x do
for z = -ctx.size_3d.z,ctx.size_3d.z do
-- look in the circle around origin
local r = (x/(ctx.size_3d.x+0.3))^2 + (z/(ctx.size_3d.z+0.3))^2
if r < 1 then
-- innermost 0.3 radius is fully filled, then sine descend
local cutoff = 0
if r > 0.6 then
cutoff = math.min(0, math.floor(depth * math.sin((r - 0.6) * math.pi / 4)))
end
-- fill with material down from cut off point to depth
for y = cutoff,depth,-1 do
i = origin + x + y * a.ystride + z * a.zstride
if is_solid(data[i]) then
2021-02-14 08:50:19 -08:00
break --stop at the first non-mask
else
ctx.draw(i)
2021-02-14 08:50:19 -08:00
end
end
end
end
end
end
}
end,
2021-02-14 13:47:51 -08:00
function()
2021-02-14 08:50:19 -08:00
return {
2021-02-14 13:47:51 -08:00
name = "smooth",
2021-02-14 08:50:19 -08:00
get_bounds = function(self, player, target_pos, size_3d)
return vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
local origin = a:indexp(target_pos)
local paint_flags = {}
local function get_weight(i,r)
local top, bottom = 0, 0
for lx = -r,r do
for ly = -r,r do
for lz = -r,r do
local weight = 1 -- all dots are equal, but this could be fancier
top = top + (data[i + lx + a.ystride*ly + a.zstride*lz] ~= minetest.CONTENT_AIR and weight or 0)
bottom = bottom + weight
end
end
end
return top / bottom
end
-- Spherical shape
-- Reduce all bounds by 1 to avoid edge glitches when looking for neighbours
for x = -ctx.size_3d.x+1,ctx.size_3d.x-1 do
for y = -ctx.size_3d.y+1,ctx.size_3d.y-1 do
for z = -ctx.size_3d.z+1,ctx.size_3d.z-1 do
local r = (x/ctx.size_3d.x)^2 + (y/ctx.size_3d.y)^2 + (z/ctx.size_3d.z)^2
if r <= 1 then
local i = origin + x + a.ystride*y + a.zstride*z
local rr = math.floor(math.max(1,math.min(ctx.size_3d.x/3,(1-r)*ctx.size_3d.x)))
paint_flags[i] = (get_weight(i, rr) < 0.5)
end
end
end
end
for pos,is_air in pairs(paint_flags) do
if is_air ~= (data[pos] == minetest.CONTENT_AIR) then
ctx.draw(pos, is_air and minetest.CONTENT_AIR or ctx.get_paint())
end
end
end,
}
end,
2021-02-14 13:47:51 -08:00
function()
2021-02-14 08:50:19 -08:00
return {
2021-02-14 13:47:51 -08:00
name = "trowel",
2021-02-14 08:50:19 -08:00
get_bounds = function(self, player, target_pos, size_3d)
local pp = vector.floor(player:get_pos())
local minp,maxp = vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
return vector.new(math.min(minp.x, pp.x), math.min(minp.y, pp.y), math.min(minp.z, pp.z)),
vector.new(math.max(maxp.x, pp.x), math.max(maxp.y, pp.y), math.max(maxp.z, pp.z))
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
local origin = a:indexp(target_pos)
local paint_flags = {}
local function get_weight(i,r)
local top, bottom = 0, 0
for lx = -r,r do
for ly = -r,r do
for lz = -r,r do
local weight = 1 -- all dots are equal, but this could be fancier
top = top + (data[i + lx + a.ystride*ly + a.zstride*lz] ~= minetest.CONTENT_AIR and weight or 0)
bottom = bottom + weight
end
end
end
return top / bottom
end
-- Spherical shape
-- Reduce all bounds by 1 to avoid edge glitches when looking for neighbours
for x = -ctx.size_3d.x+1,ctx.size_3d.x-1 do
for y = -ctx.size_3d.y+1,ctx.size_3d.y-1 do
for z = -ctx.size_3d.z+1,ctx.size_3d.z-1 do
local r = (x/ctx.size_3d.x)^2 + (y/ctx.size_3d.y)^2 + (z/ctx.size_3d.z)^2
if r <= 1 then
local i = origin + x + a.ystride*y + a.zstride*z
local rr = math.floor(math.max(1,math.min(ctx.size_3d.x/3,(1-r)*ctx.size_3d.x)))
local dotproduct = vector.dot(ctx.player:get_look_dir(), vector.normalize(vector.new(x,y,z)))
paint_flags[i] = (get_weight(i, rr) + dotproduct < 0.5)
end
end
end
end
for pos,is_air in pairs(paint_flags) do
if is_air ~= (data[pos] == minetest.CONTENT_AIR) then
ctx.draw(pos, is_air and minetest.CONTENT_AIR or ctx.get_paint())
end
end
end,
}
end,
}
})
2021-01-31 16:21:39 -08:00
-- Colorize brush when putting to inventory
minetest.register_on_player_inventory_action(function(player,action,inventory,inventory_info)
if action ~= "put" or inventory_info.listname ~= "main" or inventory_info.stack:get_name() ~= "terraform:brush" then
2021-02-14 08:50:19 -08:00
return
end
local stack = inventory_info.stack
if (stack:get_meta():get_string("color") or "") == "" then
local colors = terraform._tools["brush"].colors
local color = colors[math.random(1,#colors)]
stack:get_meta():set_string("color", color)
inventory:set_stack(inventory_info.listname, inventory_info.index, stack)
end
2021-01-31 16:21:39 -08:00
end)
2021-02-08 01:14:29 -08:00
--
-- Undo changes to the world
--
terraform:register_tool("undo", {
2021-02-14 08:50:19 -08:00
description = "Terraform Undo\n\nUndoes changes to the world",
short_description = "Terraform Undo",
inventory_image = "terraform_tool_undo.png",
execute = function(itemstack, player, target)
history:undo(player)
end
})
2021-02-08 01:14:29 -08:00
--
-- A magic wand to fix light problems.
--
2021-02-06 12:21:21 -08:00
terraform:register_tool("fixlight", {
2021-02-14 08:50:19 -08:00
description = "Terraform Fix Light\n\nFix lighting problems",
short_description = "Terraform Fix Light",
inventory_image = "terraform_tool_fix_light.png",
execute = function(itemstack, player, target)
-- Get position
local target_pos = minetest.get_pointed_thing_position(target)
if not target_pos then
return
end
local s = 100
local origin = target_pos
local minp = vector.subtract(origin, vector.new(s,s,s))
local maxp = vector.add(origin, vector.new(s,s,s))
minetest.fix_light(minp, maxp)
end
2021-02-06 12:21:21 -08:00
})
local function box_diff(a, b)
2021-02-14 08:50:19 -08:00
-- a - b for boxes a and b, both a { min = vector, max = vector }
-- return 3 boxes
-- * split along X axis, full Y and Z
-- * split along Y axis, half X, full Z
-- * half X, Y, Z
local function diff_split(ax1, ax2, bx1, bx2)
-- given boundaries of two boxes a and b along an axis (x)
-- return x coordinates of athe diff a - b as two boxes
-- first box is having full height
-- second box is having trimmed height (w/o the intersection part)
if ax1 < bx1 then
return ax1, bx1, bx1, ax2
else
return bx2, ax2, ax1, bx2
end
end
local fx1, fx2, hx1, hx2 = diff_split(a.min.x, a.max.x, b.min.x, b.max.x)
local fy1, fy2, hy1, hy2 = diff_split(a.min.y, a.max.y, b.min.y, b.max.y)
local fz1, fz2, hz1, hz2 = diff_split(a.min.z, a.max.z, b.min.z, b.max.z)
return { min = vector.new(fx1, a.min.y, a.min.z), max = vector.new(fx2, a.max.y, a.max.z)},
{ min = vector.new(hx1, fy1, a.min.z), max = vector.new(hx2, fy2, a.max.z)},
{ min = vector.new(hx1, hy1, fz1), max = vector.new(hx2, hy2, fz2)}
2021-02-06 12:21:21 -08:00
end
local light = {
2021-02-14 08:50:19 -08:00
size = 20,
level = minetest.LIGHT_MAX,
pitch_rate = 1/5,
queues = { light = {}, dark = {} },
players = {},
light_bounds = function(self, pos)
local s = self.size
return { min = vector.subtract(pos, vector.new(s,s,s)), max = vector.add(pos, vector.new(s,s,s)) }
end,
add_player = function(self, player)
self.players[player:get_player_name()] = { player = player }
terraform.skip_light[player:get_player_name()] = true
2021-02-14 08:50:19 -08:00
end,
remove_player = function(self, player)
local light = self.players[player:get_player_name()]
if light ~= nil then
table.insert(self.queues.dark, self:light_bounds(vector.floor(light.player:get_pos())))
if light.last_pos ~= nil then
table.insert(self.queues.dark, self:light_bounds(light.last_pos))
end
end
self.players[player:get_player_name()] = nil
terraform.skip_light[player:get_player_name()] = nil
2021-02-14 08:50:19 -08:00
self:tick()
end,
tick = function(self)
for name,pl in pairs(self.players) do
local origin = vector.floor(pl.player:get_pos())
local box = self:light_bounds(origin)
local minp = box.min
local maxp = box.max
pl.c = (pl.c or 0) + 1
if pl.last_pos ~= nil then
if vector.distance(origin, pl.last_pos) < self.size * self.pitch_rate then
break -- skip the player, not enough movement
end
local old_box = self:light_bounds(pl.last_pos)
local b1, b2, b3 = box_diff(old_box, box)
table.insert(self.queues.dark, b1)
table.insert(self.queues.dark, b2)
table.insert(self.queues.dark, b3)
end
table.insert(self.queues.light, box)
pl.last_pos = origin
end
-- process queues
while #self.queues.dark > 0 do
local box = table.remove(self.queues.dark, 1)
minetest.get_voxel_manip(box.min, box.max):write_to_map(true) -- fix the light
end
while #self.queues.light > 0 do
local box = table.remove(self.queues.light, 1)
-- Load manipulator
local vm = minetest.get_voxel_manip()
local mine,maxe = vm:read_from_map(box.min, box.max)
local va = VoxelArea:new({MinEdge = mine, MaxEdge = maxe})
-- Set light information in the area
local light = vm:get_light_data()
local level = self.level
for i in va:iter(box.min.x, box.min.y, box.min.z, box.max.x, box.max.y, box.max.z) do
if light[i] == nil then
light[i] = level*17
else
light[i] = math.max(math.floor(light[i] / 16), level) * 16 + math.max(light[i] % 16, level)
end
end
vm:set_light_data(light)
vm:write_to_map(false)
end
end
2021-02-06 12:21:21 -08:00
}
terraform:register_tool("light", {
2021-02-14 08:50:19 -08:00
description = "Terraform Light\n\nTurn on the lights",
short_description = "Terraform Light",
inventory_image = "terraform_tool_light.png",
execute = function(itemstack, player, target)
if player:get_day_night_ratio() ~= nil then
player:override_day_night_ratio(nil)
light:remove_player(player)
else
player:override_day_night_ratio(1)
light:add_player(player)
end
end
2021-02-06 12:21:21 -08:00
})
minetest.register_on_leaveplayer(function(player)
2021-02-14 08:50:19 -08:00
light:remove_player(player)
2021-02-06 12:21:21 -08:00
end)
local function place_lights()
2021-02-14 08:50:19 -08:00
light:tick()
minetest.after(0.5, place_lights)
2021-02-06 12:21:21 -08:00
end
2021-02-08 01:11:50 -08:00
minetest.after(0.5, place_lights)
2021-02-21 05:06:27 -08:00
terraform:register_tool("teleport", {
description = "Terraform Teleport\n\nTravel fast",
short_description = "Terraform Teleport",
inventory_image = "terraform_tool_teleport.png",
execute = function(itemstack, player, target)
-- Get position
local target_pos = minetest.get_pointed_thing_position(target)
if not target_pos then
return
end
2021-02-21 05:06:27 -08:00
local player_pos = vector.floor(player:get_pos())
local probe = { x = player_pos.x, y = player_pos.y, z = player_pos.z }
while minetest.get_node(probe).name == "air" do
probe.y = probe.y - 1
end
local vm = minetest.get_voxel_manip()
local mine,maxe = vm:read_from_map(vector.add(target_pos, vector.new(0, -128, 0)), vector.add(target_pos, vector.new(0, 128 + player_pos.y - probe.y, 0)))
local va = VoxelArea:new({MinEdge=mine, MaxEdge=maxe})
local data = vm:get_data()
local i = va:indexp(target_pos)
while data[i] ~= minetest.CONTENT_AIR do --Move to the topmost block
i = i + va.ystride
end
i = i + va.ystride * (player_pos.y - probe.y - 1)
for delta = 0,128 do -- Try up to 128 meters up or down
for sign = -1,1,2 do
if data[i + va.ystride * delta * sign] == minetest.CONTENT_AIR and data[i + va.ystride * (1 + delta * sign)] == minetest.CONTENT_AIR then
result = va:position(i + va.ystride * delta * sign)
player:set_pos(result)
return
2021-02-21 05:06:27 -08:00
end
end
end
2021-02-21 05:06:27 -08:00
end
})