Split up some files in worldedit mod

This commit is contained in:
sfan5 2024-04-21 23:54:00 +02:00
parent dc1150fe3d
commit acb3ecefe4
7 changed files with 663 additions and 629 deletions

@ -6,3 +6,10 @@ read_globals = {"minetest", "vector", "VoxelArea", "ItemStack",
globals = {"worldedit"} globals = {"worldedit"}
-- Ignore these errors until someone decides to fix them -- Ignore these errors until someone decides to fix them
ignore = {"212", "213", "411", "412", "421", "422", "431", "432", "631"} 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"},
}

@ -39,6 +39,6 @@ if minetest.settings:get_bool("log_mods") then
end end
if minetest.settings:get_bool("worldedit_run_tests") then if minetest.settings:get_bool("worldedit_run_tests") then
dofile(path .. "/test.lua") dofile(path .. "/test/init.lua")
minetest.after(0, worldedit.run_tests) minetest.after(0, worldedit.run_tests)
end end

@ -98,51 +98,6 @@ function worldedit.replace(pos1, pos2, search_node, replace_node, inverse)
end 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. --- Copies a region along `axis` by `amount` nodes.
-- @param pos1 -- @param pos1
-- @param pos2 -- @param pos2
@ -307,316 +262,6 @@ function worldedit.move(pos1, pos2, axis, amount)
return worldedit.volume(pos1, pos2) return worldedit.volume(pos1, pos2)
end 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. --- Attempts to fix the lighting in a region.
-- @return The number of nodes updated. -- @return The number of nodes updated.

@ -1,7 +1,8 @@
-- TODO: don't shit individual variables into the globals
--------------------- ---------------------
-- Helpers -- Helpers
--------------------- ---------------------
local vec = vector.new local vec = vector.new
local vecw = function(axis, n, base) local vecw = function(axis, n, base)
local ret = vec(base) local ret = vec(base)
@ -16,9 +17,9 @@ local set_node = minetest.set_node
-- Nodes -- Nodes
--------------------- ---------------------
local air = "air" local air = "air"
local testnode1 rawset(_G, "testnode1", "")
local testnode2 rawset(_G, "testnode2", "")
local testnode3 rawset(_G, "testnode3", "")
-- Loads nodenames to use for tests -- Loads nodenames to use for tests
local function init_nodes() local function init_nodes()
testnode1 = minetest.registered_aliases["mapgen_stone"] testnode1 = minetest.registered_aliases["mapgen_stone"]
@ -27,7 +28,7 @@ local function init_nodes()
assert(testnode1 and testnode2 and testnode3) assert(testnode1 and testnode2 and testnode3)
end end
-- Writes repeating pattern into given area -- 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 pos = vec()
local node = {name=""} local node = {name=""}
local i = 1 local i = 1
@ -43,14 +44,14 @@ local function place_pattern(pos1, pos2, pattern)
end end
end end
end end
end end)
--------------------- ---------------------
-- Area management -- Area management
--------------------- ---------------------
assert(minetest.get_mapgen_setting("mg_name") == "singlenode") assert(minetest.get_mapgen_setting("mg_name") == "singlenode")
local area = {} rawset(_G, "area", {})
do do
local areamin, areamax local areamin, areamax
local off local off
@ -151,7 +152,7 @@ end
--------------------- ---------------------
-- Checks -- Checks
--------------------- ---------------------
local check = {} rawset(_G, "check", {})
-- Check that all nodes in [pos1, pos2] are the node(s) specified -- Check that all nodes in [pos1, pos2] are the node(s) specified
check.filled = function(pos1, pos2, nodes) check.filled = function(pos1, pos2, nodes)
if type(nodes) == "string" then if type(nodes) == "string" then
@ -218,7 +219,7 @@ end
-- The actual tests -- The actual tests
--------------------- ---------------------
local tests = {} local tests = {}
local function register_test(name, func, opts) worldedit.register_test = function(name, func, opts)
assert(type(name) == "string") assert(type(name) == "string")
assert(func == nil or type(func) == "function") assert(func == nil or type(func) == "function")
if not opts then if not opts then
@ -230,6 +231,7 @@ local function register_test(name, func, opts)
opts.func = func opts.func = func
table.insert(tests, opts) table.insert(tests, opts)
end end
local register_test = worldedit.register_test
-- How this works: -- How this works:
-- register_test registers a test with a name and function -- register_test registers a test with a name and function
-- The function should return if the test passes or otherwise cause a Lua error -- The function should return if the test passes or otherwise cause a Lua error
@ -279,270 +281,10 @@ register_test("pattern", function()
end) end)
register_test("Generic node manipulations") for _, name in ipairs({
register_test("worldedit.set", function() "manipulations", "schematic"
local pos1, pos2 = area.get(10) }) do
local m = area.margin(1) dofile(minetest.get_modpath("worldedit") .. "/test/" .. name .. ".lua")
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
end end

@ -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)

@ -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

@ -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