summaryrefslogtreecommitdiff
path: root/worldedit/manipulations.lua
diff options
context:
space:
mode:
Diffstat (limited to 'worldedit/manipulations.lua')
-rw-r--r--worldedit/manipulations.lua629
1 files changed, 629 insertions, 0 deletions
diff --git a/worldedit/manipulations.lua b/worldedit/manipulations.lua
new file mode 100644
index 0000000..cf95517
--- /dev/null
+++ b/worldedit/manipulations.lua
@@ -0,0 +1,629 @@
+--- Generic node manipulations.
+-- @module worldedit.manipulations
+
+local mh = worldedit.manip_helpers
+
+
+--- Sets a region to `node_names`.
+-- @param pos1
+-- @param pos2
+-- @param node_names Node name or list of node names.
+-- @return The number of nodes set.
+function worldedit.set(pos1, pos2, node_names)
+ pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ local manip, area = mh.init(pos1, pos2)
+ local data = mh.get_empty_data(area)
+
+ if type(node_names) == "string" then -- Only one type of node
+ local id = minetest.get_content_id(node_names)
+ -- Fill area with node
+ for i in area:iterp(pos1, pos2) do
+ data[i] = id
+ end
+ else -- Several types of nodes specified
+ local node_ids = {}
+ for i, v in ipairs(node_names) do
+ node_ids[i] = minetest.get_content_id(v)
+ end
+ -- Fill area randomly with nodes
+ local id_count, rand = #node_ids, math.random
+ for i in area:iterp(pos1, pos2) do
+ data[i] = node_ids[rand(id_count)]
+ end
+ end
+
+ mh.finish(manip, data)
+
+ return worldedit.volume(pos1, pos2)
+end
+
+
+--- Replaces all instances of `search_node` with `replace_node` in a region.
+-- When `inverse` is `true`, replaces all instances that are NOT `search_node`.
+-- @return The number of nodes replaced.
+function worldedit.replace(pos1, pos2, search_node, replace_node, inverse)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ local manip, area = mh.init(pos1, pos2)
+ local data = manip:get_data()
+
+ local search_id = minetest.get_content_id(search_node)
+ local replace_id = minetest.get_content_id(replace_node)
+
+ local count = 0
+
+ --- TODO: This could be shortened by checking `inverse` in the loop,
+ -- but that would have a speed penalty. Is the penalty big enough
+ -- to matter?
+ if not inverse then
+ for i in area:iterp(pos1, pos2) do
+ if data[i] == search_id then
+ data[i] = replace_id
+ count = count + 1
+ end
+ end
+ else
+ for i in area:iterp(pos1, pos2) do
+ if data[i] ~= search_id then
+ data[i] = replace_id
+ count = count + 1
+ end
+ end
+ end
+
+ mh.finish(manip, data)
+
+ return count
+end
+
+
+--- Duplicates a region `amount` times with offset vector `direction`.
+-- Stacking is spread across server steps, one copy per step.
+-- @return The number of nodes stacked.
+function worldedit.stack2(pos1, pos2, direction, amount, finished)
+ local i = 0
+ local translated = {x=0, y=0, z=0}
+ local function next_one()
+ if i < amount then
+ i = i + 1
+ translated.x = translated.x + direction.x
+ translated.y = translated.y + direction.y
+ translated.z = translated.z + direction.z
+ worldedit.copy2(pos1, pos2, translated)
+ minetest.after(0, next_one)
+ else
+ if finished then
+ finished()
+ end
+ end
+ end
+ next_one()
+ return worldedit.volume(pos1, pos2) * amount
+end
+
+
+--- Copies a region along `axis` by `amount` nodes.
+-- @param pos1
+-- @param pos2
+-- @param axis Axis ("x", "y", or "z")
+-- @param amount
+-- @return The number of nodes copied.
+function worldedit.copy(pos1, pos2, axis, amount)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ worldedit.keep_loaded(pos1, pos2)
+
+ local get_node, get_meta, set_node = minetest.get_node,
+ minetest.get_meta, minetest.set_node
+ -- Copy things backwards when negative to avoid corruption.
+ -- FIXME: Lots of code duplication here.
+ if amount < 0 then
+ local pos = {}
+ pos.x = pos1.x
+ while pos.x <= pos2.x do
+ pos.y = pos1.y
+ while pos.y <= pos2.y do
+ pos.z = pos1.z
+ while pos.z <= pos2.z do
+ local node = get_node(pos) -- Obtain current node
+ local meta = get_meta(pos):to_table() -- Get meta of current node
+ local value = pos[axis] -- Store current position
+ pos[axis] = value + amount -- Move along axis
+ set_node(pos, node) -- Copy node to new position
+ get_meta(pos):from_table(meta) -- Set metadata of new node
+ pos[axis] = value -- Restore old position
+ pos.z = pos.z + 1
+ end
+ pos.y = pos.y + 1
+ end
+ pos.x = pos.x + 1
+ end
+ else
+ local pos = {}
+ pos.x = pos2.x
+ while pos.x >= pos1.x do
+ pos.y = pos2.y
+ while pos.y >= pos1.y do
+ pos.z = pos2.z
+ while pos.z >= pos1.z do
+ local node = get_node(pos) -- Obtain current node
+ local meta = get_meta(pos):to_table() -- Get meta of current node
+ local value = pos[axis] -- Store current position
+ pos[axis] = value + amount -- Move along axis
+ set_node(pos, node) -- Copy node to new position
+ get_meta(pos):from_table(meta) -- Set metadata of new node
+ pos[axis] = value -- Restore old position
+ pos.z = pos.z - 1
+ end
+ pos.y = pos.y - 1
+ end
+ pos.x = pos.x - 1
+ end
+ end
+ return worldedit.volume(pos1, pos2)
+end
+
+--- Copies a region by offset vector `off`.
+-- @param pos1
+-- @param pos2
+-- @param off
+-- @return The number of nodes copied.
+function worldedit.copy2(pos1, pos2, off)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ worldedit.keep_loaded(pos1, pos2)
+
+ local get_node, get_meta, set_node = minetest.get_node,
+ minetest.get_meta, minetest.set_node
+ local pos = {}
+ pos.x = pos2.x
+ while pos.x >= pos1.x do
+ pos.y = pos2.y
+ while pos.y >= pos1.y do
+ pos.z = pos2.z
+ while pos.z >= pos1.z do
+ local node = get_node(pos) -- Obtain current node
+ local meta = get_meta(pos):to_table() -- Get meta of current node
+ local newpos = vector.add(pos, off) -- Calculate new position
+ set_node(newpos, node) -- Copy node to new position
+ get_meta(newpos):from_table(meta) -- Set metadata of new node
+ pos.z = pos.z - 1
+ end
+ pos.y = pos.y - 1
+ end
+ pos.x = pos.x - 1
+ end
+ return worldedit.volume(pos1, pos2)
+end
+
+--- Moves a region along `axis` by `amount` nodes.
+-- @return The number of nodes moved.
+function worldedit.move(pos1, pos2, axis, amount)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ worldedit.keep_loaded(pos1, pos2)
+
+ --- TODO: Move slice by slice using schematic method in the move axis
+ -- and transfer metadata in separate loop (and if the amount is
+ -- greater than the length in the axis, copy whole thing at a time and
+ -- erase original after, using schematic method).
+ local get_node, get_meta, set_node, remove_node = minetest.get_node,
+ minetest.get_meta, minetest.set_node, minetest.remove_node
+ -- Copy things backwards when negative to avoid corruption.
+ --- FIXME: Lots of code duplication here.
+ if amount < 0 then
+ local pos = {}
+ pos.x = pos1.x
+ while pos.x <= pos2.x do
+ pos.y = pos1.y
+ while pos.y <= pos2.y do
+ pos.z = pos1.z
+ while pos.z <= pos2.z do
+ local node = get_node(pos) -- Obtain current node
+ local meta = get_meta(pos):to_table() -- Get metadata of current node
+ remove_node(pos) -- Remove current node
+ local value = pos[axis] -- Store current position
+ pos[axis] = value + amount -- Move along axis
+ set_node(pos, node) -- Move node to new position
+ get_meta(pos):from_table(meta) -- Set metadata of new node
+ pos[axis] = value -- Restore old position
+ pos.z = pos.z + 1
+ end
+ pos.y = pos.y + 1
+ end
+ pos.x = pos.x + 1
+ end
+ else
+ local pos = {}
+ pos.x = pos2.x
+ while pos.x >= pos1.x do
+ pos.y = pos2.y
+ while pos.y >= pos1.y do
+ pos.z = pos2.z
+ while pos.z >= pos1.z do
+ local node = get_node(pos) -- Obtain current node
+ local meta = get_meta(pos):to_table() -- Get metadata of current node
+ remove_node(pos) -- Remove current node
+ local value = pos[axis] -- Store current position
+ pos[axis] = value + amount -- Move along axis
+ set_node(pos, node) -- Move node to new position
+ get_meta(pos):from_table(meta) -- Set metadata of new node
+ pos[axis] = value -- Restore old position
+ pos.z = pos.z - 1
+ end
+ pos.y = pos.y - 1
+ end
+ pos.x = pos.x - 1
+ end
+ end
+ return worldedit.volume(pos1, pos2)
+end
+
+
+--- Duplicates a region along `axis` `amount` times.
+-- Stacking is spread across server steps, one copy per step.
+-- @param pos1
+-- @param pos2
+-- @param axis Axis direction, "x", "y", or "z".
+-- @param count
+-- @return The number of nodes stacked.
+function worldedit.stack(pos1, pos2, axis, count)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+ local length = pos2[axis] - pos1[axis] + 1
+ if count < 0 then
+ count = -count
+ length = -length
+ end
+ local amount = 0
+ local copy = worldedit.copy
+ local i = 1
+ function next_one()
+ if i <= count then
+ i = i + 1
+ amount = amount + length
+ copy(pos1, pos2, axis, amount)
+ minetest.after(0, next_one)
+ end
+ end
+ next_one()
+ return worldedit.volume(pos1, pos2) * count
+end
+
+
+--- Stretches a region by a factor of positive integers along the X, Y, and Z
+-- axes, respectively, with `pos1` as the origin.
+-- @param pos1
+-- @param pos2
+-- @param stretch_x Amount to stretch along X axis.
+-- @param stretch_y Amount to stretch along Y axis.
+-- @param stretch_z Amount to stretch along Z axis.
+-- @return The number of nodes scaled.
+-- @return The new scaled position 1.
+-- @return The new scaled position 2.
+function worldedit.stretch(pos1, pos2, stretch_x, stretch_y, stretch_z)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ -- Prepare schematic of large node
+ local get_node, get_meta, place_schematic = minetest.get_node,
+ minetest.get_meta, minetest.place_schematic
+ local placeholder_node = {name="", param1=255, param2=0}
+ local nodes = {}
+ for i = 1, stretch_x * stretch_y * stretch_z do
+ nodes[i] = placeholder_node
+ end
+ local schematic = {size={x=stretch_x, y=stretch_y, z=stretch_z}, data=nodes}
+
+ local size_x, size_y, size_z = stretch_x - 1, stretch_y - 1, stretch_z - 1
+
+ local new_pos2 = {
+ x = pos1.x + (pos2.x - pos1.x) * stretch_x + size_x,
+ y = pos1.y + (pos2.y - pos1.y) * stretch_y + size_y,
+ z = pos1.z + (pos2.z - pos1.z) * stretch_z + size_z,
+ }
+ worldedit.keep_loaded(pos1, new_pos2)
+
+ local pos = {x=pos2.x, y=0, z=0}
+ local big_pos = {x=0, y=0, z=0}
+ while pos.x >= pos1.x do
+ pos.y = pos2.y
+ while pos.y >= pos1.y do
+ pos.z = pos2.z
+ while pos.z >= pos1.z do
+ local node = get_node(pos) -- Get current node
+ local meta = get_meta(pos):to_table() -- Get meta of current node
+
+ -- Calculate far corner of the big node
+ local pos_x = pos1.x + (pos.x - pos1.x) * stretch_x
+ local pos_y = pos1.y + (pos.y - pos1.y) * stretch_y
+ local pos_z = pos1.z + (pos.z - pos1.z) * stretch_z
+
+ -- Create large node
+ placeholder_node.name = node.name
+ placeholder_node.param2 = node.param2
+ big_pos.x, big_pos.y, big_pos.z = pos_x, pos_y, pos_z
+ place_schematic(big_pos, schematic)
+
+ -- Fill in large node meta
+ if next(meta.fields) ~= nil or next(meta.inventory) ~= nil then
+ -- Node has meta fields
+ for x = 0, size_x do
+ for y = 0, size_y do
+ for z = 0, size_z do
+ big_pos.x = pos_x + x
+ big_pos.y = pos_y + y
+ big_pos.z = pos_z + z
+ -- Set metadata of new node
+ get_meta(big_pos):from_table(meta)
+ end
+ end
+ end
+ end
+ pos.z = pos.z - 1
+ end
+ pos.y = pos.y - 1
+ end
+ pos.x = pos.x - 1
+ end
+ return worldedit.volume(pos1, pos2) * stretch_x * stretch_y * stretch_z, pos1, new_pos2
+end
+
+
+--- Transposes a region between two axes.
+-- @return The number of nodes transposed.
+-- @return The new transposed position 1.
+-- @return The new transposed position 2.
+function worldedit.transpose(pos1, pos2, axis1, axis2)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ local compare
+ local extent1, extent2 = pos2[axis1] - pos1[axis1], pos2[axis2] - pos1[axis2]
+
+ if extent1 > extent2 then
+ compare = function(extent1, extent2)
+ return extent1 > extent2
+ end
+ else
+ compare = function(extent1, extent2)
+ return extent1 < extent2
+ end
+ end
+
+ -- Calculate the new position 2 after transposition
+ local new_pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
+ new_pos2[axis1] = pos1[axis1] + extent2
+ new_pos2[axis2] = pos1[axis2] + extent1
+
+ local upper_bound = {x=pos2.x, y=pos2.y, z=pos2.z}
+ if upper_bound[axis1] < new_pos2[axis1] then upper_bound[axis1] = new_pos2[axis1] end
+ if upper_bound[axis2] < new_pos2[axis2] then upper_bound[axis2] = new_pos2[axis2] end
+ worldedit.keep_loaded(pos1, upper_bound)
+
+ local pos = {x=pos1.x, y=0, z=0}
+ local get_node, get_meta, set_node = minetest.get_node,
+ minetest.get_meta, minetest.set_node
+ while pos.x <= pos2.x do
+ pos.y = pos1.y
+ while pos.y <= pos2.y do
+ pos.z = pos1.z
+ while pos.z <= pos2.z do
+ local extent1, extent2 = pos[axis1] - pos1[axis1], pos[axis2] - pos1[axis2]
+ if compare(extent1, extent2) then -- Transpose only if below the diagonal
+ local node1 = get_node(pos)
+ local meta1 = get_meta(pos):to_table()
+ local value1, value2 = pos[axis1], pos[axis2] -- Save position values
+ pos[axis1], pos[axis2] = pos1[axis1] + extent2, pos1[axis2] + extent1 -- Swap axis extents
+ local node2 = get_node(pos)
+ local meta2 = get_meta(pos):to_table()
+ set_node(pos, node1)
+ get_meta(pos):from_table(meta1)
+ pos[axis1], pos[axis2] = value1, value2 -- Restore position values
+ set_node(pos, node2)
+ get_meta(pos):from_table(meta2)
+ end
+ pos.z = pos.z + 1
+ end
+ pos.y = pos.y + 1
+ end
+ pos.x = pos.x + 1
+ end
+ return worldedit.volume(pos1, pos2), pos1, new_pos2
+end
+
+
+--- Flips a region along `axis`.
+-- @return The number of nodes flipped.
+function worldedit.flip(pos1, pos2, axis)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ worldedit.keep_loaded(pos1, pos2)
+
+ --- TODO: Flip the region slice by slice along the flip axis using schematic method.
+ local pos = {x=pos1.x, y=0, z=0}
+ local start = pos1[axis] + pos2[axis]
+ pos2[axis] = pos1[axis] + math.floor((pos2[axis] - pos1[axis]) / 2)
+ local get_node, get_meta, set_node = minetest.get_node,
+ minetest.get_meta, minetest.set_node
+ while pos.x <= pos2.x do
+ pos.y = pos1.y
+ while pos.y <= pos2.y do
+ pos.z = pos1.z
+ while pos.z <= pos2.z do
+ local node1 = get_node(pos)
+ local meta1 = get_meta(pos):to_table()
+ local value = pos[axis] -- Save position
+ pos[axis] = start - value -- Shift position
+ local node2 = get_node(pos)
+ local meta2 = get_meta(pos):to_table()
+ set_node(pos, node1)
+ get_meta(pos):from_table(meta1)
+ pos[axis] = value -- Restore position
+ set_node(pos, node2)
+ get_meta(pos):from_table(meta2)
+ pos.z = pos.z + 1
+ end
+ pos.y = pos.y + 1
+ end
+ pos.x = pos.x + 1
+ end
+ return worldedit.volume(pos1, pos2)
+end
+
+
+--- Rotates a region clockwise around an axis.
+-- @param pos1
+-- @param pos2
+-- @param axis Axis ("x", "y", or "z").
+-- @param angle Angle in degrees (90 degree increments only).
+-- @return The number of nodes rotated.
+-- @return The new first position.
+-- @return The new second position.
+function worldedit.rotate(pos1, pos2, axis, angle)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ local other1, other2 = worldedit.get_axis_others(axis)
+ angle = angle % 360
+
+ local count
+ if angle == 90 then
+ worldedit.flip(pos1, pos2, other1)
+ count, pos1, pos2 = worldedit.transpose(pos1, pos2, other1, other2)
+ elseif angle == 180 then
+ worldedit.flip(pos1, pos2, other1)
+ count = worldedit.flip(pos1, pos2, other2)
+ elseif angle == 270 then
+ worldedit.flip(pos1, pos2, other2)
+ count, pos1, pos2 = worldedit.transpose(pos1, pos2, other1, other2)
+ else
+ error("Only 90 degree increments are supported!")
+ end
+ return count, pos1, pos2
+end
+
+
+--- Rotates all oriented nodes in a region clockwise around the Y axis.
+-- @param pos1
+-- @param pos2
+-- @param angle Angle in degrees (90 degree increments only).
+-- @return The number of nodes oriented.
+-- TODO: Support 6D facedir rotation along arbitrary axis.
+function worldedit.orient(pos1, pos2, angle)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+ local registered_nodes = minetest.registered_nodes
+
+ local wallmounted = {
+ [90] = {[0]=0, 1, 5, 4, 2, 3},
+ [180] = {[0]=0, 1, 3, 2, 5, 4},
+ [270] = {[0]=0, 1, 4, 5, 3, 2}
+ }
+ local facedir = {
+ [90] = {[0]=1, 2, 3, 0},
+ [180] = {[0]=2, 3, 0, 1},
+ [270] = {[0]=3, 0, 1, 2}
+ }
+
+ angle = angle % 360
+ if angle == 0 then
+ return 0
+ end
+ if angle % 90 ~= 0 then
+ error("Only 90 degree increments are supported!")
+ end
+ local wallmounted_substitution = wallmounted[angle]
+ local facedir_substitution = facedir[angle]
+
+ worldedit.keep_loaded(pos1, pos2)
+
+ local count = 0
+ local set_node, get_node, get_meta, swap_node = minetest.set_node,
+ minetest.get_node, minetest.get_meta, minetest.swap_node
+ local pos = {x=pos1.x, y=0, z=0}
+ while pos.x <= pos2.x do
+ pos.y = pos1.y
+ while pos.y <= pos2.y do
+ pos.z = pos1.z
+ while pos.z <= pos2.z do
+ local node = get_node(pos)
+ local def = registered_nodes[node.name]
+ if def then
+ if def.paramtype2 == "wallmounted" then
+ node.param2 = wallmounted_substitution[node.param2]
+ local meta = get_meta(pos):to_table()
+ set_node(pos, node)
+ get_meta(pos):from_table(meta)
+ count = count + 1
+ elseif def.paramtype2 == "facedir" then
+ node.param2 = facedir_substitution[node.param2]
+ local meta = get_meta(pos):to_table()
+ set_node(pos, node)
+ get_meta(pos):from_table(meta)
+ count = count + 1
+ end
+ end
+ pos.z = pos.z + 1
+ end
+ pos.y = pos.y + 1
+ end
+ pos.x = pos.x + 1
+ end
+ return count
+end
+
+
+--- Attempts to fix the lighting in a region.
+-- @return The number of nodes updated.
+function worldedit.fixlight(pos1, pos2)
+ local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ worldedit.keep_loaded(pos1, pos2)
+
+ local nodes = minetest.find_nodes_in_area(pos1, pos2, "air")
+ local dig_node = minetest.dig_node
+ for _, pos in ipairs(nodes) do
+ dig_node(pos)
+ end
+ return #nodes
+end
+
+
+--- Clears all objects in a region.
+-- @return The number of objects cleared.
+function worldedit.clear_objects(pos1, pos2)
+ pos1, pos2 = worldedit.sort_pos(pos1, pos2)
+
+ worldedit.keep_loaded(pos1, pos2)
+
+ -- Offset positions to include full nodes (positions are in the center of nodes)
+ local pos1x, pos1y, pos1z = pos1.x - 0.5, pos1.y - 0.5, pos1.z - 0.5
+ local pos2x, pos2y, pos2z = pos2.x + 0.5, pos2.y + 0.5, pos2.z + 0.5
+
+ -- Center of region
+ local center = {
+ x = pos1x + ((pos2x - pos1x) / 2),
+ y = pos1y + ((pos2y - pos1y) / 2),
+ z = pos1z + ((pos2z - pos1z) / 2)
+ }
+ -- Bounding sphere radius
+ local radius = math.sqrt(
+ (center.x - pos1x) ^ 2 +
+ (center.y - pos1y) ^ 2 +
+ (center.z - pos1z) ^ 2)
+ local count = 0
+ for _, obj in pairs(minetest.get_objects_inside_radius(center, radius)) do
+ local entity = obj:get_luaentity()
+ -- Avoid players and WorldEdit entities
+ if not obj:is_player() and (not entity or
+ not entity.name:find("^worldedit:")) then
+ local pos = obj:getpos()
+ if pos.x >= pos1x and pos.x <= pos2x and
+ pos.y >= pos1y and pos.y <= pos2y and
+ pos.z >= pos1z and pos.z <= pos2z then
+ -- Inside region
+ obj:remove()
+ count = count + 1
+ end
+ end
+ end
+ return count
+end
+