diff --git a/.luacheckrc b/.luacheckrc index 139c9e2..23262c7 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -6,3 +6,10 @@ read_globals = {"minetest", "vector", "VoxelArea", "ItemStack", globals = {"worldedit"} -- Ignore these errors until someone decides to fix them ignore = {"212", "213", "411", "412", "421", "422", "431", "432", "631"} + +files["worldedit/test"] = { + read_globals = {"testnode1", "testnode2", "testnode3", "area", "check", "place_pattern"}, +} +files["worldedit/test/init.lua"] = { + globals = {"testnode1", "testnode2", "testnode3", "area", "check", "place_pattern"}, +} diff --git a/worldedit/init.lua b/worldedit/init.lua index 4f0f2c1..f67cd09 100644 --- a/worldedit/init.lua +++ b/worldedit/init.lua @@ -39,6 +39,6 @@ if minetest.settings:get_bool("log_mods") then end if minetest.settings:get_bool("worldedit_run_tests") then - dofile(path .. "/test.lua") + dofile(path .. "/test/init.lua") minetest.after(0, worldedit.run_tests) end diff --git a/worldedit/manipulations.lua b/worldedit/manipulations.lua index f45aa1e..73d0242 100644 --- a/worldedit/manipulations.lua +++ b/worldedit/manipulations.lua @@ -98,51 +98,6 @@ function worldedit.replace(pos1, pos2, search_node, replace_node, inverse) end -local function deferred_execution(next_one, finished) - -- Allocate 100% of server step for execution (might lag a little) - local allocated_usecs = - tonumber(minetest.settings:get("dedicated_server_step"):split(" ")[1]) * 1000000 - local function f() - local deadline = minetest.get_us_time() + allocated_usecs - repeat - local is_done = next_one() - if is_done then - if finished then - finished() - end - return - end - until minetest.get_us_time() >= deadline - minetest.after(0, f) - end - f() -end - ---- Duplicates a region `amount` times with offset vector `direction`. --- Stacking is spread across server steps. --- @return The number of nodes stacked. -function worldedit.stack2(pos1, pos2, direction, amount, finished) - -- Protect arguments from external changes during execution - pos1 = table.copy(pos1) - pos2 = table.copy(pos2) - direction = table.copy(direction) - - local i = 0 - local translated = vector.new() - local function step() - translated.x = translated.x + direction.x - translated.y = translated.y + direction.y - translated.z = translated.z + direction.z - worldedit.copy2(pos1, pos2, translated) - i = i + 1 - return i >= amount - end - deferred_execution(step, finished) - - return worldedit.volume(pos1, pos2) * amount -end - - --- Copies a region along `axis` by `amount` nodes. -- @param pos1 -- @param pos2 @@ -307,316 +262,6 @@ function worldedit.move(pos1, pos2, axis, amount) return worldedit.volume(pos1, pos2) end ---- Duplicates a region along `axis` `amount` times. --- Stacking is spread across server steps. --- @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, finished) - 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 i, distance = 0, 0 - local function step() - distance = distance + length - worldedit.copy(pos1, pos2, axis, distance) - i = i + 1 - return i >= count - end - deferred_execution(step, finished) - - 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=vector.new(stretch_x, stretch_y, 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 = vector.new(pos2.x, 0, 0) - local big_pos = vector.new() - 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 = vector.new(pos2) - new_pos2[axis1] = pos1[axis1] + extent2 - new_pos2[axis2] = pos1[axis2] + extent1 - - local upper_bound = vector.new(pos2) - 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 = vector.new(pos1.x, 0, 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 = vector.new(pos1.x, 0, 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. -function worldedit.orient(pos1, pos2, angle) - local pos1, pos2 = worldedit.sort_pos(pos1, pos2) - local registered_nodes = minetest.registered_nodes - - local wallmounted = { - [90] = {0, 1, 5, 4, 2, 3, 0, 0}, - [180] = {0, 1, 3, 2, 5, 4, 0, 0}, - [270] = {0, 1, 4, 5, 3, 2, 0, 0} - } - local facedir = { - [90] = { 1, 2, 3, 0, 13, 14, 15, 12, 17, 18, 19, 16, - 9, 10, 11, 8, 5, 6, 7, 4, 23, 20, 21, 22}, - [180] = { 2, 3, 0, 1, 10, 11, 8, 9, 6, 7, 4, 5, - 18, 19, 16, 17, 14, 15, 12, 13, 22, 23, 20, 21}, - [270] = { 3, 0, 1, 2, 19, 16, 17, 18, 15, 12, 13, 14, - 7, 4, 5, 6, 11, 8, 9, 10, 21, 22, 23, 20} - } - - 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 get_node, swap_node = minetest.get_node, minetest.swap_node - local pos = vector.new(pos1.x, 0, 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 - local paramtype2 = def.paramtype2 - if paramtype2 == "wallmounted" or - paramtype2 == "colorwallmounted" then - local orient = node.param2 % 8 - node.param2 = node.param2 - orient + - wallmounted_substitution[orient + 1] - swap_node(pos, node) - count = count + 1 - elseif paramtype2 == "facedir" or - paramtype2 == "colorfacedir" then - local orient = node.param2 % 32 - node.param2 = node.param2 - orient + - facedir_substitution[orient + 1] - swap_node(pos, node) - 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. diff --git a/worldedit/test.lua b/worldedit/test/init.lua similarity index 57% rename from worldedit/test.lua rename to worldedit/test/init.lua index f688dbf..21ba82f 100644 --- a/worldedit/test.lua +++ b/worldedit/test/init.lua @@ -1,7 +1,8 @@ +-- TODO: don't shit individual variables into the globals + --------------------- -- Helpers --------------------- - local vec = vector.new local vecw = function(axis, n, base) local ret = vec(base) @@ -16,9 +17,9 @@ local set_node = minetest.set_node -- Nodes --------------------- local air = "air" -local testnode1 -local testnode2 -local testnode3 +rawset(_G, "testnode1", "") +rawset(_G, "testnode2", "") +rawset(_G, "testnode3", "") -- Loads nodenames to use for tests local function init_nodes() testnode1 = minetest.registered_aliases["mapgen_stone"] @@ -27,7 +28,7 @@ local function init_nodes() assert(testnode1 and testnode2 and testnode3) end -- Writes repeating pattern into given area -local function place_pattern(pos1, pos2, pattern) +rawset(_G, "place_pattern", function(pos1, pos2, pattern) local pos = vec() local node = {name=""} local i = 1 @@ -43,14 +44,14 @@ local function place_pattern(pos1, pos2, pattern) end end end -end +end) --------------------- -- Area management --------------------- assert(minetest.get_mapgen_setting("mg_name") == "singlenode") -local area = {} +rawset(_G, "area", {}) do local areamin, areamax local off @@ -151,7 +152,7 @@ end --------------------- -- Checks --------------------- -local check = {} +rawset(_G, "check", {}) -- Check that all nodes in [pos1, pos2] are the node(s) specified check.filled = function(pos1, pos2, nodes) if type(nodes) == "string" then @@ -218,7 +219,7 @@ end -- The actual tests --------------------- local tests = {} -local function register_test(name, func, opts) +worldedit.register_test = function(name, func, opts) assert(type(name) == "string") assert(func == nil or type(func) == "function") if not opts then @@ -230,6 +231,7 @@ local function register_test(name, func, opts) opts.func = func table.insert(tests, opts) end +local register_test = worldedit.register_test -- How this works: -- register_test registers a test with a name and function -- The function should return if the test passes or otherwise cause a Lua error @@ -279,270 +281,10 @@ register_test("pattern", function() end) -register_test("Generic node manipulations") -register_test("worldedit.set", function() - local pos1, pos2 = area.get(10) - local m = area.margin(1) - - worldedit.set(pos1, pos2, testnode1) - - check.filled(pos1, pos2, testnode1) - check.filled2(m, air) -end) - -register_test("worldedit.set mix", function() - local pos1, pos2 = area.get(10) - local m = area.margin(1) - - worldedit.set(pos1, pos2, {testnode1, testnode2}) - - check.filled(pos1, pos2, {testnode1, testnode2}) - check.filled2(m, air) -end) - -register_test("worldedit.replace", function() - local pos1, pos2 = area.get(10) - local half1, half2 = area.split(pos1, pos2) - - worldedit.set(pos1, half1, testnode1) - worldedit.set(half2, pos2, testnode2) - worldedit.replace(pos1, pos2, testnode1, testnode3) - - check.not_filled(pos1, pos2, testnode1) - check.filled(pos1, half1, testnode3) - check.filled(half2, pos2, testnode2) -end) - -register_test("worldedit.replace inverse", function() - local pos1, pos2 = area.get(10) - local half1, half2 = area.split(pos1, pos2) - - worldedit.set(pos1, half1, testnode1) - worldedit.set(half2, pos2, testnode2) - worldedit.replace(pos1, pos2, testnode1, testnode3, true) - - check.filled(pos1, half1, testnode1) - check.filled(half2, pos2, testnode3) -end) - --- FIXME?: this one looks overcomplicated -register_test("worldedit.copy", function() - local pos1, pos2 = area.get(4) - local axis, n = area.dir(2) - local m = area.margin(1) - local b = pos1[axis] - - -- create one slice with testnode1, one with testnode2 - worldedit.set(pos1, vecw(axis, b + 1, pos2), testnode1) - worldedit.set(vecw(axis, b + 2, pos1), pos2, testnode2) - worldedit.copy(pos1, pos2, axis, n) - - -- should have three slices now - check.filled(pos1, vecw(axis, b + 1, pos2), testnode1) - check.filled(vecw(axis, b + 2, pos1), pos2, testnode1) - check.filled(vecw(axis, b + 4, pos1), vector.add(pos2, vecw(axis, n)), testnode2) - check.filled2(m, air) -end) - -register_test("worldedit.copy2", function() - local pos1, pos2 = area.get(6) - local m1 = area.margin(1) - local pos1_, pos2_ = area.get(6) - local m2 = area.margin(1) - - local pattern = {testnode1, testnode2, testnode3, testnode1, testnode2} - place_pattern(pos1, pos2, pattern) - worldedit.copy2(pos1, pos2, vector.subtract(pos1_, pos1)) - - check.pattern(pos1, pos2, pattern) - check.pattern(pos1_, pos2_, pattern) - check.filled2(m1, air) - check.filled2(m2, air) -end) - -register_test("worldedit.move (overlap)", function() - local pos1, pos2 = area.get(7) - local axis, n = area.dir(2) - local m = area.margin(1) - - local pattern = {testnode2, testnode1, testnode2, testnode3, testnode3} - place_pattern(pos1, pos2, pattern) - worldedit.move(pos1, pos2, axis, n) - - check.filled(pos1, vecw(axis, pos1[axis] + n - 1, pos2), air) - check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern) - check.filled2(m, air) -end) - -register_test("worldedit.move", function() - local pos1, pos2 = area.get(10) - local axis, n = area.dir(10) - local m = area.margin(1) - - local pattern = {testnode1, testnode3, testnode3, testnode2} - place_pattern(pos1, pos2, pattern) - worldedit.move(pos1, pos2, axis, n) - - check.filled(pos1, pos2, air) - check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern) - check.filled2(m, air) -end) - --- TODO: the rest (also testing param2 + metadata) - -register_test("Schematics") -register_test("worldedit.read_header", function() - local value = '5,foo,BAR,-1,234:the content' - local version, header, content = worldedit.read_header(value) - assert(version == 5) - assert(#header == 4) - assert(header[1] == "foo" and header[2] == "BAR") - assert(header[3] == "-1" and header[4] == "234") - assert(content == "the content") -end) - -register_test("worldedit.allocate", function() - local value = '3:-1 0 0 dummy 0 0\n0 0 4 dummy 0 0\n0 1 0 dummy 0 0' - local pos1, pos2, count = worldedit.allocate(vec(1, 1, 1), value) - assert(vector.equals(pos1, vec(0, 1, 1))) - assert(vector.equals(pos2, vec(1, 2, 5))) - assert(count == 3) -end) - -do - local function output_weird(numbers, body) - local s = {"return {"} - for _, parts in ipairs(numbers) do - s[#s+1] = "{" - for _, n in ipairs(parts) do - s[#s+1] = string.format(" {%d},", n) - end - s[#s+1] = "}," - end - return table.concat(s, "\n") .. table.concat(body, "\n") .. "}" - end - local fmt1p = '{\n ["x"]=%d,\n ["y"]=%d,\n ["z"]=%d,\n},' - local fmt1n = '{\n ["name"]="%s",\n},' - local fmt4 = '{ ["x"] = %d, ["y"] = %d, ["z"] = %d, ["meta"] = { ["fields"] = { }, ["inventory"] = { } }, ["param2"] = 0, ["param1"] = 0, ["name"] = "%s" }' - local fmt5 = '{ ["x"] = %d, ["y"] = %d, ["z"] = %d, ["name"] = "%s" }' - local fmt51 = '{[r2]=0,x=%d,y=%d,z=%d,name=r%d}' - local fmt52 = '{x=%d,y=%d,z=%d,name=_[%d]}' - local test_data = { - -- used by WorldEdit 0.2 (first public release) - { - name = "v1", ver = 1, - gen = function(pat) - local numbers = { - {2, 3, 4, 5, 6}, - {7, 8}, {9, 10}, {11, 12}, - {13, 14}, {15, 16} - } - return output_weird(numbers, { - fmt1p:format(0, 0, 0), - fmt1n:format(pat[1]), - fmt1p:format(0, 1, 0), - fmt1n:format(pat[3]), - fmt1p:format(1, 1, 0), - fmt1n:format(pat[1]), - fmt1p:format(1, 0, 1), - fmt1n:format(pat[3]), - fmt1p:format(0, 1, 1), - fmt1n:format(pat[1]), - }) - end - }, - - -- v2: missing because I couldn't find any code in my archives that actually wrote this format - - { - name = "v3", ver = 3, - gen = function(pat) - assert(pat[2] == air) - return table.concat({ - "0 0 0 " .. pat[1] .. " 0 0", - "0 1 0 " .. pat[3] .. " 0 0", - "1 1 0 " .. pat[1] .. " 0 0", - "1 0 1 " .. pat[3] .. " 0 0", - "0 1 1 " .. pat[1] .. " 0 0", - }, "\n") - end - }, - - { - name = "v4", ver = 4, - gen = function(pat) - return table.concat({ - "return { " .. fmt4:format(0, 0, 0, pat[1]), - fmt4:format(0, 1, 0, pat[3]), - fmt4:format(1, 1, 0, pat[1]), - fmt4:format(1, 0, 1, pat[3]), - fmt4:format(0, 1, 1, pat[1]) .. " }", - }, ", ") - end - }, - - -- like v4 but no meta and param (if empty) - { - name = "v5 (pre-5.6)", ver = 5, - gen = function(pat) - return table.concat({ - "5:return { " .. fmt5:format(0, 0, 0, pat[1]), - fmt5:format(0, 1, 0, pat[3]), - fmt5:format(1, 1, 0, pat[1]), - fmt5:format(1, 0, 1, pat[3]), - fmt5:format(0, 1, 1, pat[1]) .. " }", - }, ", ") - end - }, - - -- reworked engine serialization in 5.6 - { - name = "v5 (5.6)", ver = 5, - gen = function(pat) - return table.concat({ - '5:r1="' .. pat[1] .. '";r2="param1";r3="' .. pat[3] .. '";return {' - .. fmt51:format(0, 0, 0, 1), - fmt51:format(0, 1, 0, 3), - fmt51:format(1, 1, 0, 1), - fmt51:format(1, 0, 1, 3), - fmt51:format(0, 1, 1, 1) .. "}", - }, ",") - end - }, - - -- small changes on engine side again - { - name = "v5 (post-5.7)", ver = 5, - gen = function(pat) - return table.concat({ - '5:local _={};_[1]="' .. pat[1] .. '";_[3]="' .. pat[3] .. '";return {' - .. fmt52:format(0, 0, 0, 1), - fmt52:format(0, 1, 0, 3), - fmt52:format(1, 1, 0, 1), - fmt52:format(1, 0, 1, 3), - fmt52:format(0, 1, 1, 1) .. "}", - }, ",") - end - }, - } - for _, e in ipairs(test_data) do - register_test("worldedit.deserialize " .. e.name, function() - local pos1, pos2 = area.get(2) - local m = area.margin(1) - - local pat = {testnode3, air, testnode2} - local value = e.gen(pat) - assert(type(value) == "string") - - local version = worldedit.read_header(value) - assert(version == e.ver, "version: got " .. tostring(version) .. " expected " .. e.ver) - local count = worldedit.deserialize(pos1, value) - assert(count ~= nil and count > 0) - - check.pattern(pos1, pos2, pat) - check.filled2(m, air) - end) - end +for _, name in ipairs({ + "manipulations", "schematic" +}) do + dofile(minetest.get_modpath("worldedit") .. "/test/" .. name .. ".lua") end diff --git a/worldedit/test/manipulations.lua b/worldedit/test/manipulations.lua new file mode 100644 index 0000000..10368a3 --- /dev/null +++ b/worldedit/test/manipulations.lua @@ -0,0 +1,121 @@ +--------------------- +local vec = vector.new +local vecw = function(axis, n, base) + local ret = vec(base) + ret[axis] = n + return ret +end +local air = "air" +--------------------- + + +worldedit.register_test("Generic node manipulations") +worldedit.register_test("worldedit.set", function() + local pos1, pos2 = area.get(10) + local m = area.margin(1) + + worldedit.set(pos1, pos2, testnode1) + + check.filled(pos1, pos2, testnode1) + check.filled2(m, air) +end) + +worldedit.register_test("worldedit.set mix", function() + local pos1, pos2 = area.get(10) + local m = area.margin(1) + + worldedit.set(pos1, pos2, {testnode1, testnode2}) + + check.filled(pos1, pos2, {testnode1, testnode2}) + check.filled2(m, air) +end) + +worldedit.register_test("worldedit.replace", function() + local pos1, pos2 = area.get(10) + local half1, half2 = area.split(pos1, pos2) + + worldedit.set(pos1, half1, testnode1) + worldedit.set(half2, pos2, testnode2) + worldedit.replace(pos1, pos2, testnode1, testnode3) + + check.not_filled(pos1, pos2, testnode1) + check.filled(pos1, half1, testnode3) + check.filled(half2, pos2, testnode2) +end) + +worldedit.register_test("worldedit.replace inverse", function() + local pos1, pos2 = area.get(10) + local half1, half2 = area.split(pos1, pos2) + + worldedit.set(pos1, half1, testnode1) + worldedit.set(half2, pos2, testnode2) + worldedit.replace(pos1, pos2, testnode1, testnode3, true) + + check.filled(pos1, half1, testnode1) + check.filled(half2, pos2, testnode3) +end) + +-- FIXME?: this one looks overcomplicated +worldedit.register_test("worldedit.copy", function() + local pos1, pos2 = area.get(4) + local axis, n = area.dir(2) + local m = area.margin(1) + local b = pos1[axis] + + -- create one slice with testnode1, one with testnode2 + worldedit.set(pos1, vecw(axis, b + 1, pos2), testnode1) + worldedit.set(vecw(axis, b + 2, pos1), pos2, testnode2) + worldedit.copy(pos1, pos2, axis, n) + + -- should have three slices now + check.filled(pos1, vecw(axis, b + 1, pos2), testnode1) + check.filled(vecw(axis, b + 2, pos1), pos2, testnode1) + check.filled(vecw(axis, b + 4, pos1), vector.add(pos2, vecw(axis, n)), testnode2) + check.filled2(m, air) +end) + +worldedit.register_test("worldedit.copy2", function() + local pos1, pos2 = area.get(6) + local m1 = area.margin(1) + local pos1_, pos2_ = area.get(6) + local m2 = area.margin(1) + + local pattern = {testnode1, testnode2, testnode3, testnode1, testnode2} + place_pattern(pos1, pos2, pattern) + worldedit.copy2(pos1, pos2, vector.subtract(pos1_, pos1)) + + check.pattern(pos1, pos2, pattern) + check.pattern(pos1_, pos2_, pattern) + check.filled2(m1, air) + check.filled2(m2, air) +end) + +worldedit.register_test("worldedit.move (overlap)", function() + local pos1, pos2 = area.get(7) + local axis, n = area.dir(2) + local m = area.margin(1) + + local pattern = {testnode2, testnode1, testnode2, testnode3, testnode3} + place_pattern(pos1, pos2, pattern) + worldedit.move(pos1, pos2, axis, n) + + check.filled(pos1, vecw(axis, pos1[axis] + n - 1, pos2), air) + check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern) + check.filled2(m, air) +end) + +worldedit.register_test("worldedit.move", function() + local pos1, pos2 = area.get(10) + local axis, n = area.dir(10) + local m = area.margin(1) + + local pattern = {testnode1, testnode3, testnode3, testnode2} + place_pattern(pos1, pos2, pattern) + worldedit.move(pos1, pos2, axis, n) + + check.filled(pos1, pos2, air) + check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern) + check.filled2(m, air) +end) + +-- TODO: the rest (also testing param2 + metadata) diff --git a/worldedit/test/schematic.lua b/worldedit/test/schematic.lua new file mode 100644 index 0000000..2866a32 --- /dev/null +++ b/worldedit/test/schematic.lua @@ -0,0 +1,162 @@ +--------------------- +local vec = vector.new +local air = "air" +--------------------- + + +local function output_weird(numbers, body) + local s = {"return {"} + for _, parts in ipairs(numbers) do + s[#s+1] = "{" + for _, n in ipairs(parts) do + s[#s+1] = string.format(" {%d},", n) + end + s[#s+1] = "}," + end + return table.concat(s, "\n") .. table.concat(body, "\n") .. "}" +end + +local fmt1p = '{\n ["x"]=%d,\n ["y"]=%d,\n ["z"]=%d,\n},' +local fmt1n = '{\n ["name"]="%s",\n},' +local fmt4 = '{ ["x"] = %d, ["y"] = %d, ["z"] = %d, ["meta"] = { ["fields"] = { }, ["inventory"] = { } }, ["param2"] = 0, ["param1"] = 0, ["name"] = "%s" }' +local fmt5 = '{ ["x"] = %d, ["y"] = %d, ["z"] = %d, ["name"] = "%s" }' +local fmt51 = '{[r2]=0,x=%d,y=%d,z=%d,name=r%d}' +local fmt52 = '{x=%d,y=%d,z=%d,name=_[%d]}' + +local test_data = { + -- used by WorldEdit 0.2 (first public release) + { + name = "v1", ver = 1, + gen = function(pat) + local numbers = { + {2, 3, 4, 5, 6}, + {7, 8}, {9, 10}, {11, 12}, + {13, 14}, {15, 16} + } + return output_weird(numbers, { + fmt1p:format(0, 0, 0), + fmt1n:format(pat[1]), + fmt1p:format(0, 1, 0), + fmt1n:format(pat[3]), + fmt1p:format(1, 1, 0), + fmt1n:format(pat[1]), + fmt1p:format(1, 0, 1), + fmt1n:format(pat[3]), + fmt1p:format(0, 1, 1), + fmt1n:format(pat[1]), + }) + end + }, + + -- v2: missing because I couldn't find any code in my archives that actually wrote this format + + { + name = "v3", ver = 3, + gen = function(pat) + assert(pat[2] == air) + return table.concat({ + "0 0 0 " .. pat[1] .. " 0 0", + "0 1 0 " .. pat[3] .. " 0 0", + "1 1 0 " .. pat[1] .. " 0 0", + "1 0 1 " .. pat[3] .. " 0 0", + "0 1 1 " .. pat[1] .. " 0 0", + }, "\n") + end + }, + + { + name = "v4", ver = 4, + gen = function(pat) + return table.concat({ + "return { " .. fmt4:format(0, 0, 0, pat[1]), + fmt4:format(0, 1, 0, pat[3]), + fmt4:format(1, 1, 0, pat[1]), + fmt4:format(1, 0, 1, pat[3]), + fmt4:format(0, 1, 1, pat[1]) .. " }", + }, ", ") + end + }, + + -- like v4 but no meta and param (if empty) + { + name = "v5 (pre-5.6)", ver = 5, + gen = function(pat) + return table.concat({ + "5:return { " .. fmt5:format(0, 0, 0, pat[1]), + fmt5:format(0, 1, 0, pat[3]), + fmt5:format(1, 1, 0, pat[1]), + fmt5:format(1, 0, 1, pat[3]), + fmt5:format(0, 1, 1, pat[1]) .. " }", + }, ", ") + end + }, + + -- reworked engine serialization in 5.6 + { + name = "v5 (5.6)", ver = 5, + gen = function(pat) + return table.concat({ + '5:r1="' .. pat[1] .. '";r2="param1";r3="' .. pat[3] .. '";return {' + .. fmt51:format(0, 0, 0, 1), + fmt51:format(0, 1, 0, 3), + fmt51:format(1, 1, 0, 1), + fmt51:format(1, 0, 1, 3), + fmt51:format(0, 1, 1, 1) .. "}", + }, ",") + end + }, + + -- small changes on engine side again + { + name = "v5 (post-5.7)", ver = 5, + gen = function(pat) + return table.concat({ + '5:local _={};_[1]="' .. pat[1] .. '";_[3]="' .. pat[3] .. '";return {' + .. fmt52:format(0, 0, 0, 1), + fmt52:format(0, 1, 0, 3), + fmt52:format(1, 1, 0, 1), + fmt52:format(1, 0, 1, 3), + fmt52:format(0, 1, 1, 1) .. "}", + }, ",") + end + }, +} + + +worldedit.register_test("Schematics") +worldedit.register_test("worldedit.read_header", function() + local value = '5,foo,BAR,-1,234:the content' + local version, header, content = worldedit.read_header(value) + assert(version == 5) + assert(#header == 4) + assert(header[1] == "foo" and header[2] == "BAR") + assert(header[3] == "-1" and header[4] == "234") + assert(content == "the content") +end) + +worldedit.register_test("worldedit.allocate", function() + local value = '3:-1 0 0 dummy 0 0\n0 0 4 dummy 0 0\n0 1 0 dummy 0 0' + local pos1, pos2, count = worldedit.allocate(vec(1, 1, 1), value) + assert(vector.equals(pos1, vec(0, 1, 1))) + assert(vector.equals(pos2, vec(1, 2, 5))) + assert(count == 3) +end) + +for _, e in ipairs(test_data) do + worldedit.register_test("worldedit.deserialize " .. e.name, function() + local pos1, pos2 = area.get(2) + local m = area.margin(1) + + local pat = {testnode3, air, testnode2} + local value = e.gen(pat) + assert(type(value) == "string") + + local version = worldedit.read_header(value) + assert(version == e.ver, "version: got " .. tostring(version) .. " expected " .. e.ver) + local count = worldedit.deserialize(pos1, value) + assert(count ~= nil and count > 0) + + check.pattern(pos1, pos2, pat) + check.filled2(m, air) + end) +end diff --git a/worldedit/transformations.lua b/worldedit/transformations.lua new file mode 100644 index 0000000..c33b93d --- /dev/null +++ b/worldedit/transformations.lua @@ -0,0 +1,357 @@ +--- Node transformations. +-- @module worldedit.transformations + +worldedit.deferred_execution = function(next_one, finished) + -- Allocate 80% of server step for execution + local allocated_usecs = + tonumber(minetest.settings:get("dedicated_server_step"):split(" ")[1]) * 1000000 * 0.8 + local function f() + local deadline = minetest.get_us_time() + allocated_usecs + repeat + local is_done = next_one() + if is_done then + if finished then + finished() + end + return + end + until minetest.get_us_time() >= deadline + minetest.after(0, f) + end + f() +end + +--- Duplicates a region `amount` times with offset vector `direction`. +-- Stacking is spread across server steps. +-- @return The number of nodes stacked. +function worldedit.stack2(pos1, pos2, direction, amount, finished) + -- Protect arguments from external changes during execution + pos1 = vector.copy(pos1) + pos2 = vector.copy(pos2) + direction = vector.copy(direction) + + local i = 0 + local translated = vector.new() + local function step() + translated.x = translated.x + direction.x + translated.y = translated.y + direction.y + translated.z = translated.z + direction.z + worldedit.copy2(pos1, pos2, translated) + i = i + 1 + return i >= amount + end + worldedit.deferred_execution(step, finished) + + return worldedit.volume(pos1, pos2) * amount +end + + +--- Duplicates a region along `axis` `amount` times. +-- Stacking is spread across server steps. +-- @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, finished) + 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 i, distance = 0, 0 + local function step() + distance = distance + length + worldedit.copy(pos1, pos2, axis, distance) + i = i + 1 + return i >= count + end + worldedit.deferred_execution(step, finished) + + 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=vector.new(stretch_x, stretch_y, 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 = vector.new(pos2.x, 0, 0) + local big_pos = vector.new() + 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 = vector.new(pos2) + new_pos2[axis1] = pos1[axis1] + extent2 + new_pos2[axis2] = pos1[axis2] + extent1 + + local upper_bound = vector.new(pos2) + 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 = vector.new(pos1.x, 0, 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 = vector.new(pos1.x, 0, 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. +function worldedit.orient(pos1, pos2, angle) + local pos1, pos2 = worldedit.sort_pos(pos1, pos2) + local registered_nodes = minetest.registered_nodes + + local wallmounted = { + [90] = {0, 1, 5, 4, 2, 3, 0, 0}, + [180] = {0, 1, 3, 2, 5, 4, 0, 0}, + [270] = {0, 1, 4, 5, 3, 2, 0, 0} + } + local facedir = { + [90] = { 1, 2, 3, 0, 13, 14, 15, 12, 17, 18, 19, 16, + 9, 10, 11, 8, 5, 6, 7, 4, 23, 20, 21, 22}, + [180] = { 2, 3, 0, 1, 10, 11, 8, 9, 6, 7, 4, 5, + 18, 19, 16, 17, 14, 15, 12, 13, 22, 23, 20, 21}, + [270] = { 3, 0, 1, 2, 19, 16, 17, 18, 15, 12, 13, 14, + 7, 4, 5, 6, 11, 8, 9, 10, 21, 22, 23, 20} + } + + 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 get_node, swap_node = minetest.get_node, minetest.swap_node + local pos = vector.new(pos1.x, 0, 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 + local paramtype2 = def.paramtype2 + if paramtype2 == "wallmounted" or + paramtype2 == "colorwallmounted" then + local orient = node.param2 % 8 + node.param2 = node.param2 - orient + + wallmounted_substitution[orient + 1] + swap_node(pos, node) + count = count + 1 + elseif paramtype2 == "facedir" or + paramtype2 == "colorfacedir" then + local orient = node.param2 % 32 + node.param2 = node.param2 - orient + + facedir_substitution[orient + 1] + swap_node(pos, node) + 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