1
0
mirror of https://github.com/Uberi/Minetest-WorldEdit.git synced 2025-07-20 08:30:29 +02:00

16 Commits

Author SHA1 Message Date
79e5e64c44 luacheck: Reduce the number of ignored codes
After the recent changes, luacheck can be configured to be more strict now, for example to check for inconsistent indentation and undefined variables.
2023-06-10 11:38:23 +02:00
375fbf3c68 Remove trailing whitespaces 2023-06-09 19:51:10 +02:00
cc3aab00bc Fix tab after space indentations 2023-06-09 19:51:10 +02:00
eff01bc8e7 Add code linting with luacheck (#221)
With luacheck mistakes in Lua code can be found, e.g. the use of undefined variables, and the code style can be checked.
workflow by @Panquesito7
2023-06-09 19:48:46 +02:00
099d5047bd Fix undefined variable access in worldedit.metaload
`file` in the deprecated `worldedit.metaload` function was undefined, as reported by luacheck.
2023-06-09 19:47:31 +02:00
7f7e928dd9 Switch bare vectors to vector.new() 2023-06-09 14:49:58 +02:00
1a9f66f091 Fix back button in some worldedit_gui pages 2023-06-09 13:59:09 +02:00
7a5d76a9bc Add comprehensive schematic deserialization unit tests 2023-06-09 13:02:37 +02:00
5260f595c6 Log deserialization errors 2023-06-07 11:50:33 +02:00
7a645eba05 Improve loading and error handling for schematics 2023-06-07 11:41:10 +02:00
9417f2bbf1 Harden //mtschemprob against incorrect input
closes #216
2023-03-22 21:26:53 +01:00
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
19 changed files with 908 additions and 437 deletions

15
.github/workflows/check.yml vendored Normal file
View File

@ -0,0 +1,15 @@
on: [push, pull_request]
name: Check
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: apt
run: sudo apt-get install -y luarocks
- name: luacheck install
run: luarocks install --local luacheck
- name: luacheck run
run: $HOME/.luarocks/bin/luacheck ./

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

8
.luacheckrc Normal file
View File

@ -0,0 +1,8 @@
read_globals = {"minetest", "vector", "VoxelArea", "ItemStack",
"table",
"unified_inventory", "sfinv", "smart_inventory", "inventory_plus"
}
globals = {"worldedit"}
-- Ignore these errors until someone decides to fix them
ignore = {"211", "212", "213", "311", "411", "412", "421", "422",
"431", "432", "631"}

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.
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).
Usage

View File

@ -31,7 +31,7 @@ function worldedit.luatransform(pos1, pos2, code)
worldedit.keep_loaded(pos1, pos2)
local pos = {x=pos1.x, y=0, z=0}
local pos = vector.new(pos1.x, 0, 0)
while pos.x <= pos2.x do
pos.y = pos1.y
while pos.y <= pos2.y do

View File

@ -5,8 +5,8 @@
-- `pos1` is less than or equal to the corresponding component of `pos2`.
-- Returns the new positions.
function worldedit.sort_pos(pos1, pos2)
pos1 = {x=pos1.x, y=pos1.y, z=pos1.z}
pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
pos1 = vector.new(pos1.x, pos1.y, pos1.z)
pos2 = vector.new(pos2.x, pos2.y, pos2.z)
if pos1.x > pos2.x then
pos2.x, pos1.x = pos1.x, pos2.x
end

View File

@ -24,11 +24,14 @@ function worldedit.metasave(pos1, pos2, filename)
return count
end
function worldedit.metaload(originpos, filename)
function worldedit.metaload(originpos, file_name)
deprecated("load")
filename = minetest.get_worldpath() .. "/schems/" .. file .. ".wem"
local file, err = io.open(filename, "wb")
if err then return 0 end
local file_path = minetest.get_worldpath() ..
"/schems/" .. file_name .. ".wem"
local file, err = io.open(file_path, "wb")
if err then
return 0
end
local data = file:read("*a")
return worldedit.deserialize(originpos, data)
end

View File

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

View File

@ -128,7 +128,7 @@ function worldedit.stack2(pos1, pos2, direction, amount, finished)
direction = table.copy(direction)
local i = 0
local translated = {x=0, y=0, z=0}
local translated = vector.new()
local function step()
translated.x = translated.x + direction.x
translated.y = translated.y + direction.y
@ -155,7 +155,7 @@ function worldedit.copy(pos1, pos2, axis, amount)
-- Decide if we need to copy stuff backwards (only applies to metadata)
local backwards = amount > 0 and amount < (pos2[axis] - pos1[axis] + 1)
local off = {x=0, y=0, z=0}
local off = vector.new()
off[axis] = amount
return worldedit.copy2(pos1, pos2, off, backwards)
end
@ -170,7 +170,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
local src_manip, src_area = mh.init(pos1, pos2)
local src_stride = {x=1, y=src_area.ystride, z=src_area.zstride}
local src_stride = vector.new(1, src_area.ystride, src_area.zstride)
local src_offset = vector.subtract(pos1, src_area.MinEdge)
local dpos1 = vector.add(pos1, off)
@ -178,7 +178,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
local dim = vector.add(vector.subtract(pos2, pos1), 1)
local dst_manip, dst_area = mh.init(dpos1, dpos2)
local dst_stride = {x=1, y=dst_area.ystride, z=dst_area.zstride}
local dst_stride = vector.new(1, dst_area.ystride, dst_area.zstride)
local dst_offset = vector.subtract(dpos1, dst_area.MinEdge)
local function do_copy(src_data, dst_data)
@ -226,7 +226,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
for z = dim.z-1, 0, -1 do
for y = dim.y-1, 0, -1 do
for x = dim.x-1, 0, -1 do
local pos = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
local pos = vector.new(pos1.x+x, pos1.y+y, pos1.z+z)
local meta = get_meta(pos):to_table()
pos = vector.add(pos, off)
get_meta(pos):from_table(meta)
@ -237,7 +237,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
for z = 0, dim.z-1 do
for y = 0, dim.y-1 do
for x = 0, dim.x-1 do
local pos = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
local pos = vector.new(pos1.x+x, pos1.y+y, pos1.z+z)
local meta = get_meta(pos):to_table()
pos = vector.add(pos, off)
get_meta(pos):from_table(meta)
@ -286,21 +286,21 @@ function worldedit.move(pos1, pos2, axis, amount)
end
-- Copy stuff to new location
local off = {x=0, y=0, z=0}
local off = vector.new()
off[axis] = amount
worldedit.copy2(pos1, pos2, off, backwards)
-- Nuke old area
if not overlap then
nuke_area({x=0, y=0, z=0}, dim)
nuke_area(vector.new(), dim)
else
-- Source and destination region are overlapping, which means we can't
-- blindly delete the [pos1, pos2] area
local leftover = vector.new(dim) -- size of the leftover slice
leftover[axis] = math.abs(amount)
if amount > 0 then
nuke_area({x=0, y=0, z=0}, leftover)
nuke_area(vector.new(), leftover)
else
local top = {x=0, y=0, z=0} -- offset of the leftover slice from pos1
local top = vector.new() -- offset of the leftover slice from pos1
top[axis] = dim[axis] - math.abs(amount)
nuke_area(top, leftover)
end
@ -358,7 +358,7 @@ function worldedit.stretch(pos1, pos2, stretch_x, stretch_y, stretch_z)
for i = 1, stretch_x * stretch_y * stretch_z do
nodes[i] = placeholder_node
end
local schematic = {size={x=stretch_x, y=stretch_y, z=stretch_z}, data=nodes}
local 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
@ -369,8 +369,8 @@ function worldedit.stretch(pos1, pos2, stretch_x, stretch_y, stretch_z)
}
worldedit.keep_loaded(pos1, new_pos2)
local pos = {x=pos2.x, y=0, z=0}
local big_pos = {x=0, y=0, z=0}
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
@ -436,16 +436,16 @@ function worldedit.transpose(pos1, pos2, axis1, axis2)
end
-- Calculate the new position 2 after transposition
local new_pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
local new_pos2 = vector.new(pos2)
new_pos2[axis1] = pos1[axis1] + extent2
new_pos2[axis2] = pos1[axis2] + extent1
local upper_bound = {x=pos2.x, y=pos2.y, z=pos2.z}
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 = {x=pos1.x, y=0, z=0}
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
@ -485,7 +485,7 @@ function worldedit.flip(pos1, pos2, axis)
worldedit.keep_loaded(pos1, pos2)
--- TODO: Flip the region slice by slice along the flip axis using schematic method.
local pos = {x=pos1.x, y=0, z=0}
local 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,
@ -584,7 +584,7 @@ function worldedit.orient(pos1, pos2, angle)
local count = 0
local get_node, swap_node = minetest.get_node, minetest.swap_node
local pos = {x=pos1.x, y=0, z=0}
local pos = vector.new(pos1.x, 0, 0)
while pos.x <= pos2.x do
pos.y = pos1.y
while pos.y <= pos2.y do
@ -650,13 +650,12 @@ function worldedit.clear_objects(pos1, pos2)
end
-- Offset positions to include full nodes (positions are in the center of nodes)
local pos1x, pos1y, pos1z = pos1.x - 0.5, pos1.y - 0.5, pos1.z - 0.5
local pos2x, pos2y, pos2z = pos2.x + 0.5, pos2.y + 0.5, pos2.z + 0.5
pos1 = vector.add(pos1, -0.5)
pos2 = vector.add(pos1, 0.5)
local count = 0
if minetest.get_objects_in_area then
local objects = minetest.get_objects_in_area({x=pos1x, y=pos1y, z=pos1z},
{x=pos2x, y=pos2y, z=pos2z})
local objects = minetest.get_objects_in_area(pos1, pos2)
for _, obj in pairs(objects) do
if should_delete(obj) then
@ -670,21 +669,21 @@ function worldedit.clear_objects(pos1, pos2)
-- Fallback implementation via get_objects_inside_radius
-- Center of region
local center = {
x = pos1x + ((pos2x - pos1x) / 2),
y = pos1y + ((pos2y - pos1y) / 2),
z = pos1z + ((pos2z - pos1z) / 2)
x = pos1.x + ((pos2.x - pos1.x) / 2),
y = pos1.y + ((pos2.y - pos1.y) / 2),
z = pos1.z + ((pos2.z - pos1.z) / 2)
}
-- Bounding sphere radius
local radius = math.sqrt(
(center.x - pos1x) ^ 2 +
(center.y - pos1y) ^ 2 +
(center.z - pos1z) ^ 2)
(center.x - pos1.x) ^ 2 +
(center.y - pos1.y) ^ 2 +
(center.z - pos1.z) ^ 2)
for _, obj in pairs(minetest.get_objects_inside_radius(center, radius)) do
if should_delete(obj) then
local pos = obj:get_pos()
if pos.x >= pos1x and pos.x <= pos2x and
pos.y >= pos1y and pos.y <= pos2y and
pos.z >= pos1z and pos.z <= pos2z then
if pos.x >= pos1.x and pos.x <= pos2.x and
pos.y >= pos1.y and pos.y <= pos2.y and
pos.z >= pos1.z and pos.z <= pos2.z then
-- Inside region
obj:remove()
count = count + 1

View File

@ -20,7 +20,7 @@ function worldedit.cube(pos, width, height, length, node_name, hollow)
-- Add cube
local node_id = minetest.get_content_id(node_name)
local stride = {x=1, y=area.ystride, z=area.zstride}
local stride = vector.new(1, area.ystride, area.zstride)
local offset = vector.subtract(basepos, area.MinEdge)
local count = 0
@ -149,7 +149,7 @@ function worldedit.cylinder(pos, axis, length, radius1, radius2, node_name, holl
end
-- Handle negative lengths
local current_pos = {x=pos.x, y=pos.y, z=pos.z}
local current_pos = vector.new(pos)
if length < 0 then
length = -length
current_pos[axis] = current_pos[axis] - length
@ -162,7 +162,7 @@ function worldedit.cylinder(pos, axis, length, radius1, radius2, node_name, holl
-- Add desired shape (anything inbetween cylinder & cone)
local node_id = minetest.get_content_id(node_name)
local stride = {x=1, y=area.ystride, z=area.zstride}
local stride = vector.new(1, area.ystride, area.zstride)
local offset = {
x = current_pos.x - area.MinEdge.x,
y = current_pos.y - area.MinEdge.y,
@ -225,7 +225,7 @@ function worldedit.pyramid(pos, axis, height, node_name, hollow)
-- Add pyramid
local node_id = minetest.get_content_id(node_name)
local stride = {x=1, y=area.ystride, z=area.zstride}
local stride = vector.new(1, area.ystride, area.zstride)
local offset = {
x = pos.x - area.MinEdge.x,
y = pos.y - area.MinEdge.y,
@ -242,8 +242,8 @@ function worldedit.pyramid(pos, axis, height, node_name, hollow)
for index3 = -size, size do
local i = new_index2 + (index3 + offset[other2]) * stride[other2]
if (not hollow or size - math.abs(index2) < 2 or size - math.abs(index3) < 2) then
data[i] = node_id
count = count + 1
data[i] = node_id
count = count + 1
end
end
end
@ -271,7 +271,7 @@ function worldedit.spiral(pos, length, height, spacer, node_name)
-- Set up variables
local node_id = minetest.get_content_id(node_name)
local stride = {x=1, y=area.ystride, z=area.zstride}
local stride = vector.new(1, area.ystride, area.zstride)
local offset_x, offset_y, offset_z = pos.x - area.MinEdge.x, pos.y - area.MinEdge.y, pos.z - area.MinEdge.z
local i = offset_z * stride.z + offset_y * stride.y + offset_x + 1

View File

@ -1,7 +1,9 @@
--- Schematic serialization and deserialiation.
-- @module worldedit.serialization
worldedit.LATEST_SERIALIZATION_VERSION = 6
worldedit.LATEST_SERIALIZATION_VERSION = 5
local LATEST_SERIALIZATION_HEADER = worldedit.LATEST_SERIALIZATION_VERSION .. ":"
--[[
Serialization version history:
@ -13,7 +15,6 @@ Serialization version history:
`name`, `param1`, `param2`, and `meta` fields.
5: Added header and made `param1`, `param2`, and `meta` fields optional.
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
end
-- Decide axis of saved rows
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 pos = vector.new(pos1.x, 0, 0)
local count = 0
local result = {}
local cur
local cache_data, cache_param1, cache_param2
local prev_data, prev_param1, prev_param2
pos[other1] = pos1[other1]
while pos[other1] <= pos2[other1] do
pos[other2] = pos1[other2]
while pos[other2] <= pos2[other2] do
pos[axis] = pos1[axis]
while pos[axis] <= pos2[axis] do
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)
if node.name ~= "air" and node.name ~= "ignore" then
count = count + 1
local meta
if has_meta[hash_node_position(pos)] then
@ -180,76 +93,36 @@ function worldedit.serialize(pos1, pos2)
end
end
if cur == nil then -- Start a new row
cur = cur_new(pos, pos1, axis, other1, other2)
cache_data = match_init(cur.data, node.name)
cache_param1 = match_init(cur.param1, node.param1)
cache_param2 = match_init(cur.param2, node.param2)
prev_data = cur.data[1]
prev_param1 = cur.param1[1]
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
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
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
cur.meta[next_c] = meta
end
count = count + 1
else
if cur ~= nil then -- Finish row
cur_finish(result, cur)
cur = nil
end
result[count] = {
x = pos.x - pos1.x,
y = pos.y - pos1.y,
z = pos.z - pos1.z,
name = node.name,
param1 = node.param1 ~= 0 and node.param1 or nil,
param2 = node.param2 ~= 0 and node.param2 or nil,
meta = meta,
}
end
pos[axis] = pos[axis] + 1
pos.z = pos.z + 1
end
if cur ~= nil then -- Finish leftover row
cur_finish(result, cur)
cur = nil
end
pos[other2] = pos[other2] + 1
pos.y = pos.y + 1
end
pos[other1] = pos[other1] + 1
pos.x = pos.x + 1
end
-- Serialize entries
result = minetest.serialize(result)
return tonumber(worldedit.LATEST_SERIALIZATION_VERSION) .. "," ..
string.format("%d,%d,%d:", dim.x, dim.y, dim.z) .. result, count
return LATEST_SERIALIZATION_HEADER .. result, count
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 nodes
local nodes, err
if not minetest.global_exists("jit") then
nodes = minetest.deserialize(content, true)
nodes, err = 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, err = minetest.deserialize(content, true)
else
-- XXX: This is a filthy hack that works surprisingly well
-- in LuaJIT, `minetest.deserialize` will fail due to the register limit
@ -259,25 +132,35 @@ local function deserialize_workaround(content)
local escaped = content:gsub("\\\\", "@@"):gsub("\\\"", "@@"):gsub("(\"[^\"]*\")", function(s) return string.rep("@", #s) end)
local startpos, startpos1 = 1, 1
local endpos
local entry
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
break
end
local current = content:sub(startpos1, startpos)
local entry = minetest.deserialize("return " .. current, true)
entry, err = minetest.deserialize("return " .. current, true)
if not entry then
break
end
table.insert(nodes, entry)
startpos, startpos1 = endpos, endpos
end
local entry = minetest.deserialize("return " .. content:sub(startpos1), true) -- process the last entry
table.insert(nodes, entry)
if not err then
entry = minetest.deserialize("return " .. content:sub(startpos1), true) -- process the last entry
table.insert(nodes, entry)
end
end
if err then
minetest.log("warning", "WorldEdit: deserialize: " .. err)
end
return nodes
end
--- 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.
local function legacy_load_schematic(version, header, content)
local function load_schematic(value)
local version, header, content = worldedit.read_header(value)
local nodes = {}
if version == 1 or version == 2 then -- Original flat table format
local tables = minetest.deserialize(content, true)
@ -319,8 +202,6 @@ local function legacy_load_schematic(version, header, content)
end
elseif version == 4 or version == 5 then -- Nested table format
nodes = deserialize_workaround(content)
elseif version >= 6 then
error("legacy_load_schematic called for non-legacy schematic")
else
return nil
end
@ -333,29 +214,14 @@ end
-- @return High corner position.
-- @return The number of nodes.
function worldedit.allocate(origin_pos, value)
local version, header, content = worldedit.read_header(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
return worldedit.legacy_allocate_with_nodes(origin_pos, nodes)
end
local nodes = load_schematic(value)
if not nodes or #nodes == 0 then return nil end
return worldedit.allocate_with_nodes(origin_pos, nodes)
end
-- Internal
function worldedit.legacy_allocate_with_nodes(origin_pos, nodes)
function worldedit.allocate_with_nodes(origin_pos, nodes)
local huge = math.huge
local pos1x, pos1y, pos1z = huge, huge, huge
local pos2x, pos2y, pos2z = -huge, -huge, -huge
@ -369,119 +235,31 @@ function worldedit.legacy_allocate_with_nodes(origin_pos, nodes)
if y > pos2y then pos2y = y end
if z > pos2z then pos2z = z end
end
local pos1 = {x=pos1x, y=pos1y, z=pos1z}
local pos2 = {x=pos2x, y=pos2y, z=pos2z}
return pos1, pos2, #nodes
return vector.new(pos1x, pos1y, pos1z), vector.new(pos2x, pos2y, pos2z), #nodes
end
--- Loads the nodes represented by string `value` at position `origin_pos`.
-- @return The number of nodes deserialized.
function worldedit.deserialize(origin_pos, value)
local version, header, content = worldedit.read_header(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,
}
worldedit.keep_loaded(origin_pos, pos2)
local nodes = load_schematic(value)
if not nodes then return nil end
if #nodes == 0 then return #nodes end
return worldedit.deserialize_with_content(origin_pos, content)
else
local nodes = legacy_load_schematic(version, header, content)
if not nodes or #nodes == 0 then return nil end
local pos1, pos2 = worldedit.allocate_with_nodes(origin_pos, nodes)
worldedit.keep_loaded(pos1, pos2)
local pos1, pos2 = worldedit.legacy_allocate_with_nodes(origin_pos, nodes)
worldedit.keep_loaded(pos1, pos2)
local origin_x, origin_y, origin_z = origin_pos.x, origin_pos.y, origin_pos.z
local count = 0
local add_node, get_meta = minetest.add_node, minetest.get_meta
for i, entry in ipairs(nodes) do
entry.x, entry.y, entry.z = origin_x + entry.x, origin_y + entry.y, origin_z + entry.z
-- Entry acts as both position and node
add_node(entry, entry)
if entry.meta then
get_meta(entry):from_table(entry.meta)
end
end
return #nodes
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 origin_x, origin_y, origin_z = origin_pos.x, origin_pos.y, origin_pos.z
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
for i, entry in ipairs(nodes) do
entry.x, entry.y, entry.z = origin_x + entry.x, origin_y + entry.y, origin_z + entry.z
-- Entry acts as both position and node
add_node(entry, entry)
if entry.meta then
get_meta(entry):from_table(entry.meta)
end
count = count + row.c
end
return count
return #nodes
end

602
worldedit/test.lua Normal file
View File

@ -0,0 +1,602 @@
---------------------
-- 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 and sizez == nil then
size = vector.new(sizex, sizex, sizex)
else
size = vector.new(sizex, sizey, 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)
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
---------------------
-- 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(vector.new(x*16, y*16, 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

@ -19,7 +19,7 @@ function worldedit.hide(pos1, pos2)
worldedit.keep_loaded(pos1, pos2)
local pos = {x=pos1.x, y=0, z=0}
local pos = vector.new(pos1.x, 0, 0)
local get_node, get_meta, swap_node = minetest.get_node,
minetest.get_meta, minetest.swap_node
while pos.x <= pos2.x do
@ -79,7 +79,7 @@ function worldedit.highlight(pos1, pos2, node_name)
worldedit.keep_loaded(pos1, pos2)
local pos = {x=pos1.x, y=0, z=0}
local pos = vector.new(pos1.x, 0, 0)
local get_node, get_meta, swap_node = minetest.get_node,
minetest.get_meta, minetest.swap_node
local count = 0

View File

@ -27,7 +27,7 @@ local brush_on_use = function(itemstack, placer)
end
local raybegin = vector.add(placer:get_pos(),
{x=0, y=placer:get_properties().eye_height, z=0})
vector.new(0, placer:get_properties().eye_height, 0))
local rayend = vector.add(raybegin, vector.multiply(placer:get_look_dir(), BRUSH_MAX_DIST))
local ray = minetest.raycast(raybegin, rayend, false, true)
local pointed_thing = ray:next()

View File

@ -226,6 +226,39 @@ local function check_filename(name)
return name:find("^[%w%s%^&'@{}%[%],%$=!%-#%(%)%%%.%+~_]+$") ~= nil
end
local function open_schematic(name, param)
-- find the file in the world path
local testpaths = {
minetest.get_worldpath() .. "/schems/" .. param,
minetest.get_worldpath() .. "/schems/" .. param .. ".we",
minetest.get_worldpath() .. "/schems/" .. param .. ".wem",
}
local file, err
for index, path in ipairs(testpaths) do
file, err = io.open(path, "rb")
if not err then
break
end
end
if err then
worldedit.player_notify(name, "Could not open file \"" .. param .. "\"")
return
end
local value = file:read("*a")
file:close()
local version = worldedit.read_header(value)
if version == nil or version == 0 then
worldedit.player_notify(name, "File is invalid!")
return
elseif version > worldedit.LATEST_SERIALIZATION_VERSION then
worldedit.player_notify(name, "Schematic was created with a newer version of WorldEdit.")
return
end
return value
end
worldedit.register_command("about", {
privs = {},
@ -455,7 +488,7 @@ worldedit.register_command("fixedpos", {
if found == nil then
return false
end
return true, flag, {x=tonumber(x), y=tonumber(y), z=tonumber(z)}
return true, flag, vector.new(tonumber(x), tonumber(y), tonumber(z))
end,
func = function(name, flag, pos)
if flag == "set1" then
@ -490,7 +523,7 @@ minetest.register_on_punchnode(function(pos, node, puncher)
worldedit.player_notify(name, "position 2 set to " .. minetest.pos_to_string(pos))
elseif worldedit.set_pos[name] == "prob" then --setting Minetest schematic node probabilities
worldedit.prob_pos[name] = pos
minetest.show_formspec(puncher:get_player_name(), "prob_val_enter", "field[text;;]")
minetest.show_formspec(name, "prob_val_enter", "field[text;;]")
end
end
end)
@ -1014,7 +1047,7 @@ worldedit.register_command("stack2", {
return false, "invalid increments: " .. param
end
return true, tonumber(repetitions), {x=tonumber(x), y=tonumber(y), z=tonumber(z)}
return true, tonumber(repetitions), vector.new(tonumber(x), tonumber(y), tonumber(z))
end,
nodes_needed = function(name, repetitions, offset)
return check_region(name) * repetitions
@ -1186,13 +1219,16 @@ worldedit.register_command("drain", {
-- TODO: make an API function for this
local count = 0
local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name])
local get_node, remove_node = minetest.get_node, minetest.remove_node
for x = pos1.x, pos2.x do
for y = pos1.y, pos2.y do
for z = pos1.z, pos2.z do
local n = minetest.get_node({x=x, y=y, z=z}).name
local p = vector.new(x, y, z)
local n = get_node(p).name
local d = minetest.registered_nodes[n]
if d ~= nil and (d["drawtype"] == "liquid" or d["drawtype"] == "flowingliquid") then
minetest.remove_node({x=x, y=y, z=z})
if d ~= nil and (d.drawtype == "liquid" or d.drawtype == "flowingliquid") then
remove_node(p)
count = count + 1
end
end
@ -1230,13 +1266,15 @@ local function clearcut(pos1, pos2)
local count = 0
local prev, any
local get_node, remove_node = minetest.get_node, minetest.remove_node
for x = pos1.x, pos2.x do
for z = pos1.z, pos2.z do
prev = false
any = false
-- first pass: remove floating nodes that would be left over
for y = pos1.y, pos2.y do
local n = minetest.get_node({x=x, y=y, z=z}).name
local pos = vector.new(x, y, z)
local n = get_node(pos).name
if plants[n] then
prev = true
any = true
@ -1244,7 +1282,7 @@ local function clearcut(pos1, pos2)
local def = minetest.registered_nodes[n] or {}
local groups = def.groups or {}
if groups.attached_node or (def.buildable_to and groups.falling_node) then
minetest.remove_node({x=x, y=y, z=z})
remove_node(pos)
count = count + 1
else
prev = false
@ -1255,9 +1293,10 @@ local function clearcut(pos1, pos2)
-- second pass: remove plants, top-to-bottom to avoid item drops
if any then
for y = pos2.y, pos1.y, -1 do
local n = minetest.get_node({x=x, y=y, z=z}).name
local pos = vector.new(x, y, z)
local n = get_node(pos).name
if plants[n] then
minetest.remove_node({x=x, y=y, z=z})
remove_node(pos)
count = count + 1
end
end
@ -1343,6 +1382,23 @@ worldedit.register_command("restore", {
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", {
params = "<file>",
description = "Save the current WorldEdit region to \"(world folder)/schems/<file>.we\"",
@ -1361,6 +1417,7 @@ worldedit.register_command("save", {
func = function(name, param)
local result, count = worldedit.serialize(worldedit.pos1[name],
worldedit.pos2[name])
detect_misaligned_schematic(name, worldedit.pos1[name], worldedit.pos2[name])
local path = minetest.get_worldpath() .. "/schems"
-- Create directory if it does not already exist
@ -1397,28 +1454,15 @@ worldedit.register_command("allocate", {
func = function(name, param)
local pos = worldedit.pos1[name]
local filename = minetest.get_worldpath() .. "/schems/" .. param .. ".we"
local file, err = io.open(filename, "rb")
if err ~= nil then
worldedit.player_notify(name, "could not open file \"" .. filename .. "\"")
return
local value = open_schematic(name, param)
if not value then
return false
end
local value = file:read("*a")
file:close()
local version = worldedit.read_header(value)
if version == nil or version == 0 then
worldedit.player_notify(name, "File is invalid!")
return
elseif version > worldedit.LATEST_SERIALIZATION_VERSION then
worldedit.player_notify(name, "File was created with newer version of WorldEdit!")
return
end
local nodepos1, nodepos2, count = worldedit.allocate(pos, value)
if not nodepos1 then
worldedit.player_notify(name, "Schematic empty, nothing allocated")
return
return false
end
worldedit.pos1[name] = nodepos1
@ -1446,46 +1490,16 @@ worldedit.register_command("load", {
func = function(name, param)
local pos = worldedit.pos1[name]
if param == "" then
worldedit.player_notify(name, "invalid usage: " .. param)
return
end
if not string.find(param, "^[%w \t.,+-_=!@#$%%^&*()%[%]{};'\"]+$") then
worldedit.player_notify(name, "invalid file name: " .. param)
return
end
--find the file in the world path
local testpaths = {
minetest.get_worldpath() .. "/schems/" .. param,
minetest.get_worldpath() .. "/schems/" .. param .. ".we",
minetest.get_worldpath() .. "/schems/" .. param .. ".wem",
}
local file, err
for index, path in ipairs(testpaths) do
file, err = io.open(path, "rb")
if not err then
break
end
end
if err then
worldedit.player_notify(name, "could not open file \"" .. param .. "\"")
return
end
local value = file:read("*a")
file:close()
local version = worldedit.read_header(value)
if version == nil or version == 0 then
worldedit.player_notify(name, "File is invalid!")
return
elseif version > worldedit.LATEST_SERIALIZATION_VERSION then
worldedit.player_notify(name, "File was created with newer version of WorldEdit!")
return
local value = open_schematic(name, param)
if not value then
return false
end
local count = worldedit.deserialize(pos, value)
if count == nil then
worldedit.player_notify(name, "Loading failed!")
return false
end
worldedit.player_notify(name, count .. " nodes loaded")
end,
})
@ -1626,11 +1640,18 @@ worldedit.register_command("mtschemprob", {
})
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname == "prob_val_enter" and not (fields.text == "" or fields.text == nil) then
if formname == "prob_val_enter" then
local name = player:get_player_name()
local prob_entry = {pos=worldedit.prob_pos[name], prob=tonumber(fields.text)}
local index = table.getn(worldedit.prob_list[name]) + 1
worldedit.prob_list[name][index] = prob_entry
local problist = worldedit.prob_list[name]
if problist == nil then
return
end
local e = {pos=worldedit.prob_pos[name], prob=tonumber(fields.text)}
if e.pos == nil or e.prob == nil or e.prob < 0 or e.prob > 256 then
worldedit.player_notify(name, "invalid node probability given, not saved")
return
end
problist[#problist+1] = e
end
end)

View File

@ -140,7 +140,7 @@ local function execute_worldedit_command(command_name, player_name, params)
assert(chatcmd, "unknown command: " .. command_name)
local _, msg = chatcmd.func(player_name, params)
if msg then
worldedit.player_notify(player_name, msg)
worldedit.player_notify(player_name, msg)
end
end

View File

@ -4,10 +4,10 @@ worldedit = worldedit or {}
Example:
worldedit.register_gui_function("worldedit_gui_hollow_cylinder", {
name = "Make Hollow Cylinder",
privs = {worldedit=true},
get_formspec = function(name) return "some formspec here" end,
on_select = function(name) print(name .. " clicked the button!") end,
name = "Make Hollow Cylinder",
privs = {worldedit=true},
get_formspec = function(name) return "some formspec here" end,
on_select = function(name) print(name .. " clicked the button!") end,
})
Use `nil` for the `options` parameter to unregister the function associated with the given identifier.
@ -35,14 +35,14 @@ end
Example:
worldedit.register_gui_handler("worldedit_gui_hollow_cylinder", function(name, fields)
print(minetest.serialize(fields))
print(minetest.serialize(fields))
end)
]]
worldedit.register_gui_handler = function(identifier, handler)
local enabled = true
minetest.register_on_player_receive_fields(function(player, formname, fields)
if not enabled then return false end
if not enabled or formname ~= "" or fields.worldedit_gui then return false end
enabled = false
minetest.after(0.2, function() enabled = true end)
local name = player:get_player_name()
@ -216,7 +216,7 @@ elseif minetest.global_exists("sfinv") then -- sfinv installed
end
end
else
error(
return minetest.log("error",
"worldedit_gui requires a supported gui management mod to be installed.\n"..
"To use the it you need to either:\n"..
"* use minetest_game or another sfinv-compatible subgame\n"..