From 888b0ebfec8c2eff9015163549a7e47443cb8665 Mon Sep 17 00:00:00 2001 From: Vanessa Ezekowitz Date: Fri, 1 Apr 2016 21:00:20 -0400 Subject: "explode" all modpacks into their individual components (you can't have a modpack buried inside a modpack) --- worldedit/manipulations.lua | 629 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 629 insertions(+) create mode 100644 worldedit/manipulations.lua (limited to 'worldedit/manipulations.lua') 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 + -- cgit v1.2.3