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

19 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
2f2f5a7def Use minetest.global_exists for LuaJIT check
closes #199
2021-09-12 19:35:57 +02:00
7f87f1658e Add param2 button to WorldEdit GUI 2021-07-23 23:34:13 +02:00
4378750498 Use minetest.get_objects_in_area when possible 2021-04-30 19:33:27 +02:00
19 changed files with 903 additions and 168 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. 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

@ -31,7 +31,7 @@ function worldedit.luatransform(pos1, pos2, code)
worldedit.keep_loaded(pos1, pos2) 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 while pos.x <= pos2.x do
pos.y = pos1.y pos.y = pos1.y
while pos.y <= pos2.y do while pos.y <= pos2.y do

View File

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

View File

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

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

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

View File

@ -20,7 +20,7 @@ function worldedit.cube(pos, width, height, length, node_name, hollow)
-- Add cube -- Add cube
local node_id = minetest.get_content_id(node_name) 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 offset = vector.subtract(basepos, area.MinEdge)
local count = 0 local count = 0
@ -149,7 +149,7 @@ function worldedit.cylinder(pos, axis, length, radius1, radius2, node_name, holl
end end
-- Handle negative lengths -- 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 if length < 0 then
length = -length length = -length
current_pos[axis] = current_pos[axis] - 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) -- Add desired shape (anything inbetween cylinder & cone)
local node_id = minetest.get_content_id(node_name) 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 = { local offset = {
x = current_pos.x - area.MinEdge.x, x = current_pos.x - area.MinEdge.x,
y = current_pos.y - area.MinEdge.y, y = current_pos.y - area.MinEdge.y,
@ -225,7 +225,7 @@ function worldedit.pyramid(pos, axis, height, node_name, hollow)
-- Add pyramid -- Add pyramid
local node_id = minetest.get_content_id(node_name) 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 = { local offset = {
x = pos.x - area.MinEdge.x, x = pos.x - area.MinEdge.x,
y = pos.y - area.MinEdge.y, y = pos.y - area.MinEdge.y,
@ -271,7 +271,7 @@ function worldedit.spiral(pos, length, height, spacer, node_name)
-- Set up variables -- Set up variables
local node_id = minetest.get_content_id(node_name) 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 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 local i = offset_z * stride.z + offset_y * stride.y + offset_x + 1

View File

@ -66,7 +66,7 @@ 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
local pos = {x=pos1.x, y=0, z=0} local pos = vector.new(pos1.x, 0, 0)
local count = 0 local count = 0
local result = {} local result = {}
while pos.x <= pos2.x do while pos.x <= pos2.x do
@ -114,12 +114,15 @@ function worldedit.serialize(pos1, pos2)
return LATEST_SERIALIZATION_HEADER .. result, count return LATEST_SERIALIZATION_HEADER .. 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, err
if not jit then 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 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
@ -129,19 +132,28 @@ local function deserialize_workaround(content)
local escaped = content:gsub("\\\\", "@@"):gsub("\\\"", "@@"):gsub("(\"[^\"]*\")", function(s) return string.rep("@", #s) end) local escaped = content:gsub("\\\\", "@@"):gsub("\\\"", "@@"):gsub("(\"[^\"]*\")", function(s) return string.rep("@", #s) end)
local startpos, startpos1 = 1, 1 local startpos, startpos1 = 1, 1
local endpos local endpos
local entry
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
local current = content:sub(startpos1, startpos) 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) table.insert(nodes, entry)
startpos, startpos1 = endpos, endpos startpos, startpos1 = endpos, endpos
end end
local entry = minetest.deserialize("return " .. content:sub(startpos1), true) -- process the last entry if not err then
entry = minetest.deserialize("return " .. content:sub(startpos1), true) -- process the last entry
table.insert(nodes, entry) table.insert(nodes, entry)
end end
end
if err then
minetest.log("warning", "WorldEdit: deserialize: " .. err)
end
return nodes return nodes
end end
@ -223,9 +235,7 @@ function worldedit.allocate_with_nodes(origin_pos, nodes)
if y > pos2y then pos2y = y end if y > pos2y then pos2y = y end
if z > pos2z then pos2z = z end if z > pos2z then pos2z = z end
end end
local pos1 = {x=pos1x, y=pos1y, z=pos1z} return vector.new(pos1x, pos1y, pos1z), vector.new(pos2x, pos2y, pos2z), #nodes
local pos2 = {x=pos2x, y=pos2y, z=pos2z}
return pos1, pos2, #nodes
end 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) 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, local get_node, get_meta, swap_node = minetest.get_node,
minetest.get_meta, minetest.swap_node minetest.get_meta, minetest.swap_node
while pos.x <= pos2.x do while pos.x <= pos2.x do
@ -79,7 +79,7 @@ function worldedit.highlight(pos1, pos2, node_name)
worldedit.keep_loaded(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, local get_node, get_meta, swap_node = minetest.get_node,
minetest.get_meta, minetest.swap_node minetest.get_meta, minetest.swap_node
local count = 0 local count = 0

View File

@ -27,7 +27,7 @@ local brush_on_use = function(itemstack, placer)
end end
local raybegin = vector.add(placer:get_pos(), 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 rayend = vector.add(raybegin, vector.multiply(placer:get_look_dir(), BRUSH_MAX_DIST))
local ray = minetest.raycast(raybegin, rayend, false, true) local ray = minetest.raycast(raybegin, rayend, false, true)
local pointed_thing = ray:next() local pointed_thing = ray:next()

View File

@ -226,6 +226,39 @@ local function check_filename(name)
return name:find("^[%w%s%^&'@{}%[%],%$=!%-#%(%)%%%.%+~_]+$") ~= nil return name:find("^[%w%s%^&'@{}%[%],%$=!%-#%(%)%%%.%+~_]+$") ~= nil
end 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", { worldedit.register_command("about", {
privs = {}, privs = {},
@ -455,7 +488,7 @@ worldedit.register_command("fixedpos", {
if found == nil then if found == nil then
return false return false
end 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, end,
func = function(name, flag, pos) func = function(name, flag, pos)
if flag == "set1" then 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)) 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 elseif worldedit.set_pos[name] == "prob" then --setting Minetest schematic node probabilities
worldedit.prob_pos[name] = pos 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 end
end) end)
@ -1014,7 +1047,7 @@ worldedit.register_command("stack2", {
return false, "invalid increments: " .. param return false, "invalid increments: " .. param
end 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, end,
nodes_needed = function(name, repetitions, offset) nodes_needed = function(name, repetitions, offset)
return check_region(name) * repetitions return check_region(name) * repetitions
@ -1186,13 +1219,16 @@ worldedit.register_command("drain", {
-- TODO: make an API function for this -- TODO: make an API function for this
local count = 0 local count = 0
local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name]) 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 x = pos1.x, pos2.x do
for y = pos1.y, pos2.y do for y = pos1.y, pos2.y do
for z = pos1.z, pos2.z 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] local d = minetest.registered_nodes[n]
if d ~= nil and (d["drawtype"] == "liquid" or d["drawtype"] == "flowingliquid") then if d ~= nil and (d.drawtype == "liquid" or d.drawtype == "flowingliquid") then
minetest.remove_node({x=x, y=y, z=z}) remove_node(p)
count = count + 1 count = count + 1
end end
end end
@ -1230,13 +1266,15 @@ local function clearcut(pos1, pos2)
local count = 0 local count = 0
local prev, any local prev, any
local get_node, remove_node = minetest.get_node, minetest.remove_node
for x = pos1.x, pos2.x do for x = pos1.x, pos2.x do
for z = pos1.z, pos2.z do for z = pos1.z, pos2.z do
prev = false prev = false
any = false any = false
-- first pass: remove floating nodes that would be left over -- first pass: remove floating nodes that would be left over
for y = pos1.y, pos2.y do 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 if plants[n] then
prev = true prev = true
any = true any = true
@ -1244,7 +1282,7 @@ local function clearcut(pos1, pos2)
local def = minetest.registered_nodes[n] or {} local def = minetest.registered_nodes[n] or {}
local groups = def.groups or {} local groups = def.groups or {}
if groups.attached_node or (def.buildable_to and groups.falling_node) then 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 count = count + 1
else else
prev = false prev = false
@ -1255,9 +1293,10 @@ local function clearcut(pos1, pos2)
-- second pass: remove plants, top-to-bottom to avoid item drops -- second pass: remove plants, top-to-bottom to avoid item drops
if any then if any then
for y = pos2.y, pos1.y, -1 do 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 if plants[n] then
minetest.remove_node({x=x, y=y, z=z}) remove_node(pos)
count = count + 1 count = count + 1
end end
end end
@ -1415,28 +1454,15 @@ worldedit.register_command("allocate", {
func = function(name, param) func = function(name, param)
local pos = worldedit.pos1[name] local pos = worldedit.pos1[name]
local filename = minetest.get_worldpath() .. "/schems/" .. param .. ".we" local value = open_schematic(name, param)
local file, err = io.open(filename, "rb") if not value then
if err ~= nil then return false
worldedit.player_notify(name, "could not open file \"" .. filename .. "\"")
return
end 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) local nodepos1, nodepos2, count = worldedit.allocate(pos, value)
if not nodepos1 then if not nodepos1 then
worldedit.player_notify(name, "Schematic empty, nothing allocated") worldedit.player_notify(name, "Schematic empty, nothing allocated")
return return false
end end
worldedit.pos1[name] = nodepos1 worldedit.pos1[name] = nodepos1
@ -1464,46 +1490,16 @@ worldedit.register_command("load", {
func = function(name, param) func = function(name, param)
local pos = worldedit.pos1[name] local pos = worldedit.pos1[name]
if param == "" then local value = open_schematic(name, param)
worldedit.player_notify(name, "invalid usage: " .. param) if not value then
return return false
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
end end
local count = worldedit.deserialize(pos, value) 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") worldedit.player_notify(name, count .. " nodes loaded")
end, end,
}) })
@ -1644,11 +1640,18 @@ worldedit.register_command("mtschemprob", {
}) })
minetest.register_on_player_receive_fields(function(player, formname, fields) 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 name = player:get_player_name()
local prob_entry = {pos=worldedit.prob_pos[name], prob=tonumber(fields.text)} local problist = worldedit.prob_list[name]
local index = table.getn(worldedit.prob_list[name]) + 1 if problist == nil then
worldedit.prob_list[name][index] = prob_entry 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
end) end)

View File

@ -11,6 +11,7 @@ local gui_count2 = {} --mapping of player names to a quantity (arbitrary strings
local gui_count3 = {} --mapping of player names to a quantity (arbitrary strings may also appear as values) local gui_count3 = {} --mapping of player names to a quantity (arbitrary strings may also appear as values)
local gui_angle = {} --mapping of player names to an angle (one of 90, 180, 270, representing the angle in degrees clockwise) local gui_angle = {} --mapping of player names to an angle (one of 90, 180, 270, representing the angle in degrees clockwise)
local gui_filename = {} --mapping of player names to file names local gui_filename = {} --mapping of player names to file names
local gui_param2 = {} --mapping of player names to param2 values
--set default values --set default values
setmetatable(gui_nodename1, {__index = function() return "Cobblestone" end}) setmetatable(gui_nodename1, {__index = function() return "Cobblestone" end})
@ -25,6 +26,7 @@ setmetatable(gui_count2, {__index = function() return "6" end})
setmetatable(gui_count3, {__index = function() return "4" end}) setmetatable(gui_count3, {__index = function() return "4" end})
setmetatable(gui_angle, {__index = function() return 90 end}) setmetatable(gui_angle, {__index = function() return 90 end})
setmetatable(gui_filename, {__index = function() return "building" end}) setmetatable(gui_filename, {__index = function() return "building" end})
setmetatable(gui_param2, {__index = function() return "0" end})
local axis_indices = {["X axis"]=1, ["Y axis"]=2, ["Z axis"]=3, ["Look direction"]=4} local axis_indices = {["X axis"]=1, ["Y axis"]=2, ["Z axis"]=3, ["Look direction"]=4}
local axis_values = {"x", "y", "z", "?"} local axis_values = {"x", "y", "z", "?"}
@ -904,3 +906,31 @@ worldedit.register_gui_function("worldedit_gui_clearobjects", {
execute_worldedit_command("clearobjects", name, "") execute_worldedit_command("clearobjects", name, "")
end, end,
}) })
worldedit.register_gui_function("worldedit_gui_param2", {
name = "Set Param2",
privs = we_privs("param2"),
get_formspec = function(name)
local value = gui_param2[name] or "0"
return "size[6.5,3]" .. worldedit.get_formspec_header("worldedit_gui_param2") ..
"textarea[0.5,1;5,2;;;Some values may break the node!]"..
string.format("field[0.5,2.5;2,0.8;worldedit_gui_param2_value;New Param2;%s]", minetest.formspec_escape(value)) ..
"field_close_on_enter[worldedit_gui_param2_value;false]" ..
"button_exit[3.5,2.5;3,0.8;worldedit_gui_param2_submit;Set Param2]"
end,
})
worldedit.register_gui_handler("worldedit_gui_param2", function(name, fields)
local cg = {
worldedit_gui_param2_value = gui_param2,
}
local ret = handle_changes(name, "worldedit_gui_param2", fields, cg)
if fields.worldedit_gui_param2_submit then
copy_changes(name, fields, cg)
worldedit.show_page(name, "worldedit_gui_param2")
execute_worldedit_command("param2", name, gui_param2[name])
return true
end
return ret
end)

View File

@ -42,7 +42,7 @@ Example:
worldedit.register_gui_handler = function(identifier, handler) worldedit.register_gui_handler = function(identifier, handler)
local enabled = true local enabled = true
minetest.register_on_player_receive_fields(function(player, formname, fields) 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 enabled = false
minetest.after(0.2, function() enabled = true end) minetest.after(0.2, function() enabled = true end)
local name = player:get_player_name() local name = player:get_player_name()
@ -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"..