1
0
mirror of https://github.com/Uberi/Minetest-WorldEdit.git synced 2025-07-10 12:00:27 +02:00

5 Commits

Author SHA1 Message Date
abc9efeeb8 Harden deserialize workaround against unexpected input
Otherwise it will stop working entirely soon when the
serialization inside Minetest is reworked.
This allows it to work at least in the cases where the original
bug (LuaJIT register limit) doesn't apply.
2022-06-06 20:39:15 +02:00
c223ca4cec Update IRC link in README
closes #207
2021-11-15 00:16:34 +01:00
c8afa95542 Make worldedit_gui error non-fatal
to allow worldedit to be used in worldmods
2021-09-21 20:47:57 +02:00
670e421f57 Rename util folder
because mod loading woes, minetest/minetest#11240
2021-09-21 01:34:10 +02:00
770601dd5d Add automated tests for WorldEdit API functions that run under CI
uses a real Minetest instance (Docker)
currently covers most basic manipulations
2021-09-20 23:10:04 +02:00
8 changed files with 562 additions and 280 deletions

11
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,11 @@
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests
run: MINETEST_VER=latest ./.util/run_tests.sh

30
.util/run_tests.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
tempdir=/tmp/mt
confpath=$tempdir/minetest.conf
worldpath=$tempdir/world
use_docker=y
[ -x ../../bin/minetestserver ] && use_docker=
rm -rf $tempdir
mkdir -p $worldpath
# the docker image doesn't have devtest
[ -n "$use_docker" ] || printf '%s\n' gameid=devtest >$worldpath/world.mt
printf '%s\n' mg_name=singlenode '[end_of_params]' >$worldpath/map_meta.txt
printf '%s\n' worldedit_run_tests=true max_forceloaded_blocks=9999 >$confpath
if [ -n "$use_docker" ]; then
chmod -R 777 $tempdir
docker run --rm -i \
-v $confpath:/etc/minetest/minetest.conf \
-v $tempdir:/var/lib/minetest/.minetest \
-v "$PWD/worldedit":/var/lib/minetest/.minetest/world/worldmods/worldedit \
registry.gitlab.com/minetest/minetest/server:${MINETEST_VER}
else
mkdir $worldpath/worldmods
ln -s "$PWD/worldedit" $worldpath/worldmods/worldedit
../../bin/minetestserver --config $confpath --world $worldpath --logfile /dev/null
fi
test -f $worldpath/tests_ok || exit 1
exit 0

View File

@ -23,7 +23,7 @@ There is a nice installation guide over at the [Minetest Wiki](http://wiki.minet
8. You should have a mod selection screen. Select the one named something like `Minetest-WorldEdit` by left clicking once and press the **Enable Modpack** button. 8. You should have a mod selection screen. Select the one named something like `Minetest-WorldEdit` by left clicking once and press the **Enable Modpack** button.
9. Press the **Save** button. You can now use WorldEdit in that world. Repeat steps 7 to 9 to enable WorldEdit for other worlds too. 9. Press the **Save** button. You can now use WorldEdit in that world. Repeat steps 7 to 9 to enable WorldEdit for other worlds too.
If you are having trouble, try asking for help in the [IRC channel](https://webchat.freenode.net/?channels=#minetest) (faster but may not always have helpers online) If you are having trouble, try asking for help in the [IRC channel](https://web.libera.chat/#minetest) (faster but may not always have helpers online)
or ask on the [forum topic](https://forum.minetest.net/viewtopic.php?id=572) (slower but more likely to get help). or ask on the [forum topic](https://forum.minetest.net/viewtopic.php?id=572) (slower but more likely to get help).
Usage Usage

View File

@ -38,3 +38,7 @@ if minetest.settings:get_bool("log_mods") then
print("[WorldEdit] Loaded!") print("[WorldEdit] Loaded!")
end end
if minetest.settings:get_bool("worldedit_run_tests") then
dofile(path .. "/test.lua")
minetest.after(0, worldedit.run_tests)
end

View File

@ -1,7 +1,9 @@
--- Schematic serialization and deserialiation. --- Schematic serialization and deserialiation.
-- @module worldedit.serialization -- @module worldedit.serialization
worldedit.LATEST_SERIALIZATION_VERSION = 6 worldedit.LATEST_SERIALIZATION_VERSION = 5
local LATEST_SERIALIZATION_HEADER = worldedit.LATEST_SERIALIZATION_VERSION .. ":"
--[[ --[[
Serialization version history: Serialization version history:
@ -13,7 +15,6 @@ Serialization version history:
`name`, `param1`, `param2`, and `meta` fields. `name`, `param1`, `param2`, and `meta` fields.
5: Added header and made `param1`, `param2`, and `meta` fields optional. 5: Added header and made `param1`, `param2`, and `meta` fields optional.
Header format: <Version>,<ExtraHeaderField1>,...:<Content> Header format: <Version>,<ExtraHeaderField1>,...:<Content>
6: Much more complicated but also better format
--]] --]]
@ -65,105 +66,17 @@ function worldedit.serialize(pos1, pos2)
has_meta[hash_node_position(meta_positions[i])] = true has_meta[hash_node_position(meta_positions[i])] = true
end end
-- Decide axis of saved rows local pos = {x=pos1.x, y=0, z=0}
local dim = vector.add(vector.subtract(pos2, pos1), 1)
local axis
if dim.x * dim.y < math.min(dim.y * dim.z, dim.x * dim.z) then
axis = "z"
elseif dim.x * dim.z < math.min(dim.x * dim.y, dim.y * dim.z) then
axis = "y"
elseif dim.y * dim.z < math.min(dim.x * dim.y, dim.x * dim.z) then
axis = "x"
else
axis = "x" -- X or Z are usually most efficient
end
local other1, other2 = worldedit.get_axis_others(axis)
-- Helper functions (1)
local MATCH_DIST = 8
local function match_init(array, first_value)
array[1] = first_value
return {first_value}
end
local function match_try(cache, prev_pushed, value)
local i = #cache
while i >= 1 do
if cache[i] == value then
local ret = -(#cache - i + 1)
local was_value = type(prev_pushed) ~= "number" or prev_pushed >= 0
return ret, (was_value and ret == -1) or prev_pushed == ret
end
i = i - 1
end
return nil, false
end
local function match_push(cache, match, value)
if match ~= nil then -- don't advance cache
return match
end
local idx = #cache + 1
cache[idx] = value
if idx > MATCH_DIST then
table.remove(cache, 1)
end
return value
end
-- Helper functions (2)
local function cur_new(pos, pos1)
return {
a = axis,
p = {pos.x - pos1.x, pos.y - pos1.y, pos.z - pos1.z},
c = 1,
data = {},
param1 = {},
param2 = {},
meta = {},
}
end
local function is_emptyish(t)
-- returns true if <t> contains only one element and that one element is == 0
local seen = false
for _, value in pairs(t) do
if not seen then
if value ~= 0 then
return false
end
seen = true
else
return false
end
end
return true
end
local function cur_finish(result, cur)
if is_emptyish(cur.param1) then
cur.param1 = nil
end
if is_emptyish(cur.param2) then
cur.param2 = nil
end
if next(cur.meta) == nil then
cur.meta = nil
end
result[#result + 1] = cur
end
-- Serialize stuff
local pos = {}
local count = 0 local count = 0
local result = {} local result = {}
local cur while pos.x <= pos2.x do
local cache_data, cache_param1, cache_param2 pos.y = pos1.y
local prev_data, prev_param1, prev_param2 while pos.y <= pos2.y do
pos[other1] = pos1[other1] pos.z = pos1.z
while pos[other1] <= pos2[other1] do while pos.z <= pos2.z do
pos[other2] = pos1[other2]
while pos[other2] <= pos2[other2] do
pos[axis] = pos1[axis]
while pos[axis] <= pos2[axis] do
local node = get_node(pos) local node = get_node(pos)
if node.name ~= "air" and node.name ~= "ignore" then if node.name ~= "air" and node.name ~= "ignore" then
count = count + 1
local meta local meta
if has_meta[hash_node_position(pos)] then if has_meta[hash_node_position(pos)] then
@ -180,76 +93,36 @@ function worldedit.serialize(pos1, pos2)
end end
end end
if cur == nil then -- Start a new row result[count] = {
cur = cur_new(pos, pos1, axis, other1, other2) x = pos.x - pos1.x,
y = pos.y - pos1.y,
cache_data = match_init(cur.data, node.name) z = pos.z - pos1.z,
cache_param1 = match_init(cur.param1, node.param1) name = node.name,
cache_param2 = match_init(cur.param2, node.param2) param1 = node.param1 ~= 0 and node.param1 or nil,
prev_data = cur.data[1] param2 = node.param2 ~= 0 and node.param2 or nil,
prev_param1 = cur.param1[1] meta = meta,
prev_param2 = cur.param2[1] }
cur.meta[1] = meta
else -- Append to existing row
local next_c = cur.c + 1
cur.c = next_c
local value, m, can_omit
value = node.name
m, can_omit = match_try(cache_data, prev_data, node.name)
if not can_omit then
prev_data = match_push(cache_data, m, value)
cur.data[next_c] = prev_data
end end
pos.z = pos.z + 1
value = node.param1
m, can_omit = match_try(cache_param1, prev_param1, value)
if not can_omit then
prev_param1 = match_push(cache_param1, m, value)
cur.param1[next_c] = prev_param1
end end
pos.y = pos.y + 1
value = node.param2
m, can_omit = match_try(cache_param2, prev_param2, value)
if not can_omit then
prev_param2 = match_push(cache_param2, m, value)
cur.param2[next_c] = prev_param2
end end
pos.x = pos.x + 1
cur.meta[next_c] = meta
end end
count = count + 1
else
if cur ~= nil then -- Finish row
cur_finish(result, cur)
cur = nil
end
end
pos[axis] = pos[axis] + 1
end
if cur ~= nil then -- Finish leftover row
cur_finish(result, cur)
cur = nil
end
pos[other2] = pos[other2] + 1
end
pos[other1] = pos[other1] + 1
end
-- Serialize entries -- Serialize entries
result = minetest.serialize(result) result = minetest.serialize(result)
return tonumber(worldedit.LATEST_SERIALIZATION_VERSION) .. "," .. return LATEST_SERIALIZATION_HEADER .. result, count
string.format("%d,%d,%d:", dim.x, dim.y, dim.z) .. result, count
end end
-- Contains code based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile)
-- by ChillCode, available under the MIT license.
local function deserialize_workaround(content) local function deserialize_workaround(content)
local nodes local nodes
if not minetest.global_exists("jit") then if not minetest.global_exists("jit") then
nodes = minetest.deserialize(content, true) nodes = minetest.deserialize(content, true)
elseif not content:match("^%s*return%s*{") then
-- The data doesn't look like we expect it to so we can't apply the workaround.
-- hope for the best
minetest.log("warning", "WorldEdit: deserializing data but can't apply LuaJIT workaround")
nodes = minetest.deserialize(content, true)
else else
-- XXX: This is a filthy hack that works surprisingly well -- XXX: This is a filthy hack that works surprisingly well
-- in LuaJIT, `minetest.deserialize` will fail due to the register limit -- in LuaJIT, `minetest.deserialize` will fail due to the register limit
@ -260,7 +133,7 @@ local function deserialize_workaround(content)
local startpos, startpos1 = 1, 1 local startpos, startpos1 = 1, 1
local endpos local endpos
while true do -- go through each individual node entry (except the last) while true do -- go through each individual node entry (except the last)
startpos, endpos = escaped:find("},%s*{", startpos) startpos, endpos = escaped:find("}%s*,%s*{", startpos)
if not startpos then if not startpos then
break break
end end
@ -277,7 +150,8 @@ end
--- Loads the schematic in `value` into a node list in the latest format. --- Loads the schematic in `value` into a node list in the latest format.
-- @return A node list in the latest format, or nil on failure. -- @return A node list in the latest format, or nil on failure.
local function legacy_load_schematic(version, header, content) local function load_schematic(value)
local version, header, content = worldedit.read_header(value)
local nodes = {} local nodes = {}
if version == 1 or version == 2 then -- Original flat table format if version == 1 or version == 2 then -- Original flat table format
local tables = minetest.deserialize(content, true) local tables = minetest.deserialize(content, true)
@ -319,8 +193,6 @@ local function legacy_load_schematic(version, header, content)
end end
elseif version == 4 or version == 5 then -- Nested table format elseif version == 4 or version == 5 then -- Nested table format
nodes = deserialize_workaround(content) nodes = deserialize_workaround(content)
elseif version >= 6 then
error("legacy_load_schematic called for non-legacy schematic")
else else
return nil return nil
end end
@ -333,29 +205,14 @@ end
-- @return High corner position. -- @return High corner position.
-- @return The number of nodes. -- @return The number of nodes.
function worldedit.allocate(origin_pos, value) function worldedit.allocate(origin_pos, value)
local version, header, content = worldedit.read_header(value) local nodes = load_schematic(value)
if version == 6 then
local content = deserialize_workaround(content)
local pos2 = {
x = origin_pos.x + tonumber(header[1]) - 1,
y = origin_pos.y + tonumber(header[2]) - 1,
z = origin_pos.z + tonumber(header[3]) - 1,
}
local count = 0
for _, row in ipairs(content) do
count = count + row.c
end
return origin_pos, pos2, count
else
local nodes = legacy_load_schematic(version, header, content)
if not nodes or #nodes == 0 then return nil end if not nodes or #nodes == 0 then return nil end
return worldedit.legacy_allocate_with_nodes(origin_pos, nodes) return worldedit.allocate_with_nodes(origin_pos, nodes)
end
end end
-- Internal -- Internal
function worldedit.legacy_allocate_with_nodes(origin_pos, nodes) function worldedit.allocate_with_nodes(origin_pos, nodes)
local huge = math.huge local huge = math.huge
local pos1x, pos1y, pos1z = huge, huge, huge local pos1x, pos1y, pos1z = huge, huge, huge
local pos2x, pos2y, pos2z = -huge, -huge, -huge local pos2x, pos2y, pos2z = -huge, -huge, -huge
@ -378,22 +235,11 @@ end
--- Loads the nodes represented by string `value` at position `origin_pos`. --- Loads the nodes represented by string `value` at position `origin_pos`.
-- @return The number of nodes deserialized. -- @return The number of nodes deserialized.
function worldedit.deserialize(origin_pos, value) function worldedit.deserialize(origin_pos, value)
local version, header, content = worldedit.read_header(value) local nodes = load_schematic(value)
if version == 6 then if not nodes then return nil end
local content = deserialize_workaround(content) if #nodes == 0 then return #nodes end
local pos2 = {
x = origin_pos.x + tonumber(header[1]) - 1,
y = origin_pos.y + tonumber(header[2]) - 1,
z = origin_pos.z + tonumber(header[3]) - 1,
}
worldedit.keep_loaded(origin_pos, pos2)
return worldedit.deserialize_with_content(origin_pos, content) local pos1, pos2 = worldedit.allocate_with_nodes(origin_pos, nodes)
else
local nodes = legacy_load_schematic(version, header, content)
if not nodes or #nodes == 0 then return nil end
local pos1, pos2 = worldedit.legacy_allocate_with_nodes(origin_pos, nodes)
worldedit.keep_loaded(pos1, pos2) worldedit.keep_loaded(pos1, pos2)
local origin_x, origin_y, origin_z = origin_pos.x, origin_pos.y, origin_pos.z local origin_x, origin_y, origin_z = origin_pos.x, origin_pos.y, origin_pos.z
@ -409,79 +255,4 @@ function worldedit.deserialize(origin_pos, value)
end end
return #nodes return #nodes
end end
end
-- Internal
function worldedit.deserialize_with_content(origin_pos, content)
-- Helper functions
local function resolve_refs(array)
-- find (and cache) highest index
local max_i = 1
for i, _ in pairs(array) do
if i > max_i then max_i = i end
end
array.max_i = max_i
-- resolve references
local cache = {}
for i = 1, max_i do
local v = array[i]
if v ~= nil then
if type(v) == "number" and v < 0 then -- is a reference
array[i] = cache[#cache + v + 1]
else
cache[#cache + 1] = v
end
end
end
end
local function read_in_array(array, idx)
if idx > array.max_i then
return array[array.max_i]
end
-- go backwards until we find something
repeat
local v = array[idx]
if v ~= nil then
return v
end
idx = idx - 1
until idx == 0
assert(false)
end
-- Actually deserialize
local count = 0
local entry = {}
local add_node, get_meta = minetest.add_node, minetest.get_meta
for _, row in ipairs(content) do
local axis = row.a
local pos = {
x = origin_pos.x + row.p[1],
y = origin_pos.y + row.p[2],
z = origin_pos.z + row.p[3],
}
if row.param1 == nil then row.param1 = {0} end
if row.param2 == nil then row.param2 = {0} end
if row.meta == nil then row.meta = {} end
resolve_refs(row.data)
resolve_refs(row.param1)
resolve_refs(row.param2)
for i = 1, row.c do
entry.name = read_in_array(row.data, i)
entry.param1 = read_in_array(row.param1, i)
entry.param2 = read_in_array(row.param2, i)
add_node(pos, entry)
local meta = row.meta[i]
if meta then
get_meta(pos):from_table(meta)
end
pos[axis] = pos[axis] + 1
end
count = count + row.c
end
return count
end

448
worldedit/test.lua Normal file
View File

@ -0,0 +1,448 @@
---------------------
-- Helpers
---------------------
local vec = vector.new
local vecw = function(axis, n, base)
local ret = vec(base)
ret[axis] = n
return ret
end
local pos2str = minetest.pos_to_string
local get_node = minetest.get_node
local set_node = minetest.set_node
---------------------
-- Nodes
---------------------
local air = "air"
local testnode1
local testnode2
local testnode3
-- Loads nodenames to use for tests
local function init_nodes()
testnode1 = minetest.registered_aliases["mapgen_stone"]
testnode2 = minetest.registered_aliases["mapgen_dirt"]
testnode3 = minetest.registered_aliases["mapgen_cobble"] or minetest.registered_aliases["mapgen_dirt_with_grass"]
assert(testnode1 and testnode2 and testnode3)
end
-- Writes repeating pattern into given area
local function place_pattern(pos1, pos2, pattern)
local pos = vec()
local node = {name=""}
local i = 1
for z = pos1.z, pos2.z do
pos.z = z
for y = pos1.y, pos2.y do
pos.y = y
for x = pos1.x, pos2.x do
pos.x = x
node.name = pattern[i]
set_node(pos, node)
i = i % #pattern + 1
end
end
end
end
---------------------
-- Area management
---------------------
assert(minetest.get_mapgen_setting("mg_name") == "singlenode")
local area = {}
do
local areamin, areamax
local off
local c_air = minetest.get_content_id(air)
local vbuffer = {}
-- Assign a new area for use, will emerge and then call ready()
area.assign = function(min, max, ready)
areamin = min
areamax = max
minetest.emerge_area(min, max, function(bpos, action, remaining)
assert(action ~= minetest.EMERGE_ERRORED)
if remaining > 0 then return end
minetest.after(0, function()
area.clear()
ready()
end)
end)
end
-- Reset area contents and state
area.clear = function()
local vmanip = minetest.get_voxel_manip(areamin, areamax)
local vpos1, vpos2 = vmanip:get_emerged_area()
local vcount = (vpos2.x - vpos1.x + 1) * (vpos2.y - vpos1.y + 1) * (vpos2.z - vpos1.z + 1)
if #vbuffer ~= vcount then
vbuffer = {}
for i = 1, vcount do
vbuffer[i] = c_air
end
end
vmanip:set_data(vbuffer)
vmanip:write_to_map()
off = vec(0, 0, 0)
end
-- Returns an usable area [pos1, pos2] that does not overlap previous ones
area.get = function(sizex, sizey, sizez)
local size
if sizey == nil or sizez == nil then
size = {x=sizex, y=sizex, z=sizex}
else
size = {x=sizex, y=sizey, z=sizez}
end
local pos1 = vector.add(areamin, off)
local pos2 = vector.subtract(vector.add(pos1, size), 1)
if pos2.x > areamax.x or pos2.y > areamax.y or pos2.z > areamax.z then
error("Internal failure: out of space")
end
off = vector.add(off, size)
return pos1, pos2
end
-- Returns an axis and count (= n) relative to the last-requested area that is unoccupied
area.dir = function(n)
local pos1 = vector.add(areamin, off)
if pos1.x + n <= areamax.x then
off.x = off.x + n
return "x", n
elseif pos1.x + n <= areamax.y then
off.y = off.y + n
return "y", n
elseif pos1.z + n <= areamax.z then
off.z = off.z + n
return "z", n
end
error("Internal failure: out of space")
end
-- Returns [XYZ] margin (list of pos pairs) of n around last-requested area
-- (may actually be larger but doesn't matter)
area.margin = function(n)
local pos1, pos2 = area.get(n)
return {
{ vec(areamin.x, areamin.y, pos1.z), pos2 }, -- X/Y
{ vec(areamin.x, pos1.y, areamin.z), pos2 }, -- X/Z
{ vec(pos1.x, areamin.y, areamin.z), pos2 }, -- Y/Z
}
end
end
-- Split an existing area into two non-overlapping [pos1, half1], [half2, pos2] parts; returns half1, half2
area.split = function(pos1, pos2)
local axis
if pos2.x - pos1.x >= 1 then
axis = "x"
elseif pos2.y - pos1.y >= 1 then
axis = "y"
elseif pos2.z - pos1.z >= 1 then
axis = "z"
else
error("Internal failure: area too small to split")
end
local hspan = math.floor((pos2[axis] - pos1[axis] + 1) / 2)
local half1 = vecw(axis, pos1[axis] + hspan - 1, pos2)
local half2 = vecw(axis, pos1[axis] + hspan, pos2)
return half1, half2
end
---------------------
-- Checks
---------------------
local check = {}
-- Check that all nodes in [pos1, pos2] are the node(s) specified
check.filled = function(pos1, pos2, nodes)
if type(nodes) == "string" then
nodes = { nodes }
end
local _, counts = minetest.find_nodes_in_area(pos1, pos2, nodes)
local total = worldedit.volume(pos1, pos2)
local sum = 0
for _, n in pairs(counts) do
sum = sum + n
end
if sum ~= total then
error((total - sum) .. " " .. table.concat(nodes, ",") .. " nodes missing in " ..
pos2str(pos1) .. " -> " .. pos2str(pos2))
end
end
-- Check that none of the nodes in [pos1, pos2] are the node(s) specified
check.not_filled = function(pos1, pos2, nodes)
if type(nodes) == "string" then
nodes = { nodes }
end
local _, counts = minetest.find_nodes_in_area(pos1, pos2, nodes)
for nodename, n in pairs(counts) do
if n ~= 0 then
error(counts[nodename] .. " " .. nodename .. " nodes found in " ..
pos2str(pos1) .. " -> " .. pos2str(pos2))
end
end
end
-- Check that all of the areas are only made of node(s) specified
check.filled2 = function(list, nodes)
for _, pos in ipairs(list) do
check.filled(pos[1], pos[2], nodes)
end
end
-- Check that none of the areas contain the node(s) specified
check.not_filled2 = function(list, nodes)
for _, pos in ipairs(list) do
check.not_filled(pos[1], pos[2], nodes)
end
end
-- Checks presence of a repeating pattern in [pos1, po2] (cf. place_pattern)
check.pattern = function(pos1, pos2, pattern)
local pos = vec()
local i = 1
for z = pos1.z, pos2.z do
pos.z = z
for y = pos1.y, pos2.y do
pos.y = y
for x = pos1.x, pos2.x do
pos.x = x
local node = get_node(pos)
if node.name ~= pattern[i] then
error(pattern[i] .. " not found at " .. pos2str(pos) .. " (i=" .. i .. ")")
end
i = i % #pattern + 1
end
end
end
end
---------------------
-- The actual tests
---------------------
local tests = {}
local function register_test(name, func, opts)
assert(type(name) == "string")
assert(func == nil or type(func) == "function")
if not opts then
opts = {}
else
opts = table.copy(opts)
end
opts.name = name
opts.func = func
table.insert(tests, opts)
end
-- 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
-- The basic structure is: get areas + do operations + check results
-- Helpers:
-- area.get must be used to retrieve areas that can be operated on (these will be cleared before each test)
-- check.filled / check.not_filled can be used to check the result
-- area.margin + check.filled2 is useful to make sure nodes weren't placed too far
-- place_pattern + check.pattern is useful to test ops that operate on existing data
register_test("Internal self-test")
register_test("is area loaded?", function()
local pos1, _ = area.get(1)
assert(get_node(pos1).name == "air")
end, {dry=true})
register_test("area.split", function()
for i = 2, 6 do
local pos1, pos2 = area.get(1, 1, i)
local half1, half2 = area.split(pos1, pos2)
assert(pos1.x == half1.x and pos1.y == half1.y)
assert(half1.x == half2.x and half1.y == half2.y)
assert(half1.z + 1 == half2.z)
if i % 2 == 0 then
assert((half1.z - pos1.z) == (pos2.z - half2.z)) -- divided equally
end
end
end, {dry=true})
register_test("check.filled", function()
local pos1, pos2 = area.get(1, 2, 1)
set_node(pos1, {name=testnode1})
set_node(pos2, {name=testnode2})
check.filled(pos1, pos1, testnode1)
check.filled(pos1, pos2, {testnode1, testnode2})
check.not_filled(pos1, pos1, air)
check.not_filled(pos1, pos2, {air, testnode3})
end)
register_test("pattern", function()
local pos1, pos2 = area.get(3, 2, 1)
local pattern = {testnode1, testnode3}
place_pattern(pos1, pos2, pattern)
assert(get_node(pos1).name == testnode1)
check.pattern(pos1, pos2, pattern)
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)
---------------------
-- Main function
---------------------
worldedit.run_tests = function()
do
local v = minetest.get_version()
print("Running " .. #tests .. " tests for WorldEdit " ..
worldedit.version_string .. " on " .. v.project .. " " .. (v.hash or v.string))
end
init_nodes()
-- emerge area from (0,0,0) ~ (56,56,56) and keep it loaded
-- Note: making this area smaller speeds up tests
local wanted = vec(56, 56, 56)
for x = 0, math.floor(wanted.x/16) do
for y = 0, math.floor(wanted.y/16) do
for z = 0, math.floor(wanted.z/16) do
assert(minetest.forceload_block({x=x*16, y=y*16, z=z*16}, true))
end
end
end
area.assign(vec(0, 0, 0), wanted, function()
local failed = 0
for _, test in ipairs(tests) do
if not test.func then
local s = "---- " .. test.name .. " "
print(s .. string.rep("-", 60 - #s))
else
if not test.dry then
area.clear()
end
local ok, err = pcall(test.func)
print(string.format("%-60s %s", test.name, ok and "pass" or "FAIL"))
if not ok then
print(" " .. err)
failed = failed + 1
end
end
end
print("Done, " .. failed .. " tests failed.")
if failed == 0 then
io.close(io.open(minetest.get_worldpath() .. "/tests_ok", "w"))
end
minetest.request_shutdown()
end)
end
-- for debug purposes
minetest.register_on_joinplayer(function(player)
minetest.set_player_privs(player:get_player_name(),
minetest.string_to_privs("fly,fast,noclip,basic_debug,debug,interact"))
end)
minetest.register_on_punchnode(function(pos, node, puncher)
minetest.chat_send_player(puncher:get_player_name(), pos2str(pos))
end)

View File

@ -1343,6 +1343,23 @@ worldedit.register_command("restore", {
end, end,
}) })
local function detect_misaligned_schematic(name, pos1, pos2)
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
-- Check that allocate/save can position the schematic correctly
-- The expected behaviour is that the (0,0,0) corner of the schematic stays
-- sat pos1, this only works when the minimum position is actually present
-- in the schematic.
local node = minetest.get_node(pos1)
local have_node_at_origin = node.name ~= "air" and node.name ~= "ignore"
if not have_node_at_origin then
worldedit.player_notify(name,
"Warning: The schematic contains excessive free space and WILL be "..
"misaligned when allocated or loaded. To avoid this, shrink your "..
"area to cover exactly the nodes to be saved."
)
end
end
worldedit.register_command("save", { worldedit.register_command("save", {
params = "<file>", params = "<file>",
description = "Save the current WorldEdit region to \"(world folder)/schems/<file>.we\"", description = "Save the current WorldEdit region to \"(world folder)/schems/<file>.we\"",
@ -1361,6 +1378,7 @@ worldedit.register_command("save", {
func = function(name, param) func = function(name, param)
local result, count = worldedit.serialize(worldedit.pos1[name], local result, count = worldedit.serialize(worldedit.pos1[name],
worldedit.pos2[name]) worldedit.pos2[name])
detect_misaligned_schematic(name, worldedit.pos1[name], worldedit.pos2[name])
local path = minetest.get_worldpath() .. "/schems" local path = minetest.get_worldpath() .. "/schems"
-- Create directory if it does not already exist -- Create directory if it does not already exist

View File

@ -216,7 +216,7 @@ elseif minetest.global_exists("sfinv") then -- sfinv installed
end end
end end
else else
error( return minetest.log("error",
"worldedit_gui requires a supported gui management mod to be installed.\n".. "worldedit_gui requires a supported gui management mod to be installed.\n"..
"To use the it you need to either:\n".. "To use the it you need to either:\n"..
"* use minetest_game or another sfinv-compatible subgame\n".. "* use minetest_game or another sfinv-compatible subgame\n"..